0.1.39
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 # =========================================================================
10 # GPU Environment (helios-core v1.3.79+)
11 # =========================================================================
12
13 @staticmethod
14 def gpuRequiredByEnvironment() -> bool:
15 """Check whether a GPU is required by the ``HELIOS_REQUIRE_GPU`` environment variable.
16
17 True when ``HELIOS_REQUIRE_GPU`` is set to any value other than ``"0"``.
18 This is the counterpart to the ``HELIOS_NO_GPU`` veto: rather than changing
19 what hardware probes report, it makes the absence of a GPU an error.
20
21 The environment is read on every call rather than cached, so a change made
22 via ``os.environ`` is observed immediately — **except on Windows**, where it
23 is never observed at all. ``libhelios.dll`` links the MSVC C runtime
24 statically, so it holds a private copy of the environment snapshotted when
25 the DLL was loaded, while ``os.environ`` writes go through Python's own
26 runtime. On Windows ``HELIOS_REQUIRE_GPU`` must therefore be set before the
27 interpreter starts (``set HELIOS_REQUIRE_GPU=1`` in the shell, or the ``env:``
28 block of a CI job); setting it from Python has no effect on this function.
29 The same applies to ``HELIOS_NO_GPU``.
30
31 Returns:
32 True if a usable GPU is mandatory for this process
33
34 Example:
35 >>> from pyhelios import Global
36 >>> Global.gpuRequiredByEnvironment()
37 False
38 """
39 return global_wrapper.gpuRequiredByEnvironment()
40
41 @staticmethod
42 def requireGPUOrFail(context_message: str) -> None:
43 """Raise if ``HELIOS_REQUIRE_GPU`` is set but no usable GPU was found.
44
45 Call at the point code would otherwise skip for lack of a GPU. Does nothing
46 unless ``HELIOS_REQUIRE_GPU`` is set. Setting both ``HELIOS_REQUIRE_GPU`` and
47 ``HELIOS_NO_GPU`` is contradictory and is reported as such rather than letting
48 one silently win.
49
50 This function does not itself probe for hardware: reaching it is taken as
51 proof the caller already determined no GPU was usable, so when
52 ``HELIOS_REQUIRE_GPU`` is set it always raises.
53
54 Args:
55 context_message: Description of what was about to be skipped, included in
56 the error message
57
58 Raises:
59 HeliosError: If a GPU is required by the environment but none was found
60
61 Example:
62 >>> from pyhelios import Global, RadiationModel
63 >>> if not RadiationModel.probeAnyGPUBackend():
64 ... Global.requireGPUOrFail("radiation ray tracing")
65 """
66 global_wrapper.requireGPUOrFail(context_message)
67
68 # =========================================================================
69 # Process-wide random number generator (helios-core v1.3.85+)
70 # =========================================================================
71
72 @staticmethod
73 def seedRandomGenerator(seed: int) -> None:
74 """Seed the process-wide random number generator so a run can be reproduced.
75
76 helios-core has two generators. Each ``Context`` owns one, seeded with
77 :meth:`Context.seedRandomGenerator`, and it drives everything drawn through
78 the Context: primitive placement helpers, ``Context.randu()``, LiDAR
79 synthetic-scan noise, and the plant-architecture library's parameter
80 sampling. The other is a single process-wide generator behind the free
81 function ``helios::randu()``, which plug-in code uses where no Context is at
82 hand: the LiDAR leaf-group and triangle index draws in
83 ``calculateLeafArea()``, and the placement of berries within a grape
84 cluster in PlantArchitecture. This method seeds that second generator.
85
86 By default it is seeded from ``std::random_device`` and every run differs.
87 Seeding it from Python makes those draws repeatable; seed the Context too
88 if the rest of the simulation must repeat as well.
89
90 The generator is shared by all threads and access to it is synchronized, so
91 a seed set from any thread applies to every subsequent draw. Seeding fixes
92 the sequence of values drawn, not which thread draws which value, so a
93 parallel region that draws from it is still scheduling-dependent.
94
95 Args:
96 seed: Value used to seed the generator (unsigned 32-bit)
97
98 Raises:
99 ValueError: If ``seed`` is not an int in ``[0, 2**32 - 1]``
100 RuntimeError: If the native library predates helios-core v1.3.85
101
102 Example:
103 >>> from pyhelios import Global
104 >>> Global.seedRandomGenerator(42)
105 >>> a = [Global.randu() for _ in range(3)]
106 >>> Global.seedRandomGenerator(42)
107 >>> assert a == [Global.randu() for _ in range(3)]
108 """
109 global_wrapper.seedGlobalRandomGenerator(seed)
110
111 @staticmethod
112 def randu(imin: Optional[int] = None, imax: Optional[int] = None):
113 """Draw from the process-wide random number generator.
114
115 With no arguments, returns a uniform float in ``[0, 1)``. With ``imin`` and
116 ``imax``, returns a uniform integer over the **inclusive** range
117 ``[imin, imax]``; every value, endpoints included, is equally likely, and if
118 ``imin >= imax`` then ``imin`` is returned.
119
120 This draws from the same generator that :meth:`seedRandomGenerator` seeds,
121 not from any Context's generator; use :meth:`Context.randu` for that one.
122
123 Args:
124 imin: Lower bound of the integer range (inclusive). Must be given with ``imax``.
125 imax: Upper bound of the integer range (inclusive). Must be given with ``imin``.
126
127 Returns:
128 A float in ``[0, 1)`` when called without arguments, otherwise an int in
129 ``[imin, imax]``.
130
131 Raises:
132 ValueError: If exactly one of ``imin``/``imax`` is given, or either is not an int
133 RuntimeError: If the native library predates helios-core v1.3.85
134 """
135 if imin is None and imax is None:
136 return global_wrapper.globalRandu()
137 if imin is None or imax is None:
138 raise ValueError("randu() takes either no arguments or both imin and imax")
139 return global_wrapper.globalRanduInt(imin, imax)
140
141 @staticmethod
142 def evaluateBetaDistributionCDF(theta: float, mu: float, nu: float) -> float:
143 """Cumulative probability that a Beta-distributed leaf inclination is at most ``theta``.
144
145 This is the CDF of the same Beta leaf-inclination distribution that
146 PlantArchitecture's leaf angle distribution methods sample, in the same
147 parameterization: ``nu`` is the first shape parameter of the underlying Beta
148 variate and ``mu`` the second, so the mean inclination is
149 ``(pi/2) * nu / (mu + nu)``.
150
151 ``theta`` is measured from vertical and saturates outside ``[0, pi/2]``: a
152 negative angle gives 0 and an angle at or above ``pi/2`` gives 1.
153
154 Args:
155 theta: Leaf inclination angle (radians)
156 mu: First parameter of the Beta distribution; must be positive
157 nu: Second parameter of the Beta distribution; must be positive
158
159 Returns:
160 Cumulative probability in ``[0, 1]``
161
162 Raises:
163 HeliosError: If ``mu`` or ``nu`` is not positive
164 RuntimeError: If the native library predates helios-core v1.3.87
165
166 Example:
167 >>> from pyhelios import Global
168 >>> import math
169 >>> Global.evaluateBetaDistributionCDF(math.pi / 2, 1.0, 1.0)
170 1.0
171 """
172 return global_wrapper.evaluateBetaDistributionCDF(theta, mu, nu)
173
174 @staticmethod
175 def invertBetaDistributionCDF(probability: float, mu: float, nu: float) -> float:
176 """Leaf inclination angle at a given cumulative probability of the Beta distribution.
177
178 The inverse of :meth:`evaluateBetaDistributionCDF`, useful for laying out a
179 prescribed inclination distribution over a known number of leaves.
180
181 Args:
182 probability: Cumulative probability; must be in ``[0, 1]``
183 mu: First parameter of the Beta distribution; must be positive
184 nu: Second parameter of the Beta distribution; must be positive
185
186 Returns:
187 Leaf inclination angle (radians) in ``[0, pi/2]``
188
189 Raises:
190 HeliosError: If ``probability`` is outside ``[0, 1]``, or ``mu``/``nu`` is not positive
191 RuntimeError: If the native library predates helios-core v1.3.87
192 """
193 return global_wrapper.invertBetaDistributionCDF(probability, mu, nu)
194
195 @staticmethod
196 def evaluateEllipsoidalAzimuthCDF(phi: float, e: float, phi0_degrees: float) -> float:
197 """Cumulative probability that an ellipsoidally distributed leaf azimuth is at most ``phi``.
198
199 The probability is measured from the ellipse rotation ``phi0_degrees``, and
200 ``phi`` is wrapped into ``[0, 2*pi)``.
201
202 Args:
203 phi: Azimuth angle (radians)
204 e: Eccentricity of the ellipsoidal distribution; must be in ``[0, 1]``
205 phi0_degrees: Azimuthal rotation of the ellipse (degrees)
206
207 Returns:
208 Cumulative probability in ``[0, 1]``
209
210 Raises:
211 HeliosError: If ``e`` is outside ``[0, 1]``
212 RuntimeError: If the native library predates helios-core v1.3.87
213 """
214 return global_wrapper.evaluateEllipsoidalAzimuthCDF(phi, e, phi0_degrees)
215
216 @staticmethod
217 def invertEllipsoidalAzimuthCDF(probability: float, e: float, phi0_degrees: float) -> float:
218 """Leaf azimuth angle at a given cumulative probability of the ellipsoidal distribution.
219
220 The inverse of :meth:`evaluateEllipsoidalAzimuthCDF`.
221
222 Args:
223 probability: Cumulative probability; must be in ``[0, 1]``
224 e: Eccentricity of the ellipsoidal distribution; must be in ``[0, 1]``
225 phi0_degrees: Azimuthal rotation of the ellipse (degrees)
226
227 Returns:
228 Azimuth angle (radians) in ``[0, 2*pi)``
229
230 Raises:
231 HeliosError: If ``probability`` or ``e`` is outside ``[0, 1]``
232 RuntimeError: If the native library predates helios-core v1.3.87
233 """
234 return global_wrapper.invertEllipsoidalAzimuthCDF(probability, e, phi0_degrees)
235
236 # =========================================================================
237 # Blackbody Radiation (helios-core v1.3.90+)
238 # =========================================================================
239
240 @staticmethod
241 def blackbodyBandFraction(wavelength_min_nm: float, wavelength_max_nm: float, temperature_K: float) -> float:
242 """Fraction of a blackbody's total emissive power emitted between two wavelengths.
243
244 This is the integral of Planck's law between the two wavelengths divided by its
245 integral over all wavelengths (``sigma * T**4``). Multiplying the result by
246 ``emissivity * sigma * T**4`` gives the in-band emitted flux of a gray surface,
247 which is what a RadiationModel emission band added with wavelength bounds emits.
248
249 Args:
250 wavelength_min_nm: Lower wavelength bound (nm); must be >= 0
251 wavelength_max_nm: Upper wavelength bound (nm); must be greater than ``wavelength_min_nm``
252 temperature_K: Blackbody temperature (K); must be > 0
253
254 Returns:
255 Fraction of blackbody emissive power in the band, in ``[0, 1]``
256
257 Raises:
258 ValueError: If an argument is not a number
259 HeliosError: If the bounds or the temperature are out of range
260 RuntimeError: If the native library predates helios-core v1.3.90
261
262 Example:
263 >>> from pyhelios import Global
264 >>> fraction = Global.blackbodyBandFraction(8000, 14000, 300.0) # 8-14 um at 300 K
265 >>> in_band_flux = 0.98 * 5.670374e-8 * 300.0**4 * fraction # W/m^2
266 """
267 return global_wrapper.blackbodyBandFraction(wavelength_min_nm, wavelength_max_nm, temperature_K)
268
269 @staticmethod
270 def blackbodySpectralRadiance(wavelength_nm: float, temperature_K: float) -> float:
271 """Spectral radiance of a blackbody (Planck's law).
272
273 Its integral over all wavelengths is ``sigma * T**4 / pi``.
274
275 Args:
276 wavelength_nm: Wavelength (nm); must be > 0
277 temperature_K: Blackbody temperature (K); must be > 0
278
279 Returns:
280 Spectral radiance in W/m^2/sr/nm
281
282 Raises:
283 ValueError: If an argument is not a number
284 HeliosError: If the wavelength or the temperature is not positive
285 RuntimeError: If the native library predates helios-core v1.3.90
286 """
287 return global_wrapper.blackbodySpectralRadiance(wavelength_nm, temperature_K)
Process-wide helios-core functions that belong to no Context or plug-in model.
Definition Global.py:7
bool gpuRequiredByEnvironment()
Check whether a GPU is required by the HELIOS_REQUIRE_GPU environment variable.
Definition Global.py:38
randu(Optional[int] imin=None, Optional[int] imax=None)
Draw from the process-wide random number generator.
Definition Global.py:134
float evaluateBetaDistributionCDF(float theta, float mu, float nu)
Cumulative probability that a Beta-distributed leaf inclination is at most theta.
Definition Global.py:171
float blackbodySpectralRadiance(float wavelength_nm, float temperature_K)
Spectral radiance of a blackbody (Planck's law).
Definition Global.py:286
float invertEllipsoidalAzimuthCDF(float probability, float e, float phi0_degrees)
Leaf azimuth angle at a given cumulative probability of the ellipsoidal distribution.
Definition Global.py:233
None requireGPUOrFail(str context_message)
Raise if HELIOS_REQUIRE_GPU is set but no usable GPU was found.
Definition Global.py:65
float evaluateEllipsoidalAzimuthCDF(float phi, float e, float phi0_degrees)
Cumulative probability that an ellipsoidally distributed leaf azimuth is at most phi.
Definition Global.py:213
float blackbodyBandFraction(float wavelength_min_nm, float wavelength_max_nm, float temperature_K)
Fraction of a blackbody's total emissive power emitted between two wavelengths.
Definition Global.py:266
float invertBetaDistributionCDF(float probability, float mu, float nu)
Leaf inclination angle at a given cumulative probability of the Beta distribution.
Definition Global.py:192
None seedRandomGenerator(int seed)
Seed the process-wide random number generator so a run can be reproduced.
Definition Global.py:108