2High-level RadiationModel interface for PyHelios.
4This module provides a user-friendly interface to the radiation modeling
5capabilities with graceful plugin handling and informative error messages.
9from typing
import List, Optional
10from contextlib
import contextmanager
11from pathlib
import Path
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 validate_position_like, validate_direction_like, validate_size_like
21from .validation.plugin_decorators
import (
22 validate_radiation_band_params, validate_collimated_source_params, validate_sphere_source_params,
23 validate_sun_sphere_params, validate_get_source_flux_params,
24 validate_update_geometry_params, validate_run_band_params, validate_scattering_depth_params,
25 validate_min_scatter_energy_params
27from .Context
import Context
28from .assets
import get_asset_manager
30logger = logging.getLogger(__name__)
36 Context manager that temporarily changes working directory to where RadiationModel assets are located.
38 RadiationModel C++ code uses hardcoded relative paths like "plugins/radiation/" for GPU
39 backend files (SPIR-V shaders for Vulkan, PTX files for OptiX), expecting assets relative
40 to working directory. This manager temporarily changes to the build directory where assets
44 RuntimeError: If build directory or RadiationModel assets are not found, indicating a build system error.
48 asset_manager = get_asset_manager()
49 working_dir = asset_manager._get_helios_build_path()
51 if working_dir
and working_dir.exists():
52 radiation_assets = working_dir /
'plugins' /
'radiation'
55 current_dir = Path(__file__).parent
56 packaged_build = current_dir /
'assets' /
'build'
58 if packaged_build.exists():
59 working_dir = packaged_build
60 radiation_assets = working_dir /
'plugins' /
'radiation'
63 repo_root = current_dir.parent
64 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
65 working_dir = build_lib_dir.parent
66 radiation_assets = working_dir /
'plugins' /
'radiation'
68 if not build_lib_dir.exists():
70 f
"PyHelios build directory not found at {build_lib_dir}. "
71 f
"Run: python build_scripts/build_helios.py --plugins radiation"
74 if not radiation_assets.exists():
76 f
"RadiationModel assets not found at {radiation_assets}. "
77 f
"This indicates a build system error. The build script should copy shader/backend files to this location."
81 original_dir = os.getcwd()
84 logger.debug(f
"Changed working directory to {working_dir} for RadiationModel asset access")
87 os.chdir(original_dir)
88 logger.debug(f
"Restored working directory to {original_dir}")
92 """Raised when RadiationModel operations fail."""
98 Camera properties for radiation model cameras.
100 This class encapsulates the properties needed to configure a radiation camera,
101 providing sensible defaults and validation for camera parameters. Updated for
102 Helios v1.3.60 with camera_zoom support.
105 def __init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05,
106 HFOV=20.0, FOV_aspect_ratio=0.0, lens_focal_length=0.05,
107 sensor_width_mm=35.0, manufacturer="", model="generic", lens_make="",
108 lens_model="", lens_specification="", exposure="auto", shutter_speed=1.0/125.0,
109 white_balance="auto", camera_zoom=1.0):
111 Initialize camera properties with defaults matching C++ CameraProperties.
114 camera_resolution: Camera resolution as (width, height) tuple or list. Default: (512, 512)
115 focal_plane_distance: Distance from viewing plane to focal plane (working distance). Default: 1.0
116 lens_diameter: Diameter of camera lens (0 = pinhole camera). Default: 0.05
117 HFOV: Horizontal field of view in degrees. Default: 20.0
118 FOV_aspect_ratio: Ratio of horizontal to vertical FOV. Default: 0.0 (auto-calculate from resolution)
119 lens_focal_length: Camera lens optical focal length in meters (physical, not 35mm equiv). Default: 0.05 (50mm)
120 sensor_width_mm: Physical sensor width in mm. Default: 35.0 (full-frame)
121 manufacturer: Camera manufacturer (e.g., "Canon", "Nikon", "Apple"). In helios-core
122 v1.3.73 this maps to the EXIF Make tag (empty ⇒ "Helios"). NOTE: like the other
123 string fields (model, lens_make, etc.), this attribute is not yet plumbed through to
124 the native camera and currently has no effect on written images; the C++ default is
125 used. It is exposed for forward compatibility. Default: ""
126 model: Camera model name (e.g., "Nikon D700", "Canon EOS 5D"). Default: "generic"
127 lens_make: Lens manufacturer (e.g., "Canon", "Nikon"). Default: ""
128 lens_model: Lens model name (e.g., "AF-S NIKKOR 50mm f/1.8G"). Default: ""
129 lens_specification: Lens specification (e.g., "50mm f/1.8"). Default: ""
130 exposure: Exposure mode - "auto", "ISOXXX" (e.g., "ISO100"), or "manual". Default: "auto"
131 shutter_speed: Camera shutter speed in seconds (e.g., 0.008 for 1/125s). Default: 0.008 (1/125s)
132 white_balance: White balance mode - "auto" or "off". Default: "auto"
133 camera_zoom: Camera optical zoom multiplier. 1.0 = no zoom, 2.0 = 2x zoom.
134 Scales effective HFOV: effective_HFOV = HFOV / camera_zoom. Default: 1.0
138 if camera_resolution
is None:
139 self.camera_resolution = (512, 512)
141 if isinstance(camera_resolution, (list, tuple))
and len(camera_resolution) == 2:
142 self.camera_resolution = (int(camera_resolution[0]), int(camera_resolution[1]))
144 raise ValueError(
"camera_resolution must be a tuple or list of 2 integers")
148 if focal_plane_distance <= 0:
149 raise ValueError(
"focal_plane_distance must be greater than 0")
150 if lens_diameter < 0:
151 raise ValueError(
"lens_diameter must be non-negative")
152 if HFOV <= 0
or HFOV > 180:
153 raise ValueError(
"HFOV must be between 0 and 180 degrees")
154 if FOV_aspect_ratio < 0:
155 raise ValueError(
"FOV_aspect_ratio must be non-negative (0 = auto-calculate)")
156 if lens_focal_length <= 0:
157 raise ValueError(
"lens_focal_length must be greater than 0")
158 if sensor_width_mm <= 0:
159 raise ValueError(
"sensor_width_mm must be greater than 0")
160 if shutter_speed <= 0:
161 raise ValueError(
"shutter_speed must be greater than 0")
163 raise ValueError(
"camera_zoom must be greater than 0")
175 if not isinstance(manufacturer, str):
176 raise ValueError(
"manufacturer must be a string")
177 if not isinstance(model, str):
178 raise ValueError(
"model must be a string")
179 if not isinstance(lens_make, str):
180 raise ValueError(
"lens_make must be a string")
181 if not isinstance(lens_model, str):
182 raise ValueError(
"lens_model must be a string")
183 if not isinstance(lens_specification, str):
184 raise ValueError(
"lens_specification must be a string")
185 if not isinstance(exposure, str):
186 raise ValueError(
"exposure must be a string")
187 if not isinstance(white_balance, str):
188 raise ValueError(
"white_balance must be a string")
191 if exposure
not in [
"auto",
"manual"]
and not exposure.startswith(
"ISO"):
192 raise ValueError(
"exposure must be 'auto', 'manual', or 'ISOXXX' (e.g., 'ISO100')")
195 if white_balance
not in [
"auto",
"off"]:
196 raise ValueError(
"white_balance must be 'auto' or 'off'")
199 self.
model = str(model)
208 Convert to array format expected by C++ interface.
210 Note: Returns numeric fields only. String fields (model, lens_make, etc.) are
211 currently initialized with defaults in the C++ wrapper and cannot be set via
212 this interface. Use the upcoming camera library methods for full metadata control.
215 List of 10 float values: [resolution_x, resolution_y, focal_distance, lens_diameter,
216 HFOV, FOV_aspect_ratio, lens_focal_length, sensor_width_mm,
217 shutter_speed, camera_zoom]
220 float(self.camera_resolution[0]),
221 float(self.camera_resolution[1]),
233 return (f
"CameraProperties("
234 f
"camera_resolution={self.camera_resolution}, "
235 f
"focal_plane_distance={self.focal_plane_distance}, "
236 f
"lens_diameter={self.lens_diameter}, "
237 f
"HFOV={self.HFOV}, "
238 f
"FOV_aspect_ratio={self.FOV_aspect_ratio}, "
239 f
"lens_focal_length={self.lens_focal_length}, "
240 f
"sensor_width_mm={self.sensor_width_mm}, "
241 f
"manufacturer='{self.manufacturer}', "
242 f
"model='{self.model}', "
243 f
"lens_make='{self.lens_make}', "
244 f
"lens_model='{self.lens_model}', "
245 f
"lens_specification='{self.lens_specification}', "
246 f
"exposure='{self.exposure}', "
247 f
"shutter_speed={self.shutter_speed}, "
248 f
"white_balance='{self.white_balance}', "
249 f
"camera_zoom={self.camera_zoom})")
254 Camera properties for a solar-induced chlorophyll fluorescence (SIF) camera.
256 Extends :class:`CameraProperties` with two SIF-specific fields used by the
257 Fluspect-B emission pipeline introduced in helios-core v1.3.72. Image geometry,
258 resolution, exposure, and spectral-response handling are inherited from
259 :class:`CameraProperties` unchanged.
262 excitation_bin_width_nm: Excitation wavelength bin width in nm. Helios
263 auto-creates internal radiation bands spanning 400–750 nm at this
264 resolution to compute per-leaf APAR. Must be > 0. Default 10.0.
265 excitation_scattering_depth: Scattering depth for the auto-generated
266 excitation bands. ``0`` (default) treats every leaf hit as fully
267 absorbed. Set to ``>=1`` to include inter-leaf scattering at the
268 cost of additional excitation-band ray traces.
271 String fields inherited from :class:`CameraProperties` (``model``,
272 ``lens_make``, ``lens_model``, ``lens_specification``, ``exposure``,
273 ``white_balance``) are currently NOT plumbed through to the C++ camera
274 — the wrapper hard-codes ``"generic"`` / ``"auto"`` defaults. Set them
275 on this dataclass for self-documentation only; they will not affect
276 rendering. This matches the existing ``addRadiationCamera`` behaviour.
279 def __init__(self, excitation_bin_width_nm: float = 10.0,
280 excitation_scattering_depth: int = 0,
283 if excitation_bin_width_nm <= 0:
284 raise ValueError(
"excitation_bin_width_nm must be greater than 0")
285 if excitation_scattering_depth < 0:
286 raise ValueError(
"excitation_scattering_depth must be >= 0")
295 + f
", excitation_bin_width_nm={self.excitation_bin_width_nm}"
296 + f
", excitation_scattering_depth={self.excitation_scattering_depth})"
302 Metadata for radiation camera image export (Helios v1.3.58+).
304 This class encapsulates comprehensive metadata for camera images including
305 camera properties, location, acquisition settings, image processing, and
306 agronomic properties derived from plant architecture data.
309 class CameraPropertiesMetadata:
310 """Camera intrinsic properties for metadata export."""
311 def __init__(self, height=512, width=512, channels=3, type="rgb",
312 focal_length=50.0, aperture="f/2.8", sensor_width=35.0,
313 sensor_height=24.0, model="generic", lens_make="",
314 lens_model="", lens_specification="", exposure="auto",
315 shutter_speed=0.008, white_balance="auto"):
316 self.height = int(height)
317 self.width = int(width)
333 """Geographic location properties."""
335 self.latitude = float(latitude)
336 self.longitude = float(longitude)
339 """Image acquisition properties."""
340 def __init__(self, date="", time="", UTC_offset=0.0, camera_height_m=0.0,
341 camera_angle_deg=0.0, light_source="sunlight"):
342 self.date = str(date)
343 self.time = str(time)
344 self.UTC_offset = float(UTC_offset)
345 self.camera_height_m = float(camera_height_m)
346 self.camera_angle_deg = float(camera_angle_deg)
350 """Image processing corrections applied to the image."""
351 def __init__(self, saturation_adjustment=1.0, brightness_adjustment=1.0,
352 contrast_adjustment=1.0, color_space="linear"):
353 self.saturation_adjustment = float(saturation_adjustment)
354 self.brightness_adjustment = float(brightness_adjustment)
355 self.contrast_adjustment = float(contrast_adjustment)
356 self.color_space = str(color_space)
359 """Agronomic properties derived from plant architecture data."""
360 def __init__(self, plant_species=None, plant_count=None, plant_height_m=None,
361 plant_age_days=None, plant_stage=None, leaf_area_m2=None,
363 self.plant_species = plant_species
if plant_species
is not None else []
364 self.plant_count = plant_count
if plant_count
is not None else []
365 self.plant_height_m = plant_height_m
if plant_height_m
is not None else []
366 self.plant_age_days = plant_age_days
if plant_age_days
is not None else []
373 Initialize CameraMetadata with default values.
376 path: Full path to the associated image file. Default: ""
378 self.path = str(path)
379 self.camera_properties = self.CameraPropertiesMetadata()
380 self.location_properties = self.LocationProperties()
381 self.acquisition_properties = self.AcquisitionProperties()
382 self.image_processing = self.ImageProcessingProperties()
383 self.agronomic_properties = self.AgronomicProperties()
386 return (f
"CameraMetadata(path='{self.path}', "
387 f
"camera={self.camera_properties.model}, "
388 f
"resolution={self.camera_properties.width}x{self.camera_properties.height}, "
389 f
"location=({self.location_properties.latitude},{self.location_properties.longitude}))")
394 High-level interface for radiation modeling and ray tracing.
396 This class provides a user-friendly wrapper around the native Helios
397 radiation plugin with automatic plugin availability checking and
398 graceful error handling.
401 def __init__(self, context: Context):
403 Initialize RadiationModel with graceful plugin handling.
406 context: Helios Context instance
409 TypeError: If context is not a Context instance
410 RadiationModelError: If radiation plugin is not available
413 if not isinstance(context, Context):
414 raise TypeError(f
"RadiationModel requires a Context instance, got {type(context).__name__}")
420 registry = get_plugin_registry()
422 if not registry.is_plugin_available(
'radiation'):
424 plugin_info = registry.get_plugin_capabilities()
425 available_plugins = registry.get_available_plugins()
428 "RadiationModel requires the 'radiation' plugin which is not available.\n\n"
429 "The radiation plugin provides GPU-accelerated ray tracing with runtime\n"
430 "backend auto-detection (OptiX 8 -> OptiX 6 -> Vulkan).\n"
431 "System requirements (at least one backend):\n"
432 "- Vulkan: Vulkan loader library (macOS/Linux); no extra packages on Windows\n"
433 "- OptiX 8.1: NVIDIA GPU with driver >= 560 and CUDA 12.0+\n"
434 "- OptiX 6.5: NVIDIA GPU with driver < 560 and CUDA 9.0+\n\n"
435 "To enable radiation modeling:\n"
436 "1. Build PyHelios with radiation plugin:\n"
437 " build_scripts/build_helios --plugins radiation\n"
438 "2. Or build with multiple plugins:\n"
439 " build_scripts/build_helios --plugins radiation,visualizer,weberpenntree\n"
440 f
"\nCurrently available plugins: {available_plugins}"
444 alternatives = registry.suggest_alternatives(
'radiation')
446 error_msg += f
"\n\nAlternative plugins available: {alternatives}"
447 error_msg +=
"\nConsider using energybalance or leafoptics for thermal modeling."
454 self.
radiation_model = radiation_wrapper.createRadiationModel(context.getNativePtr())
457 "Failed to create RadiationModel instance. "
458 "This may indicate a problem with the native library or GPU initialization."
460 logger.info(
"RadiationModel created successfully")
462 except Exception
as e:
466 """Context manager entry."""
469 def __exit__(self, exc_type, exc_value, traceback):
470 """Context manager exit with proper cleanup."""
474 logger.debug(
"RadiationModel destroyed successfully")
475 except Exception
as e:
476 logger.warning(f
"Error destroying RadiationModel: {e}")
481 """Destructor to ensure GPU resources freed even without 'with' statement."""
482 if hasattr(self,
'radiation_model')
and self.
radiation_model is not None:
486 except Exception
as e:
488 warnings.warn(f
"Error in RadiationModel.__del__: {e}")
491 """Get native pointer for advanced operations."""
495 """Get native pointer for advanced operations. (Legacy naming for compatibility)"""
498 @require_plugin('radiation', 'disable status messages')
500 """Disable RadiationModel status messages."""
503 @require_plugin('radiation', 'enable status messages')
505 """Enable RadiationModel status messages."""
508 @require_plugin('radiation', 'add radiation band')
509 def addRadiationBand(self, band_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
511 Add radiation band with optional wavelength bounds.
514 band_label: Name/label for the radiation band
515 wavelength_min: Optional minimum wavelength (μm)
516 wavelength_max: Optional maximum wavelength (μm)
519 validate_band_label(band_label,
"band_label",
"addRadiationBand")
520 if wavelength_min
is not None and wavelength_max
is not None:
521 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"addRadiationBand")
522 radiation_wrapper.addRadiationBandWithWavelengths(self.
radiation_model, band_label, wavelength_min, wavelength_max)
523 logger.debug(f
"Added radiation band {band_label}: {wavelength_min}-{wavelength_max} μm")
526 logger.debug(f
"Added radiation band: {band_label}")
528 @require_plugin('radiation', 'copy radiation band')
529 @validate_radiation_band_params
530 def copyRadiationBand(self, old_label: str, new_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
532 Copy existing radiation band to new label, optionally with new wavelength range.
535 old_label: Existing band label to copy
536 new_label: New label for the copied band
537 wavelength_min: Optional minimum wavelength for new band (μm)
538 wavelength_max: Optional maximum wavelength for new band (μm)
541 >>> # Copy band with same wavelength range
542 >>> radiation.copyRadiationBand("SW", "SW_copy")
544 >>> # Copy band with different wavelength range
545 >>> radiation.copyRadiationBand("full_spectrum", "PAR", 400, 700)
547 if wavelength_min
is not None and wavelength_max
is not None:
548 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"copyRadiationBand")
550 radiation_wrapper.copyRadiationBand(self.
radiation_model, old_label, new_label, wavelength_min, wavelength_max)
551 if wavelength_min
is not None:
552 logger.debug(f
"Copied radiation band {old_label} to {new_label} with wavelengths {wavelength_min}-{wavelength_max} μm")
554 logger.debug(f
"Copied radiation band {old_label} to {new_label}")
556 @require_plugin('radiation', 'add radiation source')
557 @validate_collimated_source_params
560 Add collimated radiation source.
563 direction: Optional direction vector. Can be tuple (x, y, z), vec3, or None for default direction.
568 if direction
is None:
569 source_id = radiation_wrapper.addCollimatedRadiationSourceDefault(self.
radiation_model)
572 if hasattr(direction,
'x')
and hasattr(direction,
'y')
and hasattr(direction,
'z'):
574 x, y, z = direction.x, direction.y, direction.z
575 elif hasattr(direction,
'radius')
and hasattr(direction,
'elevation')
and hasattr(direction,
'azimuth'):
579 elevation = direction.elevation
580 azimuth = direction.azimuth
581 x = r * math.cos(elevation) * math.cos(azimuth)
582 y = r * math.cos(elevation) * math.sin(azimuth)
583 z = r * math.sin(elevation)
587 if len(direction) != 3:
588 raise TypeError(f
"Direction must be a 3-element tuple, vec3, or SphericalCoord, got {type(direction).__name__} with {len(direction)} elements")
590 except (TypeError, AttributeError):
592 raise TypeError(f
"Direction must be a tuple, vec3, or SphericalCoord, got {type(direction).__name__}")
593 source_id = radiation_wrapper.addCollimatedRadiationSourceVec3(self.
radiation_model, x, y, z)
595 logger.debug(f
"Added collimated radiation source: ID {source_id}")
598 @require_plugin('radiation', 'add spherical radiation source')
599 @validate_sphere_source_params
602 Add spherical radiation source.
605 position: Position of the source. Can be tuple (x, y, z) or vec3.
606 radius: Radius of the spherical source
611 validate_position_like(position,
"position",
"addSphereRadiationSource")
613 if hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z'):
614 x, y, z = position.x, position.y, position.z
617 source_id = radiation_wrapper.addSphereRadiationSource(self.
radiation_model, x, y, z, radius)
618 logger.debug(f
"Added sphere radiation source: ID {source_id} at ({x}, {y}, {z}) with radius {radius}")
621 @require_plugin('radiation', 'add sun radiation source')
622 @validate_sun_sphere_params
624 position_scaling: float = 1.0, angular_width: float = 0.53,
625 flux_scaling: float = 1.0) -> int:
627 Add sun sphere radiation source.
630 radius: Radius of the sun sphere
631 zenith: Zenith angle (degrees)
632 azimuth: Azimuth angle (degrees)
633 position_scaling: Position scaling factor
634 angular_width: Angular width of the sun (degrees)
635 flux_scaling: Flux scaling factor
640 source_id = radiation_wrapper.addSunSphereRadiationSource(
641 self.
radiation_model, radius, zenith, azimuth, position_scaling, angular_width, flux_scaling
643 logger.debug(f
"Added sun radiation source: ID {source_id}")
646 @require_plugin('radiation', 'set source position')
649 Set position of a radiation source.
651 Allows dynamic repositioning of radiation sources during simulation,
652 useful for time-series modeling or moving light sources.
655 source_id: ID of the radiation source
656 position: New position as vec3, SphericalCoord, or list/tuple [x, y, z]
659 >>> source_id = radiation.addCollimatedRadiationSource()
660 >>> radiation.setSourcePosition(source_id, [10, 20, 30])
661 >>> from pyhelios.types import vec3
662 >>> radiation.setSourcePosition(source_id, vec3(15, 25, 35))
664 if not isinstance(source_id, int)
or source_id < 0:
665 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
666 validate_direction_like(position,
"position",
"setSourcePosition")
667 radiation_wrapper.setSourcePosition(self.
radiation_model, source_id, position)
668 logger.debug(f
"Updated position for radiation source {source_id}")
670 @require_plugin('radiation', 'add rectangle radiation source')
673 Add a rectangle (planar) radiation source.
675 Rectangle sources are ideal for modeling artificial lighting such as
676 LED panels, grow lights, or window light sources.
679 position: Center position as vec3 or list [x, y, z]
680 size: Rectangle dimensions as vec2 or list [width, height]
681 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
687 >>> from pyhelios.types import vec3, vec2
688 >>> source_id = radiation.addRectangleRadiationSource(
689 ... position=vec3(0, 0, 5),
691 ... rotation=vec3(0, 0, 0)
693 >>> radiation.setSourceFlux(source_id, "PAR", 500.0)
695 validate_position_like(position,
"position",
"addRectangleRadiationSource")
696 validate_size_like(size,
"size",
"addRectangleRadiationSource")
697 validate_position_like(rotation,
"rotation",
"addRectangleRadiationSource")
698 return radiation_wrapper.addRectangleRadiationSource(self.
radiation_model, position, size, rotation)
700 @require_plugin('radiation', 'add disk radiation source')
703 Add a disk (circular planar) radiation source.
705 Disk sources are useful for modeling circular light sources such as
706 spotlights, circular LED arrays, or solar simulators.
709 position: Center position as vec3 or list [x, y, z]
711 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
717 >>> from pyhelios.types import vec3
718 >>> source_id = radiation.addDiskRadiationSource(
719 ... position=vec3(0, 0, 5),
721 ... rotation=vec3(0, 0, 0)
723 >>> radiation.setSourceFlux(source_id, "PAR", 300.0)
725 validate_position_like(position,
"position",
"addDiskRadiationSource")
726 validate_position_like(rotation,
"rotation",
"addDiskRadiationSource")
728 raise ValueError(f
"Radius must be positive, got {radius}")
729 return radiation_wrapper.addDiskRadiationSource(self.
radiation_model, position, radius, rotation)
732 @require_plugin('radiation', 'manage source spectrum')
735 Set radiation spectrum for source(s).
737 Spectral distributions define how radiation intensity varies with wavelength,
738 essential for realistic modeling of different light sources (sunlight, LEDs, etc.).
741 source_id: Source ID (int) or list of source IDs
743 - Spectrum data as list of (wavelength, value) tuples
744 - Global data label string
747 >>> # Define custom LED spectrum
749 ... (400, 0.0), (450, 0.3), (500, 0.8),
750 ... (550, 0.5), (600, 0.2), (700, 0.0)
752 >>> radiation.setSourceSpectrum(source_id, led_spectrum)
754 >>> # Use predefined spectrum from global data
755 >>> radiation.setSourceSpectrum(source_id, "D65_illuminant")
757 >>> # Apply same spectrum to multiple sources
758 >>> radiation.setSourceSpectrum([src1, src2, src3], led_spectrum)
760 radiation_wrapper.setSourceSpectrum(self.
radiation_model, source_id, spectrum)
761 logger.debug(f
"Set spectrum for source(s) {source_id}")
763 @require_plugin('radiation', 'configure source spectrum')
765 wavelength_min: float =
None, wavelength_max: float =
None):
767 Set source spectrum integral value.
769 Normalizes the spectrum so that its integral equals the specified value,
770 useful for calibrating source intensity.
774 source_integral: Target integral value
775 wavelength_min: Optional minimum wavelength for integration range
776 wavelength_max: Optional maximum wavelength for integration range
779 >>> radiation.setSourceSpectrumIntegral(source_id, 1000.0)
780 >>> radiation.setSourceSpectrumIntegral(source_id, 500.0, 400, 700) # PAR range
782 if not isinstance(source_id, int)
or source_id < 0:
783 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
784 if source_integral < 0:
785 raise ValueError(f
"Source integral must be non-negative, got {source_integral}")
787 radiation_wrapper.setSourceSpectrumIntegral(self.
radiation_model, source_id, source_integral,
788 wavelength_min, wavelength_max)
789 logger.debug(f
"Set spectrum integral for source {source_id}: {source_integral}")
792 @require_plugin('radiation', 'integrate spectrum')
794 wavelength_max: float =
None, source_id: int =
None,
795 camera_spectrum=
None) -> float:
797 Integrate spectrum with optional source/camera spectra and wavelength range.
799 This unified method handles multiple integration scenarios:
800 - Basic: Total spectrum integration
801 - Range: Integration over wavelength range
802 - Source: Integration weighted by source spectrum
803 - Camera: Integration weighted by camera spectral response
804 - Full: Integration with both source and camera spectra
807 object_spectrum: Object spectrum as list of (wavelength, value) tuples/vec2
808 wavelength_min: Optional minimum wavelength for integration range
809 wavelength_max: Optional maximum wavelength for integration range
810 source_id: Optional source ID for source spectrum weighting
811 camera_spectrum: Optional camera spectrum for camera response weighting
817 >>> leaf_reflectance = [(400, 0.1), (500, 0.4), (600, 0.6), (700, 0.5)]
819 >>> # Total integration
820 >>> total = radiation.integrateSpectrum(leaf_reflectance)
822 >>> # PAR range (400-700nm)
823 >>> par = radiation.integrateSpectrum(leaf_reflectance, 400, 700)
825 >>> # With source spectrum
826 >>> source_weighted = radiation.integrateSpectrum(
827 ... leaf_reflectance, 400, 700, source_id=sun_source
830 >>> # With camera response
831 >>> camera_response = [(400, 0.2), (550, 1.0), (700, 0.3)]
832 >>> camera_weighted = radiation.integrateSpectrum(
833 ... leaf_reflectance, camera_spectrum=camera_response
836 return radiation_wrapper.integrateSpectrum(self.
radiation_model, object_spectrum,
837 wavelength_min, wavelength_max,
838 source_id, camera_spectrum)
840 @require_plugin('radiation', 'integrate source spectrum')
843 Integrate source spectrum over wavelength range.
847 wavelength_min: Minimum wavelength
848 wavelength_max: Maximum wavelength
851 Integrated source spectrum value
854 >>> par_flux = radiation.integrateSourceSpectrum(source_id, 400, 700)
856 if not isinstance(source_id, int)
or source_id < 0:
857 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
858 return radiation_wrapper.integrateSourceSpectrum(self.
radiation_model, source_id,
859 wavelength_min, wavelength_max)
862 @require_plugin('radiation', 'scale spectrum')
863 def scaleSpectrum(self, existing_label: str, new_label_or_scale, scale_factor: float =
None):
865 Scale spectrum in-place or to new label.
867 Useful for adjusting spectrum intensities or creating variations of
868 existing spectra for sensitivity analysis.
870 Supports two call patterns:
871 - scaleSpectrum("label", scale) -> scales in-place
872 - scaleSpectrum("existing", "new", scale) -> creates new scaled spectrum
875 existing_label: Existing global data label
876 new_label_or_scale: Either new label string (if creating new) or scale factor (if in-place)
877 scale_factor: Scale factor (required only if new_label_or_scale is a string)
880 >>> # In-place scaling
881 >>> radiation.scaleSpectrum("leaf_reflectance", 1.2)
883 >>> # Create new scaled spectrum
884 >>> radiation.scaleSpectrum("leaf_reflectance", "scaled_leaf", 1.5)
886 if not isinstance(existing_label, str)
or not existing_label.strip():
887 raise ValueError(
"Existing label must be a non-empty string")
890 new_label_or_scale, scale_factor)
891 logger.debug(f
"Scaled spectrum '{existing_label}'")
893 @require_plugin('radiation', 'scale spectrum randomly')
895 min_scale: float, max_scale: float):
897 Scale spectrum with random factor and store as new label.
899 Useful for creating stochastic variations in spectral properties for
900 Monte Carlo simulations or uncertainty quantification.
903 existing_label: Existing global data label
904 new_label: New global data label for scaled spectrum
905 min_scale: Minimum scale factor
906 max_scale: Maximum scale factor
909 >>> # Create random variation of leaf reflectance
910 >>> radiation.scaleSpectrumRandomly("leaf_base", "leaf_variant", 0.8, 1.2)
912 if not isinstance(existing_label, str)
or not existing_label.strip():
913 raise ValueError(
"Existing label must be a non-empty string")
914 if not isinstance(new_label, str)
or not new_label.strip():
915 raise ValueError(
"New label must be a non-empty string")
916 if min_scale >= max_scale:
917 raise ValueError(f
"min_scale ({min_scale}) must be less than max_scale ({max_scale})")
919 radiation_wrapper.scaleSpectrumRandomly(self.
radiation_model, existing_label, new_label,
920 min_scale, max_scale)
921 logger.debug(f
"Scaled spectrum '{existing_label}' randomly to '{new_label}'")
923 @require_plugin('radiation', 'blend spectra')
924 def blendSpectra(self, new_label: str, spectrum_labels: List[str], weights: List[float]):
926 Blend multiple spectra with specified weights.
928 Creates weighted combination of spectra, useful for mixing material properties
929 or creating composite light sources.
932 new_label: New global data label for blended spectrum
933 spectrum_labels: List of spectrum labels to blend
934 weights: List of weights (must sum to reasonable values, same length as labels)
937 >>> # Mix two leaf types (70% type A, 30% type B)
938 >>> radiation.blendSpectra("mixed_leaf",
939 ... ["leaf_type_a", "leaf_type_b"],
943 if not isinstance(new_label, str)
or not new_label.strip():
944 raise ValueError(
"New label must be a non-empty string")
945 if len(spectrum_labels) != len(weights):
946 raise ValueError(f
"Number of labels ({len(spectrum_labels)}) must match number of weights ({len(weights)})")
947 if not spectrum_labels:
948 raise ValueError(
"At least one spectrum label required")
950 radiation_wrapper.blendSpectra(self.
radiation_model, new_label, spectrum_labels, weights)
951 logger.debug(f
"Blended {len(spectrum_labels)} spectra into '{new_label}'")
953 @require_plugin('radiation', 'blend spectra randomly')
956 Blend multiple spectra with random weights.
958 Creates random combinations of spectra, useful for generating diverse
959 material properties in stochastic simulations.
962 new_label: New global data label for blended spectrum
963 spectrum_labels: List of spectrum labels to blend
966 >>> # Create random mixture of leaf spectra
967 >>> radiation.blendSpectraRandomly("random_leaf",
968 ... ["young_leaf", "mature_leaf", "senescent_leaf"]
971 if not isinstance(new_label, str)
or not new_label.strip():
972 raise ValueError(
"New label must be a non-empty string")
973 if not spectrum_labels:
974 raise ValueError(
"At least one spectrum label required")
976 radiation_wrapper.blendSpectraRandomly(self.
radiation_model, new_label, spectrum_labels)
977 logger.debug(f
"Blended {len(spectrum_labels)} spectra randomly into '{new_label}'")
980 @require_plugin('radiation', 'interpolate spectrum from data')
982 spectra_labels: List[str], values: List[float],
983 primitive_data_query_label: str,
984 primitive_data_radprop_label: str):
986 Interpolate spectral properties based on primitive data values.
988 Automatically assigns spectra to primitives by interpolating between
989 reference spectra based on continuous data values (e.g., age, moisture, etc.).
992 primitive_uuids: List of primitive UUIDs to assign spectra
993 spectra_labels: List of reference spectrum labels
994 values: List of data values corresponding to each spectrum
995 primitive_data_query_label: Primitive data label containing query values
996 primitive_data_radprop_label: Primitive data label to store assigned spectra
999 >>> # Assign leaf reflectance based on age
1000 >>> leaf_patches = context.getAllUUIDs("patch")
1001 >>> radiation.interpolateSpectrumFromPrimitiveData(
1002 ... primitive_uuids=leaf_patches,
1003 ... spectra_labels=["young_leaf", "mature_leaf", "old_leaf"],
1004 ... values=[0.0, 50.0, 100.0], # Days since emergence
1005 ... primitive_data_query_label="leaf_age",
1006 ... primitive_data_radprop_label="reflectance"
1009 if not isinstance(primitive_uuids, (list, tuple))
or not primitive_uuids:
1010 raise ValueError(
"Primitive UUIDs must be a non-empty list")
1011 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
1012 raise ValueError(
"Spectra labels must be a non-empty list")
1013 if not isinstance(values, (list, tuple))
or not values:
1014 raise ValueError(
"Values must be a non-empty list")
1015 if len(spectra_labels) != len(values):
1016 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
1018 radiation_wrapper.interpolateSpectrumFromPrimitiveData(
1020 primitive_data_query_label, primitive_data_radprop_label
1022 logger.debug(f
"Interpolated spectra for {len(primitive_uuids)} primitives")
1024 @require_plugin('radiation', 'interpolate spectrum from object data')
1026 spectra_labels: List[str], values: List[float],
1027 object_data_query_label: str,
1028 primitive_data_radprop_label: str):
1030 Interpolate spectral properties based on object data values.
1032 Automatically assigns spectra to object primitives by interpolating between
1033 reference spectra based on continuous object-level data values.
1036 object_ids: List of object IDs
1037 spectra_labels: List of reference spectrum labels
1038 values: List of data values corresponding to each spectrum
1039 object_data_query_label: Object data label containing query values
1040 primitive_data_radprop_label: Primitive data label to store assigned spectra
1043 >>> # Assign tree reflectance based on health index
1044 >>> tree_ids = [tree1_id, tree2_id, tree3_id]
1045 >>> radiation.interpolateSpectrumFromObjectData(
1046 ... object_ids=tree_ids,
1047 ... spectra_labels=["healthy_tree", "stressed_tree", "diseased_tree"],
1048 ... values=[1.0, 0.5, 0.0], # Health index
1049 ... object_data_query_label="health_index",
1050 ... primitive_data_radprop_label="reflectance"
1053 if not isinstance(object_ids, (list, tuple))
or not object_ids:
1054 raise ValueError(
"Object IDs must be a non-empty list")
1055 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
1056 raise ValueError(
"Spectra labels must be a non-empty list")
1057 if not isinstance(values, (list, tuple))
or not values:
1058 raise ValueError(
"Values must be a non-empty list")
1059 if len(spectra_labels) != len(values):
1060 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
1062 radiation_wrapper.interpolateSpectrumFromObjectData(
1064 object_data_query_label, primitive_data_radprop_label
1066 logger.debug(f
"Interpolated spectra for {len(object_ids)} objects")
1068 @require_plugin('radiation', 'set ray count')
1070 """Set direct ray count for radiation band."""
1071 validate_band_label(band_label,
"band_label",
"setDirectRayCount")
1072 validate_ray_count(ray_count,
"ray_count",
"setDirectRayCount")
1073 radiation_wrapper.setDirectRayCount(self.
radiation_model, band_label, ray_count)
1075 @require_plugin('radiation', 'set ray count')
1077 """Set diffuse ray count for radiation band."""
1078 validate_band_label(band_label,
"band_label",
"setDiffuseRayCount")
1079 validate_ray_count(ray_count,
"ray_count",
"setDiffuseRayCount")
1080 radiation_wrapper.setDiffuseRayCount(self.
radiation_model, band_label, ray_count)
1082 @require_plugin('radiation', 'set radiation flux')
1084 """Set diffuse radiation flux for band."""
1085 validate_band_label(label,
"label",
"setDiffuseRadiationFlux")
1086 validate_flux_value(flux,
"flux",
"setDiffuseRadiationFlux")
1087 radiation_wrapper.setDiffuseRadiationFlux(self.
radiation_model, label, flux)
1089 @require_plugin('radiation', 'configure diffuse radiation')
1092 Set diffuse radiation extinction coefficient with directional bias.
1094 Models directionally-biased diffuse radiation (e.g., sky radiation with zenith peak).
1098 K: Extinction coefficient
1099 peak_direction: Peak direction as vec3, SphericalCoord, or list [x, y, z]
1102 >>> from pyhelios.types import vec3
1103 >>> radiation.setDiffuseRadiationExtinctionCoeff("SW", 0.5, vec3(0, 0, 1))
1105 validate_band_label(label,
"label",
"setDiffuseRadiationExtinctionCoeff")
1107 raise ValueError(f
"Extinction coefficient must be non-negative, got {K}")
1108 validate_direction_like(peak_direction,
"peak_direction",
"setDiffuseRadiationExtinctionCoeff")
1109 radiation_wrapper.setDiffuseRadiationExtinctionCoeff(self.
radiation_model, label, K, peak_direction)
1110 logger.debug(f
"Set diffuse extinction coefficient for band '{label}': K={K}")
1112 @require_plugin('radiation', 'query diffuse flux')
1115 Get diffuse flux for band.
1118 band_label: Band label
1124 >>> flux = radiation.getDiffuseFlux("SW")
1126 validate_band_label(band_label,
"band_label",
"getDiffuseFlux")
1127 return radiation_wrapper.getDiffuseFlux(self.
radiation_model, band_label)
1129 @require_plugin('radiation', 'configure diffuse spectrum')
1132 Set diffuse spectrum from global data label.
1135 band_label: Band label (string) or list of band labels
1136 spectrum_label: Spectrum global data label
1139 >>> radiation.setDiffuseSpectrum("SW", "sky_spectrum")
1140 >>> radiation.setDiffuseSpectrum(["SW", "NIR"], "sky_spectrum")
1142 if isinstance(band_label, str):
1143 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrum")
1145 for label
in band_label:
1146 validate_band_label(label,
"band_label",
"setDiffuseSpectrum")
1147 if not isinstance(spectrum_label, str)
or not spectrum_label.strip():
1148 raise ValueError(
"Spectrum label must be a non-empty string")
1150 radiation_wrapper.setDiffuseSpectrum(self.
radiation_model, band_label, spectrum_label)
1151 logger.debug(f
"Set diffuse spectrum for band(s) {band_label}")
1153 @require_plugin('radiation', 'configure diffuse spectrum')
1155 wavelength_max: float =
None, band_label: str =
None):
1157 Set diffuse spectrum integral.
1160 spectrum_integral: Integral value
1161 wavelength_min: Optional minimum wavelength
1162 wavelength_max: Optional maximum wavelength
1163 band_label: Optional specific band label (None for all bands)
1166 >>> radiation.setDiffuseSpectrumIntegral(1000.0) # All bands
1167 >>> radiation.setDiffuseSpectrumIntegral(500.0, 400, 700, band_label="PAR") # Specific band
1169 if spectrum_integral < 0:
1170 raise ValueError(f
"Spectrum integral must be non-negative, got {spectrum_integral}")
1171 if band_label
is not None:
1172 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrumIntegral")
1174 radiation_wrapper.setDiffuseSpectrumIntegral(self.
radiation_model, spectrum_integral,
1175 wavelength_min, wavelength_max, band_label)
1176 logger.debug(f
"Set diffuse spectrum integral: {spectrum_integral}")
1178 @require_plugin('radiation', 'set source flux')
1179 def setSourceFlux(self, source_id, label: str, flux: float):
1180 """Set source flux for single source or multiple sources."""
1181 validate_band_label(label,
"label",
"setSourceFlux")
1182 validate_flux_value(flux,
"flux",
"setSourceFlux")
1184 if isinstance(source_id, (list, tuple)):
1186 validate_source_id_list(list(source_id),
"source_id",
"setSourceFlux")
1187 radiation_wrapper.setSourceFluxMultiple(self.
radiation_model, source_id, label, flux)
1190 validate_source_id(source_id,
"source_id",
"setSourceFlux")
1191 radiation_wrapper.setSourceFlux(self.
radiation_model, source_id, label, flux)
1194 @require_plugin('radiation', 'get source flux')
1195 @validate_get_source_flux_params
1196 def getSourceFlux(self, source_id: int, label: str) -> float:
1197 """Get source flux for band."""
1198 return radiation_wrapper.getSourceFlux(self.
radiation_model, source_id, label)
1200 @require_plugin('radiation', 'update geometry')
1201 @validate_update_geometry_params
1204 Update geometry in radiation model.
1207 uuids: Optional list of specific UUIDs to update. If None, updates all geometry.
1211 logger.debug(
"Updated all geometry in radiation model")
1214 logger.debug(f
"Updated {len(uuids)} geometry UUIDs in radiation model")
1216 @require_plugin('radiation', 'run radiation simulation')
1217 @validate_run_band_params
1218 def runBand(self, band_label):
1220 Run radiation simulation for single band or multiple bands.
1222 PERFORMANCE NOTE: When simulating multiple radiation bands, it is HIGHLY RECOMMENDED
1223 to run all bands in a single call (e.g., runBand(["PAR", "NIR", "SW"])) rather than
1224 sequential single-band calls. This provides significant computational efficiency gains
1227 - GPU ray tracing setup is done once for all bands
1228 - Scene geometry acceleration structures are reused
1229 - GPU kernel launches are batched together
1230 - Memory transfers between CPU/GPU are minimized
1233 # EFFICIENT - Single call for multiple bands
1234 radiation.runBand(["PAR", "NIR", "SW"])
1236 # INEFFICIENT - Sequential single-band calls
1237 radiation.runBand("PAR")
1238 radiation.runBand("NIR")
1239 radiation.runBand("SW")
1242 band_label: Single band name (str) or list of band names for multi-band simulation
1244 if isinstance(band_label, (list, tuple)):
1246 for lbl
in band_label:
1247 if not isinstance(lbl, str):
1248 raise TypeError(f
"Band labels must be strings, got {type(lbl).__name__}")
1250 logger.info(f
"Completed radiation simulation for bands: {band_label}")
1253 if not isinstance(band_label, str):
1254 raise TypeError(f
"Band label must be a string, got {type(band_label).__name__}")
1256 logger.info(f
"Completed radiation simulation for band: {band_label}")
1259 @require_plugin('radiation', 'get simulation results')
1261 """Get total absorbed flux for all primitives."""
1262 results = radiation_wrapper.getTotalAbsorbedFlux(self.
radiation_model)
1263 logger.debug(f
"Retrieved absorbed flux data for {len(results)} primitives")
1267 @require_plugin('radiation', 'check band existence')
1270 Check if a radiation band exists.
1273 label: Name/label of the radiation band to check
1276 True if band exists, False otherwise
1279 >>> radiation.addRadiationBand("SW")
1280 >>> radiation.doesBandExist("SW")
1282 >>> radiation.doesBandExist("nonexistent")
1285 validate_band_label(label,
"label",
"doesBandExist")
1289 @require_plugin('radiation', 'manage radiation sources')
1292 Delete a radiation source.
1295 source_id: ID of the radiation source to delete
1298 >>> source_id = radiation.addCollimatedRadiationSource()
1299 >>> radiation.deleteRadiationSource(source_id)
1301 if not isinstance(source_id, int)
or source_id < 0:
1302 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1303 radiation_wrapper.deleteRadiationSource(self.
radiation_model, source_id)
1304 logger.debug(f
"Deleted radiation source {source_id}")
1306 @require_plugin('radiation', 'query radiation sources')
1309 Get position of a radiation source.
1312 source_id: ID of the radiation source
1315 vec3 position of the source
1318 >>> source_id = radiation.addCollimatedRadiationSource()
1319 >>> position = radiation.getSourcePosition(source_id)
1320 >>> print(f"Source at: {position}")
1322 if not isinstance(source_id, int)
or source_id < 0:
1323 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1324 position_list = radiation_wrapper.getSourcePosition(self.
radiation_model, source_id)
1325 from .wrappers.DataTypes
import vec3
1326 return vec3(position_list[0], position_list[1], position_list[2])
1329 @require_plugin('radiation', 'get sky energy')
1332 Get total sky energy.
1335 Total sky energy value
1338 >>> energy = radiation.getSkyEnergy()
1339 >>> print(f"Sky energy: {energy}")
1343 @require_plugin('radiation', 'calculate G-function')
1346 Calculate G-function (geometry factor) for given view direction.
1348 The G-function describes the geometric relationship between leaf area
1349 distribution and viewing direction, important for canopy radiation modeling.
1352 view_direction: View direction as vec3 or list/tuple [x, y, z]
1358 >>> from pyhelios.types import vec3
1359 >>> g_value = radiation.calculateGtheta(vec3(0, 0, 1))
1360 >>> print(f"G-function: {g_value}")
1362 validate_position_like(view_direction,
"view_direction",
"calculateGtheta")
1364 return radiation_wrapper.calculateGtheta(self.
radiation_model, context_ptr, view_direction)
1366 @require_plugin('radiation', 'configure output data')
1369 Enable optional primitive data output.
1372 label: Name/label of the primitive data to output
1375 >>> radiation.optionalOutputPrimitiveData("temperature")
1377 validate_band_label(label,
"label",
"optionalOutputPrimitiveData")
1378 radiation_wrapper.optionalOutputPrimitiveData(self.
radiation_model, label)
1379 logger.debug(f
"Enabled optional output for primitive data: {label}")
1381 @require_plugin('radiation', 'configure boundary conditions')
1384 Enforce periodic boundary conditions.
1386 Periodic boundaries are useful for large-scale simulations to reduce
1387 edge effects by wrapping radiation at domain boundaries.
1390 boundary: Boundary specification string (e.g., "xy", "xyz", "x", "y", "z")
1393 >>> radiation.enforcePeriodicBoundary("xy")
1395 if not isinstance(boundary, str)
or not boundary:
1396 raise ValueError(
"Boundary specification must be a non-empty string")
1397 radiation_wrapper.enforcePeriodicBoundary(self.
radiation_model, boundary)
1398 logger.debug(f
"Enforced periodic boundary: {boundary}")
1401 @require_plugin('radiation', 'configure radiation simulation')
1402 @validate_scattering_depth_params
1404 """Set scattering depth for radiation band."""
1405 radiation_wrapper.setScatteringDepth(self.
radiation_model, label, depth)
1407 @require_plugin('radiation', 'configure radiation simulation')
1408 @validate_min_scatter_energy_params
1410 """Set minimum scatter energy for radiation band."""
1411 radiation_wrapper.setMinScatterEnergy(self.
radiation_model, label, energy)
1413 @require_plugin('radiation', 'configure radiation emission')
1415 """Disable emission for radiation band."""
1416 validate_band_label(label,
"label",
"disableEmission")
1419 @require_plugin('radiation', 'configure radiation emission')
1421 """Enable emission for radiation band."""
1422 validate_band_label(label,
"label",
"enableEmission")
1429 @require_plugin('radiation', 'add radiation camera')
1430 def addRadiationCamera(self, camera_label: str, band_labels: List[str], position, lookat_or_direction,
1431 camera_properties=
None, antialiasing_samples: int = 100):
1433 Add a radiation camera to the simulation.
1436 camera_label: Unique label string for the camera
1437 band_labels: List of radiation band labels for the camera
1438 position: Camera position as vec3 object
1439 lookat_or_direction: Either:
1440 - Lookat point as vec3 object
1441 - SphericalCoord for viewing direction
1442 camera_properties: CameraProperties instance or None for defaults
1443 antialiasing_samples: Number of antialiasing samples (default: 100)
1446 ValidationError: If parameters are invalid or have wrong types
1447 RadiationModelError: If camera creation fails
1450 >>> from pyhelios import vec3, CameraProperties
1451 >>> # Create camera looking at origin from above
1452 >>> camera_props = CameraProperties(camera_resolution=(1024, 1024))
1453 >>> radiation_model.addRadiationCamera("main_camera", ["red", "green", "blue"],
1454 ... position=vec3(0, 0, 5), lookat_or_direction=vec3(0, 0, 0),
1455 ... camera_properties=camera_props)
1458 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1459 from .wrappers.DataTypes
import SphericalCoord, vec3, make_vec3
1460 from .validation.plugins
import validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
1463 validated_label = validate_camera_label(camera_label,
"camera_label",
"addRadiationCamera")
1464 validated_bands = validate_band_labels_list(band_labels,
"band_labels",
"addRadiationCamera")
1465 validated_samples = validate_antialiasing_samples(antialiasing_samples,
"antialiasing_samples",
"addRadiationCamera")
1468 if not isinstance(position, vec3):
1469 raise TypeError(
"position must be a vec3 object. Use vec3(x, y, z) to create one.")
1470 validated_position = position
1473 if isinstance(lookat_or_direction, SphericalCoord):
1474 validated_direction = lookat_or_direction
1475 elif isinstance(lookat_or_direction, vec3):
1476 validated_direction = lookat_or_direction
1478 raise TypeError(
"lookat_or_direction must be a vec3 or SphericalCoord object. Use vec3(x, y, z) or SphericalCoord to create one.")
1481 if camera_properties
is None:
1486 if hasattr(validated_direction,
'radius')
and hasattr(validated_direction,
'elevation'):
1488 direction_coords = validated_direction.to_list()
1491 if len(direction_coords) < 4:
1492 raise ValueError(
"SphericalCoord must expose radius, elevation, zenith, and azimuth")
1493 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[3]
1495 radiation_wrapper.addRadiationCameraSpherical(
1499 validated_position.x, validated_position.y, validated_position.z,
1500 radius, elevation, azimuth,
1501 camera_properties.to_array(),
1506 radiation_wrapper.addRadiationCameraVec3(
1510 validated_position.x, validated_position.y, validated_position.z,
1511 validated_direction.x, validated_direction.y, validated_direction.z,
1512 camera_properties.to_array(),
1516 except Exception
as e:
1519 @require_plugin('radiation', 'add SIF camera')
1520 def addSIFCamera(self, camera_label: str, emission_band_labels: List[str], position,
1521 lookat_or_direction, camera_properties=
None, antialiasing_samples: int = 100):
1523 Add a solar-induced chlorophyll fluorescence (SIF) camera.
1525 Each band in ``emission_band_labels`` must already exist (added via
1526 :meth:`addRadiationBand`); those bands are flagged internally as SIF-emitting and
1527 use the Fluspect-B leaf-fluorescence kernel for emission instead of Stefan-Boltzmann.
1528 Helios auto-creates internal radiation bands covering 400-750 nm at the resolution
1529 specified by ``camera_properties.excitation_bin_width_nm``.
1532 camera_label: Unique label for the camera.
1533 emission_band_labels: List of pre-existing radiation band labels to drive
1535 position: Camera position as a ``vec3``.
1536 lookat_or_direction: Either a ``vec3`` lookat point or a ``SphericalCoord``
1538 camera_properties: :class:`SIFCameraProperties` instance. If ``None`` defaults
1539 are used (10 nm excitation bins, no excitation scattering).
1540 antialiasing_samples: Antialiasing samples per pixel (>= 1, default 100).
1543 RadiationModelError: If the underlying SIF camera cannot be added (e.g.,
1544 an emission band was already bound to a different excitation bin width).
1545 NotImplementedError: If running against helios-core older than v1.3.72.
1547 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1548 from .wrappers.DataTypes
import SphericalCoord, vec3
1549 from .validation.plugins
import (
1550 validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
1553 validated_label = validate_camera_label(camera_label,
"camera_label",
"addSIFCamera")
1554 validated_bands = validate_band_labels_list(emission_band_labels,
"emission_band_labels",
"addSIFCamera")
1555 validated_samples = validate_antialiasing_samples(antialiasing_samples,
"antialiasing_samples",
"addSIFCamera")
1557 if not isinstance(position, vec3):
1558 raise TypeError(
"position must be a vec3 object. Use vec3(x, y, z) to create one.")
1560 if not isinstance(lookat_or_direction, (vec3, SphericalCoord)):
1561 raise TypeError(
"lookat_or_direction must be a vec3 or SphericalCoord object.")
1563 if camera_properties
is None:
1565 elif not isinstance(camera_properties, SIFCameraProperties):
1567 "camera_properties must be a SIFCameraProperties instance "
1568 "(use SIFCameraProperties(...) — not the plain CameraProperties)."
1572 if isinstance(lookat_or_direction, SphericalCoord):
1575 direction_coords = lookat_or_direction.to_list()
1576 if len(direction_coords) < 4:
1577 raise ValueError(
"SphericalCoord must expose radius, elevation, zenith, and azimuth")
1578 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[3]
1579 radiation_wrapper.addSIFCameraSpherical(
1583 position.x, position.y, position.z,
1584 radius, elevation, azimuth,
1585 camera_properties.to_array(),
1586 camera_properties.excitation_bin_width_nm,
1587 camera_properties.excitation_scattering_depth,
1591 radiation_wrapper.addSIFCameraVec3(
1595 position.x, position.y, position.z,
1596 lookat_or_direction.x, lookat_or_direction.y, lookat_or_direction.z,
1597 camera_properties.to_array(),
1598 camera_properties.excitation_bin_width_nm,
1599 camera_properties.excitation_scattering_depth,
1602 except Exception
as e:
1605 @require_plugin('radiation', 'check SIF camera registration')
1608 Return True if the camera was registered via :meth:`addSIFCamera` (vs. ``addRadiationCamera``).
1610 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1611 if not isinstance(camera_label, str)
or not camera_label.strip():
1612 raise ValueError(
"Camera label must be a non-empty string")
1613 return radiation_wrapper.isSIFCamera(self.
radiation_model, camera_label)
1615 @require_plugin('radiation', 'manage camera position')
1618 Set camera position.
1620 Allows dynamic camera repositioning during simulation, useful for
1621 time-series captures or multi-view imaging.
1624 camera_label: Camera label string
1625 position: Camera position as vec3 or list [x, y, z]
1628 >>> radiation.setCameraPosition("cam1", [0, 0, 10])
1629 >>> from pyhelios.types import vec3
1630 >>> radiation.setCameraPosition("cam1", vec3(5, 5, 10))
1632 if not isinstance(camera_label, str)
or not camera_label.strip():
1633 raise ValueError(
"Camera label must be a non-empty string")
1634 validate_position_like(position,
"position",
"setCameraPosition")
1635 radiation_wrapper.setCameraPosition(self.
radiation_model, camera_label, position)
1636 logger.debug(f
"Updated camera '{camera_label}' position")
1638 @require_plugin('radiation', 'query camera position')
1641 Get camera position.
1644 camera_label: Camera label string
1647 vec3 position of the camera
1650 >>> position = radiation.getCameraPosition("cam1")
1651 >>> print(f"Camera at: {position}")
1653 if not isinstance(camera_label, str)
or not camera_label.strip():
1654 raise ValueError(
"Camera label must be a non-empty string")
1655 position_list = radiation_wrapper.getCameraPosition(self.
radiation_model, camera_label)
1656 from .wrappers.DataTypes
import vec3
1657 return vec3(position_list[0], position_list[1], position_list[2])
1659 @require_plugin('radiation', 'manage camera lookat')
1662 Set camera lookat point.
1665 camera_label: Camera label string
1666 lookat: Lookat point as vec3 or list [x, y, z]
1669 >>> radiation.setCameraLookat("cam1", [0, 0, 0])
1671 if not isinstance(camera_label, str)
or not camera_label.strip():
1672 raise ValueError(
"Camera label must be a non-empty string")
1673 validate_position_like(lookat,
"lookat",
"setCameraLookat")
1674 radiation_wrapper.setCameraLookat(self.
radiation_model, camera_label, lookat)
1675 logger.debug(f
"Updated camera '{camera_label}' lookat point")
1677 @require_plugin('radiation', 'query camera lookat')
1680 Get camera lookat point.
1683 camera_label: Camera label string
1689 >>> lookat = radiation.getCameraLookat("cam1")
1690 >>> print(f"Camera looking at: {lookat}")
1692 if not isinstance(camera_label, str)
or not camera_label.strip():
1693 raise ValueError(
"Camera label must be a non-empty string")
1694 lookat_list = radiation_wrapper.getCameraLookat(self.
radiation_model, camera_label)
1695 from .wrappers.DataTypes
import vec3
1696 return vec3(lookat_list[0], lookat_list[1], lookat_list[2])
1698 @require_plugin('radiation', 'manage camera orientation')
1701 Set camera orientation.
1704 camera_label: Camera label string
1705 direction: View direction as vec3, SphericalCoord, or list [x, y, z]
1708 >>> radiation.setCameraOrientation("cam1", [0, 0, 1])
1709 >>> from pyhelios.types import SphericalCoord
1710 >>> radiation.setCameraOrientation("cam1", SphericalCoord(1.0, 45.0, 90.0))
1712 if not isinstance(camera_label, str)
or not camera_label.strip():
1713 raise ValueError(
"Camera label must be a non-empty string")
1714 validate_direction_like(direction,
"direction",
"setCameraOrientation")
1715 radiation_wrapper.setCameraOrientation(self.
radiation_model, camera_label, direction)
1716 logger.debug(f
"Updated camera '{camera_label}' orientation")
1718 @require_plugin('radiation', 'query camera orientation')
1721 Get camera orientation.
1724 camera_label: Camera label string
1727 SphericalCoord orientation [radius, elevation, azimuth]
1730 >>> orientation = radiation.getCameraOrientation("cam1")
1731 >>> print(f"Camera orientation: {orientation}")
1733 if not isinstance(camera_label, str)
or not camera_label.strip():
1734 raise ValueError(
"Camera label must be a non-empty string")
1735 orientation_list = radiation_wrapper.getCameraOrientation(self.
radiation_model, camera_label)
1736 from .wrappers.DataTypes
import SphericalCoord
1737 return SphericalCoord(orientation_list[0], orientation_list[1], orientation_list[2])
1739 @require_plugin('radiation', 'query cameras')
1742 Get all camera labels.
1745 List of all camera label strings
1748 >>> cameras = radiation.getAllCameraLabels()
1749 >>> print(f"Available cameras: {cameras}")
1753 @require_plugin('radiation', 'configure camera spectral response')
1756 Set camera spectral response from global data.
1759 camera_label: Camera label
1760 band_label: Band label
1761 global_data: Global data label for spectral response curve
1764 >>> radiation.setCameraSpectralResponse("cam1", "red", "sensor_red_response")
1766 if not isinstance(camera_label, str)
or not camera_label.strip():
1767 raise ValueError(
"Camera label must be a non-empty string")
1768 validate_band_label(band_label,
"band_label",
"setCameraSpectralResponse")
1769 if not isinstance(global_data, str)
or not global_data.strip():
1770 raise ValueError(
"Global data label must be a non-empty string")
1772 radiation_wrapper.setCameraSpectralResponse(self.
radiation_model, camera_label, band_label, global_data)
1773 logger.debug(f
"Set spectral response for camera '{camera_label}', band '{band_label}'")
1775 @require_plugin('radiation', 'configure camera from library')
1778 Set camera spectral response from standard camera library.
1780 Uses pre-defined spectral response curves for common cameras.
1783 camera_label: Camera label
1784 camera_library_name: Standard camera name (e.g., "iPhone13", "NikonD850", "CanonEOS5D")
1787 >>> radiation.setCameraSpectralResponseFromLibrary("cam1", "iPhone13")
1789 if not isinstance(camera_label, str)
or not camera_label.strip():
1790 raise ValueError(
"Camera label must be a non-empty string")
1791 if not isinstance(camera_library_name, str)
or not camera_library_name.strip():
1792 raise ValueError(
"Camera library name must be a non-empty string")
1794 radiation_wrapper.setCameraSpectralResponseFromLibrary(self.
radiation_model, camera_label, camera_library_name)
1795 logger.debug(f
"Set camera '{camera_label}' response from library: {camera_library_name}")
1797 @require_plugin('radiation', 'get camera pixel data')
1800 Get camera pixel data for specific band.
1802 Retrieves raw pixel values for programmatic access and analysis.
1805 camera_label: Camera label
1806 band_label: Band label
1809 List of pixel values
1812 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1813 >>> print(f"Mean pixel value: {sum(pixels)/len(pixels)}")
1815 if not isinstance(camera_label, str)
or not camera_label.strip():
1816 raise ValueError(
"Camera label must be a non-empty string")
1817 validate_band_label(band_label,
"band_label",
"getCameraPixelData")
1819 return radiation_wrapper.getCameraPixelData(self.
radiation_model, camera_label, band_label)
1821 @require_plugin('radiation', 'set camera pixel data')
1824 Set camera pixel data for specific band.
1826 Allows programmatic modification of pixel values.
1829 camera_label: Camera label
1830 band_label: Band label
1831 pixel_data: List of pixel values
1834 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1835 >>> modified_pixels = [p * 1.2 for p in pixels] # Brighten by 20%
1836 >>> radiation.setCameraPixelData("cam1", "red", modified_pixels)
1838 if not isinstance(camera_label, str)
or not camera_label.strip():
1839 raise ValueError(
"Camera label must be a non-empty string")
1840 validate_band_label(band_label,
"band_label",
"setCameraPixelData")
1841 if not isinstance(pixel_data, (list, tuple)):
1842 raise ValueError(
"Pixel data must be a list or tuple")
1844 radiation_wrapper.setCameraPixelData(self.
radiation_model, camera_label, band_label, pixel_data)
1845 logger.debug(f
"Set pixel data for camera '{camera_label}', band '{band_label}': {len(pixel_data)} pixels")
1851 @require_plugin('radiation', 'add camera from library')
1853 position, lookat, antialiasing_samples: int = 1,
1854 band_labels: Optional[List[str]] =
None):
1856 Add radiation camera loading all properties from camera library.
1858 Loads camera intrinsic parameters (resolution, FOV, sensor size) and spectral
1859 response data from the camera library XML file. This is the recommended way to
1860 create realistic cameras with proper spectral responses.
1863 camera_label: Label for the camera instance
1864 library_camera_label: Label of camera in library (e.g., "Canon_20D", "iPhone11", "NikonD700")
1865 position: Camera position as vec3 or (x, y, z) tuple
1866 lookat: Lookat point as vec3 or (x, y, z) tuple
1867 antialiasing_samples: Number of ray samples per pixel. Default: 1
1868 band_labels: Optional custom band labels. If None, uses library defaults.
1871 RadiationModelError: If operation fails
1872 ValueError: If parameters are invalid
1875 Available cameras in plugins/radiation/camera_library/camera_library.xml include:
1876 - Canon_20D, Nikon_D700, Nikon_D50
1877 - iPhone11, iPhone12ProMAX
1878 - Additional cameras available in library
1881 >>> radiation.addRadiationCameraFromLibrary(
1882 ... camera_label="cam1",
1883 ... library_camera_label="iPhone11",
1884 ... position=(0, -5, 1),
1885 ... lookat=(0, 0, 0.5),
1886 ... antialiasing_samples=10
1889 validate_band_label(camera_label,
"camera_label",
"addRadiationCameraFromLibrary")
1890 validate_position_like(position,
"position",
"addRadiationCameraFromLibrary")
1891 validate_position_like(lookat,
"lookat",
"addRadiationCameraFromLibrary")
1894 radiation_wrapper.addRadiationCameraFromLibrary(
1896 position, lookat, antialiasing_samples, band_labels
1898 logger.info(f
"Added camera '{camera_label}' from library '{library_camera_label}'")
1899 except Exception
as e:
1902 @require_plugin('radiation', 'update camera parameters')
1905 Update camera parameters for an existing camera.
1907 Allows modification of camera properties after creation while preserving
1908 position, lookat direction, and spectral band configuration.
1911 camera_label: Label for the camera to update
1912 camera_properties: CameraProperties instance with new parameters
1915 RadiationModelError: If operation fails or camera doesn't exist
1916 ValueError: If parameters are invalid
1919 FOV_aspect_ratio is automatically recalculated from camera_resolution.
1920 Camera position and lookat are preserved.
1923 >>> props = CameraProperties(
1924 ... camera_resolution=(1920, 1080),
1926 ... lens_focal_length=0.085 # 85mm lens
1928 >>> radiation.updateCameraParameters("cam1", props)
1930 validate_band_label(camera_label,
"camera_label",
"updateCameraParameters")
1932 if not isinstance(camera_properties, CameraProperties):
1933 raise ValueError(
"camera_properties must be a CameraProperties instance")
1936 radiation_wrapper.updateCameraParameters(self.
radiation_model, camera_label, camera_properties)
1937 logger.debug(f
"Updated parameters for camera '{camera_label}'")
1938 except Exception
as e:
1941 @require_plugin('radiation', 'enable camera metadata')
1944 Enable automatic JSON metadata file writing for camera(s).
1946 When enabled, writeCameraImage() automatically creates a JSON metadata file
1947 alongside the image containing comprehensive camera and scene information.
1950 camera_labels: Single camera label (str) or list of camera labels (List[str])
1953 RadiationModelError: If operation fails
1954 ValueError: If parameters are invalid
1958 - Camera properties (model, lens, sensor specs)
1959 - Geographic location (latitude, longitude)
1960 - Acquisition settings (date, time, exposure, white balance)
1961 - Agronomic data (plant species, heights, phenology stages)
1964 >>> # Enable for single camera
1965 >>> radiation.enableCameraMetadata("cam1")
1967 >>> # Enable for multiple cameras
1968 >>> radiation.enableCameraMetadata(["cam1", "cam2", "cam3"])
1971 radiation_wrapper.enableCameraMetadata(self.
radiation_model, camera_labels)
1972 if isinstance(camera_labels, str):
1973 logger.info(f
"Enabled metadata for camera '{camera_labels}'")
1975 logger.info(f
"Enabled metadata for {len(camera_labels)} cameras")
1976 except Exception
as e:
1979 @require_plugin('radiation', 'write camera images')
1980 def writeCameraImage(self, camera: str, bands: List[str], imagefile_base: str,
1981 image_path: str =
"./", frame: int = -1,
1982 flux_to_pixel_conversion: float = 1.0) -> str:
1984 Write camera image to file and return output filename.
1987 camera: Camera label
1988 bands: List of band labels to include in the image
1989 imagefile_base: Base filename for output
1990 image_path: Output directory path (default: current directory)
1991 frame: Frame number to write (-1 for all frames)
1992 flux_to_pixel_conversion: Conversion factor from flux to pixel values
1995 Output filename string
1998 RadiationModelError: If camera image writing fails
1999 TypeError: If parameters have incorrect types
2002 if not isinstance(camera, str)
or not camera.strip():
2003 raise TypeError(
"Camera label must be a non-empty string")
2004 if not isinstance(bands, list)
or not bands:
2005 raise TypeError(
"Bands must be a non-empty list of strings")
2006 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
2007 raise TypeError(
"All band labels must be non-empty strings")
2008 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2009 raise TypeError(
"Image file base must be a non-empty string")
2010 if not isinstance(image_path, str):
2011 raise TypeError(
"Image path must be a string")
2012 if not isinstance(frame, int):
2013 raise TypeError(
"Frame must be an integer")
2014 if not isinstance(flux_to_pixel_conversion, (int, float))
or flux_to_pixel_conversion <= 0:
2015 raise TypeError(
"Flux to pixel conversion must be a positive number")
2017 filename = radiation_wrapper.writeCameraImage(
2019 image_path, frame, flux_to_pixel_conversion)
2021 logger.info(f
"Camera image written to: {filename}")
2024 @require_plugin('radiation', 'write normalized camera images')
2026 image_path: str =
"./", frame: int = -1) -> str:
2028 Write normalized camera image to file and return output filename.
2031 camera: Camera label
2032 bands: List of band labels to include in the image
2033 imagefile_base: Base filename for output
2034 image_path: Output directory path (default: current directory)
2035 frame: Frame number to write (-1 for all frames)
2038 Output filename string
2041 RadiationModelError: If normalized camera image writing fails
2042 TypeError: If parameters have incorrect types
2045 if not isinstance(camera, str)
or not camera.strip():
2046 raise TypeError(
"Camera label must be a non-empty string")
2047 if not isinstance(bands, list)
or not bands:
2048 raise TypeError(
"Bands must be a non-empty list of strings")
2049 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
2050 raise TypeError(
"All band labels must be non-empty strings")
2051 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2052 raise TypeError(
"Image file base must be a non-empty string")
2053 if not isinstance(image_path, str):
2054 raise TypeError(
"Image path must be a string")
2055 if not isinstance(frame, int):
2056 raise TypeError(
"Frame must be an integer")
2058 filename = radiation_wrapper.writeNormCameraImage(
2059 self.
radiation_model, camera, bands, imagefile_base, image_path, frame)
2061 logger.info(f
"Normalized camera image written to: {filename}")
2064 @require_plugin('radiation', 'write camera image data')
2066 image_path: str =
"./", frame: int = -1):
2068 Write camera image data to file (ASCII format).
2071 camera: Camera label
2073 imagefile_base: Base filename for output
2074 image_path: Output directory path (default: current directory)
2075 frame: Frame number to write (-1 for all frames)
2078 RadiationModelError: If camera image data writing fails
2079 TypeError: If parameters have incorrect types
2082 if not isinstance(camera, str)
or not camera.strip():
2083 raise TypeError(
"Camera label must be a non-empty string")
2084 if not isinstance(band, str)
or not band.strip():
2085 raise TypeError(
"Band label must be a non-empty string")
2086 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2087 raise TypeError(
"Image file base must be a non-empty string")
2088 if not isinstance(image_path, str):
2089 raise TypeError(
"Image path must be a string")
2090 if not isinstance(frame, int):
2091 raise TypeError(
"Frame must be an integer")
2093 radiation_wrapper.writeCameraImageData(
2094 self.
radiation_model, camera, band, imagefile_base, image_path, frame)
2096 logger.info(f
"Camera image data written for camera {camera}, band {band}")
2098 @require_plugin('radiation', 'write image bounding boxes')
2100 primitive_data_labels=
None, object_data_labels=
None,
2101 object_class_ids=
None, image_file: str =
"",
2102 classes_txt_file: str =
"classes.txt",
2103 image_path: str =
"./"):
2105 Write image bounding boxes for object detection training.
2107 Supports both single and multiple data labels. Either provide primitive_data_labels
2108 or object_data_labels, not both.
2111 camera_label: Camera label
2112 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
2113 object_data_labels: Single object data label (str) or list of object data labels
2114 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
2115 image_file: Image filename
2116 classes_txt_file: Classes definition file (default: "classes.txt")
2117 image_path: Image output path (default: current directory)
2120 RadiationModelError: If bounding box writing fails
2121 TypeError: If parameters have incorrect types
2122 ValueError: If both primitive and object data labels are provided, or neither
2125 if primitive_data_labels
is not None and object_data_labels
is not None:
2126 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
2127 if primitive_data_labels
is None and object_data_labels
is None:
2128 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
2131 if not isinstance(camera_label, str)
or not camera_label.strip():
2132 raise TypeError(
"Camera label must be a non-empty string")
2133 if not isinstance(image_file, str)
or not image_file.strip():
2134 raise TypeError(
"Image file must be a non-empty string")
2135 if not isinstance(classes_txt_file, str):
2136 raise TypeError(
"Classes txt file must be a string")
2137 if not isinstance(image_path, str):
2138 raise TypeError(
"Image path must be a string")
2141 if primitive_data_labels
is not None:
2142 if isinstance(primitive_data_labels, str):
2144 if not isinstance(object_class_ids, int):
2145 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
2146 radiation_wrapper.writeImageBoundingBoxes(
2148 object_class_ids, image_file, classes_txt_file, image_path)
2149 logger.info(f
"Image bounding boxes written for primitive data: {primitive_data_labels}")
2151 elif isinstance(primitive_data_labels, list):
2153 if not isinstance(object_class_ids, list):
2154 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
2155 if len(primitive_data_labels) != len(object_class_ids):
2156 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
2157 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
2158 raise TypeError(
"All primitive data labels must be non-empty strings")
2159 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2160 raise TypeError(
"All object class IDs must be integers")
2162 radiation_wrapper.writeImageBoundingBoxesVector(
2164 object_class_ids, image_file, classes_txt_file, image_path)
2165 logger.info(f
"Image bounding boxes written for {len(primitive_data_labels)} primitive data labels")
2167 raise TypeError(
"primitive_data_labels must be a string or list of strings")
2170 elif object_data_labels
is not None:
2171 if isinstance(object_data_labels, str):
2173 if not isinstance(object_class_ids, int):
2174 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2175 radiation_wrapper.writeImageBoundingBoxes_ObjectData(
2177 object_class_ids, image_file, classes_txt_file, image_path)
2178 logger.info(f
"Image bounding boxes written for object data: {object_data_labels}")
2180 elif isinstance(object_data_labels, list):
2182 if not isinstance(object_class_ids, list):
2183 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2184 if len(object_data_labels) != len(object_class_ids):
2185 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2186 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2187 raise TypeError(
"All object data labels must be non-empty strings")
2188 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2189 raise TypeError(
"All object class IDs must be integers")
2191 radiation_wrapper.writeImageBoundingBoxes_ObjectDataVector(
2193 object_class_ids, image_file, classes_txt_file, image_path)
2194 logger.info(f
"Image bounding boxes written for {len(object_data_labels)} object data labels")
2196 raise TypeError(
"object_data_labels must be a string or list of strings")
2198 @require_plugin('radiation', 'write image segmentation masks')
2200 primitive_data_labels=
None, object_data_labels=
None,
2201 object_class_ids=
None, json_filename: str =
"",
2202 image_file: str =
"", append_file: bool =
False):
2204 Write image segmentation masks in COCO JSON format.
2206 Supports both single and multiple data labels. Either provide primitive_data_labels
2207 or object_data_labels, not both.
2210 camera_label: Camera label
2211 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
2212 object_data_labels: Single object data label (str) or list of object data labels
2213 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
2214 json_filename: JSON output filename
2215 image_file: Image filename
2216 append_file: Whether to append to existing JSON file
2219 RadiationModelError: If segmentation mask writing fails
2220 TypeError: If parameters have incorrect types
2221 ValueError: If both primitive and object data labels are provided, or neither
2224 if primitive_data_labels
is not None and object_data_labels
is not None:
2225 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
2226 if primitive_data_labels
is None and object_data_labels
is None:
2227 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
2230 if not isinstance(camera_label, str)
or not camera_label.strip():
2231 raise TypeError(
"Camera label must be a non-empty string")
2232 if not isinstance(json_filename, str)
or not json_filename.strip():
2233 raise TypeError(
"JSON filename must be a non-empty string")
2234 if not isinstance(image_file, str)
or not image_file.strip():
2235 raise TypeError(
"Image file must be a non-empty string")
2236 if not isinstance(append_file, bool):
2237 raise TypeError(
"append_file must be a boolean")
2240 if primitive_data_labels
is not None:
2241 if isinstance(primitive_data_labels, str):
2243 if not isinstance(object_class_ids, int):
2244 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
2245 radiation_wrapper.writeImageSegmentationMasks(
2247 object_class_ids, json_filename, image_file, append_file)
2248 logger.info(f
"Image segmentation masks written for primitive data: {primitive_data_labels}")
2250 elif isinstance(primitive_data_labels, list):
2252 if not isinstance(object_class_ids, list):
2253 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
2254 if len(primitive_data_labels) != len(object_class_ids):
2255 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
2256 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
2257 raise TypeError(
"All primitive data labels must be non-empty strings")
2258 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2259 raise TypeError(
"All object class IDs must be integers")
2261 radiation_wrapper.writeImageSegmentationMasksVector(
2263 object_class_ids, json_filename, image_file, append_file)
2264 logger.info(f
"Image segmentation masks written for {len(primitive_data_labels)} primitive data labels")
2266 raise TypeError(
"primitive_data_labels must be a string or list of strings")
2269 elif object_data_labels
is not None:
2270 if isinstance(object_data_labels, str):
2272 if not isinstance(object_class_ids, int):
2273 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2274 radiation_wrapper.writeImageSegmentationMasks_ObjectData(
2276 object_class_ids, json_filename, image_file, append_file)
2277 logger.info(f
"Image segmentation masks written for object data: {object_data_labels}")
2279 elif isinstance(object_data_labels, list):
2281 if not isinstance(object_class_ids, list):
2282 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2283 if len(object_data_labels) != len(object_class_ids):
2284 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2285 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2286 raise TypeError(
"All object data labels must be non-empty strings")
2287 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2288 raise TypeError(
"All object class IDs must be integers")
2290 radiation_wrapper.writeImageSegmentationMasks_ObjectDataVector(
2292 object_class_ids, json_filename, image_file, append_file)
2293 logger.info(f
"Image segmentation masks written for {len(object_data_labels)} object data labels")
2295 raise TypeError(
"object_data_labels must be a string or list of strings")
2297 @require_plugin('radiation', 'auto-calibrate camera image')
2299 green_band_label: str, blue_band_label: str,
2300 output_file_path: str, print_quality_report: bool =
False,
2301 algorithm: str =
"MATRIX_3X3_AUTO",
2302 ccm_export_file_path: str =
"") -> str:
2304 Auto-calibrate camera image with color correction and return output filename.
2307 camera_label: Camera label
2308 red_band_label: Red band label
2309 green_band_label: Green band label
2310 blue_band_label: Blue band label
2311 output_file_path: Output file path
2312 print_quality_report: Whether to print quality report
2313 algorithm: Color correction algorithm ("DIAGONAL_ONLY", "MATRIX_3X3_AUTO", "MATRIX_3X3_FORCE")
2314 ccm_export_file_path: Path to export color correction matrix (optional)
2317 Output filename string
2320 RadiationModelError: If auto-calibration fails
2321 TypeError: If parameters have incorrect types
2322 ValueError: If algorithm is not valid
2325 if not isinstance(camera_label, str)
or not camera_label.strip():
2326 raise TypeError(
"Camera label must be a non-empty string")
2327 if not isinstance(red_band_label, str)
or not red_band_label.strip():
2328 raise TypeError(
"Red band label must be a non-empty string")
2329 if not isinstance(green_band_label, str)
or not green_band_label.strip():
2330 raise TypeError(
"Green band label must be a non-empty string")
2331 if not isinstance(blue_band_label, str)
or not blue_band_label.strip():
2332 raise TypeError(
"Blue band label must be a non-empty string")
2333 if not isinstance(output_file_path, str)
or not output_file_path.strip():
2334 raise TypeError(
"Output file path must be a non-empty string")
2335 if not isinstance(print_quality_report, bool):
2336 raise TypeError(
"print_quality_report must be a boolean")
2337 if not isinstance(ccm_export_file_path, str):
2338 raise TypeError(
"ccm_export_file_path must be a string")
2343 "MATRIX_3X3_AUTO": 1,
2344 "MATRIX_3X3_FORCE": 2
2347 if algorithm
not in algorithm_map:
2348 raise ValueError(f
"Invalid algorithm: {algorithm}. Must be one of: {list(algorithm_map.keys())}")
2350 algorithm_int = algorithm_map[algorithm]
2352 filename = radiation_wrapper.autoCalibrateCameraImage(
2354 blue_band_label, output_file_path, print_quality_report,
2355 algorithm_int, ccm_export_file_path)
2357 logger.info(f
"Auto-calibrated camera image written to: {filename}")
2361 """Get information about the radiation plugin."""
2362 registry = get_plugin_registry()
2363 return registry.get_plugin_capabilities(
'radiation')
2370 image_path: str =
"./", frame: int = -1):
2372 Write camera pixel data to an EXR file with lossless float compression.
2374 Preserves full floating-point precision unlike JPEG/PNG exports.
2377 camera: Camera label
2378 band: Band label (str) for single-band, or list of band labels for multi-band
2379 imagefile_base: Base filename for output
2380 image_path: Output directory path (default: current directory)
2381 frame: Frame number to append to filename (-1 to omit)
2384 RadiationModelError: If writing fails
2385 TypeError: If parameters have incorrect types
2387 if not isinstance(camera, str)
or not camera.strip():
2388 raise TypeError(
"Camera label must be a non-empty string")
2389 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2390 raise TypeError(
"Image file base must be a non-empty string")
2391 if not isinstance(image_path, str):
2392 raise TypeError(
"Image path must be a string")
2393 if not isinstance(frame, int):
2394 raise TypeError(
"Frame must be an integer")
2396 if isinstance(band, str):
2397 if not band.strip():
2398 raise TypeError(
"Band label must be a non-empty string")
2399 elif isinstance(band, (list, tuple)):
2401 raise ValueError(
"Band list cannot be empty")
2403 if not isinstance(b, str)
or not b.strip():
2404 raise TypeError(
"Each band label must be a non-empty string")
2406 raise TypeError(
"band must be a string or list of strings")
2408 radiation_wrapper.writeCameraImageDataEXR(
2409 self.
radiation_model, camera, band, imagefile_base, image_path, frame)
2412 image_path: str =
"./", frame: int = -1):
2414 Write depth image data to an ASCII text file.
2417 camera_label: Camera label
2418 imagefile_base: Base filename for output
2419 image_path: Output directory path (default: current directory)
2420 frame: Frame number to append to filename (-1 to omit)
2423 RadiationModelError: If writing fails
2424 TypeError: If parameters have incorrect types
2426 if not isinstance(camera_label, str)
or not camera_label.strip():
2427 raise TypeError(
"Camera label must be a non-empty string")
2428 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2429 raise TypeError(
"Image file base must be a non-empty string")
2430 if not isinstance(image_path, str):
2431 raise TypeError(
"Image path must be a string")
2432 if not isinstance(frame, int):
2433 raise TypeError(
"Frame must be an integer")
2435 radiation_wrapper.writeDepthImageData(
2436 self.
radiation_model, camera_label, imagefile_base, image_path, frame)
2439 image_path: str =
"./", frame: int = -1):
2441 Write depth image data to an EXR file with lossless float compression.
2443 Preserves full floating-point depth precision unlike ASCII or JPEG exports.
2446 camera_label: Camera label
2447 imagefile_base: Base filename for output
2448 image_path: Output directory path (default: current directory)
2449 frame: Frame number to append to filename (-1 to omit)
2452 RadiationModelError: If writing fails
2453 TypeError: If parameters have incorrect types
2455 if not isinstance(camera_label, str)
or not camera_label.strip():
2456 raise TypeError(
"Camera label must be a non-empty string")
2457 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2458 raise TypeError(
"Image file base must be a non-empty string")
2459 if not isinstance(image_path, str):
2460 raise TypeError(
"Image path must be a string")
2461 if not isinstance(frame, int):
2462 raise TypeError(
"Frame must be an integer")
2464 radiation_wrapper.writeDepthImageDataEXR(
2465 self.
radiation_model, camera_label, imagefile_base, image_path, frame)
2468 image_path: str =
"./", frame: int = -1):
2470 Write normalized depth image as grayscale JPEG.
2472 Depth values are normalized to the range [0, max_depth] for visualization.
2475 camera_label: Camera label
2476 imagefile_base: Base filename for output
2477 max_depth: Maximum depth value for normalization (e.g., sky depth)
2478 image_path: Output directory path (default: current directory)
2479 frame: Frame number to append to filename (-1 to omit)
2482 RadiationModelError: If writing fails
2483 TypeError: If parameters have incorrect types
2484 ValueError: If max_depth is not positive
2486 if not isinstance(camera_label, str)
or not camera_label.strip():
2487 raise TypeError(
"Camera label must be a non-empty string")
2488 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2489 raise TypeError(
"Image file base must be a non-empty string")
2490 if not isinstance(max_depth, (int, float)):
2491 raise TypeError(
"max_depth must be a number")
2493 raise ValueError(
"max_depth must be positive")
2494 if not isinstance(image_path, str):
2495 raise TypeError(
"Image path must be a string")
2496 if not isinstance(frame, int):
2497 raise TypeError(
"Frame must be an integer")
2499 radiation_wrapper.writeNormDepthImage(
2500 self.
radiation_model, camera_label, imagefile_base, float(max_depth), image_path, frame)
2508 Get the name of the active ray tracing backend.
2511 Backend name string (e.g., "OptiX 8.1", "Vulkan Compute")
2518 Probe whether any compiled-in GPU backend is available on this system.
2520 Probes backends in priority order (OptiX 8 -> OptiX 6 -> Vulkan) without
2521 constructing a full backend. Useful for checking GPU availability before
2522 creating a RadiationModel.
2525 True if at least one GPU backend is available
2527 return radiation_wrapper.probeAnyGPUBackend()
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, manufacturer="", model="generic", lens_make="", lens_model="", lens_specification="", exposure="auto", shutter_speed=1.0/125.0, white_balance="auto", camera_zoom=1.0)
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.
str getBackendName(self)
Get the name of the active ray tracing backend.
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.
writeCameraImageDataEXR(self, str camera, band, str imagefile_base, str image_path="./", int frame=-1)
Write camera pixel data to an EXR file with lossless float compression.
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.
writeDepthImageDataEXR(self, str camera_label, str imagefile_base, str image_path="./", int frame=-1)
Write depth image data to an EXR file with lossless float compression.
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.
writeNormDepthImage(self, str camera_label, str imagefile_base, float max_depth, str image_path="./", int frame=-1)
Write normalized depth image as grayscale JPEG.
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.
bool probeAnyGPUBackend()
Probe whether any compiled-in GPU backend is available on this system.
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.
bool isSIFCamera(self, str camera_label)
Return True if the camera was registered via :meth:addSIFCamera (vs.
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.
__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.
addSIFCamera(self, str camera_label, List[str] emission_band_labels, position, lookat_or_direction, camera_properties=None, int antialiasing_samples=100)
Add a solar-induced chlorophyll fluorescence (SIF) camera.
__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.
writeDepthImageData(self, str camera_label, str imagefile_base, str image_path="./", int frame=-1)
Write depth image data to an ASCII text file.
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.
Camera properties for a solar-induced chlorophyll fluorescence (SIF) camera.
__init__(self, float excitation_bin_width_nm=10.0, int excitation_scattering_depth=0, **kwargs)
excitation_scattering_depth
Scattering depth for the auto-generated.
excitation_bin_width_nm
Excitation wavelength bin width in nm.
_radiation_working_directory()
Context manager that temporarily changes working directory to where RadiationModel assets are located...