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)
66 - If protein > 0 OR carbonconstituents > 0, the model uses PROSPECT-PRO mode
67 which ignores drymass and uses protein + carbonconstituents instead.
68 - Otherwise, the model uses PROSPECT-D mode which uses drymass.
70 numberlayers: float = 1.5
71 brownpigments: float = 0.0
72 chlorophyllcontent: float = 30.0
73 carotenoidcontent: float = 7.0
74 anthocyancontent: float = 1.0
75 watermass: float = 0.015
78 carbonconstituents: float = 0.0
80 def to_list(self) -> List[float]:
81 """Convert properties to a list in the order expected by the C++ interface."""
95 def from_list(cls, values: List[float]) ->
'LeafOpticsProperties':
96 """Create LeafOpticsProperties from a list of values."""
98 raise ValueError(f
"Expected 9 values, got {len(values)}")
100 numberlayers=values[0],
101 brownpigments=values[1],
102 chlorophyllcontent=values[2],
103 carotenoidcontent=values[3],
104 anthocyancontent=values[4],
108 carbonconstituents=values[8]
115 Context manager that temporarily changes working directory to where LeafOptics assets are located.
117 LeafOptics C++ code uses relative paths like "plugins/leafoptics/spectral_data/"
118 expecting assets relative to working directory. This manager temporarily changes to the build
119 directory where assets are actually located.
122 asset_manager = get_asset_manager()
123 working_dir = asset_manager._get_helios_build_path()
125 if working_dir
and working_dir.exists():
126 leafoptics_assets = working_dir /
'plugins' /
'leafoptics' /
'spectral_data'
129 current_dir = Path(__file__).parent
130 repo_root = current_dir.parent
131 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
132 working_dir = build_lib_dir.parent
133 leafoptics_assets = working_dir /
'plugins' /
'leafoptics' /
'spectral_data'
135 if not build_lib_dir.exists():
137 f
"PyHelios build directory not found at {build_lib_dir}. "
138 f
"LeafOptics requires native libraries to be built. "
139 f
"Run: build_scripts/build_helios --plugins leafoptics"
143 spectral_file = leafoptics_assets /
'prospect_spectral_library.xml'
144 if not spectral_file.exists():
146 f
"LeafOptics spectral data not found at {spectral_file}. "
147 f
"Build system failed to copy LeafOptics assets. "
148 f
"Run: build_scripts/build_helios --clean --plugins leafoptics"
152 file_size = spectral_file.stat().st_size
153 if file_size < 400000:
155 f
"LeafOptics spectral data file appears corrupted (size: {file_size} bytes, expected ~475KB). "
156 f
"Run: build_scripts/build_helios --clean --plugins leafoptics"
160 original_dir = os.getcwd()
162 os.chdir(working_dir)
163 logger.debug(f
"Changed working directory to {working_dir} for LeafOptics asset access")
166 os.chdir(original_dir)
167 logger.debug(f
"Restored working directory to {original_dir}")
172 High-level interface for PROSPECT leaf optical model.
174 This class provides a user-friendly wrapper around the native Helios
175 LeafOptics plugin for computing spectral reflectance and transmittance
176 of plant leaves based on their biochemical properties.
178 The PROSPECT model computes spectral optical properties for wavelengths
179 from 400 nm to 2500 nm at 1 nm resolution (2101 data points).
182 - Cross-platform support (Windows, Linux, macOS)
184 - Requires spectral_data assets (~475KB XML file)
185 - LeafOptics plugin compiled into PyHelios
188 >>> from pyhelios import Context, LeafOptics, LeafOpticsProperties
190 >>> with Context() as context:
191 ... with LeafOptics(context) as leafoptics:
192 ... # Get properties for a known species
193 ... props = leafoptics.getPropertiesFromLibrary("sunflower")
194 ... print(f"Sunflower chlorophyll: {props.chlorophyllcontent} ug/cm^2")
196 ... # Compute spectra
197 ... wavelengths, reflectance, transmittance = leafoptics.getLeafSpectra(props)
198 ... print(f"Spectral range: {wavelengths[0]}-{wavelengths[-1]} nm")
200 ... # Apply to geometry
201 ... leaf_uuid = context.addPatch(center=[0, 0, 1], size=[0.1, 0.1])
202 ... leafoptics.run([leaf_uuid], props, "sunflower_leaf")
205 def __init__(self, context: Context):
207 Initialize LeafOptics with graceful plugin handling.
210 context: Helios Context instance
213 TypeError: If context is not a Context instance
214 LeafOpticsError: If LeafOptics plugin is not available or spectral data missing
217 if not (hasattr(context,
'__class__')
and
218 (isinstance(context, Context)
or
219 context.__class__.__name__ ==
'Context')):
220 raise TypeError(f
"LeafOptics requires a Context instance, got {type(context).__name__}")
222 self.context = context
226 registry = get_plugin_registry()
228 if not registry.is_plugin_available(
'leafoptics'):
230 available_plugins = registry.get_available_plugins()
233 "LeafOptics requires the 'leafoptics' plugin which is not available.\n\n"
234 "The LeafOptics plugin implements the PROSPECT leaf optical model which computes:\n"
235 "- Spectral reflectance (400-2500 nm at 1 nm resolution)\n"
236 "- Spectral transmittance\n"
237 "- Based on leaf biochemical properties (chlorophyll, water, dry matter, etc.)\n\n"
239 "- Cross-platform support (Windows, Linux, macOS)\n"
240 "- No GPU required\n"
241 "- Built-in species library with 12 plant species\n"
242 "- Supports both PROSPECT-D and PROSPECT-PRO modes\n\n"
243 "To enable LeafOptics modeling:\n"
244 "1. Build PyHelios with LeafOptics plugin:\n"
245 " build_scripts/build_helios --plugins leafoptics\n"
246 "2. Or build with radiation plugins for full spectral modeling:\n"
247 " build_scripts/build_helios --plugins leafoptics,radiation\n"
248 f
"\nCurrently available plugins: {available_plugins}"
256 self.
_leafoptics_ptr = leafoptics_wrapper.createLeafOptics(context.getNativePtr())
259 "Failed to create LeafOptics instance. "
260 "This may indicate a problem with the spectral data files."
262 logger.info(
"LeafOptics created successfully")
264 except LeafOpticsError:
266 except Exception
as e:
270 """Context manager entry."""
273 def __exit__(self, exc_type, exc_value, traceback):
274 """Context manager exit with proper cleanup."""
278 logger.debug(
"LeafOptics destroyed successfully")
279 except Exception
as e:
280 logger.warning(f
"Error destroying LeafOptics: {e}")
285 """Destructor to ensure C++ resources freed even without 'with' statement."""
286 if hasattr(self,
'_leafoptics_ptr')
and self.
_leafoptics_ptr is not None:
290 except Exception
as e:
292 warnings.warn(f
"Error in LeafOptics.__del__: {e}")
294 def run(self, UUIDs: List[int], leafproperties: LeafOpticsProperties, label: str) ->
None:
296 Run the LeafOptics model to generate spectra and assign to primitives.
298 This method computes reflectance and transmittance spectra based on the given
299 leaf properties, creates global data entries, and assigns them to the specified
303 UUIDs: List of primitive UUIDs to assign spectra to
304 leafproperties: LeafOpticsProperties with biochemical parameters
305 label: Label for the spectra (appended to "leaf_reflectivity_" and "leaf_transmissivity_")
308 ValueError: If parameters are invalid
309 LeafOpticsError: If computation fails
312 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0, watermass=0.02)
313 >>> leafoptics.run([leaf_uuid], props, "my_leaf")
314 >>> # Creates: "leaf_reflectivity_my_leaf" and "leaf_transmissivity_my_leaf"
317 raise ValueError(
"UUIDs list cannot be empty")
318 if not isinstance(leafproperties, LeafOpticsProperties):
319 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
321 raise ValueError(
"Label cannot be empty")
324 leafoptics_wrapper.leafOpticsRun(
327 leafproperties.to_list(),
330 except Exception
as e:
333 def runNoUUIDs(self, leafproperties: LeafOpticsProperties, label: str) ->
None:
335 Run the LeafOptics model without assigning to primitives.
337 This method computes reflectance and transmittance spectra based on the given
338 leaf properties and creates global data entries, but does not assign them to
342 leafproperties: LeafOpticsProperties with biochemical parameters
343 label: Label for the spectra (appended to "leaf_reflectivity_" and "leaf_transmissivity_")
346 ValueError: If parameters are invalid
347 LeafOpticsError: If computation fails
349 if not isinstance(leafproperties, LeafOpticsProperties):
350 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
352 raise ValueError(
"Label cannot be empty")
355 leafoptics_wrapper.leafOpticsRunNoUUIDs(
357 leafproperties.to_list(),
360 except Exception
as e:
363 def getLeafSpectra(self, leafproperties: LeafOpticsProperties) -> Tuple[List[float], List[float], List[float]]:
365 Compute leaf reflectance and transmittance spectra.
367 This method computes spectral properties without creating global data entries
368 or assigning to primitives.
371 leafproperties: LeafOpticsProperties with biochemical parameters
374 Tuple of (wavelengths, reflectivities, transmissivities):
375 - wavelengths: List of wavelengths in nm (400-2500 at 1nm resolution, 2101 points)
376 - reflectivities: List of reflectance values (0-1) at each wavelength
377 - transmissivities: List of transmittance values (0-1) at each wavelength
380 ValueError: If parameters are invalid
381 LeafOpticsError: If computation fails
384 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0)
385 >>> wavelengths, refl, trans = leafoptics.getLeafSpectra(props)
386 >>> # Find reflectance at 550 nm (green peak)
387 >>> idx_550 = wavelengths.index(550.0)
388 >>> print(f"Reflectance at 550 nm: {refl[idx_550]:.3f}")
390 if not isinstance(leafproperties, LeafOpticsProperties):
391 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
394 return leafoptics_wrapper.leafOpticsGetLeafSpectra(
396 leafproperties.to_list()
398 except Exception
as e:
401 def setProperties(self, UUIDs: List[int], leafproperties: LeafOpticsProperties) ->
None:
403 Set leaf optical properties for primitives as Context primitive data.
405 This assigns the biochemical properties as primitive data using labels:
406 "chlorophyll", "carotenoid", "anthocyanin", "brown", "water", "drymass"
407 (or "protein" + "cellulose" in PROSPECT-PRO mode).
410 UUIDs: List of primitive UUIDs
411 leafproperties: LeafOpticsProperties with biochemical parameters
414 ValueError: If parameters are invalid
415 LeafOpticsError: If operation fails
418 raise ValueError(
"UUIDs list cannot be empty")
419 if not isinstance(leafproperties, LeafOpticsProperties):
420 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
423 leafoptics_wrapper.leafOpticsSetProperties(
426 leafproperties.to_list()
428 except Exception
as e:
433 Get PROSPECT parameters from reflectivity spectrum for primitives.
435 This method retrieves the "reflectivity_spectrum" primitive data for each
436 primitive and checks if it matches a spectrum generated by this LeafOptics
437 instance. If a match is found, the corresponding PROSPECT model parameters
438 are assigned as primitive data.
441 UUIDs: List of primitive UUIDs to query
444 Primitives without matching spectra are silently skipped.
447 raise ValueError(
"UUIDs list cannot be empty")
450 leafoptics_wrapper.leafOpticsGetPropertiesFromSpectrum(
454 except Exception
as e:
459 Get leaf optical properties from the built-in species library.
461 The library contains PROSPECT-D parameters fitted from the LOPEX93 dataset
462 for common plant species.
465 species: Name of the species (case-insensitive). Available species:
466 "default", "garden_lettuce", "alfalfa", "corn", "sunflower",
467 "english_walnut", "rice", "soybean", "wine_grape", "tomato",
468 "common_bean", "cowpea"
471 LeafOpticsProperties populated with the species-specific parameters
474 ValueError: If species name is empty
477 If species is not found, default properties are used and a warning is issued.
480 >>> props = leafoptics.getPropertiesFromLibrary("sunflower")
481 >>> print(f"Chlorophyll: {props.chlorophyllcontent} ug/cm^2")
484 raise ValueError(
"Species name cannot be empty")
487 properties_list = leafoptics_wrapper.leafOpticsGetPropertiesFromLibrary(
491 return LeafOpticsProperties.from_list(properties_list)
492 except Exception
as e:
496 """Disable command-line output messages from LeafOptics."""
499 except Exception
as e:
503 """Enable command-line output messages from LeafOptics."""
506 except Exception
as e:
511 Selectively output specific biochemical properties as primitive data.
513 By default, LeafOptics writes all biochemical properties to primitive data.
514 Use this method to specify only the properties you need for improved performance.
517 label: Biochemical property to output. Valid values:
518 - "chlorophyll": Chlorophyll content
519 - "carotenoid": Carotenoid content
520 - "anthocyanin": Anthocyanin content
521 - "brown": Brown pigment content
522 - "water": Water content
523 - "drymass": Dry mass content
524 - "protein": Protein content
525 - "cellulose": Cellulose content
528 ValueError: If label is empty or invalid
529 LeafOpticsError: If operation fails
532 Added in helios-core v1.3.59 for performance optimization when only
533 specific biochemical properties are needed for analysis.
536 >>> with LeafOptics(context) as leaf:
537 ... # Only output chlorophyll and water content
538 ... leaf.optionalOutputPrimitiveData("chlorophyll")
539 ... leaf.optionalOutputPrimitiveData("water")
540 ... leaf.run(uuids, properties, "leaf_spectra")
543 raise ValueError(
"Label cannot be empty")
545 valid_labels = [
"chlorophyll",
"carotenoid",
"anthocyanin",
"brown",
546 "water",
"drymass",
"protein",
"cellulose"]
547 if label
not in valid_labels:
548 raise ValueError(f
"Invalid label '{label}'. Must be one of: {', '.join(valid_labels)}")
551 leafoptics_wrapper.leafOpticsOptionalOutputPrimitiveData(self.
_leafoptics_ptr, label)
552 except Exception
as e:
553 raise LeafOpticsError(f
"Failed to set optional output for '{label}': {e}")
558 Get list of available species in the built-in library.
561 List of species names that can be used with getPropertiesFromLibrary()
563 return AVAILABLE_SPECIES.copy()
568 Check if LeafOptics plugin is available in the current build.
571 True if LeafOptics is available, False otherwise
573 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 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 carbonconstituents
Carbon-based constituents in grams per square cm (PROSPECT-PRO mode)
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.