413 TypeError: If context is not a Context instance
414 RadiationModelError: If radiation plugin is not available
417 if not isinstance(context, Context):
418 raise TypeError(f
"RadiationModel requires a Context instance, got {type(context).__name__}")
429 registry = get_plugin_registry()
431 if not registry.is_plugin_available(
'radiation'):
433 plugin_info = registry.get_plugin_capabilities()
434 available_plugins = registry.get_available_plugins()
437 "RadiationModel requires the 'radiation' plugin which is not available.\n\n"
438 "The radiation plugin provides GPU-accelerated ray tracing with runtime\n"
439 "backend auto-detection (OptiX 8 -> OptiX 6 -> Vulkan).\n"
440 "System requirements (at least one backend):\n"
441 "- Vulkan: Vulkan loader library (macOS/Linux); no extra packages on Windows\n"
442 "- OptiX 8.1: NVIDIA GPU with driver >= 560 and CUDA 12.0+\n"
443 "- OptiX 6.5: NVIDIA GPU with driver < 560 and CUDA 9.0+\n\n"
444 "To enable radiation modeling:\n"
445 "1. Build PyHelios with radiation plugin:\n"
446 " build_scripts/build_helios --plugins radiation\n"
447 "2. Or build with multiple plugins:\n"
448 " build_scripts/build_helios --plugins radiation,visualizer,weberpenntree\n"
449 f
"\nCurrently available plugins: {available_plugins}"
453 alternatives = registry.suggest_alternatives(
'radiation')
455 error_msg += f
"\n\nAlternative plugins available: {alternatives}"
456 error_msg +=
"\nConsider using energybalance or leafoptics for thermal modeling."
463 self.
radiation_model = radiation_wrapper.createRadiationModel(context.getNativePtr())
466 "Failed to create RadiationModel instance. "
467 "This may indicate a problem with the native library or GPU initialization."
469 logger.info(
"RadiationModel created successfully")
471 except Exception
as e:
475 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
476 check_context_alive(getattr(self,
"context",
None),
"RadiationModel")
479 """Raise an actionable error if a camera/band has no rendered pixel data.
481 A camera's pixel data is populated only by ``runBand()``, and only for
482 the bands passed to that call and for cameras that already existed when
483 it ran. Requesting an image for an unrendered camera/band otherwise
484 reaches the native layer as a bare ``invalid map<K, T> key`` from
485 ``std::map::at`` -- see GitHub issue #4 and the upstream fix landing in
486 helios-core v1.3.79. This preflight turns that into a message naming the
487 camera, the band, and the call the user is missing.
489 Kept after the upstream fix lands: it stays correct (merely redundant)
490 against a fixed core, and users on earlier versions still need it.
493 known_cameras = radiation_wrapper.getAllCameraLabels(self.
radiation_model)
499 if known_cameras
is not None and camera
not in known_cameras:
501 f
"Cannot {operation}: camera '{camera}' does not exist. "
502 f
"Add it with addRadiationCamera() before calling runBand(). "
503 f
"Existing cameras: {sorted(known_cameras) if known_cameras else 'none'}"
508 radiation_wrapper.getCameraPixelData(self.
radiation_model, camera, band)
511 f
"Cannot {operation}: camera '{camera}' has no rendered pixel data "
512 f
"for band '{band}'. Call runBand() with this band after adding the "
513 f
"camera -- e.g. runBand({list(bands)!r}). Note that runBand() only "
514 f
"renders the bands passed to it, and only for cameras that already "
515 f
"exist when it runs."
519 """Context manager entry."""
522 def __exit__(self, exc_type, exc_value, traceback):
523 """Context manager exit with proper cleanup."""
527 logger.debug(
"RadiationModel destroyed successfully")
528 except Exception
as e:
529 logger.warning(f
"Error destroying RadiationModel: {e}")
534 """Destructor to ensure GPU resources freed even without 'with' statement."""
539 except Exception
as e:
541 warnings.warn(f
"Error in RadiationModel.__del__: {e}")
544 """Get native pointer for advanced operations."""
548 """Get native pointer for advanced operations. (Legacy naming for compatibility)"""
551 @require_plugin('radiation', 'disable status messages')
553 """Disable RadiationModel status messages."""
557 @require_plugin('radiation', 'enable status messages')
559 """Enable RadiationModel status messages."""
563 @require_plugin('radiation', 'add radiation band')
564 def addRadiationBand(self, band_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
566 Add radiation band with optional wavelength bounds.
569 band_label: Name/label for the radiation band
570 wavelength_min: Optional minimum wavelength (nm)
571 wavelength_max: Optional maximum wavelength (nm)
574 validate_band_label(band_label,
"band_label",
"addRadiationBand")
575 if wavelength_min
is not None and wavelength_max
is not None:
576 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"addRadiationBand")
578 radiation_wrapper.addRadiationBandWithWavelengths(self.
radiation_model, band_label, wavelength_min, wavelength_max)
579 logger.debug(f
"Added radiation band {band_label}: {wavelength_min}-{wavelength_max} nm")
583 logger.debug(f
"Added radiation band: {band_label}")
585 @require_plugin('radiation', 'copy radiation band')
586 @validate_radiation_band_params
587 def copyRadiationBand(self, old_label: str, new_label: str, wavelength_min: float =
None, wavelength_max: float =
None):
589 Copy existing radiation band to new label, optionally with new wavelength range.
592 old_label: Existing band label to copy
593 new_label: New label for the copied band
594 wavelength_min: Optional minimum wavelength for new band (nm)
595 wavelength_max: Optional maximum wavelength for new band (nm)
598 >>> # Copy band with same wavelength range
599 >>> radiation.copyRadiationBand("SW", "SW_copy")
601 >>> # Copy band with different wavelength range
602 >>> radiation.copyRadiationBand("full_spectrum", "PAR", 400, 700)
604 if wavelength_min
is not None and wavelength_max
is not None:
605 validate_wavelength_range(wavelength_min, wavelength_max,
"wavelength_min",
"wavelength_max",
"copyRadiationBand")
608 radiation_wrapper.copyRadiationBand(self.
radiation_model, old_label, new_label, wavelength_min, wavelength_max)
609 if wavelength_min
is not None:
610 logger.debug(f
"Copied radiation band {old_label} to {new_label} with wavelengths {wavelength_min}-{wavelength_max} nm")
612 logger.debug(f
"Copied radiation band {old_label} to {new_label}")
614 @require_plugin('radiation', 'add radiation source')
615 @validate_collimated_source_params
618 Add collimated radiation source.
621 direction: Optional direction vector. Can be tuple (x, y, z), vec3, or None for default direction.
626 if direction
is None:
628 source_id = radiation_wrapper.addCollimatedRadiationSourceDefault(self.
radiation_model)
631 if hasattr(direction,
'x')
and hasattr(direction,
'y')
and hasattr(direction,
'z'):
633 x, y, z = direction.x, direction.y, direction.z
634 elif hasattr(direction,
'radius')
and hasattr(direction,
'elevation')
and hasattr(direction,
'azimuth'):
638 elevation = direction.elevation
639 azimuth = direction.azimuth
640 x = r * math.cos(elevation) * math.cos(azimuth)
641 y = r * math.cos(elevation) * math.sin(azimuth)
642 z = r * math.sin(elevation)
647 if len(direction) != 3:
648 raise TypeError(f
"Direction must be a 3-element tuple, vec3, or SphericalCoord, got {type(direction).__name__} with {len(direction)} elements")
650 except (TypeError, AttributeError):
652 raise TypeError(f
"Direction must be a tuple, vec3, or SphericalCoord, got {type(direction).__name__}")
653 source_id = radiation_wrapper.addCollimatedRadiationSourceVec3(self.
radiation_model, x, y, z)
655 logger.debug(f
"Added collimated radiation source: ID {source_id}")
658 @require_plugin('radiation', 'add spherical radiation source')
659 @validate_sphere_source_params
662 Add spherical radiation source.
665 position: Position of the source. Can be tuple (x, y, z) or vec3.
666 radius: Radius of the spherical source
671 validate_position_like(position,
"position",
"addSphereRadiationSource")
673 if hasattr(position,
'x')
and hasattr(position,
'y')
and hasattr(position,
'z'):
674 x, y, z = position.x, position.y, position.z
678 source_id = radiation_wrapper.addSphereRadiationSource(self.
radiation_model, x, y, z, radius)
679 logger.debug(f
"Added sphere radiation source: ID {source_id} at ({x}, {y}, {z}) with radius {radius}")
682 @require_plugin('radiation', 'add sun radiation source')
683 @validate_sun_sphere_params
685 position_scaling: float = 1.0, angular_width: float = 0.53,
686 flux_scaling: float = 1.0) -> int:
688 Add sun sphere radiation source.
691 radius: Radius of the sun sphere
692 zenith: Zenith angle (degrees)
693 azimuth: Azimuth angle (degrees)
694 position_scaling: Position scaling factor
695 angular_width: Angular width of the sun (degrees)
696 flux_scaling: Flux scaling factor
702 source_id = radiation_wrapper.addSunSphereRadiationSource(
703 self.
radiation_model, radius, zenith, azimuth, position_scaling, angular_width, flux_scaling
705 logger.debug(f
"Added sun radiation source: ID {source_id}")
708 @require_plugin('radiation', 'set source position')
711 Set position of a radiation source.
713 Allows dynamic repositioning of radiation sources during simulation,
714 useful for time-series modeling or moving light sources.
717 source_id: ID of the radiation source
718 position: New position as vec3, SphericalCoord, or list/tuple [x, y, z]
721 >>> source_id = radiation.addCollimatedRadiationSource()
722 >>> radiation.setSourcePosition(source_id, [10, 20, 30])
723 >>> from pyhelios.types import vec3
724 >>> radiation.setSourcePosition(source_id, vec3(15, 25, 35))
726 if not isinstance(source_id, int)
or source_id < 0:
727 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
728 validate_direction_like(position,
"position",
"setSourcePosition")
730 radiation_wrapper.setSourcePosition(self.
radiation_model, source_id, position)
731 logger.debug(f
"Updated position for radiation source {source_id}")
733 @require_plugin('radiation', 'add rectangle radiation source')
736 Add a rectangle (planar) radiation source.
738 Rectangle sources are ideal for modeling artificial lighting such as
739 LED panels, grow lights, or window light sources.
742 position: Center position as vec3 or list [x, y, z]
743 size: Rectangle dimensions as vec2 or list [width, height]
744 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
750 >>> from pyhelios.types import vec3, vec2
751 >>> source_id = radiation.addRectangleRadiationSource(
752 ... position=vec3(0, 0, 5),
754 ... rotation=vec3(0, 0, 0)
756 >>> radiation.setSourceFlux(source_id, "PAR", 500.0)
758 validate_position_like(position,
"position",
"addRectangleRadiationSource")
759 validate_size_like(size,
"size",
"addRectangleRadiationSource")
760 validate_position_like(rotation,
"rotation",
"addRectangleRadiationSource")
762 return radiation_wrapper.addRectangleRadiationSource(self.
radiation_model, position, size, rotation)
764 @require_plugin('radiation', 'add disk radiation source')
767 Add a disk (circular planar) radiation source.
769 Disk sources are useful for modeling circular light sources such as
770 spotlights, circular LED arrays, or solar simulators.
773 position: Center position as vec3 or list [x, y, z]
775 rotation: Rotation vector as vec3 or list [rx, ry, rz] (Euler angles in radians)
781 >>> from pyhelios.types import vec3
782 >>> source_id = radiation.addDiskRadiationSource(
783 ... position=vec3(0, 0, 5),
785 ... rotation=vec3(0, 0, 0)
787 >>> radiation.setSourceFlux(source_id, "PAR", 300.0)
789 validate_position_like(position,
"position",
"addDiskRadiationSource")
790 validate_position_like(rotation,
"rotation",
"addDiskRadiationSource")
792 raise ValueError(f
"Radius must be positive, got {radius}")
794 return radiation_wrapper.addDiskRadiationSource(self.
radiation_model, position, radius, rotation)
797 @require_plugin('radiation', 'manage source spectrum')
800 Set radiation spectrum for source(s).
802 Spectral distributions define how radiation intensity varies with wavelength,
803 essential for realistic modeling of different light sources (sunlight, LEDs, etc.).
806 source_id: Source ID (int) or list of source IDs
808 - Spectrum data as list of (wavelength, value) tuples
809 - Global data label string
812 >>> # Define custom LED spectrum
814 ... (400, 0.0), (450, 0.3), (500, 0.8),
815 ... (550, 0.5), (600, 0.2), (700, 0.0)
817 >>> radiation.setSourceSpectrum(source_id, led_spectrum)
819 >>> # Use predefined spectrum from global data
820 >>> radiation.setSourceSpectrum(source_id, "D65_illuminant")
822 >>> # Apply same spectrum to multiple sources
823 >>> radiation.setSourceSpectrum([src1, src2, src3], led_spectrum)
826 radiation_wrapper.setSourceSpectrum(self.
radiation_model, source_id, spectrum)
827 logger.debug(f
"Set spectrum for source(s) {source_id}")
829 @require_plugin('radiation', 'configure source spectrum')
831 wavelength_min: float =
None, wavelength_max: float =
None):
833 Set source spectrum integral value.
835 Normalizes the spectrum so that its integral equals the specified value,
836 useful for calibrating source intensity.
840 source_integral: Target integral value
841 wavelength_min: Optional minimum wavelength for integration range
842 wavelength_max: Optional maximum wavelength for integration range
845 >>> radiation.setSourceSpectrumIntegral(source_id, 1000.0)
846 >>> radiation.setSourceSpectrumIntegral(source_id, 500.0, 400, 700) # PAR range
848 if not isinstance(source_id, int)
or source_id < 0:
849 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
850 if source_integral < 0:
851 raise ValueError(f
"Source integral must be non-negative, got {source_integral}")
854 radiation_wrapper.setSourceSpectrumIntegral(self.
radiation_model, source_id, source_integral,
855 wavelength_min, wavelength_max)
856 logger.debug(f
"Set spectrum integral for source {source_id}: {source_integral}")
859 @require_plugin('radiation', 'integrate spectrum')
861 wavelength_max: float =
None, source_id: int =
None,
862 camera_spectrum=
None) -> float:
864 Integrate spectrum with optional source/camera spectra and wavelength range.
866 This unified method handles multiple integration scenarios:
867 - Basic: Total spectrum integration
868 - Range: Integration over wavelength range
869 - Source: Integration weighted by source spectrum
870 - Camera: Integration weighted by camera spectral response
871 - Full: Integration with both source and camera spectra
874 object_spectrum: Object spectrum as list of (wavelength, value) tuples/vec2
875 wavelength_min: Optional minimum wavelength for integration range
876 wavelength_max: Optional maximum wavelength for integration range
877 source_id: Optional source ID for source spectrum weighting
878 camera_spectrum: Optional camera spectrum for camera response weighting
884 >>> leaf_reflectance = [(400, 0.1), (500, 0.4), (600, 0.6), (700, 0.5)]
886 >>> # Total integration
887 >>> total = radiation.integrateSpectrum(leaf_reflectance)
889 >>> # PAR range (400-700nm)
890 >>> par = radiation.integrateSpectrum(leaf_reflectance, 400, 700)
892 >>> # With source spectrum
893 >>> source_weighted = radiation.integrateSpectrum(
894 ... leaf_reflectance, 400, 700, source_id=sun_source
897 >>> # With camera response
898 >>> camera_response = [(400, 0.2), (550, 1.0), (700, 0.3)]
899 >>> camera_weighted = radiation.integrateSpectrum(
900 ... leaf_reflectance, camera_spectrum=camera_response
904 return radiation_wrapper.integrateSpectrum(self.
radiation_model, object_spectrum,
905 wavelength_min, wavelength_max,
906 source_id, camera_spectrum)
908 @require_plugin('radiation', 'integrate source spectrum')
911 Integrate source spectrum over wavelength range.
915 wavelength_min: Minimum wavelength
916 wavelength_max: Maximum wavelength
919 Integrated source spectrum value
922 >>> par_flux = radiation.integrateSourceSpectrum(source_id, 400, 700)
924 if not isinstance(source_id, int)
or source_id < 0:
925 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
927 return radiation_wrapper.integrateSourceSpectrum(self.
radiation_model, source_id,
928 wavelength_min, wavelength_max)
931 @require_plugin('radiation', 'scale spectrum')
932 def scaleSpectrum(self, existing_label: str, new_label_or_scale, scale_factor: float =
None):
934 Scale spectrum in-place or to new label.
936 Useful for adjusting spectrum intensities or creating variations of
937 existing spectra for sensitivity analysis.
939 Supports two call patterns:
940 - scaleSpectrum("label", scale) -> scales in-place
941 - scaleSpectrum("existing", "new", scale) -> creates new scaled spectrum
944 existing_label: Existing global data label
945 new_label_or_scale: Either new label string (if creating new) or scale factor (if in-place)
946 scale_factor: Scale factor (required only if new_label_or_scale is a string)
949 >>> # In-place scaling
950 >>> radiation.scaleSpectrum("leaf_reflectance", 1.2)
952 >>> # Create new scaled spectrum
953 >>> radiation.scaleSpectrum("leaf_reflectance", "scaled_leaf", 1.5)
955 if not isinstance(existing_label, str)
or not existing_label.strip():
956 raise ValueError(
"Existing label must be a non-empty string")
960 new_label_or_scale, scale_factor)
961 logger.debug(f
"Scaled spectrum '{existing_label}'")
963 @require_plugin('radiation', 'scale spectrum randomly')
965 min_scale: float, max_scale: float):
967 Scale spectrum with random factor and store as new label.
969 Useful for creating stochastic variations in spectral properties for
970 Monte Carlo simulations or uncertainty quantification.
973 existing_label: Existing global data label
974 new_label: New global data label for scaled spectrum
975 min_scale: Minimum scale factor
976 max_scale: Maximum scale factor
979 >>> # Create random variation of leaf reflectance
980 >>> radiation.scaleSpectrumRandomly("leaf_base", "leaf_variant", 0.8, 1.2)
982 if not isinstance(existing_label, str)
or not existing_label.strip():
983 raise ValueError(
"Existing label must be a non-empty string")
984 if not isinstance(new_label, str)
or not new_label.strip():
985 raise ValueError(
"New label must be a non-empty string")
986 if min_scale >= max_scale:
987 raise ValueError(f
"min_scale ({min_scale}) must be less than max_scale ({max_scale})")
990 radiation_wrapper.scaleSpectrumRandomly(self.
radiation_model, existing_label, new_label,
991 min_scale, max_scale)
992 logger.debug(f
"Scaled spectrum '{existing_label}' randomly to '{new_label}'")
994 @require_plugin('radiation', 'blend spectra')
995 def blendSpectra(self, new_label: str, spectrum_labels: List[str], weights: List[float]):
997 Blend multiple spectra with specified weights.
999 Creates weighted combination of spectra, useful for mixing material properties
1000 or creating composite light sources.
1003 new_label: New global data label for blended spectrum
1004 spectrum_labels: List of spectrum labels to blend
1005 weights: List of weights (must sum to reasonable values, same length as labels)
1008 >>> # Mix two leaf types (70% type A, 30% type B)
1009 >>> radiation.blendSpectra("mixed_leaf",
1010 ... ["leaf_type_a", "leaf_type_b"],
1014 if not isinstance(new_label, str)
or not new_label.strip():
1015 raise ValueError(
"New label must be a non-empty string")
1016 if len(spectrum_labels) != len(weights):
1017 raise ValueError(f
"Number of labels ({len(spectrum_labels)}) must match number of weights ({len(weights)})")
1018 if not spectrum_labels:
1019 raise ValueError(
"At least one spectrum label required")
1022 radiation_wrapper.blendSpectra(self.
radiation_model, new_label, spectrum_labels, weights)
1023 logger.debug(f
"Blended {len(spectrum_labels)} spectra into '{new_label}'")
1025 @require_plugin('radiation', 'blend spectra randomly')
1028 Blend multiple spectra with random weights.
1030 Creates random combinations of spectra, useful for generating diverse
1031 material properties in stochastic simulations.
1034 new_label: New global data label for blended spectrum
1035 spectrum_labels: List of spectrum labels to blend
1038 >>> # Create random mixture of leaf spectra
1039 >>> radiation.blendSpectraRandomly("random_leaf",
1040 ... ["young_leaf", "mature_leaf", "senescent_leaf"]
1043 if not isinstance(new_label, str)
or not new_label.strip():
1044 raise ValueError(
"New label must be a non-empty string")
1045 if not spectrum_labels:
1046 raise ValueError(
"At least one spectrum label required")
1049 radiation_wrapper.blendSpectraRandomly(self.
radiation_model, new_label, spectrum_labels)
1050 logger.debug(f
"Blended {len(spectrum_labels)} spectra randomly into '{new_label}'")
1053 @require_plugin('radiation', 'interpolate spectrum from data')
1055 spectra_labels: List[str], values: List[float],
1056 primitive_data_query_label: str,
1057 primitive_data_radprop_label: str):
1059 Interpolate spectral properties based on primitive data values.
1061 Automatically assigns spectra to primitives by interpolating between
1062 reference spectra based on continuous data values (e.g., age, moisture, etc.).
1065 primitive_uuids: List of primitive UUIDs to assign spectra
1066 spectra_labels: List of reference spectrum labels
1067 values: List of data values corresponding to each spectrum
1068 primitive_data_query_label: Primitive data label containing query values
1069 primitive_data_radprop_label: Primitive data label to store assigned spectra
1072 >>> # Assign leaf reflectance based on age
1073 >>> leaf_patches = context.getAllUUIDs("patch")
1074 >>> radiation.interpolateSpectrumFromPrimitiveData(
1075 ... primitive_uuids=leaf_patches,
1076 ... spectra_labels=["young_leaf", "mature_leaf", "old_leaf"],
1077 ... values=[0.0, 50.0, 100.0], # Days since emergence
1078 ... primitive_data_query_label="leaf_age",
1079 ... primitive_data_radprop_label="reflectance"
1082 if not isinstance(primitive_uuids, (list, tuple))
or not primitive_uuids:
1083 raise ValueError(
"Primitive UUIDs must be a non-empty list")
1084 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
1085 raise ValueError(
"Spectra labels must be a non-empty list")
1086 if not isinstance(values, (list, tuple))
or not values:
1087 raise ValueError(
"Values must be a non-empty list")
1088 if len(spectra_labels) != len(values):
1089 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
1092 radiation_wrapper.interpolateSpectrumFromPrimitiveData(
1094 primitive_data_query_label, primitive_data_radprop_label
1096 logger.debug(f
"Interpolated spectra for {len(primitive_uuids)} primitives")
1098 @require_plugin('radiation', 'interpolate spectrum from object data')
1100 spectra_labels: List[str], values: List[float],
1101 object_data_query_label: str,
1102 primitive_data_radprop_label: str):
1104 Interpolate spectral properties based on object data values.
1106 Automatically assigns spectra to object primitives by interpolating between
1107 reference spectra based on continuous object-level data values.
1110 object_ids: List of object IDs
1111 spectra_labels: List of reference spectrum labels
1112 values: List of data values corresponding to each spectrum
1113 object_data_query_label: Object data label containing query values
1114 primitive_data_radprop_label: Primitive data label to store assigned spectra
1117 >>> # Assign tree reflectance based on health index
1118 >>> tree_ids = [tree1_id, tree2_id, tree3_id]
1119 >>> radiation.interpolateSpectrumFromObjectData(
1120 ... object_ids=tree_ids,
1121 ... spectra_labels=["healthy_tree", "stressed_tree", "diseased_tree"],
1122 ... values=[1.0, 0.5, 0.0], # Health index
1123 ... object_data_query_label="health_index",
1124 ... primitive_data_radprop_label="reflectance"
1127 if not isinstance(object_ids, (list, tuple))
or not object_ids:
1128 raise ValueError(
"Object IDs must be a non-empty list")
1129 if not isinstance(spectra_labels, (list, tuple))
or not spectra_labels:
1130 raise ValueError(
"Spectra labels must be a non-empty list")
1131 if not isinstance(values, (list, tuple))
or not values:
1132 raise ValueError(
"Values must be a non-empty list")
1133 if len(spectra_labels) != len(values):
1134 raise ValueError(f
"Number of spectra ({len(spectra_labels)}) must match number of values ({len(values)})")
1137 radiation_wrapper.interpolateSpectrumFromObjectData(
1139 object_data_query_label, primitive_data_radprop_label
1141 logger.debug(f
"Interpolated spectra for {len(object_ids)} objects")
1143 @require_plugin('radiation', 'set ray count')
1145 """Set direct ray count for radiation band."""
1146 validate_band_label(band_label,
"band_label",
"setDirectRayCount")
1147 validate_ray_count(ray_count,
"ray_count",
"setDirectRayCount")
1149 radiation_wrapper.setDirectRayCount(self.
radiation_model, band_label, ray_count)
1151 @require_plugin('radiation', 'set ray count')
1153 """Set diffuse ray count for radiation band."""
1154 validate_band_label(band_label,
"band_label",
"setDiffuseRayCount")
1155 validate_ray_count(ray_count,
"ray_count",
"setDiffuseRayCount")
1159 @require_plugin('radiation', 'set radiation flux')
1161 """Set diffuse radiation flux for band."""
1162 validate_band_label(label,
"label",
"setDiffuseRadiationFlux")
1163 validate_flux_value(flux,
"flux",
"setDiffuseRadiationFlux")
1167 @require_plugin('radiation', 'configure diffuse radiation')
1170 Set diffuse radiation extinction coefficient with directional bias.
1172 Models directionally-biased diffuse radiation (e.g., sky radiation with zenith peak).
1176 K: Extinction coefficient
1177 peak_direction: Peak direction as vec3, SphericalCoord, or list [x, y, z]
1180 >>> from pyhelios.types import vec3
1181 >>> radiation.setDiffuseRadiationExtinctionCoeff("SW", 0.5, vec3(0, 0, 1))
1183 validate_band_label(label,
"label",
"setDiffuseRadiationExtinctionCoeff")
1185 raise ValueError(f
"Extinction coefficient must be non-negative, got {K}")
1186 validate_direction_like(peak_direction,
"peak_direction",
"setDiffuseRadiationExtinctionCoeff")
1188 radiation_wrapper.setDiffuseRadiationExtinctionCoeff(self.
radiation_model, label, K, peak_direction)
1189 logger.debug(f
"Set diffuse extinction coefficient for band '{label}': K={K}")
1191 @require_plugin('radiation', 'query diffuse flux')
1194 Get diffuse flux for band.
1197 band_label: Band label
1203 >>> flux = radiation.getDiffuseFlux("SW")
1205 validate_band_label(band_label,
"band_label",
"getDiffuseFlux")
1207 return radiation_wrapper.getDiffuseFlux(self.
radiation_model, band_label)
1209 @require_plugin('radiation', 'configure diffuse spectrum')
1212 Set diffuse spectrum from global data label.
1215 band_label: Band label (string) or list of band labels
1216 spectrum_label: Spectrum global data label
1219 >>> radiation.setDiffuseSpectrum("SW", "sky_spectrum")
1220 >>> radiation.setDiffuseSpectrum(["SW", "NIR"], "sky_spectrum")
1222 if isinstance(band_label, str):
1223 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrum")
1225 for label
in band_label:
1226 validate_band_label(label,
"band_label",
"setDiffuseSpectrum")
1227 if not isinstance(spectrum_label, str)
or not spectrum_label.strip():
1228 raise ValueError(
"Spectrum label must be a non-empty string")
1231 radiation_wrapper.setDiffuseSpectrum(self.
radiation_model, band_label, spectrum_label)
1232 logger.debug(f
"Set diffuse spectrum for band(s) {band_label}")
1234 @require_plugin('radiation', 'configure diffuse spectrum')
1236 wavelength_max: float =
None, band_label: str =
None):
1238 Set diffuse spectrum integral.
1241 spectrum_integral: Integral value
1242 wavelength_min: Optional minimum wavelength
1243 wavelength_max: Optional maximum wavelength
1244 band_label: Optional specific band label (None for all bands)
1247 >>> radiation.setDiffuseSpectrumIntegral(1000.0) # All bands
1248 >>> radiation.setDiffuseSpectrumIntegral(500.0, 400, 700, band_label="PAR") # Specific band
1250 if spectrum_integral < 0:
1251 raise ValueError(f
"Spectrum integral must be non-negative, got {spectrum_integral}")
1252 if band_label
is not None:
1253 validate_band_label(band_label,
"band_label",
"setDiffuseSpectrumIntegral")
1256 radiation_wrapper.setDiffuseSpectrumIntegral(self.
radiation_model, spectrum_integral,
1257 wavelength_min, wavelength_max, band_label)
1258 logger.debug(f
"Set diffuse spectrum integral: {spectrum_integral}")
1260 @require_plugin('radiation', 'set source flux')
1261 def setSourceFlux(self, source_id, label: str, flux: float):
1262 """Set source flux for single source or multiple sources."""
1263 validate_band_label(label,
"label",
"setSourceFlux")
1264 validate_flux_value(flux,
"flux",
"setSourceFlux")
1266 if isinstance(source_id, (list, tuple)):
1268 validate_source_id_list(list(source_id),
"source_id",
"setSourceFlux")
1270 radiation_wrapper.setSourceFluxMultiple(self.
radiation_model, source_id, label, flux)
1273 validate_source_id(source_id,
"source_id",
"setSourceFlux")
1275 radiation_wrapper.setSourceFlux(self.
radiation_model, source_id, label, flux)
1278 @require_plugin('radiation', 'get source flux')
1279 @validate_get_source_flux_params
1280 def getSourceFlux(self, source_id: int, label: str) -> float:
1281 """Get source flux for band."""
1283 return radiation_wrapper.getSourceFlux(self.
radiation_model, source_id, label)
1285 @require_plugin('radiation', 'update geometry')
1286 @validate_update_geometry_params
1289 Update geometry in radiation model.
1292 uuids: Optional list of specific UUIDs to update. If None, updates all geometry.
1297 logger.debug(
"Updated all geometry in radiation model")
1301 logger.debug(f
"Updated {len(uuids)} geometry UUIDs in radiation model")
1304 @require_plugin('radiation', 'run radiation simulation')
1305 @validate_run_band_params
1306 def runBand(self, band_label):
1308 Run radiation simulation for single band or multiple bands.
1310 PERFORMANCE NOTE: When simulating multiple radiation bands, it is HIGHLY RECOMMENDED
1311 to run all bands in a single call (e.g., runBand(["PAR", "NIR", "SW"])) rather than
1312 sequential single-band calls. This provides significant computational efficiency gains
1315 - GPU ray tracing setup is done once for all bands
1316 - Scene geometry acceleration structures are reused
1317 - GPU kernel launches are batched together
1318 - Memory transfers between CPU/GPU are minimized
1321 # EFFICIENT - Single call for multiple bands
1322 radiation.runBand(["PAR", "NIR", "SW"])
1324 # INEFFICIENT - Sequential single-band calls
1325 radiation.runBand("PAR")
1326 radiation.runBand("NIR")
1327 radiation.runBand("SW")
1330 band_label: Single band name (str) or list of band names for multi-band simulation
1332 if isinstance(band_label, (list, tuple)):
1334 for lbl
in band_label:
1335 if not isinstance(lbl, str):
1336 raise TypeError(f
"Band labels must be strings, got {type(lbl).__name__}")
1339 logger.info(f
"Completed radiation simulation for bands: {band_label}")
1342 if not isinstance(band_label, str):
1343 raise TypeError(f
"Band label must be a string, got {type(band_label).__name__}")
1346 logger.info(f
"Completed radiation simulation for band: {band_label}")
1349 @require_plugin('radiation', 'get simulation results')
1351 """Get absorbed radiation flux density for all primitives, summed over all bands.
1353 Returns one value per primitive, in the Context's primitive order (matching
1354 ``context.getAllUUIDs()``).
1356 Units are **W/m^2** (flux density), not watts. This is the sum of the
1357 ``radiation_flux_<band>`` primitive data over every band added to the model.
1358 Because it is a density, the value does not change when a primitive's size
1359 changes: a 1x1 m and a 2x2 m patch under the same collimated source both
1360 report the same number.
1362 To obtain absorbed power in watts, weight each primitive by its area::
1364 flux = radiation.getTotalAbsorbedFlux()
1365 power = sum(f * context.getPrimitiveArea(u)
1366 for f, u in zip(flux, context.getAllUUIDs()))
1368 Summing the returned values directly (``sum(flux)``) adds flux densities of
1369 differently-sized surfaces and is not physically meaningful.
1372 Absorbed flux density per primitive in W/m^2.
1375 results = radiation_wrapper.getTotalAbsorbedFlux(self.
radiation_model)
1376 logger.debug(f
"Retrieved absorbed flux data for {len(results)} primitives")
1380 @require_plugin('radiation', 'check band existence')
1383 Check if a radiation band exists.
1386 label: Name/label of the radiation band to check
1389 True if band exists, False otherwise
1392 >>> radiation.addRadiationBand("SW")
1393 >>> radiation.doesBandExist("SW")
1395 >>> radiation.doesBandExist("nonexistent")
1398 validate_band_label(label,
"label",
"doesBandExist")
1403 @require_plugin('radiation', 'manage radiation sources')
1406 Delete a radiation source.
1409 source_id: ID of the radiation source to delete
1412 >>> source_id = radiation.addCollimatedRadiationSource()
1413 >>> radiation.deleteRadiationSource(source_id)
1415 if not isinstance(source_id, int)
or source_id < 0:
1416 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1418 radiation_wrapper.deleteRadiationSource(self.
radiation_model, source_id)
1419 logger.debug(f
"Deleted radiation source {source_id}")
1421 @require_plugin('radiation', 'query radiation sources')
1424 Get position of a radiation source.
1427 source_id: ID of the radiation source
1430 vec3 position of the source
1433 >>> source_id = radiation.addCollimatedRadiationSource()
1434 >>> position = radiation.getSourcePosition(source_id)
1435 >>> print(f"Source at: {position}")
1437 if not isinstance(source_id, int)
or source_id < 0:
1438 raise ValueError(f
"Source ID must be a non-negative integer, got {source_id}")
1440 position_list = radiation_wrapper.getSourcePosition(self.
radiation_model, source_id)
1441 from .wrappers.DataTypes
import vec3
1442 return vec3(position_list[0], position_list[1], position_list[2])
1445 @require_plugin('radiation', 'get sky energy')
1448 Get total sky energy.
1451 Total sky energy value
1454 >>> energy = radiation.getSkyEnergy()
1455 >>> print(f"Sky energy: {energy}")
1460 @require_plugin('radiation', 'calculate G-function')
1463 Calculate G-function (geometry factor) for given view direction.
1465 The G-function describes the geometric relationship between leaf area
1466 distribution and viewing direction, important for canopy radiation modeling.
1468 The G-function is computed from the geometry currently loaded in the radiation
1469 model. If updateGeometry() has not yet been called, this method calls it
1470 automatically (with a warning) so the query operates on the current context
1471 geometry. If the result is still undefined (no primitives / zero leaf area),
1472 a RuntimeError is raised rather than silently returning NaN.
1475 view_direction: View direction as vec3 or list/tuple [x, y, z]
1481 RuntimeError: If the context has no geometry (or zero total leaf area),
1482 so the G-function is undefined.
1485 >>> from pyhelios.types import vec3
1486 >>> radiation.updateGeometry()
1487 >>> g_value = radiation.calculateGtheta(vec3(0, 0, 1))
1488 >>> print(f"G-function: {g_value}")
1490 validate_position_like(view_direction,
"view_direction",
"calculateGtheta")
1494 "calculateGtheta called before updateGeometry(); updating radiation "
1495 "model geometry automatically. Call updateGeometry() explicitly after "
1496 "building the scene to avoid this."
1502 value = radiation_wrapper.calculateGtheta(self.
radiation_model, context_ptr, view_direction)
1504 if value
is None or math.isnan(value):
1506 "calculateGtheta returned an undefined (NaN) G-function. The radiation "
1507 "model has no geometry with positive leaf area for this context. Add "
1508 "primitives to the Context and ensure updateGeometry() succeeds before "
1509 "calling calculateGtheta()."
1513 @require_plugin('radiation', 'configure output data')
1516 Enable optional primitive data output.
1519 label: Name/label of the primitive data to output
1522 >>> radiation.optionalOutputPrimitiveData("temperature")
1524 validate_band_label(label,
"label",
"optionalOutputPrimitiveData")
1526 radiation_wrapper.optionalOutputPrimitiveData(self.
radiation_model, label)
1527 logger.debug(f
"Enabled optional output for primitive data: {label}")
1529 @require_plugin('radiation', 'configure boundary conditions')
1532 Enforce periodic boundary conditions.
1534 Periodic boundaries are useful for large-scale simulations to reduce
1535 edge effects by wrapping radiation at domain boundaries.
1538 boundary: Boundary specification string (e.g., "xy", "xyz", "x", "y", "z")
1541 >>> radiation.enforcePeriodicBoundary("xy")
1543 if not isinstance(boundary, str)
or not boundary:
1544 raise ValueError(
"Boundary specification must be a non-empty string")
1546 radiation_wrapper.enforcePeriodicBoundary(self.
radiation_model, boundary)
1547 logger.debug(f
"Enforced periodic boundary: {boundary}")
1550 @require_plugin('radiation', 'configure radiation simulation')
1551 @validate_scattering_depth_params
1553 """Set scattering depth for radiation band."""
1555 radiation_wrapper.setScatteringDepth(self.
radiation_model, label, depth)
1557 @require_plugin('radiation', 'configure radiation simulation')
1558 @validate_min_scatter_energy_params
1560 """Set minimum scatter energy for radiation band."""
1562 radiation_wrapper.setMinScatterEnergy(self.
radiation_model, label, energy)
1564 @require_plugin('radiation', 'configure radiation emission')
1566 """Disable emission for radiation band."""
1567 validate_band_label(label,
"label",
"disableEmission")
1571 @require_plugin('radiation', 'configure radiation emission')
1573 """Enable emission for radiation band."""
1574 validate_band_label(label,
"label",
"enableEmission")
1582 @require_plugin('radiation', 'add radiation camera')
1583 def addRadiationCamera(self, camera_label: str, band_labels: List[str], position, lookat_or_direction,
1584 camera_properties=
None, antialiasing_samples: int = 100):
1586 Add a radiation camera to the simulation.
1589 camera_label: Unique label string for the camera
1590 band_labels: List of radiation band labels for the camera
1591 position: Camera position as vec3 object
1592 lookat_or_direction: Either:
1593 - Lookat point as vec3 object
1594 - SphericalCoord for viewing direction
1595 camera_properties: CameraProperties instance or None for defaults
1596 antialiasing_samples: Number of antialiasing samples (default: 100)
1599 ValidationError: If parameters are invalid or have wrong types
1600 RadiationModelError: If camera creation fails
1603 >>> from pyhelios import vec3, CameraProperties
1604 >>> # Create camera looking at origin from above
1605 >>> camera_props = CameraProperties(camera_resolution=(1024, 1024))
1606 >>> radiation_model.addRadiationCamera("main_camera", ["red", "green", "blue"],
1607 ... position=vec3(0, 0, 5), lookat_or_direction=vec3(0, 0, 0),
1608 ... camera_properties=camera_props)
1611 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1612 from .wrappers.DataTypes
import SphericalCoord, vec3, make_vec3
1613 from .validation.plugins
import validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
1616 validated_label = validate_camera_label(camera_label,
"camera_label",
"addRadiationCamera")
1617 validated_bands = validate_band_labels_list(band_labels,
"band_labels",
"addRadiationCamera")
1618 validated_samples = validate_antialiasing_samples(antialiasing_samples,
"antialiasing_samples",
"addRadiationCamera")
1621 if not isinstance(position, vec3):
1622 raise TypeError(
"position must be a vec3 object. Use vec3(x, y, z) to create one.")
1623 validated_position = position
1626 if isinstance(lookat_or_direction, SphericalCoord):
1627 validated_direction = lookat_or_direction
1628 elif isinstance(lookat_or_direction, vec3):
1629 validated_direction = lookat_or_direction
1631 raise TypeError(
"lookat_or_direction must be a vec3 or SphericalCoord object. Use vec3(x, y, z) or SphericalCoord to create one.")
1634 if camera_properties
is None:
1640 if hasattr(validated_direction,
'radius')
and hasattr(validated_direction,
'elevation'):
1642 direction_coords = validated_direction.to_list()
1645 if len(direction_coords) < 4:
1646 raise ValueError(
"SphericalCoord must expose radius, elevation, zenith, and azimuth")
1647 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[3]
1649 radiation_wrapper.addRadiationCameraSpherical(
1653 validated_position.x, validated_position.y, validated_position.z,
1654 radius, elevation, azimuth,
1655 camera_properties.to_array(),
1657 camera_properties.exposure
1661 radiation_wrapper.addRadiationCameraVec3(
1665 validated_position.x, validated_position.y, validated_position.z,
1666 validated_direction.x, validated_direction.y, validated_direction.z,
1667 camera_properties.to_array(),
1669 camera_properties.exposure
1672 except Exception
as e:
1675 @require_plugin('radiation', 'add SIF camera')
1676 def addSIFCamera(self, camera_label: str, emission_band_labels: List[str], position,
1677 lookat_or_direction, camera_properties=
None, antialiasing_samples: int = 100):
1679 Add a solar-induced chlorophyll fluorescence (SIF) camera.
1681 Each band in ``emission_band_labels`` must already exist (added via
1682 :meth:`addRadiationBand`); those bands are flagged internally as SIF-emitting and
1683 use the Fluspect-B leaf-fluorescence kernel for emission instead of Stefan-Boltzmann.
1684 Helios auto-creates internal radiation bands covering 400-750 nm at the resolution
1685 specified by ``camera_properties.excitation_bin_width_nm``.
1688 camera_label: Unique label for the camera.
1689 emission_band_labels: List of pre-existing radiation band labels to drive
1691 position: Camera position as a ``vec3``.
1692 lookat_or_direction: Either a ``vec3`` lookat point or a ``SphericalCoord``
1694 camera_properties: :class:`SIFCameraProperties` instance. If ``None`` defaults
1695 are used (10 nm excitation bins, no excitation scattering).
1696 antialiasing_samples: Antialiasing samples per pixel (>= 1, default 100).
1699 RadiationModelError: If the underlying SIF camera cannot be added (e.g.,
1700 an emission band was already bound to a different excitation bin width).
1701 NotImplementedError: If running against helios-core older than v1.3.72.
1703 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1704 from .wrappers.DataTypes
import SphericalCoord, vec3
1705 from .validation.plugins
import (
1706 validate_camera_label, validate_band_labels_list, validate_antialiasing_samples
1709 validated_label = validate_camera_label(camera_label,
"camera_label",
"addSIFCamera")
1710 validated_bands = validate_band_labels_list(emission_band_labels,
"emission_band_labels",
"addSIFCamera")
1711 validated_samples = validate_antialiasing_samples(antialiasing_samples,
"antialiasing_samples",
"addSIFCamera")
1713 if not isinstance(position, vec3):
1714 raise TypeError(
"position must be a vec3 object. Use vec3(x, y, z) to create one.")
1716 if not isinstance(lookat_or_direction, (vec3, SphericalCoord)):
1717 raise TypeError(
"lookat_or_direction must be a vec3 or SphericalCoord object.")
1719 if camera_properties
is None:
1721 elif not isinstance(camera_properties, SIFCameraProperties):
1723 "camera_properties must be a SIFCameraProperties instance "
1724 "(use SIFCameraProperties(...) — not the plain CameraProperties)."
1729 if isinstance(lookat_or_direction, SphericalCoord):
1732 direction_coords = lookat_or_direction.to_list()
1733 if len(direction_coords) < 4:
1734 raise ValueError(
"SphericalCoord must expose radius, elevation, zenith, and azimuth")
1735 radius, elevation, azimuth = direction_coords[0], direction_coords[1], direction_coords[3]
1736 radiation_wrapper.addSIFCameraSpherical(
1740 position.x, position.y, position.z,
1741 radius, elevation, azimuth,
1742 camera_properties.to_array(),
1743 camera_properties.excitation_bin_width_nm,
1744 camera_properties.excitation_scattering_depth,
1748 radiation_wrapper.addSIFCameraVec3(
1752 position.x, position.y, position.z,
1753 lookat_or_direction.x, lookat_or_direction.y, lookat_or_direction.z,
1754 camera_properties.to_array(),
1755 camera_properties.excitation_bin_width_nm,
1756 camera_properties.excitation_scattering_depth,
1759 except Exception
as e:
1762 @require_plugin('radiation', 'check SIF camera registration')
1765 Return True if the camera was registered via :meth:`addSIFCamera` (vs. ``addRadiationCamera``).
1767 from .wrappers
import URadiationModelWrapper
as radiation_wrapper
1768 if not isinstance(camera_label, str)
or not camera_label.strip():
1769 raise ValueError(
"Camera label must be a non-empty string")
1771 return radiation_wrapper.isSIFCamera(self.
radiation_model, camera_label)
1773 @require_plugin('radiation', 'manage camera position')
1776 Set camera position.
1778 Allows dynamic camera repositioning during simulation, useful for
1779 time-series captures or multi-view imaging.
1782 camera_label: Camera label string
1783 position: Camera position as vec3 or list [x, y, z]
1786 >>> radiation.setCameraPosition("cam1", [0, 0, 10])
1787 >>> from pyhelios.types import vec3
1788 >>> radiation.setCameraPosition("cam1", vec3(5, 5, 10))
1790 if not isinstance(camera_label, str)
or not camera_label.strip():
1791 raise ValueError(
"Camera label must be a non-empty string")
1792 validate_position_like(position,
"position",
"setCameraPosition")
1794 radiation_wrapper.setCameraPosition(self.
radiation_model, camera_label, position)
1795 logger.debug(f
"Updated camera '{camera_label}' position")
1797 @require_plugin('radiation', 'query camera position')
1800 Get camera position.
1803 camera_label: Camera label string
1806 vec3 position of the camera
1809 >>> position = radiation.getCameraPosition("cam1")
1810 >>> print(f"Camera at: {position}")
1812 if not isinstance(camera_label, str)
or not camera_label.strip():
1813 raise ValueError(
"Camera label must be a non-empty string")
1815 position_list = radiation_wrapper.getCameraPosition(self.
radiation_model, camera_label)
1816 from .wrappers.DataTypes
import vec3
1817 return vec3(position_list[0], position_list[1], position_list[2])
1819 @require_plugin('radiation', 'manage camera lookat')
1822 Set camera lookat point.
1825 camera_label: Camera label string
1826 lookat: Lookat point as vec3 or list [x, y, z]
1829 >>> radiation.setCameraLookat("cam1", [0, 0, 0])
1831 if not isinstance(camera_label, str)
or not camera_label.strip():
1832 raise ValueError(
"Camera label must be a non-empty string")
1833 validate_position_like(lookat,
"lookat",
"setCameraLookat")
1835 radiation_wrapper.setCameraLookat(self.
radiation_model, camera_label, lookat)
1836 logger.debug(f
"Updated camera '{camera_label}' lookat point")
1838 @require_plugin('radiation', 'query camera lookat')
1841 Get camera lookat point.
1844 camera_label: Camera label string
1850 >>> lookat = radiation.getCameraLookat("cam1")
1851 >>> print(f"Camera looking at: {lookat}")
1853 if not isinstance(camera_label, str)
or not camera_label.strip():
1854 raise ValueError(
"Camera label must be a non-empty string")
1856 lookat_list = radiation_wrapper.getCameraLookat(self.
radiation_model, camera_label)
1857 from .wrappers.DataTypes
import vec3
1858 return vec3(lookat_list[0], lookat_list[1], lookat_list[2])
1860 @require_plugin('radiation', 'manage camera orientation')
1863 Set camera orientation.
1866 camera_label: Camera label string
1867 direction: View direction as vec3, SphericalCoord, or list [x, y, z]
1870 >>> radiation.setCameraOrientation("cam1", [0, 0, 1])
1871 >>> from pyhelios.types import SphericalCoord
1872 >>> radiation.setCameraOrientation("cam1", SphericalCoord(1.0, 45.0, 90.0))
1874 if not isinstance(camera_label, str)
or not camera_label.strip():
1875 raise ValueError(
"Camera label must be a non-empty string")
1876 validate_direction_like(direction,
"direction",
"setCameraOrientation")
1878 radiation_wrapper.setCameraOrientation(self.
radiation_model, camera_label, direction)
1879 logger.debug(f
"Updated camera '{camera_label}' orientation")
1881 @require_plugin('radiation', 'query camera orientation')
1884 Get camera orientation.
1887 camera_label: Camera label string
1890 SphericalCoord orientation [radius, elevation, azimuth]
1893 >>> orientation = radiation.getCameraOrientation("cam1")
1894 >>> print(f"Camera orientation: {orientation}")
1896 if not isinstance(camera_label, str)
or not camera_label.strip():
1897 raise ValueError(
"Camera label must be a non-empty string")
1899 orientation_list = radiation_wrapper.getCameraOrientation(self.
radiation_model, camera_label)
1900 from .wrappers.DataTypes
import SphericalCoord
1901 return SphericalCoord(orientation_list[0], orientation_list[1], orientation_list[2])
1903 @require_plugin('radiation', 'query cameras')
1906 Get all camera labels.
1909 List of all camera label strings
1912 >>> cameras = radiation.getAllCameraLabels()
1913 >>> print(f"Available cameras: {cameras}")
1918 @require_plugin('radiation', 'configure camera spectral response')
1921 Set camera spectral response from global data.
1924 camera_label: Camera label
1925 band_label: Band label
1926 global_data: Global data label for spectral response curve
1929 >>> radiation.setCameraSpectralResponse("cam1", "red", "sensor_red_response")
1931 if not isinstance(camera_label, str)
or not camera_label.strip():
1932 raise ValueError(
"Camera label must be a non-empty string")
1933 validate_band_label(band_label,
"band_label",
"setCameraSpectralResponse")
1934 if not isinstance(global_data, str)
or not global_data.strip():
1935 raise ValueError(
"Global data label must be a non-empty string")
1938 radiation_wrapper.setCameraSpectralResponse(self.
radiation_model, camera_label, band_label, global_data)
1939 logger.debug(f
"Set spectral response for camera '{camera_label}', band '{band_label}'")
1941 @require_plugin('radiation', 'configure camera from library')
1944 Set camera spectral response from standard camera library.
1946 Uses pre-defined spectral response curves for common cameras.
1949 camera_label: Camera label
1950 camera_library_name: Standard camera name (e.g., "iPhone13", "NikonD850", "CanonEOS5D")
1953 >>> radiation.setCameraSpectralResponseFromLibrary("cam1", "iPhone13")
1955 if not isinstance(camera_label, str)
or not camera_label.strip():
1956 raise ValueError(
"Camera label must be a non-empty string")
1957 if not isinstance(camera_library_name, str)
or not camera_library_name.strip():
1958 raise ValueError(
"Camera library name must be a non-empty string")
1961 radiation_wrapper.setCameraSpectralResponseFromLibrary(self.
radiation_model, camera_label, camera_library_name)
1962 logger.debug(f
"Set camera '{camera_label}' response from library: {camera_library_name}")
1964 @require_plugin('radiation', 'get camera pixel data')
1967 Get camera pixel data for specific band.
1969 Retrieves raw pixel values for programmatic access and analysis.
1972 camera_label: Camera label
1973 band_label: Band label
1976 List of pixel values
1979 >>> pixels = radiation.getCameraPixelData("cam1", "red")
1980 >>> print(f"Mean pixel value: {sum(pixels)/len(pixels)}")
1982 if not isinstance(camera_label, str)
or not camera_label.strip():
1983 raise ValueError(
"Camera label must be a non-empty string")
1984 validate_band_label(band_label,
"band_label",
"getCameraPixelData")
1987 return radiation_wrapper.getCameraPixelData(self.
radiation_model, camera_label, band_label)
1989 @require_plugin('radiation', 'set camera pixel data')
1990 def setCameraPixelData(self, camera_label: str, band_label: str, pixel_data: List[float]):
1992 Set camera pixel data for specific band.
1994 Allows programmatic modification of pixel values.
1997 camera_label: Camera label
1998 band_label: Band label
1999 pixel_data: List of pixel values
2002 >>> pixels = radiation.getCameraPixelData("cam1", "red")
2003 >>> modified_pixels = [p * 1.2 for p in pixels] # Brighten by 20%
2004 >>> radiation.setCameraPixelData("cam1", "red", modified_pixels)
2006 if not isinstance(camera_label, str)
or not camera_label.strip():
2007 raise ValueError(
"Camera label must be a non-empty string")
2008 validate_band_label(band_label,
"band_label",
"setCameraPixelData")
2009 if not isinstance(pixel_data, (list, tuple)):
2010 raise ValueError(
"Pixel data must be a list or tuple")
2013 radiation_wrapper.setCameraPixelData(self.
radiation_model, camera_label, band_label, pixel_data)
2014 logger.debug(f
"Set pixel data for camera '{camera_label}', band '{band_label}': {len(pixel_data)} pixels")
2020 @require_plugin('radiation', 'add camera from library')
2022 position, lookat, antialiasing_samples: int = 1,
2023 band_labels: Optional[List[str]] =
None):
2025 Add radiation camera loading all properties from camera library.
2027 Loads camera intrinsic parameters (resolution, FOV, sensor size) and spectral
2028 response data from the camera library XML file. This is the recommended way to
2029 create realistic cameras with proper spectral responses.
2032 camera_label: Label for the camera instance
2033 library_camera_label: Label of camera in library (e.g., "Canon_20D", "iPhone11", "NikonD700")
2034 position: Camera position as vec3 or (x, y, z) tuple
2035 lookat: Lookat point as vec3 or (x, y, z) tuple
2036 antialiasing_samples: Number of ray samples per pixel. Default: 1
2037 band_labels: Optional custom band labels. If None, uses library defaults.
2040 RadiationModelError: If operation fails
2041 ValueError: If parameters are invalid
2044 Available cameras in plugins/radiation/camera_library/camera_library.xml include:
2045 - Canon_20D, Nikon_D700, Nikon_D50
2046 - iPhone11, iPhone12ProMAX
2047 - Additional cameras available in library
2050 >>> radiation.addRadiationCameraFromLibrary(
2051 ... camera_label="cam1",
2052 ... library_camera_label="iPhone11",
2053 ... position=(0, -5, 1),
2054 ... lookat=(0, 0, 0.5),
2055 ... antialiasing_samples=10
2058 validate_band_label(camera_label,
"camera_label",
"addRadiationCameraFromLibrary")
2059 validate_position_like(position,
"position",
"addRadiationCameraFromLibrary")
2060 validate_position_like(lookat,
"lookat",
"addRadiationCameraFromLibrary")
2064 radiation_wrapper.addRadiationCameraFromLibrary(
2066 position, lookat, antialiasing_samples, band_labels
2068 logger.info(f
"Added camera '{camera_label}' from library '{library_camera_label}'")
2069 except Exception
as e:
2072 @require_plugin('radiation', 'update camera parameters')
2075 Update camera parameters for an existing camera.
2077 Allows modification of camera properties after creation while preserving
2078 position, lookat direction, and spectral band configuration.
2081 camera_label: Label for the camera to update
2082 camera_properties: CameraProperties instance with new parameters
2085 RadiationModelError: If operation fails or camera doesn't exist
2086 ValueError: If parameters are invalid
2089 FOV_aspect_ratio is automatically recalculated from camera_resolution.
2090 Camera position and lookat are preserved.
2093 >>> props = CameraProperties(
2094 ... camera_resolution=(1920, 1080),
2096 ... lens_focal_length=0.085 # 85mm lens
2098 >>> radiation.updateCameraParameters("cam1", props)
2100 validate_band_label(camera_label,
"camera_label",
"updateCameraParameters")
2102 if not isinstance(camera_properties, CameraProperties):
2103 raise ValueError(
"camera_properties must be a CameraProperties instance")
2107 radiation_wrapper.updateCameraParameters(self.
radiation_model, camera_label, camera_properties)
2108 logger.debug(f
"Updated parameters for camera '{camera_label}'")
2109 except Exception
as e:
2112 @require_plugin('radiation', 'enable camera metadata')
2115 Enable automatic JSON metadata file writing for camera(s).
2117 When enabled, writeCameraImage() automatically creates a JSON metadata file
2118 alongside the image containing comprehensive camera and scene information.
2121 camera_labels: Single camera label (str) or list of camera labels (List[str])
2124 RadiationModelError: If operation fails
2125 ValueError: If parameters are invalid
2129 - Camera properties (model, lens, sensor specs)
2130 - Geographic location (latitude, longitude)
2131 - Acquisition settings (date, time, exposure, white balance)
2132 - Agronomic data (plant species, heights, phenology stages)
2135 >>> # Enable for single camera
2136 >>> radiation.enableCameraMetadata("cam1")
2138 >>> # Enable for multiple cameras
2139 >>> radiation.enableCameraMetadata(["cam1", "cam2", "cam3"])
2143 radiation_wrapper.enableCameraMetadata(self.
radiation_model, camera_labels)
2144 if isinstance(camera_labels, str):
2145 logger.info(f
"Enabled metadata for camera '{camera_labels}'")
2147 logger.info(f
"Enabled metadata for {len(camera_labels)} cameras")
2148 except Exception
as e:
2151 @require_plugin('radiation', 'write camera images')
2153 image_path: str =
"./", frame: int = -1,
2154 flux_to_pixel_conversion: float = 1.0) -> str:
2156 Write camera image to file and return output filename.
2159 camera: Camera label
2160 bands: List of band labels to include in the image
2161 imagefile_base: Base filename for output
2162 image_path: Output directory path (default: current directory)
2163 frame: Frame number to write (-1 for all frames)
2164 flux_to_pixel_conversion: Conversion factor from flux to pixel values
2167 Output filename string
2170 RadiationModelError: If camera image writing fails
2171 TypeError: If parameters have incorrect types
2174 if not isinstance(camera, str)
or not camera.strip():
2175 raise TypeError(
"Camera label must be a non-empty string")
2176 if not isinstance(bands, list)
or not bands:
2177 raise TypeError(
"Bands must be a non-empty list of strings")
2178 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
2179 raise TypeError(
"All band labels must be non-empty strings")
2180 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2181 raise TypeError(
"Image file base must be a non-empty string")
2182 if not isinstance(image_path, str):
2183 raise TypeError(
"Image path must be a string")
2184 if not isinstance(frame, int):
2185 raise TypeError(
"Frame must be an integer")
2186 if not isinstance(flux_to_pixel_conversion, (int, float))
or flux_to_pixel_conversion <= 0:
2187 raise TypeError(
"Flux to pixel conversion must be a positive number")
2191 filename = radiation_wrapper.writeCameraImage(
2193 image_path, frame, flux_to_pixel_conversion)
2195 logger.info(f
"Camera image written to: {filename}")
2198 @require_plugin('radiation', 'write normalized camera images')
2200 image_path: str =
"./", frame: int = -1) -> str:
2202 Write normalized camera image to file and return output filename.
2205 camera: Camera label
2206 bands: List of band labels to include in the image
2207 imagefile_base: Base filename for output
2208 image_path: Output directory path (default: current directory)
2209 frame: Frame number to write (-1 for all frames)
2212 Output filename string
2215 RadiationModelError: If normalized camera image writing fails
2216 TypeError: If parameters have incorrect types
2219 if not isinstance(camera, str)
or not camera.strip():
2220 raise TypeError(
"Camera label must be a non-empty string")
2221 if not isinstance(bands, list)
or not bands:
2222 raise TypeError(
"Bands must be a non-empty list of strings")
2223 if not all(isinstance(band, str)
and band.strip()
for band
in bands):
2224 raise TypeError(
"All band labels must be non-empty strings")
2225 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2226 raise TypeError(
"Image file base must be a non-empty string")
2227 if not isinstance(image_path, str):
2228 raise TypeError(
"Image path must be a string")
2229 if not isinstance(frame, int):
2230 raise TypeError(
"Frame must be an integer")
2234 filename = radiation_wrapper.writeNormCameraImage(
2235 self.
radiation_model, camera, bands, imagefile_base, image_path, frame)
2237 logger.info(f
"Normalized camera image written to: {filename}")
2240 @require_plugin('radiation', 'write camera image data')
2242 image_path: str =
"./", frame: int = -1):
2244 Write camera image data to file (ASCII format).
2247 camera: Camera label
2249 imagefile_base: Base filename for output
2250 image_path: Output directory path (default: current directory)
2251 frame: Frame number to write (-1 for all frames)
2254 RadiationModelError: If camera image data writing fails
2255 TypeError: If parameters have incorrect types
2258 if not isinstance(camera, str)
or not camera.strip():
2259 raise TypeError(
"Camera label must be a non-empty string")
2260 if not isinstance(band, str)
or not band.strip():
2261 raise TypeError(
"Band label must be a non-empty string")
2262 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2263 raise TypeError(
"Image file base must be a non-empty string")
2264 if not isinstance(image_path, str):
2265 raise TypeError(
"Image path must be a string")
2266 if not isinstance(frame, int):
2267 raise TypeError(
"Frame must be an integer")
2270 radiation_wrapper.writeCameraImageData(
2271 self.
radiation_model, camera, band, imagefile_base, image_path, frame)
2273 logger.info(f
"Camera image data written for camera {camera}, band {band}")
2275 @require_plugin('radiation', 'write primitive data label map')
2277 image_path: str =
"./", frame: int = -1, padvalue: float = float(
'nan')):
2279 Write a per-pixel primitive-data label map for a camera to a text file.
2281 For each camera pixel, writes the value of ``primitive_data_label`` on the primitive
2282 seen at that pixel. Pixels that hit no geometry (or a primitive lacking the data) are
2283 written as ``padvalue`` (NaN by default). The primitive data must be of type
2284 float, double, uint, or int. The radiation model must have been run
2285 (``updateGeometry`` + ``runBand``) so that per-pixel primitive labels exist.
2287 The output file is written row-by-row (one line per image row), so it loads directly
2288 into a 2D ``(height, width)`` array. See :meth:`getPrimitiveDataLabelMap` for a
2289 convenience that returns a NumPy array instead of a file on disk.
2291 Output filename: ``{camera}_{imagefile_base}.txt`` when ``frame < 0`` (default), or
2292 ``{camera}_{imagefile_base}_{frame:05d}.txt`` when ``frame >= 0``.
2295 camera: Camera label
2296 primitive_data_label: Primitive data label to map (float/double/uint/int)
2297 imagefile_base: Base filename for output
2298 image_path: Output directory path (default: current directory)
2299 frame: Frame number to write (-1 to omit the frame suffix)
2300 padvalue: Value written for empty/background pixels (default: NaN)
2303 RadiationModelError: If the label map writing fails
2304 TypeError: If parameters have incorrect types
2307 >>> radiation.writePrimitiveDataLabelMap(
2308 ... camera="main_cam", primitive_data_label="leaf_id",
2309 ... imagefile_base="leaf_labels", image_path="./output")
2312 if not isinstance(camera, str)
or not camera.strip():
2313 raise TypeError(
"Camera label must be a non-empty string")
2314 if not isinstance(primitive_data_label, str)
or not primitive_data_label.strip():
2315 raise TypeError(
"Primitive data label must be a non-empty string")
2316 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2317 raise TypeError(
"Image file base must be a non-empty string")
2318 if not isinstance(image_path, str):
2319 raise TypeError(
"Image path must be a string")
2320 if not isinstance(frame, int):
2321 raise TypeError(
"Frame must be an integer")
2322 if not isinstance(padvalue, (int, float))
or isinstance(padvalue, bool):
2323 raise TypeError(
"Pad value must be a numeric type")
2326 radiation_wrapper.writePrimitiveDataLabelMap(
2328 image_path, frame, float(padvalue))
2330 logger.info(f
"Primitive data label map written for camera {camera}, label {primitive_data_label}")
2332 @require_plugin('radiation', 'write object data label map')
2334 image_path: str =
"./", frame: int = -1, padvalue: float = float(
'nan')):
2336 Write a per-pixel object-data label map for a camera to a text file.
2338 Identical to :meth:`writePrimitiveDataLabelMap` but maps the value of an object-data
2339 label (compound-object data) rather than primitive data. The object data must be of
2340 type float, double, uint, or int. The radiation model must have been run
2341 (``updateGeometry`` + ``runBand``) so that per-pixel labels exist.
2343 Output filename: ``{camera}_{imagefile_base}.txt`` when ``frame < 0`` (default), or
2344 ``{camera}_{imagefile_base}_{frame:05d}.txt`` when ``frame >= 0``.
2347 camera: Camera label
2348 object_data_label: Object data label to map (float/double/uint/int)
2349 imagefile_base: Base filename for output
2350 image_path: Output directory path (default: current directory)
2351 frame: Frame number to write (-1 to omit the frame suffix)
2352 padvalue: Value written for empty/background pixels (default: NaN)
2355 RadiationModelError: If the label map writing fails
2356 TypeError: If parameters have incorrect types
2359 if not isinstance(camera, str)
or not camera.strip():
2360 raise TypeError(
"Camera label must be a non-empty string")
2361 if not isinstance(object_data_label, str)
or not object_data_label.strip():
2362 raise TypeError(
"Object data label must be a non-empty string")
2363 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2364 raise TypeError(
"Image file base must be a non-empty string")
2365 if not isinstance(image_path, str):
2366 raise TypeError(
"Image path must be a string")
2367 if not isinstance(frame, int):
2368 raise TypeError(
"Frame must be an integer")
2369 if not isinstance(padvalue, (int, float))
or isinstance(padvalue, bool):
2370 raise TypeError(
"Pad value must be a numeric type")
2373 radiation_wrapper.writeObjectDataLabelMap(
2375 image_path, frame, float(padvalue))
2377 logger.info(f
"Object data label map written for camera {camera}, label {object_data_label}")
2379 @require_plugin('radiation', 'read primitive data label map')
2381 padvalue: float = float(
'nan')) ->
'np.ndarray':
2383 Return a per-pixel primitive-data label map for a camera as a NumPy array.
2385 Convenience wrapper around :meth:`writePrimitiveDataLabelMap` for the common use case
2386 of per-pixel masking in Python: the label map is written to a temporary file, loaded
2387 with ``numpy.loadtxt``, and returned as a 2D array. The file does not persist.
2390 camera: Camera label
2391 primitive_data_label: Primitive data label to map (float/double/uint/int)
2392 padvalue: Value used for empty/background pixels (default: NaN)
2395 2D NumPy array of shape ``(height, width)`` (row-major) holding the primitive-data
2396 value at each pixel, with ``padvalue`` (NaN by default) where no labelled geometry
2400 RadiationModelError: If the label map generation fails
2401 TypeError: If parameters have incorrect types
2404 >>> labels = radiation.getPrimitiveDataLabelMap("main_cam", "leaf_id")
2405 >>> mask = labels == 3 # per-pixel mask for primitive label 3
2406 >>> background = np.isnan(labels)
2408 with tempfile.TemporaryDirectory()
as tmpdir:
2409 imagefile_base =
"labelmap"
2411 camera, primitive_data_label, imagefile_base,
2412 image_path=os.path.join(tmpdir,
""), frame=-1, padvalue=padvalue)
2414 filepath = os.path.join(tmpdir, f
"{camera}_{imagefile_base}.txt")
2415 labels = np.loadtxt(filepath)
2418 if labels.ndim == 1:
2419 labels = labels.reshape(1, -1)
2422 @require_plugin('radiation', 'read object data label map')
2424 padvalue: float = float(
'nan')) ->
'np.ndarray':
2426 Return a per-pixel object-data label map for a camera as a NumPy array.
2428 Convenience wrapper around :meth:`writeObjectDataLabelMap`; see
2429 :meth:`getPrimitiveDataLabelMap` for behaviour. The label map is written to a temporary
2430 file, loaded with ``numpy.loadtxt``, and returned as a 2D ``(height, width)`` array with
2431 ``padvalue`` (NaN by default) for background pixels. The file does not persist.
2434 camera: Camera label
2435 object_data_label: Object data label to map (float/double/uint/int)
2436 padvalue: Value used for empty/background pixels (default: NaN)
2439 2D NumPy array of shape ``(height, width)`` (row-major).
2442 RadiationModelError: If the label map generation fails
2443 TypeError: If parameters have incorrect types
2445 with tempfile.TemporaryDirectory()
as tmpdir:
2446 imagefile_base =
"labelmap"
2448 camera, object_data_label, imagefile_base,
2449 image_path=os.path.join(tmpdir,
""), frame=-1, padvalue=padvalue)
2450 filepath = os.path.join(tmpdir, f
"{camera}_{imagefile_base}.txt")
2451 labels = np.loadtxt(filepath)
2453 if labels.ndim == 1:
2454 labels = labels.reshape(1, -1)
2457 @require_plugin('radiation', 'write image bounding boxes')
2459 primitive_data_labels=
None, object_data_labels=
None,
2460 object_class_ids=
None, image_file: str =
"",
2461 classes_txt_file: str =
"classes.txt",
2462 image_path: str =
"./"):
2464 Write image bounding boxes for object detection training.
2466 Supports both single and multiple data labels. Either provide primitive_data_labels
2467 or object_data_labels, not both.
2470 camera_label: Camera label
2471 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
2472 object_data_labels: Single object data label (str) or list of object data labels
2473 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
2474 image_file: Image filename
2475 classes_txt_file: Classes definition file (default: "classes.txt")
2476 image_path: Image output path (default: current directory)
2479 RadiationModelError: If bounding box writing fails
2480 TypeError: If parameters have incorrect types
2481 ValueError: If both primitive and object data labels are provided, or neither
2484 if primitive_data_labels
is not None and object_data_labels
is not None:
2485 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
2486 if primitive_data_labels
is None and object_data_labels
is None:
2487 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
2490 if not isinstance(camera_label, str)
or not camera_label.strip():
2491 raise TypeError(
"Camera label must be a non-empty string")
2492 if not isinstance(image_file, str)
or not image_file.strip():
2493 raise TypeError(
"Image file must be a non-empty string")
2494 if not isinstance(classes_txt_file, str):
2495 raise TypeError(
"Classes txt file must be a string")
2496 if not isinstance(image_path, str):
2497 raise TypeError(
"Image path must be a string")
2500 if primitive_data_labels
is not None:
2501 if isinstance(primitive_data_labels, str):
2503 if not isinstance(object_class_ids, int):
2504 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
2506 radiation_wrapper.writeImageBoundingBoxes(
2508 object_class_ids, image_file, classes_txt_file, image_path)
2509 logger.info(f
"Image bounding boxes written for primitive data: {primitive_data_labels}")
2511 elif isinstance(primitive_data_labels, list):
2513 if not isinstance(object_class_ids, list):
2514 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
2515 if len(primitive_data_labels) != len(object_class_ids):
2516 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
2517 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
2518 raise TypeError(
"All primitive data labels must be non-empty strings")
2519 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2520 raise TypeError(
"All object class IDs must be integers")
2523 radiation_wrapper.writeImageBoundingBoxesVector(
2525 object_class_ids, image_file, classes_txt_file, image_path)
2526 logger.info(f
"Image bounding boxes written for {len(primitive_data_labels)} primitive data labels")
2528 raise TypeError(
"primitive_data_labels must be a string or list of strings")
2531 elif object_data_labels
is not None:
2532 if isinstance(object_data_labels, str):
2534 if not isinstance(object_class_ids, int):
2535 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2537 radiation_wrapper.writeImageBoundingBoxes_ObjectData(
2539 object_class_ids, image_file, classes_txt_file, image_path)
2540 logger.info(f
"Image bounding boxes written for object data: {object_data_labels}")
2542 elif isinstance(object_data_labels, list):
2544 if not isinstance(object_class_ids, list):
2545 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2546 if len(object_data_labels) != len(object_class_ids):
2547 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2548 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2549 raise TypeError(
"All object data labels must be non-empty strings")
2550 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2551 raise TypeError(
"All object class IDs must be integers")
2554 radiation_wrapper.writeImageBoundingBoxes_ObjectDataVector(
2556 object_class_ids, image_file, classes_txt_file, image_path)
2557 logger.info(f
"Image bounding boxes written for {len(object_data_labels)} object data labels")
2559 raise TypeError(
"object_data_labels must be a string or list of strings")
2561 @require_plugin('radiation', 'write image segmentation masks')
2563 primitive_data_labels=
None, object_data_labels=
None,
2564 object_class_ids=
None, json_filename: str =
"",
2565 image_file: str =
"", append_file: bool =
False):
2567 Write image segmentation masks in COCO JSON format.
2569 Supports both single and multiple data labels. Either provide primitive_data_labels
2570 or object_data_labels, not both.
2573 camera_label: Camera label
2574 primitive_data_labels: Single primitive data label (str) or list of primitive data labels
2575 object_data_labels: Single object data label (str) or list of object data labels
2576 object_class_ids: Single class ID (int) or list of class IDs (must match data labels)
2577 json_filename: JSON output filename
2578 image_file: Image filename
2579 append_file: Whether to append to existing JSON file
2582 RadiationModelError: If segmentation mask writing fails
2583 TypeError: If parameters have incorrect types
2584 ValueError: If both primitive and object data labels are provided, or neither
2587 if primitive_data_labels
is not None and object_data_labels
is not None:
2588 raise ValueError(
"Cannot specify both primitive_data_labels and object_data_labels")
2589 if primitive_data_labels
is None and object_data_labels
is None:
2590 raise ValueError(
"Must specify either primitive_data_labels or object_data_labels")
2593 if not isinstance(camera_label, str)
or not camera_label.strip():
2594 raise TypeError(
"Camera label must be a non-empty string")
2595 if not isinstance(json_filename, str)
or not json_filename.strip():
2596 raise TypeError(
"JSON filename must be a non-empty string")
2597 if not isinstance(image_file, str)
or not image_file.strip():
2598 raise TypeError(
"Image file must be a non-empty string")
2599 if not isinstance(append_file, bool):
2600 raise TypeError(
"append_file must be a boolean")
2603 if primitive_data_labels
is not None:
2604 if isinstance(primitive_data_labels, str):
2606 if not isinstance(object_class_ids, int):
2607 raise TypeError(
"For single primitive data label, object_class_ids must be an integer")
2609 radiation_wrapper.writeImageSegmentationMasks(
2611 object_class_ids, json_filename, image_file, append_file)
2612 logger.info(f
"Image segmentation masks written for primitive data: {primitive_data_labels}")
2614 elif isinstance(primitive_data_labels, list):
2616 if not isinstance(object_class_ids, list):
2617 raise TypeError(
"For multiple primitive data labels, object_class_ids must be a list")
2618 if len(primitive_data_labels) != len(object_class_ids):
2619 raise ValueError(
"primitive_data_labels and object_class_ids must have the same length")
2620 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in primitive_data_labels):
2621 raise TypeError(
"All primitive data labels must be non-empty strings")
2622 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2623 raise TypeError(
"All object class IDs must be integers")
2626 radiation_wrapper.writeImageSegmentationMasksVector(
2628 object_class_ids, json_filename, image_file, append_file)
2629 logger.info(f
"Image segmentation masks written for {len(primitive_data_labels)} primitive data labels")
2631 raise TypeError(
"primitive_data_labels must be a string or list of strings")
2634 elif object_data_labels
is not None:
2635 if isinstance(object_data_labels, str):
2637 if not isinstance(object_class_ids, int):
2638 raise TypeError(
"For single object data label, object_class_ids must be an integer")
2640 radiation_wrapper.writeImageSegmentationMasks_ObjectData(
2642 object_class_ids, json_filename, image_file, append_file)
2643 logger.info(f
"Image segmentation masks written for object data: {object_data_labels}")
2645 elif isinstance(object_data_labels, list):
2647 if not isinstance(object_class_ids, list):
2648 raise TypeError(
"For multiple object data labels, object_class_ids must be a list")
2649 if len(object_data_labels) != len(object_class_ids):
2650 raise ValueError(
"object_data_labels and object_class_ids must have the same length")
2651 if not all(isinstance(lbl, str)
and lbl.strip()
for lbl
in object_data_labels):
2652 raise TypeError(
"All object data labels must be non-empty strings")
2653 if not all(isinstance(cid, int)
for cid
in object_class_ids):
2654 raise TypeError(
"All object class IDs must be integers")
2657 radiation_wrapper.writeImageSegmentationMasks_ObjectDataVector(
2659 object_class_ids, json_filename, image_file, append_file)
2660 logger.info(f
"Image segmentation masks written for {len(object_data_labels)} object data labels")
2662 raise TypeError(
"object_data_labels must be a string or list of strings")
2664 @require_plugin('radiation', 'auto-calibrate camera image')
2666 green_band_label: str, blue_band_label: str,
2667 output_file_path: str, print_quality_report: bool =
False,
2668 algorithm: str =
"MATRIX_3X3_AUTO",
2669 ccm_export_file_path: str =
"") -> str:
2671 Auto-calibrate camera image with color correction and return output filename.
2674 camera_label: Camera label
2675 red_band_label: Red band label
2676 green_band_label: Green band label
2677 blue_band_label: Blue band label
2678 output_file_path: Output file path
2679 print_quality_report: Whether to print quality report
2680 algorithm: Color correction algorithm ("DIAGONAL_ONLY", "MATRIX_3X3_AUTO", "MATRIX_3X3_FORCE")
2681 ccm_export_file_path: Path to export color correction matrix (optional)
2684 Output filename string
2687 RadiationModelError: If auto-calibration fails
2688 TypeError: If parameters have incorrect types
2689 ValueError: If algorithm is not valid
2692 if not isinstance(camera_label, str)
or not camera_label.strip():
2693 raise TypeError(
"Camera label must be a non-empty string")
2694 if not isinstance(red_band_label, str)
or not red_band_label.strip():
2695 raise TypeError(
"Red band label must be a non-empty string")
2696 if not isinstance(green_band_label, str)
or not green_band_label.strip():
2697 raise TypeError(
"Green band label must be a non-empty string")
2698 if not isinstance(blue_band_label, str)
or not blue_band_label.strip():
2699 raise TypeError(
"Blue band label must be a non-empty string")
2700 if not isinstance(output_file_path, str)
or not output_file_path.strip():
2701 raise TypeError(
"Output file path must be a non-empty string")
2702 if not isinstance(print_quality_report, bool):
2703 raise TypeError(
"print_quality_report must be a boolean")
2704 if not isinstance(ccm_export_file_path, str):
2705 raise TypeError(
"ccm_export_file_path must be a string")
2710 "MATRIX_3X3_AUTO": 1,
2711 "MATRIX_3X3_FORCE": 2
2714 if algorithm
not in algorithm_map:
2715 raise ValueError(f
"Invalid algorithm: {algorithm}. Must be one of: {list(algorithm_map.keys())}")
2717 algorithm_int = algorithm_map[algorithm]
2720 filename = radiation_wrapper.autoCalibrateCameraImage(
2722 blue_band_label, output_file_path, print_quality_report,
2723 algorithm_int, ccm_export_file_path)
2725 logger.info(f
"Auto-calibrated camera image written to: {filename}")
2729 """Get information about the radiation plugin."""
2730 registry = get_plugin_registry()
2731 return registry.get_plugin_capabilities(
'radiation')
2738 image_path: str =
"./", frame: int = -1):
2740 Write camera pixel data to an EXR file with lossless float compression.
2742 Preserves full floating-point precision unlike JPEG/PNG exports.
2745 camera: Camera label
2746 band: Band label (str) for single-band, or list of band labels for multi-band
2747 imagefile_base: Base filename for output
2748 image_path: Output directory path (default: current directory)
2749 frame: Frame number to append to filename (-1 to omit)
2752 RadiationModelError: If writing fails
2753 TypeError: If parameters have incorrect types
2755 if not isinstance(camera, str)
or not camera.strip():
2756 raise TypeError(
"Camera label must be a non-empty string")
2757 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2758 raise TypeError(
"Image file base must be a non-empty string")
2759 if not isinstance(image_path, str):
2760 raise TypeError(
"Image path must be a string")
2761 if not isinstance(frame, int):
2762 raise TypeError(
"Frame must be an integer")
2764 if isinstance(band, str):
2765 if not band.strip():
2766 raise TypeError(
"Band label must be a non-empty string")
2767 elif isinstance(band, (list, tuple)):
2769 raise ValueError(
"Band list cannot be empty")
2771 if not isinstance(b, str)
or not b.strip():
2772 raise TypeError(
"Each band label must be a non-empty string")
2774 raise TypeError(
"band must be a string or list of strings")
2777 radiation_wrapper.writeCameraImageDataEXR(
2778 self.
radiation_model, camera, band, imagefile_base, image_path, frame)
2781 image_path: str =
"./", frame: int = -1):
2783 Write depth image data to an ASCII text file.
2786 camera_label: Camera label
2787 imagefile_base: Base filename for output
2788 image_path: Output directory path (default: current directory)
2789 frame: Frame number to append to filename (-1 to omit)
2792 RadiationModelError: If writing fails
2793 TypeError: If parameters have incorrect types
2795 if not isinstance(camera_label, str)
or not camera_label.strip():
2796 raise TypeError(
"Camera label must be a non-empty string")
2797 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2798 raise TypeError(
"Image file base must be a non-empty string")
2799 if not isinstance(image_path, str):
2800 raise TypeError(
"Image path must be a string")
2801 if not isinstance(frame, int):
2802 raise TypeError(
"Frame must be an integer")
2805 radiation_wrapper.writeDepthImageData(
2806 self.
radiation_model, camera_label, imagefile_base, image_path, frame)
2809 image_path: str =
"./", frame: int = -1):
2811 Write depth image data to an EXR file with lossless float compression.
2813 Preserves full floating-point depth precision unlike ASCII or JPEG exports.
2816 camera_label: Camera label
2817 imagefile_base: Base filename for output
2818 image_path: Output directory path (default: current directory)
2819 frame: Frame number to append to filename (-1 to omit)
2822 RadiationModelError: If writing fails
2823 TypeError: If parameters have incorrect types
2825 if not isinstance(camera_label, str)
or not camera_label.strip():
2826 raise TypeError(
"Camera label must be a non-empty string")
2827 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2828 raise TypeError(
"Image file base must be a non-empty string")
2829 if not isinstance(image_path, str):
2830 raise TypeError(
"Image path must be a string")
2831 if not isinstance(frame, int):
2832 raise TypeError(
"Frame must be an integer")
2835 radiation_wrapper.writeDepthImageDataEXR(
2836 self.
radiation_model, camera_label, imagefile_base, image_path, frame)
2839 image_path: str =
"./", frame: int = -1):
2841 Write normalized depth image as grayscale JPEG.
2843 Depth values are normalized to the range [0, max_depth] for visualization.
2846 camera_label: Camera label
2847 imagefile_base: Base filename for output
2848 max_depth: Maximum depth value for normalization (e.g., sky depth)
2849 image_path: Output directory path (default: current directory)
2850 frame: Frame number to append to filename (-1 to omit)
2853 RadiationModelError: If writing fails
2854 TypeError: If parameters have incorrect types
2855 ValueError: If max_depth is not positive
2857 if not isinstance(camera_label, str)
or not camera_label.strip():
2858 raise TypeError(
"Camera label must be a non-empty string")
2859 if not isinstance(imagefile_base, str)
or not imagefile_base.strip():
2860 raise TypeError(
"Image file base must be a non-empty string")
2861 if not isinstance(max_depth, (int, float)):
2862 raise TypeError(
"max_depth must be a number")
2864 raise ValueError(
"max_depth must be positive")
2865 if not isinstance(image_path, str):
2866 raise TypeError(
"Image path must be a string")
2867 if not isinstance(frame, int):
2868 raise TypeError(
"Frame must be an integer")
2871 radiation_wrapper.writeNormDepthImage(
2872 self.
radiation_model, camera_label, imagefile_base, float(max_depth), image_path, frame)
2880 Get the name of the active ray tracing backend.
2883 Backend name string (e.g., "OptiX 8.1", "Vulkan Compute")
2891 Probe whether any compiled-in GPU backend is available on this system.
2893 Probes backends in priority order (OptiX 8 -> OptiX 6 -> Vulkan) without
2894 constructing a full backend. Useful for checking GPU availability before
2895 creating a RadiationModel.
2898 True if at least one GPU backend is available
2900 return radiation_wrapper.probeAnyGPUBackend()