0.1.39
Loading...
Searching...
No Matches
PlantHydraulics.py
Go to the documentation of this file.
1"""
2High-level PlantHydraulics interface for PyHelios.
3
4This module provides a user-friendly interface to the plant hydraulics modeling
5capabilities with graceful plugin handling and informative error messages.
6"""
7
8import logging
9import math
10import numbers
11from dataclasses import dataclass
12from typing import List, Optional, Sequence, Union
13
14from .plugins.registry import get_plugin_registry
15from .wrappers import UPlantHydraulicsWrapper as hydraulics_wrapper
16from .Context import Context, check_context_alive
17from .exceptions import HeliosError
18
19logger = logging.getLogger(__name__)
20
21
23 """Exception raised for PlantHydraulics-specific errors."""
24 pass
25
26
27# Species names accepted by the native leaf capacitance library. For any other name the native code
28# only prints a warning and substitutes other values, so names are checked here.
29AVAILABLE_SPECIES = (
30 "Walnut", "walnut",
31 "PistachioFemale", "pistachiofemale", "pistachio_female", "Pistachio_Female", "Pistachio_female",
32 "pistachio", "Pistachio",
33 "Elderberry", "elderberry", "blue_elderberry",
34 "Western_Redbud", "western_redbud", "Redbud", "redbud",
35)
36
37
38def _validate_species(species, method_name: str) -> str:
39 if not isinstance(species, str):
40 raise ValueError(f"{method_name}() species must be a string, got {type(species).__name__}")
41 if species not in AVAILABLE_SPECIES:
42 raise ValueError(
43 f"{method_name}() unknown species '{species}'. "
44 f"Available species: {', '.join(AVAILABLE_SPECIES)}"
45 )
46 return species
47
48
49def _validate_float(value, name: str, method_name: str) -> float:
50 if isinstance(value, bool) or not isinstance(value, numbers.Real):
51 raise ValueError(f"{method_name}() {name} must be a number, got {type(value).__name__}")
52 if not math.isfinite(value):
53 raise ValueError(f"{method_name}() {name} must be finite, got {value}")
54 return float(value)
55
56
57def _validate_int(value, name: str, method_name: str, minimum: int) -> int:
58 if isinstance(value, bool) or not isinstance(value, numbers.Integral):
59 raise ValueError(f"{method_name}() {name} must be an integer, got {type(value).__name__}")
60 if value < minimum:
61 raise ValueError(f"{method_name}() {name} must be >= {minimum}, got {value}")
62 return int(value)
63
64
65def _validate_bool(value, name: str, method_name: str) -> bool:
66 if not isinstance(value, bool):
67 raise ValueError(f"{method_name}() {name} must be a bool, got {type(value).__name__}")
68 return value
69
70
71def _validate_uuids(UUIDs, method_name: str) -> List[int]:
72 if isinstance(UUIDs, (str, bytes)) or not hasattr(UUIDs, '__iter__'):
73 raise ValueError(f"{method_name}() UUIDs must be a list of integers, got {type(UUIDs).__name__}")
74 uuid_list = [_validate_int(uuid, 'UUID', method_name, 0) for uuid in UUIDs]
75 if not uuid_list:
76 raise ValueError(f"{method_name}() UUIDs cannot be empty")
77 return uuid_list
78
79
80@dataclass
82 """
83 Hydraulic conductance of a leaf, stem or root as a function of water potential.
84
85 Conductance follows K = saturated_conductance / (1 + |psi / potential_at_half_saturated|^sensitivity).
86 It is constant at saturated_conductance when potential_at_half_saturated or sensitivity is zero.
87
88 Attributes:
89 saturated_conductance: Conductance at a water potential of zero (mol/m²/s/MPa). Must be > 0.
90 potential_at_half_saturated: Water potential at which conductance is half its saturated value (MPa).
91 sensitivity: Steepness of the decline in conductance around potential_at_half_saturated (unitless).
92 temperature_dependence: If True, conductance is scaled by (T/298.15)^7.
93 """
94 saturated_conductance: float = 0.5
95 potential_at_half_saturated: float = 0.0
96 sensitivity: float = 0.0
97 temperature_dependence: bool = False
98
99 def validate(self, method_name: str = "HydraulicConductance") -> None:
100 """Raise ValueError if any parameter is outside its valid range."""
101 saturated = _validate_float(self.saturated_conductance, 'saturated_conductance', method_name)
102 if saturated <= 0.0:
103 raise ValueError(f"{method_name}() saturated_conductance must be > 0, got {saturated}")
104 _validate_float(self.potential_at_half_saturated, 'potential_at_half_saturated', method_name)
105 sensitivity = _validate_float(self.sensitivity, 'sensitivity', method_name)
106 if sensitivity < 0.0:
107 raise ValueError(f"{method_name}() sensitivity must be >= 0, got {sensitivity}")
108 _validate_bool(self.temperature_dependence, 'temperature_dependence', method_name)
109
110 def to_list(self) -> List[float]:
111 """Flat layout expected by the native interface."""
112 return [float(self.saturated_conductance), float(self.potential_at_half_saturated),
113 float(self.sensitivity), 1.0 if self.temperature_dependence else 0.0]
114
115
116@dataclass
118 """
119 Hydraulic capacitance of a leaf, stem or root, from pressure-volume curve parameters.
120
121 A fixed_constant_capacitance greater than zero gives a constant capacitance. Use
122 HydraulicCapacitance.constant() to build one; its pressure-volume curve parameters are the defaults.
123
124 For a leaf, the model always computes relative water content, turgor pressure and osmotic
125 potential from the pressure-volume curve parameters, so a constant leaf capacitance means those
126 outputs come from the default curve. For a stem or root, capacitance is used only by
127 non-steady-state runs, which require a constant; their pressure-volume curve parameters are
128 never used.
129
130 Attributes:
131 osmotic_potential_at_full_turgor: Osmotic potential at full turgor (MPa). Must be < 0.
132 relative_water_content_at_turgor_loss: Relative water content at the turgor loss point (unitless), in (0, 1).
133 cell_wall_elasticity_exponent: Exponent of the turgor pressure versus relative water content relationship (unitless).
134 saturated_specific_water_content: Specific water content at a relative water content of 1 (mol/m²).
135 fixed_constant_capacitance: Constant capacitance (mol/m²/MPa), or -1 to use the pressure-volume curve.
136 """
137 osmotic_potential_at_full_turgor: float = -2.0
138 relative_water_content_at_turgor_loss: float = 0.8
139 cell_wall_elasticity_exponent: float = 1.0
140 saturated_specific_water_content: float = 1.0
141 fixed_constant_capacitance: float = -1.0
142
143 @classmethod
144 def constant(cls, capacitance: float) -> 'HydraulicCapacitance':
145 """
146 Create a constant hydraulic capacitance.
147
148 Args:
149 capacitance: Constant capacitance (mol/m²/MPa). Must be > 0.
150 """
151 value = _validate_float(capacitance, 'capacitance', 'HydraulicCapacitance.constant')
152 if value <= 0.0:
153 raise ValueError(f"HydraulicCapacitance.constant() capacitance must be > 0, got {value}")
154 return cls(fixed_constant_capacitance=value)
156 def is_constant(self) -> bool:
157 """True if this is a constant capacitance rather than a pressure-volume curve."""
160 def validate(self, method_name: str = "HydraulicCapacitance") -> None:
161 """Raise ValueError if any parameter is outside its valid range."""
162 fixed = _validate_float(self.fixed_constant_capacitance, 'fixed_constant_capacitance', method_name)
163 if fixed <= 0.0 and fixed != -1.0:
164 raise ValueError(
165 f"{method_name}() fixed_constant_capacitance must be > 0, or -1 to use the "
166 f"pressure-volume curve parameters, got {fixed}"
167 )
168 osmotic = _validate_float(self.osmotic_potential_at_full_turgor, 'osmotic_potential_at_full_turgor', method_name)
169 if osmotic >= 0.0:
170 raise ValueError(f"{method_name}() osmotic_potential_at_full_turgor must be < 0, got {osmotic}")
172 'relative_water_content_at_turgor_loss', method_name)
173 if not 0.0 < turgor_loss < 1.0:
174 raise ValueError(
175 f"{method_name}() relative_water_content_at_turgor_loss must be between 0 and 1 (exclusive), "
176 f"got {turgor_loss}"
177 )
178 elasticity = _validate_float(self.cell_wall_elasticity_exponent, 'cell_wall_elasticity_exponent', method_name)
179 if elasticity <= 0.0:
180 raise ValueError(f"{method_name}() cell_wall_elasticity_exponent must be > 0, got {elasticity}")
181 saturated = _validate_float(self.saturated_specific_water_content, 'saturated_specific_water_content', method_name)
182 if saturated <= 0.0:
183 raise ValueError(f"{method_name}() saturated_specific_water_content must be > 0, got {saturated}")
184
185 def to_list(self) -> List[float]:
186 """Flat layout expected by the native interface."""
189 float(self.fixed_constant_capacitance)]
190
191
192def _make_conductance(method_name, saturated_conductance, potential_at_half_saturated, sensitivity,
193 temperature_dependence) -> HydraulicConductance:
194 if potential_at_half_saturated is None:
195 if sensitivity is not None:
196 raise ValueError(f"{method_name}() sensitivity requires potential_at_half_saturated")
197 conductance = HydraulicConductance(saturated_conductance, 0.0, 0.0, temperature_dependence)
198 else:
199 conductance = HydraulicConductance(saturated_conductance, potential_at_half_saturated,
200 5.0 if sensitivity is None else sensitivity, temperature_dependence)
201 conductance.validate(method_name)
202 return conductance
203
205def _make_capacitance(method_name, first, relative_water_content_at_turgor_loss, cell_wall_elasticity_exponent,
206 saturated_specific_water_content=None) -> HydraulicCapacitance:
207 if relative_water_content_at_turgor_loss is None:
208 if cell_wall_elasticity_exponent is not None or saturated_specific_water_content is not None:
209 raise ValueError(
210 f"{method_name}() a constant capacitance takes a single value; give "
211 f"relative_water_content_at_turgor_loss to use pressure-volume curve parameters"
212 )
213 value = _validate_float(first, 'capacitance', method_name)
214 if value <= 0.0:
215 raise ValueError(f"{method_name}() capacitance must be > 0, got {value}")
216 return HydraulicCapacitance(fixed_constant_capacitance=value)
217
218 capacitance = HydraulicCapacitance(
219 osmotic_potential_at_full_turgor=first,
220 relative_water_content_at_turgor_loss=relative_water_content_at_turgor_loss,
221 cell_wall_elasticity_exponent=1.0 if cell_wall_elasticity_exponent is None else cell_wall_elasticity_exponent,
222 saturated_specific_water_content=1.0 if saturated_specific_water_content is None else saturated_specific_water_content,
224 capacitance.validate(method_name)
225 return capacitance
226
227
229 """
230 Hydraulic conductance and capacitance parameters of the leaf, stem and root compartments.
231
232 A new instance holds the model defaults: a constant conductance of 0.5 mol/m²/s/MPa for each
233 compartment, and pressure-volume curve parameters of -2 MPa osmotic potential at full turgor,
234 0.8 relative water content at turgor loss and an elasticity exponent of 1.
235
236 Attributes:
237 LeafHydraulicConductance, StemHydraulicConductance, RootHydraulicConductance: HydraulicConductance
238 LeafHydraulicCapacitance: HydraulicCapacitance, or None when the leaf capacitance comes from the
239 species library (see leaf_capacitance_species)
240 StemHydraulicCapacitance, RootHydraulicCapacitance: HydraulicCapacitance
241 leaf_capacitance_species: Species name whose library leaf capacitance is used, or None
242
243 Example:
244 >>> coeffs = PlantHydraulicsModelCoefficients()
245 >>> coeffs.setLeafHydraulicConductance(0.05, -1.5, 2.0)
246 >>> coeffs.setStemHydraulicConductance(0.5)
247 >>> coeffs.setLeafHydraulicCapacitanceFromLibrary("pistachio")
248 """
249
250 def __init__(self):
251 self.LeafHydraulicConductance = HydraulicConductance()
252 self.StemHydraulicConductance = HydraulicConductance()
253 self.RootHydraulicConductance = HydraulicConductance()
254 self.LeafHydraulicCapacitance: Optional[HydraulicCapacitance] = HydraulicCapacitance()
255 self.StemHydraulicCapacitance = HydraulicCapacitance()
256 self.RootHydraulicCapacitance = HydraulicCapacitance()
257 self.leaf_capacitance_species: Optional[str] = None
258
259 def __repr__(self):
260 leaf_capacitance = (f"library '{self.leaf_capacitance_species}'" if self.leaf_capacitance_species
261 else repr(self.LeafHydraulicCapacitance))
262 return (f"PlantHydraulicsModelCoefficients(LeafHydraulicConductance={self.LeafHydraulicConductance!r}, "
263 f"StemHydraulicConductance={self.StemHydraulicConductance!r}, "
264 f"RootHydraulicConductance={self.RootHydraulicConductance!r}, "
265 f"LeafHydraulicCapacitance={leaf_capacitance}, "
266 f"StemHydraulicCapacitance={self.StemHydraulicCapacitance!r}, "
267 f"RootHydraulicCapacitance={self.RootHydraulicCapacitance!r})")
268
269 # Hydraulic conductance
270
271 def setLeafHydraulicConductance(self, saturated_conductance: float,
272 potential_at_half_saturated: Optional[float] = None,
273 sensitivity: Optional[float] = None,
274 temperature_dependence: bool = False) -> None:
275 """
276 Set leaf hydraulic conductance, constant or as a function of leaf water potential.
278 Args:
279 saturated_conductance: Conductance at a water potential of zero (mol/m²/s/MPa). Typical range 0.001 to 1.
280 With no other arguments this is a constant conductance.
281 potential_at_half_saturated: Water potential at which conductance is half its saturated value (MPa).
282 sensitivity: Steepness of the decline in conductance with water potential (unitless).
283 Defaults to 5 when potential_at_half_saturated is given.
284 temperature_dependence: If True, conductance is scaled by (T/298.15)^7.
285 """
287 'setLeafHydraulicConductance', saturated_conductance, potential_at_half_saturated, sensitivity,
288 temperature_dependence)
289
290 def setLeafHydraulicConductanceTemperatureDependence(self, temperature_dependence: bool) -> None:
291 """Toggle the temperature dependence of leaf hydraulic conductance, K(T) = K*(T/298.15)^7."""
292 self.LeafHydraulicConductance.temperature_dependence = _validate_bool(
293 temperature_dependence, 'temperature_dependence', 'setLeafHydraulicConductanceTemperatureDependence')
294
295 def setStemHydraulicConductance(self, saturated_conductance: float,
296 potential_at_half_saturated: Optional[float] = None,
297 sensitivity: Optional[float] = None,
298 temperature_dependence: bool = False) -> None:
299 """
300 Set stem hydraulic conductance, constant or as a function of stem water potential.
301
302 Args:
303 saturated_conductance: Conductance at a water potential of zero (mol/m²/s/MPa). Typical range 0.001 to 1.
304 With no other arguments this is a constant conductance.
305 potential_at_half_saturated: Water potential at which conductance is half its saturated value (MPa).
306 sensitivity: Steepness of the decline in conductance with water potential (unitless).
307 Defaults to 5 when potential_at_half_saturated is given.
308 temperature_dependence: If True, conductance is scaled by (T/298.15)^7.
309 """
311 'setStemHydraulicConductance', saturated_conductance, potential_at_half_saturated, sensitivity,
312 temperature_dependence)
313
314 def setStemHydraulicConductanceTemperatureDependence(self, temperature_dependence: bool) -> None:
315 """Toggle the temperature dependence of stem hydraulic conductance, K(T) = K*(T/298.15)^7."""
316 self.StemHydraulicConductance.temperature_dependence = _validate_bool(
317 temperature_dependence, 'temperature_dependence', 'setStemHydraulicConductanceTemperatureDependence')
318
319 def setRootHydraulicConductance(self, saturated_conductance: float,
320 potential_at_half_saturated: Optional[float] = None,
321 sensitivity: Optional[float] = None,
322 temperature_dependence: bool = False) -> None:
323 """
324 Set root hydraulic conductance, constant or as a function of root water potential.
325
326 Args:
327 saturated_conductance: Conductance at a water potential of zero (mol/m²/s/MPa). Typical range 0.001 to 1.
328 With no other arguments this is a constant conductance.
329 potential_at_half_saturated: Water potential at which conductance is half its saturated value (MPa).
330 sensitivity: Steepness of the decline in conductance with water potential (unitless).
331 Defaults to 5 when potential_at_half_saturated is given.
332 temperature_dependence: If True, conductance is scaled by (T/298.15)^7.
333 """
335 'setRootHydraulicConductance', saturated_conductance, potential_at_half_saturated, sensitivity,
336 temperature_dependence)
337
338 def setRootHydraulicConductanceTemperatureDependence(self, temperature_dependence: bool) -> None:
339 """Toggle the temperature dependence of root hydraulic conductance, K(T) = K*(T/298.15)^7."""
340 self.RootHydraulicConductance.temperature_dependence = _validate_bool(
341 temperature_dependence, 'temperature_dependence', 'setRootHydraulicConductanceTemperatureDependence')
342
343 # Hydraulic capacitance
344
345 def setLeafHydraulicCapacitance(self, osmotic_potential_at_full_turgor: float,
346 relative_water_content_at_turgor_loss: Optional[float] = None,
347 cell_wall_elasticity_exponent: Optional[float] = None,
348 saturated_specific_water_content: Optional[float] = None) -> None:
349 """
350 Set leaf hydraulic capacitance, constant or from pressure-volume curve parameters.
351
352 Called with a single argument, that value is a constant capacitance (mol/m²/MPa, typical range
353 0.005 to 0.5). Otherwise the arguments are pressure-volume curve parameters.
355 The model computes leaf relative water content, turgor pressure and osmotic potential from
356 the pressure-volume curve parameters. A constant capacitance resets those parameters to
357 their defaults (-2 MPa, 0.8, 1) and otherwise only changes the optional
358 "hydraulic_capacitance" output.
359
360 Args:
361 osmotic_potential_at_full_turgor: Osmotic potential at full turgor (MPa), or the constant
362 capacitance (mol/m²/MPa) when it is the only argument.
363 relative_water_content_at_turgor_loss: Relative water content at the turgor loss point (unitless).
364 Typical range 0.7 to 0.95.
365 cell_wall_elasticity_exponent: Exponent of the turgor pressure versus relative water content
366 relationship (unitless). Typical range 1 to 2. Defaults to 1.
367 saturated_specific_water_content: Saturated specific water content (mol/m²). Defaults to 1.
368 """
370 'setLeafHydraulicCapacitance', osmotic_potential_at_full_turgor, relative_water_content_at_turgor_loss,
371 cell_wall_elasticity_exponent, saturated_specific_water_content)
372 self.leaf_capacitance_species = None
373
374 def setLeafHydraulicCapacitanceFromLibrary(self, species: str) -> None:
375 """
376 Set leaf hydraulic capacitance from the species library of pressure-volume curve parameters.
377
378 The library values are held by the native plugin and applied when the coefficients are passed
379 to PlantHydraulicsModel.setModelCoefficients(). LeafHydraulicCapacitance stays None unless it
380 is later replaced by setLeafHydraulicCapacitance().
381
382 Args:
383 species: Species name. See pyhelios.PlantHydraulics.AVAILABLE_SPECIES.
384
385 Raises:
386 ValueError: If the species is not in the library
387 """
388 self.leaf_capacitance_species = _validate_species(species, 'setLeafHydraulicCapacitanceFromLibrary')
390
391 def setStemHydraulicCapacitance(self, osmotic_potential_at_full_turgor: float,
392 relative_water_content_at_turgor_loss: Optional[float] = None,
393 cell_wall_elasticity_exponent: Optional[float] = None) -> None:
394 """
395 Set stem hydraulic capacitance, constant or from pressure-volume curve parameters.
396
397 Called with a single argument, that value is a constant capacitance (typical range 0.005 to 2).
398 Stem capacitance is used only by non-steady-state runs, which require it to be constant;
399 pressure-volume curve parameters are accepted but never used by the model.
400
401 Args:
402 osmotic_potential_at_full_turgor: Osmotic potential at full turgor (MPa), or the constant
403 capacitance when it is the only argument.
404 relative_water_content_at_turgor_loss: Relative water content at the turgor loss point (unitless).
405 cell_wall_elasticity_exponent: Exponent of the turgor pressure versus relative water content
406 relationship (unitless). Defaults to 1.
407 """
409 'setStemHydraulicCapacitance', osmotic_potential_at_full_turgor, relative_water_content_at_turgor_loss,
410 cell_wall_elasticity_exponent)
412 def setRootHydraulicCapacitance(self, osmotic_potential_at_full_turgor: float,
413 relative_water_content_at_turgor_loss: Optional[float] = None,
414 cell_wall_elasticity_exponent: Optional[float] = None) -> None:
415 """
416 Set root hydraulic capacitance, constant or from pressure-volume curve parameters.
417
418 Called with a single argument, that value is a constant capacitance (typical range 0.005 to 2).
419 Root capacitance is used only by non-steady-state runs, which require it to be constant;
420 pressure-volume curve parameters are accepted but never used by the model.
421
422 Args:
423 osmotic_potential_at_full_turgor: Osmotic potential at full turgor (MPa), or the constant
424 capacitance when it is the only argument.
425 relative_water_content_at_turgor_loss: Relative water content at the turgor loss point (unitless).
426 cell_wall_elasticity_exponent: Exponent of the turgor pressure versus relative water content
427 relationship (unitless). Defaults to 1.
428 """
430 'setRootHydraulicCapacitance', osmotic_potential_at_full_turgor, relative_water_content_at_turgor_loss,
431 cell_wall_elasticity_exponent)
432
433 def validate(self, method_name: str = "PlantHydraulicsModelCoefficients") -> None:
434 """Raise ValueError if any compartment's parameters are missing, mistyped or out of range."""
435 for label in ('Leaf', 'Stem', 'Root'):
436 conductance = getattr(self, f'{label}HydraulicConductance')
437 if not isinstance(conductance, HydraulicConductance):
438 raise ValueError(
439 f"{method_name}() {label}HydraulicConductance must be a HydraulicConductance, "
440 f"got {type(conductance).__name__}"
441 )
442 conductance.validate(method_name)
443
444 if self.leaf_capacitance_species is not None:
446 for label in ('Leaf', 'Stem', 'Root'):
447 capacitance = getattr(self, f'{label}HydraulicCapacitance')
448 if label == 'Leaf' and self.leaf_capacitance_species is not None and capacitance is None:
449 continue
450 if not isinstance(capacitance, HydraulicCapacitance):
451 raise ValueError(
452 f"{method_name}() {label}HydraulicCapacitance must be a HydraulicCapacitance, "
453 f"got {type(capacitance).__name__}"
454 )
455 capacitance.validate(method_name)
456 if label != 'Leaf' and capacitance.saturated_specific_water_content != 1.0:
457 raise ValueError(
458 f"{method_name}() {label}HydraulicCapacitance.saturated_specific_water_content cannot be set; "
459 f"the model uses it only for leaves"
460 )
461
462 def to_list(self) -> List[float]:
463 """Flat layout expected by the native interface (see pyhelios_wrapper_planthydraulics.h)."""
464 leaf_capacitance = self.LeafHydraulicCapacitance or HydraulicCapacitance()
466 + self.RootHydraulicConductance.to_list() + leaf_capacitance.to_list())
467 for capacitance in (self.StemHydraulicCapacitance, self.RootHydraulicCapacitance):
468 values += [float(capacitance.osmotic_potential_at_full_turgor),
469 float(capacitance.relative_water_content_at_turgor_loss),
470 float(capacitance.cell_wall_elasticity_exponent),
471 float(capacitance.fixed_constant_capacitance)]
472 return values
473
474 @classmethod
475 def from_list(cls, values: Sequence[float]) -> 'PlantHydraulicsModelCoefficients':
476 """Build coefficients from the flat layout written by to_list()."""
477 if len(values) != hydraulics_wrapper.COEFFICIENT_SIZE:
478 raise ValueError(
479 f"PlantHydraulicsModelCoefficients.from_list() expects {hydraulics_wrapper.COEFFICIENT_SIZE} "
480 f"values, got {len(values)}"
481 )
482 v = [float(value) for value in values]
483 coeffs = cls()
484 for index, label in enumerate(('Leaf', 'Stem', 'Root')):
485 block = v[4 * index:4 * index + 4]
486 setattr(coeffs, f'{label}HydraulicConductance',
487 HydraulicConductance(block[0], block[1], block[2], block[3] != 0.0))
488 coeffs.LeafHydraulicCapacitance = HydraulicCapacitance(v[12], v[13], v[14], v[15], v[16])
489 coeffs.StemHydraulicCapacitance = HydraulicCapacitance(v[17], v[18], v[19], 1.0, v[20])
490 coeffs.RootHydraulicCapacitance = HydraulicCapacitance(v[21], v[22], v[23], 1.0, v[24])
491 return coeffs
492
493
495 """
496 High-level interface for plant hydraulics modeling.
497
498 The model computes the water potential of the root, stem and leaf compartments of a plant from
499 the transpiration of its leaves and a soil water potential set by the user. Stems and roots are
500 each one compartment per plant; leaf water potential, turgor pressure, osmotic potential and
501 relative water content are computed for each leaf primitive.
502
503 Leaves are grouped into plants by a "plantID", read from the object data of a primitive's parent
504 object or from the primitive's own data. run() groups the primitives it is given that have no
505 plantID into a new plant (see run()).
506
507 Input primitive data (float):
508 - "latent_flux" (W/m²): transpiration, e.g. from EnergyBalanceModel. A leaf without it
509 transpires nothing, and the plant's transpiration is the area-weighted mean over the
510 primitives that do have it (zero if none do).
511 - "temperature" (K, default 300): surface temperature, used for a temperature-dependent
512 leaf conductance
513 - "air_temperature" (K, default 298.15): stem temperature; the root is taken to be 5 K
514 cooler. Used for a temperature-dependent stem or root conductance, and read from one
515 primitive only, the one with the lowest UUID in the plant.
516 - "water_potential" (MPa, default -0.001): leaf water potential at which a water-potential
517 dependent leaf conductance is evaluated. It is also an output, so repeated runs start
518 from the previous result.
519 - "relative_water_content" (default 1.0): initial leaf relative water content, read by
520 non-steady-state runs only
521
522 Water-potential dependent conductances are evaluated once per run at the water potentials from
523 before the run (for the stem and root, those stored from the model's previous run of the plant,
524 or -0.001 MPa), so a steady-state run is not iterated to a self-consistent solution.
525
526 Output primitive data:
527 - "water_potential" (MPa), "turgor_pressure" (MPa), "osmotic_potential" (MPa),
528 "relative_water_content" (unitless)
529
530 Steady-state runs (timespan=0) are the supported mode; non-steady-state runs are under
531 construction in the native plugin.
532
533 System requirements:
534 - Cross-platform support (Windows, Linux, macOS)
535 - No GPU required
536 - No special dependencies
537 - Plant hydraulics plugin compiled into PyHelios
538
539 Example:
540 >>> with Context() as context:
541 ... # with: from pyhelios import Context; from pyhelios.types import vec2, vec3
542 ... leaf = context.addPatch(center=vec3(0, 0, 1), size=vec2(0.1, 0.1))
543 ... context.setPrimitiveDataFloat(leaf, "latent_flux", 100.0)
544 ...
545 ... with PlantHydraulicsModel(context) as hydraulics:
546 ... coeffs = PlantHydraulicsModelCoefficients()
547 ... coeffs.setLeafHydraulicCapacitanceFromLibrary("pistachio")
548 ... hydraulics.setModelCoefficients(coeffs)
549 ...
550 ... hydraulics.run([leaf])
551 ... psi_stem = hydraulics.getStemWaterPotential(leaf)
552 """
553
554 def __init__(self, context: Context):
555 """
556 Initialize PlantHydraulicsModel with graceful plugin handling.
557
558 Args:
559 context: Helios Context instance
560
561 Raises:
562 TypeError: If context is not a Context instance
563 PlantHydraulicsModelError: If the plant hydraulics plugin is not available
564 """
565 # Validate context type - use duck typing to handle import state issues during testing
566 if not (hasattr(context, '__class__') and
567 (isinstance(context, Context) or
568 context.__class__.__name__ == 'Context')):
569 raise TypeError(f"PlantHydraulicsModel requires a Context instance, got {type(context).__name__}")
570
571 self.context = context
572 self.hydraulics_model = None
573 # UUIDs holding their own coefficients, which getModelCoefficients() can read back
574 self._per_primitive_uuids = set()
575
576 # Check plugin availability using registry
577 registry = get_plugin_registry()
578
579 if not registry.is_plugin_available('planthydraulics'):
581 "PlantHydraulicsModel requires the 'planthydraulics' plugin which is not available.\n\n"
582 "The plant hydraulics plugin computes root, stem and leaf water potentials, turgor "
583 "pressure, osmotic potential and relative water content from leaf transpiration.\n\n"
584 "To enable plant hydraulics modeling, build PyHelios with all plugins:\n"
585 " build_scripts/build_helios\n\n"
586 "System requirements:\n"
587 " - Platforms: Windows, Linux, macOS\n"
588 " - No GPU required\n"
589 " - No special dependencies"
590 )
591
592 # Plugin is available - create plant hydraulics model
593 try:
594 self.hydraulics_model = hydraulics_wrapper.createPlantHydraulicsModel(context.getNativePtr())
595 if self.hydraulics_model is None:
597 "Failed to create PlantHydraulicsModel instance. "
598 "This may indicate a problem with the native library."
599 )
600 logger.info("PlantHydraulicsModel created successfully")
601
602 except Exception as e:
603 raise PlantHydraulicsModelError(f"Failed to initialize PlantHydraulicsModel: {e}")
604
605 def _check_context_alive(self):
606 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
607 check_context_alive(getattr(self, "context", None), "PlantHydraulicsModel")
608
609 def __enter__(self):
610 """Context manager entry."""
611 return self
612
613 def __exit__(self, exc_type, exc_value, traceback):
614 """Context manager exit with proper cleanup."""
615 if self.hydraulics_model is not None:
616 try:
617 hydraulics_wrapper.destroyPlantHydraulicsModel(self.hydraulics_model)
618 logger.debug("PlantHydraulicsModel destroyed successfully")
619 except Exception as e:
620 logger.warning(f"Error destroying PlantHydraulicsModel: {e}")
621 finally:
622 self.hydraulics_model = None
623
624 def __del__(self):
625 """Destructor to ensure C++ resources freed even without 'with' statement."""
626 if hasattr(self, 'hydraulics_model') and self.hydraulics_model is not None:
627 try:
628 hydraulics_wrapper.destroyPlantHydraulicsModel(self.hydraulics_model)
629 self.hydraulics_model = None
630 except Exception as e:
631 import warnings
632 warnings.warn(f"Error in PlantHydraulicsModel.__del__: {e}")
633
634 def getNativePtr(self):
635 """Get the native pointer for advanced operations."""
637
638 # Model coefficients
639
640 def setModelCoefficients(self, modelcoefficients: PlantHydraulicsModelCoefficients,
641 UUIDs: Optional[List[int]] = None) -> None:
642 """
643 Set the model coefficients for all primitives, or for a subset of primitives.
644
645 Setting coefficients for all primitives clears any coefficients previously set for
646 specific primitives.
647
648 Coefficients set for specific primitives apply to their leaf conductance and capacitance
649 only. Stem and root water potentials are always computed from the coefficients set for
650 all primitives.
652 Args:
653 modelcoefficients: Hydraulic conductance and capacitance parameters
654 UUIDs: Optional list of primitive UUIDs. If None, applies to all primitives.
655
656 Raises:
657 ValueError: If modelcoefficients is not a PlantHydraulicsModelCoefficients, a parameter is
658 out of range, or UUIDs is empty
659 PlantHydraulicsModelError: If operation fails
660
661 Example:
662 >>> coeffs = PlantHydraulicsModelCoefficients()
663 >>> coeffs.setLeafHydraulicConductance(0.05, -1.5, 2.0)
664 >>> hydraulics.setModelCoefficients(coeffs)
665 """
666 if not isinstance(modelcoefficients, PlantHydraulicsModelCoefficients):
667 raise ValueError(
668 "setModelCoefficients() modelcoefficients must be a PlantHydraulicsModelCoefficients, "
669 f"got {type(modelcoefficients).__name__}"
670 )
671 modelcoefficients.validate('setModelCoefficients')
672 uuid_list = None if UUIDs is None else _validate_uuids(UUIDs, 'setModelCoefficients')
673
675 try:
676 if uuid_list is None:
677 hydraulics_wrapper.setModelCoefficients(
678 self.hydraulics_model, modelcoefficients.to_list(), modelcoefficients.leaf_capacitance_species)
679 self._per_primitive_uuids.clear()
680 else:
681 hydraulics_wrapper.setModelCoefficientsForUUIDs(
682 self.hydraulics_model, modelcoefficients.to_list(), uuid_list,
683 modelcoefficients.leaf_capacitance_species)
684 self._per_primitive_uuids.update(uuid_list)
685 except Exception as e:
686 raise PlantHydraulicsModelError(f"Failed to set model coefficients: {e}")
687
688 def setModelCoefficientsFromLibrary(self, species: str, UUIDs: Optional[Union[int, List[int]]] = None) -> None:
689 """
690 Set the model coefficients from the species library.
691
692 The library provides leaf pressure-volume curve parameters; all other coefficients take
693 their default values.
694
695 Args:
696 species: Species name. See pyhelios.PlantHydraulics.AVAILABLE_SPECIES.
697 UUIDs: Optional primitive UUID or list of UUIDs. If None, applies to all primitives.
698
699 Raises:
700 ValueError: If the species is not in the library or UUIDs is empty
701 PlantHydraulicsModelError: If operation fails
702
703 Example:
704 >>> hydraulics.setModelCoefficientsFromLibrary("Walnut")
705 """
706 _validate_species(species, 'setModelCoefficientsFromLibrary')
707 if UUIDs is None:
708 uuid_list = None
709 elif isinstance(UUIDs, numbers.Integral) and not isinstance(UUIDs, bool):
710 uuid_list = [_validate_int(UUIDs, 'UUID', 'setModelCoefficientsFromLibrary', 0)]
711 else:
712 uuid_list = _validate_uuids(UUIDs, 'setModelCoefficientsFromLibrary')
713
715 try:
716 if uuid_list is None:
717 hydraulics_wrapper.setModelCoefficientsFromLibrary(self.hydraulics_model, species)
718 self._per_primitive_uuids.clear()
719 else:
720 hydraulics_wrapper.setModelCoefficientsFromLibraryForUUIDs(self.hydraulics_model, species, uuid_list)
721 self._per_primitive_uuids.update(uuid_list)
722 except Exception as e:
723 raise PlantHydraulicsModelError(f"Failed to set model coefficients from library: {e}")
724
725 def getModelCoefficients(self, UUID: int) -> PlantHydraulicsModelCoefficients:
726 """
727 Get the coefficients set for a specific primitive.
728
729 Only coefficients given for specific primitives can be read: those set with
730 setModelCoefficients() or setModelCoefficientsFromLibrary() and a UUIDs argument. A primitive
731 without its own coefficients uses the model-wide ones, which the native model does not
732 expose.
733
734 Leaf capacitance taken from the species library is returned as its pressure-volume curve
735 parameters, with leaf_capacitance_species set to None.
736
737 Args:
738 UUID: Primitive UUID
739
740 Returns:
741 A new PlantHydraulicsModelCoefficients holding the primitive's coefficients
742
743 Raises:
744 ValueError: If UUID is not a non-negative integer
745 PlantHydraulicsModelError: If no coefficients were set for this primitive, or the
746 operation fails
747
748 Example:
749 >>> hydraulics.setModelCoefficients(coeffs, UUIDs=[leaf_uuid])
750 >>> hydraulics.getModelCoefficients(leaf_uuid).LeafHydraulicConductance.saturated_conductance
751 """
752 uuid = _validate_int(UUID, 'UUID', 'getModelCoefficients', 0)
753
755 if uuid not in self._per_primitive_uuids:
757 f"getModelCoefficients() primitive {uuid} has no coefficients of its own; it uses the "
758 f"model-wide coefficients, which cannot be read back from the native model. Pass UUIDs to "
759 f"setModelCoefficients() or setModelCoefficientsFromLibrary() to give it its own."
760 )
761 try:
762 values = hydraulics_wrapper.getModelCoefficients(self.hydraulics_model, uuid)
763 except Exception as e:
764 raise PlantHydraulicsModelError(f"Failed to get model coefficients of primitive {uuid}: {e}")
765 return PlantHydraulicsModelCoefficients.from_list(values)
766
767 def getModelCoefficientsFromLibrary(self, species: str) -> PlantHydraulicsModelCoefficients:
768 """
769 Get the model coefficients of a species from the library.
770
771 The library provides leaf pressure-volume curve parameters; all other coefficients are at
772 their default values. The leaf capacitance is returned as its parameter values, with
773 leaf_capacitance_species set to None.
774
775 Args:
776 species: Species name. See pyhelios.PlantHydraulics.AVAILABLE_SPECIES.
778 Returns:
779 A new PlantHydraulicsModelCoefficients
780
781 Raises:
782 ValueError: If the species is not in the library
783 PlantHydraulicsModelError: If operation fails
784
785 Example:
786 >>> coeffs = hydraulics.getModelCoefficientsFromLibrary("Walnut")
787 >>> coeffs.LeafHydraulicCapacitance.osmotic_potential_at_full_turgor
788 -1.6386...
789 """
790 _validate_species(species, 'getModelCoefficientsFromLibrary')
791
793 try:
794 values = hydraulics_wrapper.getModelCoefficientsFromLibrary(self.hydraulics_model, species)
795 except Exception as e:
796 raise PlantHydraulicsModelError(f"Failed to get model coefficients from library: {e}")
797 return PlantHydraulicsModelCoefficients.from_list(values)
798
799 # Execution
800
801 def run(self, UUIDs: Optional[List[int]] = None, timespan: int = 0, timestep: int = 100) -> None:
802 """
803 Run the plant hydraulics model.
804
805 Primitives sharing a plantID are treated as the leaves of one plant, with one stem, root and
806 soil water potential. The model runs for every plant that the given primitives belong to,
807 and writes output data to all primitives in the Context with that plantID, not only to the
808 primitives given.
809
810 Given primitives with no plantID are grouped into one new plant: those that do not belong
811 to an object are added to a new polymesh object, and all of them are given the primitive
812 data "plantID", set to that object's ID. Primitives that already belong to an object stay
813 in it.
814
815 Args:
816 UUIDs: Optional list of primitive UUIDs selecting the plants to run. If None, runs for
817 all primitives in the Context; primitives with no plantID, including any that are
818 not leaves, are then grouped into a single plant.
819 timespan: Duration of the simulation (s). 0 gives the steady-state solution.
820 timestep: Timestep of a non-steady-state simulation (s).
821
822 Raises:
823 ValueError: If UUIDs is empty, timespan is negative, or timestep is not positive
824 PlantHydraulicsModelError: If calculation fails, including when every primitive without a
825 plantID already belongs to an object, so that no polymesh object can be created
826
827 Note:
828 Non-steady-state runs (timespan > 0) are under construction in the native plugin, and
829 require constant stem and root capacitances.
830
831 Example:
832 >>> hydraulics.run(leaf_uuids) # steady state, the plants these leaves belong to
833 """
834 uuid_list = None if UUIDs is None else _validate_uuids(UUIDs, 'run')
835 timespan = _validate_int(timespan, 'timespan', 'run', 0)
836 timestep = _validate_int(timestep, 'timestep', 'run', 1)
837
839 try:
840 if uuid_list is None:
841 hydraulics_wrapper.run(self.hydraulics_model, timespan, timestep)
842 else:
843 hydraulics_wrapper.runForUUIDs(self.hydraulics_model, uuid_list, timespan, timestep)
844 except Exception as e:
845 raise PlantHydraulicsModelError(f"Failed to run plant hydraulics model: {e}")
846
847 def outputConductancePrimitiveData(self, toggle: bool) -> None:
848 """
849 Toggle writing of the optional primitive data "hydraulic_conductance" (mol/m²/s/MPa).
850
851 The data is written by non-steady-state runs only.
852
853 Args:
854 toggle: True to write the data
855 """
856 _validate_bool(toggle, 'toggle', 'outputConductancePrimitiveData')
858 try:
859 hydraulics_wrapper.outputConductancePrimitiveData(self.hydraulics_model, toggle)
860 except Exception as e:
861 raise PlantHydraulicsModelError(f"Failed to set conductance output: {e}")
862
863 def outputCapacitancePrimitiveData(self, toggle: bool) -> None:
864 """
865 Toggle writing of the optional primitive data "hydraulic_capacitance" (mol/m²/MPa).
866
867 The data is written by non-steady-state runs only.
868
869 Args:
870 toggle: True to write the data
871 """
872 _validate_bool(toggle, 'toggle', 'outputCapacitancePrimitiveData')
874 try:
875 hydraulics_wrapper.outputCapacitancePrimitiveData(self.hydraulics_model, toggle)
876 except Exception as e:
877 raise PlantHydraulicsModelError(f"Failed to set capacitance output: {e}")
878
879 # Plant grouping
880
881 def groupPrimitivesIntoPlantObject(self, UUIDs: List[int]) -> int:
882 """
883 Group leaf primitives into a plant that shares stem, root and soil hydraulics.
884
885 Those of the primitives that do not belong to an object are added to a new polymesh object.
886 All of them are given the primitive data "plantID", set to that object's ID; primitives
887 that already belong to an object stay in it.
888
889 Args:
890 UUIDs: Leaf primitive UUIDs
891
892 Returns:
893 The plantID assigned to the primitives
894
895 Raises:
896 ValueError: If UUIDs is empty, or any of the primitives already has a plantID
897 PlantHydraulicsModelError: If all of the primitives already belong to an object, so that
898 no polymesh object can be created
899 """
900 uuid_list = _validate_uuids(UUIDs, 'groupPrimitivesIntoPlantObject')
902 try:
903 ungrouped = hydraulics_wrapper.getPrimitivesWithoutPlantID(self.hydraulics_model, uuid_list)
904 except Exception as e:
905 raise PlantHydraulicsModelError(f"Failed to group primitives into plant object: {e}")
906 already_grouped = sorted(set(uuid_list) - set(ungrouped))
907 if already_grouped:
908 raise ValueError(
909 "groupPrimitivesIntoPlantObject() primitives already have a plantID: "
910 f"{already_grouped[:10]}{'...' if len(already_grouped) > 10 else ''}"
911 )
912 try:
913 hydraulics_wrapper.groupPrimitivesIntoPlantObject(self.hydraulics_model, uuid_list)
914 return hydraulics_wrapper.getPlantIDForUUIDs(self.hydraulics_model, uuid_list)
915 except Exception as e:
916 raise PlantHydraulicsModelError(f"Failed to group primitives into plant object: {e}")
917
918 def getPlantID(self, UUIDs: Union[int, List[int]]) -> int:
919 """
920 Get the plantID of a primitive, or the plantID shared by a list of primitives.
921
922 Args:
923 UUIDs: Primitive UUID or list of UUIDs
924
925 Returns:
926 The plantID
927
928 Raises:
929 ValueError: If UUIDs is empty
930 PlantHydraulicsModelError: If a primitive has no plantID, or the primitives do not share one
931 """
932 single, uuid_list = self._single_or_list(UUIDs, 'getPlantID')
934 try:
935 if single:
936 return hydraulics_wrapper.getPlantID(self.hydraulics_model, uuid_list[0])
937 self._require_shared_plant(uuid_list)
938 return hydraulics_wrapper.getPlantIDForUUIDs(self.hydraulics_model, uuid_list)
939 except Exception as e:
940 raise PlantHydraulicsModelError(f"Failed to get plantID: {e}")
941
942 def _require_shared_plant(self, uuid_list: List[int]) -> None:
943 # The native list overloads compare only the first and last primitive.
944 plant_ids = hydraulics_wrapper.getUniquePlantIDs(self.hydraulics_model, uuid_list)
945 if len(plant_ids) > 1:
946 raise ValueError(f"UUIDs do not share the same plantID; found plantIDs {sorted(plant_ids)}")
947
948 def getUniquePlantIDs(self, UUIDs: List[int]) -> List[int]:
949 """
950 Get the unique plantIDs of a list of primitives.
951
952 Args:
953 UUIDs: Primitive UUIDs
954
955 Returns:
956 List of unique plantIDs
958 Raises:
959 ValueError: If UUIDs is empty
960 PlantHydraulicsModelError: If a primitive has no plantID
961 """
962 uuid_list = _validate_uuids(UUIDs, 'getUniquePlantIDs')
964 try:
965 return hydraulics_wrapper.getUniquePlantIDs(self.hydraulics_model, uuid_list)
966 except Exception as e:
967 raise PlantHydraulicsModelError(f"Failed to get unique plantIDs: {e}")
969 def getPrimitivesByPlantID(self, plantID: int) -> List[int]:
970 """
971 Get the primitives belonging to a plant.
972
973 Args:
974 plantID: Plant identifier
975
976 Returns:
977 List of primitive UUIDs; empty if no primitive has this plantID
978 """
979 if isinstance(plantID, bool) or not isinstance(plantID, numbers.Integral):
980 raise ValueError(f"getPrimitivesByPlantID() plantID must be an integer, got {type(plantID).__name__}")
982 try:
983 return hydraulics_wrapper.getPrimitivesByPlantID(self.hydraulics_model, int(plantID))
984 except Exception as e:
985 raise PlantHydraulicsModelError(f"Failed to get primitives by plantID: {e}")
986
987 def getPrimitivesWithoutPlantID(self, UUIDs: List[int]) -> List[int]:
988 """
989 Get the primitives that have no plantID, in either parent object data or primitive data.
990
991 Args:
992 UUIDs: Primitive UUIDs to check
993
994 Returns:
995 List of primitive UUIDs without a plantID
996
997 Raises:
998 ValueError: If UUIDs is empty
999 """
1000 uuid_list = _validate_uuids(UUIDs, 'getPrimitivesWithoutPlantID')
1002 try:
1003 return hydraulics_wrapper.getPrimitivesWithoutPlantID(self.hydraulics_model, uuid_list)
1004 except Exception as e:
1005 raise PlantHydraulicsModelError(f"Failed to get primitives without plantID: {e}")
1006
1007 # Water potentials
1008
1009 @staticmethod
1010 def _single_or_list(UUIDs, method_name: str):
1011 if isinstance(UUIDs, numbers.Integral) and not isinstance(UUIDs, bool):
1012 return True, [_validate_int(UUIDs, 'UUID', method_name, 0)]
1013 return False, _validate_uuids(UUIDs, method_name)
1014
1015 def _get_water_potential(self, compartment: str, UUIDs, method_name: str) -> float:
1016 single, uuid_list = self._single_or_list(UUIDs, method_name)
1018 try:
1019 if single:
1020 return hydraulics_wrapper.getWaterPotential(self.hydraulics_model, compartment, uuid_list[0])
1021 self._require_shared_plant(uuid_list)
1022 return hydraulics_wrapper.getWaterPotentialForUUIDs(self.hydraulics_model, compartment, uuid_list)
1023 except Exception as e:
1024 raise PlantHydraulicsModelError(f"Failed to get {compartment.lower()} water potential: {e}")
1026 def _get_water_potential_of_plant(self, compartment: str, plantID, method_name: str) -> float:
1027 plant_id = _validate_int(plantID, 'plantID', method_name, 0)
1029 try:
1030 return hydraulics_wrapper.getWaterPotentialOfPlant(self.hydraulics_model, compartment, plant_id)
1031 except Exception as e:
1032 raise PlantHydraulicsModelError(f"Failed to get {compartment.lower()} water potential of plant: {e}")
1033
1034 def getStemWaterPotential(self, UUIDs: Union[int, List[int]]) -> float:
1035 """
1036 Get the stem water potential (MPa) of the plant that a leaf primitive, or a list of leaf
1037 primitives of one plant, belongs to.
1038
1039 Raises:
1040 PlantHydraulicsModelError: If the model has not been run for the plant
1041 """
1042 return self._get_water_potential('Stem', UUIDs, 'getStemWaterPotential')
1043
1044 def getStemWaterPotentialOfPlant(self, plantID: int) -> float:
1045 """
1046 Get the stem water potential (MPa) of a plant.
1047
1048 Raises:
1049 PlantHydraulicsModelError: If the model has not been run for the plant
1050 """
1051 return self._get_water_potential_of_plant('Stem', plantID, 'getStemWaterPotentialOfPlant')
1053 def getRootWaterPotential(self, UUIDs: Union[int, List[int]]) -> float:
1054 """
1055 Get the root water potential (MPa) of the plant that a leaf primitive, or a list of leaf
1056 primitives of one plant, belongs to.
1057
1058 Raises:
1059 PlantHydraulicsModelError: If the model has not been run for the plant
1060 """
1061 return self._get_water_potential('Root', UUIDs, 'getRootWaterPotential')
1062
1063 def getRootWaterPotentialOfPlant(self, plantID: int) -> float:
1064 """
1065 Get the root water potential (MPa) of a plant.
1066
1067 Raises:
1068 PlantHydraulicsModelError: If the model has not been run for the plant
1069 """
1070 return self._get_water_potential_of_plant('Root', plantID, 'getRootWaterPotentialOfPlant')
1071
1072 def getSoilWaterPotential(self, UUIDs: Union[int, List[int]]) -> float:
1073 """
1074 Get the soil water potential (MPa) of the plant that a leaf primitive, or a list of leaf
1075 primitives of one plant, belongs to.
1077 Raises:
1078 PlantHydraulicsModelError: If no soil water potential has been set and the model has not been
1079 run for the plant
1080 """
1081 return self._get_water_potential('Soil', UUIDs, 'getSoilWaterPotential')
1082
1083 def getSoilWaterPotentialOfPlant(self, plantID: int) -> float:
1084 """
1085 Get the soil water potential (MPa) of a plant.
1087 Raises:
1088 PlantHydraulicsModelError: If no soil water potential has been set and the model has not been
1089 run for the plant
1090 """
1091 return self._get_water_potential_of_plant('Soil', plantID, 'getSoilWaterPotentialOfPlant')
1092
1093 def setSoilWaterPotentialOfPlant(self, plantID: int, soil_water_potential: float) -> None:
1094 """
1095 Set the soil water potential of a plant.
1096
1097 Plants with no soil water potential set use -0.001 MPa.
1098
1099 Args:
1100 plantID: Plant identifier
1101 soil_water_potential: Soil water potential (MPa)
1102
1103 Example:
1104 >>> plantID = hydraulics.getPlantID(leaf_uuids)
1105 >>> hydraulics.setSoilWaterPotentialOfPlant(plantID, -0.05)
1106 """
1107 plant_id = _validate_int(plantID, 'plantID', 'setSoilWaterPotentialOfPlant', 0)
1108 potential = _validate_float(soil_water_potential, 'soil_water_potential', 'setSoilWaterPotentialOfPlant')
1110 try:
1111 hydraulics_wrapper.setSoilWaterPotentialOfPlant(self.hydraulics_model, plant_id, potential)
1112 except Exception as e:
1113 raise PlantHydraulicsModelError(f"Failed to set soil water potential: {e}")
1114
1115 # Pressure-volume and vulnerability curves
1117 @staticmethod
1118 def _validate_curve_args(coeffs, relative_water_content, method_name: str) -> float:
1119 if not isinstance(coeffs, HydraulicCapacitance):
1120 raise ValueError(f"{method_name}() coeffs must be a HydraulicCapacitance, got {type(coeffs).__name__}")
1121 coeffs.validate(method_name)
1122 water_content = _validate_float(relative_water_content, 'relative_water_content', method_name)
1123 if not 0.0 < water_content <= 1.0:
1124 raise ValueError(f"{method_name}() relative_water_content must be in (0, 1], got {water_content}")
1125 return water_content
1126
1127 @staticmethod
1128 def computeOsmoticPotential(coeffs: HydraulicCapacitance, relative_water_content: float) -> float:
1129 """
1130 Compute osmotic potential (MPa) at a relative water content from pressure-volume curve parameters.
1131
1132 Args:
1133 coeffs: Pressure-volume curve parameters
1134 relative_water_content: Relative water content (unitless), in (0, 1]
1135 """
1136 water_content = PlantHydraulicsModel._validate_curve_args(coeffs, relative_water_content, 'computeOsmoticPotential')
1137 return hydraulics_wrapper.computeOsmoticPotential(coeffs.to_list(), water_content)
1138
1139 @staticmethod
1140 def computeTurgorPressure(coeffs: HydraulicCapacitance, relative_water_content: float) -> float:
1141 """
1142 Compute turgor pressure (MPa) at a relative water content from pressure-volume curve parameters.
1143
1144 Args:
1145 coeffs: Pressure-volume curve parameters
1146 relative_water_content: Relative water content (unitless), in (0, 1]
1147 """
1148 water_content = PlantHydraulicsModel._validate_curve_args(coeffs, relative_water_content, 'computeTurgorPressure')
1149 return hydraulics_wrapper.computeTurgorPressure(coeffs.to_list(), water_content)
1150
1151 @staticmethod
1152 def computeWaterPotential(coeffs: HydraulicCapacitance, relative_water_content: float) -> float:
1153 """
1154 Compute water potential (MPa) at a relative water content, the sum of turgor pressure and
1155 osmotic potential from pressure-volume curve parameters.
1156
1157 Args:
1158 coeffs: Pressure-volume curve parameters
1159 relative_water_content: Relative water content (unitless), in (0, 1]
1160 """
1161 water_content = PlantHydraulicsModel._validate_curve_args(coeffs, relative_water_content, 'computeWaterPotential')
1162 return hydraulics_wrapper.computeWaterPotential(coeffs.to_list(), water_content)
1163
1164 @staticmethod
1165 def computeCapacitance(coeffs: HydraulicCapacitance, relative_water_content: float) -> float:
1166 """
1167 Compute hydraulic capacitance (mol/m²/MPa) at a relative water content.
1168
1169 Returns the constant capacitance if coeffs holds one, otherwise the saturated specific water
1170 content divided by the slope of the pressure-volume curve.
1171
1172 Args:
1173 coeffs: Hydraulic capacitance parameters
1174 relative_water_content: Relative water content (unitless), in (0.001, 1]
1175 """
1176 water_content = PlantHydraulicsModel._validate_curve_args(coeffs, relative_water_content, 'computeCapacitance')
1177 if not coeffs.is_constant() and water_content <= 0.001:
1178 raise ValueError(f"computeCapacitance() relative_water_content must be > 0.001, got {water_content}")
1179 return hydraulics_wrapper.computeCapacitance(coeffs.to_list(), water_content)
1180
1181 @staticmethod
1182 def computeConductance(coeffs: HydraulicConductance, water_potential: float, temperature: float = 298.15) -> float:
1183 """
1184 Compute hydraulic conductance (mol/m²/s/MPa) at a water potential and temperature.
1185
1186 Args:
1187 coeffs: Hydraulic conductance parameters
1188 water_potential: Water potential (MPa)
1189 temperature: Temperature (K); used only if coeffs has temperature dependence enabled
1190 """
1191 if not isinstance(coeffs, HydraulicConductance):
1192 raise ValueError(f"computeConductance() coeffs must be a HydraulicConductance, got {type(coeffs).__name__}")
1193 coeffs.validate('computeConductance')
1194 potential = _validate_float(water_potential, 'water_potential', 'computeConductance')
1195 temperature_K = _validate_float(temperature, 'temperature', 'computeConductance')
1196 if temperature_K <= 0.0:
1197 raise ValueError(f"computeConductance() temperature must be > 0 K, got {temperature_K}")
1198 return hydraulics_wrapper.computeConductance(coeffs.to_list(), potential, temperature_K)
1199
1200 def is_available(self) -> bool:
1201 """
1202 Check if PlantHydraulicsModel is available in current build.
1203
1204 Returns:
1205 True if plugin is available, False otherwise
1206 """
1207 registry = get_plugin_registry()
1208 return registry.is_plugin_available('planthydraulics')
1209
1210
1211# Convenience function
1212def create_plant_hydraulics_model(context: Context) -> PlantHydraulicsModel:
1213 """
1214 Create PlantHydraulicsModel instance with context.
1215
1216 Args:
1217 context: Helios Context
1218
1219 Returns:
1220 PlantHydraulicsModel instance
1221 """
1222 return PlantHydraulicsModel(context)
Hydraulic capacitance of a leaf, stem or root, from pressure-volume curve parameters.
bool is_constant(self)
True if this is a constant capacitance rather than a pressure-volume curve.
float cell_wall_elasticity_exponent
Exponent of the turgor pressure versus relative water content relationship (unitless).
float saturated_specific_water_content
Specific water content at a relative water content of 1 (mol/m²).
None validate(self, str method_name="HydraulicCapacitance")
Raise ValueError if any parameter is outside its valid range.
List[float] to_list(self)
Flat layout expected by the native interface.
float fixed_constant_capacitance
Constant capacitance (mol/m²/MPa), or -1 to use the pressure-volume curve.
float relative_water_content_at_turgor_loss
Relative water content at the turgor loss point (unitless), in (0, 1).
'HydraulicCapacitance' constant(cls, float capacitance)
Create a constant hydraulic capacitance.
float osmotic_potential_at_full_turgor
Osmotic potential at full turgor (MPa).
Hydraulic conductance of a leaf, stem or root as a function of water potential.
float potential_at_half_saturated
Water potential at which conductance is half its saturated value (MPa).
float saturated_conductance
Conductance at a water potential of zero (mol/m²/s/MPa).
float sensitivity
Steepness of the decline in conductance around potential_at_half_saturated (unitless).
None validate(self, str method_name="HydraulicConductance")
Raise ValueError if any parameter is outside its valid range.
bool temperature_dependence
If True, conductance is scaled by (T/298.15)^7.
List[float] to_list(self)
Flat layout expected by the native interface.
Hydraulic conductance and capacitance parameters of the leaf, stem and root compartments.
None setStemHydraulicCapacitance(self, float osmotic_potential_at_full_turgor, Optional[float] relative_water_content_at_turgor_loss=None, Optional[float] cell_wall_elasticity_exponent=None)
Set stem hydraulic capacitance, constant or from pressure-volume curve parameters.
List[float] to_list(self)
Flat layout expected by the native interface (see pyhelios_wrapper_planthydraulics....
None setRootHydraulicConductanceTemperatureDependence(self, bool temperature_dependence)
Toggle the temperature dependence of root hydraulic conductance, K(T) = K*(T/298.15)^7.
None setStemHydraulicConductance(self, float saturated_conductance, Optional[float] potential_at_half_saturated=None, Optional[float] sensitivity=None, bool temperature_dependence=False)
Set stem hydraulic conductance, constant or as a function of stem water potential.
None setLeafHydraulicCapacitanceFromLibrary(self, str species)
Set leaf hydraulic capacitance from the species library of pressure-volume curve parameters.
Optional[HydraulicCapacitance] LeafHydraulicCapacitance
HydraulicCapacitance, or None when the leaf capacitance comes from the.
Optional[str] leaf_capacitance_species
Species name whose library leaf capacitance is used, or None.
None setStemHydraulicConductanceTemperatureDependence(self, bool temperature_dependence)
Toggle the temperature dependence of stem hydraulic conductance, K(T) = K*(T/298.15)^7.
None setLeafHydraulicConductance(self, float saturated_conductance, Optional[float] potential_at_half_saturated=None, Optional[float] sensitivity=None, bool temperature_dependence=False)
Set leaf hydraulic conductance, constant or as a function of leaf water potential.
'PlantHydraulicsModelCoefficients' from_list(cls, Sequence[float] values)
Build coefficients from the flat layout written by to_list().
None setLeafHydraulicCapacitance(self, float osmotic_potential_at_full_turgor, Optional[float] relative_water_content_at_turgor_loss=None, Optional[float] cell_wall_elasticity_exponent=None, Optional[float] saturated_specific_water_content=None)
Set leaf hydraulic capacitance, constant or from pressure-volume curve parameters.
None setRootHydraulicCapacitance(self, float osmotic_potential_at_full_turgor, Optional[float] relative_water_content_at_turgor_loss=None, Optional[float] cell_wall_elasticity_exponent=None)
Set root hydraulic capacitance, constant or from pressure-volume curve parameters.
None setRootHydraulicConductance(self, float saturated_conductance, Optional[float] potential_at_half_saturated=None, Optional[float] sensitivity=None, bool temperature_dependence=False)
Set root hydraulic conductance, constant or as a function of root water potential.
None validate(self, str method_name="PlantHydraulicsModelCoefficients")
Raise ValueError if any compartment's parameters are missing, mistyped or out of range.
None setLeafHydraulicConductanceTemperatureDependence(self, bool temperature_dependence)
Toggle the temperature dependence of leaf hydraulic conductance, K(T) = K*(T/298.15)^7.
Exception raised for PlantHydraulics-specific errors.
High-level interface for plant hydraulics modeling.
float _validate_curve_args(coeffs, relative_water_content, str method_name)
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
float computeWaterPotential(HydraulicCapacitance coeffs, float relative_water_content)
Compute water potential (MPa) at a relative water content, the sum of turgor pressure and osmotic pot...
float getStemWaterPotential(self, Union[int, List[int]] UUIDs)
Get the stem water potential (MPa) of the plant that a leaf primitive, or a list of leaf primitives o...
float _get_water_potential_of_plant(self, str compartment, plantID, str method_name)
float _get_water_potential(self, str compartment, UUIDs, str method_name)
None outputCapacitancePrimitiveData(self, bool toggle)
Toggle writing of the optional primitive data "hydraulic_capacitance" (mol/m²/MPa).
float getStemWaterPotentialOfPlant(self, int plantID)
Get the stem water potential (MPa) of a plant.
List[int] getPrimitivesWithoutPlantID(self, List[int] UUIDs)
Get the primitives that have no plantID, in either parent object data or primitive data.
float getRootWaterPotential(self, Union[int, List[int]] UUIDs)
Get the root water potential (MPa) of the plant that a leaf primitive, or a list of leaf primitives o...
getNativePtr(self)
Get the native pointer for advanced operations.
bool is_available(self)
Check if PlantHydraulicsModel is available in current build.
None setModelCoefficients(self, PlantHydraulicsModelCoefficients modelcoefficients, Optional[List[int]] UUIDs=None)
Set the model coefficients for all primitives, or for a subset of primitives.
PlantHydraulicsModelCoefficients getModelCoefficients(self, int UUID)
Get the coefficients set for a specific primitive.
None setModelCoefficientsFromLibrary(self, str species, Optional[Union[int, List[int]]] UUIDs=None)
Set the model coefficients from the species library.
float getSoilWaterPotentialOfPlant(self, int plantID)
Get the soil water potential (MPa) of a plant.
int groupPrimitivesIntoPlantObject(self, List[int] UUIDs)
Group leaf primitives into a plant that shares stem, root and soil hydraulics.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
int getPlantID(self, Union[int, List[int]] UUIDs)
Get the plantID of a primitive, or the plantID shared by a list of primitives.
float computeTurgorPressure(HydraulicCapacitance coeffs, float relative_water_content)
Compute turgor pressure (MPa) at a relative water content from pressure-volume curve parameters.
List[int] getPrimitivesByPlantID(self, int plantID)
Get the primitives belonging to a plant.
None _require_shared_plant(self, List[int] uuid_list)
None outputConductancePrimitiveData(self, bool toggle)
Toggle writing of the optional primitive data "hydraulic_conductance" (mol/m²/s/MPa).
_check_context_alive(self)
Raise if the owning Context has been destroyed (see Context.check_context_alive).
PlantHydraulicsModelCoefficients getModelCoefficientsFromLibrary(self, str species)
Get the model coefficients of a species from the library.
float getRootWaterPotentialOfPlant(self, int plantID)
Get the root water potential (MPa) of a plant.
float getSoilWaterPotential(self, Union[int, List[int]] UUIDs)
Get the soil water potential (MPa) of the plant that a leaf primitive, or a list of leaf primitives o...
float computeConductance(HydraulicConductance coeffs, float water_potential, float temperature=298.15)
Compute hydraulic conductance (mol/m²/s/MPa) at a water potential and temperature.
float computeCapacitance(HydraulicCapacitance coeffs, float relative_water_content)
Compute hydraulic capacitance (mol/m²/MPa) at a relative water content.
float computeOsmoticPotential(HydraulicCapacitance coeffs, float relative_water_content)
Compute osmotic potential (MPa) at a relative water content from pressure-volume curve parameters.
None run(self, Optional[List[int]] UUIDs=None, int timespan=0, int timestep=100)
Run the plant hydraulics model.
List[int] getUniquePlantIDs(self, List[int] UUIDs)
Get the unique plantIDs of a list of primitives.
None setSoilWaterPotentialOfPlant(self, int plantID, float soil_water_potential)
Set the soil water potential of a plant.
Exception classes for PyHelios library.
Definition exceptions.py:10
bool _validate_bool(value, str name, str method_name)
HydraulicCapacitance _make_capacitance(method_name, first, relative_water_content_at_turgor_loss, cell_wall_elasticity_exponent, saturated_specific_water_content=None)
str _validate_species(species, str method_name)
List[int] _validate_uuids(UUIDs, str method_name)
HydraulicConductance _make_conductance(method_name, saturated_conductance, potential_at_half_saturated, sensitivity, temperature_dependence)
PlantHydraulicsModel create_plant_hydraulics_model(Context context)
Create PlantHydraulicsModel instance with context.
int _validate_int(value, str name, str method_name, int minimum)
float _validate_float(value, str name, str method_name)