0.1.13
Loading...
Searching...
No Matches
RadiationModel.py
Go to the documentation of this file.
1"""
2High-level RadiationModel interface for PyHelios.
3
4This module provides a user-friendly interface to the radiation modeling
5capabilities with graceful plugin handling and informative error messages.
6"""
7
8import logging
9from typing import List, Optional
10from contextlib import contextmanager
11from pathlib import Path
12import os
13
14from .plugins.registry import get_plugin_registry, require_plugin, graceful_plugin_fallback
15from .wrappers import URadiationModelWrapper as radiation_wrapper
16from .validation.plugins import (
17 validate_wavelength_range, validate_flux_value, validate_ray_count,
18 validate_direction_vector, validate_band_label, validate_source_id, validate_source_id_list
19)
20from .validation.plugin_decorators import (
21 validate_radiation_band_params, validate_collimated_source_params, validate_sphere_source_params,
22 validate_sun_sphere_params, validate_get_source_flux_params,
23 validate_update_geometry_params, validate_run_band_params, validate_scattering_depth_params,
24 validate_min_scatter_energy_params
25)
26from .Context import Context
27from .assets import get_asset_manager
28
29logger = logging.getLogger(__name__)
30
31
32@contextmanager
34 """
35 Context manager that temporarily changes working directory to where RadiationModel assets are located.
36
37 RadiationModel C++ code uses hardcoded relative paths like "plugins/radiation/cuda_compile_ptx_generated_rayGeneration.cu.ptx"
38 expecting assets relative to working directory. This manager temporarily changes to the build directory
39 where assets are actually located.
40
41 Raises:
42 RuntimeError: If build directory or RadiationModel assets are not found, indicating a build system error.
43 """
44 # Find the build directory containing RadiationModel assets
45 # Try asset manager first (works for both development and wheel installations)
46 asset_manager = get_asset_manager()
47 working_dir = asset_manager._get_helios_build_path()
48
49 if working_dir and working_dir.exists():
50 radiation_assets = working_dir / 'plugins' / 'radiation'
51 else:
52 # For wheel installations, check packaged assets
53 current_dir = Path(__file__).parent
54 packaged_build = current_dir / 'assets' / 'build'
55
56 if packaged_build.exists():
57 working_dir = packaged_build
58 radiation_assets = working_dir / 'plugins' / 'radiation'
59 else:
60 # Fallback to development paths
61 repo_root = current_dir.parent
62 build_lib_dir = repo_root / 'pyhelios_build' / 'build' / 'lib'
63 working_dir = build_lib_dir.parent
64 radiation_assets = working_dir / 'plugins' / 'radiation'
65
66 if not build_lib_dir.exists():
67 raise RuntimeError(
68 f"PyHelios build directory not found at {build_lib_dir}. "
69 f"Run: python build_scripts/build_helios.py --plugins radiation"
70 )
71
72 if not radiation_assets.exists():
73 raise RuntimeError(
74 f"RadiationModel assets not found at {radiation_assets}. "
75 f"This indicates a build system error. The build script should copy PTX files to this location."
76 )
77
78 # Change to the build directory temporarily
79 original_dir = os.getcwd()
80 try:
81 os.chdir(working_dir)
82 logger.debug(f"Changed working directory to {working_dir} for RadiationModel asset access")
83 yield working_dir
84 finally:
85 os.chdir(original_dir)
86 logger.debug(f"Restored working directory to {original_dir}")
87
88
89class RadiationModelError(Exception):
90 """Raised when RadiationModel operations fail."""
91 pass
93
95 """
96 Camera properties for radiation model cameras.
97
98 This class encapsulates the properties needed to configure a radiation camera,
99 providing sensible defaults and validation for camera parameters. Updated for
100 Helios v1.3.60 with camera_zoom support.
101 """
102
103 def __init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05,
104 HFOV=20.0, FOV_aspect_ratio=0.0, lens_focal_length=0.05,
105 sensor_width_mm=35.0, model="generic", lens_make="", lens_model="",
106 lens_specification="", exposure="auto", shutter_speed=1.0/125.0,
107 white_balance="auto", camera_zoom=1.0):
108 """
109 Initialize camera properties with defaults matching C++ CameraProperties.
110
111 Args:
112 camera_resolution: Camera resolution as (width, height) tuple or list. Default: (512, 512)
113 focal_plane_distance: Distance from viewing plane to focal plane (working distance). Default: 1.0
114 lens_diameter: Diameter of camera lens (0 = pinhole camera). Default: 0.05
115 HFOV: Horizontal field of view in degrees. Default: 20.0
116 FOV_aspect_ratio: Ratio of horizontal to vertical FOV. Default: 0.0 (auto-calculate from resolution)
117 lens_focal_length: Camera lens optical focal length in meters (physical, not 35mm equiv). Default: 0.05 (50mm)
118 sensor_width_mm: Physical sensor width in mm. Default: 35.0 (full-frame)
119 model: Camera model name (e.g., "Nikon D700", "Canon EOS 5D"). Default: "generic"
120 lens_make: Lens manufacturer (e.g., "Canon", "Nikon"). Default: ""
121 lens_model: Lens model name (e.g., "AF-S NIKKOR 50mm f/1.8G"). Default: ""
122 lens_specification: Lens specification (e.g., "50mm f/1.8"). Default: ""
123 exposure: Exposure mode - "auto", "ISOXXX" (e.g., "ISO100"), or "manual". Default: "auto"
124 shutter_speed: Camera shutter speed in seconds (e.g., 0.008 for 1/125s). Default: 0.008 (1/125s)
125 white_balance: White balance mode - "auto" or "off". Default: "auto"
126 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
128 """
129 # Set camera resolution with validation
130
131 if camera_resolution is None:
132 self.camera_resolution = (512, 512)
133 else:
134 if isinstance(camera_resolution, (list, tuple)) and len(camera_resolution) == 2:
135 self.camera_resolution = (int(camera_resolution[0]), int(camera_resolution[1]))
136 else:
137 raise ValueError("camera_resolution must be a tuple or list of 2 integers")
138
139
140 # Validate and set numeric properties
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")
155 if camera_zoom <= 0:
156 raise ValueError("camera_zoom must be greater than 0")
157
158 self.focal_plane_distance = float(focal_plane_distance)
159 self.lens_diameter = float(lens_diameter)
160 self.HFOV = float(HFOV)
161 self.FOV_aspect_ratio = float(FOV_aspect_ratio)
162 self.lens_focal_length = float(lens_focal_length)
163 self.sensor_width_mm = float(sensor_width_mm)
164 self.shutter_speed = float(shutter_speed)
165 self.camera_zoom = float(camera_zoom)
167 # Validate and set string properties
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")
180
181 # Validate exposure mode
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')")
184
185 # Validate white balance mode
186 if white_balance not in ["auto", "off"]:
187 raise ValueError("white_balance must be 'auto' or 'off'")
188
189 self.model = str(model)
190 self.lens_make = str(lens_make)
191 self.lens_model = str(lens_model)
192 self.lens_specification = str(lens_specification)
193 self.exposure = str(exposure)
194 self.white_balance = str(white_balance)
196 def to_array(self):
197 """
198 Convert to array format expected by C++ interface.
199
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.
203
204 Returns:
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]
208 """
209 return [
210 float(self.camera_resolution[0]), # resolution_x
211 float(self.camera_resolution[1]), # resolution_y
213 self.lens_diameter,
214 self.HFOV,
215 self.FOV_aspect_ratio,
217 self.sensor_width_mm,
218 self.shutter_speed,
219 self.camera_zoom
220 ]
221
222 def __repr__(self):
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})")
239
240
241class CameraMetadata:
242 """
243 Metadata for radiation camera image export (Helios v1.3.58+).
244
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.
248 """
249
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"):
257 self.height = int(height)
258 self.width = int(width)
259 self.channels = int(channels)
260 self.type = str(type)
261 self.focal_length = float(focal_length)
262 self.aperture = str(aperture)
263 self.sensor_width = float(sensor_width)
264 self.sensor_height = float(sensor_height)
265 self.model = str(model)
266 self.lens_make = str(lens_make)
267 self.lens_model = str(lens_model)
268 self.lens_specification = str(lens_specification)
269 self.exposure = str(exposure)
270 self.shutter_speed = float(shutter_speed)
271 self.white_balance = str(white_balance)
274 """Geographic location properties."""
275 def __init__(self, latitude=0.0, longitude=0.0):
276 self.latitude = float(latitude)
277 self.longitude = float(longitude)
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"):
283 self.date = str(date)
284 self.time = str(time)
285 self.UTC_offset = float(UTC_offset)
286 self.camera_height_m = float(camera_height_m)
287 self.camera_angle_deg = float(camera_angle_deg)
288 self.light_source = str(light_source)
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"):
294 self.saturation_adjustment = float(saturation_adjustment)
295 self.brightness_adjustment = float(brightness_adjustment)
296 self.contrast_adjustment = float(contrast_adjustment)
297 self.color_space = str(color_space)
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,
303 weed_pressure=""):
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 []
306 self.plant_height_m = plant_height_m if plant_height_m is not None else []
307 self.plant_age_days = plant_age_days if plant_age_days is not None else []
308 self.plant_stage = plant_stage if plant_stage is not None else []
309 self.leaf_area_m2 = leaf_area_m2 if leaf_area_m2 is not None else []
310 self.weed_pressure = str(weed_pressure)
312 def __init__(self, path=""):
313 """
314 Initialize CameraMetadata with default values.
315
316 Args:
317 path: Full path to the associated image file. Default: ""
318 """
319 self.path = str(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}))")
331
332
333class RadiationModel:
334 """
335 High-level interface for radiation modeling and ray tracing.
336
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.
340 """
341
342 def __init__(self, context: Context):
343 """
344 Initialize RadiationModel with graceful plugin handling.
345
346 Args:
347 context: Helios Context instance
348
349 Raises:
350 TypeError: If context is not a Context instance
351 RadiationModelError: If radiation plugin is not available
352 """
353 # Validate context type
354 if not isinstance(context, Context):
355 raise TypeError(f"RadiationModel requires a Context instance, got {type(context).__name__}")
356
357 self.context = context
358 self.radiation_model = None
360 # Check plugin availability using registry
361 registry = get_plugin_registry()
362
363 if not registry.is_plugin_available('radiation'):
364 # Get helpful information about the missing plugin
365 plugin_info = registry.get_plugin_capabilities()
366 available_plugins = registry.get_available_plugins()
367
368 error_msg = (
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}"
381 )
382
383 # Suggest alternatives if available
384 alternatives = registry.suggest_alternatives('radiation')
385 if alternatives:
386 error_msg += f"\n\nAlternative plugins available: {alternatives}"
387 error_msg += "\nConsider using energybalance or leafoptics for thermal modeling."
388
389 raise RadiationModelError(error_msg)
390
391 # Plugin is available - create radiation model using working directory context manager
392 try:
394 self.radiation_model = radiation_wrapper.createRadiationModel(context.getNativePtr())
395 if self.radiation_model is None:
397 "Failed to create RadiationModel instance. "
398 "This may indicate a problem with the native library or GPU initialization."
399 )
400 logger.info("RadiationModel created successfully")
401
402 except Exception as e:
403 raise RadiationModelError(f"Failed to initialize RadiationModel: {e}")
404
405 def __enter__(self):
406 """Context manager entry."""
407 return self
409 def __exit__(self, exc_type, exc_value, traceback):
410 """Context manager exit with proper cleanup."""
411 if self.radiation_model is not None:
412 try:
413 radiation_wrapper.destroyRadiationModel(self.radiation_model)
414 logger.debug("RadiationModel destroyed successfully")
415 except Exception as e:
416 logger.warning(f"Error destroying RadiationModel: {e}")
417 finally:
418 self.radiation_model = None # Prevent double deletion
419
420 def __del__(self):
421 """Destructor to ensure GPU resources freed even without 'with' statement."""
422 if hasattr(self, 'radiation_model') and self.radiation_model is not None:
423 try:
424 radiation_wrapper.destroyRadiationModel(self.radiation_model)
425 self.radiation_model = None
426 except Exception as e:
427 import warnings
428 warnings.warn(f"Error in RadiationModel.__del__: {e}")
429
430 def get_native_ptr(self):
431 """Get native pointer for advanced operations."""
432 return self.radiation_model
434 def getNativePtr(self):
435 """Get native pointer for advanced operations. (Legacy naming for compatibility)"""
436 return self.get_native_ptr()
438 @require_plugin('radiation', 'disable status messages')
439 def disableMessages(self):
440 """Disable RadiationModel status messages."""
441 radiation_wrapper.disableMessages(self.radiation_model)
443 @require_plugin('radiation', 'enable status messages')
444 def enableMessages(self):
445 """Enable RadiationModel status messages."""
446 radiation_wrapper.enableMessages(self.radiation_model)
448 @require_plugin('radiation', 'add radiation band')
449 def addRadiationBand(self, band_label: str, wavelength_min: float = None, wavelength_max: float = None):
450 """
451 Add radiation band with optional wavelength bounds.
452
453 Args:
454 band_label: Name/label for the radiation band
455 wavelength_min: Optional minimum wavelength (μm)
456 wavelength_max: Optional maximum wavelength (μm)
457 """
458 # Validate inputs
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")
464 else:
465 radiation_wrapper.addRadiationBand(self.radiation_model, band_label)
466 logger.debug(f"Added radiation band: {band_label}")
467
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):
471 """
472 Copy existing radiation band to new label, optionally with new wavelength range.
473
474 Args:
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)
479
480 Example:
481 >>> # Copy band with same wavelength range
482 >>> radiation.copyRadiationBand("SW", "SW_copy")
483 >>>
484 >>> # Copy band with different wavelength range
485 >>> radiation.copyRadiationBand("full_spectrum", "PAR", 400, 700)
486 """
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")
489
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")
493 else:
494 logger.debug(f"Copied radiation band {old_label} to {new_label}")
495
496 @require_plugin('radiation', 'add radiation source')
497 @validate_collimated_source_params
498 def addCollimatedRadiationSource(self, direction=None) -> int:
499 """
500 Add collimated radiation source.
501
502 Args:
503 direction: Optional direction vector. Can be tuple (x, y, z), vec3, or None for default direction.
504
505 Returns:
506 Source ID
507 """
508 if direction is None:
509 source_id = radiation_wrapper.addCollimatedRadiationSourceDefault(self.radiation_model)
510 else:
511 # Handle vec3, SphericalCoord, and tuple types
512 if hasattr(direction, 'x') and hasattr(direction, 'y') and hasattr(direction, 'z'):
513 # vec3-like object
514 x, y, z = direction.x, direction.y, direction.z
515 elif hasattr(direction, 'radius') and hasattr(direction, 'elevation') and hasattr(direction, 'azimuth'):
516 # SphericalCoord object - convert to Cartesian
517 import math
518 r = direction.radius
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)
524 else:
525 # Assume tuple-like object - validate it first
526 try:
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")
529 x, y, z = direction
530 except (TypeError, AttributeError):
531 # Not a valid sequence type
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)
534
535 logger.debug(f"Added collimated radiation source: ID {source_id}")
536 return source_id
537
538 @require_plugin('radiation', 'add spherical radiation source')
539 @validate_sphere_source_params
540 def addSphereRadiationSource(self, position, radius: float) -> int:
541 """
542 Add spherical radiation source.
543
544 Args:
545 position: Position of the source. Can be tuple (x, y, z) or vec3.
546 radius: Radius of the spherical source
547
548 Returns:
549 Source ID
550 """
551 # Handle both tuple and vec3 types
552 if hasattr(position, 'x') and hasattr(position, 'y') and hasattr(position, 'z'):
553 # vec3-like object
554 x, y, z = position.x, position.y, position.z
555 else:
556 # Assume tuple-like object
557 x, y, z = position
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}")
560 return source_id
561
562 @require_plugin('radiation', 'add sun radiation source')
563 @validate_sun_sphere_params
564 def addSunSphereRadiationSource(self, radius: float, zenith: float, azimuth: float,
565 position_scaling: float = 1.0, angular_width: float = 0.53,
566 flux_scaling: float = 1.0) -> int:
567 """
568 Add sun sphere radiation source.
569
570 Args:
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
577
578 Returns:
579 Source ID
580 """
581 source_id = radiation_wrapper.addSunSphereRadiationSource(
582 self.radiation_model, radius, zenith, azimuth, position_scaling, angular_width, flux_scaling
583 )
584 logger.debug(f"Added sun radiation source: ID {source_id}")
585 return source_id
586
587 @require_plugin('radiation', 'set source position')
588 def setSourcePosition(self, source_id: int, position):
589 """
590 Set position of a radiation source.
591
592 Allows dynamic repositioning of radiation sources during simulation,
593 useful for time-series modeling or moving light sources.
594
595 Args:
596 source_id: ID of the radiation source
597 position: New position as vec3, SphericalCoord, or list/tuple [x, y, z]
598
599 Example:
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))
604 """
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}")
609
610 @require_plugin('radiation', 'add rectangle radiation source')
611 def addRectangleRadiationSource(self, position, size, rotation) -> int:
612 """
613 Add a rectangle (planar) radiation source.
614
615 Rectangle sources are ideal for modeling artificial lighting such as
616 LED panels, grow lights, or window light sources.
617
618 Args:
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)
622
623 Returns:
624 Source ID
625
626 Example:
627 >>> from pyhelios.types import vec3, vec2
628 >>> source_id = radiation.addRectangleRadiationSource(
629 ... position=vec3(0, 0, 5),
630 ... size=vec2(2, 1),
631 ... rotation=vec3(0, 0, 0)
632 ... )
633 >>> radiation.setSourceFlux(source_id, "PAR", 500.0)
634 """
635 return radiation_wrapper.addRectangleRadiationSource(self.radiation_model, position, size, rotation)
637 @require_plugin('radiation', 'add disk radiation source')
638 def addDiskRadiationSource(self, position, radius: float, rotation) -> int:
639 """
640 Add a disk (circular planar) radiation source.
641
642 Disk sources are useful for modeling circular light sources such as
643 spotlights, circular LED arrays, or solar simulators.
644
645 Args:
646 position: Center position as vec3 or list [x, y, z]
647 radius: Disk radius
648 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
649
650 Returns:
651 Source ID
652
653 Example:
654 >>> from pyhelios.types import vec3
655 >>> source_id = radiation.addDiskRadiationSource(
656 ... position=vec3(0, 0, 5),
657 ... radius=1.5,
658 ... rotation=vec3(0, 0, 0)
659 ... )
660 >>> radiation.setSourceFlux(source_id, "PAR", 300.0)
661 """
662 if radius <= 0:
663 raise ValueError(f"Radius must be positive, got {radius}")
664 return radiation_wrapper.addDiskRadiationSource(self.radiation_model, position, radius, rotation)
665
666 # Source spectrum methods
667 @require_plugin('radiation', 'manage source spectrum')
668 def setSourceSpectrum(self, source_id, spectrum):
669 """
670 Set radiation spectrum for source(s).
671
672 Spectral distributions define how radiation intensity varies with wavelength,
673 essential for realistic modeling of different light sources (sunlight, LEDs, etc.).
674
675 Args:
676 source_id: Source ID (int) or list of source IDs
677 spectrum: Either:
678 - Spectrum data as list of (wavelength, value) tuples
679 - Global data label string
680
681 Example:
682 >>> # Define custom LED spectrum
683 >>> led_spectrum = [
684 ... (400, 0.0), (450, 0.3), (500, 0.8),
685 ... (550, 0.5), (600, 0.2), (700, 0.0)
686 ... ]
687 >>> radiation.setSourceSpectrum(source_id, led_spectrum)
688 >>>
689 >>> # Use predefined spectrum from global data
690 >>> radiation.setSourceSpectrum(source_id, "D65_illuminant")
691 >>>
692 >>> # Apply same spectrum to multiple sources
693 >>> radiation.setSourceSpectrum([src1, src2, src3], led_spectrum)
694 """
695 radiation_wrapper.setSourceSpectrum(self.radiation_model, source_id, spectrum)
696 logger.debug(f"Set spectrum for source(s) {source_id}")
697
698 @require_plugin('radiation', 'configure source spectrum')
699 def setSourceSpectrumIntegral(self, source_id: int, source_integral: float,
700 wavelength_min: float = None, wavelength_max: float = None):
701 """
702 Set source spectrum integral value.
703
704 Normalizes the spectrum so that its integral equals the specified value,
705 useful for calibrating source intensity.
706
707 Args:
708 source_id: Source ID
709 source_integral: Target integral value
710 wavelength_min: Optional minimum wavelength for integration range
711 wavelength_max: Optional maximum wavelength for integration range
712
713 Example:
714 >>> radiation.setSourceSpectrumIntegral(source_id, 1000.0)
715 >>> radiation.setSourceSpectrumIntegral(source_id, 500.0, 400, 700) # PAR range
716 """
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}")
721
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}")
725
726 # Spectrum integration and analysis methods
727 @require_plugin('radiation', 'integrate spectrum')
728 def integrateSpectrum(self, object_spectrum, wavelength_min: float = None,
729 wavelength_max: float = None, source_id: int = None,
730 camera_spectrum=None) -> float:
731 """
732 Integrate spectrum with optional source/camera spectra and wavelength range.
733
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
740
741 Args:
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
747
748 Returns:
749 Integrated value
750
751 Example:
752 >>> leaf_reflectance = [(400, 0.1), (500, 0.4), (600, 0.6), (700, 0.5)]
753 >>>
754 >>> # Total integration
755 >>> total = radiation.integrateSpectrum(leaf_reflectance)
756 >>>
757 >>> # PAR range (400-700nm)
758 >>> par = radiation.integrateSpectrum(leaf_reflectance, 400, 700)
759 >>>
760 >>> # With source spectrum
761 >>> source_weighted = radiation.integrateSpectrum(
762 ... leaf_reflectance, 400, 700, source_id=sun_source
763 ... )
764 >>>
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
769 ... )
770 """
771 return radiation_wrapper.integrateSpectrum(self.radiation_model, object_spectrum,
772 wavelength_min, wavelength_max,
773 source_id, camera_spectrum)
774
775 @require_plugin('radiation', 'integrate source spectrum')
776 def integrateSourceSpectrum(self, source_id: int, wavelength_min: float, wavelength_max: float) -> float:
777 """
778 Integrate source spectrum over wavelength range.
779
780 Args:
781 source_id: Source ID
782 wavelength_min: Minimum wavelength
783 wavelength_max: Maximum wavelength
784
785 Returns:
786 Integrated source spectrum value
787
788 Example:
789 >>> par_flux = radiation.integrateSourceSpectrum(source_id, 400, 700)
790 """
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)
795
796 # Spectral manipulation methods
797 @require_plugin('radiation', 'scale spectrum')
798 def scaleSpectrum(self, existing_label: str, new_label_or_scale, scale_factor: float = None):
799 """
800 Scale spectrum in-place or to new label.
801
802 Useful for adjusting spectrum intensities or creating variations of
803 existing spectra for sensitivity analysis.
804
805 Supports two call patterns:
806 - scaleSpectrum("label", scale) -> scales in-place
807 - scaleSpectrum("existing", "new", scale) -> creates new scaled spectrum
808
809 Args:
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)
813
814 Example:
815 >>> # In-place scaling
816 >>> radiation.scaleSpectrum("leaf_reflectance", 1.2)
817 >>>
818 >>> # Create new scaled spectrum
819 >>> radiation.scaleSpectrum("leaf_reflectance", "scaled_leaf", 1.5)
820 """
821 if not isinstance(existing_label, str) or not existing_label.strip():
822 raise ValueError("Existing label must be a non-empty string")
823
824 radiation_wrapper.scaleSpectrum(self.radiation_model, existing_label,
825 new_label_or_scale, scale_factor)
826 logger.debug(f"Scaled spectrum '{existing_label}'")
827
828 @require_plugin('radiation', 'scale spectrum randomly')
829 def scaleSpectrumRandomly(self, existing_label: str, new_label: str,
830 min_scale: float, max_scale: float):
831 """
832 Scale spectrum with random factor and store as new label.
833
834 Useful for creating stochastic variations in spectral properties for
835 Monte Carlo simulations or uncertainty quantification.
836
837 Args:
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
842
843 Example:
844 >>> # Create random variation of leaf reflectance
845 >>> radiation.scaleSpectrumRandomly("leaf_base", "leaf_variant", 0.8, 1.2)
846 """
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})")
853
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}'")
857
858 @require_plugin('radiation', 'blend spectra')
859 def blendSpectra(self, new_label: str, spectrum_labels: List[str], weights: List[float]):
860 """
861 Blend multiple spectra with specified weights.
862
863 Creates weighted combination of spectra, useful for mixing material properties
864 or creating composite light sources.
865
866 Args:
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)
870
871 Example:
872 >>> # Mix two leaf types (70% type A, 30% type B)
873 >>> radiation.blendSpectra("mixed_leaf",
874 ... ["leaf_type_a", "leaf_type_b"],
875 ... [0.7, 0.3]
876 ... )
877 """
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")
884
885 radiation_wrapper.blendSpectra(self.radiation_model, new_label, spectrum_labels, weights)
886 logger.debug(f"Blended {len(spectrum_labels)} spectra into '{new_label}'")
887
888 @require_plugin('radiation', 'blend spectra randomly')
889 def blendSpectraRandomly(self, new_label: str, spectrum_labels: List[str]):
890 """
891 Blend multiple spectra with random weights.
892
893 Creates random combinations of spectra, useful for generating diverse
894 material properties in stochastic simulations.
895
896 Args:
897 new_label: New global data label for blended spectrum
898 spectrum_labels: List of spectrum labels to blend
899
900 Example:
901 >>> # Create random mixture of leaf spectra
902 >>> radiation.blendSpectraRandomly("random_leaf",
903 ... ["young_leaf", "mature_leaf", "senescent_leaf"]
904 ... )
905 """
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")
910
911 radiation_wrapper.blendSpectraRandomly(self.radiation_model, new_label, spectrum_labels)
912 logger.debug(f"Blended {len(spectrum_labels)} spectra randomly into '{new_label}'")
913
914 # Spectral interpolation methods
915 @require_plugin('radiation', 'interpolate spectrum from data')
916 def interpolateSpectrumFromPrimitiveData(self, primitive_uuids: List[int],
917 spectra_labels: List[str], values: List[float],
918 primitive_data_query_label: str,
919 primitive_data_radprop_label: str):
920 """
921 Interpolate spectral properties based on primitive data values.
922
923 Automatically assigns spectra to primitives by interpolating between
924 reference spectra based on continuous data values (e.g., age, moisture, etc.).
925
926 Args:
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
932
933 Example:
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"
942 ... )
943 """
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)})")
952
953 radiation_wrapper.interpolateSpectrumFromPrimitiveData(
954 self.radiation_model, primitive_uuids, spectra_labels, values,
955 primitive_data_query_label, primitive_data_radprop_label
956 )
957 logger.debug(f"Interpolated spectra for {len(primitive_uuids)} primitives")
958
959 @require_plugin('radiation', 'interpolate spectrum from object data')
960 def interpolateSpectrumFromObjectData(self, object_ids: List[int],
961 spectra_labels: List[str], values: List[float],
962 object_data_query_label: str,
963 primitive_data_radprop_label: str):
964 """
965 Interpolate spectral properties based on object data values.
966
967 Automatically assigns spectra to object primitives by interpolating between
968 reference spectra based on continuous object-level data values.
969
970 Args:
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
976
977 Example:
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"
986 ... )
987 """
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)})")
996
997 radiation_wrapper.interpolateSpectrumFromObjectData(
998 self.radiation_model, object_ids, spectra_labels, values,
999 object_data_query_label, primitive_data_radprop_label
1000 )
1001 logger.debug(f"Interpolated spectra for {len(object_ids)} objects")
1002
1003 @require_plugin('radiation', 'set ray count')
1004 def setDirectRayCount(self, band_label: str, ray_count: int):
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)
1009
1010 @require_plugin('radiation', 'set ray count')
1011 def setDiffuseRayCount(self, band_label: str, ray_count: int):
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)
1016
1017 @require_plugin('radiation', 'set radiation flux')
1018 def setDiffuseRadiationFlux(self, label: str, flux: float):
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)
1023
1024 @require_plugin('radiation', 'configure diffuse radiation')
1025 def setDiffuseRadiationExtinctionCoeff(self, label: str, K: float, peak_direction):
1026 """
1027 Set diffuse radiation extinction coefficient with directional bias.
1028
1029 Models directionally-biased diffuse radiation (e.g., sky radiation with zenith peak).
1030
1031 Args:
1032 label: Band label
1033 K: Extinction coefficient
1034 peak_direction: Peak direction as vec3, SphericalCoord, or list [x, y, z]
1035
1036 Example:
1037 >>> from pyhelios.types import vec3
1038 >>> radiation.setDiffuseRadiationExtinctionCoeff("SW", 0.5, vec3(0, 0, 1))
1039 """
1040 validate_band_label(label, "label", "setDiffuseRadiationExtinctionCoeff")
1041 if K < 0:
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}")
1045
1046 @require_plugin('radiation', 'query diffuse flux')
1047 def getDiffuseFlux(self, band_label: str) -> float:
1048 """
1049 Get diffuse flux for band.
1050
1051 Args:
1052 band_label: Band label
1053
1054 Returns:
1055 Diffuse flux value
1056
1057 Example:
1058 >>> flux = radiation.getDiffuseFlux("SW")
1059 """
1060 validate_band_label(band_label, "band_label", "getDiffuseFlux")
1061 return radiation_wrapper.getDiffuseFlux(self.radiation_model, band_label)
1062
1063 @require_plugin('radiation', 'configure diffuse spectrum')
1064 def setDiffuseSpectrum(self, band_label, spectrum_label: str):
1065 """
1066 Set diffuse spectrum from global data label.
1067
1068 Args:
1069 band_label: Band label (string) or list of band labels
1070 spectrum_label: Spectrum global data label
1071
1072 Example:
1073 >>> radiation.setDiffuseSpectrum("SW", "sky_spectrum")
1074 >>> radiation.setDiffuseSpectrum(["SW", "NIR"], "sky_spectrum")
1075 """
1076 if isinstance(band_label, str):
1077 validate_band_label(band_label, "band_label", "setDiffuseSpectrum")
1078 else:
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")
1083
1084 radiation_wrapper.setDiffuseSpectrum(self.radiation_model, band_label, spectrum_label)
1085 logger.debug(f"Set diffuse spectrum for band(s) {band_label}")
1086
1087 @require_plugin('radiation', 'configure diffuse spectrum')
1088 def setDiffuseSpectrumIntegral(self, spectrum_integral: float, wavelength_min: float = None,
1089 wavelength_max: float = None, band_label: str = None):
1090 """
1091 Set diffuse spectrum integral.
1092
1093 Args:
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)
1098
1099 Example:
1100 >>> radiation.setDiffuseSpectrumIntegral(1000.0) # All bands
1101 >>> radiation.setDiffuseSpectrumIntegral(500.0, 400, 700, band_label="PAR") # Specific band
1102 """
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")
1107
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}")
1111
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")
1117
1118 if isinstance(source_id, (list, tuple)):
1119 # Multiple sources
1120 validate_source_id_list(list(source_id), "source_id", "setSourceFlux")
1121 radiation_wrapper.setSourceFluxMultiple(self.radiation_model, source_id, label, flux)
1122 else:
1123 # Single source
1124 validate_source_id(source_id, "source_id", "setSourceFlux")
1125 radiation_wrapper.setSourceFlux(self.radiation_model, source_id, label, flux)
1126
1127
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
1136 def updateGeometry(self, uuids: Optional[List[int]] = None):
1137 """
1138 Update geometry in radiation model.
1139
1140 Args:
1141 uuids: Optional list of specific UUIDs to update. If None, updates all geometry.
1142 """
1143 if uuids is None:
1144 radiation_wrapper.updateGeometry(self.radiation_model)
1145 logger.debug("Updated all geometry in radiation model")
1146 else:
1147 radiation_wrapper.updateGeometryUUIDs(self.radiation_model, uuids)
1148 logger.debug(f"Updated {len(uuids)} geometry UUIDs in radiation model")
1149
1150 @require_plugin('radiation', 'run radiation simulation')
1151 @validate_run_band_params
1152 def runBand(self, band_label):
1153 """
1154 Run radiation simulation for single band or multiple bands.
1155
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
1159 because:
1160
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
1165
1166 Example:
1167 # EFFICIENT - Single call for multiple bands
1168 radiation.runBand(["PAR", "NIR", "SW"])
1169
1170 # INEFFICIENT - Sequential single-band calls
1171 radiation.runBand("PAR")
1172 radiation.runBand("NIR")
1173 radiation.runBand("SW")
1174
1175 Args:
1176 band_label: Single band name (str) or list of band names for multi-band simulation
1177 """
1178 if isinstance(band_label, (list, tuple)):
1179 # Multiple bands - validate each label
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__}")
1183 radiation_wrapper.runBandMultiple(self.radiation_model, band_label)
1184 logger.info(f"Completed radiation simulation for bands: {band_label}")
1185 else:
1186 # Single band - validate label type
1187 if not isinstance(band_label, str):
1188 raise TypeError(f"Band label must be a string, got {type(band_label).__name__}")
1189 radiation_wrapper.runBand(self.radiation_model, band_label)
1190 logger.info(f"Completed radiation simulation for band: {band_label}")
1191
1192
1193 @require_plugin('radiation', 'get simulation results')
1194 def getTotalAbsorbedFlux(self) -> List[float]:
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")
1198 return results
1199
1200 # Band query methods
1201 @require_plugin('radiation', 'check band existence')
1202 def doesBandExist(self, label: str) -> bool:
1203 """
1204 Check if a radiation band exists.
1205
1206 Args:
1207 label: Name/label of the radiation band to check
1208
1209 Returns:
1210 True if band exists, False otherwise
1211
1212 Example:
1213 >>> radiation.addRadiationBand("SW")
1214 >>> radiation.doesBandExist("SW")
1215 True
1216 >>> radiation.doesBandExist("nonexistent")
1217 False
1218 """
1219 validate_band_label(label, "label", "doesBandExist")
1220 return radiation_wrapper.doesBandExist(self.radiation_model, label)
1221
1222 # Advanced source management methods
1223 @require_plugin('radiation', 'manage radiation sources')
1224 def deleteRadiationSource(self, source_id: int):
1225 """
1226 Delete a radiation source.
1227
1228 Args:
1229 source_id: ID of the radiation source to delete
1230
1231 Example:
1232 >>> source_id = radiation.addCollimatedRadiationSource()
1233 >>> radiation.deleteRadiationSource(source_id)
1234 """
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}")
1239
1240 @require_plugin('radiation', 'query radiation sources')
1241 def getSourcePosition(self, source_id: int):
1242 """
1243 Get position of a radiation source.
1244
1245 Args:
1246 source_id: ID of the radiation source
1247
1248 Returns:
1249 vec3 position of the source
1250
1251 Example:
1252 >>> source_id = radiation.addCollimatedRadiationSource()
1253 >>> position = radiation.getSourcePosition(source_id)
1254 >>> print(f"Source at: {position}")
1255 """
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])
1261
1262 # Advanced simulation methods
1263 @require_plugin('radiation', 'get sky energy')
1264 def getSkyEnergy(self) -> float:
1265 """
1266 Get total sky energy.
1267
1268 Returns:
1269 Total sky energy value
1270
1271 Example:
1272 >>> energy = radiation.getSkyEnergy()
1273 >>> print(f"Sky energy: {energy}")
1274 """
1275 return radiation_wrapper.getSkyEnergy(self.radiation_model)
1277 @require_plugin('radiation', 'calculate G-function')
1278 def calculateGtheta(self, view_direction) -> float:
1279 """
1280 Calculate G-function (geometry factor) for given view direction.
1281
1282 The G-function describes the geometric relationship between leaf area
1283 distribution and viewing direction, important for canopy radiation modeling.
1284
1285 Args:
1286 view_direction: View direction as vec3 or list/tuple [x, y, z]
1287
1288 Returns:
1289 G-function value
1290
1291 Example:
1292 >>> from pyhelios.types import vec3
1293 >>> g_value = radiation.calculateGtheta(vec3(0, 0, 1))
1294 >>> print(f"G-function: {g_value}")
1295 """
1296 context_ptr = self.context.getNativePtr()
1297 return radiation_wrapper.calculateGtheta(self.radiation_model, context_ptr, view_direction)
1298
1299 @require_plugin('radiation', 'configure output data')
1300 def optionalOutputPrimitiveData(self, label: str):
1301 """
1302 Enable optional primitive data output.
1303
1304 Args:
1305 label: Name/label of the primitive data to output
1306
1307 Example:
1308 >>> radiation.optionalOutputPrimitiveData("temperature")
1309 """
1310 validate_band_label(label, "label", "optionalOutputPrimitiveData")
1311 radiation_wrapper.optionalOutputPrimitiveData(self.radiation_model, label)
1312 logger.debug(f"Enabled optional output for primitive data: {label}")
1313
1314 @require_plugin('radiation', 'configure boundary conditions')
1315 def enforcePeriodicBoundary(self, boundary: str):
1316 """
1317 Enforce periodic boundary conditions.
1318
1319 Periodic boundaries are useful for large-scale simulations to reduce
1320 edge effects by wrapping radiation at domain boundaries.
1321
1322 Args:
1323 boundary: Boundary specification string (e.g., "xy", "xyz", "x", "y", "z")
1324
1325 Example:
1326 >>> radiation.enforcePeriodicBoundary("xy")
1327 """
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}")
1332
1333 # Configuration methods
1334 @require_plugin('radiation', 'configure radiation simulation')
1335 @validate_scattering_depth_params
1336 def setScatteringDepth(self, label: str, depth: int):
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
1342 def setMinScatterEnergy(self, label: str, energy: float):
1343 """Set minimum scatter energy for radiation band."""
1344 radiation_wrapper.setMinScatterEnergy(self.radiation_model, label, energy)
1346 @require_plugin('radiation', 'configure radiation emission')
1347 def disableEmission(self, label: str):
1348 """Disable emission for radiation band."""
1349 validate_band_label(label, "label", "disableEmission")
1350 radiation_wrapper.disableEmission(self.radiation_model, label)
1351
1352 @require_plugin('radiation', 'configure radiation emission')
1353 def enableEmission(self, label: str):
1354 """Enable emission for radiation band."""
1355 validate_band_label(label, "label", "enableEmission")
1356 radiation_wrapper.enableEmission(self.radiation_model, label)
1357
1358 #=============================================================================
1359 # Camera and Image Functions (v1.3.47)
1360 #=============================================================================
1361
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):
1365 """
1366 Add a radiation camera to the simulation.
1367
1368 Args:
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)
1377
1378 Raises:
1379 ValidationError: If parameters are invalid or have wrong types
1380 RadiationModelError: If camera creation fails
1381
1382 Example:
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)
1389 """
1390 # Import here to avoid circular imports
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
1394
1395 # Validate basic parameters
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")
1399
1400 # Validate position (must be vec3)
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
1404
1405 # Validate lookat_or_direction (must be vec3 or SphericalCoord)
1406 if hasattr(lookat_or_direction, 'radius') and hasattr(lookat_or_direction, 'elevation'):
1407 validated_direction = lookat_or_direction # SphericalCoord
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 # vec3
1410 else:
1411 raise TypeError("lookat_or_direction must be a vec3 or SphericalCoord object. Use vec3(x, y, z) or SphericalCoord to create one.")
1412
1413 # Set up camera properties
1414 if camera_properties is None:
1415 camera_properties = CameraProperties()
1416
1417 # Call appropriate wrapper function based on direction type
1418 try:
1419 if hasattr(validated_direction, 'radius') and hasattr(validated_direction, 'elevation'):
1420 # SphericalCoord case
1421 direction_coords = validated_direction.to_list()
1422 if len(direction_coords) >= 3:
1423 # Use only radius, elevation, azimuth (first 3 elements)
1424 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[2]
1425 else:
1426 raise ValueError("SphericalCoord must have at least radius, elevation, and azimuth")
1427
1428 radiation_wrapper.addRadiationCameraSpherical(
1429 self.radiation_model,
1430 validated_label,
1431 validated_bands,
1432 validated_position.x, validated_position.y, validated_position.z,
1433 radius, elevation, azimuth,
1434 camera_properties.to_array(),
1435 validated_samples
1436 )
1437 else:
1438 # vec3 case
1439 radiation_wrapper.addRadiationCameraVec3(
1440 self.radiation_model,
1441 validated_label,
1442 validated_bands,
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(),
1446 validated_samples
1447 )
1448
1449 except Exception as e:
1450 raise RadiationModelError(f"Failed to add radiation camera '{validated_label}': {e}")
1451
1452 @require_plugin('radiation', 'manage camera position')
1453 def setCameraPosition(self, camera_label: str, position):
1454 """
1455 Set camera position.
1456
1457 Allows dynamic camera repositioning during simulation, useful for
1458 time-series captures or multi-view imaging.
1459
1460 Args:
1461 camera_label: Camera label string
1462 position: Camera position as vec3 or list [x, y, z]
1463
1464 Example:
1465 >>> radiation.setCameraPosition("cam1", [0, 0, 10])
1466 >>> from pyhelios.types import vec3
1467 >>> radiation.setCameraPosition("cam1", vec3(5, 5, 10))
1468 """
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")
1473
1474 @require_plugin('radiation', 'query camera position')
1475 def getCameraPosition(self, camera_label: str):
1476 """
1477 Get camera position.
1478
1479 Args:
1480 camera_label: Camera label string
1481
1482 Returns:
1483 vec3 position of the camera
1484
1485 Example:
1486 >>> position = radiation.getCameraPosition("cam1")
1487 >>> print(f"Camera at: {position}")
1488 """
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])
1494
1495 @require_plugin('radiation', 'manage camera lookat')
1496 def setCameraLookat(self, camera_label: str, lookat):
1497 """
1498 Set camera lookat point.
1499
1500 Args:
1501 camera_label: Camera label string
1502 lookat: Lookat point as vec3 or list [x, y, z]
1503
1504 Example:
1505 >>> radiation.setCameraLookat("cam1", [0, 0, 0])
1506 """
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")
1511
1512 @require_plugin('radiation', 'query camera lookat')
1513 def getCameraLookat(self, camera_label: str):
1514 """
1515 Get camera lookat point.
1516
1517 Args:
1518 camera_label: Camera label string
1519
1520 Returns:
1521 vec3 lookat point
1522
1523 Example:
1524 >>> lookat = radiation.getCameraLookat("cam1")
1525 >>> print(f"Camera looking at: {lookat}")
1526 """
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])
1532
1533 @require_plugin('radiation', 'manage camera orientation')
1534 def setCameraOrientation(self, camera_label: str, direction):
1535 """
1536 Set camera orientation.
1537
1538 Args:
1539 camera_label: Camera label string
1540 direction: View direction as vec3, SphericalCoord, or list [x, y, z]
1541
1542 Example:
1543 >>> radiation.setCameraOrientation("cam1", [0, 0, 1])
1544 >>> from pyhelios.types import SphericalCoord
1545 >>> radiation.setCameraOrientation("cam1", SphericalCoord(1.0, 45.0, 90.0))
1546 """
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")
1551
1552 @require_plugin('radiation', 'query camera orientation')
1553 def getCameraOrientation(self, camera_label: str):
1554 """
1555 Get camera orientation.
1556
1557 Args:
1558 camera_label: Camera label string
1559
1560 Returns:
1561 SphericalCoord orientation [radius, elevation, azimuth]
1562
1563 Example:
1564 >>> orientation = radiation.getCameraOrientation("cam1")
1565 >>> print(f"Camera orientation: {orientation}")
1566 """
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])
1572
1573 @require_plugin('radiation', 'query cameras')
1574 def getAllCameraLabels(self) -> List[str]:
1575 """
1576 Get all camera labels.
1577
1578 Returns:
1579 List of all camera label strings
1580
1581 Example:
1582 >>> cameras = radiation.getAllCameraLabels()
1583 >>> print(f"Available cameras: {cameras}")
1584 """
1585 return radiation_wrapper.getAllCameraLabels(self.radiation_model)
1587 @require_plugin('radiation', 'configure camera spectral response')
1588 def setCameraSpectralResponse(self, camera_label: str, band_label: str, global_data: str):
1589 """
1590 Set camera spectral response from global data.
1591
1592 Args:
1593 camera_label: Camera label
1594 band_label: Band label
1595 global_data: Global data label for spectral response curve
1596
1597 Example:
1598 >>> radiation.setCameraSpectralResponse("cam1", "red", "sensor_red_response")
1599 """
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")
1605
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}'")
1608
1609 @require_plugin('radiation', 'configure camera from library')
1610 def setCameraSpectralResponseFromLibrary(self, camera_label: str, camera_library_name: str):
1611 """
1612 Set camera spectral response from standard camera library.
1613
1614 Uses pre-defined spectral response curves for common cameras.
1615
1616 Args:
1617 camera_label: Camera label
1618 camera_library_name: Standard camera name (e.g., "iPhone13", "NikonD850", "CanonEOS5D")
1619
1620 Example:
1621 >>> radiation.setCameraSpectralResponseFromLibrary("cam1", "iPhone13")
1622 """
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")
1627
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}")
1630
1631 @require_plugin('radiation', 'get camera pixel data')
1632 def getCameraPixelData(self, camera_label: str, band_label: str) -> List[float]:
1633 """
1634 Get camera pixel data for specific band.
1635
1636 Retrieves raw pixel values for programmatic access and analysis.
1637
1638 Args:
1639 camera_label: Camera label
1640 band_label: Band label
1641
1642 Returns:
1643 List of pixel values
1644
1645 Example:
1646 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1647 >>> print(f"Mean pixel value: {sum(pixels)/len(pixels)}")
1648 """
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")
1652
1653 return radiation_wrapper.getCameraPixelData(self.radiation_model, camera_label, band_label)
1654
1655 @require_plugin('radiation', 'set camera pixel data')
1656 def setCameraPixelData(self, camera_label: str, band_label: str, pixel_data: List[float]):
1657 """
1658 Set camera pixel data for specific band.
1659
1660 Allows programmatic modification of pixel values.
1661
1662 Args:
1663 camera_label: Camera label
1664 band_label: Band label
1665 pixel_data: List of pixel values
1666
1667 Example:
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)
1671 """
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")
1677
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")
1680
1681 # =========================================================================
1682 # Camera Library Functions (v1.3.58+)
1683 # =========================================================================
1684
1685 @require_plugin('radiation', 'add camera from library')
1686 def addRadiationCameraFromLibrary(self, camera_label: str, library_camera_label: str,
1687 position, lookat, antialiasing_samples: int = 1,
1688 band_labels: Optional[List[str]] = None):
1689 """
1690 Add radiation camera loading all properties from camera library.
1691
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.
1695
1696 Args:
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.
1703
1704 Raises:
1705 RadiationModelError: If operation fails
1706 ValueError: If parameters are invalid
1707
1708 Note:
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
1713
1714 Example:
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
1721 ... )
1722 """
1723 validate_band_label(camera_label, "camera_label", "addRadiationCameraFromLibrary")
1724
1725 try:
1726 radiation_wrapper.addRadiationCameraFromLibrary(
1727 self.radiation_model, camera_label, library_camera_label,
1728 position, lookat, antialiasing_samples, band_labels
1729 )
1730 logger.info(f"Added camera '{camera_label}' from library '{library_camera_label}'")
1731 except Exception as e:
1732 raise RadiationModelError(f"Failed to add camera from library: {e}")
1733
1734 @require_plugin('radiation', 'update camera parameters')
1735 def updateCameraParameters(self, camera_label: str, camera_properties: CameraProperties):
1736 """
1737 Update camera parameters for an existing camera.
1738
1739 Allows modification of camera properties after creation while preserving
1740 position, lookat direction, and spectral band configuration.
1741
1742 Args:
1743 camera_label: Label for the camera to update
1744 camera_properties: CameraProperties instance with new parameters
1745
1746 Raises:
1747 RadiationModelError: If operation fails or camera doesn't exist
1748 ValueError: If parameters are invalid
1749
1750 Note:
1751 FOV_aspect_ratio is automatically recalculated from camera_resolution.
1752 Camera position and lookat are preserved.
1753
1754 Example:
1755 >>> props = CameraProperties(
1756 ... camera_resolution=(1920, 1080),
1757 ... HFOV=35.0,
1758 ... lens_focal_length=0.085 # 85mm lens
1759 ... )
1760 >>> radiation.updateCameraParameters("cam1", props)
1761 """
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")
1766
1767 try:
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:
1771 raise RadiationModelError(f"Failed to update camera parameters: {e}")
1772
1773 @require_plugin('radiation', 'enable camera metadata')
1774 def enableCameraMetadata(self, camera_labels):
1775 """
1776 Enable automatic JSON metadata file writing for camera(s).
1777
1778 When enabled, writeCameraImage() automatically creates a JSON metadata file
1779 alongside the image containing comprehensive camera and scene information.
1780
1781 Args:
1782 camera_labels: Single camera label (str) or list of camera labels (List[str])
1783
1784 Raises:
1785 RadiationModelError: If operation fails
1786 ValueError: If parameters are invalid
1787
1788 Note:
1789 Metadata includes:
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)
1794
1795 Example:
1796 >>> # Enable for single camera
1797 >>> radiation.enableCameraMetadata("cam1")
1798 >>>
1799 >>> # Enable for multiple cameras
1800 >>> radiation.enableCameraMetadata(["cam1", "cam2", "cam3"])
1801 """
1802 try:
1803 radiation_wrapper.enableCameraMetadata(self.radiation_model, camera_labels)
1804 if isinstance(camera_labels, str):
1805 logger.info(f"Enabled metadata for camera '{camera_labels}'")
1806 else:
1807 logger.info(f"Enabled metadata for {len(camera_labels)} cameras")
1808 except Exception as e:
1809 raise RadiationModelError(f"Failed to enable camera metadata: {e}")
1810
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:
1815 """
1816 Write camera image to file and return output filename.
1817
1818 Args:
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
1825
1826 Returns:
1827 Output filename string
1828
1829 Raises:
1830 RadiationModelError: If camera image writing fails
1831 TypeError: If parameters have incorrect types
1832 """
1833 # Validate inputs
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")
1848
1849 filename = radiation_wrapper.writeCameraImage(
1850 self.radiation_model, camera, bands, imagefile_base,
1851 image_path, frame, flux_to_pixel_conversion)
1852
1853 logger.info(f"Camera image written to: {filename}")
1854 return filename
1855
1856 @require_plugin('radiation', 'write normalized camera images')
1857 def writeNormCameraImage(self, camera: str, bands: List[str], imagefile_base: str,
1858 image_path: str = "./", frame: int = -1) -> str:
1859 """
1860 Write normalized camera image to file and return output filename.
1861
1862 Args:
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)
1868
1869 Returns:
1870 Output filename string
1871
1872 Raises:
1873 RadiationModelError: If normalized camera image writing fails
1874 TypeError: If parameters have incorrect types
1875 """
1876 # Validate inputs
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")
1889
1890 filename = radiation_wrapper.writeNormCameraImage(
1891 self.radiation_model, camera, bands, imagefile_base, image_path, frame)
1892
1893 logger.info(f"Normalized camera image written to: {filename}")
1894 return filename
1895
1896 @require_plugin('radiation', 'write camera image data')
1897 def writeCameraImageData(self, camera: str, band: str, imagefile_base: str,
1898 image_path: str = "./", frame: int = -1):
1899 """
1900 Write camera image data to file (ASCII format).
1901
1902 Args:
1903 camera: Camera label
1904 band: Band 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)
1908
1909 Raises:
1910 RadiationModelError: If camera image data writing fails
1911 TypeError: If parameters have incorrect types
1912 """
1913 # Validate inputs
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")
1924
1925 radiation_wrapper.writeCameraImageData(
1926 self.radiation_model, camera, band, imagefile_base, image_path, frame)
1927
1928 logger.info(f"Camera image data written for camera {camera}, band {band}")
1929
1930 @require_plugin('radiation', 'write image bounding boxes')
1931 def writeImageBoundingBoxes(self, camera_label: str,
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 = "./"):
1936 """
1937 Write image bounding boxes for object detection training.
1938
1939 Supports both single and multiple data labels. Either provide primitive_data_labels
1940 or object_data_labels, not both.
1941
1942 Args:
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)
1950
1951 Raises:
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
1955 """
1956 # Validate exclusive parameter usage
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")
1961
1962 # Validate common parameters
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")
1971
1972 # Handle primitive data labels
1973 if primitive_data_labels is not None:
1974 if isinstance(primitive_data_labels, str):
1975 # Single label
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(
1979 self.radiation_model, camera_label, primitive_data_labels,
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}")
1982
1983 elif isinstance(primitive_data_labels, list):
1984 # Multiple labels
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")
1993
1994 radiation_wrapper.writeImageBoundingBoxesVector(
1995 self.radiation_model, camera_label, primitive_data_labels,
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")
1998 else:
1999 raise TypeError("primitive_data_labels must be a string or list of strings")
2000
2001 # Handle object data labels
2002 elif object_data_labels is not None:
2003 if isinstance(object_data_labels, str):
2004 # Single label
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(
2008 self.radiation_model, camera_label, object_data_labels,
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}")
2011
2012 elif isinstance(object_data_labels, list):
2013 # Multiple labels
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")
2022
2023 radiation_wrapper.writeImageBoundingBoxes_ObjectDataVector(
2024 self.radiation_model, camera_label, object_data_labels,
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")
2027 else:
2028 raise TypeError("object_data_labels must be a string or list of strings")
2029
2030 @require_plugin('radiation', 'write image segmentation masks')
2031 def writeImageSegmentationMasks(self, camera_label: str,
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):
2035 """
2036 Write image segmentation masks in COCO JSON format.
2037
2038 Supports both single and multiple data labels. Either provide primitive_data_labels
2039 or object_data_labels, not both.
2040
2041 Args:
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
2049
2050 Raises:
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
2054 """
2055 # Validate exclusive parameter usage
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")
2060
2061 # Validate common parameters
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")
2070
2071 # Handle primitive data labels
2072 if primitive_data_labels is not None:
2073 if isinstance(primitive_data_labels, str):
2074 # Single label
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(
2078 self.radiation_model, camera_label, primitive_data_labels,
2079 object_class_ids, json_filename, image_file, append_file)
2080 logger.info(f"Image segmentation masks written for primitive data: {primitive_data_labels}")
2081
2082 elif isinstance(primitive_data_labels, list):
2083 # Multiple labels
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")
2092
2093 radiation_wrapper.writeImageSegmentationMasksVector(
2094 self.radiation_model, camera_label, primitive_data_labels,
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")
2097 else:
2098 raise TypeError("primitive_data_labels must be a string or list of strings")
2099
2100 # Handle object data labels
2101 elif object_data_labels is not None:
2102 if isinstance(object_data_labels, str):
2103 # Single label
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(
2107 self.radiation_model, camera_label, object_data_labels,
2108 object_class_ids, json_filename, image_file, append_file)
2109 logger.info(f"Image segmentation masks written for object data: {object_data_labels}")
2110
2111 elif isinstance(object_data_labels, list):
2112 # Multiple labels
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")
2121
2122 radiation_wrapper.writeImageSegmentationMasks_ObjectDataVector(
2123 self.radiation_model, camera_label, object_data_labels,
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")
2126 else:
2127 raise TypeError("object_data_labels must be a string or list of strings")
2128
2129 @require_plugin('radiation', 'auto-calibrate camera image')
2130 def autoCalibrateCameraImage(self, camera_label: str, red_band_label: str,
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:
2135 """
2136 Auto-calibrate camera image with color correction and return output filename.
2137
2138 Args:
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)
2147
2148 Returns:
2149 Output filename string
2150
2151 Raises:
2152 RadiationModelError: If auto-calibration fails
2153 TypeError: If parameters have incorrect types
2154 ValueError: If algorithm is not valid
2155 """
2156 # Validate inputs
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")
2171
2172 # Map algorithm string to integer (using MATRIX_3X3_AUTO = 1 as default)
2173 algorithm_map = {
2174 "DIAGONAL_ONLY": 0,
2175 "MATRIX_3X3_AUTO": 1,
2176 "MATRIX_3X3_FORCE": 2
2177 }
2178
2179 if algorithm not in algorithm_map:
2180 raise ValueError(f"Invalid algorithm: {algorithm}. Must be one of: {list(algorithm_map.keys())}")
2181
2182 algorithm_int = algorithm_map[algorithm]
2183
2184 filename = radiation_wrapper.autoCalibrateCameraImage(
2185 self.radiation_model, camera_label, red_band_label, green_band_label,
2186 blue_band_label, output_file_path, print_quality_report,
2187 algorithm_int, ccm_export_file_path)
2188
2189 logger.info(f"Auto-calibrated camera image written to: {filename}")
2190 return filename
2191
2192 def getPluginInfo(self) -> dict:
2193 """Get information about the radiation plugin."""
2194 registry = get_plugin_registry()
2195 return registry.get_plugin_capabilities('radiation')
__init__(self, date="", time="", UTC_offset=0.0, camera_height_m=0.0, camera_angle_deg=0.0, light_source="sunlight")
Agronomic properties derived from plant architecture data.
__init__(self, plant_species=None, plant_count=None, plant_height_m=None, plant_age_days=None, plant_stage=None, leaf_area_m2=None, weed_pressure="")
Camera intrinsic properties for metadata export.
__init__(self, height=512, width=512, channels=3, type="rgb", focal_length=50.0, aperture="f/2.8", sensor_width=35.0, sensor_height=24.0, model="generic", lens_make="", lens_model="", lens_specification="", exposure="auto", shutter_speed=0.008, white_balance="auto")
Image processing corrections applied to the image.
__init__(self, saturation_adjustment=1.0, brightness_adjustment=1.0, contrast_adjustment=1.0, color_space="linear")
Metadata for radiation camera image export (Helios v1.3.58+).
__init__(self, path="")
Initialize CameraMetadata with default values.
Camera properties for radiation model cameras.
__init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05, HFOV=20.0, FOV_aspect_ratio=0.0, lens_focal_length=0.05, sensor_width_mm=35.0, model="generic", lens_make="", lens_model="", lens_specification="", exposure="auto", shutter_speed=1.0/125.0, white_balance="auto", 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...