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
20from .validation.plugin_decorators
import (
21 validate_radiation_band_params, validate_collimated_source_params, validate_sphere_source_params,
22 validate_sun_sphere_params, validate_get_source_flux_params,
23 validate_update_geometry_params, validate_run_band_params, validate_scattering_depth_params,
24 validate_min_scatter_energy_params
26from .Context
import Context
27from .assets
import get_asset_manager
29logger = logging.getLogger(__name__)
35 Context manager that temporarily changes working directory to where RadiationModel assets are located.
37 RadiationModel C++ code uses hardcoded relative paths like "plugins/radiation/cuda_compile_ptx_generated_rayGeneration.cu.ptx"
38 expecting assets relative to working directory. This manager temporarily changes to the build directory
39 where assets are actually located.
42 RuntimeError: If build directory or RadiationModel assets are not found, indicating a build system error.
46 asset_manager = get_asset_manager()
47 working_dir = asset_manager._get_helios_build_path()
49 if working_dir
and working_dir.exists():
50 radiation_assets = working_dir /
'plugins' /
'radiation'
53 current_dir = Path(__file__).parent
54 packaged_build = current_dir /
'assets' /
'build'
56 if packaged_build.exists():
57 working_dir = packaged_build
58 radiation_assets = working_dir /
'plugins' /
'radiation'
61 repo_root = current_dir.parent
62 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
63 working_dir = build_lib_dir.parent
64 radiation_assets = working_dir /
'plugins' /
'radiation'
66 if not build_lib_dir.exists():
68 f
"PyHelios build directory not found at {build_lib_dir}. "
69 f
"Run: python build_scripts/build_helios.py --plugins radiation"
72 if not radiation_assets.exists():
74 f
"RadiationModel assets not found at {radiation_assets}. "
75 f
"This indicates a build system error. The build script should copy PTX files to this location."
79 original_dir = os.getcwd()
82 logger.debug(f
"Changed working directory to {working_dir} for RadiationModel asset access")
85 os.chdir(original_dir)
86 logger.debug(f
"Restored working directory to {original_dir}")
90 """Raised when RadiationModel operations fail."""
96 Camera properties for radiation model cameras.
98 This class encapsulates the properties needed to configure a radiation camera,
99 providing sensible defaults and validation for camera parameters. Updated for
100 Helios v1.3.58 with extended camera metadata fields.
103 def __init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05,
104 HFOV=20.0, FOV_aspect_ratio=0.0, lens_focal_length=0.05,
105 sensor_width_mm=35.0, model="generic", lens_make="", lens_model="",
106 lens_specification="", exposure="auto", shutter_speed=1.0/125.0,
107 white_balance="auto"):
109 Initialize camera properties with defaults matching C++ CameraProperties.
112 camera_resolution: Camera resolution as (width, height) tuple or list. Default: (512, 512)
113 focal_plane_distance: Distance from viewing plane to focal plane (working distance). Default: 1.0
114 lens_diameter: Diameter of camera lens (0 = pinhole camera). Default: 0.05
115 HFOV: Horizontal field of view in degrees. Default: 20.0
116 FOV_aspect_ratio: Ratio of horizontal to vertical FOV. Default: 0.0 (auto-calculate from resolution)
117 lens_focal_length: Camera lens optical focal length in meters (physical, not 35mm equiv). Default: 0.05 (50mm)
118 sensor_width_mm: Physical sensor width in mm. Default: 35.0 (full-frame)
119 model: Camera model name (e.g., "Nikon D700", "Canon EOS 5D"). Default: "generic"
120 lens_make: Lens manufacturer (e.g., "Canon", "Nikon"). Default: ""
121 lens_model: Lens model name (e.g., "AF-S NIKKOR 50mm f/1.8G"). Default: ""
122 lens_specification: Lens specification (e.g., "50mm f/1.8"). Default: ""
123 exposure: Exposure mode - "auto", "ISOXXX" (e.g., "ISO100"), or "manual". Default: "auto"
124 shutter_speed: Camera shutter speed in seconds (e.g., 0.008 for 1/125s). Default: 0.008 (1/125s)
125 white_balance: White balance mode - "auto" or "off". Default: "auto"
129 if camera_resolution
is None:
130 self.camera_resolution = (512, 512)
132 if isinstance(camera_resolution, (list, tuple))
and len(camera_resolution) == 2:
133 self.camera_resolution = (int(camera_resolution[0]), int(camera_resolution[1]))
135 raise ValueError(
"camera_resolution must be a tuple or list of 2 integers")
139 if focal_plane_distance <= 0:
140 raise ValueError(
"focal_plane_distance must be greater than 0")
141 if lens_diameter < 0:
142 raise ValueError(
"lens_diameter must be non-negative")
143 if HFOV <= 0
or HFOV > 180:
144 raise ValueError(
"HFOV must be between 0 and 180 degrees")
145 if FOV_aspect_ratio < 0:
146 raise ValueError(
"FOV_aspect_ratio must be non-negative (0 = auto-calculate)")
147 if lens_focal_length <= 0:
148 raise ValueError(
"lens_focal_length must be greater than 0")
149 if sensor_width_mm <= 0:
150 raise ValueError(
"sensor_width_mm must be greater than 0")
151 if shutter_speed <= 0:
152 raise ValueError(
"shutter_speed must be greater than 0")
163 if not isinstance(model, str):
164 raise ValueError(
"model must be a string")
165 if not isinstance(lens_make, str):
166 raise ValueError(
"lens_make must be a string")
167 if not isinstance(lens_model, str):
168 raise ValueError(
"lens_model must be a string")
169 if not isinstance(lens_specification, str):
170 raise ValueError(
"lens_specification must be a string")
171 if not isinstance(exposure, str):
172 raise ValueError(
"exposure must be a string")
173 if not isinstance(white_balance, str):
174 raise ValueError(
"white_balance must be a string")
177 if exposure
not in [
"auto",
"manual"]
and not exposure.startswith(
"ISO"):
178 raise ValueError(
"exposure must be 'auto', 'manual', or 'ISOXXX' (e.g., 'ISO100')")
181 if white_balance
not in [
"auto",
"off"]:
182 raise ValueError(
"white_balance must be 'auto' or 'off'")
184 self.
model = str(model)
193 Convert to array format expected by C++ interface.
195 Note: Returns numeric fields only. String fields (model, lens_make, etc.) are
196 currently initialized with defaults in the C++ wrapper and cannot be set via
197 this interface. Use the upcoming camera library methods for full metadata control.
200 List of 9 float values: [resolution_x, resolution_y, focal_distance, lens_diameter,
201 HFOV, FOV_aspect_ratio, lens_focal_length, sensor_width_mm,
205 float(self.camera_resolution[0]),
206 float(self.camera_resolution[1]),
217 return (f
"CameraProperties("
218 f
"camera_resolution={self.camera_resolution}, "
219 f
"focal_plane_distance={self.focal_plane_distance}, "
220 f
"lens_diameter={self.lens_diameter}, "
221 f
"HFOV={self.HFOV}, "
222 f
"FOV_aspect_ratio={self.FOV_aspect_ratio}, "
223 f
"lens_focal_length={self.lens_focal_length}, "
224 f
"sensor_width_mm={self.sensor_width_mm}, "
225 f
"model='{self.model}', "
226 f
"lens_make='{self.lens_make}', "
227 f
"lens_model='{self.lens_model}', "
228 f
"lens_specification='{self.lens_specification}', "
229 f
"exposure='{self.exposure}', "
230 f
"shutter_speed={self.shutter_speed}, "
231 f
"white_balance='{self.white_balance}')")
236 Metadata for radiation camera image export (Helios v1.3.58+).
238 This class encapsulates comprehensive metadata for camera images including
239 camera properties, location, acquisition settings, image processing, and
240 agronomic properties derived from plant architecture data.
244 """Camera intrinsic properties for metadata export."""
245 def __init__(self, height=512, width=512, channels=3, type="rgb",
246 focal_length=50.0, aperture="f/2.8", sensor_width=35.0,
247 sensor_height=24.0, model="generic", lens_make="",
248 lens_model="", lens_specification="", exposure="auto",
249 shutter_speed=0.008, white_balance="auto"):
251 self.
width = int(width)
267 """Geographic location properties."""
268 def __init__(self, latitude=0.0, longitude=0.0):
273 """Image acquisition properties."""
274 def __init__(self, date="", time="", UTC_offset=0.0, camera_height_m=0.0,
275 camera_angle_deg=0.0, light_source="sunlight"):
277 self.
time = str(time)
284 """Image processing corrections applied to the image."""
285 def __init__(self, saturation_adjustment=1.0, brightness_adjustment=1.0,
286 contrast_adjustment=1.0, color_space="linear"):
293 """Agronomic properties derived from plant architecture data."""
294 def __init__(self, plant_species=None, plant_count=None, plant_height_m=None,
295 plant_age_days=None, plant_stage=None, leaf_area_m2=None,
297 self.
plant_species = plant_species
if plant_species
is not None else []
298 self.
plant_count = plant_count
if plant_count
is not None else []
307 Initialize CameraMetadata with default values.
310 path: Full path to the associated image file. Default: ""
312 self.path = str(path)
320 return (f
"CameraMetadata(path='{self.path}', "
321 f
"camera={self.camera_properties.model}, "
322 f
"resolution={self.camera_properties.width}x{self.camera_properties.height}, "
323 f
"location=({self.location_properties.latitude},{self.location_properties.longitude}))")
328 High-level interface for radiation modeling and ray tracing.
330 This class provides a user-friendly wrapper around the native Helios
331 radiation plugin with automatic plugin availability checking and
332 graceful error handling.
337 Initialize RadiationModel with graceful plugin handling.
340 context: Helios Context instance
343 TypeError: If context is not a Context instance
344 RadiationModelError: If radiation plugin is not available
347 if not isinstance(context, Context):
348 raise TypeError(f
"RadiationModel requires a Context instance, got {type(context).__name__}")
354 registry = get_plugin_registry()
356 if not registry.is_plugin_available(
'radiation'):
358 plugin_info = registry.get_plugin_capabilities()
359 available_plugins = registry.get_available_plugins()
362 "RadiationModel requires the 'radiation' plugin which is not available.\n\n"
363 "The radiation plugin provides GPU-accelerated ray tracing using OptiX.\n"
364 "System requirements:\n"
365 "- NVIDIA GPU with CUDA support\n"
366 "- CUDA Toolkit installed\n"
367 "- OptiX runtime (bundled with PyHelios)\n\n"
368 "To enable radiation modeling:\n"
369 "1. Build PyHelios with radiation plugin:\n"
370 " build_scripts/build_helios --plugins radiation\n"
371 "2. Or build with multiple plugins:\n"
372 " build_scripts/build_helios --plugins radiation,visualizer,weberpenntree\n"
373 f
"\nCurrently available plugins: {available_plugins}"
377 alternatives = registry.suggest_alternatives(
'radiation')
379 error_msg += f
"\n\nAlternative plugins available: {alternatives}"
380 error_msg +=
"\nConsider using energybalance or leafoptics for thermal modeling."
387 self.
radiation_model = radiation_wrapper.createRadiationModel(context.getNativePtr())
390 "Failed to create RadiationModel instance. "
391 "This may indicate a problem with the native library or GPU initialization."
393 logger.info(
"RadiationModel created successfully")
395 except Exception
as e:
399 """Context manager entry."""
402 def __exit__(self, exc_type, exc_value, traceback):
403 """Context manager exit with proper cleanup."""
407 logger.debug(
"RadiationModel destroyed successfully")
408 except Exception
as e:
409 logger.warning(f
"Error destroying RadiationModel: {e}")
414 """Destructor to ensure GPU resources freed even without 'with' statement."""
415 if hasattr(self,
'radiation_model')
and self.
radiation_model is not None:
419 except Exception
as e:
421 warnings.warn(f
"Error in RadiationModel.__del__: {e}")
424 """Get native pointer for advanced operations."""
428 """Get native pointer for advanced operations. (Legacy naming for compatibility)"""
431 @require_plugin('radiation', 'disable status messages')
433 """Disable RadiationModel status messages."""
436 @require_plugin('radiation', 'enable status messages')
438 """Enable RadiationModel status messages."""
441 @require_plugin('radiation', 'add radiation band')
442 def addRadiationBand(self, band_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
444 Add radiation band with optional wavelength bounds.
447 band_label: Name/label for the radiation band
448 wavelength_min: Optional minimum wavelength (μm)
449 wavelength_max: Optional maximum wavelength (μm)
452 validate_band_label(band_label,
"band_label",
"addRadiationBand")
453 if wavelength_min
is not None and wavelength_max
is not None:
454 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"addRadiationBand")
455 radiation_wrapper.addRadiationBandWithWavelengths(self.
radiation_model, band_label, wavelength_min, wavelength_max)
456 logger.debug(f
"Added radiation band {band_label}: {wavelength_min}-{wavelength_max} μm")
459 logger.debug(f
"Added radiation band: {band_label}")
461 @require_plugin('radiation', 'copy radiation band')
462 @validate_radiation_band_params
463 def copyRadiationBand(self, old_label: str, new_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
465 Copy existing radiation band to new label, optionally with new wavelength range.
468 old_label: Existing band label to copy
469 new_label: New label for the copied band
470 wavelength_min: Optional minimum wavelength for new band (μm)
471 wavelength_max: Optional maximum wavelength for new band (μm)
474 >>> # Copy band with same wavelength range
475 >>> radiation.copyRadiationBand("SW", "SW_copy")
477 >>> # Copy band with different wavelength range
478 >>> radiation.copyRadiationBand("full_spectrum", "PAR", 400, 700)
480 if wavelength_min
is not None and wavelength_max
is not None:
481 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"copyRadiationBand")
483 radiation_wrapper.copyRadiationBand(self.
radiation_model, old_label, new_label, wavelength_min, wavelength_max)
484 if wavelength_min
is not None:
485 logger.debug(f
"Copied radiation band {old_label} to {new_label} with wavelengths {wavelength_min}-{wavelength_max} μm")
487 logger.debug(f
"Copied radiation band {old_label} to {new_label}")
489 @require_plugin('radiation', 'add radiation source')
490 @validate_collimated_source_params
493 Add collimated radiation source.
496 direction: Optional direction vector. Can be tuple (x, y, z), vec3, or None for default direction.
501 if direction
is None:
502 source_id = radiation_wrapper.addCollimatedRadiationSourceDefault(self.
radiation_model)
505 if hasattr(direction,
'x')
and hasattr(direction,
'y')
and hasattr(direction,
'z'):
507 x, y, z = direction.x, direction.y, direction.z
508 elif hasattr(direction,
'radius')
and hasattr(direction,
'elevation')
and hasattr(direction,
'azimuth'):
512 elevation = direction.elevation
513 azimuth = direction.azimuth
514 x = r * math.cos(elevation) * math.cos(azimuth)
515 y = r * math.cos(elevation) * math.sin(azimuth)
516 z = r * math.sin(elevation)
520 if len(direction) != 3:
521 raise TypeError(f
"Direction must be a 3-element tuple, vec3, or SphericalCoord, got {type(direction).__name__} with {len(direction)} elements")
523 except (TypeError, AttributeError):
525 raise TypeError(f
"Direction must be a tuple, vec3, or SphericalCoord, got {type(direction).__name__}")
526 source_id = radiation_wrapper.addCollimatedRadiationSourceVec3(self.
radiation_model, x, y, z)
528 logger.debug(f
"Added collimated radiation source: ID {source_id}")
531 @require_plugin('radiation', 'add spherical radiation source')
532 @validate_sphere_source_params
535 Add spherical radiation source.
538 position: Position of the source. Can be tuple (x, y, z) or vec3.
539 radius: Radius of the spherical source
545 if hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z'):
547 x, y, z = position.x, position.y, position.z
551 source_id = radiation_wrapper.addSphereRadiationSource(self.
radiation_model, x, y, z, radius)
552 logger.debug(f
"Added sphere radiation source: ID {source_id} at ({x}, {y}, {z}) with radius {radius}")
555 @require_plugin('radiation', 'add sun radiation source')
556 @validate_sun_sphere_params
558 position_scaling: float = 1.0, angular_width: float = 0.53,
559 flux_scaling: float = 1.0) -> int:
561 Add sun sphere radiation source.
564 radius: Radius of the sun sphere
565 zenith: Zenith angle (degrees)
566 azimuth: Azimuth angle (degrees)
567 position_scaling: Position scaling factor
568 angular_width: Angular width of the sun (degrees)
569 flux_scaling: Flux scaling factor
574 source_id = radiation_wrapper.addSunSphereRadiationSource(
575 self.
radiation_model, radius, zenith, azimuth, position_scaling, angular_width, flux_scaling
577 logger.debug(f
"Added sun radiation source: ID {source_id}")
580 @require_plugin('radiation', 'set source position')
583 Set position of a radiation source.
585 Allows dynamic repositioning of radiation sources during simulation,
586 useful for time-series modeling or moving light sources.
589 source_id: ID of the radiation source
590 position: New position as vec3, SphericalCoord, or list/tuple [x, y, z]
593 >>> source_id = radiation.addCollimatedRadiationSource()
594 >>> radiation.setSourcePosition(source_id, [10, 20, 30])
595 >>> from pyhelios.types import vec3
596 >>> radiation.setSourcePosition(source_id, vec3(15, 25, 35))
598 if not isinstance(source_id, int)
or source_id < 0:
599 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
600 radiation_wrapper.setSourcePosition(self.
radiation_model, source_id, position)
601 logger.debug(f
"Updated position for radiation source {source_id}")
603 @require_plugin('radiation', 'add rectangle radiation source')
606 Add a rectangle (planar) radiation source.
608 Rectangle sources are ideal for modeling artificial lighting such as
609 LED panels, grow lights, or window light sources.
612 position: Center position as vec3 or list [x, y, z]
613 size: Rectangle dimensions as vec2 or list [width, height]
614 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
620 >>> from pyhelios.types import vec3, vec2
621 >>> source_id = radiation.addRectangleRadiationSource(
622 ... position=vec3(0, 0, 5),
624 ... rotation=vec3(0, 0, 0)
626 >>> radiation.setSourceFlux(source_id, "PAR", 500.0)
628 return radiation_wrapper.addRectangleRadiationSource(self.
radiation_model, position, size, rotation)
630 @require_plugin('radiation', 'add disk radiation source')
633 Add a disk (circular planar) radiation source.
635 Disk sources are useful for modeling circular light sources such as
636 spotlights, circular LED arrays, or solar simulators.
639 position: Center position as vec3 or list [x, y, z]
641 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
647 >>> from pyhelios.types import vec3
648 >>> source_id = radiation.addDiskRadiationSource(
649 ... position=vec3(0, 0, 5),
651 ... rotation=vec3(0, 0, 0)
653 >>> radiation.setSourceFlux(source_id, "PAR", 300.0)
656 raise ValueError(f
"Radius must be positive, got {radius}")
657 return radiation_wrapper.addDiskRadiationSource(self.
radiation_model, position, radius, rotation)
660 @require_plugin('radiation', 'manage source spectrum')
663 Set radiation spectrum for source(s).
665 Spectral distributions define how radiation intensity varies with wavelength,
666 essential for realistic modeling of different light sources (sunlight, LEDs, etc.).
669 source_id: Source ID (int) or list of source IDs
671 - Spectrum data as list of (wavelength, value) tuples
672 - Global data label string
675 >>> # Define custom LED spectrum
677 ... (400, 0.0), (450, 0.3), (500, 0.8),
678 ... (550, 0.5), (600, 0.2), (700, 0.0)
680 >>> radiation.setSourceSpectrum(source_id, led_spectrum)
682 >>> # Use predefined spectrum from global data
683 >>> radiation.setSourceSpectrum(source_id, "D65_illuminant")
685 >>> # Apply same spectrum to multiple sources
686 >>> radiation.setSourceSpectrum([src1, src2, src3], led_spectrum)
688 radiation_wrapper.setSourceSpectrum(self.
radiation_model, source_id, spectrum)
689 logger.debug(f
"Set spectrum for source(s) {source_id}")
691 @require_plugin('radiation', 'configure source spectrum')
693 wavelength_min: float =
None, wavelength_max: float =
None):
695 Set source spectrum integral value.
697 Normalizes the spectrum so that its integral equals the specified value,
698 useful for calibrating source intensity.
702 source_integral: Target integral value
703 wavelength_min: Optional minimum wavelength for integration range
704 wavelength_max: Optional maximum wavelength for integration range
707 >>> radiation.setSourceSpectrumIntegral(source_id, 1000.0)
708 >>> radiation.setSourceSpectrumIntegral(source_id, 500.0, 400, 700) # PAR range
710 if not isinstance(source_id, int)
or source_id < 0:
711 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
712 if source_integral < 0:
713 raise ValueError(f
"Source integral must be non-negative, got {source_integral}")
715 radiation_wrapper.setSourceSpectrumIntegral(self.
radiation_model, source_id, source_integral,
716 wavelength_min, wavelength_max)
717 logger.debug(f
"Set spectrum integral for source {source_id}: {source_integral}")
720 @require_plugin('radiation', 'integrate spectrum')
722 wavelength_max: float =
None, source_id: int =
None,
723 camera_spectrum=
None) -> float:
725 Integrate spectrum with optional source/camera spectra and wavelength range.
727 This unified method handles multiple integration scenarios:
728 - Basic: Total spectrum integration
729 - Range: Integration over wavelength range
730 - Source: Integration weighted by source spectrum
731 - Camera: Integration weighted by camera spectral response
732 - Full: Integration with both source and camera spectra
735 object_spectrum: Object spectrum as list of (wavelength, value) tuples/vec2
736 wavelength_min: Optional minimum wavelength for integration range
737 wavelength_max: Optional maximum wavelength for integration range
738 source_id: Optional source ID for source spectrum weighting
739 camera_spectrum: Optional camera spectrum for camera response weighting
745 >>> leaf_reflectance = [(400, 0.1), (500, 0.4), (600, 0.6), (700, 0.5)]
747 >>> # Total integration
748 >>> total = radiation.integrateSpectrum(leaf_reflectance)
750 >>> # PAR range (400-700nm)
751 >>> par = radiation.integrateSpectrum(leaf_reflectance, 400, 700)
753 >>> # With source spectrum
754 >>> source_weighted = radiation.integrateSpectrum(
755 ... leaf_reflectance, 400, 700, source_id=sun_source
758 >>> # With camera response
759 >>> camera_response = [(400, 0.2), (550, 1.0), (700, 0.3)]
760 >>> camera_weighted = radiation.integrateSpectrum(
761 ... leaf_reflectance, camera_spectrum=camera_response
764 return radiation_wrapper.integrateSpectrum(self.
radiation_model, object_spectrum,
765 wavelength_min, wavelength_max,
766 source_id, camera_spectrum)
768 @require_plugin('radiation', 'integrate source spectrum')
771 Integrate source spectrum over wavelength range.
775 wavelength_min: Minimum wavelength
776 wavelength_max: Maximum wavelength
779 Integrated source spectrum value
782 >>> par_flux = radiation.integrateSourceSpectrum(source_id, 400, 700)
784 if not isinstance(source_id, int)
or source_id < 0:
785 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
786 return radiation_wrapper.integrateSourceSpectrum(self.
radiation_model, source_id,
787 wavelength_min, wavelength_max)
790 @require_plugin('radiation', 'scale spectrum')
791 def scaleSpectrum(self, existing_label: str, new_label_or_scale, scale_factor: float =
None):
793 Scale spectrum in-place or to new label.
795 Useful for adjusting spectrum intensities or creating variations of
796 existing spectra for sensitivity analysis.
798 Supports two call patterns:
799 - scaleSpectrum("label", scale) -> scales in-place
800 - scaleSpectrum("existing", "new", scale) -> creates new scaled spectrum
803 existing_label: Existing global data label
804 new_label_or_scale: Either new label string (if creating new) or scale factor (if in-place)
805 scale_factor: Scale factor (required only if new_label_or_scale is a string)
808 >>> # In-place scaling
809 >>> radiation.scaleSpectrum("leaf_reflectance", 1.2)
811 >>> # Create new scaled spectrum
812 >>> radiation.scaleSpectrum("leaf_reflectance", "scaled_leaf", 1.5)
814 if not isinstance(existing_label, str)
or not existing_label.strip():
815 raise ValueError(
"Existing label must be a non-empty string")
818 new_label_or_scale, scale_factor)
819 logger.debug(f
"Scaled spectrum '{existing_label}'")
821 @require_plugin('radiation', 'scale spectrum randomly')
823 min_scale: float, max_scale: float):
825 Scale spectrum with random factor and store as new label.
827 Useful for creating stochastic variations in spectral properties for
828 Monte Carlo simulations or uncertainty quantification.
831 existing_label: Existing global data label
832 new_label: New global data label for scaled spectrum
833 min_scale: Minimum scale factor
834 max_scale: Maximum scale factor
837 >>> # Create random variation of leaf reflectance
838 >>> radiation.scaleSpectrumRandomly("leaf_base", "leaf_variant", 0.8, 1.2)
840 if not isinstance(existing_label, str)
or not existing_label.strip():
841 raise ValueError(
"Existing label must be a non-empty string")
842 if not isinstance(new_label, str)
or not new_label.strip():
843 raise ValueError(
"New label must be a non-empty string")
844 if min_scale >= max_scale:
845 raise ValueError(f
"min_scale ({min_scale}) must be less than max_scale ({max_scale})")
847 radiation_wrapper.scaleSpectrumRandomly(self.
radiation_model, existing_label, new_label,
848 min_scale, max_scale)
849 logger.debug(f
"Scaled spectrum '{existing_label}' randomly to '{new_label}'")
851 @require_plugin('radiation', 'blend spectra')
852 def blendSpectra(self, new_label: str, spectrum_labels: List[str], weights: List[float]):
854 Blend multiple spectra with specified weights.
856 Creates weighted combination of spectra, useful for mixing material properties
857 or creating composite light sources.
860 new_label: New global data label for blended spectrum
861 spectrum_labels: List of spectrum labels to blend
862 weights: List of weights (must sum to reasonable values, same length as labels)
865 >>> # Mix two leaf types (70% type A, 30% type B)
866 >>> radiation.blendSpectra("mixed_leaf",
867 ... ["leaf_type_a", "leaf_type_b"],
871 if not isinstance(new_label, str)
or not new_label.strip():
872 raise ValueError(
"New label must be a non-empty string")
873 if len(spectrum_labels) != len(weights):
874 raise ValueError(f
"Number of labels ({len(spectrum_labels)}) must match number of weights ({len(weights)})")
875 if not spectrum_labels:
876 raise ValueError(
"At least one spectrum label required")
878 radiation_wrapper.blendSpectra(self.
radiation_model, new_label, spectrum_labels, weights)
879 logger.debug(f
"Blended {len(spectrum_labels)} spectra into '{new_label}'")
881 @require_plugin('radiation', 'blend spectra randomly')
884 Blend multiple spectra with random weights.
886 Creates random combinations of spectra, useful for generating diverse
887 material properties in stochastic simulations.
890 new_label: New global data label for blended spectrum
891 spectrum_labels: List of spectrum labels to blend
894 >>> # Create random mixture of leaf spectra
895 >>> radiation.blendSpectraRandomly("random_leaf",
896 ... ["young_leaf", "mature_leaf", "senescent_leaf"]
899 if not isinstance(new_label, str)
or not new_label.strip():
900 raise ValueError(
"New label must be a non-empty string")
901 if not spectrum_labels:
902 raise ValueError(
"At least one spectrum label required")
904 radiation_wrapper.blendSpectraRandomly(self.
radiation_model, new_label, spectrum_labels)
905 logger.debug(f
"Blended {len(spectrum_labels)} spectra randomly into '{new_label}'")
908 @require_plugin('radiation', 'interpolate spectrum from data')
910 spectra_labels: List[str], values: List[float],
911 primitive_data_query_label: str,
912 primitive_data_radprop_label: str):
914 Interpolate spectral properties based on primitive data values.
916 Automatically assigns spectra to primitives by interpolating between
917 reference spectra based on continuous data values (e.g., age, moisture, etc.).
920 primitive_uuids: List of primitive UUIDs to assign spectra
921 spectra_labels: List of reference spectrum labels
922 values: List of data values corresponding to each spectrum
923 primitive_data_query_label: Primitive data label containing query values
924 primitive_data_radprop_label: Primitive data label to store assigned spectra
927 >>> # Assign leaf reflectance based on age
928 >>> leaf_patches = context.getAllUUIDs("patch")
929 >>> radiation.interpolateSpectrumFromPrimitiveData(
930 ... primitive_uuids=leaf_patches,
931 ... spectra_labels=["young_leaf", "mature_leaf", "old_leaf"],
932 ... values=[0.0, 50.0, 100.0], # Days since emergence
933 ... primitive_data_query_label="leaf_age",
934 ... primitive_data_radprop_label="reflectance"
937 if not isinstance(primitive_uuids, (list, tuple))
or not primitive_uuids:
938 raise ValueError(
"Primitive UUIDs must be a non-empty list")
939 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
940 raise ValueError(
"Spectra labels must be a non-empty list")
941 if not isinstance(values, (list, tuple))
or not values:
942 raise ValueError(
"Values must be a non-empty list")
943 if len(spectra_labels) != len(values):
944 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
946 radiation_wrapper.interpolateSpectrumFromPrimitiveData(
948 primitive_data_query_label, primitive_data_radprop_label
950 logger.debug(f
"Interpolated spectra for {len(primitive_uuids)} primitives")
952 @require_plugin('radiation', 'interpolate spectrum from object data')
954 spectra_labels: List[str], values: List[float],
955 object_data_query_label: str,
956 primitive_data_radprop_label: str):
958 Interpolate spectral properties based on object data values.
960 Automatically assigns spectra to object primitives by interpolating between
961 reference spectra based on continuous object-level data values.
964 object_ids: List of object IDs
965 spectra_labels: List of reference spectrum labels
966 values: List of data values corresponding to each spectrum
967 object_data_query_label: Object data label containing query values
968 primitive_data_radprop_label: Primitive data label to store assigned spectra
971 >>> # Assign tree reflectance based on health index
972 >>> tree_ids = [tree1_id, tree2_id, tree3_id]
973 >>> radiation.interpolateSpectrumFromObjectData(
974 ... object_ids=tree_ids,
975 ... spectra_labels=["healthy_tree", "stressed_tree", "diseased_tree"],
976 ... values=[1.0, 0.5, 0.0], # Health index
977 ... object_data_query_label="health_index",
978 ... primitive_data_radprop_label="reflectance"
981 if not isinstance(object_ids, (list, tuple))
or not object_ids:
982 raise ValueError(
"Object IDs must be a non-empty list")
983 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
984 raise ValueError(
"Spectra labels must be a non-empty list")
985 if not isinstance(values, (list, tuple))
or not values:
986 raise ValueError(
"Values must be a non-empty list")
987 if len(spectra_labels) != len(values):
988 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
990 radiation_wrapper.interpolateSpectrumFromObjectData(
992 object_data_query_label, primitive_data_radprop_label
994 logger.debug(f
"Interpolated spectra for {len(object_ids)} objects")
996 @require_plugin('radiation', 'set ray count')
998 """Set direct ray count for radiation band."""
999 validate_band_label(band_label,
"band_label",
"setDirectRayCount")
1000 validate_ray_count(ray_count,
"ray_count",
"setDirectRayCount")
1001 radiation_wrapper.setDirectRayCount(self.
radiation_model, band_label, ray_count)
1003 @require_plugin('radiation', 'set ray count')
1005 """Set diffuse ray count for radiation band."""
1006 validate_band_label(band_label,
"band_label",
"setDiffuseRayCount")
1007 validate_ray_count(ray_count,
"ray_count",
"setDiffuseRayCount")
1008 radiation_wrapper.setDiffuseRayCount(self.
radiation_model, band_label, ray_count)
1010 @require_plugin('radiation', 'set radiation flux')
1012 """Set diffuse radiation flux for band."""
1013 validate_band_label(label,
"label",
"setDiffuseRadiationFlux")
1014 validate_flux_value(flux,
"flux",
"setDiffuseRadiationFlux")
1015 radiation_wrapper.setDiffuseRadiationFlux(self.
radiation_model, label, flux)
1017 @require_plugin('radiation', 'configure diffuse radiation')
1020 Set diffuse radiation extinction coefficient with directional bias.
1022 Models directionally-biased diffuse radiation (e.g., sky radiation with zenith peak).
1026 K: Extinction coefficient
1027 peak_direction: Peak direction as vec3, SphericalCoord, or list [x, y, z]
1030 >>> from pyhelios.types import vec3
1031 >>> radiation.setDiffuseRadiationExtinctionCoeff("SW", 0.5, vec3(0, 0, 1))
1033 validate_band_label(label,
"label",
"setDiffuseRadiationExtinctionCoeff")
1035 raise ValueError(f
"Extinction coefficient must be non-negative, got {K}")
1036 radiation_wrapper.setDiffuseRadiationExtinctionCoeff(self.
radiation_model, label, K, peak_direction)
1037 logger.debug(f
"Set diffuse extinction coefficient for band '{label}': K={K}")
1039 @require_plugin('radiation', 'query diffuse flux')
1042 Get diffuse flux for band.
1045 band_label: Band label
1051 >>> flux = radiation.getDiffuseFlux("SW")
1053 validate_band_label(band_label,
"band_label",
"getDiffuseFlux")
1056 @require_plugin('radiation', 'configure diffuse spectrum')
1059 Set diffuse spectrum from global data label.
1062 band_label: Band label (string) or list of band labels
1063 spectrum_label: Spectrum global data label
1066 >>> radiation.setDiffuseSpectrum("SW", "sky_spectrum")
1067 >>> radiation.setDiffuseSpectrum(["SW", "NIR"], "sky_spectrum")
1069 if isinstance(band_label, str):
1070 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrum")
1072 for label
in band_label:
1073 validate_band_label(label,
"band_label",
"setDiffuseSpectrum")
1074 if not isinstance(spectrum_label, str)
or not spectrum_label.strip():
1075 raise ValueError(
"Spectrum label must be a non-empty string")
1077 radiation_wrapper.setDiffuseSpectrum(self.
radiation_model, band_label, spectrum_label)
1078 logger.debug(f
"Set diffuse spectrum for band(s) {band_label}")
1080 @require_plugin('radiation', 'configure diffuse spectrum')
1082 wavelength_max: float =
None, band_label: str =
None):
1084 Set diffuse spectrum integral.
1087 spectrum_integral: Integral value
1088 wavelength_min: Optional minimum wavelength
1089 wavelength_max: Optional maximum wavelength
1090 band_label: Optional specific band label (None for all bands)
1093 >>> radiation.setDiffuseSpectrumIntegral(1000.0) # All bands
1094 >>> radiation.setDiffuseSpectrumIntegral(500.0, 400, 700, band_label="PAR") # Specific band
1096 if spectrum_integral < 0:
1097 raise ValueError(f
"Spectrum integral must be non-negative, got {spectrum_integral}")
1098 if band_label
is not None:
1099 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrumIntegral")
1101 radiation_wrapper.setDiffuseSpectrumIntegral(self.
radiation_model, spectrum_integral,
1102 wavelength_min, wavelength_max, band_label)
1103 logger.debug(f
"Set diffuse spectrum integral: {spectrum_integral}")
1105 @require_plugin('radiation', 'set source flux')
1106 def setSourceFlux(self, source_id, label: str, flux: float):
1107 """Set source flux for single source or multiple sources."""
1108 validate_band_label(label,
"label",
"setSourceFlux")
1109 validate_flux_value(flux,
"flux",
"setSourceFlux")
1111 if isinstance(source_id, (list, tuple)):
1113 validate_source_id_list(list(source_id),
"source_id",
"setSourceFlux")
1114 radiation_wrapper.setSourceFluxMultiple(self.
radiation_model, source_id, label, flux)
1117 validate_source_id(source_id,
"source_id",
"setSourceFlux")
1118 radiation_wrapper.setSourceFlux(self.
radiation_model, source_id, label, flux)
1121 @require_plugin('radiation', 'get source flux')
1122 @validate_get_source_flux_params
1123 def getSourceFlux(self, source_id: int, label: str) -> float:
1124 """Get source flux for band."""
1125 return radiation_wrapper.getSourceFlux(self.
radiation_model, source_id, label)
1127 @require_plugin('radiation', 'update geometry')
1128 @validate_update_geometry_params
1131 Update geometry in radiation model.
1134 uuids: Optional list of specific UUIDs to update. If None, updates all geometry.
1138 logger.debug(
"Updated all geometry in radiation model")
1141 logger.debug(f
"Updated {len(uuids)} geometry UUIDs in radiation model")
1143 @require_plugin('radiation', 'run radiation simulation')
1144 @validate_run_band_params
1145 def runBand(self, band_label):
1147 Run radiation simulation for single band or multiple bands.
1149 PERFORMANCE NOTE: When simulating multiple radiation bands, it is HIGHLY RECOMMENDED
1150 to run all bands in a single call (e.g., runBand(["PAR", "NIR", "SW"])) rather than
1151 sequential single-band calls. This provides significant computational efficiency gains
1154 - GPU ray tracing setup is done once for all bands
1155 - Scene geometry acceleration structures are reused
1156 - OptiX kernel launches are batched together
1157 - Memory transfers between CPU/GPU are minimized
1160 # EFFICIENT - Single call for multiple bands
1161 radiation.runBand(["PAR", "NIR", "SW"])
1163 # INEFFICIENT - Sequential single-band calls
1164 radiation.runBand("PAR")
1165 radiation.runBand("NIR")
1166 radiation.runBand("SW")
1169 band_label: Single band name (str) or list of band names for multi-band simulation
1171 if isinstance(band_label, (list, tuple)):
1173 for lbl
in band_label:
1174 if not isinstance(lbl, str):
1175 raise TypeError(f
"Band labels must be strings, got {type(lbl).__name__}")
1177 logger.info(f
"Completed radiation simulation for bands: {band_label}")
1180 if not isinstance(band_label, str):
1181 raise TypeError(f
"Band label must be a string, got {type(band_label).__name__}")
1183 logger.info(f
"Completed radiation simulation for band: {band_label}")
1186 @require_plugin('radiation', 'get simulation results')
1188 """Get total absorbed flux for all primitives."""
1189 results = radiation_wrapper.getTotalAbsorbedFlux(self.
radiation_model)
1190 logger.debug(f
"Retrieved absorbed flux data for {len(results)} primitives")
1194 @require_plugin('radiation', 'check band existence')
1197 Check if a radiation band exists.
1200 label: Name/label of the radiation band to check
1203 True if band exists, False otherwise
1206 >>> radiation.addRadiationBand("SW")
1207 >>> radiation.doesBandExist("SW")
1209 >>> radiation.doesBandExist("nonexistent")
1212 validate_band_label(label,
"label",
"doesBandExist")
1216 @require_plugin('radiation', 'manage radiation sources')
1219 Delete a radiation source.
1222 source_id: ID of the radiation source to delete
1225 >>> source_id = radiation.addCollimatedRadiationSource()
1226 >>> radiation.deleteRadiationSource(source_id)
1228 if not isinstance(source_id, int)
or source_id < 0:
1229 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1230 radiation_wrapper.deleteRadiationSource(self.
radiation_model, source_id)
1231 logger.debug(f
"Deleted radiation source {source_id}")
1233 @require_plugin('radiation', 'query radiation sources')
1236 Get position of a radiation source.
1239 source_id: ID of the radiation source
1242 vec3 position of the source
1245 >>> source_id = radiation.addCollimatedRadiationSource()
1246 >>> position = radiation.getSourcePosition(source_id)
1247 >>> print(f"Source at: {position}")
1249 if not isinstance(source_id, int)
or source_id < 0:
1250 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1251 position_list = radiation_wrapper.getSourcePosition(self.
radiation_model, source_id)
1252 from .wrappers.DataTypes
import vec3
1253 return vec3(position_list[0], position_list[1], position_list[2])
1256 @require_plugin('radiation', 'get sky energy')
1259 Get total sky energy.
1262 Total sky energy value
1265 >>> energy = radiation.getSkyEnergy()
1266 >>> print(f"Sky energy: {energy}")
1270 @require_plugin('radiation', 'calculate G-function')
1273 Calculate G-function (geometry factor) for given view direction.
1275 The G-function describes the geometric relationship between leaf area
1276 distribution and viewing direction, important for canopy radiation modeling.
1279 view_direction: View direction as vec3 or list/tuple [x, y, z]
1285 >>> from pyhelios.types import vec3
1286 >>> g_value = radiation.calculateGtheta(vec3(0, 0, 1))
1287 >>> print(f"G-function: {g_value}")
1290 return radiation_wrapper.calculateGtheta(self.
radiation_model, context_ptr, view_direction)
1292 @require_plugin('radiation', 'configure output data')
1295 Enable optional primitive data output.
1298 label: Name/label of the primitive data to output
1301 >>> radiation.optionalOutputPrimitiveData("temperature")
1303 validate_band_label(label,
"label",
"optionalOutputPrimitiveData")
1305 logger.debug(f
"Enabled optional output for primitive data: {label}")
1307 @require_plugin('radiation', 'configure boundary conditions')
1310 Enforce periodic boundary conditions.
1312 Periodic boundaries are useful for large-scale simulations to reduce
1313 edge effects by wrapping radiation at domain boundaries.
1316 boundary: Boundary specification string (e.g., "xy", "xyz", "x", "y", "z")
1319 >>> radiation.enforcePeriodicBoundary("xy")
1321 if not isinstance(boundary, str)
or not boundary:
1322 raise ValueError(
"Boundary specification must be a non-empty string")
1323 radiation_wrapper.enforcePeriodicBoundary(self.
radiation_model, boundary)
1324 logger.debug(f
"Enforced periodic boundary: {boundary}")
1327 @require_plugin('radiation', 'configure radiation simulation')
1328 @validate_scattering_depth_params
1330 """Set scattering depth for radiation band."""
1331 radiation_wrapper.setScatteringDepth(self.
radiation_model, label, depth)
1333 @require_plugin('radiation', 'configure radiation simulation')
1334 @validate_min_scatter_energy_params
1336 """Set minimum scatter energy for radiation band."""
1337 radiation_wrapper.setMinScatterEnergy(self.
radiation_model, label, energy)
1339 @require_plugin('radiation', 'configure radiation emission')
1341 """Disable emission for radiation band."""
1342 validate_band_label(label,
"label",
"disableEmission")
1345 @require_plugin('radiation', 'configure radiation emission')
1347 """Enable emission for radiation band."""
1348 validate_band_label(label,
"label",
"enableEmission")
1355 @require_plugin('radiation', 'add radiation camera')
1356 def addRadiationCamera(self, camera_label: str, band_labels: List[str], position, lookat_or_direction,
1357 camera_properties=
None, antialiasing_samples: int = 100):
1359 Add a radiation camera to the simulation.
1362 camera_label: Unique label string for the camera
1363 band_labels: List of radiation band labels for the camera
1364 position: Camera position as vec3 object
1365 lookat_or_direction: Either:
1366 - Lookat point as vec3 object
1367 - SphericalCoord for viewing direction
1368 camera_properties: CameraProperties instance or None for defaults
1369 antialiasing_samples: Number of antialiasing samples (default: 100)
1372 ValidationError: If parameters are invalid or have wrong types
1373 RadiationModelError: If camera creation fails
1376 >>> from pyhelios import vec3, CameraProperties
1377 >>> # Create camera looking at origin from above
1378 >>> camera_props = CameraProperties(camera_resolution=(1024, 1024))
1379 >>> radiation_model.addRadiationCamera("main_camera", ["red", "green", "blue"],
1380 ... position=vec3(0, 0, 5), lookat_or_direction=vec3(0, 0, 0),
1381 ... camera_properties=camera_props)
1384 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1385 from .wrappers.DataTypes
import SphericalCoord, vec3, make_vec3
1386 from .validation.plugins
import validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
1389 validated_label = validate_camera_label(camera_label,
"camera_label",
"addRadiationCamera")
1390 validated_bands = validate_band_labels_list(band_labels,
"band_labels",
"addRadiationCamera")
1391 validated_samples = validate_antialiasing_samples(antialiasing_samples,
"antialiasing_samples",
"addRadiationCamera")
1394 if not (hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z')):
1395 raise TypeError(
"position must be a vec3 object. Use vec3(x, y, z) to create one.")
1396 validated_position = position
1399 if hasattr(lookat_or_direction,
'radius')
and hasattr(lookat_or_direction,
'elevation'):
1400 validated_direction = lookat_or_direction
1401 elif hasattr(lookat_or_direction,
'x')
and hasattr(lookat_or_direction,
'y')
and hasattr(lookat_or_direction,
'z'):
1402 validated_direction = lookat_or_direction
1404 raise TypeError(
"lookat_or_direction must be a vec3 or SphericalCoord object. Use vec3(x, y, z) or SphericalCoord to create one.")
1407 if camera_properties
is None:
1412 if hasattr(validated_direction,
'radius')
and hasattr(validated_direction,
'elevation'):
1414 direction_coords = validated_direction.to_list()
1415 if len(direction_coords) >= 3:
1417 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[2]
1419 raise ValueError(
"SphericalCoord must have at least radius, elevation, and azimuth")
1421 radiation_wrapper.addRadiationCameraSpherical(
1425 validated_position.x, validated_position.y, validated_position.z,
1426 radius, elevation, azimuth,
1427 camera_properties.to_array(),
1432 radiation_wrapper.addRadiationCameraVec3(
1436 validated_position.x, validated_position.y, validated_position.z,
1437 validated_direction.x, validated_direction.y, validated_direction.z,
1438 camera_properties.to_array(),
1442 except Exception
as e:
1445 @require_plugin('radiation', 'manage camera position')
1448 Set camera position.
1450 Allows dynamic camera repositioning during simulation, useful for
1451 time-series captures or multi-view imaging.
1454 camera_label: Camera label string
1455 position: Camera position as vec3 or list [x, y, z]
1458 >>> radiation.setCameraPosition("cam1", [0, 0, 10])
1459 >>> from pyhelios.types import vec3
1460 >>> radiation.setCameraPosition("cam1", vec3(5, 5, 10))
1462 if not isinstance(camera_label, str)
or not camera_label.strip():
1463 raise ValueError(
"Camera label must be a non-empty string")
1464 radiation_wrapper.setCameraPosition(self.
radiation_model, camera_label, position)
1465 logger.debug(f
"Updated camera '{camera_label}' position")
1467 @require_plugin('radiation', 'query camera position')
1470 Get camera position.
1473 camera_label: Camera label string
1476 vec3 position of the camera
1479 >>> position = radiation.getCameraPosition("cam1")
1480 >>> print(f"Camera at: {position}")
1482 if not isinstance(camera_label, str)
or not camera_label.strip():
1483 raise ValueError(
"Camera label must be a non-empty string")
1484 position_list = radiation_wrapper.getCameraPosition(self.
radiation_model, camera_label)
1485 from .wrappers.DataTypes
import vec3
1486 return vec3(position_list[0], position_list[1], position_list[2])
1488 @require_plugin('radiation', 'manage camera lookat')
1491 Set camera lookat point.
1494 camera_label: Camera label string
1495 lookat: Lookat point as vec3 or list [x, y, z]
1498 >>> radiation.setCameraLookat("cam1", [0, 0, 0])
1500 if not isinstance(camera_label, str)
or not camera_label.strip():
1501 raise ValueError(
"Camera label must be a non-empty string")
1502 radiation_wrapper.setCameraLookat(self.
radiation_model, camera_label, lookat)
1503 logger.debug(f
"Updated camera '{camera_label}' lookat point")
1505 @require_plugin('radiation', 'query camera lookat')
1508 Get camera lookat point.
1511 camera_label: Camera label string
1517 >>> lookat = radiation.getCameraLookat("cam1")
1518 >>> print(f"Camera looking at: {lookat}")
1520 if not isinstance(camera_label, str)
or not camera_label.strip():
1521 raise ValueError(
"Camera label must be a non-empty string")
1522 lookat_list = radiation_wrapper.getCameraLookat(self.
radiation_model, camera_label)
1523 from .wrappers.DataTypes
import vec3
1524 return vec3(lookat_list[0], lookat_list[1], lookat_list[2])
1526 @require_plugin('radiation', 'manage camera orientation')
1529 Set camera orientation.
1532 camera_label: Camera label string
1533 direction: View direction as vec3, SphericalCoord, or list [x, y, z]
1536 >>> radiation.setCameraOrientation("cam1", [0, 0, 1])
1537 >>> from pyhelios.types import SphericalCoord
1538 >>> radiation.setCameraOrientation("cam1", SphericalCoord(1.0, 45.0, 90.0))
1540 if not isinstance(camera_label, str)
or not camera_label.strip():
1541 raise ValueError(
"Camera label must be a non-empty string")
1542 radiation_wrapper.setCameraOrientation(self.
radiation_model, camera_label, direction)
1543 logger.debug(f
"Updated camera '{camera_label}' orientation")
1545 @require_plugin('radiation', 'query camera orientation')
1548 Get camera orientation.
1551 camera_label: Camera label string
1554 SphericalCoord orientation [radius, elevation, azimuth]
1557 >>> orientation = radiation.getCameraOrientation("cam1")
1558 >>> print(f"Camera orientation: {orientation}")
1560 if not isinstance(camera_label, str)
or not camera_label.strip():
1561 raise ValueError(
"Camera label must be a non-empty string")
1562 orientation_list = radiation_wrapper.getCameraOrientation(self.
radiation_model, camera_label)
1563 from .wrappers.DataTypes
import SphericalCoord
1564 return SphericalCoord(orientation_list[0], orientation_list[1], orientation_list[2])
1566 @require_plugin('radiation', 'query cameras')
1569 Get all camera labels.
1572 List of all camera label strings
1575 >>> cameras = radiation.getAllCameraLabels()
1576 >>> print(f"Available cameras: {cameras}")
1580 @require_plugin('radiation', 'configure camera spectral response')
1583 Set camera spectral response from global data.
1586 camera_label: Camera label
1587 band_label: Band label
1588 global_data: Global data label for spectral response curve
1591 >>> radiation.setCameraSpectralResponse("cam1", "red", "sensor_red_response")
1593 if not isinstance(camera_label, str)
or not camera_label.strip():
1594 raise ValueError(
"Camera label must be a non-empty string")
1595 validate_band_label(band_label,
"band_label",
"setCameraSpectralResponse")
1596 if not isinstance(global_data, str)
or not global_data.strip():
1597 raise ValueError(
"Global data label must be a non-empty string")
1599 radiation_wrapper.setCameraSpectralResponse(self.
radiation_model, camera_label, band_label, global_data)
1600 logger.debug(f
"Set spectral response for camera '{camera_label}', band '{band_label}'")
1602 @require_plugin('radiation', 'configure camera from library')
1605 Set camera spectral response from standard camera library.
1607 Uses pre-defined spectral response curves for common cameras.
1610 camera_label: Camera label
1611 camera_library_name: Standard camera name (e.g., "iPhone13", "NikonD850", "CanonEOS5D")
1614 >>> radiation.setCameraSpectralResponseFromLibrary("cam1", "iPhone13")
1616 if not isinstance(camera_label, str)
or not camera_label.strip():
1617 raise ValueError(
"Camera label must be a non-empty string")
1618 if not isinstance(camera_library_name, str)
or not camera_library_name.strip():
1619 raise ValueError(
"Camera library name must be a non-empty string")
1621 radiation_wrapper.setCameraSpectralResponseFromLibrary(self.
radiation_model, camera_label, camera_library_name)
1622 logger.debug(f
"Set camera '{camera_label}' response from library: {camera_library_name}")
1624 @require_plugin('radiation', 'get camera pixel data')
1627 Get camera pixel data for specific band.
1629 Retrieves raw pixel values for programmatic access and analysis.
1632 camera_label: Camera label
1633 band_label: Band label
1636 List of pixel values
1639 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1640 >>> print(f"Mean pixel value: {sum(pixels)/len(pixels)}")
1642 if not isinstance(camera_label, str)
or not camera_label.strip():
1643 raise ValueError(
"Camera label must be a non-empty string")
1644 validate_band_label(band_label,
"band_label",
"getCameraPixelData")
1646 return radiation_wrapper.getCameraPixelData(self.
radiation_model, camera_label, band_label)
1648 @require_plugin('radiation', 'set camera pixel data')
1649 def setCameraPixelData(self, camera_label: str, band_label: str, pixel_data: List[float]):
1651 Set camera pixel data for specific band.
1653 Allows programmatic modification of pixel values.
1656 camera_label: Camera label
1657 band_label: Band label
1658 pixel_data: List of pixel values
1661 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1662 >>> modified_pixels = [p * 1.2 for p in pixels] # Brighten by 20%
1663 >>> radiation.setCameraPixelData("cam1", "red", modified_pixels)
1665 if not isinstance(camera_label, str)
or not camera_label.strip():
1666 raise ValueError(
"Camera label must be a non-empty string")
1667 validate_band_label(band_label,
"band_label",
"setCameraPixelData")
1668 if not isinstance(pixel_data, (list, tuple)):
1669 raise ValueError(
"Pixel data must be a list or tuple")
1671 radiation_wrapper.setCameraPixelData(self.
radiation_model, camera_label, band_label, pixel_data)
1672 logger.debug(f
"Set pixel data for camera '{camera_label}', band '{band_label}': {len(pixel_data)} pixels")
1678 @require_plugin('radiation', 'add camera from library')
1680 position, lookat, antialiasing_samples: int = 1,
1681 band_labels: Optional[List[str]] =
None):
1683 Add radiation camera loading all properties from camera library.
1685 Loads camera intrinsic parameters (resolution, FOV, sensor size) and spectral
1686 response data from the camera library XML file. This is the recommended way to
1687 create realistic cameras with proper spectral responses.
1690 camera_label: Label for the camera instance
1691 library_camera_label: Label of camera in library (e.g., "Canon_20D", "iPhone11", "NikonD700")
1692 position: Camera position as vec3 or (x, y, z) tuple
1693 lookat: Lookat point as vec3 or (x, y, z) tuple
1694 antialiasing_samples: Number of ray samples per pixel. Default: 1
1695 band_labels: Optional custom band labels. If None, uses library defaults.
1698 RadiationModelError: If operation fails
1699 ValueError: If parameters are invalid
1702 Available cameras in plugins/radiation/camera_library/camera_library.xml include:
1703 - Canon_20D, Nikon_D700, Nikon_D50
1704 - iPhone11, iPhone12ProMAX
1705 - Additional cameras available in library
1708 >>> radiation.addRadiationCameraFromLibrary(
1709 ... camera_label="cam1",
1710 ... library_camera_label="iPhone11",
1711 ... position=(0, -5, 1),
1712 ... lookat=(0, 0, 0.5),
1713 ... antialiasing_samples=10
1716 validate_band_label(camera_label,
"camera_label",
"addRadiationCameraFromLibrary")
1719 radiation_wrapper.addRadiationCameraFromLibrary(
1721 position, lookat, antialiasing_samples, band_labels
1723 logger.info(f
"Added camera '{camera_label}' from library '{library_camera_label}'")
1724 except Exception
as e:
1727 @require_plugin('radiation', 'update camera parameters')
1730 Update camera parameters for an existing camera.
1732 Allows modification of camera properties after creation while preserving
1733 position, lookat direction, and spectral band configuration.
1736 camera_label: Label for the camera to update
1737 camera_properties: CameraProperties instance with new parameters
1740 RadiationModelError: If operation fails or camera doesn't exist
1741 ValueError: If parameters are invalid
1744 FOV_aspect_ratio is automatically recalculated from camera_resolution.
1745 Camera position and lookat are preserved.
1748 >>> props = CameraProperties(
1749 ... camera_resolution=(1920, 1080),
1751 ... lens_focal_length=0.085 # 85mm lens
1753 >>> radiation.updateCameraParameters("cam1", props)
1755 validate_band_label(camera_label,
"camera_label",
"updateCameraParameters")
1757 if not isinstance(camera_properties, CameraProperties):
1758 raise ValueError(
"camera_properties must be a CameraProperties instance")
1761 radiation_wrapper.updateCameraParameters(self.
radiation_model, camera_label, camera_properties)
1762 logger.debug(f
"Updated parameters for camera '{camera_label}'")
1763 except Exception
as e:
1766 @require_plugin('radiation', 'enable camera metadata')
1769 Enable automatic JSON metadata file writing for camera(s).
1771 When enabled, writeCameraImage() automatically creates a JSON metadata file
1772 alongside the image containing comprehensive camera and scene information.
1775 camera_labels: Single camera label (str) or list of camera labels (List[str])
1778 RadiationModelError: If operation fails
1779 ValueError: If parameters are invalid
1783 - Camera properties (model, lens, sensor specs)
1784 - Geographic location (latitude, longitude)
1785 - Acquisition settings (date, time, exposure, white balance)
1786 - Agronomic data (plant species, heights, phenology stages)
1789 >>> # Enable for single camera
1790 >>> radiation.enableCameraMetadata("cam1")
1792 >>> # Enable for multiple cameras
1793 >>> radiation.enableCameraMetadata(["cam1", "cam2", "cam3"])
1797 if isinstance(camera_labels, str):
1798 logger.info(f
"Enabled metadata for camera '{camera_labels}'")
1800 logger.info(f
"Enabled metadata for {len(camera_labels)} cameras")
1801 except Exception
as e:
1804 @require_plugin('radiation', 'write camera images')
1805 def writeCameraImage(self, camera: str, bands: List[str], imagefile_base: str,
1806 image_path: str =
"./", frame: int = -1,
1807 flux_to_pixel_conversion: float = 1.0) -> str:
1809 Write camera image to file and return output filename.
1812 camera: Camera label
1813 bands: List of band labels to include in the image
1814 imagefile_base: Base filename for output
1815 image_path: Output directory path (default: current directory)
1816 frame: Frame number to write (-1 for all frames)
1817 flux_to_pixel_conversion: Conversion factor from flux to pixel values
1820 Output filename string
1823 RadiationModelError: If camera image writing fails
1824 TypeError: If parameters have incorrect types
1827 if not isinstance(camera, str)
or not camera.strip():
1828 raise TypeError(
"Camera label must be a non-empty string")
1829 if not isinstance(bands, list)
or not bands:
1830 raise TypeError(
"Bands must be a non-empty list of strings")
1831 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
1832 raise TypeError(
"All band labels must be non-empty strings")
1833 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
1834 raise TypeError(
"Image file base must be a non-empty string")
1835 if not isinstance(image_path, str):
1836 raise TypeError(
"Image path must be a string")
1837 if not isinstance(frame, int):
1838 raise TypeError(
"Frame must be an integer")
1839 if not isinstance(flux_to_pixel_conversion, (int, float))
or flux_to_pixel_conversion <= 0:
1840 raise TypeError(
"Flux to pixel conversion must be a positive number")
1842 filename = radiation_wrapper.writeCameraImage(
1844 image_path, frame, flux_to_pixel_conversion)
1846 logger.info(f
"Camera image written to: {filename}")
1849 @require_plugin('radiation', 'write normalized camera images')
1851 image_path: str =
"./", frame: int = -1) -> str:
1853 Write normalized camera image to file and return output filename.
1856 camera: Camera label
1857 bands: List of band labels to include in the image
1858 imagefile_base: Base filename for output
1859 image_path: Output directory path (default: current directory)
1860 frame: Frame number to write (-1 for all frames)
1863 Output filename string
1866 RadiationModelError: If normalized camera image writing fails
1867 TypeError: If parameters have incorrect types
1870 if not isinstance(camera, str)
or not camera.strip():
1871 raise TypeError(
"Camera label must be a non-empty string")
1872 if not isinstance(bands, list)
or not bands:
1873 raise TypeError(
"Bands must be a non-empty list of strings")
1874 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
1875 raise TypeError(
"All band labels must be non-empty strings")
1876 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
1877 raise TypeError(
"Image file base must be a non-empty string")
1878 if not isinstance(image_path, str):
1879 raise TypeError(
"Image path must be a string")
1880 if not isinstance(frame, int):
1881 raise TypeError(
"Frame must be an integer")
1883 filename = radiation_wrapper.writeNormCameraImage(
1884 self.
radiation_model, camera, bands, imagefile_base, image_path, frame)
1886 logger.info(f
"Normalized camera image written to: {filename}")
1889 @require_plugin('radiation', 'write camera image data')
1891 image_path: str =
"./", frame: int = -1):
1893 Write camera image data to file (ASCII format).
1896 camera: Camera label
1898 imagefile_base: Base filename for output
1899 image_path: Output directory path (default: current directory)
1900 frame: Frame number to write (-1 for all frames)
1903 RadiationModelError: If camera image data writing fails
1904 TypeError: If parameters have incorrect types
1907 if not isinstance(camera, str)
or not camera.strip():
1908 raise TypeError(
"Camera label must be a non-empty string")
1909 if not isinstance(band, str)
or not band.strip():
1910 raise TypeError(
"Band label must be a non-empty string")
1911 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
1912 raise TypeError(
"Image file base must be a non-empty string")
1913 if not isinstance(image_path, str):
1914 raise TypeError(
"Image path must be a string")
1915 if not isinstance(frame, int):
1916 raise TypeError(
"Frame must be an integer")
1918 radiation_wrapper.writeCameraImageData(
1919 self.
radiation_model, camera, band, imagefile_base, image_path, frame)
1921 logger.info(f
"Camera image data written for camera {camera}, band {band}")
1923 @require_plugin('radiation', 'write image bounding boxes')
1925 primitive_data_labels=
None, object_data_labels=
None,
1926 object_class_ids=
None, image_file: str =
"",
1927 classes_txt_file: str =
"classes.txt",
1928 image_path: str =
"./"):
1930 Write image bounding boxes for object detection training.
1932 Supports both single and multiple data labels. Either provide primitive_data_labels
1933 or object_data_labels, not both.
1936 camera_label: Camera label
1937 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
1938 object_data_labels: Single object data label (str) or list of object data labels
1939 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
1940 image_file: Image filename
1941 classes_txt_file: Classes definition file (default: "classes.txt")
1942 image_path: Image output path (default: current directory)
1945 RadiationModelError: If bounding box writing fails
1946 TypeError: If parameters have incorrect types
1947 ValueError: If both primitive and object data labels are provided, or neither
1950 if primitive_data_labels
is not None and object_data_labels
is not None:
1951 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
1952 if primitive_data_labels
is None and object_data_labels
is None:
1953 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
1956 if not isinstance(camera_label, str)
or not camera_label.strip():
1957 raise TypeError(
"Camera label must be a non-empty string")
1958 if not isinstance(image_file, str)
or not image_file.strip():
1959 raise TypeError(
"Image file must be a non-empty string")
1960 if not isinstance(classes_txt_file, str):
1961 raise TypeError(
"Classes txt file must be a string")
1962 if not isinstance(image_path, str):
1963 raise TypeError(
"Image path must be a string")
1966 if primitive_data_labels
is not None:
1967 if isinstance(primitive_data_labels, str):
1969 if not isinstance(object_class_ids, int):
1970 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
1971 radiation_wrapper.writeImageBoundingBoxes(
1973 object_class_ids, image_file, classes_txt_file, image_path)
1974 logger.info(f
"Image bounding boxes written for primitive data: {primitive_data_labels}")
1976 elif isinstance(primitive_data_labels, list):
1978 if not isinstance(object_class_ids, list):
1979 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
1980 if len(primitive_data_labels) != len(object_class_ids):
1981 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
1982 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
1983 raise TypeError(
"All primitive data labels must be non-empty strings")
1984 if not all(isinstance(cid, int)
for cid
in object_class_ids):
1985 raise TypeError(
"All object class IDs must be integers")
1987 radiation_wrapper.writeImageBoundingBoxesVector(
1989 object_class_ids, image_file, classes_txt_file, image_path)
1990 logger.info(f
"Image bounding boxes written for {len(primitive_data_labels)} primitive data labels")
1992 raise TypeError(
"primitive_data_labels must be a string or list of strings")
1995 elif object_data_labels
is not None:
1996 if isinstance(object_data_labels, str):
1998 if not isinstance(object_class_ids, int):
1999 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2000 radiation_wrapper.writeImageBoundingBoxes_ObjectData(
2002 object_class_ids, image_file, classes_txt_file, image_path)
2003 logger.info(f
"Image bounding boxes written for object data: {object_data_labels}")
2005 elif isinstance(object_data_labels, list):
2007 if not isinstance(object_class_ids, list):
2008 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2009 if len(object_data_labels) != len(object_class_ids):
2010 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2011 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2012 raise TypeError(
"All object data labels must be non-empty strings")
2013 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2014 raise TypeError(
"All object class IDs must be integers")
2016 radiation_wrapper.writeImageBoundingBoxes_ObjectDataVector(
2018 object_class_ids, image_file, classes_txt_file, image_path)
2019 logger.info(f
"Image bounding boxes written for {len(object_data_labels)} object data labels")
2021 raise TypeError(
"object_data_labels must be a string or list of strings")
2023 @require_plugin('radiation', 'write image segmentation masks')
2025 primitive_data_labels=
None, object_data_labels=
None,
2026 object_class_ids=
None, json_filename: str =
"",
2027 image_file: str =
"", append_file: bool =
False):
2029 Write image segmentation masks in COCO JSON format.
2031 Supports both single and multiple data labels. Either provide primitive_data_labels
2032 or object_data_labels, not both.
2035 camera_label: Camera label
2036 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
2037 object_data_labels: Single object data label (str) or list of object data labels
2038 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
2039 json_filename: JSON output filename
2040 image_file: Image filename
2041 append_file: Whether to append to existing JSON file
2044 RadiationModelError: If segmentation mask writing fails
2045 TypeError: If parameters have incorrect types
2046 ValueError: If both primitive and object data labels are provided, or neither
2049 if primitive_data_labels
is not None and object_data_labels
is not None:
2050 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
2051 if primitive_data_labels
is None and object_data_labels
is None:
2052 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
2055 if not isinstance(camera_label, str)
or not camera_label.strip():
2056 raise TypeError(
"Camera label must be a non-empty string")
2057 if not isinstance(json_filename, str)
or not json_filename.strip():
2058 raise TypeError(
"JSON filename must be a non-empty string")
2059 if not isinstance(image_file, str)
or not image_file.strip():
2060 raise TypeError(
"Image file must be a non-empty string")
2061 if not isinstance(append_file, bool):
2062 raise TypeError(
"append_file must be a boolean")
2065 if primitive_data_labels
is not None:
2066 if isinstance(primitive_data_labels, str):
2068 if not isinstance(object_class_ids, int):
2069 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
2070 radiation_wrapper.writeImageSegmentationMasks(
2072 object_class_ids, json_filename, image_file, append_file)
2073 logger.info(f
"Image segmentation masks written for primitive data: {primitive_data_labels}")
2075 elif isinstance(primitive_data_labels, list):
2077 if not isinstance(object_class_ids, list):
2078 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
2079 if len(primitive_data_labels) != len(object_class_ids):
2080 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
2081 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
2082 raise TypeError(
"All primitive data labels must be non-empty strings")
2083 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2084 raise TypeError(
"All object class IDs must be integers")
2086 radiation_wrapper.writeImageSegmentationMasksVector(
2088 object_class_ids, json_filename, image_file, append_file)
2089 logger.info(f
"Image segmentation masks written for {len(primitive_data_labels)} primitive data labels")
2091 raise TypeError(
"primitive_data_labels must be a string or list of strings")
2094 elif object_data_labels
is not None:
2095 if isinstance(object_data_labels, str):
2097 if not isinstance(object_class_ids, int):
2098 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2099 radiation_wrapper.writeImageSegmentationMasks_ObjectData(
2101 object_class_ids, json_filename, image_file, append_file)
2102 logger.info(f
"Image segmentation masks written for object data: {object_data_labels}")
2104 elif isinstance(object_data_labels, list):
2106 if not isinstance(object_class_ids, list):
2107 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2108 if len(object_data_labels) != len(object_class_ids):
2109 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2110 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2111 raise TypeError(
"All object data labels must be non-empty strings")
2112 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2113 raise TypeError(
"All object class IDs must be integers")
2115 radiation_wrapper.writeImageSegmentationMasks_ObjectDataVector(
2117 object_class_ids, json_filename, image_file, append_file)
2118 logger.info(f
"Image segmentation masks written for {len(object_data_labels)} object data labels")
2120 raise TypeError(
"object_data_labels must be a string or list of strings")
2122 @require_plugin('radiation', 'auto-calibrate camera image')
2124 green_band_label: str, blue_band_label: str,
2125 output_file_path: str, print_quality_report: bool =
False,
2126 algorithm: str =
"MATRIX_3X3_AUTO",
2127 ccm_export_file_path: str =
"") -> str:
2129 Auto-calibrate camera image with color correction and return output filename.
2132 camera_label: Camera label
2133 red_band_label: Red band label
2134 green_band_label: Green band label
2135 blue_band_label: Blue band label
2136 output_file_path: Output file path
2137 print_quality_report: Whether to print quality report
2138 algorithm: Color correction algorithm ("DIAGONAL_ONLY", "MATRIX_3X3_AUTO", "MATRIX_3X3_FORCE")
2139 ccm_export_file_path: Path to export color correction matrix (optional)
2142 Output filename string
2145 RadiationModelError: If auto-calibration fails
2146 TypeError: If parameters have incorrect types
2147 ValueError: If algorithm is not valid
2150 if not isinstance(camera_label, str)
or not camera_label.strip():
2151 raise TypeError(
"Camera label must be a non-empty string")
2152 if not isinstance(red_band_label, str)
or not red_band_label.strip():
2153 raise TypeError(
"Red band label must be a non-empty string")
2154 if not isinstance(green_band_label, str)
or not green_band_label.strip():
2155 raise TypeError(
"Green band label must be a non-empty string")
2156 if not isinstance(blue_band_label, str)
or not blue_band_label.strip():
2157 raise TypeError(
"Blue band label must be a non-empty string")
2158 if not isinstance(output_file_path, str)
or not output_file_path.strip():
2159 raise TypeError(
"Output file path must be a non-empty string")
2160 if not isinstance(print_quality_report, bool):
2161 raise TypeError(
"print_quality_report must be a boolean")
2162 if not isinstance(ccm_export_file_path, str):
2163 raise TypeError(
"ccm_export_file_path must be a string")
2168 "MATRIX_3X3_AUTO": 1,
2169 "MATRIX_3X3_FORCE": 2
2172 if algorithm
not in algorithm_map:
2173 raise ValueError(f
"Invalid algorithm: {algorithm}. Must be one of: {list(algorithm_map.keys())}")
2175 algorithm_int = algorithm_map[algorithm]
2177 filename = radiation_wrapper.autoCalibrateCameraImage(
2179 blue_band_label, output_file_path, print_quality_report,
2180 algorithm_int, ccm_export_file_path)
2182 logger.info(f
"Auto-calibrated camera image written to: {filename}")
2186 """Get information about the radiation plugin."""
2187 registry = get_plugin_registry()
2188 return registry.get_plugin_capabilities(
'radiation')
Camera properties for radiation model cameras.
__init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05, HFOV=20.0, FOV_aspect_ratio=0.0, lens_focal_length=0.05, sensor_width_mm=35.0, model="generic", lens_make="", lens_model="", lens_specification="", exposure="auto", shutter_speed=1.0/125.0, white_balance="auto")
Initialize camera properties with defaults matching C++ CameraProperties.
to_array(self)
Convert to array format expected by C++ interface.
Raised when RadiationModel operations fail.
High-level interface for radiation modeling and ray tracing.
setDirectRayCount(self, str band_label, int ray_count)
Set direct ray count for radiation band.
float integrateSpectrum(self, object_spectrum, float wavelength_min=None, float wavelength_max=None, int source_id=None, camera_spectrum=None)
Integrate spectrum with optional source/camera spectra and wavelength range.
__del__(self)
Destructor to ensure GPU resources freed even without 'with' statement.
getCameraPosition(self, str camera_label)
Get camera position.
disableMessages(self)
Disable RadiationModel status messages.
int addRectangleRadiationSource(self, position, size, rotation)
Add a rectangle (planar) radiation source.
interpolateSpectrumFromPrimitiveData(self, List[int] primitive_uuids, List[str] spectra_labels, List[float] values, str primitive_data_query_label, str primitive_data_radprop_label)
Interpolate spectral properties based on primitive data values.
runBand(self, band_label)
Run radiation simulation for single band or multiple bands.
setSourceSpectrum(self, source_id, spectrum)
Set radiation spectrum for source(s).
str writeCameraImage(self, str camera, List[str] bands, str imagefile_base, str image_path="./", int frame=-1, float flux_to_pixel_conversion=1.0)
Write camera image to file and return output filename.
List[float] getCameraPixelData(self, str camera_label, str band_label)
Get camera pixel data for specific band.
setCameraOrientation(self, str camera_label, direction)
Set camera orientation.
get_native_ptr(self)
Get native pointer for advanced operations.
addRadiationBand(self, str band_label, float wavelength_min=None, float wavelength_max=None)
Add radiation band with optional wavelength bounds.
setScatteringDepth(self, str label, int depth)
Set scattering depth for radiation band.
getSourcePosition(self, int source_id)
Get position of a radiation source.
addRadiationCameraFromLibrary(self, str camera_label, str library_camera_label, position, lookat, int antialiasing_samples=1, Optional[List[str]] band_labels=None)
Add radiation camera loading all properties from camera library.
updateCameraParameters(self, str camera_label, CameraProperties camera_properties)
Update camera parameters for an existing camera.
setCameraPosition(self, str camera_label, position)
Set camera position.
enableMessages(self)
Enable RadiationModel status messages.
setCameraSpectralResponse(self, str camera_label, str band_label, str global_data)
Set camera spectral response from global data.
interpolateSpectrumFromObjectData(self, List[int] object_ids, List[str] spectra_labels, List[float] values, str object_data_query_label, str primitive_data_radprop_label)
Interpolate spectral properties based on object data values.
setSourceFlux(self, source_id, str label, float flux)
Set source flux for single source or multiple sources.
writeImageBoundingBoxes(self, str camera_label, primitive_data_labels=None, object_data_labels=None, object_class_ids=None, str image_file="", str classes_txt_file="classes.txt", str image_path="./")
Write image bounding boxes for object detection training.
deleteRadiationSource(self, int source_id)
Delete a radiation source.
setSourcePosition(self, int source_id, position)
Set position of a radiation source.
setDiffuseRadiationExtinctionCoeff(self, str label, float K, peak_direction)
Set diffuse radiation extinction coefficient with directional bias.
setCameraPixelData(self, str camera_label, str band_label, List[float] pixel_data)
Set camera pixel data for specific band.
getCameraOrientation(self, str camera_label)
Get camera orientation.
enforcePeriodicBoundary(self, str boundary)
Enforce periodic boundary conditions.
enableCameraMetadata(self, camera_labels)
Enable automatic JSON metadata file writing for camera(s).
scaleSpectrumRandomly(self, str existing_label, str new_label, float min_scale, float max_scale)
Scale spectrum with random factor and store as new label.
setSourceSpectrumIntegral(self, int source_id, float source_integral, float wavelength_min=None, float wavelength_max=None)
Set source spectrum integral value.
setDiffuseRayCount(self, str band_label, int ray_count)
Set diffuse ray count for radiation band.
setCameraLookat(self, str camera_label, lookat)
Set camera lookat point.
int addDiskRadiationSource(self, position, float radius, rotation)
Add a disk (circular planar) radiation source.
getCameraLookat(self, str camera_label)
Get camera lookat point.
float getSourceFlux(self, int source_id, str label)
Get source flux for band.
blendSpectra(self, str new_label, List[str] spectrum_labels, List[float] weights)
Blend multiple spectra with specified weights.
float calculateGtheta(self, view_direction)
Calculate G-function (geometry factor) for given view direction.
int addSunSphereRadiationSource(self, float radius, float zenith, float azimuth, float position_scaling=1.0, float angular_width=0.53, float flux_scaling=1.0)
Add sun sphere radiation source.
bool doesBandExist(self, str label)
Check if a radiation band exists.
setCameraSpectralResponseFromLibrary(self, str camera_label, str camera_library_name)
Set camera spectral response from standard camera library.
setDiffuseSpectrumIntegral(self, float spectrum_integral, float wavelength_min=None, float wavelength_max=None, str band_label=None)
Set diffuse spectrum integral.
scaleSpectrum(self, str existing_label, new_label_or_scale, float scale_factor=None)
Scale spectrum in-place or to new label.
float getSkyEnergy(self)
Get total sky energy.
disableEmission(self, str label)
Disable emission for radiation band.
copyRadiationBand(self, str old_label, str new_label, float wavelength_min=None, float wavelength_max=None)
Copy existing radiation band to new label, optionally with new wavelength range.
enableEmission(self, str label)
Enable emission for radiation band.
addRadiationCamera(self, str camera_label, List[str] band_labels, position, lookat_or_direction, camera_properties=None, int antialiasing_samples=100)
Add a radiation camera to the simulation.
List[float] getTotalAbsorbedFlux(self)
Get total absorbed flux for all primitives.
int addSphereRadiationSource(self, position, float radius)
Add spherical radiation source.
float integrateSourceSpectrum(self, int source_id, float wavelength_min, float wavelength_max)
Integrate source spectrum over wavelength range.
__init__(self, Context context)
Initialize RadiationModel with graceful plugin handling.
__enter__(self)
Context manager entry.
optionalOutputPrimitiveData(self, str label)
Enable optional primitive data output.
str autoCalibrateCameraImage(self, str camera_label, str red_band_label, str green_band_label, str blue_band_label, str output_file_path, bool print_quality_report=False, str algorithm="MATRIX_3X3_AUTO", str ccm_export_file_path="")
Auto-calibrate camera image with color correction and return output filename.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
updateGeometry(self, Optional[List[int]] uuids=None)
Update geometry in radiation model.
setDiffuseSpectrum(self, band_label, str spectrum_label)
Set diffuse spectrum from global data label.
setMinScatterEnergy(self, str label, float energy)
Set minimum scatter energy for radiation band.
float getDiffuseFlux(self, str band_label)
Get diffuse flux for band.
setDiffuseRadiationFlux(self, str label, float flux)
Set diffuse radiation flux for band.
writeImageSegmentationMasks(self, str camera_label, primitive_data_labels=None, object_data_labels=None, object_class_ids=None, str json_filename="", str image_file="", bool append_file=False)
Write image segmentation masks in COCO JSON format.
writeCameraImageData(self, str camera, str band, str imagefile_base, str image_path="./", int frame=-1)
Write camera image data to file (ASCII format).
int addCollimatedRadiationSource(self, direction=None)
Add collimated radiation source.
List[str] getAllCameraLabels(self)
Get all camera labels.
str writeNormCameraImage(self, str camera, List[str] bands, str imagefile_base, str image_path="./", int frame=-1)
Write normalized camera image to file and return output filename.
dict getPluginInfo(self)
Get information about the radiation plugin.
blendSpectraRandomly(self, str new_label, List[str] spectrum_labels)
Blend multiple spectra with random weights.
getNativePtr(self)
Get native pointer for advanced operations.
_radiation_working_directory()
Context manager that temporarily changes working directory to where RadiationModel assets are located...