2High-level LeafOptics interface for PyHelios.
4This module provides a user-friendly interface to the PROSPECT leaf optical model
5for computing spectral reflectance and transmittance of plant leaves.
10from contextlib
import contextmanager
11from dataclasses
import dataclass, field
12from pathlib
import Path
13from typing
import List, Optional, Tuple
15from .plugins.registry
import get_plugin_registry
16from .wrappers
import ULeafOpticsWrapper
as leafoptics_wrapper
17from .Context
import Context
18from .exceptions
import HeliosError
19from .assets
import get_asset_manager
21logger = logging.getLogger(__name__)
25 """Exception raised for LeafOptics-specific errors."""
49 Data class for PROSPECT leaf optical model parameters.
51 These parameters define the physical and biochemical properties of a leaf
52 that determine its spectral reflectance and transmittance.
55 numberlayers: Number of mesophyll layers in the leaf (typically 1-3)
56 brownpigments: Brown pigment content (arbitrary units, typically 0)
57 chlorophyllcontent: Chlorophyll a+b content in micrograms per square cm
58 carotenoidcontent: Carotenoid content in micrograms per square cm
59 anthocyancontent: Anthocyanin content in micrograms per square cm
60 watermass: Equivalent water thickness in grams per square cm
61 drymass: Dry matter content (leaf mass per area) in grams per square cm
62 protein: Protein content in grams per square cm (PROSPECT-PRO mode)
63 carbonconstituents: Carbon-based constituents in grams per square cm (PROSPECT-PRO mode)
64 V2Z: Violaxanthin-to-zeaxanthin de-epoxidation state, [0, 1]. Used by the
65 radiation plugin's solar-induced fluorescence (SIF) pipeline; ignored by
66 the pure PROSPECT reflectance/transmittance calculation. Default 0
67 (dark-adapted, all violaxanthin).
68 fqe: Intrinsic fluorescence quantum efficiency scalar applied on top of the
69 per-leaf Phi_F at SIF emission time (radiation plugin only). Ignored by
70 PROSPECT. Default 1.0.
73 - If protein > 0 OR carbonconstituents > 0, the model uses PROSPECT-PRO mode
74 which ignores drymass and uses protein + carbonconstituents instead.
75 - Otherwise, the model uses PROSPECT-D mode which uses drymass.
76 - V2Z and fqe are inert unless the radiation plugin's SIF camera is in use.
78 numberlayers: float = 1.5
79 brownpigments: float = 0.0
80 chlorophyllcontent: float = 30.0
81 carotenoidcontent: float = 7.0
82 anthocyancontent: float = 1.0
83 watermass: float = 0.015
86 carbonconstituents: float = 0.0
90 def to_list(self) -> List[float]:
91 """Convert properties to a list in the order expected by the C++ interface."""
107 def from_list(cls, values: List[float]) ->
'LeafOpticsProperties':
108 """Create LeafOpticsProperties from a list of values.
110 Accepts the legacy 9-float layout (without V2Z/fqe) for backward
111 compatibility with serialized data; missing fields fall back to
112 defaults and a ``DeprecationWarning`` is emitted so callers can
113 migrate to the 11-element layout.
115 if len(values)
not in (9, 11):
116 raise ValueError(f
"Expected 9 or 11 values, got {len(values)}")
120 "LeafOpticsProperties.from_list received a 9-element legacy "
121 "array (pre-helios-core 1.3.72). V2Z and fqe will default to "
122 "0.0 and 1.0 respectively. Migrate serialized data to the "
123 "11-element layout to silence this warning.",
128 numberlayers=values[0],
129 brownpigments=values[1],
130 chlorophyllcontent=values[2],
131 carotenoidcontent=values[3],
132 anthocyancontent=values[4],
136 carbonconstituents=values[8],
137 V2Z=values[9]
if len(values) > 9
else 0.0,
138 fqe=values[10]
if len(values) > 10
else 1.0,
145 Context manager that temporarily changes working directory to where LeafOptics assets are located.
147 LeafOptics C++ code uses relative paths like "plugins/leafoptics/spectral_data/"
148 expecting assets relative to working directory. This manager temporarily changes to the build
149 directory where assets are actually located.
152 asset_manager = get_asset_manager()
153 working_dir = asset_manager._get_helios_build_path()
155 if working_dir
and working_dir.exists():
156 leafoptics_assets = working_dir /
'plugins' /
'leafoptics' /
'spectral_data'
159 current_dir = Path(__file__).parent
160 repo_root = current_dir.parent
161 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
162 working_dir = build_lib_dir.parent
163 leafoptics_assets = working_dir /
'plugins' /
'leafoptics' /
'spectral_data'
165 if not build_lib_dir.exists():
167 f
"PyHelios build directory not found at {build_lib_dir}. "
168 f
"LeafOptics requires native libraries to be built. "
169 f
"Run: build_scripts/build_helios --plugins leafoptics"
173 spectral_file = leafoptics_assets /
'prospect_spectral_library.xml'
174 if not spectral_file.exists():
176 f
"LeafOptics spectral data not found at {spectral_file}. "
177 f
"Build system failed to copy LeafOptics assets. "
178 f
"Run: build_scripts/build_helios --clean --plugins leafoptics"
182 file_size = spectral_file.stat().st_size
183 if file_size < 400000:
185 f
"LeafOptics spectral data file appears corrupted (size: {file_size} bytes, expected ~475KB). "
186 f
"Run: build_scripts/build_helios --clean --plugins leafoptics"
190 original_dir = os.getcwd()
192 os.chdir(working_dir)
193 logger.debug(f
"Changed working directory to {working_dir} for LeafOptics asset access")
196 os.chdir(original_dir)
197 logger.debug(f
"Restored working directory to {original_dir}")
202 High-level interface for PROSPECT leaf optical model.
204 This class provides a user-friendly wrapper around the native Helios
205 LeafOptics plugin for computing spectral reflectance and transmittance
206 of plant leaves based on their biochemical properties.
208 The PROSPECT model computes spectral optical properties for wavelengths
209 from 400 nm to 2500 nm at 1 nm resolution (2101 data points).
212 - Cross-platform support (Windows, Linux, macOS)
214 - Requires spectral_data assets (~475KB XML file)
215 - LeafOptics plugin compiled into PyHelios
218 >>> from pyhelios import Context, LeafOptics, LeafOpticsProperties
220 >>> with Context() as context:
221 ... with LeafOptics(context) as leafoptics:
222 ... # Get properties for a known species
223 ... props = leafoptics.getPropertiesFromLibrary("sunflower")
224 ... print(f"Sunflower chlorophyll: {props.chlorophyllcontent} ug/cm^2")
226 ... # Compute spectra
227 ... wavelengths, reflectance, transmittance = leafoptics.getLeafSpectra(props)
228 ... print(f"Spectral range: {wavelengths[0]}-{wavelengths[-1]} nm")
230 ... # Apply to geometry
231 ... leaf_uuid = context.addPatch(center=[0, 0, 1], size=[0.1, 0.1])
232 ... leafoptics.run([leaf_uuid], props, "sunflower_leaf")
235 def __init__(self, context: Context):
237 Initialize LeafOptics with graceful plugin handling.
240 context: Helios Context instance
243 TypeError: If context is not a Context instance
244 LeafOpticsError: If LeafOptics plugin is not available or spectral data missing
247 if not (hasattr(context,
'__class__')
and
248 (isinstance(context, Context)
or
249 context.__class__.__name__ ==
'Context')):
250 raise TypeError(f
"LeafOptics requires a Context instance, got {type(context).__name__}")
252 self.context = context
253 self._leafoptics_ptr =
None
256 registry = get_plugin_registry()
258 if not registry.is_plugin_available(
'leafoptics'):
260 available_plugins = registry.get_available_plugins()
263 "LeafOptics requires the 'leafoptics' plugin which is not available.\n\n"
264 "The LeafOptics plugin implements the PROSPECT leaf optical model which computes:\n"
265 "- Spectral reflectance (400-2500 nm at 1 nm resolution)\n"
266 "- Spectral transmittance\n"
267 "- Based on leaf biochemical properties (chlorophyll, water, dry matter, etc.)\n\n"
269 "- Cross-platform support (Windows, Linux, macOS)\n"
270 "- No GPU required\n"
271 "- Built-in species library with 12 plant species\n"
272 "- Supports both PROSPECT-D and PROSPECT-PRO modes\n\n"
273 "To enable LeafOptics modeling:\n"
274 "1. Build PyHelios with LeafOptics plugin:\n"
275 " build_scripts/build_helios --plugins leafoptics\n"
276 "2. Or build with radiation plugins for full spectral modeling:\n"
277 " build_scripts/build_helios --plugins leafoptics,radiation\n"
278 f
"\nCurrently available plugins: {available_plugins}"
286 self.
_leafoptics_ptr = leafoptics_wrapper.createLeafOptics(context.getNativePtr())
289 "Failed to create LeafOptics instance. "
290 "This may indicate a problem with the spectral data files."
292 logger.info(
"LeafOptics created successfully")
294 except LeafOpticsError:
296 except Exception
as e:
300 """Context manager entry."""
303 def __exit__(self, exc_type, exc_value, traceback):
304 """Context manager exit with proper cleanup."""
308 logger.debug(
"LeafOptics destroyed successfully")
309 except Exception
as e:
310 logger.warning(f
"Error destroying LeafOptics: {e}")
315 """Destructor to ensure C++ resources freed even without 'with' statement."""
316 if hasattr(self,
'_leafoptics_ptr')
and self.
_leafoptics_ptr is not None:
320 except Exception
as e:
322 warnings.warn(f
"Error in LeafOptics.__del__: {e}")
324 def run(self, UUIDs: List[int], leafproperties: LeafOpticsProperties, label: str) ->
None:
326 Run the LeafOptics model to generate spectra and assign to primitives.
328 This method computes reflectance and transmittance spectra based on the given
329 leaf properties, creates global data entries, and assigns them to the specified
333 UUIDs: List of primitive UUIDs to assign spectra to
334 leafproperties: LeafOpticsProperties with biochemical parameters
335 label: Label for the spectra (appended to "leaf_reflectivity_" and "leaf_transmissivity_")
338 ValueError: If parameters are invalid
339 LeafOpticsError: If computation fails
342 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0, watermass=0.02)
343 >>> leafoptics.run([leaf_uuid], props, "my_leaf")
344 >>> # Creates: "leaf_reflectivity_my_leaf" and "leaf_transmissivity_my_leaf"
347 raise ValueError(
"UUIDs list cannot be empty")
348 if not isinstance(leafproperties, LeafOpticsProperties):
349 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
351 raise ValueError(
"Label cannot be empty")
354 leafoptics_wrapper.leafOpticsRun(
357 leafproperties.to_list(),
360 except Exception
as e:
363 def runNoUUIDs(self, leafproperties: LeafOpticsProperties, label: str) ->
None:
365 Run the LeafOptics model without assigning to primitives.
367 This method computes reflectance and transmittance spectra based on the given
368 leaf properties and creates global data entries, but does not assign them to
372 leafproperties: LeafOpticsProperties with biochemical parameters
373 label: Label for the spectra (appended to "leaf_reflectivity_" and "leaf_transmissivity_")
376 ValueError: If parameters are invalid
377 LeafOpticsError: If computation fails
379 if not isinstance(leafproperties, LeafOpticsProperties):
380 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
382 raise ValueError(
"Label cannot be empty")
385 leafoptics_wrapper.leafOpticsRunNoUUIDs(
387 leafproperties.to_list(),
390 except Exception
as e:
393 def getLeafSpectra(self, leafproperties: LeafOpticsProperties) -> Tuple[List[float], List[float], List[float]]:
395 Compute leaf reflectance and transmittance spectra.
397 This method computes spectral properties without creating global data entries
398 or assigning to primitives.
401 leafproperties: LeafOpticsProperties with biochemical parameters
404 Tuple of (wavelengths, reflectivities, transmissivities):
405 - wavelengths: List of wavelengths in nm (400-2500 at 1nm resolution, 2101 points)
406 - reflectivities: List of reflectance values (0-1) at each wavelength
407 - transmissivities: List of transmittance values (0-1) at each wavelength
410 ValueError: If parameters are invalid
411 LeafOpticsError: If computation fails
414 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0)
415 >>> wavelengths, refl, trans = leafoptics.getLeafSpectra(props)
416 >>> # Find reflectance at 550 nm (green peak)
417 >>> idx_550 = wavelengths.index(550.0)
418 >>> print(f"Reflectance at 550 nm: {refl[idx_550]:.3f}")
420 if not isinstance(leafproperties, LeafOpticsProperties):
421 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
424 return leafoptics_wrapper.leafOpticsGetLeafSpectra(
426 leafproperties.to_list()
428 except Exception
as e:
431 def setProperties(self, UUIDs: List[int], leafproperties: LeafOpticsProperties) ->
None:
433 Set leaf optical properties for primitives as Context primitive data.
435 This assigns the biochemical properties as primitive data using labels:
436 "chlorophyll", "carotenoid", "anthocyanin", "brown", "water", "drymass"
437 (or "protein" + "cellulose" in PROSPECT-PRO mode).
440 UUIDs: List of primitive UUIDs
441 leafproperties: LeafOpticsProperties with biochemical parameters
444 ValueError: If parameters are invalid
445 LeafOpticsError: If operation fails
448 raise ValueError(
"UUIDs list cannot be empty")
449 if not isinstance(leafproperties, LeafOpticsProperties):
450 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
453 leafoptics_wrapper.leafOpticsSetProperties(
456 leafproperties.to_list()
458 except Exception
as e:
463 Get PROSPECT parameters from reflectivity spectrum for primitives.
465 This method retrieves the "reflectivity_spectrum" primitive data for each
466 primitive and checks if it matches a spectrum generated by this LeafOptics
467 instance. If a match is found, the corresponding PROSPECT model parameters
468 are assigned as primitive data.
471 UUIDs: List of primitive UUIDs to query
474 Primitives without matching spectra are silently skipped.
477 raise ValueError(
"UUIDs list cannot be empty")
480 leafoptics_wrapper.leafOpticsGetPropertiesFromSpectrum(
484 except Exception
as e:
489 Get leaf optical properties from the built-in species library.
491 The library contains PROSPECT-D parameters fitted from the LOPEX93 dataset
492 for common plant species.
495 species: Name of the species (case-insensitive). Available species:
496 "default", "garden_lettuce", "alfalfa", "corn", "sunflower",
497 "english_walnut", "rice", "soybean", "wine_grape", "tomato",
498 "common_bean", "cowpea"
501 LeafOpticsProperties populated with the species-specific parameters
504 ValueError: If species name is empty
507 If species is not found, default properties are used and a warning is issued.
510 >>> props = leafoptics.getPropertiesFromLibrary("sunflower")
511 >>> print(f"Chlorophyll: {props.chlorophyllcontent} ug/cm^2")
514 raise ValueError(
"Species name cannot be empty")
517 properties_list = leafoptics_wrapper.leafOpticsGetPropertiesFromLibrary(
521 return LeafOpticsProperties.from_list(properties_list)
522 except Exception
as e:
526 """Disable command-line output messages from LeafOptics."""
529 except Exception
as e:
533 """Enable command-line output messages from LeafOptics."""
536 except Exception
as e:
541 Selectively output specific biochemical properties as primitive data.
543 By default, LeafOptics writes all biochemical properties to primitive data.
544 Use this method to specify only the properties you need for improved performance.
547 label: Biochemical property to output. Valid values:
548 - "chlorophyll": Chlorophyll content
549 - "carotenoid": Carotenoid content
550 - "anthocyanin": Anthocyanin content
551 - "brown": Brown pigment content
552 - "water": Water content
553 - "drymass": Dry mass content
554 - "protein": Protein content
555 - "cellulose": Cellulose content
558 ValueError: If label is empty or invalid
559 LeafOpticsError: If operation fails
562 Added in helios-core v1.3.59 for performance optimization when only
563 specific biochemical properties are needed for analysis.
566 >>> with LeafOptics(context) as leaf:
567 ... # Only output chlorophyll and water content
568 ... leaf.optionalOutputPrimitiveData("chlorophyll")
569 ... leaf.optionalOutputPrimitiveData("water")
570 ... leaf.run(uuids, properties, "leaf_spectra")
573 raise ValueError(
"Label cannot be empty")
575 valid_labels = [
"chlorophyll",
"carotenoid",
"anthocyanin",
"brown",
576 "water",
"drymass",
"protein",
"cellulose"]
577 if label
not in valid_labels:
578 raise ValueError(f
"Invalid label '{label}'. Must be one of: {', '.join(valid_labels)}")
581 leafoptics_wrapper.leafOpticsOptionalOutputPrimitiveData(self.
_leafoptics_ptr, label)
582 except Exception
as e:
583 raise LeafOpticsError(f
"Failed to set optional output for '{label}': {e}")
588 Get list of available species in the built-in library.
591 List of species names that can be used with getPropertiesFromLibrary()
593 return AVAILABLE_SPECIES.copy()
598 Check if LeafOptics plugin is available in the current build.
601 True if LeafOptics is available, False otherwise
603 return leafoptics_wrapper.is_leafoptics_available()
Exception raised for LeafOptics-specific errors.
Data class for PROSPECT leaf optical model parameters.
float anthocyancontent
Anthocyanin content in micrograms per square cm.
float watermass
Equivalent water thickness in grams per square cm.
float drymass
Dry matter content (leaf mass per area) in grams per square cm.
float numberlayers
Number of mesophyll layers in the leaf (typically 1-3)
float carbonconstituents
Carbon-based constituents in grams per square cm (PROSPECT-PRO mode)
float protein
Protein content in grams per square cm (PROSPECT-PRO mode)
float carotenoidcontent
Carotenoid content in micrograms per square cm.
'LeafOpticsProperties' from_list(cls, List[float] values)
Create LeafOpticsProperties from a list of values.
float brownpigments
Brown pigment content (arbitrary units, typically 0)
float chlorophyllcontent
Chlorophyll a+b content in micrograms per square cm.
List[float] to_list(self)
Convert properties to a list in the order expected by the C++ interface.
float V2Z
Violaxanthin-to-zeaxanthin de-epoxidation state, [0, 1].
float fqe
Intrinsic fluorescence quantum efficiency scalar applied on top of the.
High-level interface for PROSPECT leaf optical model.
None setProperties(self, List[int] UUIDs, LeafOpticsProperties leafproperties)
Set leaf optical properties for primitives as Context primitive data.
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
List[str] getAvailableSpecies()
Get list of available species in the built-in library.
LeafOpticsProperties getPropertiesFromLibrary(self, str species)
Get leaf optical properties from the built-in species library.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
None run(self, List[int] UUIDs, LeafOpticsProperties leafproperties, str label)
Run the LeafOptics model to generate spectra and assign to primitives.
Tuple[List[float], List[float], List[float]] getLeafSpectra(self, LeafOpticsProperties leafproperties)
Compute leaf reflectance and transmittance spectra.
None disableMessages(self)
Disable command-line output messages from LeafOptics.
None optionalOutputPrimitiveData(self, str label)
Selectively output specific biochemical properties as primitive data.
bool isAvailable()
Check if LeafOptics plugin is available in the current build.
None runNoUUIDs(self, LeafOpticsProperties leafproperties, str label)
Run the LeafOptics model without assigning to primitives.
None enableMessages(self)
Enable command-line output messages from LeafOptics.
__enter__(self)
Context manager entry.
None getPropertiesFromSpectrum(self, List[int] UUIDs)
Get PROSPECT parameters from reflectivity spectrum for primitives.
Exception classes for PyHelios library.
_leafoptics_working_directory()
Context manager that temporarily changes working directory to where LeafOptics assets are located.