0.1.33
Loading...
Searching...
No Matches
Global.py
Go to the documentation of this file.
1import os
2from typing import Optional
3
4from .wrappers import UGlobalWrapper as global_wrapper
5
6class Global:
7 """Process-wide helios-core functions that belong to no Context or plug-in model.
8
9 The plug-in build root used to locate runtime assets is not settable here: it is a
10 per-model construction argument (see ``WeberPennTree(context, build_directory)``),
11 and helios-core exposes no global setter for it.
12 """
13
14 # =========================================================================
15 # GPU Environment (helios-core v1.3.79+)
16 # =========================================================================
17
18 @staticmethod
19 def gpuRequiredByEnvironment() -> bool:
20 """Check whether a GPU is required by the ``HELIOS_REQUIRE_GPU`` environment variable.
21
22 True when ``HELIOS_REQUIRE_GPU`` is set to any value other than ``"0"``.
23 This is the counterpart to the ``HELIOS_NO_GPU`` veto: rather than changing
24 what hardware probes report, it changes what a test does when no GPU is
25 found, so a CI runner dedicated to GPU coverage cannot report success after
26 silently skipping every GPU test.
27
28 The environment is read on every call rather than cached, so a change made
29 via ``os.environ`` is observed immediately — **except on Windows**, where it
30 is never observed at all. ``libhelios.dll`` links the MSVC C runtime
31 statically, so it holds a private copy of the environment snapshotted when
32 the DLL was loaded, while ``os.environ`` writes go through Python's own
33 runtime. On Windows ``HELIOS_REQUIRE_GPU`` must therefore be set before the
34 interpreter starts (``set HELIOS_REQUIRE_GPU=1`` in the shell, or the ``env:``
35 block of a CI job); setting it from Python has no effect on this function.
36 The same applies to ``HELIOS_NO_GPU``.
37
38 Returns:
39 True if a usable GPU is mandatory for this process
40
41 Example:
42 >>> from pyhelios import Global
43 >>> Global.gpuRequiredByEnvironment()
44 False
45 """
46 return global_wrapper.gpuRequiredByEnvironment()
47
48 @staticmethod
49 def requireGPUOrFail(context_message: str) -> None:
50 """Raise if ``HELIOS_REQUIRE_GPU`` is set but no usable GPU was found.
51
52 Call at the point code would otherwise skip for lack of a GPU. Does nothing
53 unless ``HELIOS_REQUIRE_GPU`` is set. Setting both ``HELIOS_REQUIRE_GPU`` and
54 ``HELIOS_NO_GPU`` is contradictory and is reported as such rather than letting
55 one silently win.
56
57 This function does not itself probe for hardware: reaching it is taken as
58 proof the caller already determined no GPU was usable, so when
59 ``HELIOS_REQUIRE_GPU`` is set it always raises.
60
61 For gating PyHelios's own tests prefer the ``skip_or_fail_without_gpu``
62 helper in ``conftest.py``, which reports a skip or failure to pytest directly
63 and works in mock mode where no native library is loaded. It also reads the
64 environment from Python, so unlike this function it works on Windows when
65 the variable was set after the interpreter started — see
66 :meth:`gpuRequiredByEnvironment` for why that difference exists.
67
68 Args:
69 context_message: Description of what was about to be skipped, included in
70 the error message
71
72 Raises:
73 HeliosError: If a GPU is required by the environment but none was found
74
75 Example:
76 >>> from pyhelios import Global, RadiationModel
77 >>> if not RadiationModel.probeAnyGPUBackend():
78 ... Global.requireGPUOrFail("radiation ray tracing")
79 """
80 global_wrapper.requireGPUOrFail(context_message)
81
82 # =========================================================================
83 # Process-wide random number generator (helios-core v1.3.85+)
84 # =========================================================================
85
86 @staticmethod
87 def seedRandomGenerator(seed: int) -> None:
88 """Seed the process-wide random number generator so a run can be reproduced.
89
90 helios-core has two generators. Each ``Context`` owns one, seeded with
91 :meth:`Context.seedRandomGenerator`, and it drives everything drawn through
92 the Context: primitive placement helpers, ``Context.randu()``, LiDAR
93 synthetic-scan noise, and the plant-architecture library's parameter
94 sampling. The other is a single process-wide generator behind the free
95 function ``helios::randu()``, which plug-in code uses where no Context is at
96 hand: the LiDAR leaf-group and triangle index draws in
97 ``calculateLeafArea()``, and the placement of berries within a grape
98 cluster in PlantArchitecture. This method seeds that second generator.
99
100 By default it is seeded from ``std::random_device`` and every run differs.
101 Seeding it from Python makes those draws repeatable; seed the Context too
102 if the rest of the simulation must repeat as well.
103
104 The generator is shared by all threads and access to it is synchronized, so
105 a seed set from any thread applies to every subsequent draw. Seeding fixes
106 the sequence of values drawn, not which thread draws which value, so a
107 parallel region that draws from it is still scheduling-dependent.
108
109 Args:
110 seed: Value used to seed the generator (unsigned 32-bit)
111
112 Raises:
113 ValueError: If ``seed`` is not an int in ``[0, 2**32 - 1]``
114 RuntimeError: If the native library predates helios-core v1.3.85
115
116 Example:
117 >>> from pyhelios import Global
118 >>> Global.seedRandomGenerator(42)
119 >>> a = [Global.randu() for _ in range(3)]
120 >>> Global.seedRandomGenerator(42)
121 >>> assert a == [Global.randu() for _ in range(3)]
122 """
123 global_wrapper.seedGlobalRandomGenerator(seed)
124
125 @staticmethod
126 def randu(imin: Optional[int] = None, imax: Optional[int] = None):
127 """Draw from the process-wide random number generator.
128
129 With no arguments, returns a uniform float in ``[0, 1)``. With ``imin`` and
130 ``imax``, returns a uniform integer over the **inclusive** range
131 ``[imin, imax]``; every value, endpoints included, is equally likely, and if
132 ``imin >= imax`` then ``imin`` is returned.
133
134 This draws from the same generator that :meth:`seedRandomGenerator` seeds,
135 not from any Context's generator; use :meth:`Context.randu` for that one.
136
137 Args:
138 imin: Lower bound of the integer range (inclusive). Must be given with ``imax``.
139 imax: Upper bound of the integer range (inclusive). Must be given with ``imin``.
140
141 Returns:
142 A float in ``[0, 1)`` when called without arguments, otherwise an int in
143 ``[imin, imax]``.
144
145 Raises:
146 ValueError: If exactly one of ``imin``/``imax`` is given, or either is not an int
147 RuntimeError: If the native library predates helios-core v1.3.85
148 """
149 if imin is None and imax is None:
150 return global_wrapper.globalRandu()
151 if imin is None or imax is None:
152 raise ValueError("randu() takes either no arguments or both imin and imax")
153 return global_wrapper.globalRanduInt(imin, imax)
154
155 @staticmethod
156 def evaluateBetaDistributionCDF(theta: float, mu: float, nu: float) -> float:
157 """Cumulative probability that a Beta-distributed leaf inclination is at most ``theta``.
158
159 This is the CDF of the same Beta leaf-inclination distribution that
160 PlantArchitecture's leaf angle distribution methods sample, in the same
161 parameterization: ``nu`` is the first shape parameter of the underlying Beta
162 variate and ``mu`` the second, so the mean inclination is
163 ``(pi/2) * nu / (mu + nu)``.
164
165 ``theta`` is measured from vertical and saturates outside ``[0, pi/2]``: a
166 negative angle gives 0 and an angle at or above ``pi/2`` gives 1.
167
168 Args:
169 theta: Leaf inclination angle (radians)
170 mu: First parameter of the Beta distribution; must be positive
171 nu: Second parameter of the Beta distribution; must be positive
172
173 Returns:
174 Cumulative probability in ``[0, 1]``
175
176 Raises:
177 HeliosError: If ``mu`` or ``nu`` is not positive
178 RuntimeError: If the native library predates helios-core v1.3.87
179
180 Example:
181 >>> from pyhelios import Global
182 >>> import math
183 >>> Global.evaluateBetaDistributionCDF(math.pi / 2, 1.0, 1.0)
184 1.0
185 """
186 return global_wrapper.evaluateBetaDistributionCDF(theta, mu, nu)
187
188 @staticmethod
189 def invertBetaDistributionCDF(probability: float, mu: float, nu: float) -> float:
190 """Leaf inclination angle at a given cumulative probability of the Beta distribution.
191
192 The inverse of :meth:`evaluateBetaDistributionCDF`, useful for laying out a
193 prescribed inclination distribution over a known number of leaves.
194
195 Args:
196 probability: Cumulative probability; must be in ``[0, 1]``
197 mu: First parameter of the Beta distribution; must be positive
198 nu: Second parameter of the Beta distribution; must be positive
199
200 Returns:
201 Leaf inclination angle (radians) in ``[0, pi/2]``
202
203 Raises:
204 HeliosError: If ``probability`` is outside ``[0, 1]``, or ``mu``/``nu`` is not positive
205 RuntimeError: If the native library predates helios-core v1.3.87
206 """
207 return global_wrapper.invertBetaDistributionCDF(probability, mu, nu)
208
209 @staticmethod
210 def evaluateEllipsoidalAzimuthCDF(phi: float, e: float, phi0_degrees: float) -> float:
211 """Cumulative probability that an ellipsoidally distributed leaf azimuth is at most ``phi``.
212
213 The probability is measured from the ellipse rotation ``phi0_degrees``, and
214 ``phi`` is wrapped into ``[0, 2*pi)``.
215
216 Args:
217 phi: Azimuth angle (radians)
218 e: Eccentricity of the ellipsoidal distribution; must be in ``[0, 1]``
219 phi0_degrees: Azimuthal rotation of the ellipse (degrees)
220
221 Returns:
222 Cumulative probability in ``[0, 1]``
223
224 Raises:
225 HeliosError: If ``e`` is outside ``[0, 1]``
226 RuntimeError: If the native library predates helios-core v1.3.87
227 """
228 return global_wrapper.evaluateEllipsoidalAzimuthCDF(phi, e, phi0_degrees)
229
230 @staticmethod
231 def invertEllipsoidalAzimuthCDF(probability: float, e: float, phi0_degrees: float) -> float:
232 """Leaf azimuth angle at a given cumulative probability of the ellipsoidal distribution.
233
234 The inverse of :meth:`evaluateEllipsoidalAzimuthCDF`.
235
236 Args:
237 probability: Cumulative probability; must be in ``[0, 1]``
238 e: Eccentricity of the ellipsoidal distribution; must be in ``[0, 1]``
239 phi0_degrees: Azimuthal rotation of the ellipse (degrees)
240
241 Returns:
242 Azimuth angle (radians) in ``[0, 2*pi)``
243
244 Raises:
245 HeliosError: If ``probability`` or ``e`` is outside ``[0, 1]``
246 RuntimeError: If the native library predates helios-core v1.3.87
247 """
248 return global_wrapper.invertEllipsoidalAzimuthCDF(probability, e, phi0_degrees)
Process-wide helios-core functions that belong to no Context or plug-in model.
Definition Global.py:12
bool gpuRequiredByEnvironment()
Check whether a GPU is required by the HELIOS_REQUIRE_GPU environment variable.
Definition Global.py:45
randu(Optional[int] imin=None, Optional[int] imax=None)
Draw from the process-wide random number generator.
Definition Global.py:148
float evaluateBetaDistributionCDF(float theta, float mu, float nu)
Cumulative probability that a Beta-distributed leaf inclination is at most theta.
Definition Global.py:185
float invertEllipsoidalAzimuthCDF(float probability, float e, float phi0_degrees)
Leaf azimuth angle at a given cumulative probability of the ellipsoidal distribution.
Definition Global.py:247
None requireGPUOrFail(str context_message)
Raise if HELIOS_REQUIRE_GPU is set but no usable GPU was found.
Definition Global.py:79
float evaluateEllipsoidalAzimuthCDF(float phi, float e, float phi0_degrees)
Cumulative probability that an ellipsoidally distributed leaf azimuth is at most phi.
Definition Global.py:227
float invertBetaDistributionCDF(float probability, float mu, float nu)
Leaf inclination angle at a given cumulative probability of the Beta distribution.
Definition Global.py:206
None seedRandomGenerator(int seed)
Seed the process-wide random number generator so a run can be reproduced.
Definition Global.py:122