0.1.22
Loading...
Searching...
No Matches
photosynthesis.py
Go to the documentation of this file.
1"""
2Photosynthesis parameter structures and data classes for PyHelios.
3
4This module provides Python data structures that mirror the C++ parameter
5classes used by the PhotosynthesisModel plugin, with proper defaults and
6validation support.
7"""
8
9from dataclasses import dataclass, field
10from typing import List, Optional, Union
11import math
12
13# Known species in the photosynthesis library (21 species with aliases)
14PHOTOSYNTHESIS_SPECIES = [
15 "Almond", "Apple", "Cherry", "Prune", "Pear",
16 "PistachioFemale", "PistachioMale", "Walnut",
17 "Grape", # cv. Cabernet Sauvignon
18 "Elderberry", "Toyon", "Big_Leaf_Maple", "Western_Redbud", "Baylaurel", "Olive",
19 "EasternRedbudSunlit", "EasternRedbudShaded"
20]
21
22# Species aliases for case-insensitive and format-flexible lookup
23SPECIES_ALIASES = {
24 # Standard names (already in PHOTOSYNTHESIS_SPECIES)
25 "almond": "Almond",
26 "apple": "Apple",
27 "cherry": "Cherry",
28 "prune": "Prune",
29 "pear": "Pear",
30 "pistachiofemale": "PistachioFemale",
31 "pistachiomale": "PistachioMale",
32 "walnut": "Walnut",
33 "grape": "Grape",
34 "elderberry": "Elderberry",
35 "toyon": "Toyon",
36 "big_leaf_maple": "Big_Leaf_Maple",
37 "western_redbud": "Western_Redbud",
38 "baylaurel": "Baylaurel",
39 "olive": "Olive",
40 "easternredbudsunlit": "EasternRedbudSunlit",
41 "easternredbudshaded": "EasternRedbudShaded",
42
43 # Common aliases and variations
44 "bigleafmaple": "Big_Leaf_Maple",
45 "bigmaple": "Big_Leaf_Maple",
46 "westernredbud": "Western_Redbud",
47 "redbud": "Western_Redbud",
48 "easternredbud": "EasternRedbudSunlit", # Default to sunlit
49 "pistachio": "PistachioFemale", # Default to female
50 "cabernet": "Grape",
51 "cabernetSauvignon": "Grape",
52 "grapevine": "Grape"
53}
54
55
56@dataclass
58 """
59 Temperature response parameters for photosynthetic processes.
60
61 These parameters define how photosynthetic rates vary with temperature
62 using the modified Arrhenius equation.
63
64 Attributes:
65 value_at_25C: Value of the parameter at 25°C
66 dHa: Activation energy (rate of increase parameter)
67 dHd: Deactivation energy (rate of decrease parameter)
68 Topt: Optimum temperature in Kelvin (10000K means no optimum)
69 """
70 value_at_25C: float = 100.0
71 dHa: float = 60.0
72 dHd: float = 600.0
73 Topt: float = 10000.0 # Very high = no temperature optimum
74
75 def __post_init__(self):
76 """Validate parameter values after initialization."""
77 if not math.isfinite(self.value_at_25C):
78 raise ValueError("value_at_25C must be finite")
79 if not math.isfinite(self.dHa) or self.dHa < 0:
80 raise ValueError("dHa must be finite and non-negative")
81 if not math.isfinite(self.dHd) or self.dHd < 0:
82 raise ValueError("dHd must be finite and non-negative")
83 if not math.isfinite(self.Topt) or self.Topt < 0:
84 raise ValueError("Topt must be finite and non-negative")
85
86
87@dataclass
89 """
90 Empirical photosynthesis model coefficients.
91
92 This model uses empirical relationships to estimate photosynthetic
93 rates based on environmental conditions.
94
95 Attributes:
96 Tref: Reference temperature (K)
97 Ci_ref: Reference CO2 concentration (μmol CO2/mol air)
98 Asat: Light-saturated photosynthetic rate (μmol/m²/s)
99 theta: Half-saturation light level (W/m²)
100 Tmin: Minimum temperature for photosynthesis (K)
101 Topt: Optimum temperature for photosynthesis (K)
102 q: Temperature response parameter (unitless)
103 R: Respiration temperature coefficient (μmol·K^0.5/m²/s)
104 ER: Respiration activation energy (1/K)
105 kC: CO2 response coefficient (unitless)
106 """
107 Tref: float = 298.0 # K
108 Ci_ref: float = 290.0 # μmol CO2/mol air
109 Asat: float = 18.18 # μmol/m²/s
110 theta: float = 62.03 # W/m²
111 Tmin: float = 290.0 # K
112 Topt: float = 303.0 # K
113 q: float = 0.344 # unitless
114 R: float = 1.663e5 # μmol·K^0.5/m²/s
115 ER: float = 3740.0 # 1/K
116 kC: float = 0.791 # unitless
117
118 def __post_init__(self):
119 """Validate parameter values after initialization."""
120 if self.Tref <= 0:
121 raise ValueError("Reference temperature must be positive")
122 if self.Ci_ref <= 0:
123 raise ValueError("Reference CO2 concentration must be positive")
124 if self.Asat < 0:
125 raise ValueError("Light-saturated photosynthetic rate cannot be negative")
126 if self.theta <= 0:
127 raise ValueError("Half-saturation light level must be positive")
128 if self.Tmin <= 0:
129 raise ValueError("Minimum temperature must be positive")
130 if self.Topt <= 0:
131 raise ValueError("Optimum temperature must be positive")
132 if self.Tmin >= self.Topt:
133 raise ValueError("Minimum temperature must be less than optimum temperature")
134 if self.q <= 0:
135 raise ValueError("Temperature response parameter must be positive")
136 if self.R < 0:
137 raise ValueError("Respiration coefficient cannot be negative")
138 if self.ER < 0:
139 raise ValueError("Respiration activation energy cannot be negative")
140 if self.kC < 0:
141 raise ValueError("CO2 response coefficient cannot be negative")
143 def to_array(self) -> List[float]:
144 """Convert to float array for C++ interface."""
145 return [
146 self.Tref, self.Ci_ref, self.Asat, self.theta, self.Tmin,
147 self.Topt, self.q, self.R, self.ER, self.kC
148 ]
149
150 @classmethod
151 def from_array(cls, coefficients: List[float]) -> 'EmpiricalModelCoefficients':
152 """Create from float array (from C++ interface)."""
153 if len(coefficients) < 10:
154 raise ValueError("Need at least 10 coefficients for empirical model")
155 return cls(
156 Tref=coefficients[0], Ci_ref=coefficients[1], Asat=coefficients[2],
157 theta=coefficients[3], Tmin=coefficients[4], Topt=coefficients[5],
158 q=coefficients[6], R=coefficients[7], ER=coefficients[8], kC=coefficients[9]
159 )
160
161
162@dataclass
164 """
165 Farquhar-von Caemmerer-Berry photosynthesis model coefficients.
166
167 This model provides a mechanistic description of leaf photosynthesis
168 based on biochemical limitations and temperature responses.
169
170 Core Parameters (at 25°C):
171 Vcmax: Maximum carboxylation rate (μmol/m²/s, -1 = uninitialized)
172 Jmax: Maximum electron transport rate (μmol/m²/s, -1 = uninitialized)
173 alpha: Quantum efficiency of photosystem II (μmol electrons/μmol photons)
174 Rd: Dark respiration rate (μmol/m²/s, -1 = uninitialized)
175 O: Ambient oxygen concentration (mmol/mol)
176 TPU_flag: Enable triose phosphate utilization limitation (0/1)
177
178 Temperature Response Parameters:
179 c_*: Scaling factor for Arrhenius equation
180 dH_*: Activation energy for temperature response
181 """
182 # Core parameters at 25°C
183 Vcmax: float = -1.0 # μmol/m²/s (uninitialized)
184 Jmax: float = -1.0 # μmol/m²/s (uninitialized)
185 alpha: float = -1.0 # unitless (uninitialized)
186 Rd: float = -1.0 # μmol/m²/s (uninitialized)
187 O: float = 213.5 # ambient oxygen concentration (mmol/mol)
188 TPU_flag: int = 0 # enable TPU limitation
189
190 # Temperature scaling factors (c_*)
191 c_Rd: float = 18.72
192 c_Vcmax: float = 26.35
193 c_Jmax: float = 18.86
194 c_Gamma: float = 19.02
195 c_Kc: float = 38.05
196 c_Ko: float = 20.30
197
198 # Activation energies (dH_*)
199 dH_Rd: float = 46.39
200 dH_Vcmax: float = 65.33
201 dH_Jmax: float = 46.36
202 dH_Gamma: float = 37.83
203 dH_Kc: float = 79.43
204 dH_Ko: float = 36.38
205
206 # Mesophyll conductance gm (helios-core 1.3.72+). The default math.inf reproduces
207 # the legacy Cc = Ci behaviour (no mesophyll diffusion limitation). When packed into
208 # the flat coefficient array, slots [18..21] are (gm_at_25C, dHa, Topt_C, dHd) using
209 # the same -1 sentinel convention as the C4 model: dHa < 0 → constant gm with no
210 # temperature response.
211 gm_at_25C: float = float('inf') # mol CO2 / m^2 / s / bar
212 dHa_gm: float = -1.0 # kJ/mol; -1 disables temperature response
213 Topt_gm_C: float = -1.0 # Celsius; -1 → monotonic Arrhenius
214 dHd_gm: float = -1.0 # kJ/mol; -1 → default
216 # Temperature response parameter containers
217 _vcmax_temp_response: Optional[PhotosyntheticTemperatureResponseParameters] = field(default=None, init=False)
218 _jmax_temp_response: Optional[PhotosyntheticTemperatureResponseParameters] = field(default=None, init=False)
219 _rd_temp_response: Optional[PhotosyntheticTemperatureResponseParameters] = field(default=None, init=False)
220 _alpha_temp_response: Optional[PhotosyntheticTemperatureResponseParameters] = field(default=None, init=False)
221 _theta_temp_response: Optional[PhotosyntheticTemperatureResponseParameters] = field(default=None, init=False)
223 def __post_init__(self):
224 """Validate parameter values after initialization."""
225 if self.O <= 0:
226 raise ValueError("Oxygen concentration must be positive")
227 if self.TPU_flag not in (0, 1):
228 raise ValueError("TPU_flag must be 0 or 1")
230 # Validate temperature parameters
231 for param_name, value in [
232 ('c_Rd', self.c_Rd), ('c_Vcmax', self.c_Vcmax), ('c_Jmax', self.c_Jmax),
233 ('c_Gamma', self.c_Gamma), ('c_Kc', self.c_Kc), ('c_Ko', self.c_Ko),
234 ('dH_Rd', self.dH_Rd), ('dH_Vcmax', self.dH_Vcmax), ('dH_Jmax', self.dH_Jmax),
235 ('dH_Gamma', self.dH_Gamma), ('dH_Kc', self.dH_Kc), ('dH_Ko', self.dH_Ko)
236 ]:
237 if not math.isfinite(value):
238 raise ValueError(f"Temperature parameter {param_name} must be finite")
240 def setVcmax(self, vcmax_at_25c: float, dha: Optional[float] = None,
241 topt: Optional[float] = None, dhd: Optional[float] = None) -> None:
242 """Set Vcmax with temperature response (mimics C++ overloads)."""
243 if dha is None:
244 # 1-parameter version
246 elif topt is None:
247 # 2-parameter version
249 elif dhd is None:
250 # 3-parameter version
252 else:
253 # 4-parameter version
254 self._vcmax_temp_response = PhotosyntheticTemperatureResponseParameters(vcmax_at_25c, dha, dhd, topt)
255
256 self.Vcmax = vcmax_at_25c
257
258 def setJmax(self, jmax_at_25c: float, dha: Optional[float] = None,
259 topt: Optional[float] = None, dhd: Optional[float] = None) -> None:
260 """Set Jmax with temperature response (mimics C++ overloads)."""
261 if dha is None:
263 elif topt is None:
265 elif dhd is None:
267 else:
268 self._jmax_temp_response = PhotosyntheticTemperatureResponseParameters(jmax_at_25c, dha, dhd, topt)
270 self.Jmax = jmax_at_25c
271
272 def setRd(self, rd_at_25c: float, dha: Optional[float] = None,
273 topt: Optional[float] = None, dhd: Optional[float] = None) -> None:
274 """Set dark respiration with temperature response (mimics C++ overloads)."""
275 if dha is None:
277 elif topt is None:
279 elif dhd is None:
281 else:
282 self._rd_temp_response = PhotosyntheticTemperatureResponseParameters(rd_at_25c, dha, dhd, topt)
283
284 self.Rd = rd_at_25c
285
286 def setQuantumEfficiency_alpha(self, alpha_at_25c: float, dha: Optional[float] = None,
287 topt: Optional[float] = None, dhd: Optional[float] = None) -> None:
288 """Set quantum efficiency with temperature response (mimics C++ overloads)."""
289 if dha is None:
291 elif topt is None:
293 elif dhd is None:
295 else:
296 self._alpha_temp_response = PhotosyntheticTemperatureResponseParameters(alpha_at_25c, dha, dhd, topt)
297
298 self.alpha = alpha_at_25c
299
300 def setLightResponseCurvature_theta(self, theta_at_25c: float, dha: Optional[float] = None,
301 topt: Optional[float] = None, dhd: Optional[float] = None) -> None:
302 """Set light response curvature with temperature response (mimics C++ overloads)."""
303 if dha is None:
305 elif topt is None:
307 elif dhd is None:
309 else:
310 self._theta_temp_response = PhotosyntheticTemperatureResponseParameters(theta_at_25c, dha, dhd, topt)
311
312 def getVcmaxTempResponse(self) -> PhotosyntheticTemperatureResponseParameters:
313 """Get Vcmax temperature response parameters."""
314 if self._vcmax_temp_response is None:
315 return PhotosyntheticTemperatureResponseParameters(self.Vcmax if self.Vcmax > 0 else 100.0)
316 return self._vcmax_temp_response
317
318 def getJmaxTempResponse(self) -> PhotosyntheticTemperatureResponseParameters:
319 """Get Jmax temperature response parameters."""
320 if self._jmax_temp_response is None:
321 return PhotosyntheticTemperatureResponseParameters(self.Jmax if self.Jmax > 0 else 100.0)
322 return self._jmax_temp_response
323
324 def getRdTempResponse(self) -> PhotosyntheticTemperatureResponseParameters:
325 """Get dark respiration temperature response parameters."""
326 if self._rd_temp_response is None:
327 return PhotosyntheticTemperatureResponseParameters(self.Rd if self.Rd > 0 else 1.0)
328 return self._rd_temp_response
330 def getQuantumEfficiencyTempResponse(self) -> PhotosyntheticTemperatureResponseParameters:
331 """Get quantum efficiency temperature response parameters."""
332 if self._alpha_temp_response is None:
333 return PhotosyntheticTemperatureResponseParameters(self.alpha if self.alpha > 0 else 0.3)
334 return self._alpha_temp_response
335
336 def getLightResponseCurvatureTempResponse(self) -> PhotosyntheticTemperatureResponseParameters:
337 """Get light response curvature temperature response parameters."""
338 if self._theta_temp_response is None:
340 return self._theta_temp_response
342 def to_array(self) -> List[float]:
343 """Convert to float array for C++ interface (22 floats; helios-core 1.3.72+).
344
345 Slots 0..17 are the legacy Farquhar fields (Vcmax/Jmax/alpha/Rd/O/TPU_flag plus
346 the 12 c_*/dH_* temperature constants). Slots 18..21 carry the mesophyll
347 conductance gm temperature response: (gm_at_25C, dHa, Topt_C, dHd) using the
348 -1 sentinel convention (dHa < 0 → constant gm with no temperature response,
349 Topt_C < 0 → monotonic Arrhenius, dHd < 0 → default deactivation energy).
350 Default ``gm_at_25C`` is ``+inf`` which reproduces the legacy ``Cc = Ci`` behaviour.
351 """
352 return [
353 self.Vcmax, self.Jmax, self.alpha, self.Rd, self.O, float(self.TPU_flag),
354 # Temperature scaling factors
355 self.c_Vcmax, self.dH_Vcmax, self.c_Jmax, self.dH_Jmax,
356 self.c_Rd, self.dH_Rd, self.c_Kc, self.dH_Kc,
357 self.c_Ko, self.dH_Ko, self.c_Gamma, self.dH_Gamma,
358 # Mesophyll conductance gm temperature response (1.3.72+)
359 self.gm_at_25C, self.dHa_gm, self.Topt_gm_C, self.dHd_gm,
360 ]
361
362 @classmethod
363 def from_array(cls, coefficients: List[float]) -> 'FarquharModelCoefficients':
364 """Create from float array (from C++ interface).
366 Accepts both the legacy 18-float layout (pre-1.3.72) and the 22-float layout
367 with mesophyll conductance gm in slots 18..21. When the array is 18 floats,
368 gm defaults to +infinity (legacy ``Cc = Ci`` behaviour).
369 """
370 if len(coefficients) < 18:
371 raise ValueError("Need at least 18 coefficients for Farquhar model")
372
373 gm_at_25C = coefficients[18] if len(coefficients) > 18 else float('inf')
374 dHa_gm = coefficients[19] if len(coefficients) > 19 else -1.0
375 Topt_gm_C = coefficients[20] if len(coefficients) > 20 else -1.0
376 dHd_gm = coefficients[21] if len(coefficients) > 21 else -1.0
377
378 return cls(
379 Vcmax=coefficients[0], Jmax=coefficients[1], alpha=coefficients[2],
380 Rd=coefficients[3], O=coefficients[4], TPU_flag=int(coefficients[5]),
381 c_Vcmax=coefficients[6], dH_Vcmax=coefficients[7],
382 c_Jmax=coefficients[8], dH_Jmax=coefficients[9],
383 c_Rd=coefficients[10], dH_Rd=coefficients[11],
384 c_Kc=coefficients[12], dH_Kc=coefficients[13],
385 c_Ko=coefficients[14], dH_Ko=coefficients[15],
386 c_Gamma=coefficients[16], dH_Gamma=coefficients[17],
387 gm_at_25C=gm_at_25C, dHa_gm=dHa_gm, Topt_gm_C=Topt_gm_C, dHd_gm=dHd_gm,
388 )
389
390
391def validate_species_name(species: str) -> str:
392 """
393 Validate and normalize species name for photosynthesis library.
394
395 Args:
396 species: Species name (case insensitive, supports aliases)
398 Returns:
399 Normalized species name
400
401 Raises:
402 ValueError: If species is not recognized
403 """
404 if not species:
405 raise ValueError("Species name cannot be empty")
406
407 # Try exact match first (case sensitive)
408 if species in PHOTOSYNTHESIS_SPECIES:
409 return species
410
411 # Try case-insensitive match
412 species_lower = species.lower()
413 if species_lower in SPECIES_ALIASES:
414 return SPECIES_ALIASES[species_lower]
415
416 # Try case-insensitive match against known species
417 for known_species in PHOTOSYNTHESIS_SPECIES:
418 if known_species.lower() == species_lower:
419 return known_species
420
421 # Species not found - provide helpful error message
422 available_species = sorted(set(list(PHOTOSYNTHESIS_SPECIES) + list(SPECIES_ALIASES.keys())))
423 raise ValueError(
424 f"Unknown species '{species}'. Available species and aliases:\n"
425 f" {', '.join(available_species[:8])}\n"
426 f" {', '.join(available_species[8:16])}\n"
427 f" {', '.join(available_species[16:])}"
428 )
429
430
431def get_available_species() -> List[str]:
432 """Get list of available species in the photosynthesis library."""
433 return sorted(PHOTOSYNTHESIS_SPECIES.copy())
434
435
436def get_species_aliases() -> dict:
437 """Get dictionary of species aliases."""
438 return SPECIES_ALIASES.copy()
Empirical photosynthesis model coefficients.
float Topt
Optimum temperature for photosynthesis (K)
float Tmin
Minimum temperature for photosynthesis (K)
float Asat
Light-saturated photosynthetic rate (μmol/m²/s)
__post_init__(self)
Validate parameter values after initialization.
float q
Temperature response parameter (unitless)
'EmpiricalModelCoefficients' from_array(cls, List[float] coefficients)
Create from float array (from C++ interface).
float R
Respiration temperature coefficient (μmol·K^0.5/m²/s)
List[float] to_array(self)
Convert to float array for C++ interface.
float ER
Respiration activation energy (1/K)
float Ci_ref
Reference CO2 concentration (μmol CO2/mol air)
float theta
Half-saturation light level (W/m²)
float kC
CO2 response coefficient (unitless)
Farquhar-von Caemmerer-Berry photosynthesis model coefficients.
List[float] to_array(self)
Convert to float array for C++ interface (22 floats; helios-core 1.3.72+).
None setVcmax(self, float vcmax_at_25c, Optional[float] dha=None, Optional[float] topt=None, Optional[float] dhd=None)
Set Vcmax with temperature response (mimics C++ overloads).
__post_init__(self)
Validate parameter values after initialization.
PhotosyntheticTemperatureResponseParameters getLightResponseCurvatureTempResponse(self)
Get light response curvature temperature response parameters.
None setQuantumEfficiency_alpha(self, float alpha_at_25c, Optional[float] dha=None, Optional[float] topt=None, Optional[float] dhd=None)
Set quantum efficiency with temperature response (mimics C++ overloads).
PhotosyntheticTemperatureResponseParameters getQuantumEfficiencyTempResponse(self)
Get quantum efficiency temperature response parameters.
PhotosyntheticTemperatureResponseParameters getVcmaxTempResponse(self)
Get Vcmax temperature response parameters.
'FarquharModelCoefficients' from_array(cls, List[float] coefficients)
Create from float array (from C++ interface).
PhotosyntheticTemperatureResponseParameters getRdTempResponse(self)
Get dark respiration temperature response parameters.
PhotosyntheticTemperatureResponseParameters getJmaxTempResponse(self)
Get Jmax temperature response parameters.
None setLightResponseCurvature_theta(self, float theta_at_25c, Optional[float] dha=None, Optional[float] topt=None, Optional[float] dhd=None)
Set light response curvature with temperature response (mimics C++ overloads).
None setRd(self, float rd_at_25c, Optional[float] dha=None, Optional[float] topt=None, Optional[float] dhd=None)
Set dark respiration with temperature response (mimics C++ overloads).
None setJmax(self, float jmax_at_25c, Optional[float] dha=None, Optional[float] topt=None, Optional[float] dhd=None)
Set Jmax with temperature response (mimics C++ overloads).
Temperature response parameters for photosynthetic processes.
float dHa
Activation energy (rate of increase parameter)
float dHd
Deactivation energy (rate of decrease parameter)
__post_init__(self)
Validate parameter values after initialization.
float Topt
Optimum temperature in Kelvin (10000K means no optimum)
List[str] get_available_species()
Get list of available species in the photosynthesis library.
str validate_species_name(str species)
Validate and normalize species name for photosynthesis library.
dict get_species_aliases()
Get dictionary of species aliases.