zero2geospatial3d-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. zero2geospatial3d/__init__.py +105 -0
  2. zero2geospatial3d/acoustics.py +284 -0
  3. zero2geospatial3d/classify.py +379 -0
  4. zero2geospatial3d/cli.py +367 -0
  5. zero2geospatial3d/comfort.py +687 -0
  6. zero2geospatial3d/data/epw_stations.json.gz +0 -0
  7. zero2geospatial3d/daylight.py +337 -0
  8. zero2geospatial3d/dispersion.py +249 -0
  9. zero2geospatial3d/epw.py +162 -0
  10. zero2geospatial3d/geo.py +138 -0
  11. zero2geospatial3d/grid.py +119 -0
  12. zero2geospatial3d/microclimate.py +388 -0
  13. zero2geospatial3d/morphology.py +283 -0
  14. zero2geospatial3d/nsga2.py +315 -0
  15. zero2geospatial3d/osm.py +450 -0
  16. zero2geospatial3d/osm_tags.py +180 -0
  17. zero2geospatial3d/py.typed +0 -0
  18. zero2geospatial3d/results.py +231 -0
  19. zero2geospatial3d/road_sources.py +141 -0
  20. zero2geospatial3d/scene.py +555 -0
  21. zero2geospatial3d/server.py +332 -0
  22. zero2geospatial3d/shading.py +443 -0
  23. zero2geospatial3d/solar.py +207 -0
  24. zero2geospatial3d/terrain.py +610 -0
  25. zero2geospatial3d/urban_form.py +326 -0
  26. zero2geospatial3d/walkability.py +380 -0
  27. zero2geospatial3d/weather.py +373 -0
  28. zero2geospatial3d/web/index.html +50 -0
  29. zero2geospatial3d/web/src/analysis/climate.js +278 -0
  30. zero2geospatial3d/web/src/analysis/compliance.js +345 -0
  31. zero2geospatial3d/web/src/analysis/gpu.js +431 -0
  32. zero2geospatial3d/web/src/analysis/optimize.js +208 -0
  33. zero2geospatial3d/web/src/analysis/runners.js +1644 -0
  34. zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
  35. zero2geospatial3d/web/src/app.js +630 -0
  36. zero2geospatial3d/web/src/core/acoustics.js +341 -0
  37. zero2geospatial3d/web/src/core/classify.js +158 -0
  38. zero2geospatial3d/web/src/core/comfort.js +686 -0
  39. zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
  40. zero2geospatial3d/web/src/core/dispersion.js +264 -0
  41. zero2geospatial3d/web/src/core/microclimate.js +372 -0
  42. zero2geospatial3d/web/src/core/morphology.js +153 -0
  43. zero2geospatial3d/web/src/core/nsga2.js +181 -0
  44. zero2geospatial3d/web/src/core/party_walls.js +60 -0
  45. zero2geospatial3d/web/src/core/road_sources.js +127 -0
  46. zero2geospatial3d/web/src/core/solar.js +112 -0
  47. zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
  48. zero2geospatial3d/web/src/core/terrain_math.js +279 -0
  49. zero2geospatial3d/web/src/core/urban_form.js +405 -0
  50. zero2geospatial3d/web/src/core/walkability.js +316 -0
  51. zero2geospatial3d/web/src/core/weather.js +225 -0
  52. zero2geospatial3d/web/src/core/wind.js +131 -0
  53. zero2geospatial3d/web/src/export/check_pdf.js +74 -0
  54. zero2geospatial3d/web/src/export/exporters.js +556 -0
  55. zero2geospatial3d/web/src/export/pdf.js +149 -0
  56. zero2geospatial3d/web/src/export/snapshot.js +234 -0
  57. zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
  58. zero2geospatial3d/web/src/main.js +75 -0
  59. zero2geospatial3d/web/src/scene/objects.js +645 -0
  60. zero2geospatial3d/web/src/scene/overlay.js +145 -0
  61. zero2geospatial3d/web/src/scene/terrain.js +546 -0
  62. zero2geospatial3d/web/src/scene/viewer.js +275 -0
  63. zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
  64. zero2geospatial3d/web/src/style.css +225 -0
  65. zero2geospatial3d/web/src/ui/building_report.js +404 -0
  66. zero2geospatial3d/web/src/ui/charts.js +441 -0
  67. zero2geospatial3d/web/src/ui/colorize.js +101 -0
  68. zero2geospatial3d/web/src/ui/compare.js +362 -0
  69. zero2geospatial3d/web/src/ui/dom.js +104 -0
  70. zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
  71. zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
  72. zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
  73. zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
  74. zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
  75. zero2geospatial3d/web/src/ui/sunclock.js +45 -0
  76. zero2geospatial3d/web/src/ui/tools.js +197 -0
  77. zero2geospatial3d/web/src/ui/viewtools.js +173 -0
  78. zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
  79. zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
  80. zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
  81. zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
  82. zero2geospatial3d/wind.py +178 -0
  83. zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
  84. zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
  85. zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
  86. zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  87. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  88. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
  89. zero2geospatial3d_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,687 @@
