0.1.39
Loading...
Searching...
No Matches
LeafOptics.py
Go to the documentation of this file.
1"""
2High-level LeafOptics interface for PyHelios.
3
4This module provides a user-friendly interface to the PROSPECT leaf optical model
5for computing spectral reflectance and transmittance of plant leaves.
6"""
7
8import logging
9import os
10from contextlib import contextmanager
11from dataclasses import dataclass, field
12from pathlib import Path
13from typing import List, Optional, Tuple, Union
14
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
20
21logger = logging.getLogger(__name__)
22
23
25 """Exception raised for LeafOptics-specific errors."""
26 pass
27
28
29# Available species in the built-in library
30AVAILABLE_SPECIES = [
31 "default",
32 "garden_lettuce",
33 "alfalfa",
34 "corn",
35 "sunflower",
36 "english_walnut",
37 "rice",
38 "soybean",
39 "wine_grape",
40 "tomato",
41 "common_bean",
42 "cowpea"
43]
44
45
46@dataclass
48 """
49 Data class for PROSPECT leaf optical model parameters.
50
51 These parameters define the physical and biochemical properties of a leaf
52 that determine its spectral reflectance and transmittance.
53
54 Attributes:
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.
71
72 Note:
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.
77 """
78 numberlayers: float = 1.5
79 brownpigments: float = 0.0
80 chlorophyllcontent: float = 30.0 # micrograms/cm^2
81 carotenoidcontent: float = 7.0 # micrograms/cm^2
82 anthocyancontent: float = 1.0 # micrograms/cm^2
83 watermass: float = 0.015 # g/cm^2
84 drymass: float = 0.09 # g/cm^2
85 protein: float = 0.0 # g/cm^2 (PROSPECT-PRO mode)
86 carbonconstituents: float = 0.0 # g/cm^2 (PROSPECT-PRO mode)
87 V2Z: float = 0.0 # SIF: violaxanthin↔zeaxanthin state, [0, 1]
88 fqe: float = 1.0 # SIF: intrinsic fluorescence quantum efficiency scalar
89
90 def to_list(self) -> List[float]:
91 """Convert properties to a list in the order expected by the C++ interface."""
92 return [
93 self.numberlayers,
94 self.brownpigments,
98 self.watermass,
99 self.drymass,
100 self.protein,
102 self.V2Z,
103 self.fqe,
106 @classmethod
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.
114 """
115 if len(values) not in (9, 11):
116 raise ValueError(f"Expected 9 or 11 values, got {len(values)}")
117 if len(values) == 9:
118 import warnings
119 warnings.warn(
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.",
124 DeprecationWarning,
125 stacklevel=2,
126 )
127 return cls(
128 numberlayers=values[0],
129 brownpigments=values[1],
130 chlorophyllcontent=values[2],
131 carotenoidcontent=values[3],
132 anthocyancontent=values[4],
133 watermass=values[5],
134 drymass=values[6],
135 protein=values[7],
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,
140
141
142@dataclass
144 """
145 Parameters for nitrogen-based automatic leaf optics.
146
147 Passed to :meth:`LeafOptics.run` (without a label) to derive each leaf's PROSPECT
148 chlorophyll and carotenoid content from its ``"leaf_nitrogen_gN_m2"`` object data
149 (g N/m^2, typically written by the PlantArchitecture nitrogen model):
150
151 - Cab (ug/cm^2) = N_area (g/m^2) * 100 * f_photosynthetic * N_to_Cab_coefficient,
152 clamped to [5, 80]
153 - Car (ug/cm^2) = Cab * Car_to_Cab_ratio
154
155 All other PROSPECT parameters are fixed at the values given here. Leaves are grouped
156 into ``num_bins`` spectrum bins spaced evenly across the nitrogen range, so at most
157 ``num_bins`` spectra are generated (labelled ``"Nauto_0"``, ``"Nauto_1"``, ...).
158
159 Attributes:
160 f_photosynthetic: Fraction of leaf nitrogen in photosynthetic machinery [0-1]
161 N_to_Cab_coefficient: Empirical nitrogen-to-chlorophyll coefficient
162 Car_to_Cab_ratio: Carotenoid to chlorophyll ratio
163 numberlayers: Leaf structure parameter N (must be >= 1)
164 anthocyancontent: Anthocyanin content in micrograms per square cm
165 brownpigments: Brown pigment content (senescence indicator)
166 watermass: Equivalent water thickness in grams per square cm
167 drymass: Leaf mass per area in grams per square cm
168 protein: Protein content in grams per square cm (PROSPECT-PRO mode)
169 carbonconstituents: Carbon constituents in grams per square cm (PROSPECT-PRO mode)
170 V2Z: Violaxanthin-to-zeaxanthin state [0, 1] for the SIF pipeline (fixed across bins)
171 fqe: Intrinsic fluorescence quantum efficiency scalar for the SIF pipeline (fixed across bins)
172 num_bins: Number of spectrum bins (a single bin is used when all leaves share one nitrogen value)
173 reassignment_threshold: Relative nitrogen change (fraction of bin centre) a leaf must
174 exceed before it is reassigned to another bin; 0 disables the relative test
175 min_reassignment_change: Minimum absolute nitrogen change (g/m^2) before a leaf is
176 reassigned; the new bin must also be closer by at least half this amount (hysteresis)
177 """
178 f_photosynthetic: float = 0.50
179 N_to_Cab_coefficient: float = 0.40
180 Car_to_Cab_ratio: float = 0.25
181 numberlayers: float = 1.5
182 anthocyancontent: float = 1.0 # micrograms/cm^2
183 brownpigments: float = 0.0
184 watermass: float = 0.015 # g/cm^2
185 drymass: float = 0.006 # g/cm^2
186 protein: float = 0.0 # g/cm^2 (PROSPECT-PRO mode)
187 carbonconstituents: float = 0.0 # g/cm^2 (PROSPECT-PRO mode)
188 V2Z: float = 0.0
189 fqe: float = 1.0
190 num_bins: int = 20
191 reassignment_threshold: float = 0.0
192 min_reassignment_change: float = 0.05 # g/m^2
193
194 _FLOAT_FIELDS = (
195 "f_photosynthetic", "N_to_Cab_coefficient", "Car_to_Cab_ratio", "numberlayers",
196 "anthocyancontent", "brownpigments", "watermass", "drymass", "protein",
197 "carbonconstituents", "V2Z", "fqe", "reassignment_threshold", "min_reassignment_change",
198 )
199
200 def validate(self) -> None:
201 """Raise ValueError if any field has the wrong type or an unphysical value."""
202 for name in self._FLOAT_FIELDS:
203 value = getattr(self, name)
204 if isinstance(value, bool) or not isinstance(value, (int, float)):
205 raise ValueError(f"{name} must be a number, got {type(value).__name__}")
206 if value != value or value in (float("inf"), float("-inf")):
207 raise ValueError(f"{name} must be finite, got {value}")
208 if value < 0:
209 raise ValueError(f"{name} must be non-negative, got {value}")
210 if self.numberlayers < 1:
211 raise ValueError(f"numberlayers must be >= 1, got {self.numberlayers}")
212 if self.f_photosynthetic > 1:
213 raise ValueError(f"f_photosynthetic must be in [0, 1], got {self.f_photosynthetic}")
214 if self.V2Z > 1:
215 raise ValueError(f"V2Z must be in [0, 1], got {self.V2Z}")
216 if isinstance(self.num_bins, bool) or not isinstance(self.num_bins, int):
217 raise ValueError(f"num_bins must be an int, got {type(self.num_bins).__name__}")
218 if self.num_bins < 1:
219 raise ValueError(f"num_bins must be at least 1, got {self.num_bins}")
220
221 def to_list(self) -> List[float]:
222 """Convert the float fields to a list in the order expected by the C++ interface (num_bins excluded)."""
223 return [float(getattr(self, name)) for name in self._FLOAT_FIELDS]
224
225
226@contextmanager
228 """
229 Context manager that temporarily changes working directory to where LeafOptics assets are located.
230
231 LeafOptics C++ code uses relative paths like "plugins/leafoptics/spectral_data/"
232 expecting assets relative to working directory. This manager temporarily changes to the build
233 directory where assets are actually located.
234 """
235 # Find the build directory containing LeafOptics assets
236 asset_manager = get_asset_manager()
237 working_dir = asset_manager._get_helios_build_path()
239 if working_dir and working_dir.exists():
240 leafoptics_assets = working_dir / 'plugins' / 'leafoptics' / 'spectral_data'
241 else:
242 # Fallback to development paths
243 current_dir = Path(__file__).parent
244 repo_root = current_dir.parent
245 build_lib_dir = repo_root / 'pyhelios_build' / 'build' / 'lib'
246 working_dir = build_lib_dir.parent
247 leafoptics_assets = working_dir / 'plugins' / 'leafoptics' / 'spectral_data'
249 if not build_lib_dir.exists():
250 raise LeafOpticsError(
251 f"PyHelios build directory not found at {build_lib_dir}. "
252 f"LeafOptics requires native libraries to be built. "
253 f"Run: build_scripts/build_helios --plugins leafoptics"
254 )
255
256 # Validate spectral data file exists
257 spectral_file = leafoptics_assets / 'prospect_spectral_library.xml'
258 if not spectral_file.exists():
259 raise LeafOpticsError(
260 f"LeafOptics spectral data not found at {spectral_file}. "
261 f"Build system failed to copy LeafOptics assets. "
262 f"Run: build_scripts/build_helios --clean --plugins leafoptics"
263 )
264
265 # Validate file size (should be ~475KB)
266 file_size = spectral_file.stat().st_size
267 if file_size < 400000: # Less than 400KB indicates corruption
268 raise LeafOpticsError(
269 f"LeafOptics spectral data file appears corrupted (size: {file_size} bytes, expected ~475KB). "
270 f"Run: build_scripts/build_helios --clean --plugins leafoptics"
271 )
272
273 # Change to the build directory temporarily
274 original_dir = os.getcwd()
275 try:
276 os.chdir(working_dir)
277 logger.debug(f"Changed working directory to {working_dir} for LeafOptics asset access")
278 yield working_dir
279 finally:
280 os.chdir(original_dir)
281 logger.debug(f"Restored working directory to {original_dir}")
282
283
284class LeafOptics:
285 """
286 High-level interface for PROSPECT leaf optical model.
287
288 This class provides a user-friendly wrapper around the native Helios
289 LeafOptics plugin for computing spectral reflectance and transmittance
290 of plant leaves based on their biochemical properties.
291
292 The PROSPECT model computes spectral optical properties for wavelengths
293 from 400 nm to 2500 nm at 1 nm resolution (2101 data points).
294
295 System requirements:
296 - Cross-platform support (Windows, Linux, macOS)
297 - No GPU required
298 - Requires spectral_data assets (~475KB XML file)
299 - LeafOptics plugin compiled into PyHelios
300
301 Example:
302 >>> from pyhelios import Context, LeafOptics, LeafOpticsProperties
303 >>>
304 >>> with Context() as context:
305 ... with LeafOptics(context) as leafoptics:
306 ... # Get properties for a known species
307 ... props = leafoptics.getPropertiesFromLibrary("sunflower")
308 ... print(f"Sunflower chlorophyll: {props.chlorophyllcontent} ug/cm^2")
309 ...
310 ... # Compute spectra
311 ... wavelengths, reflectance, transmittance = leafoptics.getLeafSpectra(props)
312 ... print(f"Spectral range: {wavelengths[0]}-{wavelengths[-1]} nm")
313 ...
314 ... # Apply to geometry
315 ... leaf_uuid = context.addPatch(center=[0, 0, 1], size=[0.1, 0.1])
316 ... leafoptics.run([leaf_uuid], props, "sunflower_leaf")
317 """
318
319 def __init__(self, context: Context):
320 """
321 Initialize LeafOptics with graceful plugin handling.
322
323 Args:
324 context: Helios Context instance
325
326 Raises:
327 TypeError: If context is not a Context instance
328 LeafOpticsError: If LeafOptics plugin is not available or spectral data missing
329 """
330 # Validate context type - use duck typing to handle import state issues during testing
331 if not (hasattr(context, '__class__') and
332 (isinstance(context, Context) or
333 context.__class__.__name__ == 'Context')):
334 raise TypeError(f"LeafOptics requires a Context instance, got {type(context).__name__}")
335
336 self.context = context
337 self._leafoptics_ptr = None
338
339 # Check plugin availability using registry
340 registry = get_plugin_registry()
341
342 if not registry.is_plugin_available('leafoptics'):
343 # Get helpful information about the missing plugin
344 available_plugins = registry.get_available_plugins()
345
346 error_msg = (
347 "LeafOptics requires the 'leafoptics' plugin which is not available.\n\n"
348 "The LeafOptics plugin implements the PROSPECT leaf optical model which computes:\n"
349 "- Spectral reflectance (400-2500 nm at 1 nm resolution)\n"
350 "- Spectral transmittance\n"
351 "- Based on leaf biochemical properties (chlorophyll, water, dry matter, etc.)\n\n"
352 "Features:\n"
353 "- Cross-platform support (Windows, Linux, macOS)\n"
354 "- No GPU required\n"
355 "- Built-in species library with 12 plant species\n"
356 "- Supports both PROSPECT-D and PROSPECT-PRO modes\n\n"
357 "To enable LeafOptics modeling:\n"
358 "1. Build PyHelios with LeafOptics plugin:\n"
359 " build_scripts/build_helios --plugins leafoptics\n"
360 "2. Or build with radiation plugins for full spectral modeling:\n"
361 " build_scripts/build_helios --plugins leafoptics,radiation\n"
362 f"\nCurrently available plugins: {available_plugins}"
363 )
364
365 raise LeafOpticsError(error_msg)
366
367 # Plugin is available - create LeafOptics with asset-aware working directory
368 try:
370 self._leafoptics_ptr = leafoptics_wrapper.createLeafOptics(context.getNativePtr())
371 if self._leafoptics_ptr is None:
372 raise LeafOpticsError(
373 "Failed to create LeafOptics instance. "
374 "This may indicate a problem with the spectral data files."
375 )
376 logger.info("LeafOptics created successfully")
377
378 except LeafOpticsError:
379 raise
380 except Exception as e:
381 raise LeafOpticsError(f"Failed to initialize LeafOptics: {e}")
382
383 def _check_context_alive(self):
384 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
385 check_context_alive(getattr(self, "context", None), "LeafOptics")
386
387 def __enter__(self):
388 """Context manager entry."""
389 return self
391 def __exit__(self, exc_type, exc_value, traceback):
392 """Context manager exit with proper cleanup."""
393 if self._leafoptics_ptr is not None:
394 try:
395 leafoptics_wrapper.destroyLeafOptics(self._leafoptics_ptr)
396 logger.debug("LeafOptics destroyed successfully")
397 except Exception as e:
398 logger.warning(f"Error destroying LeafOptics: {e}")
399 finally:
400 self._leafoptics_ptr = None
401
402 def __del__(self):
403 """Destructor to ensure C++ resources freed even without 'with' statement."""
404 if hasattr(self, '_leafoptics_ptr') and self._leafoptics_ptr is not None:
405 try:
406 leafoptics_wrapper.destroyLeafOptics(self._leafoptics_ptr)
407 self._leafoptics_ptr = None
408 except Exception as e:
409 import warnings
410 warnings.warn(f"Error in LeafOptics.__del__: {e}")
411
412 def run(self, UUIDs: List[int],
413 leafproperties: Union[LeafOpticsProperties, LeafOpticsProperties_Nauto],
414 label: Optional[str] = None) -> None:
415 """
416 Run the LeafOptics model to generate spectra and assign them to primitives.
417
418 Two modes, selected by the type of ``leafproperties``:
419
420 - :class:`LeafOpticsProperties` with a ``label``: computes one reflectance and
421 transmittance spectrum, stores it as global data ``"leaf_reflectivity_<label>"`` /
422 ``"leaf_transmissivity_<label>"``, and assigns it to every primitive.
423 - :class:`LeafOpticsProperties_Nauto` without a label (nitrogen mode): groups the
424 primitives by parent object, reads ``"leaf_nitrogen_gN_m2"`` object data from each,
425 derives chlorophyll and carotenoid content from it, and assigns each object the
426 spectrum of its nitrogen bin (global data ``"leaf_reflectivity_Nauto_<i>"`` etc.).
427 The first nitrogen-mode call creates the bins and fixes the parameters; later calls
428 track added/removed primitives, reassign leaves whose nitrogen changed, and rebuild
429 the bins if nitrogen moves outside their range. Parameters passed on later calls are
430 ignored. Primitives without a parent object are skipped with a warning.
431
432 Args:
433 UUIDs: List of primitive UUIDs to assign spectra to
434 leafproperties: LeafOpticsProperties or LeafOpticsProperties_Nauto
435 label: Spectrum label (required for LeafOpticsProperties, must be omitted for
436 LeafOpticsProperties_Nauto)
437
438 Raises:
439 ValueError: If parameters are invalid
440 LeafOpticsError: If computation fails (e.g. an object lacks "leaf_nitrogen_gN_m2")
441
442 Example:
443 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0, watermass=0.02)
444 >>> leafoptics.run([leaf_uuid], props, "my_leaf")
445 >>> # Creates: "leaf_reflectivity_my_leaf" and "leaf_transmissivity_my_leaf"
446 >>>
447 >>> context.setObjectDataFloat(leaf_objID, "leaf_nitrogen_gN_m2", 2.0)
448 >>> leafoptics.run(context.getObjectPrimitiveUUIDs(leaf_objID), LeafOpticsProperties_Nauto())
449 """
450 if not UUIDs:
451 raise ValueError("UUIDs list cannot be empty")
452
453 if isinstance(leafproperties, LeafOpticsProperties_Nauto):
454 if label is not None:
455 raise ValueError("label must not be given in nitrogen mode (LeafOpticsProperties_Nauto); "
456 "spectra are labelled 'Nauto_<bin>' automatically")
457 leafproperties.validate()
459 try:
460 leafoptics_wrapper.leafOpticsRunNitrogen(
461 self._leafoptics_ptr,
462 UUIDs,
463 leafproperties.to_list(),
464 leafproperties.num_bins
465 )
466 except Exception as e:
467 raise LeafOpticsError(f"Failed to run nitrogen-based LeafOptics model: {e}")
468 return
469
470 if not isinstance(leafproperties, LeafOpticsProperties):
471 raise ValueError("leafproperties must be a LeafOpticsProperties or LeafOpticsProperties_Nauto instance, "
472 f"got {type(leafproperties).__name__}")
473 if not label:
474 raise ValueError("Label cannot be empty")
475
477 try:
478 leafoptics_wrapper.leafOpticsRun(
479 self._leafoptics_ptr,
480 UUIDs,
481 leafproperties.to_list(),
482 label
483 )
484 except Exception as e:
485 raise LeafOpticsError(f"Failed to run LeafOptics model: {e}")
486
487 def updateNitrogenBasedSpectra(self) -> None:
488 """
489 Re-read leaf nitrogen and reassign spectra for the primitives tracked in nitrogen mode.
490
491 Faster than calling :meth:`run` again with the same UUIDs when only nitrogen values
492 have changed (e.g. after advancing the PlantArchitecture nitrogen model): it skips
493 primitive grouping and add/remove processing. A leaf is moved to another bin only when
494 its nitrogen has moved past the configured reassignment thresholds. Bins are not
495 rebuilt here; use :meth:`run` if nitrogen may have left the original range.
496
497 Raises:
498 LeafOpticsError: If nitrogen mode has not been initialized with
499 ``run(UUIDs, LeafOpticsProperties_Nauto(...))``
500 """
502 try:
503 leafoptics_wrapper.leafOpticsUpdateNitrogenBasedSpectra(self._leafoptics_ptr)
504 except Exception as e:
505 raise LeafOpticsError(f"Failed to update nitrogen-based spectra: {e}")
506
507 def runNoUUIDs(self, leafproperties: LeafOpticsProperties, label: str) -> None:
508 """
509 Run the LeafOptics model without assigning to primitives.
511 This method computes reflectance and transmittance spectra based on the given
512 leaf properties and creates global data entries, but does not assign them to
513 any primitives.
514
515 Args:
516 leafproperties: LeafOpticsProperties with biochemical parameters
517 label: Label for the spectra (appended to "leaf_reflectivity_" and "leaf_transmissivity_")
518
519 Raises:
520 ValueError: If parameters are invalid
521 LeafOpticsError: If computation fails
522 """
523 if not isinstance(leafproperties, LeafOpticsProperties):
524 raise ValueError("leafproperties must be a LeafOpticsProperties instance")
525 if not label:
526 raise ValueError("Label cannot be empty")
527
529 try:
530 leafoptics_wrapper.leafOpticsRunNoUUIDs(
531 self._leafoptics_ptr,
532 leafproperties.to_list(),
533 label
534 )
535 except Exception as e:
536 raise LeafOpticsError(f"Failed to run LeafOptics model: {e}")
537
538 def getLeafSpectra(self, leafproperties: LeafOpticsProperties) -> Tuple[List[float], List[float], List[float]]:
539 """
540 Compute leaf reflectance and transmittance spectra.
541
542 This method computes spectral properties without creating global data entries
543 or assigning to primitives.
544
545 Args:
546 leafproperties: LeafOpticsProperties with biochemical parameters
547
548 Returns:
549 Tuple of (wavelengths, reflectivities, transmissivities):
550 - wavelengths: List of wavelengths in nm (400-2500 at 1nm resolution, 2101 points)
551 - reflectivities: List of reflectance values (0-1) at each wavelength
552 - transmissivities: List of transmittance values (0-1) at each wavelength
553
554 Raises:
555 ValueError: If parameters are invalid
556 LeafOpticsError: If computation fails
557
558 Example:
559 >>> props = LeafOpticsProperties(chlorophyllcontent=40.0)
560 >>> wavelengths, refl, trans = leafoptics.getLeafSpectra(props)
561 >>> # Find reflectance at 550 nm (green peak)
562 >>> idx_550 = wavelengths.index(550.0)
563 >>> print(f"Reflectance at 550 nm: {refl[idx_550]:.3f}")
564 """
565 if not isinstance(leafproperties, LeafOpticsProperties):
566 raise ValueError("leafproperties must be a LeafOpticsProperties instance")
567
569 try:
570 return leafoptics_wrapper.leafOpticsGetLeafSpectra(
571 self._leafoptics_ptr,
572 leafproperties.to_list()
573 )
574 except Exception as e:
575 raise LeafOpticsError(f"Failed to get leaf spectra: {e}")
576
577 def setProperties(self, UUIDs: List[int], leafproperties: LeafOpticsProperties) -> None:
578 """
579 Set leaf optical properties for primitives as Context primitive data.
580
581 This assigns the biochemical properties as primitive data using labels:
582 "chlorophyll", "carotenoid", "anthocyanin", "brown", "water", "drymass"
583 (or "protein" + "cellulose" in PROSPECT-PRO mode).
584
585 Args:
586 UUIDs: List of primitive UUIDs
587 leafproperties: LeafOpticsProperties with biochemical parameters
588
589 Raises:
590 ValueError: If parameters are invalid
591 LeafOpticsError: If operation fails
592 """
593 if not UUIDs:
594 raise ValueError("UUIDs list cannot be empty")
595 if not isinstance(leafproperties, LeafOpticsProperties):
596 raise ValueError("leafproperties must be a LeafOpticsProperties instance")
597
599 try:
600 leafoptics_wrapper.leafOpticsSetProperties(
601 self._leafoptics_ptr,
602 UUIDs,
603 leafproperties.to_list()
604 )
605 except Exception as e:
606 raise LeafOpticsError(f"Failed to set properties: {e}")
607
608 def getPropertiesFromSpectrum(self, UUIDs: List[int]) -> None:
609 """
610 Get PROSPECT parameters from reflectivity spectrum for primitives.
611
612 This method retrieves the "reflectivity_spectrum" primitive data for each
613 primitive and checks if it matches a spectrum generated by this LeafOptics
614 instance. If a match is found, the corresponding PROSPECT model parameters
615 are assigned as primitive data.
616
617 Args:
618 UUIDs: List of primitive UUIDs to query
619
620 Note:
621 Primitives without matching spectra are silently skipped.
622 """
623 if not UUIDs:
624 raise ValueError("UUIDs list cannot be empty")
625
627 try:
628 leafoptics_wrapper.leafOpticsGetPropertiesFromSpectrum(
629 self._leafoptics_ptr,
630 UUIDs
631 )
632 except Exception as e:
633 raise LeafOpticsError(f"Failed to get properties from spectrum: {e}")
634
635 def getPropertiesFromLibrary(self, species: str) -> LeafOpticsProperties:
636 """
637 Get leaf optical properties from the built-in species library.
638
639 The library contains PROSPECT-D parameters fitted from the LOPEX93 dataset
640 for common plant species.
641
642 Args:
643 species: Name of the species (case-insensitive). Available species:
644 "default", "garden_lettuce", "alfalfa", "corn", "sunflower",
645 "english_walnut", "rice", "soybean", "wine_grape", "tomato",
646 "common_bean", "cowpea"
647
648 Returns:
649 LeafOpticsProperties populated with the species-specific parameters
650
651 Raises:
652 ValueError: If species name is empty
653
654 Note:
655 If species is not found, default properties are used and a warning is issued.
656
657 Example:
658 >>> props = leafoptics.getPropertiesFromLibrary("sunflower")
659 >>> print(f"Chlorophyll: {props.chlorophyllcontent} ug/cm^2")
660 """
661 if not species:
662 raise ValueError("Species name cannot be empty")
663
665 try:
666 properties_list = leafoptics_wrapper.leafOpticsGetPropertiesFromLibrary(
667 self._leafoptics_ptr,
668 species
669 )
670 return LeafOpticsProperties.from_list(properties_list)
671 except Exception as e:
672 raise LeafOpticsError(f"Failed to get properties from library: {e}")
673
674 def disableMessages(self) -> None:
675 """Disable command-line output messages from LeafOptics."""
677 try:
678 leafoptics_wrapper.leafOpticsDisableMessages(self._leafoptics_ptr)
679 except Exception as e:
680 raise LeafOpticsError(f"Failed to disable messages: {e}")
681
682 def enableMessages(self) -> None:
683 """Enable command-line output messages from LeafOptics."""
685 try:
686 leafoptics_wrapper.leafOpticsEnableMessages(self._leafoptics_ptr)
687 except Exception as e:
688 raise LeafOpticsError(f"Failed to enable messages: {e}")
689
690 def optionalOutputPrimitiveData(self, label: str) -> None:
691 """
692 Selectively output specific biochemical properties as primitive data.
693
694 By default, LeafOptics writes all biochemical properties to primitive data.
695 Use this method to specify only the properties you need for improved performance.
696
697 Args:
698 label: Biochemical property to output. Valid values:
699 - "chlorophyll": Chlorophyll content
700 - "carotenoid": Carotenoid content
701 - "anthocyanin": Anthocyanin content
702 - "brown": Brown pigment content
703 - "water": Water content
704 - "drymass": Dry mass content
705 - "protein": Protein content
706 - "cellulose": Cellulose content
707
708 Raises:
709 ValueError: If label is empty or invalid
710 LeafOpticsError: If operation fails
711
712 Note:
713 Added in helios-core v1.3.59 for performance optimization when only
714 specific biochemical properties are needed for analysis.
715
716 Example:
717 >>> with LeafOptics(context) as leaf:
718 ... # Only output chlorophyll and water content
719 ... leaf.optionalOutputPrimitiveData("chlorophyll")
720 ... leaf.optionalOutputPrimitiveData("water")
721 ... leaf.run(uuids, properties, "leaf_spectra")
722 """
723 if not label:
724 raise ValueError("Label cannot be empty")
725
726 valid_labels = ["chlorophyll", "carotenoid", "anthocyanin", "brown",
727 "water", "drymass", "protein", "cellulose"]
728 if label not in valid_labels:
729 raise ValueError(f"Invalid label '{label}'. Must be one of: {', '.join(valid_labels)}")
730
732 try:
733 leafoptics_wrapper.leafOpticsOptionalOutputPrimitiveData(self._leafoptics_ptr, label)
734 except Exception as e:
735 raise LeafOpticsError(f"Failed to set optional output for '{label}': {e}")
736
737 @staticmethod
738 def getAvailableSpecies() -> List[str]:
739 """
740 Get list of available species in the built-in library.
741
742 Returns:
743 List of species names that can be used with getPropertiesFromLibrary()
744 """
745 return AVAILABLE_SPECIES.copy()
747 @staticmethod
748 def isAvailable() -> bool:
749 """
750 Check if LeafOptics plugin is available in the current build.
751
752 Returns:
753 True if LeafOptics is available, False otherwise
754 """
755 return leafoptics_wrapper.is_leafoptics_available()
Exception raised for LeafOptics-specific errors.
Definition LeafOptics.py:25
Parameters for nitrogen-based automatic leaf optics.
int num_bins
Number of spectrum bins (a single bin is used when all leaves share one nitrogen value)
List[float] to_list(self)
Convert the float fields to a list in the order expected by the C++ interface (num_bins excluded).
float f_photosynthetic
Fraction of leaf nitrogen in photosynthetic machinery [0-1].
float V2Z
Violaxanthin-to-zeaxanthin state [0, 1] for the SIF pipeline (fixed across bins)
float numberlayers
Leaf structure parameter N (must be >= 1)
None validate(self)
Raise ValueError if any field has the wrong type or an unphysical value.
Data class for PROSPECT leaf optical model parameters.
Definition LeafOptics.py:63
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.
None updateNitrogenBasedSpectra(self)
Re-read leaf nitrogen and reassign spectra for the primitives tracked in nitrogen mode.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
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.
None run(self, List[int] UUIDs, Union[LeafOpticsProperties, LeafOpticsProperties_Nauto] leafproperties, Optional[str] label=None)
Run the LeafOptics model to generate spectra and assign them to primitives.
__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.
Definition exceptions.py:10
_leafoptics_working_directory()
Context manager that temporarily changes working directory to where LeafOptics assets are located.