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, check_context_alive
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 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
301 check_context_alive(getattr(self,
"context",
None),
"LeafOptics")
304 """Context manager entry."""
307 def __exit__(self, exc_type, exc_value, traceback):
308 """Context manager exit with proper cleanup."""
312 logger.debug(
"LeafOptics destroyed successfully")
313 except Exception
as e:
314 logger.warning(f
"Error destroying LeafOptics: {e}")
319 """Destructor to ensure C++ resources freed even without 'with' statement."""
320 if hasattr(self,
'_leafoptics_ptr')
and self.
_leafoptics_ptr is not None:
324 except Exception
as e:
326 warnings.warn(f
"Error in LeafOptics.__del__: {e}")
328 def run(self, UUIDs: List[int], leafproperties: LeafOpticsProperties, label: str) ->
None:
330 Run the LeafOptics model to generate spectra and assign to primitives.
332 This method computes reflectance and transmittance spectra based on the given
333 leaf properties, creates global data entries, and assigns them to the specified
337 UUIDs: List of primitive UUIDs to assign spectra to
338 leafproperties: LeafOpticsProperties with biochemical parameters
339 label: Label for the spectra (appended to "leaf_reflectivity_" and "leaf_transmissivity_")
342 ValueError: If parameters are invalid
343 LeafOpticsError: If computation fails
346 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0, watermass=0.02)
347 >>> leafoptics.run([leaf_uuid], props, "my_leaf")
348 >>> # Creates: "leaf_reflectivity_my_leaf" and "leaf_transmissivity_my_leaf"
351 raise ValueError(
"UUIDs list cannot be empty")
352 if not isinstance(leafproperties, LeafOpticsProperties):
353 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
355 raise ValueError(
"Label cannot be empty")
359 leafoptics_wrapper.leafOpticsRun(
362 leafproperties.to_list(),
365 except Exception
as e:
368 def runNoUUIDs(self, leafproperties: LeafOpticsProperties, label: str) ->
None:
370 Run the LeafOptics model without assigning to primitives.
372 This method computes reflectance and transmittance spectra based on the given
373 leaf properties and creates global data entries, but does not assign them to
377 leafproperties: LeafOpticsProperties with biochemical parameters
378 label: Label for the spectra (appended to "leaf_reflectivity_" and "leaf_transmissivity_")
381 ValueError: If parameters are invalid
382 LeafOpticsError: If computation fails
384 if not isinstance(leafproperties, LeafOpticsProperties):
385 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
387 raise ValueError(
"Label cannot be empty")
391 leafoptics_wrapper.leafOpticsRunNoUUIDs(
393 leafproperties.to_list(),
396 except Exception
as e:
399 def getLeafSpectra(self, leafproperties: LeafOpticsProperties) -> Tuple[List[float], List[float], List[float]]:
401 Compute leaf reflectance and transmittance spectra.
403 This method computes spectral properties without creating global data entries
404 or assigning to primitives.
407 leafproperties: LeafOpticsProperties with biochemical parameters
410 Tuple of (wavelengths, reflectivities, transmissivities):
411 - wavelengths: List of wavelengths in nm (400-2500 at 1nm resolution, 2101 points)
412 - reflectivities: List of reflectance values (0-1) at each wavelength
413 - transmissivities: List of transmittance values (0-1) at each wavelength
416 ValueError: If parameters are invalid
417 LeafOpticsError: If computation fails
420 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0)
421 >>> wavelengths, refl, trans = leafoptics.getLeafSpectra(props)
422 >>> # Find reflectance at 550 nm (green peak)
423 >>> idx_550 = wavelengths.index(550.0)
424 >>> print(f"Reflectance at 550 nm: {refl[idx_550]:.3f}")
426 if not isinstance(leafproperties, LeafOpticsProperties):
427 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
431 return leafoptics_wrapper.leafOpticsGetLeafSpectra(
433 leafproperties.to_list()
435 except Exception
as e:
438 def setProperties(self, UUIDs: List[int], leafproperties: LeafOpticsProperties) ->
None:
440 Set leaf optical properties for primitives as Context primitive data.
442 This assigns the biochemical properties as primitive data using labels:
443 "chlorophyll", "carotenoid", "anthocyanin", "brown", "water", "drymass"
444 (or "protein" + "cellulose" in PROSPECT-PRO mode).
447 UUIDs: List of primitive UUIDs
448 leafproperties: LeafOpticsProperties with biochemical parameters
451 ValueError: If parameters are invalid
452 LeafOpticsError: If operation fails
455 raise ValueError(
"UUIDs list cannot be empty")
456 if not isinstance(leafproperties, LeafOpticsProperties):
457 raise ValueError(
"leafproperties must be a LeafOpticsProperties instance")
461 leafoptics_wrapper.leafOpticsSetProperties(
464 leafproperties.to_list()
466 except Exception
as e:
471 Get PROSPECT parameters from reflectivity spectrum for primitives.
473 This method retrieves the "reflectivity_spectrum" primitive data for each
474 primitive and checks if it matches a spectrum generated by this LeafOptics
475 instance. If a match is found, the corresponding PROSPECT model parameters
476 are assigned as primitive data.
479 UUIDs: List of primitive UUIDs to query
482 Primitives without matching spectra are silently skipped.
485 raise ValueError(
"UUIDs list cannot be empty")
489 leafoptics_wrapper.leafOpticsGetPropertiesFromSpectrum(
493 except Exception
as e:
498 Get leaf optical properties from the built-in species library.
500 The library contains PROSPECT-D parameters fitted from the LOPEX93 dataset
501 for common plant species.
504 species: Name of the species (case-insensitive). Available species:
505 "default", "garden_lettuce", "alfalfa", "corn", "sunflower",
506 "english_walnut", "rice", "soybean", "wine_grape", "tomato",
507 "common_bean", "cowpea"
510 LeafOpticsProperties populated with the species-specific parameters
513 ValueError: If species name is empty
516 If species is not found, default properties are used and a warning is issued.
519 >>> props = leafoptics.getPropertiesFromLibrary("sunflower")
520 >>> print(f"Chlorophyll: {props.chlorophyllcontent} ug/cm^2")
523 raise ValueError(
"Species name cannot be empty")
527 properties_list = leafoptics_wrapper.leafOpticsGetPropertiesFromLibrary(
531 return LeafOpticsProperties.from_list(properties_list)
532 except Exception
as e:
536 """Disable command-line output messages from LeafOptics."""
540 except Exception
as e:
544 """Enable command-line output messages from LeafOptics."""
548 except Exception
as e:
553 Selectively output specific biochemical properties as primitive data.
555 By default, LeafOptics writes all biochemical properties to primitive data.
556 Use this method to specify only the properties you need for improved performance.
559 label: Biochemical property to output. Valid values:
560 - "chlorophyll": Chlorophyll content
561 - "carotenoid": Carotenoid content
562 - "anthocyanin": Anthocyanin content
563 - "brown": Brown pigment content
564 - "water": Water content
565 - "drymass": Dry mass content
566 - "protein": Protein content
567 - "cellulose": Cellulose content
570 ValueError: If label is empty or invalid
571 LeafOpticsError: If operation fails
574 Added in helios-core v1.3.59 for performance optimization when only
575 specific biochemical properties are needed for analysis.
578 >>> with LeafOptics(context) as leaf:
579 ... # Only output chlorophyll and water content
580 ... leaf.optionalOutputPrimitiveData("chlorophyll")
581 ... leaf.optionalOutputPrimitiveData("water")
582 ... leaf.run(uuids, properties, "leaf_spectra")
585 raise ValueError(
"Label cannot be empty")
587 valid_labels = [
"chlorophyll",
"carotenoid",
"anthocyanin",
"brown",
588 "water",
"drymass",
"protein",
"cellulose"]
589 if label
not in valid_labels:
590 raise ValueError(f
"Invalid label '{label}'. Must be one of: {', '.join(valid_labels)}")
594 leafoptics_wrapper.leafOpticsOptionalOutputPrimitiveData(self.
_leafoptics_ptr, label)
595 except Exception
as e:
596 raise LeafOpticsError(f
"Failed to set optional output for '{label}': {e}")
601 Get list of available species in the built-in library.
604 List of species names that can be used with getPropertiesFromLibrary()
606 return AVAILABLE_SPECIES.copy()
611 Check if LeafOptics plugin is available in the current build.
614 True if LeafOptics is available, False otherwise
616 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.
_check_context_alive(self)
Raise if the owning Context has been destroyed (see Context.check_context_alive).
Exception classes for PyHelios library.
_leafoptics_working_directory()
Context manager that temporarily changes working directory to where LeafOptics assets are located.