0.1.26
Loading...
Searching...
No Matches
plant_architecture_params.py
Go to the documentation of this file.
1"""Typed model for Helios plant architecture parameters.
2
3This module provides discoverable, validated dataclasses mirroring the nested
4C++ ``ShootParameters`` / ``PhytomerParameters`` / ``LeafPrototype`` structures
5(plus the flat ``CarbohydrateParameters`` / ``NitrogenParameters``). The objects
6serialize to/from the plain ``dict`` JSON transport used by the PlantArchitecture
7wrapper, so they can be used interchangeably with the raw-dict API.
8
9Canonical usage builds a typed object from the values currently defined in the
10native library, mutates it, and applies it back -- this avoids any drift between
11Python-side defaults and the C++ defaults::
12
13 from pyhelios import Context, PlantArchitecture
14 from pyhelios.plant_architecture_params import ShootParameters, RandomParameterFloat
15
16 with Context() as ctx:
17 pa = PlantArchitecture(ctx)
18 pa.loadPlantModelFromLibrary("almond")
19 sp = ShootParameters.from_dict(pa.getCurrentShootParameters("trunk"))
20 sp.phytomer_parameters.leaf.pitch = RandomParameterFloat.uniform(40, 50)
21 pa.defineShootType("trunk2", sp)
22
23Notes
24-----
25* Every numeric leaf/shoot/phytomer field is a :class:`RandomParameterFloat` or
26 :class:`RandomParameterInt` carrying a distribution and its parameters.
27* Prototype functions (leaf/flower/fruit) are referenced by *name* -- a string
28 naming a built-in Helios prototype (e.g. ``"AlmondFlowerPrototype"``). An empty
29 string / ``None`` means "unset". The C++ ``shared_ptr<Phytomer>`` creation and
30 callback functions are not exposable from Python and are not represented here.
31* Child shoot types serialize on input only (the native struct has no public
32 getter); see :attr:`ShootParameters.child_shoot_types`.
33"""
34
35from __future__ import annotations
36
37from dataclasses import dataclass, field
38from typing import Any, Dict, List, Optional, Tuple
39
40__all__ = [
41 "RandomParameterFloat",
42 "RandomParameterInt",
43 "RandomParameter",
44 "LeafPrototype",
45 "InternodeParameters",
46 "PetioleParameters",
47 "LeafParameters",
48 "PeduncleParameters",
49 "InflorescenceParameters",
50 "PhytomerParameters",
51 "ShootParameters",
52 "CarbohydrateParameters",
53 "NitrogenParameters",
54]
55
56
57# --------------------------------------------------------------------------- #
58# Random parameters
59# --------------------------------------------------------------------------- #
60@dataclass
62 """A float-valued parameter with a sampling distribution.
63
64 Use the classmethod constructors (:meth:`constant`, :meth:`uniform`,
65 :meth:`normal`, :meth:`weibull`) rather than constructing directly.
66 """
67
68 distribution: str = "constant"
69 parameters: List[float] = field(default_factory=lambda: [0.0])
70
71 @classmethod
72 def constant(cls, value: float) -> "RandomParameterFloat":
73 return cls("constant", [float(value)])
74
75 @classmethod
76 def uniform(cls, min_val: float, max_val: float) -> "RandomParameterFloat":
77 if min_val > max_val:
78 raise ValueError(f"min_val ({min_val}) must be <= max_val ({max_val})")
79 return cls("uniform", [float(min_val), float(max_val)])
80
81 @classmethod
82 def normal(cls, mean: float, std_dev: float) -> "RandomParameterFloat":
83 if std_dev < 0:
84 raise ValueError(f"std_dev ({std_dev}) must be >= 0")
85 return cls("normal", [float(mean), float(std_dev)])
86
87 @classmethod
88 def weibull(cls, shape: float, scale: float) -> "RandomParameterFloat":
89 if shape <= 0:
90 raise ValueError(f"shape ({shape}) must be > 0")
91 if scale <= 0:
92 raise ValueError(f"scale ({scale}) must be > 0")
93 return cls("weibull", [float(shape), float(scale)])
94
95 def to_dict(self) -> Dict[str, Any]:
96 return {"distribution": self.distribution, "parameters": [float(p) for p in self.parameters]}
97
98 @classmethod
99 def from_dict(cls, d: Dict[str, Any]) -> "RandomParameterFloat":
100 return cls(str(d["distribution"]), [float(p) for p in d["parameters"]])
101
102
103@dataclass
105 """An int-valued parameter with a sampling distribution."""
106
107 distribution: str = "constant"
108 parameters: List[int] = field(default_factory=lambda: [0])
109
110 @classmethod
111 def constant(cls, value: int) -> "RandomParameterInt":
112 return cls("constant", [int(value)])
113
114 @classmethod
115 def uniform(cls, min_val: int, max_val: int) -> "RandomParameterInt":
116 if min_val > max_val:
117 raise ValueError(f"min_val ({min_val}) must be <= max_val ({max_val})")
118 return cls("uniform", [int(min_val), int(max_val)])
119
120 @classmethod
121 def discrete(cls, values: List[int]) -> "RandomParameterInt":
122 if not values:
123 raise ValueError("values list cannot be empty")
124 return cls("discretevalues", [int(v) for v in values])
125
126 def to_dict(self) -> Dict[str, Any]:
127 # The native JSON transport encodes int distribution parameters as floats.
128 return {"distribution": self.distribution, "parameters": [float(p) for p in self.parameters]}
129
130 @classmethod
131 def from_dict(cls, d: Dict[str, Any]) -> "RandomParameterInt":
132 return cls(str(d["distribution"]), [int(round(float(p))) for p in d["parameters"]])
133
134
135# Backward-compatible name for the float-valued parameter, retained from the
136# original dict-returning ``PlantArchitecture.RandomParameter`` helper.
137RandomParameter = RandomParameterFloat
138
139
140# --------------------------------------------------------------------------- #
141# Helpers for color / vec3 (encoded as plain tuples on the Python side)
142# --------------------------------------------------------------------------- #
143Color = Tuple[float, float, float]
144Vec3 = Tuple[float, float, float]
145
146
147def _rpf(d: Dict[str, Any], key: str, default: RandomParameterFloat) -> RandomParameterFloat:
148 return RandomParameterFloat.from_dict(d[key]) if key in d else default
149
150
151def _rpi(d: Dict[str, Any], key: str, default: RandomParameterInt) -> RandomParameterInt:
152 return RandomParameterInt.from_dict(d[key]) if key in d else default
153
154
155def _color_to_dict(c: Color) -> Dict[str, float]:
156 return {"r": float(c[0]), "g": float(c[1]), "b": float(c[2])}
157
158
159def _color_from_dict(d: Dict[str, Any], default: Color) -> Color:
160 return (float(d.get("r", default[0])), float(d.get("g", default[1])), float(d.get("b", default[2])))
161
162
163def _vec3_to_dict(v: Vec3) -> Dict[str, float]:
164 return {"x": float(v[0]), "y": float(v[1]), "z": float(v[2])}
165
166
167def _vec3_from_dict(d: Dict[str, Any], default: Vec3) -> Vec3:
168 return (float(d.get("x", default[0])), float(d.get("y", default[1])), float(d.get("z", default[2])))
169
170
171# --------------------------------------------------------------------------- #
172# Leaf prototype
173# --------------------------------------------------------------------------- #
174@dataclass
176 leaf_aspect_ratio: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(1.0))
177 midrib_fold_fraction: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
178 longitudinal_curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
179 lateral_curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
180 petiole_roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
181 wave_period: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
182 wave_amplitude: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
183 leaf_buckle_length: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
184 leaf_buckle_angle: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
185 leaf_offset: Vec3 = (0.0, 0.0, 0.0)
186 subdivisions: int = 1
187 unique_prototypes: int = 1
188 build_petiolule: bool = False
189 OBJ_model_file: str = ""
190 leaf_texture_file: Dict[int, str] = field(default_factory=dict)
191 prototype_function: Optional[str] = None
192
193 def to_dict(self) -> Dict[str, Any]:
194 return {
195 "leaf_aspect_ratio": self.leaf_aspect_ratio.to_dict(),
196 "midrib_fold_fraction": self.midrib_fold_fraction.to_dict(),
197 "longitudinal_curvature": self.longitudinal_curvature.to_dict(),
198 "lateral_curvature": self.lateral_curvature.to_dict(),
199 "petiole_roll": self.petiole_roll.to_dict(),
200 "wave_period": self.wave_period.to_dict(),
201 "wave_amplitude": self.wave_amplitude.to_dict(),
202 "leaf_buckle_length": self.leaf_buckle_length.to_dict(),
203 "leaf_buckle_angle": self.leaf_buckle_angle.to_dict(),
204 "leaf_offset": _vec3_to_dict(self.leaf_offset),
205 "subdivisions": int(self.subdivisions),
206 "unique_prototypes": int(self.unique_prototypes),
207 "build_petiolule": bool(self.build_petiolule),
208 "OBJ_model_file": self.OBJ_model_file,
209 "leaf_texture_file": {str(k): v for k, v in self.leaf_texture_file.items()},
210 "prototype_function": self.prototype_function or "",
211 }
212
213 @classmethod
214 def from_dict(cls, d: Dict[str, Any]) -> "LeafPrototype":
215 base = cls()
216 tex = d.get("leaf_texture_file", {})
217 return cls(
218 leaf_aspect_ratio=_rpf(d, "leaf_aspect_ratio", base.leaf_aspect_ratio),
219 midrib_fold_fraction=_rpf(d, "midrib_fold_fraction", base.midrib_fold_fraction),
220 longitudinal_curvature=_rpf(d, "longitudinal_curvature", base.longitudinal_curvature),
221 lateral_curvature=_rpf(d, "lateral_curvature", base.lateral_curvature),
222 petiole_roll=_rpf(d, "petiole_roll", base.petiole_roll),
223 wave_period=_rpf(d, "wave_period", base.wave_period),
224 wave_amplitude=_rpf(d, "wave_amplitude", base.wave_amplitude),
225 leaf_buckle_length=_rpf(d, "leaf_buckle_length", base.leaf_buckle_length),
226 leaf_buckle_angle=_rpf(d, "leaf_buckle_angle", base.leaf_buckle_angle),
227 leaf_offset=_vec3_from_dict(d["leaf_offset"], base.leaf_offset) if "leaf_offset" in d else base.leaf_offset,
228 subdivisions=int(d.get("subdivisions", base.subdivisions)),
229 unique_prototypes=int(d.get("unique_prototypes", base.unique_prototypes)),
230 build_petiolule=bool(d.get("build_petiolule", base.build_petiolule)),
231 OBJ_model_file=str(d.get("OBJ_model_file", base.OBJ_model_file)),
232 leaf_texture_file={int(k): str(v) for k, v in tex.items()},
233 prototype_function=(d.get("prototype_function") or None),
234 )
235
236
237# --------------------------------------------------------------------------- #
238# Phytomer sub-structures
239# --------------------------------------------------------------------------- #
240@dataclass
242 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(20.0))
243 phyllotactic_angle: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(137.5))
244 radius_initial: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.001))
245 max_vegetative_buds_per_petiole: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(0))
246 max_floral_buds_per_petiole: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(0))
247 color: Color = (0.0, 0.0, 0.0)
248 image_texture: str = ""
249 length_segments: int = 1
250 radial_subdivisions: int = 7
251
252 def to_dict(self) -> Dict[str, Any]:
253 return {
254 "pitch": self.pitch.to_dict(),
255 "phyllotactic_angle": self.phyllotactic_angle.to_dict(),
256 "radius_initial": self.radius_initial.to_dict(),
257 "max_vegetative_buds_per_petiole": self.max_vegetative_buds_per_petiole.to_dict(),
258 "max_floral_buds_per_petiole": self.max_floral_buds_per_petiole.to_dict(),
259 "color": _color_to_dict(self.color),
260 "image_texture": self.image_texture,
261 "length_segments": int(self.length_segments),
262 "radial_subdivisions": int(self.radial_subdivisions),
263 }
264
265 @classmethod
266 def from_dict(cls, d: Dict[str, Any]) -> "InternodeParameters":
267 base = cls()
268 return cls(
269 pitch=_rpf(d, "pitch", base.pitch),
270 phyllotactic_angle=_rpf(d, "phyllotactic_angle", base.phyllotactic_angle),
271 radius_initial=_rpf(d, "radius_initial", base.radius_initial),
272 max_vegetative_buds_per_petiole=_rpi(d, "max_vegetative_buds_per_petiole", base.max_vegetative_buds_per_petiole),
273 max_floral_buds_per_petiole=_rpi(d, "max_floral_buds_per_petiole", base.max_floral_buds_per_petiole),
274 color=_color_from_dict(d["color"], base.color) if "color" in d else base.color,
275 image_texture=str(d.get("image_texture", base.image_texture)),
276 length_segments=int(d.get("length_segments", base.length_segments)),
277 radial_subdivisions=int(d.get("radial_subdivisions", base.radial_subdivisions)),
278 )
279
280
281@dataclass
283 petioles_per_internode: int = 1
284 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(90.0))
285 radius: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.001))
286 length: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.05))
287 curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
288 taper: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
289 color: Color = (0.0, 0.0, 0.0)
290 length_segments: int = 1
291 radial_subdivisions: int = 7
292
293 def to_dict(self) -> Dict[str, Any]:
294 return {
295 "petioles_per_internode": int(self.petioles_per_internode),
296 "pitch": self.pitch.to_dict(),
297 "radius": self.radius.to_dict(),
298 "length": self.length.to_dict(),
299 "curvature": self.curvature.to_dict(),
300 "taper": self.taper.to_dict(),
301 "color": _color_to_dict(self.color),
302 "length_segments": int(self.length_segments),
303 "radial_subdivisions": int(self.radial_subdivisions),
304 }
305
306 @classmethod
307 def from_dict(cls, d: Dict[str, Any]) -> "PetioleParameters":
308 base = cls()
309 return cls(
310 petioles_per_internode=int(d.get("petioles_per_internode", base.petioles_per_internode)),
311 pitch=_rpf(d, "pitch", base.pitch),
312 radius=_rpf(d, "radius", base.radius),
313 length=_rpf(d, "length", base.length),
314 curvature=_rpf(d, "curvature", base.curvature),
315 taper=_rpf(d, "taper", base.taper),
316 color=_color_from_dict(d["color"], base.color) if "color" in d else base.color,
317 length_segments=int(d.get("length_segments", base.length_segments)),
318 radial_subdivisions=int(d.get("radial_subdivisions", base.radial_subdivisions)),
319 )
320
321
322@dataclass
324 leaves_per_petiole: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(1))
325 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
326 yaw: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
327 roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
328 leaflet_offset: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
329 leaflet_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(1.0))
330 prototype_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.05))
331 prototype: LeafPrototype = field(default_factory=LeafPrototype)
332
333 def to_dict(self) -> Dict[str, Any]:
334 return {
335 "leaves_per_petiole": self.leaves_per_petiole.to_dict(),
336 "pitch": self.pitch.to_dict(),
337 "yaw": self.yaw.to_dict(),
338 "roll": self.roll.to_dict(),
339 "leaflet_offset": self.leaflet_offset.to_dict(),
340 "leaflet_scale": self.leaflet_scale.to_dict(),
341 "prototype_scale": self.prototype_scale.to_dict(),
342 "prototype": self.prototype.to_dict(),
343 }
344
345 @classmethod
346 def from_dict(cls, d: Dict[str, Any]) -> "LeafParameters":
347 base = cls()
348 return cls(
349 leaves_per_petiole=_rpi(d, "leaves_per_petiole", base.leaves_per_petiole),
350 pitch=_rpf(d, "pitch", base.pitch),
351 yaw=_rpf(d, "yaw", base.yaw),
352 roll=_rpf(d, "roll", base.roll),
353 leaflet_offset=_rpf(d, "leaflet_offset", base.leaflet_offset),
354 leaflet_scale=_rpf(d, "leaflet_scale", base.leaflet_scale),
355 prototype_scale=_rpf(d, "prototype_scale", base.prototype_scale),
356 prototype=LeafPrototype.from_dict(d["prototype"]) if "prototype" in d else base.prototype,
357 )
358
359
360@dataclass
362 length: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.05))
363 radius: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.001))
364 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
365 roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
366 curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
367 color: Color = (0.0, 0.0, 0.0)
368 length_segments: int = 3
369 radial_subdivisions: int = 7
370
371 def to_dict(self) -> Dict[str, Any]:
372 return {
373 "length": self.length.to_dict(),
374 "radius": self.radius.to_dict(),
375 "pitch": self.pitch.to_dict(),
376 "roll": self.roll.to_dict(),
377 "curvature": self.curvature.to_dict(),
378 "color": _color_to_dict(self.color),
379 "length_segments": int(self.length_segments),
380 "radial_subdivisions": int(self.radial_subdivisions),
381 }
382
383 @classmethod
384 def from_dict(cls, d: Dict[str, Any]) -> "PeduncleParameters":
385 base = cls()
386 return cls(
387 length=_rpf(d, "length", base.length),
388 radius=_rpf(d, "radius", base.radius),
389 pitch=_rpf(d, "pitch", base.pitch),
390 roll=_rpf(d, "roll", base.roll),
391 curvature=_rpf(d, "curvature", base.curvature),
392 color=_color_from_dict(d["color"], base.color) if "color" in d else base.color,
393 length_segments=int(d.get("length_segments", base.length_segments)),
394 radial_subdivisions=int(d.get("radial_subdivisions", base.radial_subdivisions)),
395 )
396
397
398@dataclass
400 flowers_per_peduncle: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(1))
401 flower_offset: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
402 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
403 roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
404 flower_prototype_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0075))
405 fruit_prototype_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0075))
406 fruit_gravity_factor_fraction: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
407 unique_prototypes: int = 1
408 flower_prototype_function: Optional[str] = None
409 fruit_prototype_function: Optional[str] = None
410
411 def to_dict(self) -> Dict[str, Any]:
412 return {
413 "flowers_per_peduncle": self.flowers_per_peduncle.to_dict(),
414 "flower_offset": self.flower_offset.to_dict(),
415 "pitch": self.pitch.to_dict(),
416 "roll": self.roll.to_dict(),
417 "flower_prototype_scale": self.flower_prototype_scale.to_dict(),
418 "fruit_prototype_scale": self.fruit_prototype_scale.to_dict(),
419 "fruit_gravity_factor_fraction": self.fruit_gravity_factor_fraction.to_dict(),
420 "unique_prototypes": int(self.unique_prototypes),
421 "flower_prototype_function": self.flower_prototype_function or "",
422 "fruit_prototype_function": self.fruit_prototype_function or "",
423 }
424
425 @classmethod
426 def from_dict(cls, d: Dict[str, Any]) -> "InflorescenceParameters":
427 base = cls()
428 return cls(
429 flowers_per_peduncle=_rpi(d, "flowers_per_peduncle", base.flowers_per_peduncle),
430 flower_offset=_rpf(d, "flower_offset", base.flower_offset),
431 pitch=_rpf(d, "pitch", base.pitch),
432 roll=_rpf(d, "roll", base.roll),
433 flower_prototype_scale=_rpf(d, "flower_prototype_scale", base.flower_prototype_scale),
434 fruit_prototype_scale=_rpf(d, "fruit_prototype_scale", base.fruit_prototype_scale),
435 fruit_gravity_factor_fraction=_rpf(d, "fruit_gravity_factor_fraction", base.fruit_gravity_factor_fraction),
436 unique_prototypes=int(d.get("unique_prototypes", base.unique_prototypes)),
437 flower_prototype_function=(d.get("flower_prototype_function") or None),
438 fruit_prototype_function=(d.get("fruit_prototype_function") or None),
439 )
440
441
442@dataclass
444 internode: InternodeParameters = field(default_factory=InternodeParameters)
445 petiole: PetioleParameters = field(default_factory=PetioleParameters)
446 leaf: LeafParameters = field(default_factory=LeafParameters)
447 peduncle: PeduncleParameters = field(default_factory=PeduncleParameters)
448 inflorescence: InflorescenceParameters = field(default_factory=InflorescenceParameters)
449
450 def to_dict(self) -> Dict[str, Any]:
451 return {
452 "internode": self.internode.to_dict(),
453 "petiole": self.petiole.to_dict(),
454 "leaf": self.leaf.to_dict(),
455 "peduncle": self.peduncle.to_dict(),
456 "inflorescence": self.inflorescence.to_dict(),
457 }
458
459 @classmethod
460 def from_dict(cls, d: Dict[str, Any]) -> "PhytomerParameters":
461 base = cls()
462 return cls(
463 internode=InternodeParameters.from_dict(d["internode"]) if "internode" in d else base.internode,
464 petiole=PetioleParameters.from_dict(d["petiole"]) if "petiole" in d else base.petiole,
465 leaf=LeafParameters.from_dict(d["leaf"]) if "leaf" in d else base.leaf,
466 peduncle=PeduncleParameters.from_dict(d["peduncle"]) if "peduncle" in d else base.peduncle,
467 inflorescence=InflorescenceParameters.from_dict(d["inflorescence"]) if "inflorescence" in d else base.inflorescence,
468 )
469
470
471# --------------------------------------------------------------------------- #
472# Shoot parameters
473# --------------------------------------------------------------------------- #
474# Top-level ShootParameters fields, with their RandomParameter kind. Keeping this
475# as a table avoids 20+ near-identical lines in both to_dict and from_dict.
476_SHOOT_RPF_FIELDS = (
477 ("girth_area_factor", 0.0),
478 ("insertion_angle_tip", 20.0),
479 ("insertion_angle_decay_rate", 0.0),
480 ("internode_length_max", 0.02),
481 ("internode_length_min", 0.002),
482 ("internode_length_decay_rate", 0.0),
483 ("base_roll", 0.0),
484 ("base_yaw", 0.0),
485 ("gravitropic_curvature", 0.0),
486 ("tortuosity", 0.0),
487 ("phyllochron_min", 2.0),
488 ("elongation_rate_max", 0.2),
489 ("vegetative_bud_break_probability_min", 0.0),
490 ("vegetative_bud_break_probability_max", 1.0),
491 ("vegetative_bud_break_probability_decay_rate", -0.5),
492 ("flower_bud_break_probability", 0.0),
493 ("fruit_set_probability", 0.0),
494 ("vegetative_bud_break_time", 5.0),
495)
496_SHOOT_RPI_FIELDS = (
497 ("max_nodes", 10),
498 ("max_nodes_per_season", 9999),
499 ("max_terminal_floral_buds", 0),
500)
501_SHOOT_BOOL_FIELDS = (
502 ("flowers_require_dormancy", False),
503 ("growth_requires_dormancy", False),
504 ("determinate_shoot_growth", True),
505)
506
507
508@dataclass
509class ShootParameters:
510 phytomer_parameters: PhytomerParameters = field(default_factory=PhytomerParameters)
511
512 # Geometric / growth RandomParameter_float fields
513 girth_area_factor: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
514 insertion_angle_tip: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(20.0))
515 insertion_angle_decay_rate: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
516 internode_length_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.02))
517 internode_length_min: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.002))
518 internode_length_decay_rate: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
519 base_roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
520 base_yaw: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
521 gravitropic_curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
522 tortuosity: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
523 phyllochron_min: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(2.0))
524 elongation_rate_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.2))
525 vegetative_bud_break_probability_min: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
526 vegetative_bud_break_probability_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(1.0))
527 vegetative_bud_break_probability_decay_rate: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(-0.5))
528 flower_bud_break_probability: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
529 fruit_set_probability: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
530 vegetative_bud_break_time: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(5.0))
532 # RandomParameter_int fields
533 max_nodes: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(10))
534 max_nodes_per_season: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(9999))
535 max_terminal_floral_buds: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(0))
537 # Boolean flags
538 flowers_require_dormancy: bool = False
539 growth_requires_dormancy: bool = False
540 determinate_shoot_growth: bool = True
541
542 # Optional child shoot type definition: {"labels": [...], "probabilities": [...]}.
543 # Note: the native struct exposes no getter, so this round-trips on input only --
544 # getCurrentShootParameters() will not populate it.
545 child_shoot_types: Optional[Dict[str, Any]] = None
546
547 def to_dict(self) -> Dict[str, Any]:
548 d: Dict[str, Any] = {"phytomer_parameters": self.phytomer_parameters.to_dict()}
549 for name, _ in _SHOOT_RPF_FIELDS:
550 d[name] = getattr(self, name).to_dict()
551 for name, _ in _SHOOT_RPI_FIELDS:
552 d[name] = getattr(self, name).to_dict()
553 for name, _ in _SHOOT_BOOL_FIELDS:
554 d[name] = bool(getattr(self, name))
555 if self.child_shoot_types is not None:
556 d["child_shoot_types"] = self.child_shoot_types
557 return d
558
559 @classmethod
560 def from_dict(cls, d: Dict[str, Any]) -> "ShootParameters":
561 kwargs: Dict[str, Any] = {}
562 if "phytomer_parameters" in d:
563 kwargs["phytomer_parameters"] = PhytomerParameters.from_dict(d["phytomer_parameters"])
564 for name, default in _SHOOT_RPF_FIELDS:
565 if name in d:
566 kwargs[name] = RandomParameterFloat.from_dict(d[name])
567 for name, default in _SHOOT_RPI_FIELDS:
568 if name in d:
569 kwargs[name] = RandomParameterInt.from_dict(d[name])
570 for name, default in _SHOOT_BOOL_FIELDS:
571 if name in d:
572 kwargs[name] = bool(d[name])
573 if "child_shoot_types" in d:
574 kwargs["child_shoot_types"] = d["child_shoot_types"]
575 return cls(**kwargs)
576
577 def define_child_shoot_types(self, labels: List[str], probabilities: List[float]) -> None:
578 """Set child shoot types; probabilities must sum to 1 (validated natively)."""
579 if len(labels) != len(probabilities):
580 raise ValueError("labels and probabilities must be the same length")
581 if not labels:
582 raise ValueError("labels and probabilities cannot be empty")
583 self.child_shoot_types = {"labels": list(labels), "probabilities": [float(p) for p in probabilities]}
584
585
586# --------------------------------------------------------------------------- #
587# Flat physiology parameter structs
588# --------------------------------------------------------------------------- #
589def _flat_to_dict(obj: Any, names: Tuple[str, ...]) -> Dict[str, float]:
590 return {n: float(getattr(obj, n)) for n in names}
591
592
593def _flat_from_dict(cls: Any, d: Dict[str, Any], names: Tuple[str, ...]) -> Any:
594 base = cls()
595 kwargs = {n: float(d[n]) for n in names if n in d}
596 for n in names:
597 kwargs.setdefault(n, getattr(base, n))
598 return cls(**kwargs)
599
600
601_CARB_FIELDS = (
602 "stem_density", "stem_carbon_percentage", "stem_carbohydrate_percentage",
603 "stem_structural_carbon_percentage", "maturity_age", "initial_density_ratio",
604 "shoot_root_ratio", "leaf_total_carbon_percentage", "SLA",
605 "leaf_carbohydrate_percentage", "leaf_carbon_percentage", "total_flower_cost",
606 "fruit_density", "fruit_carbon_percentage", "r_m_w_20", "r_m_r_20",
607 "living_wood_fraction", "growth_respiration_fraction", "carbohydrate_abortion_threshold",
608 "carbohydrate_pruning_threshold", "bud_death_threshold_days", "branch_death_threshold_days",
609 "carbohydrate_phyllochron_threshold", "carbohydrate_vegetative_break_threshold",
610 "carbohydrate_growth_threshold", "starch_sequestration_ratio",
611 "carbohydrate_transfer_threshold_down", "carbohydrate_transfer_threshold_up",
612 "carbon_conductance_down", "carbon_conductance_up",
614
615
616@dataclass
618 """Flat carbohydrate-model parameters. Defaults mirror the Helios C++ defaults;
619 prefer ``CarbohydrateParameters.from_dict(pa.getDefaultCarbohydrateParameters())``."""
620
621 stem_density: float = 675000.0
622 stem_carbon_percentage: float = 0.457
623 stem_carbohydrate_percentage: float = 1 - (1 / 1.14)
624 stem_structural_carbon_percentage: float = 0.457 - (1 - (1 / 1.14))
625 maturity_age: float = 120.0
626 initial_density_ratio: float = 0.25
627 shoot_root_ratio: float = 3.0
628 leaf_total_carbon_percentage: float = 0.453
629 SLA: float = 9.2 / 10000 / 0.453 * 12.01
630 leaf_carbohydrate_percentage: float = 1 - (1 / 1.13)
631 leaf_carbon_percentage: float = 0.453
632 total_flower_cost: float = 8.33e-4
633 fruit_density: float = 525000.0
634 fruit_carbon_percentage: float = 0.475
635 r_m_w_20: float = 5.25164e-05
636 r_m_r_20: float = 5.25164e-03
637 living_wood_fraction: float = 0.5
638 growth_respiration_fraction: float = 0.211
639 carbohydrate_abortion_threshold: float = 0.1
640 carbohydrate_pruning_threshold: float = 0.025
641 bud_death_threshold_days: float = 2.0
642 branch_death_threshold_days: float = 5.0
643 carbohydrate_phyllochron_threshold: float = 0.05
644 carbohydrate_vegetative_break_threshold: float = 0.05
645 carbohydrate_growth_threshold: float = 0.2
646 starch_sequestration_ratio: float = 0.025
647 carbohydrate_transfer_threshold_down: float = 0.025
648 carbohydrate_transfer_threshold_up: float = 0.04
649 carbon_conductance_down: float = 0.95
650 carbon_conductance_up: float = 0.95 * 0.5
652 def to_dict(self) -> Dict[str, float]:
653 return _flat_to_dict(self, _CARB_FIELDS)
655 @classmethod
656 def from_dict(cls, d: Dict[str, Any]) -> "CarbohydrateParameters":
657 return _flat_from_dict(cls, d, _CARB_FIELDS)
660_NITROGEN_FIELDS = (
661 "target_leaf_N_area", "minimum_leaf_N_area", "root_allocation_fraction",
662 "max_N_accumulation_rate", "leaf_remobilization_efficiency",
663 "remobilization_age_threshold", "fruit_N_area",
665
666
667@dataclass
669 """Flat nitrogen-model parameters. Prefer
670 ``NitrogenParameters.from_dict(pa.getDefaultNitrogenParameters())``."""
671
672 target_leaf_N_area: float = 1.5
673 minimum_leaf_N_area: float = 0.5
674 root_allocation_fraction: float = 0.15
675 max_N_accumulation_rate: float = 0.1
676 leaf_remobilization_efficiency: float = 0.70
677 remobilization_age_threshold: float = 0.70
678 fruit_N_area: float = 1.0
679
680 def to_dict(self) -> Dict[str, float]:
681 return _flat_to_dict(self, _NITROGEN_FIELDS)
682
683 @classmethod
684 def from_dict(cls, d: Dict[str, Any]) -> "NitrogenParameters":
685 return _flat_from_dict(cls, d, _NITROGEN_FIELDS)
"CarbohydrateParameters" from_dict(cls, Dict[str, Any] d)
"InflorescenceParameters" from_dict(cls, Dict[str, Any] d)
"InternodeParameters" from_dict(cls, Dict[str, Any] d)
"PeduncleParameters" from_dict(cls, Dict[str, Any] d)
"PetioleParameters" from_dict(cls, Dict[str, Any] d)
"PhytomerParameters" from_dict(cls, Dict[str, Any] d)
A float-valued parameter with a sampling distribution.
"RandomParameterFloat" from_dict(cls, Dict[str, Any] d)
"RandomParameterFloat" normal(cls, float mean, float std_dev)
"RandomParameterFloat" weibull(cls, float shape, float scale)
"RandomParameterFloat" uniform(cls, float min_val, float max_val)
An int-valued parameter with a sampling distribution.
"RandomParameterInt" from_dict(cls, Dict[str, Any] d)
"RandomParameterInt" discrete(cls, List[int] values)
"RandomParameterInt" uniform(cls, int min_val, int max_val)
None define_child_shoot_types(self, List[str] labels, List[float] probabilities)
Set child shoot types; probabilities must sum to 1 (validated natively).
Vec3 _vec3_from_dict(Dict[str, Any] d, Vec3 default)
Any _flat_from_dict(Any cls, Dict[str, Any] d, Tuple[str,...] names)
Dict[str, float] _flat_to_dict(Any obj, Tuple[str,...] names)
RandomParameterInt _rpi(Dict[str, Any] d, str key, RandomParameterInt default)
Color _color_from_dict(Dict[str, Any] d, Color default)
RandomParameterFloat _rpf(Dict[str, Any] d, str key, RandomParameterFloat default)