0.1.39
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". Phytomer creation and callback functions are not
30 part of these parameter objects; see
31 :meth:`~pyhelios.PlantArchitecture.PlantArchitecture.setPhytomerCreationFunction` and
32 :meth:`~pyhelios.PlantArchitecture.PlantArchitecture.setPhytomerCallbackFunction`.
33* Child shoot types round-trip in both directions as of helios-core 1.3.84; see
34 :attr:`ShootParameters.child_shoot_types`.
35"""
36
37from __future__ import annotations
38
39import warnings
40from dataclasses import dataclass, field
41from enum import IntEnum
42from typing import Any, Dict, List, Optional, Tuple
43
44__all__ = [
45 "BudState",
46 "RandomParameterFloat",
47 "RandomParameterInt",
48 "RandomParameter",
49 "LeafPrototype",
50 "InternodeParameters",
51 "PetioleParameters",
52 "LeafParameters",
53 "PeduncleParameters",
54 "InflorescenceParameters",
55 "PhytomerParameters",
56 "ShootParameters",
57 "CarbohydrateParameters",
58 "NitrogenParameters",
59 "LEAF_EXPANSION_RATE_UNSET",
60]
61
62
63#: Sentinel value of :attr:`ShootParameters.leaf_expansion_rate_max` meaning "expand leaves at
64#: the shoot's internode elongation rate". The native comparison is against zero rather than
65#: this exact value, so any negative rate is treated as unset; a rate is a non-negative
66#: quantity, so no negative value can be confused with one a user intended.
67LEAF_EXPANSION_RATE_UNSET = -1.0
68
69
70# --------------------------------------------------------------------------- #
71# Bud state
72# --------------------------------------------------------------------------- #
73class BudState(IntEnum):
74 """State of a vegetative or floral bud.
75
76 ``DEAD`` means the bud will produce nothing further. It is set both when a bud is
77 killed -- by :meth:`~pyhelios.PlantArchitecture.PlantArchitecture.removeShootVegetativeBuds`
78 or by a failed bud-break draw -- and when a bud has *already broken into a child
79 shoot*. A count of dead buds is therefore not a count of killed buds; to test
80 whether a shoot can still grow, count the live states instead.
81 """
82
83 DORMANT = 0
84 ACTIVE = 1
85 FLOWER_CLOSED = 2
86 FLOWER_OPEN = 3
87 FRUITING = 4
88 DEAD = 5
89
90
91# --------------------------------------------------------------------------- #
92# Random parameters
93# --------------------------------------------------------------------------- #
94@dataclass
96 """A float-valued parameter with a sampling distribution.
97
98 Use the classmethod constructors (:meth:`constant`, :meth:`uniform`,
99 :meth:`normal`, :meth:`weibull`) rather than constructing directly.
100 """
101
102 distribution: str = "constant"
103 parameters: List[float] = field(default_factory=lambda: [0.0])
104
105 @classmethod
106 def constant(cls, value: float) -> "RandomParameterFloat":
107 return cls("constant", [float(value)])
108
109 @classmethod
110 def uniform(cls, min_val: float, max_val: float) -> "RandomParameterFloat":
111 if min_val > max_val:
112 raise ValueError(f"min_val ({min_val}) must be <= max_val ({max_val})")
113 return cls("uniform", [float(min_val), float(max_val)])
114
115 @classmethod
116 def normal(cls, mean: float, std_dev: float) -> "RandomParameterFloat":
117 if std_dev < 0:
118 raise ValueError(f"std_dev ({std_dev}) must be >= 0")
119 return cls("normal", [float(mean), float(std_dev)])
120
121 @classmethod
122 def weibull(cls, shape: float, scale: float) -> "RandomParameterFloat":
123 if shape <= 0:
124 raise ValueError(f"shape ({shape}) must be > 0")
125 if scale <= 0:
126 raise ValueError(f"scale ({scale}) must be > 0")
127 return cls("weibull", [float(shape), float(scale)])
128
129 def to_dict(self) -> Dict[str, Any]:
130 return {"distribution": self.distribution, "parameters": [float(p) for p in self.parameters]}
131
132 @classmethod
133 def from_dict(cls, d: Dict[str, Any]) -> "RandomParameterFloat":
134 return cls(str(d["distribution"]), [float(p) for p in d["parameters"]])
135
136
137@dataclass
139 """An int-valued parameter with a sampling distribution."""
140
141 distribution: str = "constant"
142 parameters: List[int] = field(default_factory=lambda: [0])
143
144 @classmethod
145 def constant(cls, value: int) -> "RandomParameterInt":
146 return cls("constant", [int(value)])
147
148 @classmethod
149 def uniform(cls, min_val: int, max_val: int) -> "RandomParameterInt":
150 if min_val > max_val:
151 raise ValueError(f"min_val ({min_val}) must be <= max_val ({max_val})")
152 return cls("uniform", [int(min_val), int(max_val)])
153
154 @classmethod
155 def discrete(cls, values: List[int]) -> "RandomParameterInt":
156 if not values:
157 raise ValueError("values list cannot be empty")
158 return cls("discretevalues", [int(v) for v in values])
159
160 def to_dict(self) -> Dict[str, Any]:
161 # The native JSON transport encodes int distribution parameters as floats.
162 return {"distribution": self.distribution, "parameters": [float(p) for p in self.parameters]}
163
164 @classmethod
165 def from_dict(cls, d: Dict[str, Any]) -> "RandomParameterInt":
166 return cls(str(d["distribution"]), [int(round(float(p))) for p in d["parameters"]])
167
168
169# Backward-compatible name for the float-valued parameter, retained from the
170# original dict-returning ``PlantArchitecture.RandomParameter`` helper.
171RandomParameter = RandomParameterFloat
172
173
174# --------------------------------------------------------------------------- #
175# Helpers for color / vec3 (encoded as plain tuples on the Python side)
176# --------------------------------------------------------------------------- #
177Color = Tuple[float, float, float]
178Vec3 = Tuple[float, float, float]
179
180
181def _rpf(d: Dict[str, Any], key: str, default: RandomParameterFloat) -> RandomParameterFloat:
182 return RandomParameterFloat.from_dict(d[key]) if key in d else default
183
184
185def _rpi(d: Dict[str, Any], key: str, default: RandomParameterInt) -> RandomParameterInt:
186 return RandomParameterInt.from_dict(d[key]) if key in d else default
187
188
189def _color_to_dict(c: Color) -> Dict[str, float]:
190 return {"r": float(c[0]), "g": float(c[1]), "b": float(c[2])}
191
192
193def _color_from_dict(d: Dict[str, Any], default: Color) -> Color:
194 return (float(d.get("r", default[0])), float(d.get("g", default[1])), float(d.get("b", default[2])))
195
196
197def _vec3_to_dict(v: Vec3) -> Dict[str, float]:
198 return {"x": float(v[0]), "y": float(v[1]), "z": float(v[2])}
199
200
201def _vec3_from_dict(d: Dict[str, Any], default: Vec3) -> Vec3:
202 return (float(d.get("x", default[0])), float(d.get("y", default[1])), float(d.get("z", default[2])))
203
204
205# --------------------------------------------------------------------------- #
206# Leaf prototype
207# --------------------------------------------------------------------------- #
208@dataclass
210 leaf_aspect_ratio: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(1.0))
211 midrib_fold_fraction: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
212 longitudinal_curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
213 lateral_curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
214 petiole_roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
215 wave_period: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
216 wave_amplitude: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
217 longitudinal_curvature_exponent: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(4.0))
218 flexibility: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
219 flexibility_taper: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(1.0))
220 flexibility_aging: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
221 flexibility_aging_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(4.0))
222 #: Deprecated since helios-core 1.3.84; superseded by :attr:`flexibility`. Setting a non-zero
223 #: value emits a DeprecationWarning. The value is still honoured natively -- it is converted to
224 #: an equivalent ``flexibility`` -- but only when ``flexibility`` itself is left at zero.
225 leaf_buckle_length: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
226 #: Deprecated since helios-core 1.3.84; superseded by :attr:`flexibility`. See
227 #: :attr:`leaf_buckle_length`.
228 leaf_buckle_angle: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
229 leaf_offset: Vec3 = (0.0, 0.0, 0.0)
230 subdivisions: int = 1
231 unique_prototypes: int = 1
232 #: Build a petiolule (the stalk joining a leaflet to the rachis): a cylinder along the
233 #: leaflet's midrib ending where the blade begins, then a short join that flattens and
234 #: tapers onto the blade base. Its primitives are labelled "petiolule", are drawn with the
235 #: petiole's material, and are not counted in leaf area.
236 build_petiolule: bool = False
237 #: Petiolule length as a fraction of the leaflet's blade length, keyed by compound-leaf
238 #: index like :attr:`leaf_texture_file` (0 is the terminal leaflet; negative and positive
239 #: indices are the lateral leaflets on either side, counted from the tip). A single entry
240 #: applies to every leaflet. Only used when :attr:`build_petiolule` is True
241 #: (helios-core 1.3.89+). The petiolule's radius is not a parameter: it is taken from the
242 #: petiole where the leaflet attaches.
243 petiolule_length: Dict[int, float] = field(default_factory=lambda: {0: 0.05})
244 OBJ_model_file: str = ""
245 leaf_texture_file: Dict[int, str] = field(default_factory=dict)
246 prototype_function: Optional[str] = None
247
248 def _warn_if_deprecated_buckle_set(self) -> None:
249 """Warn when a deprecated buckle parameter carries a non-zero value.
250
251 helios-core 1.3.84 replaced the fixed-station kink these produced with the
252 continuous self-weight droop of :attr:`flexibility`. The buckle values are still
253 converted to an equivalent flexibility natively, but only while :attr:`flexibility`
254 is zero, so a plant setting both silently ignores the buckle pair.
255 """
256 for name in ("leaf_buckle_length", "leaf_buckle_angle"):
257 param = getattr(self, name)
258 if any(v != 0.0 for v in param.parameters):
259 warnings.warn(
260 f"LeafPrototype.{name} is deprecated since helios-core 1.3.84 and is "
261 "superseded by LeafPrototype.flexibility, which bends the leaf continuously "
262 "under its own weight as it grows rather than kinking it at a fixed station. "
263 "Set flexibility (and optionally flexibility_taper / flexibility_aging) instead; "
264 "the buckle value is converted to an equivalent flexibility only while "
265 "flexibility is left at zero.",
266 DeprecationWarning,
267 stacklevel=3,
268 )
269
270 def to_dict(self) -> Dict[str, Any]:
272 return {
273 "leaf_aspect_ratio": self.leaf_aspect_ratio.to_dict(),
274 "midrib_fold_fraction": self.midrib_fold_fraction.to_dict(),
275 "longitudinal_curvature": self.longitudinal_curvature.to_dict(),
276 "lateral_curvature": self.lateral_curvature.to_dict(),
277 "petiole_roll": self.petiole_roll.to_dict(),
278 "wave_period": self.wave_period.to_dict(),
279 "wave_amplitude": self.wave_amplitude.to_dict(),
280 "longitudinal_curvature_exponent": self.longitudinal_curvature_exponent.to_dict(),
281 "flexibility": self.flexibility.to_dict(),
282 "flexibility_taper": self.flexibility_taper.to_dict(),
283 "flexibility_aging": self.flexibility_aging.to_dict(),
284 "flexibility_aging_max": self.flexibility_aging_max.to_dict(),
285 "leaf_buckle_length": self.leaf_buckle_length.to_dict(),
286 "leaf_buckle_angle": self.leaf_buckle_angle.to_dict(),
287 "leaf_offset": _vec3_to_dict(self.leaf_offset),
288 "subdivisions": int(self.subdivisions),
289 "unique_prototypes": int(self.unique_prototypes),
290 "build_petiolule": bool(self.build_petiolule),
291 "petiolule_length": {str(int(k)): float(v) for k, v in self.petiolule_length.items()},
292 "OBJ_model_file": self.OBJ_model_file,
293 "leaf_texture_file": {str(k): v for k, v in self.leaf_texture_file.items()},
294 "prototype_function": self.prototype_function or "",
295 }
296
297 @classmethod
298 def from_dict(cls, d: Dict[str, Any]) -> "LeafPrototype":
299 base = cls()
300 tex = d.get("leaf_texture_file", {})
301 return cls(
302 leaf_aspect_ratio=_rpf(d, "leaf_aspect_ratio", base.leaf_aspect_ratio),
303 midrib_fold_fraction=_rpf(d, "midrib_fold_fraction", base.midrib_fold_fraction),
304 longitudinal_curvature=_rpf(d, "longitudinal_curvature", base.longitudinal_curvature),
305 lateral_curvature=_rpf(d, "lateral_curvature", base.lateral_curvature),
306 petiole_roll=_rpf(d, "petiole_roll", base.petiole_roll),
307 wave_period=_rpf(d, "wave_period", base.wave_period),
308 wave_amplitude=_rpf(d, "wave_amplitude", base.wave_amplitude),
309 longitudinal_curvature_exponent=_rpf(d, "longitudinal_curvature_exponent", base.longitudinal_curvature_exponent),
310 flexibility=_rpf(d, "flexibility", base.flexibility),
311 flexibility_taper=_rpf(d, "flexibility_taper", base.flexibility_taper),
312 flexibility_aging=_rpf(d, "flexibility_aging", base.flexibility_aging),
313 flexibility_aging_max=_rpf(d, "flexibility_aging_max", base.flexibility_aging_max),
314 leaf_buckle_length=_rpf(d, "leaf_buckle_length", base.leaf_buckle_length),
315 leaf_buckle_angle=_rpf(d, "leaf_buckle_angle", base.leaf_buckle_angle),
316 leaf_offset=_vec3_from_dict(d["leaf_offset"], base.leaf_offset) if "leaf_offset" in d else base.leaf_offset,
317 subdivisions=int(d.get("subdivisions", base.subdivisions)),
318 unique_prototypes=int(d.get("unique_prototypes", base.unique_prototypes)),
319 build_petiolule=bool(d.get("build_petiolule", base.build_petiolule)),
320 petiolule_length=({int(k): float(v) for k, v in d["petiolule_length"].items()}
321 if "petiolule_length" in d else base.petiolule_length),
322 OBJ_model_file=str(d.get("OBJ_model_file", base.OBJ_model_file)),
323 leaf_texture_file={int(k): str(v) for k, v in tex.items()},
324 prototype_function=(d.get("prototype_function") or None),
325 )
326
327
328# --------------------------------------------------------------------------- #
329# Phytomer sub-structures
330# --------------------------------------------------------------------------- #
331@dataclass
333 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(20.0))
334 phyllotactic_angle: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(137.5))
335 radius_initial: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.001))
336 max_vegetative_buds_per_petiole: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(0))
337 max_floral_buds_per_petiole: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(0))
338 color: Color = (0.0, 0.0, 0.0)
339 image_texture: str = ""
340 length_segments: int = 1
341 radial_subdivisions: int = 7
343 def to_dict(self) -> Dict[str, Any]:
344 return {
345 "pitch": self.pitch.to_dict(),
346 "phyllotactic_angle": self.phyllotactic_angle.to_dict(),
347 "radius_initial": self.radius_initial.to_dict(),
348 "max_vegetative_buds_per_petiole": self.max_vegetative_buds_per_petiole.to_dict(),
349 "max_floral_buds_per_petiole": self.max_floral_buds_per_petiole.to_dict(),
350 "color": _color_to_dict(self.color),
351 "image_texture": self.image_texture,
352 "length_segments": int(self.length_segments),
353 "radial_subdivisions": int(self.radial_subdivisions),
354 }
355
356 @classmethod
357 def from_dict(cls, d: Dict[str, Any]) -> "InternodeParameters":
358 base = cls()
359 return cls(
360 pitch=_rpf(d, "pitch", base.pitch),
361 phyllotactic_angle=_rpf(d, "phyllotactic_angle", base.phyllotactic_angle),
362 radius_initial=_rpf(d, "radius_initial", base.radius_initial),
363 max_vegetative_buds_per_petiole=_rpi(d, "max_vegetative_buds_per_petiole", base.max_vegetative_buds_per_petiole),
364 max_floral_buds_per_petiole=_rpi(d, "max_floral_buds_per_petiole", base.max_floral_buds_per_petiole),
365 color=_color_from_dict(d["color"], base.color) if "color" in d else base.color,
366 image_texture=str(d.get("image_texture", base.image_texture)),
367 length_segments=int(d.get("length_segments", base.length_segments)),
368 radial_subdivisions=int(d.get("radial_subdivisions", base.radial_subdivisions)),
369 )
370
371
372@dataclass
374 petioles_per_internode: int = 1
375 #: Angle in degrees of the petiole base axis relative to its parent internode axis. Not
376 #: used for leaves without a petiole (``length`` of exactly 0).
377 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(90.0))
378 #: Radius in meters of the petiole cross-section. If either radius or length is 0, no
379 #: petiole geometry is created.
380 radius: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.001))
381 #: Length in meters of the petiole tube. A length of exactly 0 means the leaf has no
382 #: petiole at all and sits directly on the stem, as a grass leaf does: ``leaf.pitch`` is
383 #: then its angle off the internode (zero or negative), none of the other petiole
384 #: parameters are used, and the leaf must be simple (one leaf per petiole). A small
385 #: positive length gives a short petiole, whose leaf is posed as on any other petiole.
386 length: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.05))
387 curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
388 taper: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
389 color: Color = (0.0, 0.0, 0.0)
390 length_segments: int = 1
391 radial_subdivisions: int = 7
392 #: Dimensionless bending compliance: the petiole arches toward the ground under its
393 #: leaflets' weight as the leaf grows and the petiole ages. Normalized so a straight,
394 #: horizontal, untapered petiole carrying its full-grown leaf weight at the tip bends
395 #: by this many radians there, independent of length. Zero keeps the petiole rigid.
396 flexibility: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
397 #: Timescale in days over which the compliance grows with age: the effective compliance
398 #: is ``flexibility * (1 + age / flexibility_aging)``, so a petiole goes on lowering
399 #: after its leaf has stopped growing. Zero disables ageing.
400 flexibility_aging: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
401
402 def to_dict(self) -> Dict[str, Any]:
403 return {
404 "petioles_per_internode": int(self.petioles_per_internode),
405 "pitch": self.pitch.to_dict(),
406 "radius": self.radius.to_dict(),
407 "length": self.length.to_dict(),
408 "curvature": self.curvature.to_dict(),
409 "taper": self.taper.to_dict(),
410 "color": _color_to_dict(self.color),
411 "length_segments": int(self.length_segments),
412 "radial_subdivisions": int(self.radial_subdivisions),
413 "flexibility": self.flexibility.to_dict(),
414 "flexibility_aging": self.flexibility_aging.to_dict(),
415 }
416
417 @classmethod
418 def from_dict(cls, d: Dict[str, Any]) -> "PetioleParameters":
419 base = cls()
420 return cls(
421 petioles_per_internode=int(d.get("petioles_per_internode", base.petioles_per_internode)),
422 pitch=_rpf(d, "pitch", base.pitch),
423 radius=_rpf(d, "radius", base.radius),
424 length=_rpf(d, "length", base.length),
425 curvature=_rpf(d, "curvature", base.curvature),
426 taper=_rpf(d, "taper", base.taper),
427 color=_color_from_dict(d["color"], base.color) if "color" in d else base.color,
428 length_segments=int(d.get("length_segments", base.length_segments)),
429 radial_subdivisions=int(d.get("radial_subdivisions", base.radial_subdivisions)),
430 flexibility=_rpf(d, "flexibility", base.flexibility),
431 flexibility_aging=_rpf(d, "flexibility_aging", base.flexibility_aging),
432 )
433
434
435@dataclass
436class LeafParameters:
437 leaves_per_petiole: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(1))
438 #: Angle in degrees of the leaf axis relative to its parent petiole axis; positive values
439 #: turn the leaf toward its upper surface. For a leaf without a petiole
440 #: (``petiole.length`` of exactly 0) it is the angle off the internode and must be zero or
441 #: negative: -20 gives a blade leaving the stem at 20 degrees.
442 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
443 yaw: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
444 roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
445 leaflet_offset: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
446 leaflet_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(1.0))
447 #: Relative size of the intercalary leaflets of an interruptedly pinnate compound leaf,
448 #: as a fraction of the major leaflet just distal to them. Zero gives a simply pinnate
449 #: leaf whose leaflets shrink monotonically from the tip.
450 intercalary_leaflet_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
451 prototype_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.05))
452 prototype: LeafPrototype = field(default_factory=LeafPrototype)
454 def to_dict(self) -> Dict[str, Any]:
455 return {
456 "leaves_per_petiole": self.leaves_per_petiole.to_dict(),
457 "pitch": self.pitch.to_dict(),
458 "yaw": self.yaw.to_dict(),
459 "roll": self.roll.to_dict(),
460 "leaflet_offset": self.leaflet_offset.to_dict(),
461 "leaflet_scale": self.leaflet_scale.to_dict(),
462 "intercalary_leaflet_scale": self.intercalary_leaflet_scale.to_dict(),
463 "prototype_scale": self.prototype_scale.to_dict(),
464 "prototype": self.prototype.to_dict(),
465 }
466
467 @classmethod
468 def from_dict(cls, d: Dict[str, Any]) -> "LeafParameters":
469 base = cls()
470 return cls(
471 leaves_per_petiole=_rpi(d, "leaves_per_petiole", base.leaves_per_petiole),
472 pitch=_rpf(d, "pitch", base.pitch),
473 yaw=_rpf(d, "yaw", base.yaw),
474 roll=_rpf(d, "roll", base.roll),
475 leaflet_offset=_rpf(d, "leaflet_offset", base.leaflet_offset),
476 leaflet_scale=_rpf(d, "leaflet_scale", base.leaflet_scale),
477 intercalary_leaflet_scale=_rpf(d, "intercalary_leaflet_scale", base.intercalary_leaflet_scale),
478 prototype_scale=_rpf(d, "prototype_scale", base.prototype_scale),
479 prototype=LeafPrototype.from_dict(d["prototype"]) if "prototype" in d else base.prototype,
480 )
481
482
483@dataclass
485 length: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.05))
486 radius: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.001))
487 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
488 roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
489 # Degrees the peduncle is turned about its parent internode, away from the leaf side of the node:
490 # 0 leaves the node from the leaf axil, 180 is opposite the leaf.
491 yaw: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
492 curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
493 #: Diffuse color of the peduncle. For a terminal inflorescence on a herbaceous stem, whose
494 #: peduncle continues the culm, this is the color at the top of the peduncle, blended along
495 #: its length (in steps of one length segment) from the internode color at its base. A
496 #: lateral peduncle, or one on a woody stem, is this color throughout.
497 color: Color = (0.0, 0.0, 0.0)
498 length_segments: int = 3
499 radial_subdivisions: int = 7
501 def to_dict(self) -> Dict[str, Any]:
502 return {
503 "length": self.length.to_dict(),
504 "radius": self.radius.to_dict(),
505 "pitch": self.pitch.to_dict(),
506 "roll": self.roll.to_dict(),
507 "yaw": self.yaw.to_dict(),
508 "curvature": self.curvature.to_dict(),
509 "color": _color_to_dict(self.color),
510 "length_segments": int(self.length_segments),
511 "radial_subdivisions": int(self.radial_subdivisions),
512 }
513
514 @classmethod
515 def from_dict(cls, d: Dict[str, Any]) -> "PeduncleParameters":
516 base = cls()
517 return cls(
518 length=_rpf(d, "length", base.length),
519 radius=_rpf(d, "radius", base.radius),
520 pitch=_rpf(d, "pitch", base.pitch),
521 roll=_rpf(d, "roll", base.roll),
522 yaw=_rpf(d, "yaw", base.yaw),
523 curvature=_rpf(d, "curvature", base.curvature),
524 color=_color_from_dict(d["color"], base.color) if "color" in d else base.color,
525 length_segments=int(d.get("length_segments", base.length_segments)),
526 radial_subdivisions=int(d.get("radial_subdivisions", base.radial_subdivisions)),
527 )
528
529
530@dataclass
532 flowers_per_peduncle: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(1))
533 flower_offset: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
534 pitch: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
535 roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
536 flower_prototype_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0075))
537 fruit_prototype_scale: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0075))
538 fruit_gravity_factor_fraction: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
539 unique_prototypes: int = 1
540 #: Days for the inflorescence to expand from its initial quarter size to full size (helios-core
541 #: 1.3.85+). A non-positive value (the default, -1) defers to the plant-level fruit-maturity
542 #: threshold set by ``setPlantPhenologicalThresholds()``. Set it where the inflorescence finishes
543 #: elongating on a different schedule from the fruit, as a maize tassel does (6 days).
544 inflorescence_maturity_period: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(-1.0))
545 flower_prototype_function: Optional[str] = None
546 fruit_prototype_function: Optional[str] = None
548 def to_dict(self) -> Dict[str, Any]:
549 return {
550 "flowers_per_peduncle": self.flowers_per_peduncle.to_dict(),
551 "flower_offset": self.flower_offset.to_dict(),
552 "pitch": self.pitch.to_dict(),
553 "roll": self.roll.to_dict(),
554 "flower_prototype_scale": self.flower_prototype_scale.to_dict(),
555 "fruit_prototype_scale": self.fruit_prototype_scale.to_dict(),
556 "fruit_gravity_factor_fraction": self.fruit_gravity_factor_fraction.to_dict(),
557 "unique_prototypes": int(self.unique_prototypes),
558 "inflorescence_maturity_period": self.inflorescence_maturity_period.to_dict(),
559 "flower_prototype_function": self.flower_prototype_function or "",
560 "fruit_prototype_function": self.fruit_prototype_function or "",
561 }
562
563 @classmethod
564 def from_dict(cls, d: Dict[str, Any]) -> "InflorescenceParameters":
565 base = cls()
566 return cls(
567 flowers_per_peduncle=_rpi(d, "flowers_per_peduncle", base.flowers_per_peduncle),
568 flower_offset=_rpf(d, "flower_offset", base.flower_offset),
569 pitch=_rpf(d, "pitch", base.pitch),
570 roll=_rpf(d, "roll", base.roll),
571 flower_prototype_scale=_rpf(d, "flower_prototype_scale", base.flower_prototype_scale),
572 fruit_prototype_scale=_rpf(d, "fruit_prototype_scale", base.fruit_prototype_scale),
573 fruit_gravity_factor_fraction=_rpf(d, "fruit_gravity_factor_fraction", base.fruit_gravity_factor_fraction),
574 unique_prototypes=int(d.get("unique_prototypes", base.unique_prototypes)),
575 inflorescence_maturity_period=_rpf(d, "inflorescence_maturity_period", base.inflorescence_maturity_period),
576 flower_prototype_function=(d.get("flower_prototype_function") or None),
577 fruit_prototype_function=(d.get("fruit_prototype_function") or None),
578 )
579
580
581@dataclass
583 internode: InternodeParameters = field(default_factory=InternodeParameters)
584 petiole: PetioleParameters = field(default_factory=PetioleParameters)
585 leaf: LeafParameters = field(default_factory=LeafParameters)
586 peduncle: PeduncleParameters = field(default_factory=PeduncleParameters)
587 inflorescence: InflorescenceParameters = field(default_factory=InflorescenceParameters)
589 def to_dict(self) -> Dict[str, Any]:
590 return {
591 "internode": self.internode.to_dict(),
592 "petiole": self.petiole.to_dict(),
593 "leaf": self.leaf.to_dict(),
594 "peduncle": self.peduncle.to_dict(),
595 "inflorescence": self.inflorescence.to_dict(),
596 }
597
598 @classmethod
599 def from_dict(cls, d: Dict[str, Any]) -> "PhytomerParameters":
600 base = cls()
601 return cls(
602 internode=InternodeParameters.from_dict(d["internode"]) if "internode" in d else base.internode,
603 petiole=PetioleParameters.from_dict(d["petiole"]) if "petiole" in d else base.petiole,
604 leaf=LeafParameters.from_dict(d["leaf"]) if "leaf" in d else base.leaf,
605 peduncle=PeduncleParameters.from_dict(d["peduncle"]) if "peduncle" in d else base.peduncle,
606 inflorescence=InflorescenceParameters.from_dict(d["inflorescence"]) if "inflorescence" in d else base.inflorescence,
607 )
608
609
610# --------------------------------------------------------------------------- #
611# Shoot parameters
612# --------------------------------------------------------------------------- #
613# Top-level ShootParameters fields, with their RandomParameter kind. Keeping this
614# as a table avoids 20+ near-identical lines in both to_dict and from_dict.
615_SHOOT_RPF_FIELDS = (
616 ("girth_area_factor", 0.0),
617 ("insertion_angle_tip", 20.0),
618 ("insertion_angle_decay_rate", 0.0),
619 ("internode_length_max", 0.02),
620 ("internode_length_min", 0.002),
621 ("internode_length_decay_rate", 0.0),
622 ("base_roll", 0.0),
623 ("base_yaw", 0.0),
624 ("gravitropic_curvature", 0.0),
625 ("tortuosity", 0.0),
626 ("tortuosity_persistence_length", 0.5),
627 ("phyllochron_min", 2.0),
628 ("elongation_rate_max", 0.2),
629 ("leaf_expansion_rate_max", LEAF_EXPANSION_RATE_UNSET),
630 ("vegetative_bud_break_probability_min", 0.0),
631 ("vegetative_bud_break_probability_max", 1.0),
632 ("vegetative_bud_break_probability_decay_rate", -0.5),
633 ("flower_bud_break_probability", 0.0),
634 ("fruit_set_probability", 0.0),
635 ("vegetative_bud_break_time", 5.0),
636)
637_SHOOT_RPI_FIELDS = (
638 ("max_nodes", 10),
639 ("max_nodes_per_season", 9999),
640 ("max_terminal_floral_buds", 0),
641)
642_SHOOT_BOOL_FIELDS = (
643 ("flowers_require_dormancy", False),
644 ("growth_requires_dormancy", False),
645 ("determinate_shoot_growth", True),
646)
647
648
649@dataclass
650class ShootParameters:
651 phytomer_parameters: PhytomerParameters = field(default_factory=PhytomerParameters)
652
653 # Geometric / growth RandomParameter_float fields
654 #: Internode cross-sectional area (cm^2) per m^2 of downstream leaf area. Downstream leaf
655 #: area is cumulative -- every leaf ever produced above the internode, not reduced when
656 #: leaves drop or branches are shed (pipe model with disused pipes retained), so perennial
657 #: species need a much smaller factor than one driven by the current canopy. 0 disables
658 #: girth scaling.
659 girth_area_factor: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
660 insertion_angle_tip: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(20.0))
661 insertion_angle_decay_rate: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
662 internode_length_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.02))
663 internode_length_min: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.002))
664 internode_length_decay_rate: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
665 base_roll: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
666 base_yaw: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
667 gravitropic_curvature: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
668 #: Standard deviation (degrees/m) of the random curvature that produces "wiggle" along
669 #: the shoot (an Ornstein-Uhlenbeck process in arc length), so the shoot's shape does not
670 #: depend on ``internode.length_segments``. Values from helios-core before 1.3.89, which
671 #: were an angle per internode segment, convert as
672 #: ``tortuosity * internode.length_segments / internode_length_max``.
673 tortuosity: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
674 #: Persistence length (m) over which the random tortuosity perturbation of the shoot's
675 #: curvature decorrelates (helios-core 1.3.89+): larger values give long, smooth meanders,
676 #: smaller values rapid, fine-scale wiggle. Values <= 0 fall back to the default of 0.5 m.
677 tortuosity_persistence_length: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.5))
678 phyllochron_min: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(2.0))
679 elongation_rate_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.2))
680 #: Maximum relative expansion rate of the shoot's leaves and petioles
681 #: (m * m^-1 * day^-1), in the same form as ``elongation_rate_max``, so 0.1 expands a
682 #: leaf from nothing to full size in ten days regardless of that leaf's size. Setting it
683 #: decouples leaf expansion from internode elongation, which a species whose leaves
684 #: finish expanding before its internodes stop elongating needs. Any negative value --
685 #: the default :data:`LEAF_EXPANSION_RATE_UNSET` -- means leaves expand at the shoot's
686 #: own ``elongation_rate_max``, reproducing the historical single-rate growth. Zero is a
687 #: real rate meaning "leaves never expand", so it differs from the unset sentinel.
688 leaf_expansion_rate_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(LEAF_EXPANSION_RATE_UNSET))
689 vegetative_bud_break_probability_min: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
690 vegetative_bud_break_probability_max: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(1.0))
691 vegetative_bud_break_probability_decay_rate: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(-0.5))
692 flower_bud_break_probability: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
693 fruit_set_probability: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(0.0))
694 vegetative_bud_break_time: RandomParameterFloat = field(default_factory=lambda: RandomParameterFloat.constant(5.0))
695
696 # RandomParameter_int fields
697 max_nodes: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(10))
698 max_nodes_per_season: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(9999))
699 max_terminal_floral_buds: RandomParameterInt = field(default_factory=lambda: RandomParameterInt.constant(0))
701 # Boolean flags
702 flowers_require_dormancy: bool = False
703 growth_requires_dormancy: bool = False
704 determinate_shoot_growth: bool = True
706 # Optional child shoot type definition: {"labels": [...], "probabilities": [...]}.
707 # Round-trips in both directions since helios-core 1.3.84 added
708 # ShootParameters::getChildShootTypeLabels()/getChildShootTypeProbabilities(), so
709 # getCurrentShootParameters() populates it. The one value that cannot be applied is an
710 # explicitly empty list: defineChildShootTypes() rejects empty input natively, so an
711 # empty list leaves whatever the shoot type being replaced already carried.
712 child_shoot_types: Optional[Dict[str, Any]] = None
714 def to_dict(self) -> Dict[str, Any]:
715 d: Dict[str, Any] = {"phytomer_parameters": self.phytomer_parameters.to_dict()}
716 for name, _ in _SHOOT_RPF_FIELDS:
717 d[name] = getattr(self, name).to_dict()
718 for name, _ in _SHOOT_RPI_FIELDS:
719 d[name] = getattr(self, name).to_dict()
720 for name, _ in _SHOOT_BOOL_FIELDS:
721 d[name] = bool(getattr(self, name))
722 if self.child_shoot_types is not None:
723 d["child_shoot_types"] = self.child_shoot_types
724 return d
726 @classmethod
727 def from_dict(cls, d: Dict[str, Any]) -> "ShootParameters":
728 kwargs: Dict[str, Any] = {}
729 if "phytomer_parameters" in d:
730 kwargs["phytomer_parameters"] = PhytomerParameters.from_dict(d["phytomer_parameters"])
731 for name, default in _SHOOT_RPF_FIELDS:
732 if name in d:
733 kwargs[name] = RandomParameterFloat.from_dict(d[name])
734 for name, default in _SHOOT_RPI_FIELDS:
735 if name in d:
736 kwargs[name] = RandomParameterInt.from_dict(d[name])
737 for name, default in _SHOOT_BOOL_FIELDS:
738 if name in d:
739 kwargs[name] = bool(d[name])
740 if "child_shoot_types" in d:
741 kwargs["child_shoot_types"] = d["child_shoot_types"]
742 return cls(**kwargs)
743
744 def define_child_shoot_types(self, labels: List[str], probabilities: List[float]) -> None:
745 """Set child shoot types; probabilities must sum to 1 (validated natively)."""
746 if len(labels) != len(probabilities):
747 raise ValueError("labels and probabilities must be the same length")
748 if not labels:
749 raise ValueError("labels and probabilities cannot be empty")
750 self.child_shoot_types = {"labels": list(labels), "probabilities": [float(p) for p in probabilities]}
751
752
753# --------------------------------------------------------------------------- #
754# Flat physiology parameter structs
755# --------------------------------------------------------------------------- #
756def _flat_to_dict(obj: Any, names: Tuple[str, ...]) -> Dict[str, float]:
757 return {n: float(getattr(obj, n)) for n in names}
758
759
760def _flat_from_dict(cls: Any, d: Dict[str, Any], names: Tuple[str, ...]) -> Any:
761 base = cls()
762 kwargs = {n: float(d[n]) for n in names if n in d}
763 for n in names:
764 kwargs.setdefault(n, getattr(base, n))
765 return cls(**kwargs)
766
768_CARB_FIELDS = (
769 "stem_density", "stem_carbon_percentage", "stem_carbohydrate_percentage",
770 "stem_structural_carbon_percentage", "maturity_age", "initial_density_ratio",
771 "shoot_root_ratio", "leaf_total_carbon_percentage", "SLA",
772 "leaf_carbohydrate_percentage", "leaf_carbon_percentage", "total_flower_cost",
773 "fruit_density", "fruit_carbon_percentage", "r_m_w_20", "r_m_r_20",
774 "living_wood_fraction", "growth_respiration_fraction", "carbohydrate_abortion_threshold",
775 "carbohydrate_pruning_threshold", "bud_death_threshold_days", "branch_death_threshold_days",
776 "carbohydrate_phyllochron_threshold", "carbohydrate_vegetative_break_threshold",
777 "carbohydrate_growth_threshold", "starch_sequestration_ratio",
778 "carbohydrate_transfer_threshold_down", "carbohydrate_transfer_threshold_up",
779 "carbon_conductance_down", "carbon_conductance_up",
780)
781
783@dataclass
785 """Flat carbohydrate-model parameters. Defaults mirror the Helios C++ defaults;
786 prefer ``CarbohydrateParameters.from_dict(pa.getDefaultCarbohydrateParameters())``."""
787
788 stem_density: float = 675000.0
789 stem_carbon_percentage: float = 0.457
790 stem_carbohydrate_percentage: float = 1 - (1 / 1.14)
791 stem_structural_carbon_percentage: float = 0.457 - (1 - (1 / 1.14))
792 maturity_age: float = 120.0
793 initial_density_ratio: float = 0.25
794 shoot_root_ratio: float = 3.0
795 leaf_total_carbon_percentage: float = 0.453
796 SLA: float = 9.2 / 10000 / 0.453 * 12.01
797 leaf_carbohydrate_percentage: float = 1 - (1 / 1.13)
798 leaf_carbon_percentage: float = 0.453
799 total_flower_cost: float = 8.33e-4
800 fruit_density: float = 525000.0
801 fruit_carbon_percentage: float = 0.475
802 r_m_w_20: float = 5.25164e-05
803 r_m_r_20: float = 5.25164e-03
804 living_wood_fraction: float = 0.5
805 growth_respiration_fraction: float = 0.211
806 carbohydrate_abortion_threshold: float = 0.1
807 carbohydrate_pruning_threshold: float = 0.025
808 bud_death_threshold_days: float = 2.0
809 branch_death_threshold_days: float = 5.0
810 carbohydrate_phyllochron_threshold: float = 0.05
811 carbohydrate_vegetative_break_threshold: float = 0.05
812 carbohydrate_growth_threshold: float = 0.2
813 starch_sequestration_ratio: float = 0.025
814 carbohydrate_transfer_threshold_down: float = 0.025
815 carbohydrate_transfer_threshold_up: float = 0.04
816 carbon_conductance_down: float = 0.95
817 carbon_conductance_up: float = 0.95 * 0.5
819 def to_dict(self) -> Dict[str, float]:
820 return _flat_to_dict(self, _CARB_FIELDS)
822 @classmethod
823 def from_dict(cls, d: Dict[str, Any]) -> "CarbohydrateParameters":
824 return _flat_from_dict(cls, d, _CARB_FIELDS)
827_NITROGEN_FIELDS = (
828 "target_leaf_N_area", "minimum_leaf_N_area", "root_allocation_fraction",
829 "max_N_accumulation_rate", "leaf_remobilization_rate", "leaf_remobilization_efficiency",
830 "leaf_senescence_duration_fraction", "stress_senescence_advance_fraction", "fruit_N_area",
832
834@dataclass
836 """Flat nitrogen-model parameters. Prefer
837 ``NitrogenParameters.from_dict(pa.getDefaultNitrogenParameters())``."""
838
839 target_leaf_N_area: float = 1.5
840 minimum_leaf_N_area: float = 0.5
841 root_allocation_fraction: float = 0.15
842 max_N_accumulation_rate: float = 0.1
843 leaf_remobilization_rate: float = 0.03
844 leaf_remobilization_efficiency: float = 0.70
845 leaf_senescence_duration_fraction: float = 0.25
846 stress_senescence_advance_fraction: float = 0.30
847 fruit_N_area: float = 1.0
848
849 def to_dict(self) -> Dict[str, float]:
850 return _flat_to_dict(self, _NITROGEN_FIELDS)
851
852 @classmethod
853 def from_dict(cls, d: Dict[str, Any]) -> "NitrogenParameters":
854 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)
None _warn_if_deprecated_buckle_set(self)
Warn when a deprecated buckle parameter carries a non-zero value.
"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)