PyHelios 0.1.11
Loading...
Searching...
No Matches
RadiationModel.py
Go to the documentation of this file.
1"""
2High-level RadiationModel interface for PyHelios.
3
4This module provides a user-friendly interface to the radiation modeling
5capabilities with graceful plugin handling and informative error messages.
6"""
7
8import logging
9from typing import List, Optional
10from contextlib import contextmanager
11from pathlib import Path
12import os
13
14from .plugins.registry import get_plugin_registry, require_plugin, graceful_plugin_fallback
15from .wrappers import URadiationModelWrapper as radiation_wrapper
16from .validation.plugins import (
17 validate_wavelength_range, validate_flux_value, validate_ray_count,
18 validate_direction_vector, validate_band_label, validate_source_id, validate_source_id_list
19)
20from .validation.plugin_decorators import (
21 validate_radiation_band_params, validate_collimated_source_params, validate_sphere_source_params,
22 validate_sun_sphere_params, validate_get_source_flux_params,
23 validate_update_geometry_params, validate_run_band_params, validate_scattering_depth_params,
24 validate_min_scatter_energy_params
25)
26from .Context import Context
27from .assets import get_asset_manager
28
29logger = logging.getLogger(__name__)
30
31
32@contextmanager
34 """
35 Context manager that temporarily changes working directory to where RadiationModel assets are located.
36
37 RadiationModel C++ code uses hardcoded relative paths like "plugins/radiation/cuda_compile_ptx_generated_rayGeneration.cu.ptx"
38 expecting assets relative to working directory. This manager temporarily changes to the build directory
39 where assets are actually located.
40
41 Raises:
42 RuntimeError: If build directory or RadiationModel assets are not found, indicating a build system error.
43 """
44 # Find the build directory containing RadiationModel assets
45 # Try asset manager first (works for both development and wheel installations)
46 asset_manager = get_asset_manager()
47 working_dir = asset_manager._get_helios_build_path()
48
49 if working_dir and working_dir.exists():
50 radiation_assets = working_dir / 'plugins' / 'radiation'
51 else:
52 # For wheel installations, check packaged assets
53 current_dir = Path(__file__).parent
54 packaged_build = current_dir / 'assets' / 'build'
55
56 if packaged_build.exists():
57 working_dir = packaged_build
58 radiation_assets = working_dir / 'plugins' / 'radiation'
59 else:
60 # Fallback to development paths
61 repo_root = current_dir.parent
62 build_lib_dir = repo_root / 'pyhelios_build' / 'build' / 'lib'
63 working_dir = build_lib_dir.parent
64 radiation_assets = working_dir / 'plugins' / 'radiation'
65
66 if not build_lib_dir.exists():
67 raise RuntimeError(
68 f"PyHelios build directory not found at {build_lib_dir}. "
69 f"Run: python build_scripts/build_helios.py --plugins radiation"
70 )
71
72 if not radiation_assets.exists():
73 raise RuntimeError(
74 f"RadiationModel assets not found at {radiation_assets}. "
75 f"This indicates a build system error. The build script should copy PTX files to this location."
76 )
77
78 # Change to the build directory temporarily
79 original_dir = os.getcwd()
80 try:
81 os.chdir(working_dir)
82 logger.debug(f"Changed working directory to {working_dir} for RadiationModel asset access")
83 yield working_dir
84 finally:
85 os.chdir(original_dir)
86 logger.debug(f"Restored working directory to {original_dir}")
87
88
89class RadiationModelError(Exception):
90 """Raised when RadiationModel operations fail."""
91 pass
93
95 """
96 Camera properties for radiation model cameras.
97
98 This class encapsulates the properties needed to configure a radiation camera,
99 providing sensible defaults and validation for camera parameters. Updated for
100 Helios v1.3.58 with extended camera metadata fields.
101 """
102
103 def __init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05,
104 HFOV=20.0, FOV_aspect_ratio=0.0, lens_focal_length=0.05,
105 sensor_width_mm=35.0, model="generic", lens_make="", lens_model="",
106 lens_specification="", exposure="auto", shutter_speed=1.0/125.0,
107 white_balance="auto"):
108 """
109 Initialize camera properties with defaults matching C++ CameraProperties.
110
111 Args:
112 camera_resolution: Camera resolution as (width, height) tuple or list. Default: (512, 512)
113 focal_plane_distance: Distance from viewing plane to focal plane (working distance). Default: 1.0
114 lens_diameter: Diameter of camera lens (0 = pinhole camera). Default: 0.05
115 HFOV: Horizontal field of view in degrees. Default: 20.0
116 FOV_aspect_ratio: Ratio of horizontal to vertical FOV. Default: 0.0 (auto-calculate from resolution)
117 lens_focal_length: Camera lens optical focal length in meters (physical, not 35mm equiv). Default: 0.05 (50mm)
118 sensor_width_mm: Physical sensor width in mm. Default: 35.0 (full-frame)
119 model: Camera model name (e.g., "Nikon D700", "Canon EOS 5D"). Default: "generic"
120 lens_make: Lens manufacturer (e.g., "Canon", "Nikon"). Default: ""
121 lens_model: Lens model name (e.g., "AF-S NIKKOR 50mm f/1.8G"). Default: ""
122 lens_specification: Lens specification (e.g., "50mm f/1.8"). Default: ""
123 exposure: Exposure mode - "auto", "ISOXXX" (e.g., "ISO100"), or "manual". Default: "auto"
124 shutter_speed: Camera shutter speed in seconds (e.g., 0.008 for 1/125s). Default: 0.008 (1/125s)
125 white_balance: White balance mode - "auto" or "off". Default: "auto"
126 """
127 # Set camera resolution with validation
128
129 if camera_resolution is None:
130 self.camera_resolution = (512, 512)
131 else:
132 if isinstance(camera_resolution, (list, tuple)) and len(camera_resolution) == 2:
133 self.camera_resolution = (int(camera_resolution[0]), int(camera_resolution[1]))
134 else:
135 raise ValueError("camera_resolution must be a tuple or list of 2 integers")
136
137
138 # Validate and set numeric properties
139 if focal_plane_distance <= 0:
140 raise ValueError("focal_plane_distance must be greater than 0")
141 if lens_diameter < 0:
142 raise ValueError("lens_diameter must be non-negative")
143 if HFOV <= 0 or HFOV > 180:
144 raise ValueError("HFOV must be between 0 and 180 degrees")
145 if FOV_aspect_ratio < 0:
146 raise ValueError("FOV_aspect_ratio must be non-negative (0 = auto-calculate)")
147 if lens_focal_length <= 0:
148 raise ValueError("lens_focal_length must be greater than 0")
149 if sensor_width_mm <= 0:
150 raise ValueError("sensor_width_mm must be greater than 0")
151 if shutter_speed <= 0:
152 raise ValueError("shutter_speed must be greater than 0")
153
154 self.focal_plane_distance = float(focal_plane_distance)
155 self.lens_diameter = float(lens_diameter)
156 self.HFOV = float(HFOV)
157 self.FOV_aspect_ratio = float(FOV_aspect_ratio)
158 self.lens_focal_length = float(lens_focal_length)
159 self.sensor_width_mm = float(sensor_width_mm)
160 self.shutter_speed = float(shutter_speed)
162 # Validate and set string properties
163 if not isinstance(model, str):
164 raise ValueError("model must be a string")
165 if not isinstance(lens_make, str):
166 raise ValueError("lens_make must be a string")
167 if not isinstance(lens_model, str):
168 raise ValueError("lens_model must be a string")
169 if not isinstance(lens_specification, str):
170 raise ValueError("lens_specification must be a string")
171 if not isinstance(exposure, str):
172 raise ValueError("exposure must be a string")
173 if not isinstance(white_balance, str):
174 raise ValueError("white_balance must be a string")
175
176 # Validate exposure mode
177 if exposure not in ["auto", "manual"] and not exposure.startswith("ISO"):
178 raise ValueError("exposure must be 'auto', 'manual', or 'ISOXXX' (e.g., 'ISO100')")
179
180 # Validate white balance mode
181 if white_balance not in ["auto", "off"]:
182 raise ValueError("white_balance must be 'auto' or 'off'")
183
184 self.model = str(model)
185 self.lens_make = str(lens_make)
186 self.lens_model = str(lens_model)
187 self.lens_specification = str(lens_specification)
188 self.exposure = str(exposure)
189 self.white_balance = str(white_balance)
191 def to_array(self):
192 """
193 Convert to array format expected by C++ interface.
194
195 Note: Returns numeric fields only. String fields (model, lens_make, etc.) are
196 currently initialized with defaults in the C++ wrapper and cannot be set via
197 this interface. Use the upcoming camera library methods for full metadata control.
198
199 Returns:
200 List of 9 float values: [resolution_x, resolution_y, focal_distance, lens_diameter,
201 HFOV, FOV_aspect_ratio, lens_focal_length, sensor_width_mm,
202 shutter_speed]
203 """
204 return [
205 float(self.camera_resolution[0]), # resolution_x
206 float(self.camera_resolution[1]), # resolution_y
208 self.lens_diameter,
209 self.HFOV,
210 self.FOV_aspect_ratio,
212 self.sensor_width_mm,
213 self.shutter_speed
214 ]
215
216 def __repr__(self):
217 return (f"CameraProperties("
218 f"camera_resolution={self.camera_resolution}, "
219 f"focal_plane_distance={self.focal_plane_distance}, "
220 f"lens_diameter={self.lens_diameter}, "
221 f"HFOV={self.HFOV}, "
222 f"FOV_aspect_ratio={self.FOV_aspect_ratio}, "
223 f"lens_focal_length={self.lens_focal_length}, "
224 f"sensor_width_mm={self.sensor_width_mm}, "
225 f"model='{self.model}', "
226 f"lens_make='{self.lens_make}', "
227 f"lens_model='{self.lens_model}', "
228 f"lens_specification='{self.lens_specification}', "
229 f"exposure='{self.exposure}', "
230 f"shutter_speed={self.shutter_speed}, "
231 f"white_balance='{self.white_balance}')")
232
233
234class CameraMetadata:
235 """
236 Metadata for radiation camera image export (Helios v1.3.58+).
237
238 This class encapsulates comprehensive metadata for camera images including
239 camera properties, location, acquisition settings, image processing, and
240 agronomic properties derived from plant architecture data.
241 """
242
244 """Camera intrinsic properties for metadata export."""
245 def __init__(self, height=512, width=512, channels=3, type="rgb",
246 focal_length=50.0, aperture="f/2.8", sensor_width=35.0,
247 sensor_height=24.0, model="generic", lens_make="",
248 lens_model="", lens_specification="", exposure="auto",
249 shutter_speed=0.008, white_balance="auto"):
250 self.height = int(height)
251 self.width = int(width)
252 self.channels = int(channels)
253 self.type = str(type)
254 self.focal_length = float(focal_length)
255 self.aperture = str(aperture)
256 self.sensor_width = float(sensor_width)
257 self.sensor_height = float(sensor_height)
258 self.model = str(model)
259 self.lens_make = str(lens_make)
260 self.lens_model = str(lens_model)
261 self.lens_specification = str(lens_specification)
262 self.exposure = str(exposure)
263 self.shutter_speed = float(shutter_speed)
264 self.white_balance = str(white_balance)
267 """Geographic location properties."""
268 def __init__(self, latitude=0.0, longitude=0.0):
269 self.latitude = float(latitude)
270 self.longitude = float(longitude)
273 """Image acquisition properties."""
274 def __init__(self, date="", time="", UTC_offset=0.0, camera_height_m=0.0,
275 camera_angle_deg=0.0, light_source="sunlight"):
276 self.date = str(date)
277 self.time = str(time)
278 self.UTC_offset = float(UTC_offset)
279 self.camera_height_m = float(camera_height_m)
280 self.camera_angle_deg = float(camera_angle_deg)
281 self.light_source = str(light_source)
284 """Image processing corrections applied to the image."""
285 def __init__(self, saturation_adjustment=1.0, brightness_adjustment=1.0,
286 contrast_adjustment=1.0, color_space="linear"):
287 self.saturation_adjustment = float(saturation_adjustment)
288 self.brightness_adjustment = float(brightness_adjustment)
289 self.contrast_adjustment = float(contrast_adjustment)
290 self.color_space = str(color_space)
293 """Agronomic properties derived from plant architecture data."""
294 def __init__(self, plant_species=None, plant_count=None, plant_height_m=None,
295 plant_age_days=None, plant_stage=None, leaf_area_m2=None,
296 weed_pressure=""):
297 self.plant_species = plant_species if plant_species is not None else []
298 self.plant_count = plant_count if plant_count is not None else []
299 self.plant_height_m = plant_height_m if plant_height_m is not None else []
300 self.plant_age_days = plant_age_days if plant_age_days is not None else []
301 self.plant_stage = plant_stage if plant_stage is not None else []
302 self.leaf_area_m2 = leaf_area_m2 if leaf_area_m2 is not None else []
303 self.weed_pressure = str(weed_pressure)
305 def __init__(self, path=""):
306 """
307 Initialize CameraMetadata with default values.
308
309 Args:
310 path: Full path to the associated image file. Default: ""
311 """
312 self.path = str(path)
321 f"camera={self.camera_properties.model}, "
322 f"resolution={self.camera_properties.width}x{self.camera_properties.height}, "
323 f"location=({self.location_properties.latitude},{self.location_properties.longitude}))")
324
325
326class RadiationModel:
327 """
328 High-level interface for radiation modeling and ray tracing.
329
330 This class provides a user-friendly wrapper around the native Helios
331 radiation plugin with automatic plugin availability checking and
332 graceful error handling.
333 """
334
335 def __init__(self, context: Context):
336 """
337 Initialize RadiationModel with graceful plugin handling.
338
339 Args:
340 context: Helios Context instance
341
342 Raises:
343 TypeError: If context is not a Context instance
344 RadiationModelError: If radiation plugin is not available
345 """
346 # Validate context type
347 if not isinstance(context, Context):
348 raise TypeError(f"RadiationModel requires a Context instance, got {type(context).__name__}")
349
350 self.context = context
351 self.radiation_model = None
353 # Check plugin availability using registry
354 registry = get_plugin_registry()
355
356 if not registry.is_plugin_available('radiation'):
357 # Get helpful information about the missing plugin
358 plugin_info = registry.get_plugin_capabilities()
359 available_plugins = registry.get_available_plugins()
360
361 error_msg = (
362 "RadiationModel requires the 'radiation' plugin which is not available.\n\n"
363 "The radiation plugin provides GPU-accelerated ray tracing using OptiX.\n"
364 "System requirements:\n"
365 "- NVIDIA GPU with CUDA support\n"
366 "- CUDA Toolkit installed\n"
367 "- OptiX runtime (bundled with PyHelios)\n\n"
368 "To enable radiation modeling:\n"
369 "1. Build PyHelios with radiation plugin:\n"
370 " build_scripts/build_helios --plugins radiation\n"
371 "2. Or build with multiple plugins:\n"
372 " build_scripts/build_helios --plugins radiation,visualizer,weberpenntree\n"
373 f"\nCurrently available plugins: {available_plugins}"
374 )
375
376 # Suggest alternatives if available
377 alternatives = registry.suggest_alternatives('radiation')
378 if alternatives:
379 error_msg += f"\n\nAlternative plugins available: {alternatives}"
380 error_msg += "\nConsider using energybalance or leafoptics for thermal modeling."
381
382 raise RadiationModelError(error_msg)
383
384 # Plugin is available - create radiation model using working directory context manager
385 try:
387 self.radiation_model = radiation_wrapper.createRadiationModel(context.getNativePtr())
388 if self.radiation_model is None:
390 "Failed to create RadiationModel instance. "
391 "This may indicate a problem with the native library or GPU initialization."
392 )
393 logger.info("RadiationModel created successfully")
394
395 except Exception as e:
396 raise RadiationModelError(f"Failed to initialize RadiationModel: {e}")
397
398 def __enter__(self):
399 """Context manager entry."""
400 return self
402 def __exit__(self, exc_type, exc_value, traceback):
403 """Context manager exit with proper cleanup."""
404 if self.radiation_model is not None:
405 try:
406 radiation_wrapper.destroyRadiationModel(self.radiation_model)
407 logger.debug("RadiationModel destroyed successfully")
408 except Exception as e:
409 logger.warning(f"Error destroying RadiationModel: {e}")
410 finally:
411 self.radiation_model = None # Prevent double deletion
412
413 def __del__(self):
414 """Destructor to ensure GPU resources freed even without 'with' statement."""
415 if hasattr(self, 'radiation_model') and self.radiation_model is not None:
416 try:
417 radiation_wrapper.destroyRadiationModel(self.radiation_model)
418 self.radiation_model = None
419 except Exception as e:
420 import warnings
421 warnings.warn(f"Error in RadiationModel.__del__: {e}")
422
423 def get_native_ptr(self):
424 """Get native pointer for advanced operations."""
425 return self.radiation_model
427 def getNativePtr(self):
428 """Get native pointer for advanced operations. (Legacy naming for compatibility)"""
429 return self.get_native_ptr()
431 @require_plugin('radiation', 'disable status messages')
432 def disableMessages(self):
433 """Disable RadiationModel status messages."""
434 radiation_wrapper.disableMessages(self.radiation_model)
436 @require_plugin('radiation', 'enable status messages')
437 def enableMessages(self):
438 """Enable RadiationModel status messages."""
439 radiation_wrapper.enableMessages(self.radiation_model)
441 @require_plugin('radiation', 'add radiation band')
442 def addRadiationBand(self, band_label: str, wavelength_min: float = None, wavelength_max: float = None):
443 """
444 Add radiation band with optional wavelength bounds.
445
446 Args:
447 band_label: Name/label for the radiation band
448 wavelength_min: Optional minimum wavelength (μm)
449 wavelength_max: Optional maximum wavelength (μm)
450 """
451 # Validate inputs
452 validate_band_label(band_label, "band_label", "addRadiationBand")
453 if wavelength_min is not None and wavelength_max is not None:
454 validate_wavelength_range(wavelength_min, wavelength_max, "wavelength_min", "wavelength_max", "addRadiationBand")
455 radiation_wrapper.addRadiationBandWithWavelengths(self.radiation_model, band_label, wavelength_min, wavelength_max)
456 logger.debug(f"Added radiation band {band_label}: {wavelength_min}-{wavelength_max} μm")
457 else:
458 radiation_wrapper.addRadiationBand(self.radiation_model, band_label)
459 logger.debug(f"Added radiation band: {band_label}")
460
461 @require_plugin('radiation', 'copy radiation band')
462 @validate_radiation_band_params
463 def copyRadiationBand(self, old_label: str, new_label: str, wavelength_min: float = None, wavelength_max: float = None):
464 """
465 Copy existing radiation band to new label, optionally with new wavelength range.
466
467 Args:
468 old_label: Existing band label to copy
469 new_label: New label for the copied band
470 wavelength_min: Optional minimum wavelength for new band (μm)
471 wavelength_max: Optional maximum wavelength for new band (μm)
472
473 Example:
474 >>> # Copy band with same wavelength range
475 >>> radiation.copyRadiationBand("SW", "SW_copy")
476 >>>
477 >>> # Copy band with different wavelength range
478 >>> radiation.copyRadiationBand("full_spectrum", "PAR", 400, 700)
479 """
480 if wavelength_min is not None and wavelength_max is not None:
481 validate_wavelength_range(wavelength_min, wavelength_max, "wavelength_min", "wavelength_max", "copyRadiationBand")
482
483 radiation_wrapper.copyRadiationBand(self.radiation_model, old_label, new_label, wavelength_min, wavelength_max)
484 if wavelength_min is not None:
485 logger.debug(f"Copied radiation band {old_label} to {new_label} with wavelengths {wavelength_min}-{wavelength_max} μm")
486 else:
487 logger.debug(f"Copied radiation band {old_label} to {new_label}")
488
489 @require_plugin('radiation', 'add radiation source')
490 @validate_collimated_source_params
491 def addCollimatedRadiationSource(self, direction=None) -> int:
492 """
493 Add collimated radiation source.
494
495 Args:
496 direction: Optional direction vector. Can be tuple (x, y, z), vec3, or None for default direction.
497
498 Returns:
499 Source ID
500 """
501 if direction is None:
502 source_id = radiation_wrapper.addCollimatedRadiationSourceDefault(self.radiation_model)
503 else:
504 # Handle vec3, SphericalCoord, and tuple types
505 if hasattr(direction, 'x') and hasattr(direction, 'y') and hasattr(direction, 'z'):
506 # vec3-like object
507 x, y, z = direction.x, direction.y, direction.z
508 elif hasattr(direction, 'radius') and hasattr(direction, 'elevation') and hasattr(direction, 'azimuth'):
509 # SphericalCoord object - convert to Cartesian
510 import math
511 r = direction.radius
512 elevation = direction.elevation
513 azimuth = direction.azimuth
514 x = r * math.cos(elevation) * math.cos(azimuth)
515 y = r * math.cos(elevation) * math.sin(azimuth)
516 z = r * math.sin(elevation)
517 else:
518 # Assume tuple-like object - validate it first
519 try:
520 if len(direction) != 3:
521 raise TypeError(f"Direction must be a 3-element tuple, vec3, or SphericalCoord, got {type(direction).__name__} with {len(direction)} elements")
522 x, y, z = direction
523 except (TypeError, AttributeError):
524 # Not a valid sequence type
525 raise TypeError(f"Direction must be a tuple, vec3, or SphericalCoord, got {type(direction).__name__}")
526 source_id = radiation_wrapper.addCollimatedRadiationSourceVec3(self.radiation_model, x, y, z)
527
528 logger.debug(f"Added collimated radiation source: ID {source_id}")
529 return source_id
530
531 @require_plugin('radiation', 'add spherical radiation source')
532 @validate_sphere_source_params
533 def addSphereRadiationSource(self, position, radius: float) -> int:
534 """
535 Add spherical radiation source.
536
537 Args:
538 position: Position of the source. Can be tuple (x, y, z) or vec3.
539 radius: Radius of the spherical source
540
541 Returns:
542 Source ID
543 """
544 # Handle both tuple and vec3 types
545 if hasattr(position, 'x') and hasattr(position, 'y') and hasattr(position, 'z'):
546 # vec3-like object
547 x, y, z = position.x, position.y, position.z
548 else:
549 # Assume tuple-like object
550 x, y, z = position
551 source_id = radiation_wrapper.addSphereRadiationSource(self.radiation_model, x, y, z, radius)
552 logger.debug(f"Added sphere radiation source: ID {source_id} at ({x}, {y}, {z}) with radius {radius}")
553 return source_id
554
555 @require_plugin('radiation', 'add sun radiation source')
556 @validate_sun_sphere_params
557 def addSunSphereRadiationSource(self, radius: float, zenith: float, azimuth: float,
558 position_scaling: float = 1.0, angular_width: float = 0.53,
559 flux_scaling: float = 1.0) -> int:
560 """
561 Add sun sphere radiation source.
562
563 Args:
564 radius: Radius of the sun sphere
565 zenith: Zenith angle (degrees)
566 azimuth: Azimuth angle (degrees)
567 position_scaling: Position scaling factor
568 angular_width: Angular width of the sun (degrees)
569 flux_scaling: Flux scaling factor
570
571 Returns:
572 Source ID
573 """
574 source_id = radiation_wrapper.addSunSphereRadiationSource(
575 self.radiation_model, radius, zenith, azimuth, position_scaling, angular_width, flux_scaling
576 )
577 logger.debug(f"Added sun radiation source: ID {source_id}")
578 return source_id
579
580 @require_plugin('radiation', 'set source position')
581 def setSourcePosition(self, source_id: int, position):
582 """
583 Set position of a radiation source.
584
585 Allows dynamic repositioning of radiation sources during simulation,
586 useful for time-series modeling or moving light sources.
587
588 Args:
589 source_id: ID of the radiation source
590 position: New position as vec3, SphericalCoord, or list/tuple [x, y, z]
591
592 Example:
593 >>> source_id = radiation.addCollimatedRadiationSource()
594 >>> radiation.setSourcePosition(source_id, [10, 20, 30])
595 >>> from pyhelios.types import vec3
596 >>> radiation.setSourcePosition(source_id, vec3(15, 25, 35))
597 """
598 if not isinstance(source_id, int) or source_id < 0:
599 raise ValueError(f"Source ID must be a non-negative integer, got {source_id}")
600 radiation_wrapper.setSourcePosition(self.radiation_model, source_id, position)
601 logger.debug(f"Updated position for radiation source {source_id}")
602
603 @require_plugin('radiation', 'add rectangle radiation source')
604 def addRectangleRadiationSource(self, position, size, rotation) -> int:
605 """
606 Add a rectangle (planar) radiation source.
607
608 Rectangle sources are ideal for modeling artificial lighting such as
609 LED panels, grow lights, or window light sources.
610
611 Args:
612 position: Center position as vec3 or list [x, y, z]
613 size: Rectangle dimensions as vec2 or list [width, height]
614 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
615
616 Returns:
617 Source ID
618
619 Example:
620 >>> from pyhelios.types import vec3, vec2
621 >>> source_id = radiation.addRectangleRadiationSource(
622 ... position=vec3(0, 0, 5),
623 ... size=vec2(2, 1),
624 ... rotation=vec3(0, 0, 0)
625 ... )
626 >>> radiation.setSourceFlux(source_id, "PAR", 500.0)
627 """
628 return radiation_wrapper.addRectangleRadiationSource(self.radiation_model, position, size, rotation)
630 @require_plugin('radiation', 'add disk radiation source')
631 def addDiskRadiationSource(self, position, radius: float, rotation) -> int:
632 """
633 Add a disk (circular planar) radiation source.
634
635 Disk sources are useful for modeling circular light sources such as
636 spotlights, circular LED arrays, or solar simulators.
637
638 Args:
639 position: Center position as vec3 or list [x, y, z]
640 radius: Disk radius
641 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
642
643 Returns:
644 Source ID
645
646 Example:
647 >>> from pyhelios.types import vec3
648 >>> source_id = radiation.addDiskRadiationSource(
649 ... position=vec3(0, 0, 5),
650 ... radius=1.5,
651 ... rotation=vec3(0, 0, 0)
652 ... )
653 >>> radiation.setSourceFlux(source_id, "PAR", 300.0)
654 """
655 if radius <= 0:
656 raise ValueError(f"Radius must be positive, got {radius}")
657 return radiation_wrapper.addDiskRadiationSource(self.radiation_model, position, radius, rotation)
658
659 # Source spectrum methods
660 @require_plugin('radiation', 'manage source spectrum')
661 def setSourceSpectrum(self, source_id, spectrum):
662 """
663 Set radiation spectrum for source(s).
664
665 Spectral distributions define how radiation intensity varies with wavelength,
666 essential for realistic modeling of different light sources (sunlight, LEDs, etc.).
667
668 Args:
669 source_id: Source ID (int) or list of source IDs
670 spectrum: Either:
671 - Spectrum data as list of (wavelength, value) tuples
672 - Global data label string
673
674 Example:
675 >>> # Define custom LED spectrum
676 >>> led_spectrum = [
677 ... (400, 0.0), (450, 0.3), (500, 0.8),
678 ... (550, 0.5), (600, 0.2), (700, 0.0)
679 ... ]
680 >>> radiation.setSourceSpectrum(source_id, led_spectrum)
681 >>>
682 >>> # Use predefined spectrum from global data
683 >>> radiation.setSourceSpectrum(source_id, "D65_illuminant")
684 >>>
685 >>> # Apply same spectrum to multiple sources
686 >>> radiation.setSourceSpectrum([src1, src2, src3], led_spectrum)
687 """
688 radiation_wrapper.setSourceSpectrum(self.radiation_model, source_id, spectrum)
689 logger.debug(f"Set spectrum for source(s) {source_id}")
690
691 @require_plugin('radiation', 'configure source spectrum')
692 def setSourceSpectrumIntegral(self, source_id: int, source_integral: float,
693 wavelength_min: float = None, wavelength_max: float = None):
694 """
695 Set source spectrum integral value.
696
697 Normalizes the spectrum so that its integral equals the specified value,
698 useful for calibrating source intensity.
699
700 Args:
701 source_id: Source ID
702 source_integral: Target integral value
703 wavelength_min: Optional minimum wavelength for integration range
704 wavelength_max: Optional maximum wavelength for integration range
705
706 Example:
707 >>> radiation.setSourceSpectrumIntegral(source_id, 1000.0)
708 >>> radiation.setSourceSpectrumIntegral(source_id, 500.0, 400, 700) # PAR range
709 """
710 if not isinstance(source_id, int) or source_id < 0:
711 raise ValueError(f"Source ID must be a non-negative integer, got {source_id}")
712 if source_integral < 0:
713 raise ValueError(f"Source integral must be non-negative, got {source_integral}")
714
715 radiation_wrapper.setSourceSpectrumIntegral(self.radiation_model, source_id, source_integral,
716 wavelength_min, wavelength_max)
717 logger.debug(f"Set spectrum integral for source {source_id}: {source_integral}")
718
719 # Spectrum integration and analysis methods
720 @require_plugin('radiation', 'integrate spectrum')
721 def integrateSpectrum(self, object_spectrum, wavelength_min: float = None,
722 wavelength_max: float = None, source_id: int = None,
723 camera_spectrum=None) -> float:
724 """
725 Integrate spectrum with optional source/camera spectra and wavelength range.
726
727 This unified method handles multiple integration scenarios:
728 - Basic: Total spectrum integration
729 - Range: Integration over wavelength range
730 - Source: Integration weighted by source spectrum
731 - Camera: Integration weighted by camera spectral response
732 - Full: Integration with both source and camera spectra
733
734 Args:
735 object_spectrum: Object spectrum as list of (wavelength, value) tuples/vec2
736 wavelength_min: Optional minimum wavelength for integration range
737 wavelength_max: Optional maximum wavelength for integration range
738 source_id: Optional source ID for source spectrum weighting
739 camera_spectrum: Optional camera spectrum for camera response weighting
740
741 Returns:
742 Integrated value
743
744 Example:
745 >>> leaf_reflectance = [(400, 0.1), (500, 0.4), (600, 0.6), (700, 0.5)]
746 >>>
747 >>> # Total integration
748 >>> total = radiation.integrateSpectrum(leaf_reflectance)
749 >>>
750 >>> # PAR range (400-700nm)
751 >>> par = radiation.integrateSpectrum(leaf_reflectance, 400, 700)
752 >>>
753 >>> # With source spectrum
754 >>> source_weighted = radiation.integrateSpectrum(
755 ... leaf_reflectance, 400, 700, source_id=sun_source
756 ... )
757 >>>
758 >>> # With camera response
759 >>> camera_response = [(400, 0.2), (550, 1.0), (700, 0.3)]
760 >>> camera_weighted = radiation.integrateSpectrum(
761 ... leaf_reflectance, camera_spectrum=camera_response
762 ... )
763 """
764 return radiation_wrapper.integrateSpectrum(self.radiation_model, object_spectrum,
765 wavelength_min, wavelength_max,
766 source_id, camera_spectrum)
767
768 @require_plugin('radiation', 'integrate source spectrum')
769 def integrateSourceSpectrum(self, source_id: int, wavelength_min: float, wavelength_max: float) -> float:
770 """
771 Integrate source spectrum over wavelength range.
772
773 Args:
774 source_id: Source ID
775 wavelength_min: Minimum wavelength
776 wavelength_max: Maximum wavelength
777
778 Returns:
779 Integrated source spectrum value
780
781 Example:
782 >>> par_flux = radiation.integrateSourceSpectrum(source_id, 400, 700)
783 """
784 if not isinstance(source_id, int) or source_id < 0:
785 raise ValueError(f"Source ID must be a non-negative integer, got {source_id}")
786 return radiation_wrapper.integrateSourceSpectrum(self.radiation_model, source_id,
787 wavelength_min, wavelength_max)
788
789 # Spectral manipulation methods
790 @require_plugin('radiation', 'scale spectrum')
791 def scaleSpectrum(self, existing_label: str, new_label_or_scale, scale_factor: float = None):
792 """
793 Scale spectrum in-place or to new label.
794
795 Useful for adjusting spectrum intensities or creating variations of
796 existing spectra for sensitivity analysis.
797
798 Supports two call patterns:
799 - scaleSpectrum("label", scale) -> scales in-place
800 - scaleSpectrum("existing", "new", scale) -> creates new scaled spectrum
801
802 Args:
803 existing_label: Existing global data label
804 new_label_or_scale: Either new label string (if creating new) or scale factor (if in-place)
805 scale_factor: Scale factor (required only if new_label_or_scale is a string)
806
807 Example:
808 >>> # In-place scaling
809 >>> radiation.scaleSpectrum("leaf_reflectance", 1.2)
810 >>>
811 >>> # Create new scaled spectrum
812 >>> radiation.scaleSpectrum("leaf_reflectance", "scaled_leaf", 1.5)
813 """
814 if not isinstance(existing_label, str) or not existing_label.strip():
815 raise ValueError("Existing label must be a non-empty string")
816
817 radiation_wrapper.scaleSpectrum(self.radiation_model, existing_label,
818 new_label_or_scale, scale_factor)
819 logger.debug(f"Scaled spectrum '{existing_label}'")
820
821 @require_plugin('radiation', 'scale spectrum randomly')
822 def scaleSpectrumRandomly(self, existing_label: str, new_label: str,
823 min_scale: float, max_scale: float):
824 """
825 Scale spectrum with random factor and store as new label.
826
827 Useful for creating stochastic variations in spectral properties for
828 Monte Carlo simulations or uncertainty quantification.
829
830 Args:
831 existing_label: Existing global data label
832 new_label: New global data label for scaled spectrum
833 min_scale: Minimum scale factor
834 max_scale: Maximum scale factor
835
836 Example:
837 >>> # Create random variation of leaf reflectance
838 >>> radiation.scaleSpectrumRandomly("leaf_base", "leaf_variant", 0.8, 1.2)
839 """
840 if not isinstance(existing_label, str) or not existing_label.strip():
841 raise ValueError("Existing label must be a non-empty string")
842 if not isinstance(new_label, str) or not new_label.strip():
843 raise ValueError("New label must be a non-empty string")
844 if min_scale >= max_scale:
845 raise ValueError(f"min_scale ({min_scale}) must be less than max_scale ({max_scale})")
846
847 radiation_wrapper.scaleSpectrumRandomly(self.radiation_model, existing_label, new_label,
848 min_scale, max_scale)
849 logger.debug(f"Scaled spectrum '{existing_label}' randomly to '{new_label}'")
850
851 @require_plugin('radiation', 'blend spectra')
852 def blendSpectra(self, new_label: str, spectrum_labels: List[str], weights: List[float]):
853 """
854 Blend multiple spectra with specified weights.
855
856 Creates weighted combination of spectra, useful for mixing material properties
857 or creating composite light sources.
858
859 Args:
860 new_label: New global data label for blended spectrum
861 spectrum_labels: List of spectrum labels to blend
862 weights: List of weights (must sum to reasonable values, same length as labels)
863
864 Example:
865 >>> # Mix two leaf types (70% type A, 30% type B)
866 >>> radiation.blendSpectra("mixed_leaf",
867 ... ["leaf_type_a", "leaf_type_b"],
868 ... [0.7, 0.3]
869 ... )
870 """
871 if not isinstance(new_label, str) or not new_label.strip():
872 raise ValueError("New label must be a non-empty string")
873 if len(spectrum_labels) != len(weights):
874 raise ValueError(f"Number of labels ({len(spectrum_labels)}) must match number of weights ({len(weights)})")
875 if not spectrum_labels:
876 raise ValueError("At least one spectrum label required")
877
878 radiation_wrapper.blendSpectra(self.radiation_model, new_label, spectrum_labels, weights)
879 logger.debug(f"Blended {len(spectrum_labels)} spectra into '{new_label}'")
880
881 @require_plugin('radiation', 'blend spectra randomly')
882 def blendSpectraRandomly(self, new_label: str, spectrum_labels: List[str]):
883 """
884 Blend multiple spectra with random weights.
885
886 Creates random combinations of spectra, useful for generating diverse
887 material properties in stochastic simulations.
888
889 Args:
890 new_label: New global data label for blended spectrum
891 spectrum_labels: List of spectrum labels to blend
892
893 Example:
894 >>> # Create random mixture of leaf spectra
895 >>> radiation.blendSpectraRandomly("random_leaf",
896 ... ["young_leaf", "mature_leaf", "senescent_leaf"]
897 ... )
898 """
899 if not isinstance(new_label, str) or not new_label.strip():
900 raise ValueError("New label must be a non-empty string")
901 if not spectrum_labels:
902 raise ValueError("At least one spectrum label required")
903
904 radiation_wrapper.blendSpectraRandomly(self.radiation_model, new_label, spectrum_labels)
905 logger.debug(f"Blended {len(spectrum_labels)} spectra randomly into '{new_label}'")
906
907 # Spectral interpolation methods
908 @require_plugin('radiation', 'interpolate spectrum from data')
909 def interpolateSpectrumFromPrimitiveData(self, primitive_uuids: List[int],
910 spectra_labels: List[str], values: List[float],
911 primitive_data_query_label: str,
912 primitive_data_radprop_label: str):
913 """
914 Interpolate spectral properties based on primitive data values.
915
916 Automatically assigns spectra to primitives by interpolating between
917 reference spectra based on continuous data values (e.g., age, moisture, etc.).
918
919 Args:
920 primitive_uuids: List of primitive UUIDs to assign spectra
921 spectra_labels: List of reference spectrum labels
922 values: List of data values corresponding to each spectrum
923 primitive_data_query_label: Primitive data label containing query values
924 primitive_data_radprop_label: Primitive data label to store assigned spectra
925
926 Example:
927 >>> # Assign leaf reflectance based on age
928 >>> leaf_patches = context.getAllUUIDs("patch")
929 >>> radiation.interpolateSpectrumFromPrimitiveData(
930 ... primitive_uuids=leaf_patches,
931 ... spectra_labels=["young_leaf", "mature_leaf", "old_leaf"],
932 ... values=[0.0, 50.0, 100.0], # Days since emergence
933 ... primitive_data_query_label="leaf_age",
934 ... primitive_data_radprop_label="reflectance"
935 ... )
936 """
937 if not isinstance(primitive_uuids, (list, tuple)) or not primitive_uuids:
938 raise ValueError("Primitive UUIDs must be a non-empty list")
939 if not isinstance(spectra_labels, (list, tuple)) or not spectra_labels:
940 raise ValueError("Spectra labels must be a non-empty list")
941 if not isinstance(values, (list, tuple)) or not values:
942 raise ValueError("Values must be a non-empty list")
943 if len(spectra_labels) != len(values):
944 raise ValueError(f"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
945
946 radiation_wrapper.interpolateSpectrumFromPrimitiveData(
947 self.radiation_model, primitive_uuids, spectra_labels, values,
948 primitive_data_query_label, primitive_data_radprop_label
949 )
950 logger.debug(f"Interpolated spectra for {len(primitive_uuids)} primitives")
951
952 @require_plugin('radiation', 'interpolate spectrum from object data')
953 def interpolateSpectrumFromObjectData(self, object_ids: List[int],
954 spectra_labels: List[str], values: List[float],
955 object_data_query_label: str,
956 primitive_data_radprop_label: str):
957 """
958 Interpolate spectral properties based on object data values.
959
960 Automatically assigns spectra to object primitives by interpolating between
961 reference spectra based on continuous object-level data values.
962
963 Args:
964 object_ids: List of object IDs
965 spectra_labels: List of reference spectrum labels
966 values: List of data values corresponding to each spectrum
967 object_data_query_label: Object data label containing query values
968 primitive_data_radprop_label: Primitive data label to store assigned spectra
969
970 Example:
971 >>> # Assign tree reflectance based on health index
972 >>> tree_ids = [tree1_id, tree2_id, tree3_id]
973 >>> radiation.interpolateSpectrumFromObjectData(
974 ... object_ids=tree_ids,
975 ... spectra_labels=["healthy_tree", "stressed_tree", "diseased_tree"],
976 ... values=[1.0, 0.5, 0.0], # Health index
977 ... object_data_query_label="health_index",
978 ... primitive_data_radprop_label="reflectance"
979 ... )
980 """
981 if not isinstance(object_ids, (list, tuple)) or not object_ids:
982 raise ValueError("Object IDs must be a non-empty list")
983 if not isinstance(spectra_labels, (list, tuple)) or not spectra_labels:
984 raise ValueError("Spectra labels must be a non-empty list")
985 if not isinstance(values, (list, tuple)) or not values:
986 raise ValueError("Values must be a non-empty list")
987 if len(spectra_labels) != len(values):
988 raise ValueError(f"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
989
990 radiation_wrapper.interpolateSpectrumFromObjectData(
991 self.radiation_model, object_ids, spectra_labels, values,
992 object_data_query_label, primitive_data_radprop_label
993 )
994 logger.debug(f"Interpolated spectra for {len(object_ids)} objects")
995
996 @require_plugin('radiation', 'set ray count')
997 def setDirectRayCount(self, band_label: str, ray_count: int):
998 """Set direct ray count for radiation band."""
999 validate_band_label(band_label, "band_label", "setDirectRayCount")
1000 validate_ray_count(ray_count, "ray_count", "setDirectRayCount")
1001 radiation_wrapper.setDirectRayCount(self.radiation_model, band_label, ray_count)
1002
1003 @require_plugin('radiation', 'set ray count')
1004 def setDiffuseRayCount(self, band_label: str, ray_count: int):
1005 """Set diffuse ray count for radiation band."""
1006 validate_band_label(band_label, "band_label", "setDiffuseRayCount")
1007 validate_ray_count(ray_count, "ray_count", "setDiffuseRayCount")
1008 radiation_wrapper.setDiffuseRayCount(self.radiation_model, band_label, ray_count)
1009
1010 @require_plugin('radiation', 'set radiation flux')
1011 def setDiffuseRadiationFlux(self, label: str, flux: float):
1012 """Set diffuse radiation flux for band."""
1013 validate_band_label(label, "label", "setDiffuseRadiationFlux")
1014 validate_flux_value(flux, "flux", "setDiffuseRadiationFlux")
1015 radiation_wrapper.setDiffuseRadiationFlux(self.radiation_model, label, flux)
1016
1017 @require_plugin('radiation', 'configure diffuse radiation')
1018 def setDiffuseRadiationExtinctionCoeff(self, label: str, K: float, peak_direction):
1019 """
1020 Set diffuse radiation extinction coefficient with directional bias.
1021
1022 Models directionally-biased diffuse radiation (e.g., sky radiation with zenith peak).
1023
1024 Args:
1025 label: Band label
1026 K: Extinction coefficient
1027 peak_direction: Peak direction as vec3, SphericalCoord, or list [x, y, z]
1028
1029 Example:
1030 >>> from pyhelios.types import vec3
1031 >>> radiation.setDiffuseRadiationExtinctionCoeff("SW", 0.5, vec3(0, 0, 1))
1032 """
1033 validate_band_label(label, "label", "setDiffuseRadiationExtinctionCoeff")
1034 if K < 0:
1035 raise ValueError(f"Extinction coefficient must be non-negative, got {K}")
1036 radiation_wrapper.setDiffuseRadiationExtinctionCoeff(self.radiation_model, label, K, peak_direction)
1037 logger.debug(f"Set diffuse extinction coefficient for band '{label}': K={K}")
1038
1039 @require_plugin('radiation', 'query diffuse flux')
1040 def getDiffuseFlux(self, band_label: str) -> float:
1041 """
1042 Get diffuse flux for band.
1043
1044 Args:
1045 band_label: Band label
1046
1047 Returns:
1048 Diffuse flux value
1049
1050 Example:
1051 >>> flux = radiation.getDiffuseFlux("SW")
1052 """
1053 validate_band_label(band_label, "band_label", "getDiffuseFlux")
1054 return radiation_wrapper.getDiffuseFlux(self.radiation_model, band_label)
1055
1056 @require_plugin('radiation', 'configure diffuse spectrum')
1057 def setDiffuseSpectrum(self, band_label, spectrum_label: str):
1058 """
1059 Set diffuse spectrum from global data label.
1060
1061 Args:
1062 band_label: Band label (string) or list of band labels
1063 spectrum_label: Spectrum global data label
1064
1065 Example:
1066 >>> radiation.setDiffuseSpectrum("SW", "sky_spectrum")
1067 >>> radiation.setDiffuseSpectrum(["SW", "NIR"], "sky_spectrum")
1068 """
1069 if isinstance(band_label, str):
1070 validate_band_label(band_label, "band_label", "setDiffuseSpectrum")
1071 else:
1072 for label in band_label:
1073 validate_band_label(label, "band_label", "setDiffuseSpectrum")
1074 if not isinstance(spectrum_label, str) or not spectrum_label.strip():
1075 raise ValueError("Spectrum label must be a non-empty string")
1076
1077 radiation_wrapper.setDiffuseSpectrum(self.radiation_model, band_label, spectrum_label)
1078 logger.debug(f"Set diffuse spectrum for band(s) {band_label}")
1079
1080 @require_plugin('radiation', 'configure diffuse spectrum')
1081 def setDiffuseSpectrumIntegral(self, spectrum_integral: float, wavelength_min: float = None,
1082 wavelength_max: float = None, band_label: str = None):
1083 """
1084 Set diffuse spectrum integral.
1085
1086 Args:
1087 spectrum_integral: Integral value
1088 wavelength_min: Optional minimum wavelength
1089 wavelength_max: Optional maximum wavelength
1090 band_label: Optional specific band label (None for all bands)
1091
1092 Example:
1093 >>> radiation.setDiffuseSpectrumIntegral(1000.0) # All bands
1094 >>> radiation.setDiffuseSpectrumIntegral(500.0, 400, 700, band_label="PAR") # Specific band
1095 """
1096 if spectrum_integral < 0:
1097 raise ValueError(f"Spectrum integral must be non-negative, got {spectrum_integral}")
1098 if band_label is not None:
1099 validate_band_label(band_label, "band_label", "setDiffuseSpectrumIntegral")
1100
1101 radiation_wrapper.setDiffuseSpectrumIntegral(self.radiation_model, spectrum_integral,
1102 wavelength_min, wavelength_max, band_label)
1103 logger.debug(f"Set diffuse spectrum integral: {spectrum_integral}")
1104
1105 @require_plugin('radiation', 'set source flux')
1106 def setSourceFlux(self, source_id, label: str, flux: float):
1107 """Set source flux for single source or multiple sources."""
1108 validate_band_label(label, "label", "setSourceFlux")
1109 validate_flux_value(flux, "flux", "setSourceFlux")
1110
1111 if isinstance(source_id, (list, tuple)):
1112 # Multiple sources
1113 validate_source_id_list(list(source_id), "source_id", "setSourceFlux")
1114 radiation_wrapper.setSourceFluxMultiple(self.radiation_model, source_id, label, flux)
1115 else:
1116 # Single source
1117 validate_source_id(source_id, "source_id", "setSourceFlux")
1118 radiation_wrapper.setSourceFlux(self.radiation_model, source_id, label, flux)
1119
1120
1121 @require_plugin('radiation', 'get source flux')
1122 @validate_get_source_flux_params
1123 def getSourceFlux(self, source_id: int, label: str) -> float:
1124 """Get source flux for band."""
1125 return radiation_wrapper.getSourceFlux(self.radiation_model, source_id, label)
1127 @require_plugin('radiation', 'update geometry')
1128 @validate_update_geometry_params
1129 def updateGeometry(self, uuids: Optional[List[int]] = None):
1130 """
1131 Update geometry in radiation model.
1132
1133 Args:
1134 uuids: Optional list of specific UUIDs to update. If None, updates all geometry.
1135 """
1136 if uuids is None:
1137 radiation_wrapper.updateGeometry(self.radiation_model)
1138 logger.debug("Updated all geometry in radiation model")
1139 else:
1140 radiation_wrapper.updateGeometryUUIDs(self.radiation_model, uuids)
1141 logger.debug(f"Updated {len(uuids)} geometry UUIDs in radiation model")
1142
1143 @require_plugin('radiation', 'run radiation simulation')
1144 @validate_run_band_params
1145 def runBand(self, band_label):
1146 """
1147 Run radiation simulation for single band or multiple bands.
1148
1149 PERFORMANCE NOTE: When simulating multiple radiation bands, it is HIGHLY RECOMMENDED
1150 to run all bands in a single call (e.g., runBand(["PAR", "NIR", "SW"])) rather than
1151 sequential single-band calls. This provides significant computational efficiency gains
1152 because:
1153
1154 - GPU ray tracing setup is done once for all bands
1155 - Scene geometry acceleration structures are reused
1156 - OptiX kernel launches are batched together
1157 - Memory transfers between CPU/GPU are minimized
1158
1159 Example:
1160 # EFFICIENT - Single call for multiple bands
1161 radiation.runBand(["PAR", "NIR", "SW"])
1162
1163 # INEFFICIENT - Sequential single-band calls
1164 radiation.runBand("PAR")
1165 radiation.runBand("NIR")
1166 radiation.runBand("SW")
1167
1168 Args:
1169 band_label: Single band name (str) or list of band names for multi-band simulation
1170 """
1171 if isinstance(band_label, (list, tuple)):
1172 # Multiple bands - validate each label
1173 for lbl in band_label:
1174 if not isinstance(lbl, str):
1175 raise TypeError(f"Band labels must be strings, got {type(lbl).__name__}")
1176 radiation_wrapper.runBandMultiple(self.radiation_model, band_label)
1177 logger.info(f"Completed radiation simulation for bands: {band_label}")
1178 else:
1179 # Single band - validate label type
1180 if not isinstance(band_label, str):
1181 raise TypeError(f"Band label must be a string, got {type(band_label).__name__}")
1182 radiation_wrapper.runBand(self.radiation_model, band_label)
1183 logger.info(f"Completed radiation simulation for band: {band_label}")
1184
1185
1186 @require_plugin('radiation', 'get simulation results')
1187 def getTotalAbsorbedFlux(self) -> List[float]:
1188 """Get total absorbed flux for all primitives."""
1189 results = radiation_wrapper.getTotalAbsorbedFlux(self.radiation_model)
1190 logger.debug(f"Retrieved absorbed flux data for {len(results)} primitives")
1191 return results
1192
1193 # Band query methods
1194 @require_plugin('radiation', 'check band existence')
1195 def doesBandExist(self, label: str) -> bool:
1196 """
1197 Check if a radiation band exists.
1198
1199 Args:
1200 label: Name/label of the radiation band to check
1201
1202 Returns:
1203 True if band exists, False otherwise
1204
1205 Example:
1206 >>> radiation.addRadiationBand("SW")
1207 >>> radiation.doesBandExist("SW")
1208 True
1209 >>> radiation.doesBandExist("nonexistent")
1210 False
1211 """
1212 validate_band_label(label, "label", "doesBandExist")
1213 return radiation_wrapper.doesBandExist(self.radiation_model, label)
1214
1215 # Advanced source management methods
1216 @require_plugin('radiation', 'manage radiation sources')
1217 def deleteRadiationSource(self, source_id: int):
1218 """
1219 Delete a radiation source.
1220
1221 Args:
1222 source_id: ID of the radiation source to delete
1223
1224 Example:
1225 >>> source_id = radiation.addCollimatedRadiationSource()
1226 >>> radiation.deleteRadiationSource(source_id)
1227 """
1228 if not isinstance(source_id, int) or source_id < 0:
1229 raise ValueError(f"Source ID must be a non-negative integer, got {source_id}")
1230 radiation_wrapper.deleteRadiationSource(self.radiation_model, source_id)
1231 logger.debug(f"Deleted radiation source {source_id}")
1232
1233 @require_plugin('radiation', 'query radiation sources')
1234 def getSourcePosition(self, source_id: int):
1235 """
1236 Get position of a radiation source.
1237
1238 Args:
1239 source_id: ID of the radiation source
1240
1241 Returns:
1242 vec3 position of the source
1243
1244 Example:
1245 >>> source_id = radiation.addCollimatedRadiationSource()
1246 >>> position = radiation.getSourcePosition(source_id)
1247 >>> print(f"Source at: {position}")
1248 """
1249 if not isinstance(source_id, int) or source_id < 0:
1250 raise ValueError(f"Source ID must be a non-negative integer, got {source_id}")
1251 position_list = radiation_wrapper.getSourcePosition(self.radiation_model, source_id)
1252 from .wrappers.DataTypes import vec3
1253 return vec3(position_list[0], position_list[1], position_list[2])
1254
1255 # Advanced simulation methods
1256 @require_plugin('radiation', 'get sky energy')
1257 def getSkyEnergy(self) -> float:
1258 """
1259 Get total sky energy.
1260
1261 Returns:
1262 Total sky energy value
1263
1264 Example:
1265 >>> energy = radiation.getSkyEnergy()
1266 >>> print(f"Sky energy: {energy}")
1267 """
1268 return radiation_wrapper.getSkyEnergy(self.radiation_model)
1270 @require_plugin('radiation', 'calculate G-function')
1271 def calculateGtheta(self, view_direction) -> float:
1272 """
1273 Calculate G-function (geometry factor) for given view direction.
1274
1275 The G-function describes the geometric relationship between leaf area
1276 distribution and viewing direction, important for canopy radiation modeling.
1277
1278 Args:
1279 view_direction: View direction as vec3 or list/tuple [x, y, z]
1280
1281 Returns:
1282 G-function value
1283
1284 Example:
1285 >>> from pyhelios.types import vec3
1286 >>> g_value = radiation.calculateGtheta(vec3(0, 0, 1))
1287 >>> print(f"G-function: {g_value}")
1288 """
1289 context_ptr = self.context.getNativePtr()
1290 return radiation_wrapper.calculateGtheta(self.radiation_model, context_ptr, view_direction)
1291
1292 @require_plugin('radiation', 'configure output data')
1293 def optionalOutputPrimitiveData(self, label: str):
1294 """
1295 Enable optional primitive data output.
1296
1297 Args:
1298 label: Name/label of the primitive data to output
1299
1300 Example:
1301 >>> radiation.optionalOutputPrimitiveData("temperature")
1302 """
1303 validate_band_label(label, "label", "optionalOutputPrimitiveData")
1304 radiation_wrapper.optionalOutputPrimitiveData(self.radiation_model, label)
1305 logger.debug(f"Enabled optional output for primitive data: {label}")
1306
1307 @require_plugin('radiation', 'configure boundary conditions')
1308 def enforcePeriodicBoundary(self, boundary: str):
1309 """
1310 Enforce periodic boundary conditions.
1311
1312 Periodic boundaries are useful for large-scale simulations to reduce
1313 edge effects by wrapping radiation at domain boundaries.
1314
1315 Args:
1316 boundary: Boundary specification string (e.g., "xy", "xyz", "x", "y", "z")
1317
1318 Example:
1319 >>> radiation.enforcePeriodicBoundary("xy")
1320 """
1321 if not isinstance(boundary, str) or not boundary:
1322 raise ValueError("Boundary specification must be a non-empty string")
1323 radiation_wrapper.enforcePeriodicBoundary(self.radiation_model, boundary)
1324 logger.debug(f"Enforced periodic boundary: {boundary}")
1325
1326 # Configuration methods
1327 @require_plugin('radiation', 'configure radiation simulation')
1328 @validate_scattering_depth_params
1329 def setScatteringDepth(self, label: str, depth: int):
1330 """Set scattering depth for radiation band."""
1331 radiation_wrapper.setScatteringDepth(self.radiation_model, label, depth)
1333 @require_plugin('radiation', 'configure radiation simulation')
1334 @validate_min_scatter_energy_params
1335 def setMinScatterEnergy(self, label: str, energy: float):
1336 """Set minimum scatter energy for radiation band."""
1337 radiation_wrapper.setMinScatterEnergy(self.radiation_model, label, energy)
1339 @require_plugin('radiation', 'configure radiation emission')
1340 def disableEmission(self, label: str):
1341 """Disable emission for radiation band."""
1342 validate_band_label(label, "label", "disableEmission")
1343 radiation_wrapper.disableEmission(self.radiation_model, label)
1344
1345 @require_plugin('radiation', 'configure radiation emission')
1346 def enableEmission(self, label: str):
1347 """Enable emission for radiation band."""
1348 validate_band_label(label, "label", "enableEmission")
1349 radiation_wrapper.enableEmission(self.radiation_model, label)
1350
1351 #=============================================================================
1352 # Camera and Image Functions (v1.3.47)
1353 #=============================================================================
1354
1355 @require_plugin('radiation', 'add radiation camera')
1356 def addRadiationCamera(self, camera_label: str, band_labels: List[str], position, lookat_or_direction,
1357 camera_properties=None, antialiasing_samples: int = 100):
1358 """
1359 Add a radiation camera to the simulation.
1360
1361 Args:
1362 camera_label: Unique label string for the camera
1363 band_labels: List of radiation band labels for the camera
1364 position: Camera position as vec3 object
1365 lookat_or_direction: Either:
1366 - Lookat point as vec3 object
1367 - SphericalCoord for viewing direction
1368 camera_properties: CameraProperties instance or None for defaults
1369 antialiasing_samples: Number of antialiasing samples (default: 100)
1370
1371 Raises:
1372 ValidationError: If parameters are invalid or have wrong types
1373 RadiationModelError: If camera creation fails
1374
1375 Example:
1376 >>> from pyhelios import vec3, CameraProperties
1377 >>> # Create camera looking at origin from above
1378 >>> camera_props = CameraProperties(camera_resolution=(1024, 1024))
1379 >>> radiation_model.addRadiationCamera("main_camera", ["red", "green", "blue"],
1380 ... position=vec3(0, 0, 5), lookat_or_direction=vec3(0, 0, 0),
1381 ... camera_properties=camera_props)
1382 """
1383 # Import here to avoid circular imports
1384 from .wrappers import URadiationModelWrapper as radiation_wrapper
1385 from .wrappers.DataTypes import SphericalCoord, vec3, make_vec3
1386 from .validation.plugins import validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
1387
1388 # Validate basic parameters
1389 validated_label = validate_camera_label(camera_label, "camera_label", "addRadiationCamera")
1390 validated_bands = validate_band_labels_list(band_labels, "band_labels", "addRadiationCamera")
1391 validated_samples = validate_antialiasing_samples(antialiasing_samples, "antialiasing_samples", "addRadiationCamera")
1392
1393 # Validate position (must be vec3)
1394 if not (hasattr(position, 'x') and hasattr(position, 'y') and hasattr(position, 'z')):
1395 raise TypeError("position must be a vec3 object. Use vec3(x, y, z) to create one.")
1396 validated_position = position
1397
1398 # Validate lookat_or_direction (must be vec3 or SphericalCoord)
1399 if hasattr(lookat_or_direction, 'radius') and hasattr(lookat_or_direction, 'elevation'):
1400 validated_direction = lookat_or_direction # SphericalCoord
1401 elif hasattr(lookat_or_direction, 'x') and hasattr(lookat_or_direction, 'y') and hasattr(lookat_or_direction, 'z'):
1402 validated_direction = lookat_or_direction # vec3
1403 else:
1404 raise TypeError("lookat_or_direction must be a vec3 or SphericalCoord object. Use vec3(x, y, z) or SphericalCoord to create one.")
1405
1406 # Set up camera properties
1407 if camera_properties is None:
1408 camera_properties = CameraProperties()
1409
1410 # Call appropriate wrapper function based on direction type
1411 try:
1412 if hasattr(validated_direction, 'radius') and hasattr(validated_direction, 'elevation'):
1413 # SphericalCoord case
1414 direction_coords = validated_direction.to_list()
1415 if len(direction_coords) >= 3:
1416 # Use only radius, elevation, azimuth (first 3 elements)
1417 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[2]
1418 else:
1419 raise ValueError("SphericalCoord must have at least radius, elevation, and azimuth")
1420
1421 radiation_wrapper.addRadiationCameraSpherical(
1422 self.radiation_model,
1423 validated_label,
1424 validated_bands,
1425 validated_position.x, validated_position.y, validated_position.z,
1426 radius, elevation, azimuth,
1427 camera_properties.to_array(),
1428 validated_samples
1429 )
1430 else:
1431 # vec3 case
1432 radiation_wrapper.addRadiationCameraVec3(
1433 self.radiation_model,
1434 validated_label,
1435 validated_bands,
1436 validated_position.x, validated_position.y, validated_position.z,
1437 validated_direction.x, validated_direction.y, validated_direction.z,
1438 camera_properties.to_array(),
1439 validated_samples
1440 )
1441
1442 except Exception as e:
1443 raise RadiationModelError(f"Failed to add radiation camera '{validated_label}': {e}")
1444
1445 @require_plugin('radiation', 'manage camera position')
1446 def setCameraPosition(self, camera_label: str, position):
1447 """
1448 Set camera position.
1449
1450 Allows dynamic camera repositioning during simulation, useful for
1451 time-series captures or multi-view imaging.
1452
1453 Args:
1454 camera_label: Camera label string
1455 position: Camera position as vec3 or list [x, y, z]
1456
1457 Example:
1458 >>> radiation.setCameraPosition("cam1", [0, 0, 10])
1459 >>> from pyhelios.types import vec3
1460 >>> radiation.setCameraPosition("cam1", vec3(5, 5, 10))
1461 """
1462 if not isinstance(camera_label, str) or not camera_label.strip():
1463 raise ValueError("Camera label must be a non-empty string")
1464 radiation_wrapper.setCameraPosition(self.radiation_model, camera_label, position)
1465 logger.debug(f"Updated camera '{camera_label}' position")
1466
1467 @require_plugin('radiation', 'query camera position')
1468 def getCameraPosition(self, camera_label: str):
1469 """
1470 Get camera position.
1471
1472 Args:
1473 camera_label: Camera label string
1474
1475 Returns:
1476 vec3 position of the camera
1477
1478 Example:
1479 >>> position = radiation.getCameraPosition("cam1")
1480 >>> print(f"Camera at: {position}")
1481 """
1482 if not isinstance(camera_label, str) or not camera_label.strip():
1483 raise ValueError("Camera label must be a non-empty string")
1484 position_list = radiation_wrapper.getCameraPosition(self.radiation_model, camera_label)
1485 from .wrappers.DataTypes import vec3
1486 return vec3(position_list[0], position_list[1], position_list[2])
1487
1488 @require_plugin('radiation', 'manage camera lookat')
1489 def setCameraLookat(self, camera_label: str, lookat):
1490 """
1491 Set camera lookat point.
1492
1493 Args:
1494 camera_label: Camera label string
1495 lookat: Lookat point as vec3 or list [x, y, z]
1496
1497 Example:
1498 >>> radiation.setCameraLookat("cam1", [0, 0, 0])
1499 """
1500 if not isinstance(camera_label, str) or not camera_label.strip():
1501 raise ValueError("Camera label must be a non-empty string")
1502 radiation_wrapper.setCameraLookat(self.radiation_model, camera_label, lookat)
1503 logger.debug(f"Updated camera '{camera_label}' lookat point")
1504
1505 @require_plugin('radiation', 'query camera lookat')
1506 def getCameraLookat(self, camera_label: str):
1507 """
1508 Get camera lookat point.
1509
1510 Args:
1511 camera_label: Camera label string
1512
1513 Returns:
1514 vec3 lookat point
1515
1516 Example:
1517 >>> lookat = radiation.getCameraLookat("cam1")
1518 >>> print(f"Camera looking at: {lookat}")
1519 """
1520 if not isinstance(camera_label, str) or not camera_label.strip():
1521 raise ValueError("Camera label must be a non-empty string")
1522 lookat_list = radiation_wrapper.getCameraLookat(self.radiation_model, camera_label)
1523 from .wrappers.DataTypes import vec3
1524 return vec3(lookat_list[0], lookat_list[1], lookat_list[2])
1525
1526 @require_plugin('radiation', 'manage camera orientation')
1527 def setCameraOrientation(self, camera_label: str, direction):
1528 """
1529 Set camera orientation.
1530
1531 Args:
1532 camera_label: Camera label string
1533 direction: View direction as vec3, SphericalCoord, or list [x, y, z]
1534
1535 Example:
1536 >>> radiation.setCameraOrientation("cam1", [0, 0, 1])
1537 >>> from pyhelios.types import SphericalCoord
1538 >>> radiation.setCameraOrientation("cam1", SphericalCoord(1.0, 45.0, 90.0))
1539 """
1540 if not isinstance(camera_label, str) or not camera_label.strip():
1541 raise ValueError("Camera label must be a non-empty string")
1542 radiation_wrapper.setCameraOrientation(self.radiation_model, camera_label, direction)
1543 logger.debug(f"Updated camera '{camera_label}' orientation")
1544
1545 @require_plugin('radiation', 'query camera orientation')
1546 def getCameraOrientation(self, camera_label: str):
1547 """
1548 Get camera orientation.
1549
1550 Args:
1551 camera_label: Camera label string
1552
1553 Returns:
1554 SphericalCoord orientation [radius, elevation, azimuth]
1555
1556 Example:
1557 >>> orientation = radiation.getCameraOrientation("cam1")
1558 >>> print(f"Camera orientation: {orientation}")
1559 """
1560 if not isinstance(camera_label, str) or not camera_label.strip():
1561 raise ValueError("Camera label must be a non-empty string")
1562 orientation_list = radiation_wrapper.getCameraOrientation(self.radiation_model, camera_label)
1563 from .wrappers.DataTypes import SphericalCoord
1564 return SphericalCoord(orientation_list[0], orientation_list[1], orientation_list[2])
1565
1566 @require_plugin('radiation', 'query cameras')
1567 def getAllCameraLabels(self) -> List[str]:
1568 """
1569 Get all camera labels.
1570
1571 Returns:
1572 List of all camera label strings
1573
1574 Example:
1575 >>> cameras = radiation.getAllCameraLabels()
1576 >>> print(f"Available cameras: {cameras}")
1577 """
1578 return radiation_wrapper.getAllCameraLabels(self.radiation_model)
1580 @require_plugin('radiation', 'configure camera spectral response')
1581 def setCameraSpectralResponse(self, camera_label: str, band_label: str, global_data: str):
1582 """
1583 Set camera spectral response from global data.
1584
1585 Args:
1586 camera_label: Camera label
1587 band_label: Band label
1588 global_data: Global data label for spectral response curve
1589
1590 Example:
1591 >>> radiation.setCameraSpectralResponse("cam1", "red", "sensor_red_response")
1592 """
1593 if not isinstance(camera_label, str) or not camera_label.strip():
1594 raise ValueError("Camera label must be a non-empty string")
1595 validate_band_label(band_label, "band_label", "setCameraSpectralResponse")
1596 if not isinstance(global_data, str) or not global_data.strip():
1597 raise ValueError("Global data label must be a non-empty string")
1598
1599 radiation_wrapper.setCameraSpectralResponse(self.radiation_model, camera_label, band_label, global_data)
1600 logger.debug(f"Set spectral response for camera '{camera_label}', band '{band_label}'")
1601
1602 @require_plugin('radiation', 'configure camera from library')
1603 def setCameraSpectralResponseFromLibrary(self, camera_label: str, camera_library_name: str):
1604 """
1605 Set camera spectral response from standard camera library.
1606
1607 Uses pre-defined spectral response curves for common cameras.
1608
1609 Args:
1610 camera_label: Camera label
1611 camera_library_name: Standard camera name (e.g., "iPhone13", "NikonD850", "CanonEOS5D")
1612
1613 Example:
1614 >>> radiation.setCameraSpectralResponseFromLibrary("cam1", "iPhone13")
1615 """
1616 if not isinstance(camera_label, str) or not camera_label.strip():
1617 raise ValueError("Camera label must be a non-empty string")
1618 if not isinstance(camera_library_name, str) or not camera_library_name.strip():
1619 raise ValueError("Camera library name must be a non-empty string")
1620
1621 radiation_wrapper.setCameraSpectralResponseFromLibrary(self.radiation_model, camera_label, camera_library_name)
1622 logger.debug(f"Set camera '{camera_label}' response from library: {camera_library_name}")
1623
1624 @require_plugin('radiation', 'get camera pixel data')
1625 def getCameraPixelData(self, camera_label: str, band_label: str) -> List[float]:
1626 """
1627 Get camera pixel data for specific band.
1628
1629 Retrieves raw pixel values for programmatic access and analysis.
1630
1631 Args:
1632 camera_label: Camera label
1633 band_label: Band label
1634
1635 Returns:
1636 List of pixel values
1637
1638 Example:
1639 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1640 >>> print(f"Mean pixel value: {sum(pixels)/len(pixels)}")
1641 """
1642 if not isinstance(camera_label, str) or not camera_label.strip():
1643 raise ValueError("Camera label must be a non-empty string")
1644 validate_band_label(band_label, "band_label", "getCameraPixelData")
1645
1646 return radiation_wrapper.getCameraPixelData(self.radiation_model, camera_label, band_label)
1647
1648 @require_plugin('radiation', 'set camera pixel data')
1649 def setCameraPixelData(self, camera_label: str, band_label: str, pixel_data: List[float]):
1650 """
1651 Set camera pixel data for specific band.
1652
1653 Allows programmatic modification of pixel values.
1654
1655 Args:
1656 camera_label: Camera label
1657 band_label: Band label
1658 pixel_data: List of pixel values
1659
1660 Example:
1661 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1662 >>> modified_pixels = [p * 1.2 for p in pixels] # Brighten by 20%
1663 >>> radiation.setCameraPixelData("cam1", "red", modified_pixels)
1664 """
1665 if not isinstance(camera_label, str) or not camera_label.strip():
1666 raise ValueError("Camera label must be a non-empty string")
1667 validate_band_label(band_label, "band_label", "setCameraPixelData")
1668 if not isinstance(pixel_data, (list, tuple)):
1669 raise ValueError("Pixel data must be a list or tuple")
1670
1671 radiation_wrapper.setCameraPixelData(self.radiation_model, camera_label, band_label, pixel_data)
1672 logger.debug(f"Set pixel data for camera '{camera_label}', band '{band_label}': {len(pixel_data)} pixels")
1673
1674 # =========================================================================
1675 # Camera Library Functions (v1.3.58+)
1676 # =========================================================================
1677
1678 @require_plugin('radiation', 'add camera from library')
1679 def addRadiationCameraFromLibrary(self, camera_label: str, library_camera_label: str,
1680 position, lookat, antialiasing_samples: int = 1,
1681 band_labels: Optional[List[str]] = None):
1682 """
1683 Add radiation camera loading all properties from camera library.
1684
1685 Loads camera intrinsic parameters (resolution, FOV, sensor size) and spectral
1686 response data from the camera library XML file. This is the recommended way to
1687 create realistic cameras with proper spectral responses.
1688
1689 Args:
1690 camera_label: Label for the camera instance
1691 library_camera_label: Label of camera in library (e.g., "Canon_20D", "iPhone11", "NikonD700")
1692 position: Camera position as vec3 or (x, y, z) tuple
1693 lookat: Lookat point as vec3 or (x, y, z) tuple
1694 antialiasing_samples: Number of ray samples per pixel. Default: 1
1695 band_labels: Optional custom band labels. If None, uses library defaults.
1696
1697 Raises:
1698 RadiationModelError: If operation fails
1699 ValueError: If parameters are invalid
1700
1701 Note:
1702 Available cameras in plugins/radiation/camera_library/camera_library.xml include:
1703 - Canon_20D, Nikon_D700, Nikon_D50
1704 - iPhone11, iPhone12ProMAX
1705 - Additional cameras available in library
1706
1707 Example:
1708 >>> radiation.addRadiationCameraFromLibrary(
1709 ... camera_label="cam1",
1710 ... library_camera_label="iPhone11",
1711 ... position=(0, -5, 1),
1712 ... lookat=(0, 0, 0.5),
1713 ... antialiasing_samples=10
1714 ... )
1715 """
1716 validate_band_label(camera_label, "camera_label", "addRadiationCameraFromLibrary")
1717
1718 try:
1719 radiation_wrapper.addRadiationCameraFromLibrary(
1720 self.radiation_model, camera_label, library_camera_label,
1721 position, lookat, antialiasing_samples, band_labels
1722 )
1723 logger.info(f"Added camera '{camera_label}' from library '{library_camera_label}'")
1724 except Exception as e:
1725 raise RadiationModelError(f"Failed to add camera from library: {e}")
1726
1727 @require_plugin('radiation', 'update camera parameters')
1728 def updateCameraParameters(self, camera_label: str, camera_properties: CameraProperties):
1729 """
1730 Update camera parameters for an existing camera.
1731
1732 Allows modification of camera properties after creation while preserving
1733 position, lookat direction, and spectral band configuration.
1734
1735 Args:
1736 camera_label: Label for the camera to update
1737 camera_properties: CameraProperties instance with new parameters
1738
1739 Raises:
1740 RadiationModelError: If operation fails or camera doesn't exist
1741 ValueError: If parameters are invalid
1742
1743 Note:
1744 FOV_aspect_ratio is automatically recalculated from camera_resolution.
1745 Camera position and lookat are preserved.
1746
1747 Example:
1748 >>> props = CameraProperties(
1749 ... camera_resolution=(1920, 1080),
1750 ... HFOV=35.0,
1751 ... lens_focal_length=0.085 # 85mm lens
1752 ... )
1753 >>> radiation.updateCameraParameters("cam1", props)
1754 """
1755 validate_band_label(camera_label, "camera_label", "updateCameraParameters")
1757 if not isinstance(camera_properties, CameraProperties):
1758 raise ValueError("camera_properties must be a CameraProperties instance")
1759
1760 try:
1761 radiation_wrapper.updateCameraParameters(self.radiation_model, camera_label, camera_properties)
1762 logger.debug(f"Updated parameters for camera '{camera_label}'")
1763 except Exception as e:
1764 raise RadiationModelError(f"Failed to update camera parameters: {e}")
1765
1766 @require_plugin('radiation', 'enable camera metadata')
1767 def enableCameraMetadata(self, camera_labels):
1768 """
1769 Enable automatic JSON metadata file writing for camera(s).
1770
1771 When enabled, writeCameraImage() automatically creates a JSON metadata file
1772 alongside the image containing comprehensive camera and scene information.
1773
1774 Args:
1775 camera_labels: Single camera label (str) or list of camera labels (List[str])
1776
1777 Raises:
1778 RadiationModelError: If operation fails
1779 ValueError: If parameters are invalid
1780
1781 Note:
1782 Metadata includes:
1783 - Camera properties (model, lens, sensor specs)
1784 - Geographic location (latitude, longitude)
1785 - Acquisition settings (date, time, exposure, white balance)
1786 - Agronomic data (plant species, heights, phenology stages)
1787
1788 Example:
1789 >>> # Enable for single camera
1790 >>> radiation.enableCameraMetadata("cam1")
1791 >>>
1792 >>> # Enable for multiple cameras
1793 >>> radiation.enableCameraMetadata(["cam1", "cam2", "cam3"])
1794 """
1795 try:
1796 radiation_wrapper.enableCameraMetadata(self.radiation_model, camera_labels)
1797 if isinstance(camera_labels, str):
1798 logger.info(f"Enabled metadata for camera '{camera_labels}'")
1799 else:
1800 logger.info(f"Enabled metadata for {len(camera_labels)} cameras")
1801 except Exception as e:
1802 raise RadiationModelError(f"Failed to enable camera metadata: {e}")
1803
1804 @require_plugin('radiation', 'write camera images')
1805 def writeCameraImage(self, camera: str, bands: List[str], imagefile_base: str,
1806 image_path: str = "./", frame: int = -1,
1807 flux_to_pixel_conversion: float = 1.0) -> str:
1808 """
1809 Write camera image to file and return output filename.
1810
1811 Args:
1812 camera: Camera label
1813 bands: List of band labels to include in the image
1814 imagefile_base: Base filename for output
1815 image_path: Output directory path (default: current directory)
1816 frame: Frame number to write (-1 for all frames)
1817 flux_to_pixel_conversion: Conversion factor from flux to pixel values
1818
1819 Returns:
1820 Output filename string
1821
1822 Raises:
1823 RadiationModelError: If camera image writing fails
1824 TypeError: If parameters have incorrect types
1825 """
1826 # Validate inputs
1827 if not isinstance(camera, str) or not camera.strip():
1828 raise TypeError("Camera label must be a non-empty string")
1829 if not isinstance(bands, list) or not bands:
1830 raise TypeError("Bands must be a non-empty list of strings")
1831 if not all(isinstance(band, str) and band.strip() for band in bands):
1832 raise TypeError("All band labels must be non-empty strings")
1833 if not isinstance(imagefile_base, str) or not imagefile_base.strip():
1834 raise TypeError("Image file base must be a non-empty string")
1835 if not isinstance(image_path, str):
1836 raise TypeError("Image path must be a string")
1837 if not isinstance(frame, int):
1838 raise TypeError("Frame must be an integer")
1839 if not isinstance(flux_to_pixel_conversion, (int, float)) or flux_to_pixel_conversion <= 0:
1840 raise TypeError("Flux to pixel conversion must be a positive number")
1841
1842 filename = radiation_wrapper.writeCameraImage(
1843 self.radiation_model, camera, bands, imagefile_base,
1844 image_path, frame, flux_to_pixel_conversion)
1845
1846 logger.info(f"Camera image written to: {filename}")
1847 return filename
1848
1849 @require_plugin('radiation', 'write normalized camera images')
1850 def writeNormCameraImage(self, camera: str, bands: List[str], imagefile_base: str,
1851 image_path: str = "./", frame: int = -1) -> str:
1852 """
1853 Write normalized camera image to file and return output filename.
1854
1855 Args:
1856 camera: Camera label
1857 bands: List of band labels to include in the image
1858 imagefile_base: Base filename for output
1859 image_path: Output directory path (default: current directory)
1860 frame: Frame number to write (-1 for all frames)
1861
1862 Returns:
1863 Output filename string
1864
1865 Raises:
1866 RadiationModelError: If normalized camera image writing fails
1867 TypeError: If parameters have incorrect types
1868 """
1869 # Validate inputs
1870 if not isinstance(camera, str) or not camera.strip():
1871 raise TypeError("Camera label must be a non-empty string")
1872 if not isinstance(bands, list) or not bands:
1873 raise TypeError("Bands must be a non-empty list of strings")
1874 if not all(isinstance(band, str) and band.strip() for band in bands):
1875 raise TypeError("All band labels must be non-empty strings")
1876 if not isinstance(imagefile_base, str) or not imagefile_base.strip():
1877 raise TypeError("Image file base must be a non-empty string")
1878 if not isinstance(image_path, str):
1879 raise TypeError("Image path must be a string")
1880 if not isinstance(frame, int):
1881 raise TypeError("Frame must be an integer")
1882
1883 filename = radiation_wrapper.writeNormCameraImage(
1884 self.radiation_model, camera, bands, imagefile_base, image_path, frame)
1885
1886 logger.info(f"Normalized camera image written to: {filename}")
1887 return filename
1888
1889 @require_plugin('radiation', 'write camera image data')
1890 def writeCameraImageData(self, camera: str, band: str, imagefile_base: str,
1891 image_path: str = "./", frame: int = -1):
1892 """
1893 Write camera image data to file (ASCII format).
1894
1895 Args:
1896 camera: Camera label
1897 band: Band label
1898 imagefile_base: Base filename for output
1899 image_path: Output directory path (default: current directory)
1900 frame: Frame number to write (-1 for all frames)
1901
1902 Raises:
1903 RadiationModelError: If camera image data writing fails
1904 TypeError: If parameters have incorrect types
1905 """
1906 # Validate inputs
1907 if not isinstance(camera, str) or not camera.strip():
1908 raise TypeError("Camera label must be a non-empty string")
1909 if not isinstance(band, str) or not band.strip():
1910 raise TypeError("Band label must be a non-empty string")
1911 if not isinstance(imagefile_base, str) or not imagefile_base.strip():
1912 raise TypeError("Image file base must be a non-empty string")
1913 if not isinstance(image_path, str):
1914 raise TypeError("Image path must be a string")
1915 if not isinstance(frame, int):
1916 raise TypeError("Frame must be an integer")
1917
1918 radiation_wrapper.writeCameraImageData(
1919 self.radiation_model, camera, band, imagefile_base, image_path, frame)
1920
1921 logger.info(f"Camera image data written for camera {camera}, band {band}")
1922
1923 @require_plugin('radiation', 'write image bounding boxes')
1924 def writeImageBoundingBoxes(self, camera_label: str,
1925 primitive_data_labels=None, object_data_labels=None,
1926 object_class_ids=None, image_file: str = "",
1927 classes_txt_file: str = "classes.txt",
1928 image_path: str = "./"):
1929 """
1930 Write image bounding boxes for object detection training.
1931
1932 Supports both single and multiple data labels. Either provide primitive_data_labels
1933 or object_data_labels, not both.
1934
1935 Args:
1936 camera_label: Camera label
1937 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
1938 object_data_labels: Single object data label (str) or list of object data labels
1939 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
1940 image_file: Image filename
1941 classes_txt_file: Classes definition file (default: "classes.txt")
1942 image_path: Image output path (default: current directory)
1943
1944 Raises:
1945 RadiationModelError: If bounding box writing fails
1946 TypeError: If parameters have incorrect types
1947 ValueError: If both primitive and object data labels are provided, or neither
1948 """
1949 # Validate exclusive parameter usage
1950 if primitive_data_labels is not None and object_data_labels is not None:
1951 raise ValueError("Cannot specify both primitive_data_labels and object_data_labels")
1952 if primitive_data_labels is None and object_data_labels is None:
1953 raise ValueError("Must specify either primitive_data_labels or object_data_labels")
1954
1955 # Validate common parameters
1956 if not isinstance(camera_label, str) or not camera_label.strip():
1957 raise TypeError("Camera label must be a non-empty string")
1958 if not isinstance(image_file, str) or not image_file.strip():
1959 raise TypeError("Image file must be a non-empty string")
1960 if not isinstance(classes_txt_file, str):
1961 raise TypeError("Classes txt file must be a string")
1962 if not isinstance(image_path, str):
1963 raise TypeError("Image path must be a string")
1964
1965 # Handle primitive data labels
1966 if primitive_data_labels is not None:
1967 if isinstance(primitive_data_labels, str):
1968 # Single label
1969 if not isinstance(object_class_ids, int):
1970 raise TypeError("For single primitive data label, object_class_ids must be an integer")
1971 radiation_wrapper.writeImageBoundingBoxes(
1972 self.radiation_model, camera_label, primitive_data_labels,
1973 object_class_ids, image_file, classes_txt_file, image_path)
1974 logger.info(f"Image bounding boxes written for primitive data: {primitive_data_labels}")
1975
1976 elif isinstance(primitive_data_labels, list):
1977 # Multiple labels
1978 if not isinstance(object_class_ids, list):
1979 raise TypeError("For multiple primitive data labels, object_class_ids must be a list")
1980 if len(primitive_data_labels) != len(object_class_ids):
1981 raise ValueError("primitive_data_labels and object_class_ids must have the same length")
1982 if not all(isinstance(lbl, str) and lbl.strip() for lbl in primitive_data_labels):
1983 raise TypeError("All primitive data labels must be non-empty strings")
1984 if not all(isinstance(cid, int) for cid in object_class_ids):
1985 raise TypeError("All object class IDs must be integers")
1986
1987 radiation_wrapper.writeImageBoundingBoxesVector(
1988 self.radiation_model, camera_label, primitive_data_labels,
1989 object_class_ids, image_file, classes_txt_file, image_path)
1990 logger.info(f"Image bounding boxes written for {len(primitive_data_labels)} primitive data labels")
1991 else:
1992 raise TypeError("primitive_data_labels must be a string or list of strings")
1993
1994 # Handle object data labels
1995 elif object_data_labels is not None:
1996 if isinstance(object_data_labels, str):
1997 # Single label
1998 if not isinstance(object_class_ids, int):
1999 raise TypeError("For single object data label, object_class_ids must be an integer")
2000 radiation_wrapper.writeImageBoundingBoxes_ObjectData(
2001 self.radiation_model, camera_label, object_data_labels,
2002 object_class_ids, image_file, classes_txt_file, image_path)
2003 logger.info(f"Image bounding boxes written for object data: {object_data_labels}")
2004
2005 elif isinstance(object_data_labels, list):
2006 # Multiple labels
2007 if not isinstance(object_class_ids, list):
2008 raise TypeError("For multiple object data labels, object_class_ids must be a list")
2009 if len(object_data_labels) != len(object_class_ids):
2010 raise ValueError("object_data_labels and object_class_ids must have the same length")
2011 if not all(isinstance(lbl, str) and lbl.strip() for lbl in object_data_labels):
2012 raise TypeError("All object data labels must be non-empty strings")
2013 if not all(isinstance(cid, int) for cid in object_class_ids):
2014 raise TypeError("All object class IDs must be integers")
2015
2016 radiation_wrapper.writeImageBoundingBoxes_ObjectDataVector(
2017 self.radiation_model, camera_label, object_data_labels,
2018 object_class_ids, image_file, classes_txt_file, image_path)
2019 logger.info(f"Image bounding boxes written for {len(object_data_labels)} object data labels")
2020 else:
2021 raise TypeError("object_data_labels must be a string or list of strings")
2022
2023 @require_plugin('radiation', 'write image segmentation masks')
2024 def writeImageSegmentationMasks(self, camera_label: str,
2025 primitive_data_labels=None, object_data_labels=None,
2026 object_class_ids=None, json_filename: str = "",
2027 image_file: str = "", append_file: bool = False):
2028 """
2029 Write image segmentation masks in COCO JSON format.
2030
2031 Supports both single and multiple data labels. Either provide primitive_data_labels
2032 or object_data_labels, not both.
2033
2034 Args:
2035 camera_label: Camera label
2036 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
2037 object_data_labels: Single object data label (str) or list of object data labels
2038 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
2039 json_filename: JSON output filename
2040 image_file: Image filename
2041 append_file: Whether to append to existing JSON file
2042
2043 Raises:
2044 RadiationModelError: If segmentation mask writing fails
2045 TypeError: If parameters have incorrect types
2046 ValueError: If both primitive and object data labels are provided, or neither
2047 """
2048 # Validate exclusive parameter usage
2049 if primitive_data_labels is not None and object_data_labels is not None:
2050 raise ValueError("Cannot specify both primitive_data_labels and object_data_labels")
2051 if primitive_data_labels is None and object_data_labels is None:
2052 raise ValueError("Must specify either primitive_data_labels or object_data_labels")
2053
2054 # Validate common parameters
2055 if not isinstance(camera_label, str) or not camera_label.strip():
2056 raise TypeError("Camera label must be a non-empty string")
2057 if not isinstance(json_filename, str) or not json_filename.strip():
2058 raise TypeError("JSON filename must be a non-empty string")
2059 if not isinstance(image_file, str) or not image_file.strip():
2060 raise TypeError("Image file must be a non-empty string")
2061 if not isinstance(append_file, bool):
2062 raise TypeError("append_file must be a boolean")
2063
2064 # Handle primitive data labels
2065 if primitive_data_labels is not None:
2066 if isinstance(primitive_data_labels, str):
2067 # Single label
2068 if not isinstance(object_class_ids, int):
2069 raise TypeError("For single primitive data label, object_class_ids must be an integer")
2070 radiation_wrapper.writeImageSegmentationMasks(
2071 self.radiation_model, camera_label, primitive_data_labels,
2072 object_class_ids, json_filename, image_file, append_file)
2073 logger.info(f"Image segmentation masks written for primitive data: {primitive_data_labels}")
2074
2075 elif isinstance(primitive_data_labels, list):
2076 # Multiple labels
2077 if not isinstance(object_class_ids, list):
2078 raise TypeError("For multiple primitive data labels, object_class_ids must be a list")
2079 if len(primitive_data_labels) != len(object_class_ids):
2080 raise ValueError("primitive_data_labels and object_class_ids must have the same length")
2081 if not all(isinstance(lbl, str) and lbl.strip() for lbl in primitive_data_labels):
2082 raise TypeError("All primitive data labels must be non-empty strings")
2083 if not all(isinstance(cid, int) for cid in object_class_ids):
2084 raise TypeError("All object class IDs must be integers")
2085
2086 radiation_wrapper.writeImageSegmentationMasksVector(
2087 self.radiation_model, camera_label, primitive_data_labels,
2088 object_class_ids, json_filename, image_file, append_file)
2089 logger.info(f"Image segmentation masks written for {len(primitive_data_labels)} primitive data labels")
2090 else:
2091 raise TypeError("primitive_data_labels must be a string or list of strings")
2092
2093 # Handle object data labels
2094 elif object_data_labels is not None:
2095 if isinstance(object_data_labels, str):
2096 # Single label
2097 if not isinstance(object_class_ids, int):
2098 raise TypeError("For single object data label, object_class_ids must be an integer")
2099 radiation_wrapper.writeImageSegmentationMasks_ObjectData(
2100 self.radiation_model, camera_label, object_data_labels,
2101 object_class_ids, json_filename, image_file, append_file)
2102 logger.info(f"Image segmentation masks written for object data: {object_data_labels}")
2103
2104 elif isinstance(object_data_labels, list):
2105 # Multiple labels
2106 if not isinstance(object_class_ids, list):
2107 raise TypeError("For multiple object data labels, object_class_ids must be a list")
2108 if len(object_data_labels) != len(object_class_ids):
2109 raise ValueError("object_data_labels and object_class_ids must have the same length")
2110 if not all(isinstance(lbl, str) and lbl.strip() for lbl in object_data_labels):
2111 raise TypeError("All object data labels must be non-empty strings")
2112 if not all(isinstance(cid, int) for cid in object_class_ids):
2113 raise TypeError("All object class IDs must be integers")
2114
2115 radiation_wrapper.writeImageSegmentationMasks_ObjectDataVector(
2116 self.radiation_model, camera_label, object_data_labels,
2117 object_class_ids, json_filename, image_file, append_file)
2118 logger.info(f"Image segmentation masks written for {len(object_data_labels)} object data labels")
2119 else:
2120 raise TypeError("object_data_labels must be a string or list of strings")
2121
2122 @require_plugin('radiation', 'auto-calibrate camera image')
2123 def autoCalibrateCameraImage(self, camera_label: str, red_band_label: str,
2124 green_band_label: str, blue_band_label: str,
2125 output_file_path: str, print_quality_report: bool = False,
2126 algorithm: str = "MATRIX_3X3_AUTO",
2127 ccm_export_file_path: str = "") -> str:
2128 """
2129 Auto-calibrate camera image with color correction and return output filename.
2130
2131 Args:
2132 camera_label: Camera label
2133 red_band_label: Red band label
2134 green_band_label: Green band label
2135 blue_band_label: Blue band label
2136 output_file_path: Output file path
2137 print_quality_report: Whether to print quality report
2138 algorithm: Color correction algorithm ("DIAGONAL_ONLY", "MATRIX_3X3_AUTO", "MATRIX_3X3_FORCE")
2139 ccm_export_file_path: Path to export color correction matrix (optional)
2140
2141 Returns:
2142 Output filename string
2143
2144 Raises:
2145 RadiationModelError: If auto-calibration fails
2146 TypeError: If parameters have incorrect types
2147 ValueError: If algorithm is not valid
2148 """
2149 # Validate inputs
2150 if not isinstance(camera_label, str) or not camera_label.strip():
2151 raise TypeError("Camera label must be a non-empty string")
2152 if not isinstance(red_band_label, str) or not red_band_label.strip():
2153 raise TypeError("Red band label must be a non-empty string")
2154 if not isinstance(green_band_label, str) or not green_band_label.strip():
2155 raise TypeError("Green band label must be a non-empty string")
2156 if not isinstance(blue_band_label, str) or not blue_band_label.strip():
2157 raise TypeError("Blue band label must be a non-empty string")
2158 if not isinstance(output_file_path, str) or not output_file_path.strip():
2159 raise TypeError("Output file path must be a non-empty string")
2160 if not isinstance(print_quality_report, bool):
2161 raise TypeError("print_quality_report must be a boolean")
2162 if not isinstance(ccm_export_file_path, str):
2163 raise TypeError("ccm_export_file_path must be a string")
2164
2165 # Map algorithm string to integer (using MATRIX_3X3_AUTO = 1 as default)
2166 algorithm_map = {
2167 "DIAGONAL_ONLY": 0,
2168 "MATRIX_3X3_AUTO": 1,
2169 "MATRIX_3X3_FORCE": 2
2170 }
2171
2172 if algorithm not in algorithm_map:
2173 raise ValueError(f"Invalid algorithm: {algorithm}. Must be one of: {list(algorithm_map.keys())}")
2174
2175 algorithm_int = algorithm_map[algorithm]
2176
2177 filename = radiation_wrapper.autoCalibrateCameraImage(
2178 self.radiation_model, camera_label, red_band_label, green_band_label,
2179 blue_band_label, output_file_path, print_quality_report,
2180 algorithm_int, ccm_export_file_path)
2181
2182 logger.info(f"Auto-calibrated camera image written to: {filename}")
2183 return filename
2184
2185 def getPluginInfo(self) -> dict:
2186 """Get information about the radiation plugin."""
2187 registry = get_plugin_registry()
2188 return registry.get_plugin_capabilities('radiation')
__init__(self, date="", time="", UTC_offset=0.0, camera_height_m=0.0, camera_angle_deg=0.0, light_source="sunlight")
Agronomic properties derived from plant architecture data.
__init__(self, plant_species=None, plant_count=None, plant_height_m=None, plant_age_days=None, plant_stage=None, leaf_area_m2=None, weed_pressure="")
Camera intrinsic properties for metadata export.
__init__(self, height=512, width=512, channels=3, type="rgb", focal_length=50.0, aperture="f/2.8", sensor_width=35.0, sensor_height=24.0, model="generic", lens_make="", lens_model="", lens_specification="", exposure="auto", shutter_speed=0.008, white_balance="auto")
Image processing corrections applied to the image.
__init__(self, saturation_adjustment=1.0, brightness_adjustment=1.0, contrast_adjustment=1.0, color_space="linear")
Metadata for radiation camera image export (Helios v1.3.58+).
__init__(self, path="")
Initialize CameraMetadata with default values.
Camera properties for radiation model cameras.
__init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05, HFOV=20.0, FOV_aspect_ratio=0.0, lens_focal_length=0.05, sensor_width_mm=35.0, model="generic", lens_make="", lens_model="", lens_specification="", exposure="auto", shutter_speed=1.0/125.0, white_balance="auto")
Initialize camera properties with defaults matching C++ CameraProperties.
to_array(self)
Convert to array format expected by C++ interface.
Raised when RadiationModel operations fail.
High-level interface for radiation modeling and ray tracing.
setDirectRayCount(self, str band_label, int ray_count)
Set direct ray count for radiation band.
float integrateSpectrum(self, object_spectrum, float wavelength_min=None, float wavelength_max=None, int source_id=None, camera_spectrum=None)
Integrate spectrum with optional source/camera spectra and wavelength range.
__del__(self)
Destructor to ensure GPU resources freed even without 'with' statement.
getCameraPosition(self, str camera_label)
Get camera position.
disableMessages(self)
Disable RadiationModel status messages.
int addRectangleRadiationSource(self, position, size, rotation)
Add a rectangle (planar) radiation source.
interpolateSpectrumFromPrimitiveData(self, List[int] primitive_uuids, List[str] spectra_labels, List[float] values, str primitive_data_query_label, str primitive_data_radprop_label)
Interpolate spectral properties based on primitive data values.
runBand(self, band_label)
Run radiation simulation for single band or multiple bands.
setSourceSpectrum(self, source_id, spectrum)
Set radiation spectrum for source(s).
str writeCameraImage(self, str camera, List[str] bands, str imagefile_base, str image_path="./", int frame=-1, float flux_to_pixel_conversion=1.0)
Write camera image to file and return output filename.
List[float] getCameraPixelData(self, str camera_label, str band_label)
Get camera pixel data for specific band.
setCameraOrientation(self, str camera_label, direction)
Set camera orientation.
get_native_ptr(self)
Get native pointer for advanced operations.
addRadiationBand(self, str band_label, float wavelength_min=None, float wavelength_max=None)
Add radiation band with optional wavelength bounds.
setScatteringDepth(self, str label, int depth)
Set scattering depth for radiation band.
getSourcePosition(self, int source_id)
Get position of a radiation source.
addRadiationCameraFromLibrary(self, str camera_label, str library_camera_label, position, lookat, int antialiasing_samples=1, Optional[List[str]] band_labels=None)
Add radiation camera loading all properties from camera library.
updateCameraParameters(self, str camera_label, CameraProperties camera_properties)
Update camera parameters for an existing camera.
setCameraPosition(self, str camera_label, position)
Set camera position.
enableMessages(self)
Enable RadiationModel status messages.
setCameraSpectralResponse(self, str camera_label, str band_label, str global_data)
Set camera spectral response from global data.
interpolateSpectrumFromObjectData(self, List[int] object_ids, List[str] spectra_labels, List[float] values, str object_data_query_label, str primitive_data_radprop_label)
Interpolate spectral properties based on object data values.
setSourceFlux(self, source_id, str label, float flux)
Set source flux for single source or multiple sources.
writeImageBoundingBoxes(self, str camera_label, primitive_data_labels=None, object_data_labels=None, object_class_ids=None, str image_file="", str classes_txt_file="classes.txt", str image_path="./")
Write image bounding boxes for object detection training.
deleteRadiationSource(self, int source_id)
Delete a radiation source.
setSourcePosition(self, int source_id, position)
Set position of a radiation source.
setDiffuseRadiationExtinctionCoeff(self, str label, float K, peak_direction)
Set diffuse radiation extinction coefficient with directional bias.
setCameraPixelData(self, str camera_label, str band_label, List[float] pixel_data)
Set camera pixel data for specific band.
getCameraOrientation(self, str camera_label)
Get camera orientation.
enforcePeriodicBoundary(self, str boundary)
Enforce periodic boundary conditions.
enableCameraMetadata(self, camera_labels)
Enable automatic JSON metadata file writing for camera(s).
scaleSpectrumRandomly(self, str existing_label, str new_label, float min_scale, float max_scale)
Scale spectrum with random factor and store as new label.
setSourceSpectrumIntegral(self, int source_id, float source_integral, float wavelength_min=None, float wavelength_max=None)
Set source spectrum integral value.
setDiffuseRayCount(self, str band_label, int ray_count)
Set diffuse ray count for radiation band.
setCameraLookat(self, str camera_label, lookat)
Set camera lookat point.
int addDiskRadiationSource(self, position, float radius, rotation)
Add a disk (circular planar) radiation source.
getCameraLookat(self, str camera_label)
Get camera lookat point.
float getSourceFlux(self, int source_id, str label)
Get source flux for band.
blendSpectra(self, str new_label, List[str] spectrum_labels, List[float] weights)
Blend multiple spectra with specified weights.
float calculateGtheta(self, view_direction)
Calculate G-function (geometry factor) for given view direction.
int addSunSphereRadiationSource(self, float radius, float zenith, float azimuth, float position_scaling=1.0, float angular_width=0.53, float flux_scaling=1.0)
Add sun sphere radiation source.
bool doesBandExist(self, str label)
Check if a radiation band exists.
setCameraSpectralResponseFromLibrary(self, str camera_label, str camera_library_name)
Set camera spectral response from standard camera library.
setDiffuseSpectrumIntegral(self, float spectrum_integral, float wavelength_min=None, float wavelength_max=None, str band_label=None)
Set diffuse spectrum integral.
scaleSpectrum(self, str existing_label, new_label_or_scale, float scale_factor=None)
Scale spectrum in-place or to new label.
float getSkyEnergy(self)
Get total sky energy.
disableEmission(self, str label)
Disable emission for radiation band.
copyRadiationBand(self, str old_label, str new_label, float wavelength_min=None, float wavelength_max=None)
Copy existing radiation band to new label, optionally with new wavelength range.
enableEmission(self, str label)
Enable emission for radiation band.
addRadiationCamera(self, str camera_label, List[str] band_labels, position, lookat_or_direction, camera_properties=None, int antialiasing_samples=100)
Add a radiation camera to the simulation.
List[float] getTotalAbsorbedFlux(self)
Get total absorbed flux for all primitives.
int addSphereRadiationSource(self, position, float radius)
Add spherical radiation source.
float integrateSourceSpectrum(self, int source_id, float wavelength_min, float wavelength_max)
Integrate source spectrum over wavelength range.
__init__(self, Context context)
Initialize RadiationModel with graceful plugin handling.
__enter__(self)
Context manager entry.
optionalOutputPrimitiveData(self, str label)
Enable optional primitive data output.
str autoCalibrateCameraImage(self, str camera_label, str red_band_label, str green_band_label, str blue_band_label, str output_file_path, bool print_quality_report=False, str algorithm="MATRIX_3X3_AUTO", str ccm_export_file_path="")
Auto-calibrate camera image with color correction and return output filename.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
updateGeometry(self, Optional[List[int]] uuids=None)
Update geometry in radiation model.
setDiffuseSpectrum(self, band_label, str spectrum_label)
Set diffuse spectrum from global data label.
setMinScatterEnergy(self, str label, float energy)
Set minimum scatter energy for radiation band.
float getDiffuseFlux(self, str band_label)
Get diffuse flux for band.
setDiffuseRadiationFlux(self, str label, float flux)
Set diffuse radiation flux for band.
writeImageSegmentationMasks(self, str camera_label, primitive_data_labels=None, object_data_labels=None, object_class_ids=None, str json_filename="", str image_file="", bool append_file=False)
Write image segmentation masks in COCO JSON format.
writeCameraImageData(self, str camera, str band, str imagefile_base, str image_path="./", int frame=-1)
Write camera image data to file (ASCII format).
int addCollimatedRadiationSource(self, direction=None)
Add collimated radiation source.
List[str] getAllCameraLabels(self)
Get all camera labels.
str writeNormCameraImage(self, str camera, List[str] bands, str imagefile_base, str image_path="./", int frame=-1)
Write normalized camera image to file and return output filename.
dict getPluginInfo(self)
Get information about the radiation plugin.
blendSpectraRandomly(self, str new_label, List[str] spectrum_labels)
Blend multiple spectra with random weights.
getNativePtr(self)
Get native pointer for advanced operations.
_radiation_working_directory()
Context manager that temporarily changes working directory to where RadiationModel assets are located...