1
+ """Outdoor thermal comfort: UTCI, PET and mean radiant temperature.
2
+
3
+ UTCI: the 6th-order polynomial approximation of Broede et al. (2012),
4
+ "Deriving the operational procedure for the Universal Thermal Climate
5
+ Index (UTCI)", Int J Biometeorol 56:481-494, doi:10.1007/s00484-011-0454-1.
6
+ The polynomial and the saturation vapour pressure function are transcribed
7
+ from pythermalcomfort (models/utci.py), MIT License, Copyright (c) 2019
8
+ Federico Tartarini, and checked against its documented values
9
+ (utci(25, 25, 1.0, 50) = 24.6; utci(40, 25, 1.0, 50) = 40.4).
10
+
11
+ MRT: longwave exchange with sky and surroundings plus the SolarCal
12
+ short-wave increment (Arens et al. 2015, "Modeling the comfort effects of
13
+ short-wave solar radiation indoors", Building and Environment 88:3-9,
14
+ doi:10.1016/j.buildenv.2014.09.004; ANSI/ASHRAE Standard 55, Appendix C).
15
+ Surrounding surfaces are taken at air temperature unless given: a screening
16
+ model, not a full surface energy balance.
17
+
18
+ PET: Physiological Equivalent Temperature after Hoeppe (1999) / Walther and
19
+ Goestchel (2018) MEMI, solved with a damped Newton iteration.
20
+
21
+ Ported from the 02Geo3D QGIS plugin (``web/src/core/comfort.js``).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import math
27
+ from typing import Any, Optional, Union
28
+
29
+ import numpy as np
30
+
31
+ __all__ = [
32
+ "PET_CATEGORIES",
33
+ "UTCI_CATEGORIES",
34
+ "mean_radiant_temperature",
35
+ "pet",
36
+ "pet_category",
37
+ "projected_area_factor",
38
+ "saturation_vapour_pressure",
39
+ "sky_temperature",
40
+ "utci",
41
+ "utci_category",
42
+ ]
43
+
44
+ SIGMA = 5.67e-8
45
+
46
+ ArrayLike = Union[float, np.ndarray]
47
+
48
+ _SVP_G = (
49
+ -2836.5744,
50
+ -6028.076559,
51
+ 19.54263612,
52
+ -0.02737830188,
53
+ 0.000016261698,
54
+ 7.0229056e-10,
55
+ -1.8680009e-13,
56
+ )
57
+
58
+
59
+ def saturation_vapour_pressure(tdb: ArrayLike) -> Any:
60
+ """Saturation vapour pressure over water, hPa (as in the UTCI reference code)."""
61
+ tk = np.asarray(tdb, dtype=float) + 273.15
62
+ es = 2.7150305 * np.log(tk)
63
+ for i, g in enumerate(_SVP_G):
64
+ es = es + g * tk ** float(i - 2)
65
+ out = np.exp(es) * 0.01
66
+ return float(out) if np.ndim(out) == 0 else out
67
+
68
+
69
+ def utci(tdb: ArrayLike, tr: ArrayLike, v: ArrayLike, rh: ArrayLike, limit: bool = False) -> Any:
70
+ """UTCI equivalent temperature, degrees C.
71
+
72
+ Args:
73
+ tdb: air temperature, C.
74
+ tr: mean radiant temperature, C.
75
+ v: wind speed at 10 m, m/s.
76
+ rh: relative humidity, %.
77
+ limit: return NaN outside the model's validity range
78
+ (-50..50 C air, -30..70 K radiant difference, 0.5..17 m/s wind).
79
+
80
+ Scalars give a float; arrays broadcast and give an array.
81
+ """
82
+ tdb = np.asarray(tdb, dtype=float)
83
+ tr = np.asarray(tr, dtype=float)
84
+ v = np.asarray(v, dtype=float)
85
+ rh = np.asarray(rh, dtype=float)
86
+ pa = np.asarray(saturation_vapour_pressure(tdb)) * (rh / 100) / 10 # kPa
87
+ delta_t_tr = tr - tdb
88
+ out = (
89
+ tdb
90
+ + 0.607562052
91
+ + (-0.0227712343) * tdb
92
+ + (8.06470249 * (10 ** (-4))) * tdb * tdb
93
+ + (-1.54271372 * (10 ** (-4))) * tdb * tdb * tdb
94
+ + (-3.24651735 * (10 ** (-6))) * tdb * tdb * tdb * tdb
95
+ + (7.32602852 * (10 ** (-8))) * tdb * tdb * tdb * tdb * tdb
96
+ + (1.35959073 * (10 ** (-9))) * tdb * tdb * tdb * tdb * tdb * tdb
97
+ + (-2.25836520) * v
98
+ + 0.0880326035 * tdb * v
99
+ + 0.00216844454 * tdb * tdb * v
100
+ + (-1.53347087 * (10 ** (-5))) * tdb * tdb * tdb * v
101
+ + (-5.72983704 * (10 ** (-7))) * tdb * tdb * tdb * tdb * v
102
+ + (-2.55090145 * (10 ** (-9))) * tdb * tdb * tdb * tdb * tdb * v
103
+ + (-0.751269505) * v * v
104
+ + (-0.00408350271) * tdb * v * v
105
+ + (-5.21670675 * (10 ** (-5))) * tdb * tdb * v * v
106
+ + (1.94544667 * (10 ** (-6))) * tdb * tdb * tdb * v * v
107
+ + (1.14099531 * (10 ** (-8))) * tdb * tdb * tdb * tdb * v * v
108
+ + 0.158137256 * v * v * v
109
+ + (-6.57263143 * (10 ** (-5))) * tdb * v * v * v
110
+ + (2.22697524 * (10 ** (-7))) * tdb * tdb * v * v * v
111
+ + (-4.16117031 * (10 ** (-8))) * tdb * tdb * tdb * v * v * v
112
+ + (-0.0127762753) * v * v * v * v
113
+ + (9.66891875 * (10 ** (-6))) * tdb * v * v * v * v
114
+ + (2.52785852 * (10 ** (-9))) * tdb * tdb * v * v * v * v
115
+ + (4.56306672 * (10 ** (-4))) * v * v * v * v * v
116
+ + (-1.74202546 * (10 ** (-7))) * tdb * v * v * v * v * v
117
+ + (-5.91491269 * (10 ** (-6))) * v * v * v * v * v * v
118
+ + 0.398374029 * delta_t_tr
119
+ + (1.83945314 * (10 ** (-4))) * tdb * delta_t_tr
120
+ + (-1.73754510 * (10 ** (-4))) * tdb * tdb * delta_t_tr
121
+ + (-7.60781159 * (10 ** (-7))) * tdb * tdb * tdb * delta_t_tr
122
+ + (3.77830287 * (10 ** (-8))) * tdb * tdb * tdb * tdb * delta_t_tr
123
+ + (5.43079673 * (10 ** (-10))) * tdb * tdb * tdb * tdb * tdb * delta_t_tr
124
+ + (-0.0200518269) * v * delta_t_tr
125
+ + (8.92859837 * (10 ** (-4))) * tdb * v * delta_t_tr
126
+ + (3.45433048 * (10 ** (-6))) * tdb * tdb * v * delta_t_tr
127
+ + (-3.77925774 * (10 ** (-7))) * tdb * tdb * tdb * v * delta_t_tr
128
+ + (-1.69699377 * (10 ** (-9))) * tdb * tdb * tdb * tdb * v * delta_t_tr
129
+ + (1.69992415 * (10 ** (-4))) * v * v * delta_t_tr
130
+ + (-4.99204314 * (10 ** (-5))) * tdb * v * v * delta_t_tr
131
+ + (2.47417178 * (10 ** (-7))) * tdb * tdb * v * v * delta_t_tr
132
+ + (1.07596466 * (10 ** (-8))) * tdb * tdb * tdb * v * v * delta_t_tr
133
+ + (8.49242932 * (10 ** (-5))) * v * v * v * delta_t_tr
134
+ + (1.35191328 * (10 ** (-6))) * tdb * v * v * v * delta_t_tr
135
+ + (-6.21531254 * (10 ** (-9))) * tdb * tdb * v * v * v * delta_t_tr
136
+ + (-4.99410301 * (10 ** (-6))) * v * v * v * v * delta_t_tr
137
+ + (-1.89489258 * (10 ** (-8))) * tdb * v * v * v * v * delta_t_tr
138
+ + (8.15300114 * (10 ** (-8))) * v * v * v * v * v * delta_t_tr
139
+ + (7.55043090 * (10 ** (-4))) * delta_t_tr * delta_t_tr
140
+ + (-5.65095215 * (10 ** (-5))) * tdb * delta_t_tr * delta_t_tr
141
+ + (-4.52166564 * (10 ** (-7))) * tdb * tdb * delta_t_tr * delta_t_tr
142
+ + (2.46688878 * (10 ** (-8))) * tdb * tdb * tdb * delta_t_tr * delta_t_tr
143
+ + (2.42674348 * (10 ** (-10))) * tdb * tdb * tdb * tdb * delta_t_tr * delta_t_tr
144
+ + (1.54547250 * (10 ** (-4))) * v * delta_t_tr * delta_t_tr
145
+ + (5.24110970 * (10 ** (-6))) * tdb * v * delta_t_tr * delta_t_tr
146
+ + (-8.75874982 * (10 ** (-8))) * tdb * tdb * v * delta_t_tr * delta_t_tr
147
+ + (-1.50743064 * (10 ** (-9))) * tdb * tdb * tdb * v * delta_t_tr * delta_t_tr
148
+ + (-1.56236307 * (10 ** (-5))) * v * v * delta_t_tr * delta_t_tr
149
+ + (-1.33895614 * (10 ** (-7))) * tdb * v * v * delta_t_tr * delta_t_tr
150
+ + (2.49709824 * (10 ** (-9))) * tdb * tdb * v * v * delta_t_tr * delta_t_tr
151
+ + (6.51711721 * (10 ** (-7))) * v * v * v * delta_t_tr * delta_t_tr
152
+ + (1.94960053 * (10 ** (-9))) * tdb * v * v * v * delta_t_tr * delta_t_tr
153
+ + (-1.00361113 * (10 ** (-8))) * v * v * v * v * delta_t_tr * delta_t_tr
154
+ + (-1.21206673 * (10 ** (-5))) * delta_t_tr * delta_t_tr * delta_t_tr
155
+ + (-2.18203660 * (10 ** (-7))) * tdb * delta_t_tr * delta_t_tr * delta_t_tr
156
+ + (7.51269482 * (10 ** (-9))) * tdb * tdb * delta_t_tr * delta_t_tr * delta_t_tr
157
+ + (9.79063848 * (10 ** (-11))) * tdb * tdb * tdb * delta_t_tr * delta_t_tr * delta_t_tr
158
+ + (1.25006734 * (10 ** (-6))) * v * delta_t_tr * delta_t_tr * delta_t_tr
159
+ + (-1.81584736 * (10 ** (-9))) * tdb * v * delta_t_tr * delta_t_tr * delta_t_tr
160
+ + (-3.52197671 * (10 ** (-10))) * tdb * tdb * v * delta_t_tr * delta_t_tr * delta_t_tr
161
+ + (-3.36514630 * (10 ** (-8))) * v * v * delta_t_tr * delta_t_tr * delta_t_tr
162
+ + (1.35908359 * (10 ** (-10))) * tdb * v * v * delta_t_tr * delta_t_tr * delta_t_tr
163
+ + (4.17032620 * (10 ** (-10))) * v * v * v * delta_t_tr * delta_t_tr * delta_t_tr
164
+ + (-1.30369025 * (10 ** (-9))) * delta_t_tr * delta_t_tr * delta_t_tr * delta_t_tr
165
+ + (4.13908461 * (10 ** (-10))) * tdb * delta_t_tr * delta_t_tr * delta_t_tr * delta_t_tr
166
+ + (9.22652254 * (10 ** (-12)))
167
+ * tdb
168
+ * tdb
169
+ * delta_t_tr
170
+ * delta_t_tr
171
+ * delta_t_tr
172
+ * delta_t_tr
173
+ + (-5.08220384 * (10 ** (-9))) * v * delta_t_tr * delta_t_tr * delta_t_tr * delta_t_tr
174
+ + (-2.24730961 * (10 ** (-11)))
175
+ * tdb
176
+ * v
177
+ * delta_t_tr
178
+ * delta_t_tr
179
+ * delta_t_tr
180
+ * delta_t_tr
181
+ + (1.17139133 * (10 ** (-10))) * v * v * delta_t_tr * delta_t_tr * delta_t_tr * delta_t_tr
182
+ + (6.62154879 * (10 ** (-10)))
183
+ * delta_t_tr
184
+ * delta_t_tr
185
+ * delta_t_tr
186
+ * delta_t_tr
187
+ * delta_t_tr
188
+ + (4.03863260 * (10 ** (-13)))
189
+ * tdb
190
+ * delta_t_tr
191
+ * delta_t_tr
192
+ * delta_t_tr
193
+ * delta_t_tr
194
+ * delta_t_tr
195
+ + (1.95087203 * (10 ** (-12)))
196
+ * v
197
+ * delta_t_tr
198
+ * delta_t_tr
199
+ * delta_t_tr
200
+ * delta_t_tr
201
+ * delta_t_tr
202
+ + (-4.73602469 * (10 ** (-12)))
203
+ * delta_t_tr
204
+ * delta_t_tr
205
+ * delta_t_tr
206
+ * delta_t_tr
207
+ * delta_t_tr
208
+ * delta_t_tr
209
+ + 5.12733497 * pa
210
+ + (-0.312788561) * tdb * pa
211
+ + (-0.0196701861) * tdb * tdb * pa
212
+ + (9.99690870 * (10 ** (-4))) * tdb * tdb * tdb * pa
213
+ + (9.51738512 * (10 ** (-6))) * tdb * tdb * tdb * tdb * pa
214
+ + (-4.66426341 * (10 ** (-7))) * tdb * tdb * tdb * tdb * tdb * pa
215
+ + 0.548050612 * v * pa
216
+ + (-0.00330552823) * tdb * v * pa
217
+ + (-0.00164119440) * tdb * tdb * v * pa
218
+ + (-5.16670694 * (10 ** (-6))) * tdb * tdb * tdb * v * pa
219
+ + (9.52692432 * (10 ** (-7))) * tdb * tdb * tdb * tdb * v * pa
220
+ + (-0.0429223622) * v * v * pa
221
+ + 0.00500845667 * tdb * v * v * pa
222
+ + (1.00601257 * (10 ** (-6))) * tdb * tdb * v * v * pa
223
+ + (-1.81748644 * (10 ** (-6))) * tdb * tdb * tdb * v * v * pa
224
+ + (-1.25813502 * (10 ** (-3))) * v * v * v * pa
225
+ + (-1.79330391 * (10 ** (-4))) * tdb * v * v * v * pa
226
+ + (2.34994441 * (10 ** (-6))) * tdb * tdb * v * v * v * pa
227
+ + (1.29735808 * (10 ** (-4))) * v * v * v * v * pa
228
+ + (1.29064870 * (10 ** (-6))) * tdb * v * v * v * v * pa
229
+ + (-2.28558686 * (10 ** (-6))) * v * v * v * v * v * pa
230
+ + (-0.0369476348) * delta_t_tr * pa
231
+ + 0.00162325322 * tdb * delta_t_tr * pa
232
+ + (-3.14279680 * (10 ** (-5))) * tdb * tdb * delta_t_tr * pa
233
+ + (2.59835559 * (10 ** (-6))) * tdb * tdb * tdb * delta_t_tr * pa
234
+ + (-4.77136523 * (10 ** (-8))) * tdb * tdb * tdb * tdb * delta_t_tr * pa
235
+ + (8.64203390 * (10 ** (-3))) * v * delta_t_tr * pa
236
+ + (-6.87405181 * (10 ** (-4))) * tdb * v * delta_t_tr * pa
237
+ + (-9.13863872 * (10 ** (-6))) * tdb * tdb * v * delta_t_tr * pa
238
+ + (5.15916806 * (10 ** (-7))) * tdb * tdb * tdb * v * delta_t_tr * pa
239
+ + (-3.59217476 * (10 ** (-5))) * v * v * delta_t_tr * pa
240
+ + (3.28696511 * (10 ** (-5))) * tdb * v * v * delta_t_tr * pa
241
+ + (-7.10542454 * (10 ** (-7))) * tdb * tdb * v * v * delta_t_tr * pa
242
+ + (-1.24382300 * (10 ** (-5))) * v * v * v * delta_t_tr * pa
243
+ + (-7.38584400 * (10 ** (-9))) * tdb * v * v * v * delta_t_tr * pa
244
+ + (2.20609296 * (10 ** (-7))) * v * v * v * v * delta_t_tr * pa
245
+ + (-7.32469180 * (10 ** (-4))) * delta_t_tr * delta_t_tr * pa
246
+ + (-1.87381964 * (10 ** (-5))) * tdb * delta_t_tr * delta_t_tr * pa
247
+ + (4.80925239 * (10 ** (-6))) * tdb * tdb * delta_t_tr * delta_t_tr * pa
248
+ + (-8.75492040 * (10 ** (-8))) * tdb * tdb * tdb * delta_t_tr * delta_t_tr * pa
249
+ + (2.77862930 * (10 ** (-5))) * v * delta_t_tr * delta_t_tr * pa
250
+ + (-5.06004592 * (10 ** (-6))) * tdb * v * delta_t_tr * delta_t_tr * pa
251
+ + (1.14325367 * (10 ** (-7))) * tdb * tdb * v * delta_t_tr * delta_t_tr * pa
252
+ + (2.53016723 * (10 ** (-6))) * v * v * delta_t_tr * delta_t_tr * pa
253
+ + (-1.72857035 * (10 ** (-8))) * tdb * v * v * delta_t_tr * delta_t_tr * pa
254
+ + (-3.95079398 * (10 ** (-8))) * v * v * v * delta_t_tr * delta_t_tr * pa
255
+ + (-3.59413173 * (10 ** (-7))) * delta_t_tr * delta_t_tr * delta_t_tr * pa
256
+ + (7.04388046 * (10 ** (-7))) * tdb * delta_t_tr * delta_t_tr * delta_t_tr * pa
257
+ + (-1.89309167 * (10 ** (-8))) * tdb * tdb * delta_t_tr * delta_t_tr * delta_t_tr * pa
258
+ + (-4.79768731 * (10 ** (-7))) * v * delta_t_tr * delta_t_tr * delta_t_tr * pa
259
+ + (7.96079978 * (10 ** (-9))) * tdb * v * delta_t_tr * delta_t_tr * delta_t_tr * pa
260
+ + (1.62897058 * (10 ** (-9))) * v * v * delta_t_tr * delta_t_tr * delta_t_tr * pa
261
+ + (3.94367674 * (10 ** (-8))) * delta_t_tr * delta_t_tr * delta_t_tr * delta_t_tr * pa
262
+ + (-1.18566247 * (10 ** (-9)))
263
+ * tdb
264
+ * delta_t_tr
265
+ * delta_t_tr
266
+ * delta_t_tr
267
+ * delta_t_tr
268
+ * pa
269
+ + (3.34678041 * (10 ** (-10))) * v * delta_t_tr * delta_t_tr * delta_t_tr * delta_t_tr * pa
270
+ + (-1.15606447 * (10 ** (-10)))
271
+ * delta_t_tr
272
+ * delta_t_tr
273
+ * delta_t_tr
274
+ * delta_t_tr
275
+ * delta_t_tr
276
+ * pa
277
+ + (-2.80626406) * pa * pa
278
+ + 0.548712484 * tdb * pa * pa
279
+ + (-0.00399428410) * tdb * tdb * pa * pa
280
+ + (-9.54009191 * (10 ** (-4))) * tdb * tdb * tdb * pa * pa
281
+ + (1.93090978 * (10 ** (-5))) * tdb * tdb * tdb * tdb * pa * pa
282
+ + (-0.308806365) * v * pa * pa
283
+ + 0.0116952364 * tdb * v * pa * pa
284
+ + (4.95271903 * (10 ** (-4))) * tdb * tdb * v * pa * pa
285
+ + (-1.90710882 * (10 ** (-5))) * tdb * tdb * tdb * v * pa * pa
286
+ + 0.00210787756 * v * v * pa * pa
287
+ + (-6.98445738 * (10 ** (-4))) * tdb * v * v * pa * pa
288
+ + (2.30109073 * (10 ** (-5))) * tdb * tdb * v * v * pa * pa
289
+ + (4.17856590 * (10 ** (-4))) * v * v * v * pa * pa
290
+ + (-1.27043871 * (10 ** (-5))) * tdb * v * v * v * pa * pa
291
+ + (-3.04620472 * (10 ** (-6))) * v * v * v * v * pa * pa
292
+ + 0.0514507424 * delta_t_tr * pa * pa
293
+ + (-0.00432510997) * tdb * delta_t_tr * pa * pa
294
+ + (8.99281156 * (10 ** (-5))) * tdb * tdb * delta_t_tr * pa * pa
295
+ + (-7.14663943 * (10 ** (-7))) * tdb * tdb * tdb * delta_t_tr * pa * pa
296
+ + (-2.66016305 * (10 ** (-4))) * v * delta_t_tr * pa * pa
297
+ + (2.63789586 * (10 ** (-4))) * tdb * v * delta_t_tr * pa * pa
298
+ + (-7.01199003 * (10 ** (-6))) * tdb * tdb * v * delta_t_tr * pa * pa
299
+ + (-1.06823306 * (10 ** (-4))) * v * v * delta_t_tr * pa * pa
300
+ + (3.61341136 * (10 ** (-6))) * tdb * v * v * delta_t_tr * pa * pa
301
+ + (2.29748967 * (10 ** (-7))) * v * v * v * delta_t_tr * pa * pa
302
+ + (3.04788893 * (10 ** (-4))) * delta_t_tr * delta_t_tr * pa * pa
303
+ + (-6.42070836 * (10 ** (-5))) * tdb * delta_t_tr * delta_t_tr * pa * pa
304
+ + (1.16257971 * (10 ** (-6))) * tdb * tdb * delta_t_tr * delta_t_tr * pa * pa
305
+ + (7.68023384 * (10 ** (-6))) * v * delta_t_tr * delta_t_tr * pa * pa
306
+ + (-5.47446896 * (10 ** (-7))) * tdb * v * delta_t_tr * delta_t_tr * pa * pa
307
+ + (-3.59937910 * (10 ** (-8))) * v * v * delta_t_tr * delta_t_tr * pa * pa
308
+ + (-4.36497725 * (10 ** (-6))) * delta_t_tr * delta_t_tr * delta_t_tr * pa * pa
309
+ + (1.68737969 * (10 ** (-7))) * tdb * delta_t_tr * delta_t_tr * delta_t_tr * pa * pa
310
+ + (2.67489271 * (10 ** (-8))) * v * delta_t_tr * delta_t_tr * delta_t_tr * pa * pa
311
+ + (3.23926897 * (10 ** (-9))) * delta_t_tr * delta_t_tr * delta_t_tr * delta_t_tr * pa * pa
312
+ + (-0.0353874123) * pa * pa * pa
313
+ + (-0.221201190) * tdb * pa * pa * pa
314
+ + 0.0155126038 * tdb * tdb * pa * pa * pa
315
+ + (-2.63917279 * (10 ** (-4))) * tdb * tdb * tdb * pa * pa * pa
316
+ + 0.0453433455 * v * pa * pa * pa
317
+ + (-0.00432943862) * tdb * v * pa * pa * pa
318
+ + (1.45389826 * (10 ** (-4))) * tdb * tdb * v * pa * pa * pa
319
+ + (2.17508610 * (10 ** (-4))) * v * v * pa * pa * pa
320
+ + (-6.66724702 * (10 ** (-5))) * tdb * v * v * pa * pa * pa
321
+ + (3.33217140 * (10 ** (-5))) * v * v * v * pa * pa * pa
322
+ + (-0.00226921615) * delta_t_tr * pa * pa * pa
323
+ + (3.80261982 * (10 ** (-4))) * tdb * delta_t_tr * pa * pa * pa
324
+ + (-5.45314314 * (10 ** (-9))) * tdb * tdb * delta_t_tr * pa * pa * pa
325
+ + (-7.96355448 * (10 ** (-4))) * v * delta_t_tr * pa * pa * pa
326
+ + (2.53458034 * (10 ** (-5))) * tdb * v * delta_t_tr * pa * pa * pa
327
+ + (-6.31223658 * (10 ** (-6))) * v * v * delta_t_tr * pa * pa * pa
328
+ + (3.02122035 * (10 ** (-4))) * delta_t_tr * delta_t_tr * pa * pa * pa
329
+ + (-4.77403547 * (10 ** (-6))) * tdb * delta_t_tr * delta_t_tr * pa * pa * pa
330
+ + (1.73825715 * (10 ** (-6))) * v * delta_t_tr * delta_t_tr * pa * pa * pa
331
+ + (-4.09087898 * (10 ** (-7))) * delta_t_tr * delta_t_tr * delta_t_tr * pa * pa * pa
332
+ + 0.614155345 * pa * pa * pa * pa
333
+ + (-0.0616755931) * tdb * pa * pa * pa * pa
334
+ + 0.00133374846 * tdb * tdb * pa * pa * pa * pa
335
+ + 0.00355375387 * v * pa * pa * pa * pa
336
+ + (-5.13027851 * (10 ** (-4))) * tdb * v * pa * pa * pa * pa
337
+ + (1.02449757 * (10 ** (-4))) * v * v * pa * pa * pa * pa
338
+ + (-0.00148526421) * delta_t_tr * pa * pa * pa * pa
339
+ + (-4.11469183 * (10 ** (-5))) * tdb * delta_t_tr * pa * pa * pa * pa
340
+ + (-6.80434415 * (10 ** (-6))) * v * delta_t_tr * pa * pa * pa * pa
341
+ + (-9.77675906 * (10 ** (-6))) * delta_t_tr * delta_t_tr * pa * pa * pa * pa
342
+ + 0.0882773108 * pa * pa * pa * pa * pa
343
+ + (-0.00301859306) * tdb * pa * pa * pa * pa * pa
344
+ + 0.00104452989 * v * pa * pa * pa * pa * pa
345
+ + (2.47090539 * (10 ** (-4))) * delta_t_tr * pa * pa * pa * pa * pa
346
+ + 0.00148348065 * pa * pa * pa * pa * pa * pa
347
+ )
348
+ if limit:
349
+ bad = (
350
+ (tdb < -50) | (tdb > 50) | (delta_t_tr < -30) | (delta_t_tr > 70) | (v < 0.5) | (v > 17)
351
+ )
352
+ out = np.where(bad, np.nan, out)
353
+ return float(out) if np.ndim(out) == 0 else out
354
+
355
+
356
+ UTCI_CATEGORIES: list[dict[str, Any]] = [
357
+ {"max": -40, "label": "Extreme cold stress", "color": "#08306b"},
358
+ {"max": -27, "label": "Very strong cold stress", "color": "#08519c"},
359
+ {"max": -13, "label": "Strong cold stress", "color": "#2171b5"},
360
+ {"max": 0, "label": "Moderate cold stress", "color": "#6baed6"},
361
+ {"max": 9, "label": "Slight cold stress", "color": "#c6dbef"},
362
+ {"max": 26, "label": "No thermal stress", "color": "#a1d99b"},
363
+ {"max": 32, "label": "Moderate heat stress", "color": "#fdd0a2"},
364
+ {"max": 38, "label": "Strong heat stress", "color": "#fd8d3c"},
365
+ {"max": 46, "label": "Very strong heat stress", "color": "#d94801"},
366
+ {"max": math.inf, "label": "Extreme heat stress", "color": "#7f2704"},
367
+ ]
368
+
369
+
370
+ def _finite(x: Any) -> bool:
371
+ try:
372
+ return x is not None and math.isfinite(float(x))
373
+ except (TypeError, ValueError):
374
+ return False
375
+
376
+
377
+ def _category(table: list[dict[str, Any]], value: Any) -> Optional[dict[str, Any]]:
378
+ if not _finite(value):
379
+ return None
380
+ x = float(value)
381
+ for c in table:
382
+ if x < c["max"]:
383
+ return c
384
+ return table[-1]
385
+
386
+
387
+ def utci_category(value: Optional[float]) -> Optional[dict[str, Any]]:
388
+ """UTCI heat/cold stress class (``{"max", "label", "color"}``), None if not finite."""
389
+ return _category(UTCI_CATEGORIES, value)
390
+
391
+
392
+ def projected_area_factor(elevation_deg: float) -> float:
393
+ """Projected area factor of a standing person for a sun elevation in degrees.
394
+
395
+ Underwood and Ward (1966), as used by SOLWEIG: projected area over the
396
+ effective radiating area.
397
+ """
398
+ b = max(0.0, min(90.0, float(elevation_deg)))
399
+ return 0.308 * math.cos(math.radians(b * (0.998 - (b * b) / 50000)))
400
+
401
+
402
+ def sky_temperature(air_temp: float, horizontal_ir: Optional[float] = None) -> float:
403
+ """Sky temperature (C) from EPW horizontal infrared, or Swinbank (1963) when missing."""
404
+ if _finite(horizontal_ir) and float(horizontal_ir) > 50: # type: ignore[arg-type]
405
+ return float((float(horizontal_ir) / SIGMA) ** 0.25 - 273.15) # type: ignore[arg-type]
406
+ ta = float(air_temp) + 273.15
407
+ return 0.0552 * ta**1.5 - 273.15
408
+
409
+
410
+ def mean_radiant_temperature(
411
+ air_temp: float,
412
+ sky_temp: float,
413
+ svf: float,
414
+ sunlit: float,
415
+ dni: float,
416
+ dhi: float,
417
+ ghi: float,
418
+ elevation_deg: float,
419
+ albedo: float = 0.2,
420
+ absorptivity: float = 0.7,
421
+ ground_temp: Optional[float] = None,
422
+ wall_temp: Optional[float] = None,
423
+ ) -> float:
424
+ """Mean radiant temperature of a standing person outdoors, C.
425
+
426
+ Args:
427
+ air_temp: air temperature, C.
428
+ sky_temp: effective sky temperature, C (see :func:`sky_temperature`).
429
+ svf: sky view factor of the spot (0..1).
430
+ sunlit: 1 if the person is in the sun (a fraction is allowed).
431
+ dni: direct normal irradiance, W/m2.
432
+ dhi: diffuse horizontal irradiance, W/m2.
433
+ ghi: global horizontal irradiance, W/m2.
434
+ elevation_deg: sun elevation, degrees.
435
+ albedo: ground reflectance.
436
+ absorptivity: clothing/skin short-wave absorptivity.
437
+ ground_temp: ground surface temperature, C (None/NaN = air temperature).
438
+ wall_temp: surrounding wall temperature, C (None/NaN = air temperature).
439
+ """
440
+ f_eff = 0.725 # standing person
441
+ hr = 6.012 # W/m2K
442
+ emissivity = 0.95 # long-wave absorptivity
443
+ s = max(0.0, min(1.0, float(svf)))
444
+ t_sky = sky_temp + 273.15
445
+ t_air = air_temp + 273.15
446
+
447
+ if not _finite(ground_temp) and not _finite(wall_temp):
448
+ f_sky = 0.5 * s
449
+ t_lw = (f_sky * t_sky**4 + (1 - f_sky) * t_air**4) ** 0.25 - 273.15
450
+ else:
451
+ # Upper hemisphere (weight 0.5): svf sky at t_sky and (1 - svf) walls.
452
+ # Lower hemisphere (weight 0.5): ground.
453
+ t_wall = (float(wall_temp) if _finite(wall_temp) else air_temp) + 273.15 # type: ignore[arg-type]
454
+ t_gnd = (float(ground_temp) if _finite(ground_temp) else air_temp) + 273.15 # type: ignore[arg-type]
455
+ t_lw = (0.5 * s * t_sky**4 + 0.5 * (1 - s) * t_wall**4 + 0.5 * t_gnd**4) ** 0.25 - 273.15
456
+
457
+ if not (elevation_deg > 0) or not (ghi > 0):
458
+ return float(t_lw)
459
+ f_bes = max(0.0, min(1.0, float(sunlit)))
460
+ ap_over_ad = projected_area_factor(elevation_deg) * f_eff
461
+ e_diff = 0.5 * f_eff * s * (dhi + ghi * albedo)
462
+ e_dir = ap_over_ad * f_bes * dni
463
+ erf = (e_diff + e_dir) * (absorptivity / emissivity)
464
+ return float(t_lw + erf / (f_eff * hr))
465
+
466
+
467
+ PET_CATEGORIES: list[dict[str, Any]] = [
468
+ {"max": 4, "label": "Extreme cold stress", "color": "#08306b"},
469
+ {"max": 8, "label": "Strong cold stress", "color": "#2171b5"},
470
+ {"max": 13, "label": "Moderate cold stress", "color": "#6baed6"},
471
+ {"max": 18, "label": "Slight cold stress", "color": "#c6dbef"},
472
+ {"max": 23, "label": "Comfortable", "color": "#a1d99b"},
473
+ {"max": 29, "label": "Slight heat stress", "color": "#fdd0a2"},
474
+ {"max": 35, "label": "Moderate heat stress", "color": "#fd8d3c"},
475
+ {"max": 41, "label": "Strong heat stress", "color": "#d94801"},
476
+ {"max": math.inf, "label": "Extreme heat stress", "color": "#7f2704"},
477
+ ]
478
+
479
+
480
+ def pet_category(value: Optional[float]) -> Optional[dict[str, Any]]:
481
+ """PET thermal stress class (Matzarakis scale), None if not finite."""
482
+ return _category(PET_CATEGORIES, value)
483
+
484
+
485
+ def _solve3(a: list[list[float]], f: list[float]) -> Optional[tuple[float, float, float]]:
486
+ """Newton step ``-J^-1 f`` for a 3x3 system by Cramer's rule (None if singular)."""
487
+ det = (
488
+ a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
489
+ - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
490
+ + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
491
+ )
492
+ if abs(det) < 1e-12:
493
+ return None
494
+ d0 = (
495
+ -f[0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
496
+ + f[1] * (a[0][1] * a[2][2] - a[0][2] * a[2][1])
497
+ - f[2] * (a[0][1] * a[1][2] - a[0][2] * a[1][1])
498
+ ) / det
499
+ d1 = (
500
+ f[0] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
501
+ - f[1] * (a[0][0] * a[2][2] - a[0][2] * a[2][0])
502
+ + f[2] * (a[0][0] * a[1][2] - a[0][2] * a[1][0])
503
+ ) / det
504
+ d2 = (
505
+ -f[0] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
506
+ + f[1] * (a[0][0] * a[2][1] - a[0][1] * a[2][0])
507
+ - f[2] * (a[0][0] * a[1][1] - a[0][1] * a[1][0])
508
+ ) / det
509
+ return d0, d1, d2
510
+
511
+
512
+ def _facl(clo: float) -> float:
513
+ return max(0.0, min(1.0, (173.51 * clo - 2.36 - 100.76 * clo * clo + 19.28 * clo**3) / 100))
514
+
515
+
516
+ def pet(
517
+ tdb: float,
518
+ tr: float,
519
+ v: Optional[float],
520
+ rh: Optional[float],
521
+ met: float = 80.0,
522
+ clo: float = 0.9,
523
+ ) -> float:
524
+ """Physiological Equivalent Temperature (PET), degrees C.
525
+
526
+ Args:
527
+ tdb: dry-bulb air temperature, C.
528
+ tr: mean radiant temperature, C.
529
+ v: wind speed, m/s (None/NaN -> 0.5; clamped to 0.1..20).
530
+ rh: relative humidity, % (None/NaN -> 50).
531
+ met: activity in W added to basal metabolism (default 80).
532
+ clo: clothing insulation (default 0.9).
533
+
534
+ Returns NaN when ``tdb`` or ``tr`` is not finite.
535
+ """
536
+ if not _finite(tdb) or not _finite(tr):
537
+ return math.nan
538
+ va = max(0.1, min(20.0, float(v) if _finite(v) else 0.5)) # type: ignore[arg-type]
539
+ rh_c = max(0.0, min(100.0, float(rh) if _finite(rh) else 50.0)) # type: ignore[arg-type]
540
+ ta = float(tdb)
541
+ tmrt = float(tr)
542
+ vpa = float(saturation_vapour_pressure(ta)) * (rh_c / 100)
543
+
544
+ age = 35
545
+ ht = 1.8
546
+ mbody = 75.0
547
+ p = 1013.25
548
+ cb = 3640.0
549
+ cair = 1010.0
550
+ emsk = 0.99
551
+ emcl = 0.95
552
+ lvap = 2.42e6
553
+ sigm = 5.67e-8
554
+ eta = 0.0
555
+ tc_set = 36.6
556
+ tsk_set = 34.0
557
+ tbody_set = 0.1 * tsk_set + 0.9 * tc_set
558
+
559
+ adu = 0.203 * mbody**0.425 * ht**0.725
560
+ feff = 0.725
561
+ fcl = 1.0 + 0.31 * clo
562
+ facl = _facl(clo)
563
+ aclo = adu * facl + adu * (fcl - 1.0)
564
+ aeffr = adu * feff
565
+
566
+ hc = 2.67 + 6.5 * va**0.67
567
+ metab_male = (
568
+ 3.45
569
+ * mbody**0.75
570
+ * (1.0 + 0.004 * (30.0 - age) + 0.01 * (ht * 100.0 / mbody ** (1.0 / 3.0) - 43.4))
571
+ )
572
+ he = (met + metab_male) / adu
573
+ h = he * (1.0 - eta)
574
+
575
+ texp = 0.47 * ta + 21.0
576
+ dventpulm = he * 1.44e-6
577
+ eres = cair * (ta - texp) * dventpulm
578
+ vpexp = 6.11 * 10.0 ** (7.45 * texp / (235.0 + texp))
579
+ erel = 0.623 * (lvap / p) * (vpa - vpexp) * dventpulm
580
+ ere = eres + erel
581
+
582
+ rcl = clo / 6.45 if clo > 0.01 else 0.001
583
+ y = (ht - 0.2) / ht
584
+ r2 = adu * (fcl - 1.0 + facl) / (6.28 * ht * y)
585
+ r1 = facl * adu / (6.28 * ht * y)
586
+ di = r2 - r1
587
+ htcl = (6.28 * ht * y * di) / (rcl * math.log(r2 / r1) * aclo) if clo > 0.01 else 1e5
588
+
589
+ lw = 1.67
590
+ he_diff = hc * lw
591
+ fecl = 1.0 / (1.0 + 0.92 * hc * rcl)
592
+ r_ecl = (1.0 / (fcl * hc) + rcl) / (lw * 0.38)
593
+
594
+ def syst3(t: list[float]) -> list[float]:
595
+ tcr, tsk, tcl = t
596
+ sig_skin = max(0.0, tsk_set - tsk)
597
+ sig_core = max(0.0, tcr - tc_set)
598
+ qmblood = min(90.0, (6.3 + 75.0 * sig_core) / (1.0 + 0.5 * sig_skin))
599
+ alpha = 0.1
600
+ tbody = alpha * tsk + (1.0 - alpha) * tcr
601
+ sig_body = max(0.0, tbody - tbody_set)
602
+ qmsw = min(500.0, 304.94e-3 * sig_body * 1000.0)
603
+ esw = (lvap / 1000.0) * (qmsw / 3600.0)
604
+ pvsk = 6.105 * math.exp((17.27 * (tsk + 273.15) - 4717.03) / (237.7 + tsk))
605
+ emax = he_diff * fecl * (pvsk - vpa)
606
+ w = min(1.0, max(0.0, esw / emax)) if emax > 0 else 1.0
607
+ ediff = (1.0 - w) * (pvsk - vpa) / r_ecl
608
+ evap = -(ediff + esw)
609
+ t_mrt4 = (tmrt + 273.15) ** 4
610
+ rbare = aeffr * (1.0 - facl) * emsk * sigm * (t_mrt4 - (tsk + 273.15) ** 4) / adu
611
+ rclo = feff * aclo * emcl * sigm * (t_mrt4 - (tcl + 273.15) ** 4) / adu
612
+ cbare = hc * (ta - tsk) * (1.0 - facl)
613
+ cclo = hc * (ta - tcl) * aclo / adu
614
+ kcs = (qmblood / 3600.0) * cb + 5.28
615
+ return [
616
+ h + ere - kcs * (tcr - tsk),
617
+ rbare + cbare + evap + kcs * (tcr - tsk) - htcl * (tsk - tcl),
618
+ cclo + rclo + htcl * (tsk - tcl),
619
+ ]
620
+
621
+ # Damped Newton-Raphson on the three-node heat balance.
622
+ t = [37.0, 34.0, (ta + tmrt) / 2.0]
623
+ for _ in range(40):
624
+ f = syst3(t)
625
+ if max(abs(f[0]), abs(f[1]), abs(f[2])) < 1e-4:
626
+ break
627
+ jac = [[0.0] * 3 for _ in range(3)]
628
+ eps = 1e-5
629
+ for j in range(3):
630
+ tp = list(t)
631
+ tp[j] += eps
632
+ fp = syst3(tp)
633
+ for i in range(3):
634
+ jac[i][j] = (fp[i] - f[i]) / eps
635
+ step = _solve3(jac, f)
636
+ if step is None:
637
+ break
638
+ d0, d1, d2 = step
639
+ damping = min(1.0, 5.0 / (abs(d0) + abs(d1) + abs(d2) + 1e-6))
640
+ t[0] += damping * d0
641
+ t[1] += damping * d1
642
+ t[2] += damping * d2
643
+
644
+ # Indoor reference state: v = 0.1 m/s, rh = 50 % (vpa = 12 hPa), clo = 0.9, met = 80 W.
645
+ tcr, tsk, tcl = t
646
+ ref_icl = 0.9
647
+ ref_fcl = 1.0 + 0.31 * ref_icl
648
+ ref_facl = _facl(ref_icl)
649
+ ref_aclo = adu * ref_facl + adu * (ref_fcl - 1.0)
650
+ ref_rcl = ref_icl / 6.45
651
+ ref_hc = 2.67 + 6.5 * 0.1**0.67
652
+ ref_he_diff = ref_hc * lw
653
+ ref_fecl = 1.0 / (1.0 + 0.92 * ref_hc * ref_rcl)
654
+ ref_r_ecl = (1.0 / (ref_fcl * ref_hc) + ref_rcl) / (lw * 0.38)
655
+ ref_vpa = 12.0
656
+
657
+ tbody = 0.1 * tsk + 0.9 * tcr
658
+ sig_body = max(0.0, tbody - tbody_set)
659
+ qmsw = min(500.0, 304.94e-3 * sig_body * 1000.0)
660
+ esw = (lvap / 1000.0) * (qmsw / 3600.0)
661
+ pvsk = 6.105 * math.exp((17.27 * (tsk + 273.15) - 4717.03) / (237.7 + tsk))
662
+ emax = ref_he_diff * ref_fecl * (pvsk - ref_vpa)
663
+ w = min(1.0, max(0.0, esw / emax)) if emax > 0 else 1.0
664
+ ediff = (1.0 - w) * (pvsk - ref_vpa) / ref_r_ecl
665
+ evap = -(ediff + esw)
666
+
667
+ def eval_ref(tx: float) -> float:
668
+ texp_ref = 0.47 * tx + 21.0
669
+ eres_ref = cair * (tx - texp_ref) * dventpulm
670
+ vpexp_ref = 6.11 * 10.0 ** (7.45 * texp_ref / (235.0 + texp_ref))
671
+ erel_ref = 0.623 * (lvap / p) * (ref_vpa - vpexp_ref) * dventpulm
672
+ ere_ref = eres_ref + erel_ref
673
+ tx4 = (tx + 273.15) ** 4
674
+ rbare_ref = aeffr * (1.0 - ref_facl) * emsk * sigm * (tx4 - (tsk + 273.15) ** 4) / adu
675
+ rclo_ref = feff * ref_aclo * emcl * sigm * (tx4 - (tcl + 273.15) ** 4) / adu
676
+ cbare_ref = ref_hc * (tx - tsk) * (1.0 - ref_facl)
677
+ cclo_ref = ref_hc * (tx - tcl) * ref_aclo / adu
678
+ return h + ere_ref + rbare_ref + rclo_ref + cbare_ref + cclo_ref + evap
679
+
680
+ lo, hi = -50.0, 70.0
681
+ for _ in range(40):
682
+ mid = (lo + hi) / 2.0
683
+ if eval_ref(mid) < 0:
684
+ lo = mid
685
+ else:
686
+ hi = mid
687
+ return (lo + hi) / 2.0