2High-level RadiationModel interface for PyHelios.
4This module provides a user-friendly interface to the radiation modeling
5capabilities with graceful plugin handling and informative error messages.
9from typing
import List, Optional
10from contextlib
import contextmanager
11from pathlib
import Path
14from .plugins.registry
import get_plugin_registry, require_plugin, graceful_plugin_fallback
15from .wrappers
import URadiationModelWrapper
as radiation_wrapper
16from .validation.plugins
import (
17 validate_wavelength_range, validate_flux_value, validate_ray_count,
18 validate_direction_vector, validate_band_label, validate_source_id, validate_source_id_list
20from .validation.plugin_decorators
import (
21 validate_radiation_band_params, validate_collimated_source_params, validate_sphere_source_params,
22 validate_sun_sphere_params, validate_get_source_flux_params,
23 validate_update_geometry_params, validate_run_band_params, validate_scattering_depth_params,
24 validate_min_scatter_energy_params
26from .Context
import Context
27from .assets
import get_asset_manager
29logger = logging.getLogger(__name__)
35 Context manager that temporarily changes working directory to where RadiationModel assets are located.
37 RadiationModel C++ code uses hardcoded relative paths like "plugins/radiation/cuda_compile_ptx_generated_rayGeneration.cu.ptx"
38 expecting assets relative to working directory. This manager temporarily changes to the build directory
39 where assets are actually located.
42 RuntimeError: If build directory or RadiationModel assets are not found, indicating a build system error.
46 asset_manager = get_asset_manager()
47 working_dir = asset_manager._get_helios_build_path()
49 if working_dir
and working_dir.exists():
50 radiation_assets = working_dir /
'plugins' /
'radiation'
53 current_dir = Path(__file__).parent
54 packaged_build = current_dir /
'assets' /
'build'
56 if packaged_build.exists():
57 working_dir = packaged_build
58 radiation_assets = working_dir /
'plugins' /
'radiation'
61 repo_root = current_dir.parent
62 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
63 working_dir = build_lib_dir.parent
64 radiation_assets = working_dir /
'plugins' /
'radiation'
66 if not build_lib_dir.exists():
68 f
"PyHelios build directory not found at {build_lib_dir}. "
69 f
"Run: python build_scripts/build_helios.py --plugins radiation"
72 if not radiation_assets.exists():
74 f
"RadiationModel assets not found at {radiation_assets}. "
75 f
"This indicates a build system error. The build script should copy PTX files to this location."
79 original_dir = os.getcwd()
82 logger.debug(f
"Changed working directory to {working_dir} for RadiationModel asset access")
85 os.chdir(original_dir)
86 logger.debug(f
"Restored working directory to {original_dir}")
90 """Raised when RadiationModel operations fail."""
96 Camera properties for radiation model cameras.
98 This class encapsulates the properties needed to configure a radiation camera,
99 providing sensible defaults and validation for camera parameters.
102 def __init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05,
103 HFOV=20.0, FOV_aspect_ratio=1.0):
105 Initialize camera properties with defaults matching C++ CameraProperties.
108 camera_resolution: Camera resolution as (width, height) tuple or list. Default: (512, 512)
109 focal_plane_distance: Distance from viewing plane to focal plane. Default: 1.0
110 lens_diameter: Diameter of camera lens (0 = pinhole camera). Default: 0.05
111 HFOV: Horizontal field of view in degrees. Default: 20.0
112 FOV_aspect_ratio: Ratio of horizontal to vertical field of view. Default: 1.0
115 if camera_resolution
is None:
118 if isinstance(camera_resolution, (list, tuple))
and len(camera_resolution) == 2:
121 raise ValueError(
"camera_resolution must be a tuple or list of 2 integers")
124 if focal_plane_distance <= 0:
125 raise ValueError(
"focal_plane_distance must be greater than 0")
126 if lens_diameter < 0:
127 raise ValueError(
"lens_diameter must be non-negative")
128 if HFOV <= 0
or HFOV > 180:
129 raise ValueError(
"HFOV must be between 0 and 180 degrees")
130 if FOV_aspect_ratio <= 0:
131 raise ValueError(
"FOV_aspect_ratio must be greater than 0")
140 Convert to array format expected by C++ interface.
143 List of 6 float values: [resolution_x, resolution_y, focal_distance, lens_diameter, HFOV, FOV_aspect_ratio]
155 return (f
"CameraProperties(camera_resolution={self.camera_resolution}, "
156 f
"focal_plane_distance={self.focal_plane_distance}, "
157 f
"lens_diameter={self.lens_diameter}, "
158 f
"HFOV={self.HFOV}, "
159 f
"FOV_aspect_ratio={self.FOV_aspect_ratio})")
164 High-level interface for radiation modeling and ray tracing.
166 This class provides a user-friendly wrapper around the native Helios
167 radiation plugin with automatic plugin availability checking and
168 graceful error handling.
173 Initialize RadiationModel with graceful plugin handling.
176 context: Helios Context instance
179 TypeError: If context is not a Context instance
180 RadiationModelError: If radiation plugin is not available
183 if not isinstance(context, Context):
184 raise TypeError(f
"RadiationModel requires a Context instance, got {type(context).__name__}")
190 registry = get_plugin_registry()
192 if not registry.is_plugin_available(
'radiation'):
194 plugin_info = registry.get_plugin_capabilities()
195 available_plugins = registry.get_available_plugins()
198 "RadiationModel requires the 'radiation' plugin which is not available.\n\n"
199 "The radiation plugin provides GPU-accelerated ray tracing using OptiX.\n"
200 "System requirements:\n"
201 "- NVIDIA GPU with CUDA support\n"
202 "- CUDA Toolkit installed\n"
203 "- OptiX runtime (bundled with PyHelios)\n\n"
204 "To enable radiation modeling:\n"
205 "1. Build PyHelios with radiation plugin:\n"
206 " build_scripts/build_helios --plugins radiation\n"
207 "2. Or build with multiple plugins:\n"
208 " build_scripts/build_helios --plugins radiation,visualizer,weberpenntree\n"
209 f
"\nCurrently available plugins: {available_plugins}"
213 alternatives = registry.suggest_alternatives(
'radiation')
215 error_msg += f
"\n\nAlternative plugins available: {alternatives}"
216 error_msg +=
"\nConsider using energybalance or leafoptics for thermal modeling."
223 self.
radiation_model = radiation_wrapper.createRadiationModel(context.getNativePtr())
226 "Failed to create RadiationModel instance. "
227 "This may indicate a problem with the native library or GPU initialization."
229 logger.info(
"RadiationModel created successfully")
231 except Exception
as e:
235 """Context manager entry."""
238 def __exit__(self, exc_type, exc_value, traceback):
239 """Context manager exit with proper cleanup."""
243 logger.debug(
"RadiationModel destroyed successfully")
244 except Exception
as e:
245 logger.warning(f
"Error destroying RadiationModel: {e}")
248 """Get native pointer for advanced operations."""
252 """Get native pointer for advanced operations. (Legacy naming for compatibility)"""
255 @require_plugin('radiation', 'disable status messages')
257 """Disable RadiationModel status messages."""
260 @require_plugin('radiation', 'enable status messages')
262 """Enable RadiationModel status messages."""
265 @require_plugin('radiation', 'add radiation band')
266 def addRadiationBand(self, band_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
268 Add radiation band with optional wavelength bounds.
271 band_label: Name/label for the radiation band
272 wavelength_min: Optional minimum wavelength (μm)
273 wavelength_max: Optional maximum wavelength (μm)
276 validate_band_label(band_label,
"band_label",
"addRadiationBand")
277 if wavelength_min
is not None and wavelength_max
is not None:
278 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"addRadiationBand")
279 radiation_wrapper.addRadiationBandWithWavelengths(self.
radiation_model, band_label, wavelength_min, wavelength_max)
280 logger.debug(f
"Added radiation band {band_label}: {wavelength_min}-{wavelength_max} μm")
283 logger.debug(f
"Added radiation band: {band_label}")
285 @require_plugin('radiation', 'copy radiation band')
286 @validate_radiation_band_params
289 Copy existing radiation band to new label.
292 old_label: Existing band label to copy
293 new_label: New label for the copied band
295 radiation_wrapper.copyRadiationBand(self.
radiation_model, old_label, new_label)
296 logger.debug(f
"Copied radiation band {old_label} to {new_label}")
298 @require_plugin('radiation', 'add radiation source')
299 @validate_collimated_source_params
302 Add collimated radiation source.
305 direction: Optional direction vector. Can be tuple (x, y, z), vec3, or None for default direction.
310 if direction
is None:
311 source_id = radiation_wrapper.addCollimatedRadiationSourceDefault(self.
radiation_model)
314 if hasattr(direction,
'x')
and hasattr(direction,
'y')
and hasattr(direction,
'z'):
316 x, y, z = direction.x, direction.y, direction.z
317 elif hasattr(direction,
'radius')
and hasattr(direction,
'elevation')
and hasattr(direction,
'azimuth'):
321 elevation = direction.elevation
322 azimuth = direction.azimuth
323 x = r * math.cos(elevation) * math.cos(azimuth)
324 y = r * math.cos(elevation) * math.sin(azimuth)
325 z = r * math.sin(elevation)
329 if len(direction) != 3:
330 raise TypeError(f
"Direction must be a 3-element tuple, vec3, or SphericalCoord, got {type(direction).__name__} with {len(direction)} elements")
332 except (TypeError, AttributeError):
334 raise TypeError(f
"Direction must be a tuple, vec3, or SphericalCoord, got {type(direction).__name__}")
335 source_id = radiation_wrapper.addCollimatedRadiationSourceVec3(self.
radiation_model, x, y, z)
337 logger.debug(f
"Added collimated radiation source: ID {source_id}")
340 @require_plugin('radiation', 'add spherical radiation source')
341 @validate_sphere_source_params
344 Add spherical radiation source.
347 position: Position of the source. Can be tuple (x, y, z) or vec3.
348 radius: Radius of the spherical source
354 if hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z'):
356 x, y, z = position.x, position.y, position.z
360 source_id = radiation_wrapper.addSphereRadiationSource(self.
radiation_model, x, y, z, radius)
361 logger.debug(f
"Added sphere radiation source: ID {source_id} at ({x}, {y}, {z}) with radius {radius}")
364 @require_plugin('radiation', 'add sun radiation source')
365 @validate_sun_sphere_params
367 position_scaling: float = 1.0, angular_width: float = 0.53,
368 flux_scaling: float = 1.0) -> int:
370 Add sun sphere radiation source.
373 radius: Radius of the sun sphere
374 zenith: Zenith angle (degrees)
375 azimuth: Azimuth angle (degrees)
376 position_scaling: Position scaling factor
377 angular_width: Angular width of the sun (degrees)
378 flux_scaling: Flux scaling factor
383 source_id = radiation_wrapper.addSunSphereRadiationSource(
384 self.
radiation_model, radius, zenith, azimuth, position_scaling, angular_width, flux_scaling
386 logger.debug(f
"Added sun radiation source: ID {source_id}")
389 @require_plugin('radiation', 'set ray count')
391 """Set direct ray count for radiation band."""
392 validate_band_label(band_label,
"band_label",
"setDirectRayCount")
393 validate_ray_count(ray_count,
"ray_count",
"setDirectRayCount")
394 radiation_wrapper.setDirectRayCount(self.
radiation_model, band_label, ray_count)
396 @require_plugin('radiation', 'set ray count')
398 """Set diffuse ray count for radiation band."""
399 validate_band_label(band_label,
"band_label",
"setDiffuseRayCount")
400 validate_ray_count(ray_count,
"ray_count",
"setDiffuseRayCount")
401 radiation_wrapper.setDiffuseRayCount(self.
radiation_model, band_label, ray_count)
403 @require_plugin('radiation', 'set radiation flux')
405 """Set diffuse radiation flux for band."""
406 validate_band_label(label,
"label",
"setDiffuseRadiationFlux")
407 validate_flux_value(flux,
"flux",
"setDiffuseRadiationFlux")
408 radiation_wrapper.setDiffuseRadiationFlux(self.
radiation_model, label, flux)
410 @require_plugin('radiation', 'set source flux')
412 """Set source flux for single source or multiple sources."""
413 validate_band_label(label,
"label",
"setSourceFlux")
414 validate_flux_value(flux,
"flux",
"setSourceFlux")
416 if isinstance(source_id, (list, tuple)):
418 validate_source_id_list(list(source_id),
"source_id",
"setSourceFlux")
419 radiation_wrapper.setSourceFluxMultiple(self.
radiation_model, source_id, label, flux)
422 validate_source_id(source_id,
"source_id",
"setSourceFlux")
423 radiation_wrapper.setSourceFlux(self.
radiation_model, source_id, label, flux)
426 @require_plugin('radiation', 'get source flux')
427 @validate_get_source_flux_params
428 def getSourceFlux(self, source_id: int, label: str) -> float:
429 """Get source flux for band."""
430 return radiation_wrapper.getSourceFlux(self.
radiation_model, source_id, label)
432 @require_plugin('radiation', 'update geometry')
433 @validate_update_geometry_params
436 Update geometry in radiation model.
439 uuids: Optional list of specific UUIDs to update. If None, updates all geometry.
443 logger.debug(
"Updated all geometry in radiation model")
446 logger.debug(f
"Updated {len(uuids)} geometry UUIDs in radiation model")
448 @require_plugin('radiation', 'run radiation simulation')
449 @validate_run_band_params
452 Run radiation simulation for single band or multiple bands.
454 PERFORMANCE NOTE: When simulating multiple radiation bands, it is HIGHLY RECOMMENDED
455 to run all bands in a single call (e.g., runBand(["PAR", "NIR", "SW"])) rather than
456 sequential single-band calls. This provides significant computational efficiency gains
459 - GPU ray tracing setup is done once for all bands
460 - Scene geometry acceleration structures are reused
461 - OptiX kernel launches are batched together
462 - Memory transfers between CPU/GPU are minimized
465 # EFFICIENT - Single call for multiple bands
466 radiation.runBand(["PAR", "NIR", "SW"])
468 # INEFFICIENT - Sequential single-band calls
469 radiation.runBand("PAR")
470 radiation.runBand("NIR")
471 radiation.runBand("SW")
474 band_label: Single band name (str) or list of band names for multi-band simulation
476 if isinstance(band_label, (list, tuple)):
478 for lbl
in band_label:
479 if not isinstance(lbl, str):
480 raise TypeError(f
"Band labels must be strings, got {type(lbl).__name__}")
482 logger.info(f
"Completed radiation simulation for bands: {band_label}")
485 if not isinstance(band_label, str):
486 raise TypeError(f
"Band label must be a string, got {type(band_label).__name__}")
488 logger.info(f
"Completed radiation simulation for band: {band_label}")
491 @require_plugin('radiation', 'get simulation results')
493 """Get total absorbed flux for all primitives."""
495 logger.debug(f
"Retrieved absorbed flux data for {len(results)} primitives")
499 @require_plugin('radiation', 'configure radiation simulation')
500 @validate_scattering_depth_params
502 """Set scattering depth for radiation band."""
503 radiation_wrapper.setScatteringDepth(self.
radiation_model, label, depth)
505 @require_plugin('radiation', 'configure radiation simulation')
506 @validate_min_scatter_energy_params
508 """Set minimum scatter energy for radiation band."""
509 radiation_wrapper.setMinScatterEnergy(self.
radiation_model, label, energy)
511 @require_plugin('radiation', 'configure radiation emission')
513 """Disable emission for radiation band."""
514 validate_band_label(label,
"label",
"disableEmission")
517 @require_plugin('radiation', 'configure radiation emission')
519 """Enable emission for radiation band."""
520 validate_band_label(label,
"label",
"enableEmission")
527 @require_plugin('radiation', 'add radiation camera')
528 def addRadiationCamera(self, camera_label: str, band_labels: List[str], position, lookat_or_direction,
529 camera_properties=
None, antialiasing_samples: int = 100):
531 Add a radiation camera to the simulation.
534 camera_label: Unique label string for the camera
535 band_labels: List of radiation band labels for the camera
536 position: Camera position as vec3 object
537 lookat_or_direction: Either:
538 - Lookat point as vec3 object
539 - SphericalCoord for viewing direction
540 camera_properties: CameraProperties instance or None for defaults
541 antialiasing_samples: Number of antialiasing samples (default: 100)
544 ValidationError: If parameters are invalid or have wrong types
545 RadiationModelError: If camera creation fails
548 >>> from pyhelios import vec3, CameraProperties
549 >>> # Create camera looking at origin from above
550 >>> camera_props = CameraProperties(camera_resolution=(1024, 1024))
551 >>> radiation_model.addRadiationCamera("main_camera", ["red", "green", "blue"],
552 ... position=vec3(0, 0, 5), lookat_or_direction=vec3(0, 0, 0),
553 ... camera_properties=camera_props)
556 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
557 from .wrappers.DataTypes
import SphericalCoord, vec3, make_vec3
558 from .validation.plugins
import validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
561 validated_label = validate_camera_label(camera_label,
"camera_label",
"addRadiationCamera")
562 validated_bands = validate_band_labels_list(band_labels,
"band_labels",
"addRadiationCamera")
563 validated_samples = validate_antialiasing_samples(antialiasing_samples,
"antialiasing_samples",
"addRadiationCamera")
566 if not (hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z')):
567 raise TypeError(
"position must be a vec3 object. Use vec3(x, y, z) to create one.")
568 validated_position = position
571 if hasattr(lookat_or_direction,
'radius')
and hasattr(lookat_or_direction,
'elevation'):
572 validated_direction = lookat_or_direction
573 elif hasattr(lookat_or_direction,
'x')
and hasattr(lookat_or_direction,
'y')
and hasattr(lookat_or_direction,
'z'):
574 validated_direction = lookat_or_direction
576 raise TypeError(
"lookat_or_direction must be a vec3 or SphericalCoord object. Use vec3(x, y, z) or SphericalCoord to create one.")
579 if camera_properties
is None:
584 if hasattr(validated_direction,
'radius')
and hasattr(validated_direction,
'elevation'):
586 direction_coords = validated_direction.to_list()
587 if len(direction_coords) >= 3:
589 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[2]
591 raise ValueError(
"SphericalCoord must have at least radius, elevation, and azimuth")
593 radiation_wrapper.addRadiationCameraSpherical(
597 validated_position.x, validated_position.y, validated_position.z,
598 radius, elevation, azimuth,
599 camera_properties.to_array(),
604 radiation_wrapper.addRadiationCameraVec3(
608 validated_position.x, validated_position.y, validated_position.z,
609 validated_direction.x, validated_direction.y, validated_direction.z,
610 camera_properties.to_array(),
614 except Exception
as e:
617 @require_plugin('radiation', 'write camera images')
618 def writeCameraImage(self, camera: str, bands: List[str], imagefile_base: str,
619 image_path: str =
"./", frame: int = -1,
620 flux_to_pixel_conversion: float = 1.0) -> str:
622 Write camera image to file and return output filename.
626 bands: List of band labels to include in the image
627 imagefile_base: Base filename for output
628 image_path: Output directory path (default: current directory)
629 frame: Frame number to write (-1 for all frames)
630 flux_to_pixel_conversion: Conversion factor from flux to pixel values
633 Output filename string
636 RadiationModelError: If camera image writing fails
637 TypeError: If parameters have incorrect types
640 if not isinstance(camera, str)
or not camera.strip():
641 raise TypeError(
"Camera label must be a non-empty string")
642 if not isinstance(bands, list)
or not bands:
643 raise TypeError(
"Bands must be a non-empty list of strings")
644 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
645 raise TypeError(
"All band labels must be non-empty strings")
646 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
647 raise TypeError(
"Image file base must be a non-empty string")
648 if not isinstance(image_path, str):
649 raise TypeError(
"Image path must be a string")
650 if not isinstance(frame, int):
651 raise TypeError(
"Frame must be an integer")
652 if not isinstance(flux_to_pixel_conversion, (int, float))
or flux_to_pixel_conversion <= 0:
653 raise TypeError(
"Flux to pixel conversion must be a positive number")
655 filename = radiation_wrapper.writeCameraImage(
657 image_path, frame, flux_to_pixel_conversion)
659 logger.info(f
"Camera image written to: {filename}")
662 @require_plugin('radiation', 'write normalized camera images')
664 image_path: str =
"./", frame: int = -1) -> str:
666 Write normalized camera image to file and return output filename.
670 bands: List of band labels to include in the image
671 imagefile_base: Base filename for output
672 image_path: Output directory path (default: current directory)
673 frame: Frame number to write (-1 for all frames)
676 Output filename string
679 RadiationModelError: If normalized camera image writing fails
680 TypeError: If parameters have incorrect types
683 if not isinstance(camera, str)
or not camera.strip():
684 raise TypeError(
"Camera label must be a non-empty string")
685 if not isinstance(bands, list)
or not bands:
686 raise TypeError(
"Bands must be a non-empty list of strings")
687 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
688 raise TypeError(
"All band labels must be non-empty strings")
689 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
690 raise TypeError(
"Image file base must be a non-empty string")
691 if not isinstance(image_path, str):
692 raise TypeError(
"Image path must be a string")
693 if not isinstance(frame, int):
694 raise TypeError(
"Frame must be an integer")
696 filename = radiation_wrapper.writeNormCameraImage(
697 self.
radiation_model, camera, bands, imagefile_base, image_path, frame)
699 logger.info(f
"Normalized camera image written to: {filename}")
702 @require_plugin('radiation', 'write camera image data')
704 image_path: str =
"./", frame: int = -1):
706 Write camera image data to file (ASCII format).
711 imagefile_base: Base filename for output
712 image_path: Output directory path (default: current directory)
713 frame: Frame number to write (-1 for all frames)
716 RadiationModelError: If camera image data writing fails
717 TypeError: If parameters have incorrect types
720 if not isinstance(camera, str)
or not camera.strip():
721 raise TypeError(
"Camera label must be a non-empty string")
722 if not isinstance(band, str)
or not band.strip():
723 raise TypeError(
"Band label must be a non-empty string")
724 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
725 raise TypeError(
"Image file base must be a non-empty string")
726 if not isinstance(image_path, str):
727 raise TypeError(
"Image path must be a string")
728 if not isinstance(frame, int):
729 raise TypeError(
"Frame must be an integer")
731 radiation_wrapper.writeCameraImageData(
734 logger.info(f
"Camera image data written for camera {camera}, band {band}")
736 @require_plugin('radiation', 'write image bounding boxes')
738 primitive_data_labels=
None, object_data_labels=
None,
739 object_class_ids=
None, image_file: str =
"",
740 classes_txt_file: str =
"classes.txt",
741 image_path: str =
"./"):
743 Write image bounding boxes for object detection training.
745 Supports both single and multiple data labels. Either provide primitive_data_labels
746 or object_data_labels, not both.
749 camera_label: Camera label
750 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
751 object_data_labels: Single object data label (str) or list of object data labels
752 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
753 image_file: Image filename
754 classes_txt_file: Classes definition file (default: "classes.txt")
755 image_path: Image output path (default: current directory)
758 RadiationModelError: If bounding box writing fails
759 TypeError: If parameters have incorrect types
760 ValueError: If both primitive and object data labels are provided, or neither
763 if primitive_data_labels
is not None and object_data_labels
is not None:
764 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
765 if primitive_data_labels
is None and object_data_labels
is None:
766 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
769 if not isinstance(camera_label, str)
or not camera_label.strip():
770 raise TypeError(
"Camera label must be a non-empty string")
771 if not isinstance(image_file, str)
or not image_file.strip():
772 raise TypeError(
"Image file must be a non-empty string")
773 if not isinstance(classes_txt_file, str):
774 raise TypeError(
"Classes txt file must be a string")
775 if not isinstance(image_path, str):
776 raise TypeError(
"Image path must be a string")
779 if primitive_data_labels
is not None:
780 if isinstance(primitive_data_labels, str):
782 if not isinstance(object_class_ids, int):
783 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
784 radiation_wrapper.writeImageBoundingBoxes(
786 object_class_ids, image_file, classes_txt_file, image_path)
787 logger.info(f
"Image bounding boxes written for primitive data: {primitive_data_labels}")
789 elif isinstance(primitive_data_labels, list):
791 if not isinstance(object_class_ids, list):
792 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
793 if len(primitive_data_labels) != len(object_class_ids):
794 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
795 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
796 raise TypeError(
"All primitive data labels must be non-empty strings")
797 if not all(isinstance(cid, int)
for cid
in object_class_ids):
798 raise TypeError(
"All object class IDs must be integers")
800 radiation_wrapper.writeImageBoundingBoxesVector(
802 object_class_ids, image_file, classes_txt_file, image_path)
803 logger.info(f
"Image bounding boxes written for {len(primitive_data_labels)} primitive data labels")
805 raise TypeError(
"primitive_data_labels must be a string or list of strings")
808 elif object_data_labels
is not None:
809 if isinstance(object_data_labels, str):
811 if not isinstance(object_class_ids, int):
812 raise TypeError(
"For single object data label, object_class_ids must be an integer")
813 radiation_wrapper.writeImageBoundingBoxes_ObjectData(
815 object_class_ids, image_file, classes_txt_file, image_path)
816 logger.info(f
"Image bounding boxes written for object data: {object_data_labels}")
818 elif isinstance(object_data_labels, list):
820 if not isinstance(object_class_ids, list):
821 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
822 if len(object_data_labels) != len(object_class_ids):
823 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
824 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
825 raise TypeError(
"All object data labels must be non-empty strings")
826 if not all(isinstance(cid, int)
for cid
in object_class_ids):
827 raise TypeError(
"All object class IDs must be integers")
829 radiation_wrapper.writeImageBoundingBoxes_ObjectDataVector(
831 object_class_ids, image_file, classes_txt_file, image_path)
832 logger.info(f
"Image bounding boxes written for {len(object_data_labels)} object data labels")
834 raise TypeError(
"object_data_labels must be a string or list of strings")
836 @require_plugin('radiation', 'write image segmentation masks')
838 primitive_data_labels=
None, object_data_labels=
None,
839 object_class_ids=
None, json_filename: str =
"",
840 image_file: str =
"", append_file: bool =
False):
842 Write image segmentation masks in COCO JSON format.
844 Supports both single and multiple data labels. Either provide primitive_data_labels
845 or object_data_labels, not both.
848 camera_label: Camera label
849 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
850 object_data_labels: Single object data label (str) or list of object data labels
851 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
852 json_filename: JSON output filename
853 image_file: Image filename
854 append_file: Whether to append to existing JSON file
857 RadiationModelError: If segmentation mask writing fails
858 TypeError: If parameters have incorrect types
859 ValueError: If both primitive and object data labels are provided, or neither
862 if primitive_data_labels
is not None and object_data_labels
is not None:
863 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
864 if primitive_data_labels
is None and object_data_labels
is None:
865 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
868 if not isinstance(camera_label, str)
or not camera_label.strip():
869 raise TypeError(
"Camera label must be a non-empty string")
870 if not isinstance(json_filename, str)
or not json_filename.strip():
871 raise TypeError(
"JSON filename must be a non-empty string")
872 if not isinstance(image_file, str)
or not image_file.strip():
873 raise TypeError(
"Image file must be a non-empty string")
874 if not isinstance(append_file, bool):
875 raise TypeError(
"append_file must be a boolean")
878 if primitive_data_labels
is not None:
879 if isinstance(primitive_data_labels, str):
881 if not isinstance(object_class_ids, int):
882 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
883 radiation_wrapper.writeImageSegmentationMasks(
885 object_class_ids, json_filename, image_file, append_file)
886 logger.info(f
"Image segmentation masks written for primitive data: {primitive_data_labels}")
888 elif isinstance(primitive_data_labels, list):
890 if not isinstance(object_class_ids, list):
891 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
892 if len(primitive_data_labels) != len(object_class_ids):
893 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
894 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
895 raise TypeError(
"All primitive data labels must be non-empty strings")
896 if not all(isinstance(cid, int)
for cid
in object_class_ids):
897 raise TypeError(
"All object class IDs must be integers")
899 radiation_wrapper.writeImageSegmentationMasksVector(
901 object_class_ids, json_filename, image_file, append_file)
902 logger.info(f
"Image segmentation masks written for {len(primitive_data_labels)} primitive data labels")
904 raise TypeError(
"primitive_data_labels must be a string or list of strings")
907 elif object_data_labels
is not None:
908 if isinstance(object_data_labels, str):
910 if not isinstance(object_class_ids, int):
911 raise TypeError(
"For single object data label, object_class_ids must be an integer")
912 radiation_wrapper.writeImageSegmentationMasks_ObjectData(
914 object_class_ids, json_filename, image_file, append_file)
915 logger.info(f
"Image segmentation masks written for object data: {object_data_labels}")
917 elif isinstance(object_data_labels, list):
919 if not isinstance(object_class_ids, list):
920 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
921 if len(object_data_labels) != len(object_class_ids):
922 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
923 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
924 raise TypeError(
"All object data labels must be non-empty strings")
925 if not all(isinstance(cid, int)
for cid
in object_class_ids):
926 raise TypeError(
"All object class IDs must be integers")
928 radiation_wrapper.writeImageSegmentationMasks_ObjectDataVector(
930 object_class_ids, json_filename, image_file, append_file)
931 logger.info(f
"Image segmentation masks written for {len(object_data_labels)} object data labels")
933 raise TypeError(
"object_data_labels must be a string or list of strings")
935 @require_plugin('radiation', 'auto-calibrate camera image')
937 green_band_label: str, blue_band_label: str,
938 output_file_path: str, print_quality_report: bool =
False,
939 algorithm: str =
"MATRIX_3X3_AUTO",
940 ccm_export_file_path: str =
"") -> str:
942 Auto-calibrate camera image with color correction and return output filename.
945 camera_label: Camera label
946 red_band_label: Red band label
947 green_band_label: Green band label
948 blue_band_label: Blue band label
949 output_file_path: Output file path
950 print_quality_report: Whether to print quality report
951 algorithm: Color correction algorithm ("DIAGONAL_ONLY", "MATRIX_3X3_AUTO", "MATRIX_3X3_FORCE")
952 ccm_export_file_path: Path to export color correction matrix (optional)
955 Output filename string
958 RadiationModelError: If auto-calibration fails
959 TypeError: If parameters have incorrect types
960 ValueError: If algorithm is not valid
963 if not isinstance(camera_label, str)
or not camera_label.strip():
964 raise TypeError(
"Camera label must be a non-empty string")
965 if not isinstance(red_band_label, str)
or not red_band_label.strip():
966 raise TypeError(
"Red band label must be a non-empty string")
967 if not isinstance(green_band_label, str)
or not green_band_label.strip():
968 raise TypeError(
"Green band label must be a non-empty string")
969 if not isinstance(blue_band_label, str)
or not blue_band_label.strip():
970 raise TypeError(
"Blue band label must be a non-empty string")
971 if not isinstance(output_file_path, str)
or not output_file_path.strip():
972 raise TypeError(
"Output file path must be a non-empty string")
973 if not isinstance(print_quality_report, bool):
974 raise TypeError(
"print_quality_report must be a boolean")
975 if not isinstance(ccm_export_file_path, str):
976 raise TypeError(
"ccm_export_file_path must be a string")
981 "MATRIX_3X3_AUTO": 1,
982 "MATRIX_3X3_FORCE": 2
985 if algorithm
not in algorithm_map:
986 raise ValueError(f
"Invalid algorithm: {algorithm}. Must be one of: {list(algorithm_map.keys())}")
988 algorithm_int = algorithm_map[algorithm]
990 filename = radiation_wrapper.autoCalibrateCameraImage(
992 blue_band_label, output_file_path, print_quality_report,
993 algorithm_int, ccm_export_file_path)
995 logger.info(f
"Auto-calibrated camera image written to: {filename}")
999 """Get information about the radiation plugin."""
1000 registry = get_plugin_registry()
1001 return registry.get_plugin_capabilities(
'radiation')
Camera properties for radiation model cameras.
__init__(self, camera_resolution=None, focal_plane_distance=1.0, lens_diameter=0.05, HFOV=20.0, FOV_aspect_ratio=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.
disableMessages(self)
Disable RadiationModel status messages.
runBand(self, band_label)
Run radiation simulation for single band or multiple bands.
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.
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.
copyRadiationBand(self, str old_label, str new_label)
Copy existing radiation band to new label.
enableMessages(self)
Enable RadiationModel status messages.
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.
setDiffuseRayCount(self, str band_label, int ray_count)
Set diffuse ray count for radiation band.
float getSourceFlux(self, int source_id, str label)
Get source flux for band.
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.
disableEmission(self, str label)
Disable emission for radiation band.
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.
__init__(self, Context context)
Initialize RadiationModel with graceful plugin handling.
__enter__(self)
Context manager entry.
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.
setMinScatterEnergy(self, str label, float energy)
Set minimum scatter energy for radiation 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.
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.
getNativePtr(self)
Get native pointer for advanced operations.
_radiation_working_directory()
Context manager that temporarily changes working directory to where RadiationModel assets are located...