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.60 with camera_zoom support.
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", camera_zoom=1.0):
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"
126 camera_zoom: Camera optical zoom multiplier. 1.0 = no zoom, 2.0 = 2x zoom.
127 Scales effective HFOV: effective_HFOV = HFOV / camera_zoom. Default: 1.0
131 if camera_resolution
is None:
132 self.camera_resolution = (512, 512)
134 if isinstance(camera_resolution, (list, tuple))
and len(camera_resolution) == 2:
135 self.camera_resolution = (int(camera_resolution[0]), int(camera_resolution[1]))
137 raise ValueError(
"camera_resolution must be a tuple or list of 2 integers")
141 if focal_plane_distance <= 0:
142 raise ValueError(
"focal_plane_distance must be greater than 0")
143 if lens_diameter < 0:
144 raise ValueError(
"lens_diameter must be non-negative")
145 if HFOV <= 0
or HFOV > 180:
146 raise ValueError(
"HFOV must be between 0 and 180 degrees")
147 if FOV_aspect_ratio < 0:
148 raise ValueError(
"FOV_aspect_ratio must be non-negative (0 = auto-calculate)")
149 if lens_focal_length <= 0:
150 raise ValueError(
"lens_focal_length must be greater than 0")
151 if sensor_width_mm <= 0:
152 raise ValueError(
"sensor_width_mm must be greater than 0")
153 if shutter_speed <= 0:
154 raise ValueError(
"shutter_speed must be greater than 0")
156 raise ValueError(
"camera_zoom must be greater than 0")
168 if not isinstance(model, str):
169 raise ValueError(
"model must be a string")
170 if not isinstance(lens_make, str):
171 raise ValueError(
"lens_make must be a string")
172 if not isinstance(lens_model, str):
173 raise ValueError(
"lens_model must be a string")
174 if not isinstance(lens_specification, str):
175 raise ValueError(
"lens_specification must be a string")
176 if not isinstance(exposure, str):
177 raise ValueError(
"exposure must be a string")
178 if not isinstance(white_balance, str):
179 raise ValueError(
"white_balance must be a string")
182 if exposure
not in [
"auto",
"manual"]
and not exposure.startswith(
"ISO"):
183 raise ValueError(
"exposure must be 'auto', 'manual', or 'ISOXXX' (e.g., 'ISO100')")
186 if white_balance
not in [
"auto",
"off"]:
187 raise ValueError(
"white_balance must be 'auto' or 'off'")
189 self.
model = str(model)
198 Convert to array format expected by C++ interface.
200 Note: Returns numeric fields only. String fields (model, lens_make, etc.) are
201 currently initialized with defaults in the C++ wrapper and cannot be set via
202 this interface. Use the upcoming camera library methods for full metadata control.
205 List of 10 float values: [resolution_x, resolution_y, focal_distance, lens_diameter,
206 HFOV, FOV_aspect_ratio, lens_focal_length, sensor_width_mm,
207 shutter_speed, camera_zoom]
210 float(self.camera_resolution[0]),
211 float(self.camera_resolution[1]),
223 return (f
"CameraProperties("
224 f
"camera_resolution={self.camera_resolution}, "
225 f
"focal_plane_distance={self.focal_plane_distance}, "
226 f
"lens_diameter={self.lens_diameter}, "
227 f
"HFOV={self.HFOV}, "
228 f
"FOV_aspect_ratio={self.FOV_aspect_ratio}, "
229 f
"lens_focal_length={self.lens_focal_length}, "
230 f
"sensor_width_mm={self.sensor_width_mm}, "
231 f
"model='{self.model}', "
232 f
"lens_make='{self.lens_make}', "
233 f
"lens_model='{self.lens_model}', "
234 f
"lens_specification='{self.lens_specification}', "
235 f
"exposure='{self.exposure}', "
236 f
"shutter_speed={self.shutter_speed}, "
237 f
"white_balance='{self.white_balance}', "
238 f
"camera_zoom={self.camera_zoom})")
243 Metadata for radiation camera image export (Helios v1.3.58+).
245 This class encapsulates comprehensive metadata for camera images including
246 camera properties, location, acquisition settings, image processing, and
247 agronomic properties derived from plant architecture data.
251 """Camera intrinsic properties for metadata export."""
252 def __init__(self, height=512, width=512, channels=3, type="rgb",
253 focal_length=50.0, aperture="f/2.8", sensor_width=35.0,
254 sensor_height=24.0, model="generic", lens_make="",
255 lens_model="", lens_specification="", exposure="auto",
256 shutter_speed=0.008, white_balance="auto"):
258 self.
width = int(width)
274 """Geographic location properties."""
275 def __init__(self, latitude=0.0, longitude=0.0):
280 """Image acquisition properties."""
281 def __init__(self, date="", time="", UTC_offset=0.0, camera_height_m=0.0,
282 camera_angle_deg=0.0, light_source="sunlight"):
284 self.
time = str(time)
291 """Image processing corrections applied to the image."""
292 def __init__(self, saturation_adjustment=1.0, brightness_adjustment=1.0,
293 contrast_adjustment=1.0, color_space="linear"):
300 """Agronomic properties derived from plant architecture data."""
301 def __init__(self, plant_species=None, plant_count=None, plant_height_m=None,
302 plant_age_days=None, plant_stage=None, leaf_area_m2=None,
304 self.
plant_species = plant_species
if plant_species
is not None else []
305 self.
plant_count = plant_count
if plant_count
is not None else []
314 Initialize CameraMetadata with default values.
317 path: Full path to the associated image file. Default: ""
319 self.path = str(path)
327 return (f
"CameraMetadata(path='{self.path}', "
328 f
"camera={self.camera_properties.model}, "
329 f
"resolution={self.camera_properties.width}x{self.camera_properties.height}, "
330 f
"location=({self.location_properties.latitude},{self.location_properties.longitude}))")
335 High-level interface for radiation modeling and ray tracing.
337 This class provides a user-friendly wrapper around the native Helios
338 radiation plugin with automatic plugin availability checking and
339 graceful error handling.
344 Initialize RadiationModel with graceful plugin handling.
347 context: Helios Context instance
350 TypeError: If context is not a Context instance
351 RadiationModelError: If radiation plugin is not available
354 if not isinstance(context, Context):
355 raise TypeError(f
"RadiationModel requires a Context instance, got {type(context).__name__}")
361 registry = get_plugin_registry()
363 if not registry.is_plugin_available(
'radiation'):
365 plugin_info = registry.get_plugin_capabilities()
366 available_plugins = registry.get_available_plugins()
369 "RadiationModel requires the 'radiation' plugin which is not available.\n\n"
370 "The radiation plugin provides GPU-accelerated ray tracing using OptiX.\n"
371 "System requirements:\n"
372 "- NVIDIA GPU with CUDA support\n"
373 "- CUDA Toolkit installed\n"
374 "- OptiX runtime (bundled with PyHelios)\n\n"
375 "To enable radiation modeling:\n"
376 "1. Build PyHelios with radiation plugin:\n"
377 " build_scripts/build_helios --plugins radiation\n"
378 "2. Or build with multiple plugins:\n"
379 " build_scripts/build_helios --plugins radiation,visualizer,weberpenntree\n"
380 f
"\nCurrently available plugins: {available_plugins}"
384 alternatives = registry.suggest_alternatives(
'radiation')
386 error_msg += f
"\n\nAlternative plugins available: {alternatives}"
387 error_msg +=
"\nConsider using energybalance or leafoptics for thermal modeling."
394 self.
radiation_model = radiation_wrapper.createRadiationModel(context.getNativePtr())
397 "Failed to create RadiationModel instance. "
398 "This may indicate a problem with the native library or GPU initialization."
400 logger.info(
"RadiationModel created successfully")
402 except Exception
as e:
406 """Context manager entry."""
409 def __exit__(self, exc_type, exc_value, traceback):
410 """Context manager exit with proper cleanup."""
414 logger.debug(
"RadiationModel destroyed successfully")
415 except Exception
as e:
416 logger.warning(f
"Error destroying RadiationModel: {e}")
421 """Destructor to ensure GPU resources freed even without 'with' statement."""
422 if hasattr(self,
'radiation_model')
and self.
radiation_model is not None:
426 except Exception
as e:
428 warnings.warn(f
"Error in RadiationModel.__del__: {e}")
431 """Get native pointer for advanced operations."""
435 """Get native pointer for advanced operations. (Legacy naming for compatibility)"""
438 @require_plugin('radiation', 'disable status messages')
440 """Disable RadiationModel status messages."""
443 @require_plugin('radiation', 'enable status messages')
445 """Enable RadiationModel status messages."""
448 @require_plugin('radiation', 'add radiation band')
449 def addRadiationBand(self, band_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
451 Add radiation band with optional wavelength bounds.
454 band_label: Name/label for the radiation band
455 wavelength_min: Optional minimum wavelength (μm)
456 wavelength_max: Optional maximum wavelength (μm)
459 validate_band_label(band_label,
"band_label",
"addRadiationBand")
460 if wavelength_min
is not None and wavelength_max
is not None:
461 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"addRadiationBand")
462 radiation_wrapper.addRadiationBandWithWavelengths(self.
radiation_model, band_label, wavelength_min, wavelength_max)
463 logger.debug(f
"Added radiation band {band_label}: {wavelength_min}-{wavelength_max} μm")
466 logger.debug(f
"Added radiation band: {band_label}")
468 @require_plugin('radiation', 'copy radiation band')
469 @validate_radiation_band_params
470 def copyRadiationBand(self, old_label: str, new_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
472 Copy existing radiation band to new label, optionally with new wavelength range.
475 old_label: Existing band label to copy
476 new_label: New label for the copied band
477 wavelength_min: Optional minimum wavelength for new band (μm)
478 wavelength_max: Optional maximum wavelength for new band (μm)
481 >>> # Copy band with same wavelength range
482 >>> radiation.copyRadiationBand("SW", "SW_copy")
484 >>> # Copy band with different wavelength range
485 >>> radiation.copyRadiationBand("full_spectrum", "PAR", 400, 700)
487 if wavelength_min
is not None and wavelength_max
is not None:
488 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"copyRadiationBand")
490 radiation_wrapper.copyRadiationBand(self.
radiation_model, old_label, new_label, wavelength_min, wavelength_max)
491 if wavelength_min
is not None:
492 logger.debug(f
"Copied radiation band {old_label} to {new_label} with wavelengths {wavelength_min}-{wavelength_max} μm")
494 logger.debug(f
"Copied radiation band {old_label} to {new_label}")
496 @require_plugin('radiation', 'add radiation source')
497 @validate_collimated_source_params
500 Add collimated radiation source.
503 direction: Optional direction vector. Can be tuple (x, y, z), vec3, or None for default direction.
508 if direction
is None:
509 source_id = radiation_wrapper.addCollimatedRadiationSourceDefault(self.
radiation_model)
512 if hasattr(direction,
'x')
and hasattr(direction,
'y')
and hasattr(direction,
'z'):
514 x, y, z = direction.x, direction.y, direction.z
515 elif hasattr(direction,
'radius')
and hasattr(direction,
'elevation')
and hasattr(direction,
'azimuth'):
519 elevation = direction.elevation
520 azimuth = direction.azimuth
521 x = r * math.cos(elevation) * math.cos(azimuth)
522 y = r * math.cos(elevation) * math.sin(azimuth)
523 z = r * math.sin(elevation)
527 if len(direction) != 3:
528 raise TypeError(f
"Direction must be a 3-element tuple, vec3, or SphericalCoord, got {type(direction).__name__} with {len(direction)} elements")
530 except (TypeError, AttributeError):
532 raise TypeError(f
"Direction must be a tuple, vec3, or SphericalCoord, got {type(direction).__name__}")
533 source_id = radiation_wrapper.addCollimatedRadiationSourceVec3(self.
radiation_model, x, y, z)
535 logger.debug(f
"Added collimated radiation source: ID {source_id}")
538 @require_plugin('radiation', 'add spherical radiation source')
539 @validate_sphere_source_params
542 Add spherical radiation source.
545 position: Position of the source. Can be tuple (x, y, z) or vec3.
546 radius: Radius of the spherical source
552 if hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z'):
554 x, y, z = position.x, position.y, position.z
558 source_id = radiation_wrapper.addSphereRadiationSource(self.
radiation_model, x, y, z, radius)
559 logger.debug(f
"Added sphere radiation source: ID {source_id} at ({x}, {y}, {z}) with radius {radius}")
562 @require_plugin('radiation', 'add sun radiation source')
563 @validate_sun_sphere_params
565 position_scaling: float = 1.0, angular_width: float = 0.53,
566 flux_scaling: float = 1.0) -> int:
568 Add sun sphere radiation source.
571 radius: Radius of the sun sphere
572 zenith: Zenith angle (degrees)
573 azimuth: Azimuth angle (degrees)
574 position_scaling: Position scaling factor
575 angular_width: Angular width of the sun (degrees)
576 flux_scaling: Flux scaling factor
581 source_id = radiation_wrapper.addSunSphereRadiationSource(
582 self.
radiation_model, radius, zenith, azimuth, position_scaling, angular_width, flux_scaling
584 logger.debug(f
"Added sun radiation source: ID {source_id}")
587 @require_plugin('radiation', 'set source position')
590 Set position of a radiation source.
592 Allows dynamic repositioning of radiation sources during simulation,
593 useful for time-series modeling or moving light sources.
596 source_id: ID of the radiation source
597 position: New position as vec3, SphericalCoord, or list/tuple [x, y, z]
600 >>> source_id = radiation.addCollimatedRadiationSource()
601 >>> radiation.setSourcePosition(source_id, [10, 20, 30])
602 >>> from pyhelios.types import vec3
603 >>> radiation.setSourcePosition(source_id, vec3(15, 25, 35))
605 if not isinstance(source_id, int)
or source_id < 0:
606 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
607 radiation_wrapper.setSourcePosition(self.
radiation_model, source_id, position)
608 logger.debug(f
"Updated position for radiation source {source_id}")
610 @require_plugin('radiation', 'add rectangle radiation source')
613 Add a rectangle (planar) radiation source.
615 Rectangle sources are ideal for modeling artificial lighting such as
616 LED panels, grow lights, or window light sources.
619 position: Center position as vec3 or list [x, y, z]
620 size: Rectangle dimensions as vec2 or list [width, height]
621 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
627 >>> from pyhelios.types import vec3, vec2
628 >>> source_id = radiation.addRectangleRadiationSource(
629 ... position=vec3(0, 0, 5),
631 ... rotation=vec3(0, 0, 0)
633 >>> radiation.setSourceFlux(source_id, "PAR", 500.0)
635 return radiation_wrapper.addRectangleRadiationSource(self.
radiation_model, position, size, rotation)
637 @require_plugin('radiation', 'add disk radiation source')
640 Add a disk (circular planar) radiation source.
642 Disk sources are useful for modeling circular light sources such as
643 spotlights, circular LED arrays, or solar simulators.
646 position: Center position as vec3 or list [x, y, z]
648 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
654 >>> from pyhelios.types import vec3
655 >>> source_id = radiation.addDiskRadiationSource(
656 ... position=vec3(0, 0, 5),
658 ... rotation=vec3(0, 0, 0)
660 >>> radiation.setSourceFlux(source_id, "PAR", 300.0)
663 raise ValueError(f
"Radius must be positive, got {radius}")
664 return radiation_wrapper.addDiskRadiationSource(self.
radiation_model, position, radius, rotation)
667 @require_plugin('radiation', 'manage source spectrum')
670 Set radiation spectrum for source(s).
672 Spectral distributions define how radiation intensity varies with wavelength,
673 essential for realistic modeling of different light sources (sunlight, LEDs, etc.).
676 source_id: Source ID (int) or list of source IDs
678 - Spectrum data as list of (wavelength, value) tuples
679 - Global data label string
682 >>> # Define custom LED spectrum
684 ... (400, 0.0), (450, 0.3), (500, 0.8),
685 ... (550, 0.5), (600, 0.2), (700, 0.0)
687 >>> radiation.setSourceSpectrum(source_id, led_spectrum)
689 >>> # Use predefined spectrum from global data
690 >>> radiation.setSourceSpectrum(source_id, "D65_illuminant")
692 >>> # Apply same spectrum to multiple sources
693 >>> radiation.setSourceSpectrum([src1, src2, src3], led_spectrum)
695 radiation_wrapper.setSourceSpectrum(self.
radiation_model, source_id, spectrum)
696 logger.debug(f
"Set spectrum for source(s) {source_id}")
698 @require_plugin('radiation', 'configure source spectrum')
700 wavelength_min: float =
None, wavelength_max: float =
None):
702 Set source spectrum integral value.
704 Normalizes the spectrum so that its integral equals the specified value,
705 useful for calibrating source intensity.
709 source_integral: Target integral value
710 wavelength_min: Optional minimum wavelength for integration range
711 wavelength_max: Optional maximum wavelength for integration range
714 >>> radiation.setSourceSpectrumIntegral(source_id, 1000.0)
715 >>> radiation.setSourceSpectrumIntegral(source_id, 500.0, 400, 700) # PAR range
717 if not isinstance(source_id, int)
or source_id < 0:
718 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
719 if source_integral < 0:
720 raise ValueError(f
"Source integral must be non-negative, got {source_integral}")
722 radiation_wrapper.setSourceSpectrumIntegral(self.
radiation_model, source_id, source_integral,
723 wavelength_min, wavelength_max)
724 logger.debug(f
"Set spectrum integral for source {source_id}: {source_integral}")
727 @require_plugin('radiation', 'integrate spectrum')
729 wavelength_max: float =
None, source_id: int =
None,
730 camera_spectrum=
None) -> float:
732 Integrate spectrum with optional source/camera spectra and wavelength range.
734 This unified method handles multiple integration scenarios:
735 - Basic: Total spectrum integration
736 - Range: Integration over wavelength range
737 - Source: Integration weighted by source spectrum
738 - Camera: Integration weighted by camera spectral response
739 - Full: Integration with both source and camera spectra
742 object_spectrum: Object spectrum as list of (wavelength, value) tuples/vec2
743 wavelength_min: Optional minimum wavelength for integration range
744 wavelength_max: Optional maximum wavelength for integration range
745 source_id: Optional source ID for source spectrum weighting
746 camera_spectrum: Optional camera spectrum for camera response weighting
752 >>> leaf_reflectance = [(400, 0.1), (500, 0.4), (600, 0.6), (700, 0.5)]
754 >>> # Total integration
755 >>> total = radiation.integrateSpectrum(leaf_reflectance)
757 >>> # PAR range (400-700nm)
758 >>> par = radiation.integrateSpectrum(leaf_reflectance, 400, 700)
760 >>> # With source spectrum
761 >>> source_weighted = radiation.integrateSpectrum(
762 ... leaf_reflectance, 400, 700, source_id=sun_source
765 >>> # With camera response
766 >>> camera_response = [(400, 0.2), (550, 1.0), (700, 0.3)]
767 >>> camera_weighted = radiation.integrateSpectrum(
768 ... leaf_reflectance, camera_spectrum=camera_response
771 return radiation_wrapper.integrateSpectrum(self.
radiation_model, object_spectrum,
772 wavelength_min, wavelength_max,
773 source_id, camera_spectrum)
775 @require_plugin('radiation', 'integrate source spectrum')
778 Integrate source spectrum over wavelength range.
782 wavelength_min: Minimum wavelength
783 wavelength_max: Maximum wavelength
786 Integrated source spectrum value
789 >>> par_flux = radiation.integrateSourceSpectrum(source_id, 400, 700)
791 if not isinstance(source_id, int)
or source_id < 0:
792 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
793 return radiation_wrapper.integrateSourceSpectrum(self.
radiation_model, source_id,
794 wavelength_min, wavelength_max)
797 @require_plugin('radiation', 'scale spectrum')
798 def scaleSpectrum(self, existing_label: str, new_label_or_scale, scale_factor: float =
None):
800 Scale spectrum in-place or to new label.
802 Useful for adjusting spectrum intensities or creating variations of
803 existing spectra for sensitivity analysis.
805 Supports two call patterns:
806 - scaleSpectrum("label", scale) -> scales in-place
807 - scaleSpectrum("existing", "new", scale) -> creates new scaled spectrum
810 existing_label: Existing global data label
811 new_label_or_scale: Either new label string (if creating new) or scale factor (if in-place)
812 scale_factor: Scale factor (required only if new_label_or_scale is a string)
815 >>> # In-place scaling
816 >>> radiation.scaleSpectrum("leaf_reflectance", 1.2)
818 >>> # Create new scaled spectrum
819 >>> radiation.scaleSpectrum("leaf_reflectance", "scaled_leaf", 1.5)
821 if not isinstance(existing_label, str)
or not existing_label.strip():
822 raise ValueError(
"Existing label must be a non-empty string")
825 new_label_or_scale, scale_factor)
826 logger.debug(f
"Scaled spectrum '{existing_label}'")
828 @require_plugin('radiation', 'scale spectrum randomly')
830 min_scale: float, max_scale: float):
832 Scale spectrum with random factor and store as new label.
834 Useful for creating stochastic variations in spectral properties for
835 Monte Carlo simulations or uncertainty quantification.
838 existing_label: Existing global data label
839 new_label: New global data label for scaled spectrum
840 min_scale: Minimum scale factor
841 max_scale: Maximum scale factor
844 >>> # Create random variation of leaf reflectance
845 >>> radiation.scaleSpectrumRandomly("leaf_base", "leaf_variant", 0.8, 1.2)
847 if not isinstance(existing_label, str)
or not existing_label.strip():
848 raise ValueError(
"Existing label must be a non-empty string")
849 if not isinstance(new_label, str)
or not new_label.strip():
850 raise ValueError(
"New label must be a non-empty string")
851 if min_scale >= max_scale:
852 raise ValueError(f
"min_scale ({min_scale}) must be less than max_scale ({max_scale})")
854 radiation_wrapper.scaleSpectrumRandomly(self.
radiation_model, existing_label, new_label,
855 min_scale, max_scale)
856 logger.debug(f
"Scaled spectrum '{existing_label}' randomly to '{new_label}'")
858 @require_plugin('radiation', 'blend spectra')
859 def blendSpectra(self, new_label: str, spectrum_labels: List[str], weights: List[float]):
861 Blend multiple spectra with specified weights.
863 Creates weighted combination of spectra, useful for mixing material properties
864 or creating composite light sources.
867 new_label: New global data label for blended spectrum
868 spectrum_labels: List of spectrum labels to blend
869 weights: List of weights (must sum to reasonable values, same length as labels)
872 >>> # Mix two leaf types (70% type A, 30% type B)
873 >>> radiation.blendSpectra("mixed_leaf",
874 ... ["leaf_type_a", "leaf_type_b"],
878 if not isinstance(new_label, str)
or not new_label.strip():
879 raise ValueError(
"New label must be a non-empty string")
880 if len(spectrum_labels) != len(weights):
881 raise ValueError(f
"Number of labels ({len(spectrum_labels)}) must match number of weights ({len(weights)})")
882 if not spectrum_labels:
883 raise ValueError(
"At least one spectrum label required")
885 radiation_wrapper.blendSpectra(self.
radiation_model, new_label, spectrum_labels, weights)
886 logger.debug(f
"Blended {len(spectrum_labels)} spectra into '{new_label}'")
888 @require_plugin('radiation', 'blend spectra randomly')
891 Blend multiple spectra with random weights.
893 Creates random combinations of spectra, useful for generating diverse
894 material properties in stochastic simulations.
897 new_label: New global data label for blended spectrum
898 spectrum_labels: List of spectrum labels to blend
901 >>> # Create random mixture of leaf spectra
902 >>> radiation.blendSpectraRandomly("random_leaf",
903 ... ["young_leaf", "mature_leaf", "senescent_leaf"]
906 if not isinstance(new_label, str)
or not new_label.strip():
907 raise ValueError(
"New label must be a non-empty string")
908 if not spectrum_labels:
909 raise ValueError(
"At least one spectrum label required")
911 radiation_wrapper.blendSpectraRandomly(self.
radiation_model, new_label, spectrum_labels)
912 logger.debug(f
"Blended {len(spectrum_labels)} spectra randomly into '{new_label}'")
915 @require_plugin('radiation', 'interpolate spectrum from data')
917 spectra_labels: List[str], values: List[float],
918 primitive_data_query_label: str,
919 primitive_data_radprop_label: str):
921 Interpolate spectral properties based on primitive data values.
923 Automatically assigns spectra to primitives by interpolating between
924 reference spectra based on continuous data values (e.g., age, moisture, etc.).
927 primitive_uuids: List of primitive UUIDs to assign spectra
928 spectra_labels: List of reference spectrum labels
929 values: List of data values corresponding to each spectrum
930 primitive_data_query_label: Primitive data label containing query values
931 primitive_data_radprop_label: Primitive data label to store assigned spectra
934 >>> # Assign leaf reflectance based on age
935 >>> leaf_patches = context.getAllUUIDs("patch")
936 >>> radiation.interpolateSpectrumFromPrimitiveData(
937 ... primitive_uuids=leaf_patches,
938 ... spectra_labels=["young_leaf", "mature_leaf", "old_leaf"],
939 ... values=[0.0, 50.0, 100.0], # Days since emergence
940 ... primitive_data_query_label="leaf_age",
941 ... primitive_data_radprop_label="reflectance"
944 if not isinstance(primitive_uuids, (list, tuple))
or not primitive_uuids:
945 raise ValueError(
"Primitive UUIDs must be a non-empty list")
946 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
947 raise ValueError(
"Spectra labels must be a non-empty list")
948 if not isinstance(values, (list, tuple))
or not values:
949 raise ValueError(
"Values must be a non-empty list")
950 if len(spectra_labels) != len(values):
951 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
953 radiation_wrapper.interpolateSpectrumFromPrimitiveData(
955 primitive_data_query_label, primitive_data_radprop_label
957 logger.debug(f
"Interpolated spectra for {len(primitive_uuids)} primitives")
959 @require_plugin('radiation', 'interpolate spectrum from object data')
961 spectra_labels: List[str], values: List[float],
962 object_data_query_label: str,
963 primitive_data_radprop_label: str):
965 Interpolate spectral properties based on object data values.
967 Automatically assigns spectra to object primitives by interpolating between
968 reference spectra based on continuous object-level data values.
971 object_ids: List of object IDs
972 spectra_labels: List of reference spectrum labels
973 values: List of data values corresponding to each spectrum
974 object_data_query_label: Object data label containing query values
975 primitive_data_radprop_label: Primitive data label to store assigned spectra
978 >>> # Assign tree reflectance based on health index
979 >>> tree_ids = [tree1_id, tree2_id, tree3_id]
980 >>> radiation.interpolateSpectrumFromObjectData(
981 ... object_ids=tree_ids,
982 ... spectra_labels=["healthy_tree", "stressed_tree", "diseased_tree"],
983 ... values=[1.0, 0.5, 0.0], # Health index
984 ... object_data_query_label="health_index",
985 ... primitive_data_radprop_label="reflectance"
988 if not isinstance(object_ids, (list, tuple))
or not object_ids:
989 raise ValueError(
"Object IDs must be a non-empty list")
990 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
991 raise ValueError(
"Spectra labels must be a non-empty list")
992 if not isinstance(values, (list, tuple))
or not values:
993 raise ValueError(
"Values must be a non-empty list")
994 if len(spectra_labels) != len(values):
995 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
997 radiation_wrapper.interpolateSpectrumFromObjectData(
999 object_data_query_label, primitive_data_radprop_label
1001 logger.debug(f
"Interpolated spectra for {len(object_ids)} objects")
1003 @require_plugin('radiation', 'set ray count')
1005 """Set direct ray count for radiation band."""
1006 validate_band_label(band_label,
"band_label",
"setDirectRayCount")
1007 validate_ray_count(ray_count,
"ray_count",
"setDirectRayCount")
1008 radiation_wrapper.setDirectRayCount(self.
radiation_model, band_label, ray_count)
1010 @require_plugin('radiation', 'set ray count')
1012 """Set diffuse ray count for radiation band."""
1013 validate_band_label(band_label,
"band_label",
"setDiffuseRayCount")
1014 validate_ray_count(ray_count,
"ray_count",
"setDiffuseRayCount")
1015 radiation_wrapper.setDiffuseRayCount(self.
radiation_model, band_label, ray_count)
1017 @require_plugin('radiation', 'set radiation flux')
1019 """Set diffuse radiation flux for band."""
1020 validate_band_label(label,
"label",
"setDiffuseRadiationFlux")
1021 validate_flux_value(flux,
"flux",
"setDiffuseRadiationFlux")
1022 radiation_wrapper.setDiffuseRadiationFlux(self.
radiation_model, label, flux)
1024 @require_plugin('radiation', 'configure diffuse radiation')
1027 Set diffuse radiation extinction coefficient with directional bias.
1029 Models directionally-biased diffuse radiation (e.g., sky radiation with zenith peak).
1033 K: Extinction coefficient
1034 peak_direction: Peak direction as vec3, SphericalCoord, or list [x, y, z]
1037 >>> from pyhelios.types import vec3
1038 >>> radiation.setDiffuseRadiationExtinctionCoeff("SW", 0.5, vec3(0, 0, 1))
1040 validate_band_label(label,
"label",
"setDiffuseRadiationExtinctionCoeff")
1042 raise ValueError(f
"Extinction coefficient must be non-negative, got {K}")
1043 radiation_wrapper.setDiffuseRadiationExtinctionCoeff(self.
radiation_model, label, K, peak_direction)
1044 logger.debug(f
"Set diffuse extinction coefficient for band '{label}': K={K}")
1046 @require_plugin('radiation', 'query diffuse flux')
1049 Get diffuse flux for band.
1052 band_label: Band label
1058 >>> flux = radiation.getDiffuseFlux("SW")
1060 validate_band_label(band_label,
"band_label",
"getDiffuseFlux")
1063 @require_plugin('radiation', 'configure diffuse spectrum')
1066 Set diffuse spectrum from global data label.
1069 band_label: Band label (string) or list of band labels
1070 spectrum_label: Spectrum global data label
1073 >>> radiation.setDiffuseSpectrum("SW", "sky_spectrum")
1074 >>> radiation.setDiffuseSpectrum(["SW", "NIR"], "sky_spectrum")
1076 if isinstance(band_label, str):
1077 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrum")
1079 for label
in band_label:
1080 validate_band_label(label,
"band_label",
"setDiffuseSpectrum")
1081 if not isinstance(spectrum_label, str)
or not spectrum_label.strip():
1082 raise ValueError(
"Spectrum label must be a non-empty string")
1084 radiation_wrapper.setDiffuseSpectrum(self.
radiation_model, band_label, spectrum_label)
1085 logger.debug(f
"Set diffuse spectrum for band(s) {band_label}")
1087 @require_plugin('radiation', 'configure diffuse spectrum')
1089 wavelength_max: float =
None, band_label: str =
None):
1091 Set diffuse spectrum integral.
1094 spectrum_integral: Integral value
1095 wavelength_min: Optional minimum wavelength
1096 wavelength_max: Optional maximum wavelength
1097 band_label: Optional specific band label (None for all bands)
1100 >>> radiation.setDiffuseSpectrumIntegral(1000.0) # All bands
1101 >>> radiation.setDiffuseSpectrumIntegral(500.0, 400, 700, band_label="PAR") # Specific band
1103 if spectrum_integral < 0:
1104 raise ValueError(f
"Spectrum integral must be non-negative, got {spectrum_integral}")
1105 if band_label
is not None:
1106 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrumIntegral")
1108 radiation_wrapper.setDiffuseSpectrumIntegral(self.
radiation_model, spectrum_integral,
1109 wavelength_min, wavelength_max, band_label)
1110 logger.debug(f
"Set diffuse spectrum integral: {spectrum_integral}")
1112 @require_plugin('radiation', 'set source flux')
1113 def setSourceFlux(self, source_id, label: str, flux: float):
1114 """Set source flux for single source or multiple sources."""
1115 validate_band_label(label,
"label",
"setSourceFlux")
1116 validate_flux_value(flux,
"flux",
"setSourceFlux")
1118 if isinstance(source_id, (list, tuple)):
1120 validate_source_id_list(list(source_id),
"source_id",
"setSourceFlux")
1121 radiation_wrapper.setSourceFluxMultiple(self.
radiation_model, source_id, label, flux)
1124 validate_source_id(source_id,
"source_id",
"setSourceFlux")
1125 radiation_wrapper.setSourceFlux(self.
radiation_model, source_id, label, flux)
1128 @require_plugin('radiation', 'get source flux')
1129 @validate_get_source_flux_params
1130 def getSourceFlux(self, source_id: int, label: str) -> float:
1131 """Get source flux for band."""
1132 return radiation_wrapper.getSourceFlux(self.
radiation_model, source_id, label)
1134 @require_plugin('radiation', 'update geometry')
1135 @validate_update_geometry_params
1138 Update geometry in radiation model.
1141 uuids: Optional list of specific UUIDs to update. If None, updates all geometry.
1145 logger.debug(
"Updated all geometry in radiation model")
1148 logger.debug(f
"Updated {len(uuids)} geometry UUIDs in radiation model")
1150 @require_plugin('radiation', 'run radiation simulation')
1151 @validate_run_band_params
1152 def runBand(self, band_label):
1154 Run radiation simulation for single band or multiple bands.
1156 PERFORMANCE NOTE: When simulating multiple radiation bands, it is HIGHLY RECOMMENDED
1157 to run all bands in a single call (e.g., runBand(["PAR", "NIR", "SW"])) rather than
1158 sequential single-band calls. This provides significant computational efficiency gains
1161 - GPU ray tracing setup is done once for all bands
1162 - Scene geometry acceleration structures are reused
1163 - OptiX kernel launches are batched together
1164 - Memory transfers between CPU/GPU are minimized
1167 # EFFICIENT - Single call for multiple bands
1168 radiation.runBand(["PAR", "NIR", "SW"])
1170 # INEFFICIENT - Sequential single-band calls
1171 radiation.runBand("PAR")
1172 radiation.runBand("NIR")
1173 radiation.runBand("SW")
1176 band_label: Single band name (str) or list of band names for multi-band simulation
1178 if isinstance(band_label, (list, tuple)):
1180 for lbl
in band_label:
1181 if not isinstance(lbl, str):
1182 raise TypeError(f
"Band labels must be strings, got {type(lbl).__name__}")
1184 logger.info(f
"Completed radiation simulation for bands: {band_label}")
1187 if not isinstance(band_label, str):
1188 raise TypeError(f
"Band label must be a string, got {type(band_label).__name__}")
1190 logger.info(f
"Completed radiation simulation for band: {band_label}")
1193 @require_plugin('radiation', 'get simulation results')
1195 """Get total absorbed flux for all primitives."""
1196 results = radiation_wrapper.getTotalAbsorbedFlux(self.
radiation_model)
1197 logger.debug(f
"Retrieved absorbed flux data for {len(results)} primitives")
1201 @require_plugin('radiation', 'check band existence')
1204 Check if a radiation band exists.
1207 label: Name/label of the radiation band to check
1210 True if band exists, False otherwise
1213 >>> radiation.addRadiationBand("SW")
1214 >>> radiation.doesBandExist("SW")
1216 >>> radiation.doesBandExist("nonexistent")
1219 validate_band_label(label,
"label",
"doesBandExist")
1223 @require_plugin('radiation', 'manage radiation sources')
1226 Delete a radiation source.
1229 source_id: ID of the radiation source to delete
1232 >>> source_id = radiation.addCollimatedRadiationSource()
1233 >>> radiation.deleteRadiationSource(source_id)
1235 if not isinstance(source_id, int)
or source_id < 0:
1236 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1237 radiation_wrapper.deleteRadiationSource(self.
radiation_model, source_id)
1238 logger.debug(f
"Deleted radiation source {source_id}")
1240 @require_plugin('radiation', 'query radiation sources')
1243 Get position of a radiation source.
1246 source_id: ID of the radiation source
1249 vec3 position of the source
1252 >>> source_id = radiation.addCollimatedRadiationSource()
1253 >>> position = radiation.getSourcePosition(source_id)
1254 >>> print(f"Source at: {position}")
1256 if not isinstance(source_id, int)
or source_id < 0:
1257 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1258 position_list = radiation_wrapper.getSourcePosition(self.
radiation_model, source_id)
1259 from .wrappers.DataTypes
import vec3
1260 return vec3(position_list[0], position_list[1], position_list[2])
1263 @require_plugin('radiation', 'get sky energy')
1266 Get total sky energy.
1269 Total sky energy value
1272 >>> energy = radiation.getSkyEnergy()
1273 >>> print(f"Sky energy: {energy}")
1277 @require_plugin('radiation', 'calculate G-function')
1280 Calculate G-function (geometry factor) for given view direction.
1282 The G-function describes the geometric relationship between leaf area
1283 distribution and viewing direction, important for canopy radiation modeling.
1286 view_direction: View direction as vec3 or list/tuple [x, y, z]
1292 >>> from pyhelios.types import vec3
1293 >>> g_value = radiation.calculateGtheta(vec3(0, 0, 1))
1294 >>> print(f"G-function: {g_value}")
1297 return radiation_wrapper.calculateGtheta(self.
radiation_model, context_ptr, view_direction)
1299 @require_plugin('radiation', 'configure output data')
1302 Enable optional primitive data output.
1305 label: Name/label of the primitive data to output
1308 >>> radiation.optionalOutputPrimitiveData("temperature")
1310 validate_band_label(label,
"label",
"optionalOutputPrimitiveData")
1312 logger.debug(f
"Enabled optional output for primitive data: {label}")
1314 @require_plugin('radiation', 'configure boundary conditions')
1317 Enforce periodic boundary conditions.
1319 Periodic boundaries are useful for large-scale simulations to reduce
1320 edge effects by wrapping radiation at domain boundaries.
1323 boundary: Boundary specification string (e.g., "xy", "xyz", "x", "y", "z")
1326 >>> radiation.enforcePeriodicBoundary("xy")
1328 if not isinstance(boundary, str)
or not boundary:
1329 raise ValueError(
"Boundary specification must be a non-empty string")
1330 radiation_wrapper.enforcePeriodicBoundary(self.
radiation_model, boundary)
1331 logger.debug(f
"Enforced periodic boundary: {boundary}")
1334 @require_plugin('radiation', 'configure radiation simulation')
1335 @validate_scattering_depth_params
1337 """Set scattering depth for radiation band."""
1338 radiation_wrapper.setScatteringDepth(self.
radiation_model, label, depth)
1340 @require_plugin('radiation', 'configure radiation simulation')
1341 @validate_min_scatter_energy_params
1343 """Set minimum scatter energy for radiation band."""
1344 radiation_wrapper.setMinScatterEnergy(self.
radiation_model, label, energy)
1346 @require_plugin('radiation', 'configure radiation emission')
1348 """Disable emission for radiation band."""
1349 validate_band_label(label,
"label",
"disableEmission")
1352 @require_plugin('radiation', 'configure radiation emission')
1354 """Enable emission for radiation band."""
1355 validate_band_label(label,
"label",
"enableEmission")
1362 @require_plugin('radiation', 'add radiation camera')
1363 def addRadiationCamera(self, camera_label: str, band_labels: List[str], position, lookat_or_direction,
1364 camera_properties=
None, antialiasing_samples: int = 100):
1366 Add a radiation camera to the simulation.
1369 camera_label: Unique label string for the camera
1370 band_labels: List of radiation band labels for the camera
1371 position: Camera position as vec3 object
1372 lookat_or_direction: Either:
1373 - Lookat point as vec3 object
1374 - SphericalCoord for viewing direction
1375 camera_properties: CameraProperties instance or None for defaults
1376 antialiasing_samples: Number of antialiasing samples (default: 100)
1379 ValidationError: If parameters are invalid or have wrong types
1380 RadiationModelError: If camera creation fails
1383 >>> from pyhelios import vec3, CameraProperties
1384 >>> # Create camera looking at origin from above
1385 >>> camera_props = CameraProperties(camera_resolution=(1024, 1024))
1386 >>> radiation_model.addRadiationCamera("main_camera", ["red", "green", "blue"],
1387 ... position=vec3(0, 0, 5), lookat_or_direction=vec3(0, 0, 0),
1388 ... camera_properties=camera_props)
1391 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1392 from .wrappers.DataTypes
import SphericalCoord, vec3, make_vec3
1393 from .validation.plugins
import validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
1396 validated_label = validate_camera_label(camera_label,
"camera_label",
"addRadiationCamera")
1397 validated_bands = validate_band_labels_list(band_labels,
"band_labels",
"addRadiationCamera")
1398 validated_samples = validate_antialiasing_samples(antialiasing_samples,
"antialiasing_samples",
"addRadiationCamera")
1401 if not (hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z')):
1402 raise TypeError(
"position must be a vec3 object. Use vec3(x, y, z) to create one.")
1403 validated_position = position
1406 if hasattr(lookat_or_direction,
'radius')
and hasattr(lookat_or_direction,
'elevation'):
1407 validated_direction = lookat_or_direction
1408 elif hasattr(lookat_or_direction,
'x')
and hasattr(lookat_or_direction,
'y')
and hasattr(lookat_or_direction,
'z'):
1409 validated_direction = lookat_or_direction
1411 raise TypeError(
"lookat_or_direction must be a vec3 or SphericalCoord object. Use vec3(x, y, z) or SphericalCoord to create one.")
1414 if camera_properties
is None:
1419 if hasattr(validated_direction,
'radius')
and hasattr(validated_direction,
'elevation'):
1421 direction_coords = validated_direction.to_list()
1422 if len(direction_coords) >= 3:
1424 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[2]
1426 raise ValueError(
"SphericalCoord must have at least radius, elevation, and azimuth")
1428 radiation_wrapper.addRadiationCameraSpherical(
1432 validated_position.x, validated_position.y, validated_position.z,
1433 radius, elevation, azimuth,
1434 camera_properties.to_array(),
1439 radiation_wrapper.addRadiationCameraVec3(
1443 validated_position.x, validated_position.y, validated_position.z,
1444 validated_direction.x, validated_direction.y, validated_direction.z,
1445 camera_properties.to_array(),
1449 except Exception
as e:
1452 @require_plugin('radiation', 'manage camera position')
1455 Set camera position.
1457 Allows dynamic camera repositioning during simulation, useful for
1458 time-series captures or multi-view imaging.
1461 camera_label: Camera label string
1462 position: Camera position as vec3 or list [x, y, z]
1465 >>> radiation.setCameraPosition("cam1", [0, 0, 10])
1466 >>> from pyhelios.types import vec3
1467 >>> radiation.setCameraPosition("cam1", vec3(5, 5, 10))
1469 if not isinstance(camera_label, str)
or not camera_label.strip():
1470 raise ValueError(
"Camera label must be a non-empty string")
1471 radiation_wrapper.setCameraPosition(self.
radiation_model, camera_label, position)
1472 logger.debug(f
"Updated camera '{camera_label}' position")
1474 @require_plugin('radiation', 'query camera position')
1477 Get camera position.
1480 camera_label: Camera label string
1483 vec3 position of the camera
1486 >>> position = radiation.getCameraPosition("cam1")
1487 >>> print(f"Camera at: {position}")
1489 if not isinstance(camera_label, str)
or not camera_label.strip():
1490 raise ValueError(
"Camera label must be a non-empty string")
1491 position_list = radiation_wrapper.getCameraPosition(self.
radiation_model, camera_label)
1492 from .wrappers.DataTypes
import vec3
1493 return vec3(position_list[0], position_list[1], position_list[2])
1495 @require_plugin('radiation', 'manage camera lookat')
1498 Set camera lookat point.
1501 camera_label: Camera label string
1502 lookat: Lookat point as vec3 or list [x, y, z]
1505 >>> radiation.setCameraLookat("cam1", [0, 0, 0])
1507 if not isinstance(camera_label, str)
or not camera_label.strip():
1508 raise ValueError(
"Camera label must be a non-empty string")
1509 radiation_wrapper.setCameraLookat(self.
radiation_model, camera_label, lookat)
1510 logger.debug(f
"Updated camera '{camera_label}' lookat point")
1512 @require_plugin('radiation', 'query camera lookat')
1515 Get camera lookat point.
1518 camera_label: Camera label string
1524 >>> lookat = radiation.getCameraLookat("cam1")
1525 >>> print(f"Camera looking at: {lookat}")
1527 if not isinstance(camera_label, str)
or not camera_label.strip():
1528 raise ValueError(
"Camera label must be a non-empty string")
1529 lookat_list = radiation_wrapper.getCameraLookat(self.
radiation_model, camera_label)
1530 from .wrappers.DataTypes
import vec3
1531 return vec3(lookat_list[0], lookat_list[1], lookat_list[2])
1533 @require_plugin('radiation', 'manage camera orientation')
1536 Set camera orientation.
1539 camera_label: Camera label string
1540 direction: View direction as vec3, SphericalCoord, or list [x, y, z]
1543 >>> radiation.setCameraOrientation("cam1", [0, 0, 1])
1544 >>> from pyhelios.types import SphericalCoord
1545 >>> radiation.setCameraOrientation("cam1", SphericalCoord(1.0, 45.0, 90.0))
1547 if not isinstance(camera_label, str)
or not camera_label.strip():
1548 raise ValueError(
"Camera label must be a non-empty string")
1549 radiation_wrapper.setCameraOrientation(self.
radiation_model, camera_label, direction)
1550 logger.debug(f
"Updated camera '{camera_label}' orientation")
1552 @require_plugin('radiation', 'query camera orientation')
1555 Get camera orientation.
1558 camera_label: Camera label string
1561 SphericalCoord orientation [radius, elevation, azimuth]
1564 >>> orientation = radiation.getCameraOrientation("cam1")
1565 >>> print(f"Camera orientation: {orientation}")
1567 if not isinstance(camera_label, str)
or not camera_label.strip():
1568 raise ValueError(
"Camera label must be a non-empty string")
1569 orientation_list = radiation_wrapper.getCameraOrientation(self.
radiation_model, camera_label)
1570 from .wrappers.DataTypes
import SphericalCoord
1571 return SphericalCoord(orientation_list[0], orientation_list[1], orientation_list[2])
1573 @require_plugin('radiation', 'query cameras')
1576 Get all camera labels.
1579 List of all camera label strings
1582 >>> cameras = radiation.getAllCameraLabels()
1583 >>> print(f"Available cameras: {cameras}")
1587 @require_plugin('radiation', 'configure camera spectral response')
1590 Set camera spectral response from global data.
1593 camera_label: Camera label
1594 band_label: Band label
1595 global_data: Global data label for spectral response curve
1598 >>> radiation.setCameraSpectralResponse("cam1", "red", "sensor_red_response")
1600 if not isinstance(camera_label, str)
or not camera_label.strip():
1601 raise ValueError(
"Camera label must be a non-empty string")
1602 validate_band_label(band_label,
"band_label",
"setCameraSpectralResponse")
1603 if not isinstance(global_data, str)
or not global_data.strip():
1604 raise ValueError(
"Global data label must be a non-empty string")
1606 radiation_wrapper.setCameraSpectralResponse(self.
radiation_model, camera_label, band_label, global_data)
1607 logger.debug(f
"Set spectral response for camera '{camera_label}', band '{band_label}'")
1609 @require_plugin('radiation', 'configure camera from library')
1612 Set camera spectral response from standard camera library.
1614 Uses pre-defined spectral response curves for common cameras.
1617 camera_label: Camera label
1618 camera_library_name: Standard camera name (e.g., "iPhone13", "NikonD850", "CanonEOS5D")
1621 >>> radiation.setCameraSpectralResponseFromLibrary("cam1", "iPhone13")
1623 if not isinstance(camera_label, str)
or not camera_label.strip():
1624 raise ValueError(
"Camera label must be a non-empty string")
1625 if not isinstance(camera_library_name, str)
or not camera_library_name.strip():
1626 raise ValueError(
"Camera library name must be a non-empty string")
1628 radiation_wrapper.setCameraSpectralResponseFromLibrary(self.
radiation_model, camera_label, camera_library_name)
1629 logger.debug(f
"Set camera '{camera_label}' response from library: {camera_library_name}")
1631 @require_plugin('radiation', 'get camera pixel data')
1634 Get camera pixel data for specific band.
1636 Retrieves raw pixel values for programmatic access and analysis.
1639 camera_label: Camera label
1640 band_label: Band label
1643 List of pixel values
1646 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1647 >>> print(f"Mean pixel value: {sum(pixels)/len(pixels)}")
1649 if not isinstance(camera_label, str)
or not camera_label.strip():
1650 raise ValueError(
"Camera label must be a non-empty string")
1651 validate_band_label(band_label,
"band_label",
"getCameraPixelData")
1653 return radiation_wrapper.getCameraPixelData(self.
radiation_model, camera_label, band_label)
1655 @require_plugin('radiation', 'set camera pixel data')
1656 def setCameraPixelData(self, camera_label: str, band_label: str, pixel_data: List[float]):
1658 Set camera pixel data for specific band.
1660 Allows programmatic modification of pixel values.
1663 camera_label: Camera label
1664 band_label: Band label
1665 pixel_data: List of pixel values
1668 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1669 >>> modified_pixels = [p * 1.2 for p in pixels] # Brighten by 20%
1670 >>> radiation.setCameraPixelData("cam1", "red", modified_pixels)
1672 if not isinstance(camera_label, str)
or not camera_label.strip():
1673 raise ValueError(
"Camera label must be a non-empty string")
1674 validate_band_label(band_label,
"band_label",
"setCameraPixelData")
1675 if not isinstance(pixel_data, (list, tuple)):
1676 raise ValueError(
"Pixel data must be a list or tuple")
1678 radiation_wrapper.setCameraPixelData(self.
radiation_model, camera_label, band_label, pixel_data)
1679 logger.debug(f
"Set pixel data for camera '{camera_label}', band '{band_label}': {len(pixel_data)} pixels")
1685 @require_plugin('radiation', 'add camera from library')
1687 position, lookat, antialiasing_samples: int = 1,
1688 band_labels: Optional[List[str]] =
None):
1690 Add radiation camera loading all properties from camera library.
1692 Loads camera intrinsic parameters (resolution, FOV, sensor size) and spectral
1693 response data from the camera library XML file. This is the recommended way to
1694 create realistic cameras with proper spectral responses.
1697 camera_label: Label for the camera instance
1698 library_camera_label: Label of camera in library (e.g., "Canon_20D", "iPhone11", "NikonD700")
1699 position: Camera position as vec3 or (x, y, z) tuple
1700 lookat: Lookat point as vec3 or (x, y, z) tuple
1701 antialiasing_samples: Number of ray samples per pixel. Default: 1
1702 band_labels: Optional custom band labels. If None, uses library defaults.
1705 RadiationModelError: If operation fails
1706 ValueError: If parameters are invalid
1709 Available cameras in plugins/radiation/camera_library/camera_library.xml include:
1710 - Canon_20D, Nikon_D700, Nikon_D50
1711 - iPhone11, iPhone12ProMAX
1712 - Additional cameras available in library
1715 >>> radiation.addRadiationCameraFromLibrary(
1716 ... camera_label="cam1",
1717 ... library_camera_label="iPhone11",
1718 ... position=(0, -5, 1),
1719 ... lookat=(0, 0, 0.5),
1720 ... antialiasing_samples=10
1723 validate_band_label(camera_label,
"camera_label",
"addRadiationCameraFromLibrary")
1726 radiation_wrapper.addRadiationCameraFromLibrary(
1728 position, lookat, antialiasing_samples, band_labels
1730 logger.info(f
"Added camera '{camera_label}' from library '{library_camera_label}'")
1731 except Exception
as e:
1734 @require_plugin('radiation', 'update camera parameters')
1737 Update camera parameters for an existing camera.
1739 Allows modification of camera properties after creation while preserving
1740 position, lookat direction, and spectral band configuration.
1743 camera_label: Label for the camera to update
1744 camera_properties: CameraProperties instance with new parameters
1747 RadiationModelError: If operation fails or camera doesn't exist
1748 ValueError: If parameters are invalid
1751 FOV_aspect_ratio is automatically recalculated from camera_resolution.
1752 Camera position and lookat are preserved.
1755 >>> props = CameraProperties(
1756 ... camera_resolution=(1920, 1080),
1758 ... lens_focal_length=0.085 # 85mm lens
1760 >>> radiation.updateCameraParameters("cam1", props)
1762 validate_band_label(camera_label,
"camera_label",
"updateCameraParameters")
1764 if not isinstance(camera_properties, CameraProperties):
1765 raise ValueError(
"camera_properties must be a CameraProperties instance")
1768 radiation_wrapper.updateCameraParameters(self.
radiation_model, camera_label, camera_properties)
1769 logger.debug(f
"Updated parameters for camera '{camera_label}'")
1770 except Exception
as e:
1773 @require_plugin('radiation', 'enable camera metadata')
1776 Enable automatic JSON metadata file writing for camera(s).
1778 When enabled, writeCameraImage() automatically creates a JSON metadata file
1779 alongside the image containing comprehensive camera and scene information.
1782 camera_labels: Single camera label (str) or list of camera labels (List[str])
1785 RadiationModelError: If operation fails
1786 ValueError: If parameters are invalid
1790 - Camera properties (model, lens, sensor specs)
1791 - Geographic location (latitude, longitude)
1792 - Acquisition settings (date, time, exposure, white balance)
1793 - Agronomic data (plant species, heights, phenology stages)
1796 >>> # Enable for single camera
1797 >>> radiation.enableCameraMetadata("cam1")
1799 >>> # Enable for multiple cameras
1800 >>> radiation.enableCameraMetadata(["cam1", "cam2", "cam3"])
1804 if isinstance(camera_labels, str):
1805 logger.info(f
"Enabled metadata for camera '{camera_labels}'")
1807 logger.info(f
"Enabled metadata for {len(camera_labels)} cameras")
1808 except Exception
as e:
1811 @require_plugin('radiation', 'write camera images')
1812 def writeCameraImage(self, camera: str, bands: List[str], imagefile_base: str,
1813 image_path: str =
"./", frame: int = -1,
1814 flux_to_pixel_conversion: float = 1.0) -> str:
1816 Write camera image to file and return output filename.
1819 camera: Camera label
1820 bands: List of band labels to include in the image
1821 imagefile_base: Base filename for output
1822 image_path: Output directory path (default: current directory)
1823 frame: Frame number to write (-1 for all frames)
1824 flux_to_pixel_conversion: Conversion factor from flux to pixel values
1827 Output filename string
1830 RadiationModelError: If camera image writing fails
1831 TypeError: If parameters have incorrect types
1834 if not isinstance(camera, str)
or not camera.strip():
1835 raise TypeError(
"Camera label must be a non-empty string")
1836 if not isinstance(bands, list)
or not bands:
1837 raise TypeError(
"Bands must be a non-empty list of strings")
1838 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
1839 raise TypeError(
"All band labels must be non-empty strings")
1840 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
1841 raise TypeError(
"Image file base must be a non-empty string")
1842 if not isinstance(image_path, str):
1843 raise TypeError(
"Image path must be a string")
1844 if not isinstance(frame, int):
1845 raise TypeError(
"Frame must be an integer")
1846 if not isinstance(flux_to_pixel_conversion, (int, float))
or flux_to_pixel_conversion <= 0:
1847 raise TypeError(
"Flux to pixel conversion must be a positive number")
1849 filename = radiation_wrapper.writeCameraImage(
1851 image_path, frame, flux_to_pixel_conversion)
1853 logger.info(f
"Camera image written to: {filename}")
1856 @require_plugin('radiation', 'write normalized camera images')
1858 image_path: str =
"./", frame: int = -1) -> str:
1860 Write normalized camera image to file and return output filename.
1863 camera: Camera label
1864 bands: List of band labels to include in the image
1865 imagefile_base: Base filename for output
1866 image_path: Output directory path (default: current directory)
1867 frame: Frame number to write (-1 for all frames)
1870 Output filename string
1873 RadiationModelError: If normalized camera image writing fails
1874 TypeError: If parameters have incorrect types
1877 if not isinstance(camera, str)
or not camera.strip():
1878 raise TypeError(
"Camera label must be a non-empty string")
1879 if not isinstance(bands, list)
or not bands:
1880 raise TypeError(
"Bands must be a non-empty list of strings")
1881 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
1882 raise TypeError(
"All band labels must be non-empty strings")
1883 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
1884 raise TypeError(
"Image file base must be a non-empty string")
1885 if not isinstance(image_path, str):
1886 raise TypeError(
"Image path must be a string")
1887 if not isinstance(frame, int):
1888 raise TypeError(
"Frame must be an integer")
1890 filename = radiation_wrapper.writeNormCameraImage(
1891 self.
radiation_model, camera, bands, imagefile_base, image_path, frame)
1893 logger.info(f
"Normalized camera image written to: {filename}")
1896 @require_plugin('radiation', 'write camera image data')
1898 image_path: str =
"./", frame: int = -1):
1900 Write camera image data to file (ASCII format).
1903 camera: Camera label
1905 imagefile_base: Base filename for output
1906 image_path: Output directory path (default: current directory)
1907 frame: Frame number to write (-1 for all frames)
1910 RadiationModelError: If camera image data writing fails
1911 TypeError: If parameters have incorrect types
1914 if not isinstance(camera, str)
or not camera.strip():
1915 raise TypeError(
"Camera label must be a non-empty string")
1916 if not isinstance(band, str)
or not band.strip():
1917 raise TypeError(
"Band label must be a non-empty string")
1918 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
1919 raise TypeError(
"Image file base must be a non-empty string")
1920 if not isinstance(image_path, str):
1921 raise TypeError(
"Image path must be a string")
1922 if not isinstance(frame, int):
1923 raise TypeError(
"Frame must be an integer")
1925 radiation_wrapper.writeCameraImageData(
1926 self.
radiation_model, camera, band, imagefile_base, image_path, frame)
1928 logger.info(f
"Camera image data written for camera {camera}, band {band}")
1930 @require_plugin('radiation', 'write image bounding boxes')
1932 primitive_data_labels=
None, object_data_labels=
None,
1933 object_class_ids=
None, image_file: str =
"",
1934 classes_txt_file: str =
"classes.txt",
1935 image_path: str =
"./"):
1937 Write image bounding boxes for object detection training.
1939 Supports both single and multiple data labels. Either provide primitive_data_labels
1940 or object_data_labels, not both.
1943 camera_label: Camera label
1944 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
1945 object_data_labels: Single object data label (str) or list of object data labels
1946 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
1947 image_file: Image filename
1948 classes_txt_file: Classes definition file (default: "classes.txt")
1949 image_path: Image output path (default: current directory)
1952 RadiationModelError: If bounding box writing fails
1953 TypeError: If parameters have incorrect types
1954 ValueError: If both primitive and object data labels are provided, or neither
1957 if primitive_data_labels
is not None and object_data_labels
is not None:
1958 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
1959 if primitive_data_labels
is None and object_data_labels
is None:
1960 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
1963 if not isinstance(camera_label, str)
or not camera_label.strip():
1964 raise TypeError(
"Camera label must be a non-empty string")
1965 if not isinstance(image_file, str)
or not image_file.strip():
1966 raise TypeError(
"Image file must be a non-empty string")
1967 if not isinstance(classes_txt_file, str):
1968 raise TypeError(
"Classes txt file must be a string")
1969 if not isinstance(image_path, str):
1970 raise TypeError(
"Image path must be a string")
1973 if primitive_data_labels
is not None:
1974 if isinstance(primitive_data_labels, str):
1976 if not isinstance(object_class_ids, int):
1977 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
1978 radiation_wrapper.writeImageBoundingBoxes(
1980 object_class_ids, image_file, classes_txt_file, image_path)
1981 logger.info(f
"Image bounding boxes written for primitive data: {primitive_data_labels}")
1983 elif isinstance(primitive_data_labels, list):
1985 if not isinstance(object_class_ids, list):
1986 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
1987 if len(primitive_data_labels) != len(object_class_ids):
1988 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
1989 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
1990 raise TypeError(
"All primitive data labels must be non-empty strings")
1991 if not all(isinstance(cid, int)
for cid
in object_class_ids):
1992 raise TypeError(
"All object class IDs must be integers")
1994 radiation_wrapper.writeImageBoundingBoxesVector(
1996 object_class_ids, image_file, classes_txt_file, image_path)
1997 logger.info(f
"Image bounding boxes written for {len(primitive_data_labels)} primitive data labels")
1999 raise TypeError(
"primitive_data_labels must be a string or list of strings")
2002 elif object_data_labels
is not None:
2003 if isinstance(object_data_labels, str):
2005 if not isinstance(object_class_ids, int):
2006 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2007 radiation_wrapper.writeImageBoundingBoxes_ObjectData(
2009 object_class_ids, image_file, classes_txt_file, image_path)
2010 logger.info(f
"Image bounding boxes written for object data: {object_data_labels}")
2012 elif isinstance(object_data_labels, list):
2014 if not isinstance(object_class_ids, list):
2015 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2016 if len(object_data_labels) != len(object_class_ids):
2017 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2018 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2019 raise TypeError(
"All object data labels must be non-empty strings")
2020 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2021 raise TypeError(
"All object class IDs must be integers")
2023 radiation_wrapper.writeImageBoundingBoxes_ObjectDataVector(
2025 object_class_ids, image_file, classes_txt_file, image_path)
2026 logger.info(f
"Image bounding boxes written for {len(object_data_labels)} object data labels")
2028 raise TypeError(
"object_data_labels must be a string or list of strings")
2030 @require_plugin('radiation', 'write image segmentation masks')
2032 primitive_data_labels=
None, object_data_labels=
None,
2033 object_class_ids=
None, json_filename: str =
"",
2034 image_file: str =
"", append_file: bool =
False):
2036 Write image segmentation masks in COCO JSON format.
2038 Supports both single and multiple data labels. Either provide primitive_data_labels
2039 or object_data_labels, not both.
2042 camera_label: Camera label
2043 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
2044 object_data_labels: Single object data label (str) or list of object data labels
2045 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
2046 json_filename: JSON output filename
2047 image_file: Image filename
2048 append_file: Whether to append to existing JSON file
2051 RadiationModelError: If segmentation mask writing fails
2052 TypeError: If parameters have incorrect types
2053 ValueError: If both primitive and object data labels are provided, or neither
2056 if primitive_data_labels
is not None and object_data_labels
is not None:
2057 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
2058 if primitive_data_labels
is None and object_data_labels
is None:
2059 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
2062 if not isinstance(camera_label, str)
or not camera_label.strip():
2063 raise TypeError(
"Camera label must be a non-empty string")
2064 if not isinstance(json_filename, str)
or not json_filename.strip():
2065 raise TypeError(
"JSON filename must be a non-empty string")
2066 if not isinstance(image_file, str)
or not image_file.strip():
2067 raise TypeError(
"Image file must be a non-empty string")
2068 if not isinstance(append_file, bool):
2069 raise TypeError(
"append_file must be a boolean")
2072 if primitive_data_labels
is not None:
2073 if isinstance(primitive_data_labels, str):
2075 if not isinstance(object_class_ids, int):
2076 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
2077 radiation_wrapper.writeImageSegmentationMasks(
2079 object_class_ids, json_filename, image_file, append_file)
2080 logger.info(f
"Image segmentation masks written for primitive data: {primitive_data_labels}")
2082 elif isinstance(primitive_data_labels, list):
2084 if not isinstance(object_class_ids, list):
2085 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
2086 if len(primitive_data_labels) != len(object_class_ids):
2087 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
2088 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
2089 raise TypeError(
"All primitive data labels must be non-empty strings")
2090 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2091 raise TypeError(
"All object class IDs must be integers")
2093 radiation_wrapper.writeImageSegmentationMasksVector(
2095 object_class_ids, json_filename, image_file, append_file)
2096 logger.info(f
"Image segmentation masks written for {len(primitive_data_labels)} primitive data labels")
2098 raise TypeError(
"primitive_data_labels must be a string or list of strings")
2101 elif object_data_labels
is not None:
2102 if isinstance(object_data_labels, str):
2104 if not isinstance(object_class_ids, int):
2105 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2106 radiation_wrapper.writeImageSegmentationMasks_ObjectData(
2108 object_class_ids, json_filename, image_file, append_file)
2109 logger.info(f
"Image segmentation masks written for object data: {object_data_labels}")
2111 elif isinstance(object_data_labels, list):
2113 if not isinstance(object_class_ids, list):
2114 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2115 if len(object_data_labels) != len(object_class_ids):
2116 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2117 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2118 raise TypeError(
"All object data labels must be non-empty strings")
2119 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2120 raise TypeError(
"All object class IDs must be integers")
2122 radiation_wrapper.writeImageSegmentationMasks_ObjectDataVector(
2124 object_class_ids, json_filename, image_file, append_file)
2125 logger.info(f
"Image segmentation masks written for {len(object_data_labels)} object data labels")
2127 raise TypeError(
"object_data_labels must be a string or list of strings")
2129 @require_plugin('radiation', 'auto-calibrate camera image')
2131 green_band_label: str, blue_band_label: str,
2132 output_file_path: str, print_quality_report: bool =
False,
2133 algorithm: str =
"MATRIX_3X3_AUTO",
2134 ccm_export_file_path: str =
"") -> str:
2136 Auto-calibrate camera image with color correction and return output filename.
2139 camera_label: Camera label
2140 red_band_label: Red band label
2141 green_band_label: Green band label
2142 blue_band_label: Blue band label
2143 output_file_path: Output file path
2144 print_quality_report: Whether to print quality report
2145 algorithm: Color correction algorithm ("DIAGONAL_ONLY", "MATRIX_3X3_AUTO", "MATRIX_3X3_FORCE")
2146 ccm_export_file_path: Path to export color correction matrix (optional)
2149 Output filename string
2152 RadiationModelError: If auto-calibration fails
2153 TypeError: If parameters have incorrect types
2154 ValueError: If algorithm is not valid
2157 if not isinstance(camera_label, str)
or not camera_label.strip():
2158 raise TypeError(
"Camera label must be a non-empty string")
2159 if not isinstance(red_band_label, str)
or not red_band_label.strip():
2160 raise TypeError(
"Red band label must be a non-empty string")
2161 if not isinstance(green_band_label, str)
or not green_band_label.strip():
2162 raise TypeError(
"Green band label must be a non-empty string")
2163 if not isinstance(blue_band_label, str)
or not blue_band_label.strip():
2164 raise TypeError(
"Blue band label must be a non-empty string")
2165 if not isinstance(output_file_path, str)
or not output_file_path.strip():
2166 raise TypeError(
"Output file path must be a non-empty string")
2167 if not isinstance(print_quality_report, bool):
2168 raise TypeError(
"print_quality_report must be a boolean")
2169 if not isinstance(ccm_export_file_path, str):
2170 raise TypeError(
"ccm_export_file_path must be a string")
2175 "MATRIX_3X3_AUTO": 1,
2176 "MATRIX_3X3_FORCE": 2
2179 if algorithm
not in algorithm_map:
2180 raise ValueError(f
"Invalid algorithm: {algorithm}. Must be one of: {list(algorithm_map.keys())}")
2182 algorithm_int = algorithm_map[algorithm]
2184 filename = radiation_wrapper.autoCalibrateCameraImage(
2186 blue_band_label, output_file_path, print_quality_report,
2187 algorithm_int, ccm_export_file_path)
2189 logger.info(f
"Auto-calibrated camera image written to: {filename}")
2193 """Get information about the radiation plugin."""
2194 registry = get_plugin_registry()
2195 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", 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.
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...