OpenSimula 0.8.7__tar.gz → 0.8.8__tar.gz
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.
- {opensimula-0.8.7/src/opensimula.egg-info → opensimula-0.8.8}/PKG-INFO +18 -1
- {opensimula-0.8.7 → opensimula-0.8.8}/README.md +17 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/pyproject.toml +1 -1
- opensimula-0.8.8/src/opensimula/Iterative_process.py +50 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Building.py +17 -3
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Construction.py +9 -5
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/File_met.py +80 -58
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/HVAC_SZW_system.py +22 -22
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Space.py +4 -1
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/visual_3D/Environment_3D.py +23 -5
- {opensimula-0.8.7 → opensimula-0.8.8/src/opensimula.egg-info}/PKG-INFO +18 -1
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_Case_600FF.py +3 -3
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_Case_610.py +20 -4
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_Construction.py +35 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_File_met.py +19 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_HVAC_DX_system.py +12 -12
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_HVAC_FAN_COIL_water_system.py +10 -10
- opensimula-0.8.8/test/test_Iterative_process.py +30 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_editor_schema.py +15 -9
- opensimula-0.8.7/src/opensimula/Iterative_process.py +0 -47
- opensimula-0.8.7/test/test_Iterative_process.py +0 -13
- {opensimula-0.8.7 → opensimula-0.8.8}/LICENSE +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/setup.cfg +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Child.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Component.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Message.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Parameter_container.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Parameters.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Project.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Simulation.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/Variable.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/__init__.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Building_surface.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Calculator.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Chiller_heat_pump.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/DX_unit.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Day_schedule.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Fan.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/File_data.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Frame.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Glazing.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/HVAC_DX_system.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/HVAC_MZW_system.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/HVAC_perfect_system.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/HVAC_water_system.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Material.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Opening.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Opening_type.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Pump.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Solar_surface.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Space_type.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Surface.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Test_component.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Water_coil.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Week_schedule.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/Year_schedule.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/components/__init__.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/editor/__init__.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/editor/schema.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/editor/static/editor.css +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/editor/static/editor.js +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/editor/validation.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/editor/widget.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula/visual_3D/Polygon_3D.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula.egg-info/SOURCES.txt +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula.egg-info/dependency_links.txt +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula.egg-info/requires.txt +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/src/opensimula.egg-info/top_level.txt +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_Calculator.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_File_data.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_HVAC_DX_Equipment.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_HVAC_water_system_process.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_Project.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_Schedules.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_editor_widget.py +0 -0
- {opensimula-0.8.7 → opensimula-0.8.8}/test/test_underground_repeated_simulation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensimula
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.8
|
|
4
4
|
Summary: Component-based time simulation environment in Python
|
|
5
5
|
Author-email: "Juan F. Coronel" <jfc@us.es>
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -24,8 +24,10 @@ Dynamic: license-file
|
|
|
24
24
|
|
|
25
25
|
# OpenSimula
|
|
26
26
|
|
|
27
|
+
[](https://github.com/jfCoronel/OpenSimula/actions/workflows/tests.yml)
|
|
27
28
|
[](https://pypi.org/project/opensimula/)
|
|
28
29
|
[](https://pypi.org/project/opensimula/)
|
|
30
|
+
[](https://doi.org/10.5281/zenodo.23210985)
|
|
29
31
|
[](LICENSE)
|
|
30
32
|
|
|
31
33
|
**OpenSimula** is a component-based time simulation environment in Python, focused on the
|
|
@@ -162,6 +164,21 @@ reference, is at [jfcoronel.github.io/OpenSimula](https://jfcoronel.github.io/Op
|
|
|
162
164
|
Bugs and feature requests are tracked on
|
|
163
165
|
[GitHub Issues](https://github.com/jfCoronel/OpenSimula/issues).
|
|
164
166
|
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Contributions are welcome: bug reports, fixes, new components, validation cases and examples.
|
|
170
|
+
See [CONTRIBUTING.md](https://github.com/jfCoronel/OpenSimula/blob/main/CONTRIBUTING.md) for how to set up a development environment, run the
|
|
171
|
+
tests and submit a pull request.
|
|
172
|
+
|
|
173
|
+
## Citing OpenSimula
|
|
174
|
+
|
|
175
|
+
If you use OpenSimula in your research, please cite it with its DOI,
|
|
176
|
+
[10.5281/zenodo.23210985](https://doi.org/10.5281/zenodo.23210985), which always resolves to
|
|
177
|
+
the latest version (each version also has its own DOI, listed on
|
|
178
|
+
[Zenodo](https://doi.org/10.5281/zenodo.23210985)). The full metadata is in
|
|
179
|
+
[CITATION.cff](https://github.com/jfCoronel/OpenSimula/blob/main/CITATION.cff) (GitHub's "Cite this repository" button generates APA and BibTeX
|
|
180
|
+
entries from it).
|
|
181
|
+
|
|
165
182
|
## Main Developers
|
|
166
183
|
|
|
167
184
|
* [Juan F. Coronel](http://jfc.us.es), Universidad de Sevilla
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# OpenSimula
|
|
4
4
|
|
|
5
|
+
[](https://github.com/jfCoronel/OpenSimula/actions/workflows/tests.yml)
|
|
5
6
|
[](https://pypi.org/project/opensimula/)
|
|
6
7
|
[](https://pypi.org/project/opensimula/)
|
|
8
|
+
[](https://doi.org/10.5281/zenodo.23210985)
|
|
7
9
|
[](LICENSE)
|
|
8
10
|
|
|
9
11
|
**OpenSimula** is a component-based time simulation environment in Python, focused on the
|
|
@@ -140,6 +142,21 @@ reference, is at [jfcoronel.github.io/OpenSimula](https://jfcoronel.github.io/Op
|
|
|
140
142
|
Bugs and feature requests are tracked on
|
|
141
143
|
[GitHub Issues](https://github.com/jfCoronel/OpenSimula/issues).
|
|
142
144
|
|
|
145
|
+
## Contributing
|
|
146
|
+
|
|
147
|
+
Contributions are welcome: bug reports, fixes, new components, validation cases and examples.
|
|
148
|
+
See [CONTRIBUTING.md](https://github.com/jfCoronel/OpenSimula/blob/main/CONTRIBUTING.md) for how to set up a development environment, run the
|
|
149
|
+
tests and submit a pull request.
|
|
150
|
+
|
|
151
|
+
## Citing OpenSimula
|
|
152
|
+
|
|
153
|
+
If you use OpenSimula in your research, please cite it with its DOI,
|
|
154
|
+
[10.5281/zenodo.23210985](https://doi.org/10.5281/zenodo.23210985), which always resolves to
|
|
155
|
+
the latest version (each version also has its own DOI, listed on
|
|
156
|
+
[Zenodo](https://doi.org/10.5281/zenodo.23210985)). The full metadata is in
|
|
157
|
+
[CITATION.cff](https://github.com/jfCoronel/OpenSimula/blob/main/CITATION.cff) (GitHub's "Cite this repository" button generates APA and BibTeX
|
|
158
|
+
entries from it).
|
|
159
|
+
|
|
143
160
|
## Main Developers
|
|
144
161
|
|
|
145
162
|
* [Juan F. Coronel](http://jfc.us.es), Universidad de Sevilla
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
class Iterative_process:
|
|
4
|
+
def __init__(self, x_0, tol = 1e-5, lambda_min=0.1, lambda_max=1.0, n_ini_relax=2,rel_vel = 0.9):
|
|
5
|
+
"""
|
|
6
|
+
Iterative process using the fixed-point method with adaptive relaxation coefficient
|
|
7
|
+
|
|
8
|
+
Parameters:
|
|
9
|
+
x_0: Initial value
|
|
10
|
+
tol: convergence tolerance
|
|
11
|
+
lambda_min, lambda_max: relaxation coefficient range
|
|
12
|
+
n_ini_relax: Number of iterations in which adaptive relaxation is initiated, must be greater than 1
|
|
13
|
+
"""
|
|
14
|
+
self.x = [x_0]
|
|
15
|
+
self.tol = tol
|
|
16
|
+
self.lambda_min = lambda_min
|
|
17
|
+
self.lambda_max = lambda_max
|
|
18
|
+
self.lambda_i = lambda_max
|
|
19
|
+
self.n_ini_relax = n_ini_relax
|
|
20
|
+
self.rel_vel = rel_vel
|
|
21
|
+
self.residual = None # |proposed_x - x| of the last iteration
|
|
22
|
+
|
|
23
|
+
def converged(self):
|
|
24
|
+
# Fixed-point residual, not the relaxed step: with relaxation lambda the step is only
|
|
25
|
+
# lambda * residual, so testing the step stopped with residuals up to tol / lambda_min
|
|
26
|
+
# (10 x tol), e.g. zones far from their moisture balance in steady state
|
|
27
|
+
return self.residual is not None and self.residual < self.tol
|
|
28
|
+
|
|
29
|
+
def estimate_next_x(self, proposed_x):
|
|
30
|
+
residual = proposed_x - self.x[-1]
|
|
31
|
+
x_next = (1 - self.lambda_i) * self.x[-1] + self.lambda_i * proposed_x
|
|
32
|
+
|
|
33
|
+
# Adaptive relaxation: reduce lambda when the iteration oscillates (the residual
|
|
34
|
+
# changes sign) or does not contract (the residual grows), and recover it while it
|
|
35
|
+
# contracts; reducing it unconditionally made well-behaved iterations crawl
|
|
36
|
+
# (lambda_min after ~10 iterations)
|
|
37
|
+
if len(self.x) > self.n_ini_relax and self.residual is not None:
|
|
38
|
+
if residual * self._last_residual < 0 or abs(residual) > self.residual:
|
|
39
|
+
self.lambda_i = max(self.lambda_min, self.lambda_i * self.rel_vel)
|
|
40
|
+
else:
|
|
41
|
+
self.lambda_i = min(self.lambda_max, self.lambda_i / self.rel_vel)
|
|
42
|
+
self._last_residual = residual
|
|
43
|
+
self.residual = abs(residual)
|
|
44
|
+
|
|
45
|
+
self.x.append(x_next)
|
|
46
|
+
return x_next
|
|
47
|
+
|
|
48
|
+
def set_next_x(self, next_x):
|
|
49
|
+
self.x.append(next_x)
|
|
50
|
+
|
|
@@ -85,7 +85,7 @@ class Building(Component):
|
|
|
85
85
|
SWR_matrix = (SWR_matrix - np.matmul(self.ff_matrix, rho_matrix)
|
|
86
86
|
- np.matmul(self.ff_matrix, np.matmul(tau_matrix, self.B_matrix)))
|
|
87
87
|
|
|
88
|
-
SWR_matrix =
|
|
88
|
+
SWR_matrix = _radiosity_inverse_(SWR_matrix)
|
|
89
89
|
aux_matrix = np.matmul(area_matrix, np.matmul(alpha_matrix, SWR_matrix))
|
|
90
90
|
self.SWDIF_matrix = np.matmul(aux_matrix, np.matmul(self.ff_matrix, tau_matrix)) # SW Solar Diffuse
|
|
91
91
|
|
|
@@ -119,7 +119,7 @@ class Building(Component):
|
|
|
119
119
|
LWR_matrix = (LWR_matrix - np.matmul(self.ff_matrix, rho_matrix)
|
|
120
120
|
- np.matmul(self.ff_matrix, np.matmul(tau_matrix, self.B_matrix)))
|
|
121
121
|
|
|
122
|
-
LWR_matrix =
|
|
122
|
+
LWR_matrix = _radiosity_inverse_(LWR_matrix)
|
|
123
123
|
aux_matrix = np.matmul(area_matrix, np.matmul(alpha_matrix,LWR_matrix))
|
|
124
124
|
|
|
125
125
|
ig_dist_matrix = np.zeros((self._n_surfaces, self._n_spaces))
|
|
@@ -508,4 +508,18 @@ class Building(Component):
|
|
|
508
508
|
|
|
509
509
|
# _______________ post_iteration _______________
|
|
510
510
|
def post_iteration(self, time_index, date, daylight_saving, converged):
|
|
511
|
-
super().post_iteration(time_index, date, daylight_saving, converged)
|
|
511
|
+
super().post_iteration(time_index, date, daylight_saving, converged)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _radiosity_inverse_(matrix):
|
|
515
|
+
"""Inverse of a radiosity matrix (I - F·rho - F·tau·B).
|
|
516
|
+
|
|
517
|
+
In a closed enclosure whose surfaces neither absorb nor transmit (rho = 1, e.g. surfaces
|
|
518
|
+
with solar absorptance or emissivity 0, as in the ASHRAE 140 ground-coupling cases) the
|
|
519
|
+
matrix is singular, since the rows of F sum to 1. Nothing is absorbed then, so any
|
|
520
|
+
generalised inverse gives the same (zero) absorbed radiation: use the pseudo-inverse.
|
|
521
|
+
"""
|
|
522
|
+
try:
|
|
523
|
+
return np.linalg.inv(matrix)
|
|
524
|
+
except np.linalg.LinAlgError:
|
|
525
|
+
return np.linalg.pinv(matrix)
|
|
@@ -281,7 +281,10 @@ class Construction(Component):
|
|
|
281
281
|
ox = []
|
|
282
282
|
oy = []
|
|
283
283
|
oz = []
|
|
284
|
-
|
|
284
|
+
# n_coef terms of the response series give n_coef valid a, b, c coefficients, as many
|
|
285
|
+
# as d has: with one fewer, the steady state (sum(a)/sum(d) = U) failed for light
|
|
286
|
+
# constructions with few roots, e.g. 6.59 instead of 5.05 W/m²K for a thin panel
|
|
287
|
+
for i in range(1, n_coef + 1):
|
|
285
288
|
sum_exp_x = 0
|
|
286
289
|
sum_exp_y = 0
|
|
287
290
|
sum_exp_z = 0
|
|
@@ -304,10 +307,11 @@ class Construction(Component):
|
|
|
304
307
|
if math.fabs(d[i]) < min_coef:
|
|
305
308
|
d = d[0: i+1]
|
|
306
309
|
break
|
|
307
|
-
#
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
310
|
+
# Only the first n_coef terms of the convolutions are valid (the response series
|
|
311
|
+
# above are truncated after n_coef terms)
|
|
312
|
+
a = np.resize(a, n_coef)
|
|
313
|
+
b = np.resize(b, n_coef)
|
|
314
|
+
c = np.resize(c, n_coef)
|
|
311
315
|
# Cut a, b, c coefficients
|
|
312
316
|
for i in range(len(a)):
|
|
313
317
|
if (
|
|
@@ -68,6 +68,7 @@ class File_met(Component):
|
|
|
68
68
|
self.pressure = np.zeros(8760)
|
|
69
69
|
self.total_cloud_cover = np.zeros(8760)
|
|
70
70
|
self.opaque_cloud_cover = np.zeros(8760)
|
|
71
|
+
self._delta_t = 3600 # time step, set by pre_simulation
|
|
71
72
|
|
|
72
73
|
def check(self):
|
|
73
74
|
errors = super().check()
|
|
@@ -219,6 +220,7 @@ class File_met(Component):
|
|
|
219
220
|
|
|
220
221
|
def pre_simulation(self, n_time_steps, delta_t):
|
|
221
222
|
super().pre_simulation(n_time_steps, delta_t)
|
|
223
|
+
self._delta_t = delta_t
|
|
222
224
|
|
|
223
225
|
def pre_iteration(self, time_index, date, daylight_saving):
|
|
224
226
|
super().pre_iteration(time_index, date, daylight_saving)
|
|
@@ -262,6 +264,8 @@ class File_met(Component):
|
|
|
262
264
|
time_index, i, j, f)
|
|
263
265
|
self._interpolate("opaque_cloud_cover", self.opaque_cloud_cover,
|
|
264
266
|
time_index, i, j, f)
|
|
267
|
+
if self._sub_hourly_integrated_radiation_():
|
|
268
|
+
self._set_sub_hourly_radiation_(time_index, date, alt)
|
|
265
269
|
# Corregir la directa si el sol no ha salido, y con alturas solares pequeñas
|
|
266
270
|
if (alt < 3 and self.variable("sol_direct").values[time_index] > 0):
|
|
267
271
|
self.variable(
|
|
@@ -282,6 +286,37 @@ class File_met(Component):
|
|
|
282
286
|
self.variable("wet_bulb_temp").values[time_index] = sicro.GetTWetBulbFromRelHum(
|
|
283
287
|
T, HR, p)
|
|
284
288
|
|
|
289
|
+
def _sub_hourly_integrated_radiation_(self):
|
|
290
|
+
"""TMY3, TMY2 and WYEC2 radiation values are totals over the hour before each record."""
|
|
291
|
+
return (
|
|
292
|
+
self.parameter("file_type").value in ("TMY3", "TMY2", "WYEC2")
|
|
293
|
+
and self._delta_t < 3600
|
|
294
|
+
and 3600 % self._delta_t == 0
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _set_sub_hourly_radiation_(self, time_index, date, alt):
|
|
298
|
+
"""Radiation of a sub-hourly time step that keeps the hour total of the file.
|
|
299
|
+
|
|
300
|
+
Interpolating linearly between hour centres would spread a quarter of each hour's
|
|
301
|
+
radiation over the neighbouring hours. Instead, the diffuse radiation is held through
|
|
302
|
+
the hour and the horizontal direct radiation follows the sine of the solar altitude,
|
|
303
|
+
scaled so that its mean over the time steps of the hour is the file value.
|
|
304
|
+
"""
|
|
305
|
+
hour_start = date.replace(minute=0, second=0, microsecond=0)
|
|
306
|
+
k = int((hour_start - dt.datetime(date.year, 1, 1)).total_seconds() // 3600) % 8760
|
|
307
|
+
n = round(3600 / self._delta_t)
|
|
308
|
+
sin_alt = [
|
|
309
|
+
max(math.sin(math.radians(self.sunpos(hour_start + dt.timedelta(seconds=(m + 0.5) * self._delta_t))[1])), 0)
|
|
310
|
+
for m in range(n)
|
|
311
|
+
]
|
|
312
|
+
mean_sin_alt = sum(sin_alt) / n
|
|
313
|
+
if mean_sin_alt > 0:
|
|
314
|
+
direct = self.sol_direct[k] * max(math.sin(math.radians(alt)), 0) / mean_sin_alt
|
|
315
|
+
else:
|
|
316
|
+
direct = 0
|
|
317
|
+
self.variable("sol_direct").values[time_index] = direct
|
|
318
|
+
self.variable("sol_diffuse").values[time_index] = self.sol_diffuse[k]
|
|
319
|
+
|
|
285
320
|
def _interpolate(self, variable, array, time_i, i, j, f):
|
|
286
321
|
self.variable(
|
|
287
322
|
variable).values[time_i] = array[i] * (1 - f) + array[j] * f
|
|
@@ -377,66 +412,53 @@ class File_met(Component):
|
|
|
377
412
|
return 0
|
|
378
413
|
model = self.parameter("tilted_diffuse_model").value
|
|
379
414
|
beta = math.radians(surf_altitude)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
epsilon = ((sol_direct/math.sin(sol_altitude)+sol_diffuse) /
|
|
425
|
-
sol_diffuse + 1.041 * sol_zenit**3)/(1 + 1.041*sol_zenit**3)
|
|
426
|
-
m = 1 / math.cos(sol_zenit)
|
|
427
|
-
Delta = m*sol_diffuse/1353
|
|
428
|
-
f = self._perez_coef(epsilon)
|
|
429
|
-
F_2 = f[3]+f[4]*Delta+f[5]*sol_zenit
|
|
430
|
-
a = math.cos(theta)
|
|
431
|
-
b = max(0.087156, math.cos(sol_zenit))
|
|
432
|
-
F_1 = max(0, f[0]+f[1]*Delta+f[2]*sol_zenit)
|
|
433
|
-
E_dif = sol_diffuse * \
|
|
434
|
-
((1-F_1)*(1+math.cos(zenit))/2 +
|
|
435
|
-
F_1 * a/b + F_2 * math.sin(zenit))
|
|
436
|
-
return E_dif
|
|
415
|
+
isotropic = sol_diffuse * (1 + math.sin(beta)) / 2
|
|
416
|
+
if model == "ISOTROPIC": # Liu-Jordan model
|
|
417
|
+
return isotropic
|
|
418
|
+
# Anisotropic models need the sun above the horizon (sol_altitude is NaN below 3°)
|
|
419
|
+
alt_sol = self.variable("sol_altitude").values[time_index]
|
|
420
|
+
if math.isnan(alt_sol):
|
|
421
|
+
return isotropic
|
|
422
|
+
sol_altitude = math.radians(alt_sol)
|
|
423
|
+
sol_direct = self.variable("sol_direct").values[time_index]
|
|
424
|
+
# Circumsolar term only on surfaces that see the sun (cos_theta clipped at 0), but the
|
|
425
|
+
# sky part keeps its anisotropy weighting also on surfaces facing away from the sun:
|
|
426
|
+
# using the isotropic model for them overestimated their diffuse radiation
|
|
427
|
+
cos_theta = max(0.0, self._cos_incidence(time_index, surf_azimuth, surf_altitude))
|
|
428
|
+
if model in ("HAY-DAVIES", "REINDL"):
|
|
429
|
+
A = sol_direct / math.sin(sol_altitude) / 1353 # anisotropy index
|
|
430
|
+
R_b = cos_theta / math.sin(sol_altitude)
|
|
431
|
+
f_horizon = 1
|
|
432
|
+
if model == "REINDL":
|
|
433
|
+
zenit = math.pi / 2 - beta
|
|
434
|
+
f_horizon = 1 + math.sqrt(sol_direct / (sol_direct + sol_diffuse)) * (math.sin(zenit / 2)) ** 3
|
|
435
|
+
return sol_diffuse * A * R_b + sol_diffuse * (1 - A) * (1 + math.sin(beta)) / 2 * f_horizon
|
|
436
|
+
# PEREZ
|
|
437
|
+
zenit = math.pi / 2 - beta
|
|
438
|
+
sol_zenit = math.pi / 2 - sol_altitude
|
|
439
|
+
epsilon = ((sol_direct / math.sin(sol_altitude) + sol_diffuse) / sol_diffuse
|
|
440
|
+
+ 1.041 * sol_zenit**3) / (1 + 1.041 * sol_zenit**3)
|
|
441
|
+
m = 1 / math.cos(sol_zenit)
|
|
442
|
+
Delta = m * sol_diffuse / 1353
|
|
443
|
+
f = self._perez_coef(epsilon)
|
|
444
|
+
F_1 = max(0, f[0] + f[1] * Delta + f[2] * sol_zenit)
|
|
445
|
+
F_2 = f[3] + f[4] * Delta + f[5] * sol_zenit
|
|
446
|
+
b = max(0.087156, math.cos(sol_zenit))
|
|
447
|
+
return sol_diffuse * ((1 - F_1) * (1 + math.cos(zenit)) / 2 + F_1 * cos_theta / b + F_2 * math.sin(zenit))
|
|
448
|
+
|
|
449
|
+
def _cos_incidence(self, time_index, surf_azimuth, surf_altitude):
|
|
450
|
+
"""Cosine of the angle between the sun and the surface exterior normal (negative when
|
|
451
|
+
the sun is behind the surface)."""
|
|
452
|
+
azi_sur = math.radians(surf_azimuth)
|
|
453
|
+
alt_sur = math.radians(surf_altitude)
|
|
454
|
+
azi_sol = math.radians(self.variable("sol_azimuth").values[time_index])
|
|
455
|
+
alt_sol = math.radians(self.variable("sol_altitude").values[time_index])
|
|
456
|
+
return (math.cos(azi_sol) * math.cos(alt_sol) * math.cos(azi_sur) * math.cos(alt_sur)
|
|
457
|
+
+ math.sin(azi_sol) * math.cos(alt_sol) * math.sin(azi_sur) * math.cos(alt_sur)
|
|
458
|
+
+ math.sin(alt_sol) * math.sin(alt_sur))
|
|
437
459
|
|
|
438
460
|
def _perez_coef(self, epsilon):
|
|
439
|
-
if epsilon
|
|
461
|
+
if epsilon <= 1.065: # overcast bin, also epsilon = 1 (no beam radiation)
|
|
440
462
|
return [-0.0083117, 0.5877285, -0.0620636, -0.0596012, 0.0721249, -0.0220216]
|
|
441
463
|
elif epsilon > 1.065 and epsilon <= 1.23:
|
|
442
464
|
return [0.1299457, 0.6825954, -0.1513752, -0.0189325, 0.0659650, -0.0288748]
|
|
@@ -383,14 +383,14 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
|
|
|
383
383
|
self.M_w = 0
|
|
384
384
|
self.w_CA = self.w_MA
|
|
385
385
|
return
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
386
|
+
# Coil capacity with the water temperature of either source
|
|
387
|
+
capacity, self.epsilon = self.h_coil.get_heating_capacity(
|
|
388
|
+
self.T_MA,
|
|
389
|
+
self.T_MAwb,
|
|
390
|
+
heating_Tw,
|
|
391
|
+
self.air_flow,
|
|
392
|
+
self.heating_water_flow,
|
|
393
|
+
)
|
|
394
394
|
self.M_w = 0 # No latent load in heating
|
|
395
395
|
self.w_CA = self.w_MA
|
|
396
396
|
# Q_required is only the coil capacity
|
|
@@ -435,20 +435,20 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
|
|
|
435
435
|
self.M_w = 0
|
|
436
436
|
self.w_CA = self.w_MA
|
|
437
437
|
return
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
438
|
+
# Coil capacity with the water temperature of either source
|
|
439
|
+
(
|
|
440
|
+
capacity_sen,
|
|
441
|
+
capacity_lat,
|
|
442
|
+
self.T_ADP,
|
|
443
|
+
self.epsilon,
|
|
444
|
+
self.epsilon_adp,
|
|
445
|
+
) = self.c_coil.get_cooling_capacity(
|
|
446
|
+
self.T_MA,
|
|
447
|
+
self.T_MAwb,
|
|
448
|
+
cooling_Tw,
|
|
449
|
+
self.air_flow,
|
|
450
|
+
self.cooling_water_flow,
|
|
451
|
+
)
|
|
452
452
|
|
|
453
453
|
Q_required = -self.Q_required
|
|
454
454
|
# Q_required is only the coil capacity
|
|
@@ -30,7 +30,7 @@ class Space(Component):
|
|
|
30
30
|
self.add_parameter(Parameter_float("volume", 1, "m³", min=0.0))
|
|
31
31
|
self.add_parameter(Parameter_float("furniture_weight", 10, "kg/m²", min=0.0))
|
|
32
32
|
self.add_parameter(Parameter_float("convergence_DT", 0.01, "°C", min=0.0))
|
|
33
|
-
self.add_parameter(Parameter_float("convergence_Dw", 0.
|
|
33
|
+
self.add_parameter(Parameter_float("convergence_Dw", 0.01, "g/kg", min=0.0))
|
|
34
34
|
|
|
35
35
|
# Variables
|
|
36
36
|
self.add_variable(Variable("temperature", unit="°C"))
|
|
@@ -504,6 +504,9 @@ class Space(Component):
|
|
|
504
504
|
self._calculate_heat_fluxes(time_index)
|
|
505
505
|
|
|
506
506
|
def _calculate_heat_fluxes(self, time_i):
|
|
507
|
+
# The uncontrolled systems may have changed since the last thermal solution (e.g. a VAV
|
|
508
|
+
# system replaces its maximum air flow by a controlled flow): report the current ones
|
|
509
|
+
self._acumulate_u_systems()
|
|
507
510
|
V_inf = self.variable("infiltration_flow").values[time_i]
|
|
508
511
|
T_ext = self._file_met.variable("temperature").values[time_i]
|
|
509
512
|
w_ext = self._file_met.variable("abs_humidity").values[time_i]
|
|
@@ -2,6 +2,7 @@ import time
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
import math
|
|
4
4
|
from scipy.interpolate import RegularGridInterpolator
|
|
5
|
+
from scipy.ndimage import distance_transform_edt
|
|
5
6
|
import matplotlib.pyplot as plt
|
|
6
7
|
from opensimula.visual_3D.Polygon_3D import Polygon_3D
|
|
7
8
|
|
|
@@ -424,15 +425,31 @@ class Environment_3D:
|
|
|
424
425
|
k = k + 1
|
|
425
426
|
j = j + 1
|
|
426
427
|
|
|
428
|
+
# Interpolation tables. Where the sun is behind a polygon its sunny fraction drops to 0,
|
|
429
|
+
# but there the direct radiation is already 0, so those cells take the value of the
|
|
430
|
+
# nearest cell with the sun in front: otherwise the interpolation between both sides
|
|
431
|
+
# of that step gives wrong fractions near grazing sun (e.g. 0.84-1.11 for an unshaded
|
|
432
|
+
# roof with cubic splines). Linear interpolation keeps the values within [0, 1], and
|
|
433
|
+
# the azimuth grid is closed at 360° so that 350°-360° is interpolated, not extrapolated.
|
|
434
|
+
azimuth_grid = np.append(self.shadow_azimuth_grid, 360)
|
|
427
435
|
self.sunny_interpolation_functions = []
|
|
428
|
-
for i in
|
|
436
|
+
for i, polygon in enumerate(sunny_polygons):
|
|
437
|
+
table = self.sunny_fraction_tables[i].copy()
|
|
438
|
+
behind = np.array(
|
|
439
|
+
[[polygon.get_angle_with_normal(azi, alt) >= math.pi / 2
|
|
440
|
+
for alt in self.shadow_altitude_grid] for azi in self.shadow_azimuth_grid]
|
|
441
|
+
)
|
|
442
|
+
if behind.any() and not behind.all():
|
|
443
|
+
nearest = distance_transform_edt(behind, return_distances=False, return_indices=True)
|
|
444
|
+
table = table[nearest[0], nearest[1]]
|
|
445
|
+
table = np.vstack([table, table[0]])
|
|
429
446
|
self.sunny_interpolation_functions.append(
|
|
430
447
|
RegularGridInterpolator(
|
|
431
|
-
(
|
|
432
|
-
|
|
448
|
+
(azimuth_grid, self.shadow_altitude_grid),
|
|
449
|
+
table,
|
|
433
450
|
bounds_error=False,
|
|
434
451
|
fill_value=None,
|
|
435
|
-
method="
|
|
452
|
+
method="linear",
|
|
436
453
|
)
|
|
437
454
|
)
|
|
438
455
|
|
|
@@ -471,7 +488,8 @@ class Environment_3D:
|
|
|
471
488
|
if self.solar_tables_calculated == True:
|
|
472
489
|
if azi < 0:
|
|
473
490
|
azi = azi + 360
|
|
474
|
-
|
|
491
|
+
# clip: linear extrapolation beyond the ±85° altitude grid could leave [0, 1]
|
|
492
|
+
return float(np.clip(self.sunny_interpolation_functions[sunny_i]((azi, alt)), 0, 1))
|
|
475
493
|
else:
|
|
476
494
|
return 1
|
|
477
495
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensimula
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.8
|
|
4
4
|
Summary: Component-based time simulation environment in Python
|
|
5
5
|
Author-email: "Juan F. Coronel" <jfc@us.es>
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -24,8 +24,10 @@ Dynamic: license-file
|
|
|
24
24
|
|
|
25
25
|
# OpenSimula
|
|
26
26
|
|
|
27
|
+
[](https://github.com/jfCoronel/OpenSimula/actions/workflows/tests.yml)
|
|
27
28
|
[](https://pypi.org/project/opensimula/)
|
|
28
29
|
[](https://pypi.org/project/opensimula/)
|
|
30
|
+
[](https://doi.org/10.5281/zenodo.23210985)
|
|
29
31
|
[](LICENSE)
|
|
30
32
|
|
|
31
33
|
**OpenSimula** is a component-based time simulation environment in Python, focused on the
|
|
@@ -162,6 +164,21 @@ reference, is at [jfcoronel.github.io/OpenSimula](https://jfcoronel.github.io/Op
|
|
|
162
164
|
Bugs and feature requests are tracked on
|
|
163
165
|
[GitHub Issues](https://github.com/jfCoronel/OpenSimula/issues).
|
|
164
166
|
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Contributions are welcome: bug reports, fixes, new components, validation cases and examples.
|
|
170
|
+
See [CONTRIBUTING.md](https://github.com/jfCoronel/OpenSimula/blob/main/CONTRIBUTING.md) for how to set up a development environment, run the
|
|
171
|
+
tests and submit a pull request.
|
|
172
|
+
|
|
173
|
+
## Citing OpenSimula
|
|
174
|
+
|
|
175
|
+
If you use OpenSimula in your research, please cite it with its DOI,
|
|
176
|
+
[10.5281/zenodo.23210985](https://doi.org/10.5281/zenodo.23210985), which always resolves to
|
|
177
|
+
the latest version (each version also has its own DOI, listed on
|
|
178
|
+
[Zenodo](https://doi.org/10.5281/zenodo.23210985)). The full metadata is in
|
|
179
|
+
[CITATION.cff](https://github.com/jfCoronel/OpenSimula/blob/main/CITATION.cff) (GitHub's "Cite this repository" button generates APA and BibTeX
|
|
180
|
+
entries from it).
|
|
181
|
+
|
|
165
182
|
## Main Developers
|
|
166
183
|
|
|
167
184
|
* [Juan F. Coronel](http://jfc.us.es), Universidad de Sevilla
|
|
@@ -319,6 +319,6 @@ def test_ff_temperature():
|
|
|
319
319
|
|
|
320
320
|
T = pro.component("spaces_1").variable("temperature").values
|
|
321
321
|
|
|
322
|
-
assert T.mean() == pytest.approx(25.
|
|
323
|
-
assert T.max() == pytest.approx(65.
|
|
324
|
-
assert T.min() == pytest.approx(-13.
|
|
322
|
+
assert T.mean() == pytest.approx(25.07681213119194)
|
|
323
|
+
assert T.max() == pytest.approx(65.66725801535114)
|
|
324
|
+
assert T.min() == pytest.approx(-13.387879316775047)
|
|
@@ -349,7 +349,23 @@ def test_HVAC_perfect_system():
|
|
|
349
349
|
peak_heating = load.max()/1000
|
|
350
350
|
peak_cooling = -load.min()/1000
|
|
351
351
|
|
|
352
|
-
assert annual_heating == pytest.approx(4.
|
|
353
|
-
assert annual_cooling == pytest.approx(4.
|
|
354
|
-
assert peak_heating == pytest.approx(3.
|
|
355
|
-
assert peak_cooling == pytest.approx(5.
|
|
352
|
+
assert annual_heating == pytest.approx(4.635639661707876)
|
|
353
|
+
assert annual_cooling == pytest.approx(4.135999834242331)
|
|
354
|
+
assert peak_heating == pytest.approx(3.2579149609779443)
|
|
355
|
+
assert peak_cooling == pytest.approx(5.451313222438749)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_interpolated_sunny_fraction():
|
|
359
|
+
# Unshaded surfaces must get a sunny fraction of exactly 1 with INTERPOLATION, also with
|
|
360
|
+
# low sun, and every fraction must stay within [0, 1] (cubic interpolation overshot both)
|
|
361
|
+
sim = osim.Simulation()
|
|
362
|
+
pro = sim.new_project("pro")
|
|
363
|
+
pro.read_dict({**case610_dict, "shadow_calculation": "INTERPOLATION", "n_time_steps": 24})
|
|
364
|
+
pro.simulate()
|
|
365
|
+
env = pro.env_3D
|
|
366
|
+
roof = env.get_sunny_index("roof_wall")
|
|
367
|
+
window = env.get_sunny_index("south_window_1")
|
|
368
|
+
for azi in range(-180, 181, 7):
|
|
369
|
+
for alt in [1, 3, 5, 8, 12, 20, 40, 70, 88]:
|
|
370
|
+
assert env.get_direct_interpolated_sunny_fraction(roof, azi, alt) == pytest.approx(1)
|
|
371
|
+
assert 0 <= env.get_direct_interpolated_sunny_fraction(window, azi, alt) <= 1
|
|
@@ -74,6 +74,29 @@ project_dic = {
|
|
|
74
74
|
],
|
|
75
75
|
"thicknesses": [0.016, 0.076, 0.203, 0.076, 0.025],
|
|
76
76
|
},
|
|
77
|
+
{
|
|
78
|
+
"type": "Material",
|
|
79
|
+
"name": "Opaque panel",
|
|
80
|
+
"conductivity": 1,
|
|
81
|
+
"density": 2470,
|
|
82
|
+
"specific_heat": 750,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"type": "Material",
|
|
86
|
+
"name": "Air gap",
|
|
87
|
+
"conductivity": 0.0625,
|
|
88
|
+
"density": 1.2925,
|
|
89
|
+
"specific_heat": 1006,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
# Thin and light, with a single root: its CTF used to fail the steady state
|
|
93
|
+
# (ASHRAE 140 high-conductance wall)
|
|
94
|
+
"type": "Construction",
|
|
95
|
+
"name": "Thin panel",
|
|
96
|
+
"solar_alpha": [0.8, 0.8],
|
|
97
|
+
"materials": ["Opaque panel", "Air gap", "Opaque panel"],
|
|
98
|
+
"thicknesses": [0.003048, 0.012, 0.003048],
|
|
99
|
+
},
|
|
77
100
|
],
|
|
78
101
|
}
|
|
79
102
|
|
|
@@ -102,6 +125,12 @@ def test_walls_1h():
|
|
|
102
125
|
U = sum(wall._coef_T_a)/sum(wall._coef_Q)
|
|
103
126
|
assert 1/wall.thermal_resistance() == pytest.approx(U, 0.00001)
|
|
104
127
|
|
|
128
|
+
wall = pro.component("Thin panel")
|
|
129
|
+
assert sum(wall._coef_T_a) == pytest.approx(sum(wall._coef_T_b), 0.00001)
|
|
130
|
+
assert sum(wall._coef_T_a) == pytest.approx(sum(wall._coef_T_c), 0.00001)
|
|
131
|
+
U = sum(wall._coef_T_a)/sum(wall._coef_Q)
|
|
132
|
+
assert 1/wall.thermal_resistance() == pytest.approx(U, 0.00001)
|
|
133
|
+
|
|
105
134
|
|
|
106
135
|
def test_walls_15min():
|
|
107
136
|
sim = osim.Simulation()
|
|
@@ -127,3 +156,9 @@ def test_walls_15min():
|
|
|
127
156
|
assert sum(wall._coef_T_a) == pytest.approx(sum(wall._coef_T_c), 0.00001)
|
|
128
157
|
U = sum(wall._coef_T_a)/sum(wall._coef_Q)
|
|
129
158
|
assert 1/wall.thermal_resistance() == pytest.approx(U, 0.00001)
|
|
159
|
+
|
|
160
|
+
wall = pro.component("Thin panel")
|
|
161
|
+
assert sum(wall._coef_T_a) == pytest.approx(sum(wall._coef_T_b), 0.00001)
|
|
162
|
+
assert sum(wall._coef_T_a) == pytest.approx(sum(wall._coef_T_c), 0.00001)
|
|
163
|
+
U = sum(wall._coef_T_a)/sum(wall._coef_Q)
|
|
164
|
+
assert 1/wall.thermal_resistance() == pytest.approx(U, 0.00001)
|
|
@@ -131,3 +131,22 @@ def test_ground_temperature_kasuda():
|
|
|
131
131
|
T_mean = met.T_average
|
|
132
132
|
# At 10m, temperature should be very close to annual mean
|
|
133
133
|
assert abs(var_z10.values[0] - T_mean) < 2.0 # Within 2°C of mean
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_perez_surface_facing_away_from_sun():
|
|
137
|
+
# A surface that does not see the sun gets no circumsolar diffuse, but the rest of the
|
|
138
|
+
# sky radiation keeps the Perez (1 - F1) weighting, so it is below the isotropic value
|
|
139
|
+
# (the fallback to the isotropic model overestimated north walls by ~20 %)
|
|
140
|
+
sim = osim.Simulation()
|
|
141
|
+
pro = sim.new_project("pro")
|
|
142
|
+
pro.read_dict({**project, "simulation_file_met": "denver"})
|
|
143
|
+
pro.simulate()
|
|
144
|
+
met = pro.component("denver")
|
|
145
|
+
perez, isotropic = 0, 0
|
|
146
|
+
for i in range(8760):
|
|
147
|
+
cos_theta = met._cos_incidence(i, 180, 0) # north vertical wall
|
|
148
|
+
if met.variable("sol_direct").values[i] > 0 and cos_theta < 0:
|
|
149
|
+
perez += met.solar_diffuse_rad(i, 180, 0)
|
|
150
|
+
isotropic += met.variable("sol_diffuse").values[i] / 2
|
|
151
|
+
assert isotropic > 0
|
|
152
|
+
assert perez < 0.95 * isotropic
|
|
@@ -373,12 +373,12 @@ def test_HVAC_DX_system_without_vent():
|
|
|
373
373
|
power = pro.component("system").variable("power").values.sum()/1e6
|
|
374
374
|
annual_latent = pro.component("system").variable("Q_latent").values.sum()/1e6
|
|
375
375
|
|
|
376
|
-
assert annual_heating == pytest.approx(3.
|
|
377
|
-
assert annual_cooling == pytest.approx(
|
|
378
|
-
assert peak_heating == pytest.approx(2.
|
|
379
|
-
assert peak_cooling == pytest.approx(5.
|
|
380
|
-
assert power == pytest.approx(14.
|
|
381
|
-
assert annual_latent == pytest.approx(0.
|
|
376
|
+
assert annual_heating == pytest.approx(3.662558442285695)
|
|
377
|
+
assert annual_cooling == pytest.approx(4.953843972941708)
|
|
378
|
+
assert peak_heating == pytest.approx(2.753548603988852)
|
|
379
|
+
assert peak_cooling == pytest.approx(5.697679866477179)
|
|
380
|
+
assert power == pytest.approx(14.602619928128332)
|
|
381
|
+
assert annual_latent == pytest.approx(0.05448078004799185)
|
|
382
382
|
|
|
383
383
|
|
|
384
384
|
def test_HVAC_DX_system_with_vent():
|
|
@@ -400,10 +400,10 @@ def test_HVAC_DX_system_with_vent():
|
|
|
400
400
|
annual_latent = pro.component("system").variable("Q_latent").values.sum()/1e6
|
|
401
401
|
print(annual_heating,annual_cooling,peak_heating,peak_cooling,power,annual_latent)
|
|
402
402
|
|
|
403
|
-
assert annual_heating == pytest.approx(6.
|
|
404
|
-
assert annual_cooling == pytest.approx(4.
|
|
405
|
-
assert peak_heating == pytest.approx(4.
|
|
406
|
-
assert peak_cooling == pytest.approx(5.
|
|
407
|
-
assert power == pytest.approx(16.
|
|
408
|
-
assert annual_latent == pytest.approx(0.
|
|
403
|
+
assert annual_heating == pytest.approx(6.584015158948548)
|
|
404
|
+
assert annual_cooling == pytest.approx(4.047643778543491)
|
|
405
|
+
assert peak_heating == pytest.approx(4.013662511248714)
|
|
406
|
+
assert peak_cooling == pytest.approx(5.378485230257034)
|
|
407
|
+
assert power == pytest.approx(16.73810229504327)
|
|
408
|
+
assert annual_latent == pytest.approx(0.07588097994857204)
|
|
409
409
|
|
|
@@ -376,12 +376,12 @@ def test_HVAC_FAN_COIL_water_system():
|
|
|
376
376
|
peak_heating = Q_sens.max() / 1000
|
|
377
377
|
peak_cooling = -Q_sens.min() / 1000
|
|
378
378
|
|
|
379
|
-
assert annual_heating == pytest.approx(0.
|
|
380
|
-
assert annual_cooling == pytest.approx(4.
|
|
381
|
-
assert annual_humidification == pytest.approx(0.
|
|
379
|
+
assert annual_heating == pytest.approx(0.07271765233509604, rel=1e-5)
|
|
380
|
+
assert annual_cooling == pytest.approx(4.838342243586122, rel=1e-5)
|
|
381
|
+
assert annual_humidification == pytest.approx(0.5796872363278687, rel=1e-5)
|
|
382
382
|
assert annual_dehumidification == pytest.approx(0.0)
|
|
383
|
-
assert peak_heating == pytest.approx(2.
|
|
384
|
-
assert peak_cooling == pytest.approx(5.
|
|
383
|
+
assert peak_heating == pytest.approx(2.679327322280834, rel=1e-5)
|
|
384
|
+
assert peak_cooling == pytest.approx(5.531297059385839, rel=1e-5)
|
|
385
385
|
|
|
386
386
|
yearly_df = water.variable_dataframe(
|
|
387
387
|
frequency="yearly", value="sum", pos_neg_columns=["Q_gen"]
|
|
@@ -389,13 +389,13 @@ def test_HVAC_FAN_COIL_water_system():
|
|
|
389
389
|
heating_generation = yearly_df["Q_gen_pos"].values[0] / 1e6
|
|
390
390
|
cooling_generation = yearly_df["Q_gen_neg"].values[0] / 1e6
|
|
391
391
|
|
|
392
|
-
assert heating_generation == pytest.approx(0.
|
|
393
|
-
assert cooling_generation == pytest.approx(-4.
|
|
392
|
+
assert heating_generation == pytest.approx(0.07388369713417615, rel=1e-5)
|
|
393
|
+
assert cooling_generation == pytest.approx(-4.93133992091134, rel=1e-5)
|
|
394
394
|
|
|
395
395
|
T_WGO = water.variable("T_WGO").values
|
|
396
396
|
T_WGI = water.variable("T_WGI").values
|
|
397
397
|
|
|
398
|
-
assert T_WGO.max() == pytest.approx(
|
|
398
|
+
assert T_WGO.max() == pytest.approx(50.6756558449134, rel=1e-5)
|
|
399
399
|
assert T_WGO.min() == pytest.approx(7.0)
|
|
400
|
-
assert T_WGI.max() == pytest.approx(
|
|
401
|
-
assert T_WGI.min() == pytest.approx(7.
|
|
400
|
+
assert T_WGI.max() == pytest.approx(50.688432226123474, rel=1e-5)
|
|
401
|
+
assert T_WGI.min() == pytest.approx(7.041926599097343, rel=1e-5)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import opensimula as osim
|
|
2
|
+
import math
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
def test_Iterative_process():
|
|
6
|
+
def y(x):
|
|
7
|
+
return math.cos(x)
|
|
8
|
+
x_i = 0.5
|
|
9
|
+
itera = osim.Iterative_process(x_i)
|
|
10
|
+
while not itera.converged():
|
|
11
|
+
x_i = itera.estimate_next_x(y(x_i))
|
|
12
|
+
|
|
13
|
+
assert x_i == pytest.approx(0.73908,abs=1e-4)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_Iterative_process_residual():
|
|
17
|
+
# Slowly contracting fixed point (x = 0.95 x + 0.05, solution 1): at convergence the
|
|
18
|
+
# fixed-point residual must be below the tolerance. Testing the relaxed step instead
|
|
19
|
+
# stopped with residuals up to tol / lambda_min.
|
|
20
|
+
def y(x):
|
|
21
|
+
return 0.95 * x + 0.05
|
|
22
|
+
tol = 1e-4
|
|
23
|
+
x_i = 0.0
|
|
24
|
+
itera = osim.Iterative_process(x_i, tol=tol, n_ini_relax=3, rel_vel=0.8)
|
|
25
|
+
n = 0
|
|
26
|
+
while not itera.converged() and n < 10000:
|
|
27
|
+
x_i = itera.estimate_next_x(y(x_i))
|
|
28
|
+
n += 1
|
|
29
|
+
assert abs(y(x_i) - x_i) < tol
|
|
30
|
+
assert x_i == pytest.approx(1, abs=tol / (1 - 0.95))
|
|
@@ -124,15 +124,21 @@ def test_real_projects_validate(schema, relative_path):
|
|
|
124
124
|
jsonschema.validate(document, schema)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def
|
|
128
|
-
"""Yield (
|
|
129
|
-
|
|
130
|
-
The
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
def _ashrae_140_projects(directory):
|
|
128
|
+
"""Yield (name, project dict) for every ASHRAE 140 case.
|
|
129
|
+
|
|
130
|
+
The cases are the largest corpus of real project definitions in the
|
|
131
|
+
repository: project JSON files in <TEST>/cases/ for the automated folders,
|
|
132
|
+
plus dict literals still defined inside notebooks (folders not automated
|
|
133
|
+
yet; read with ast.literal_eval, so cells that build a dict from variables
|
|
134
|
+
are skipped rather than executed). Archived notebooks are skipped: their
|
|
135
|
+
cases are the JSON files.
|
|
134
136
|
"""
|
|
137
|
+
for json_path in sorted(directory.glob("*/cases/*.json")):
|
|
138
|
+
yield json_path.name, json.loads(json_path.read_text())
|
|
135
139
|
for notebook_path in sorted(directory.rglob("*.ipynb")):
|
|
140
|
+
if "old_notebooks" in notebook_path.parts:
|
|
141
|
+
continue
|
|
136
142
|
notebook = nbformat.read(notebook_path, as_version=4)
|
|
137
143
|
for cell in notebook.cells:
|
|
138
144
|
if cell.cell_type != "code":
|
|
@@ -160,7 +166,7 @@ def test_ashrae_140_projects_validate(schema):
|
|
|
160
166
|
validator = jsonschema.Draft202012Validator(schema)
|
|
161
167
|
failures = []
|
|
162
168
|
n_documents = 0
|
|
163
|
-
for notebook, document in
|
|
169
|
+
for notebook, document in _ashrae_140_projects(directory):
|
|
164
170
|
n_documents += 1
|
|
165
171
|
for component in document["components"]:
|
|
166
172
|
type_name = component.get("type")
|
|
@@ -176,7 +182,7 @@ def test_ashrae_140_projects_validate(schema):
|
|
|
176
182
|
failures.append(f"{notebook}: {type_name}.{path}: {error.message}")
|
|
177
183
|
validator.validate(document)
|
|
178
184
|
|
|
179
|
-
assert n_documents > 0, "no project
|
|
185
|
+
assert n_documents > 0, "no ASHRAE 140 project definitions found"
|
|
180
186
|
assert failures == [], "\n".join(failures[:20])
|
|
181
187
|
|
|
182
188
|
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import math
|
|
2
|
-
|
|
3
|
-
class Iterative_process:
|
|
4
|
-
def __init__(self, x_0, tol = 1e-5, lambda_min=0.1, lambda_max=1.0, n_ini_relax=2,rel_vel = 0.9):
|
|
5
|
-
"""
|
|
6
|
-
Iterative process using the fixed-point method with adaptive relaxation coefficient
|
|
7
|
-
|
|
8
|
-
Parameters:
|
|
9
|
-
x_0: Initial value
|
|
10
|
-
tol: convergence tolerance
|
|
11
|
-
lambda_min, lambda_max: relaxation coefficient range
|
|
12
|
-
n_ini_relax: Number of iterations in which adaptive relaxation is initiated, must be greater than 1
|
|
13
|
-
"""
|
|
14
|
-
self.x = [x_0]
|
|
15
|
-
self.tol = tol
|
|
16
|
-
self.lambda_min = lambda_min
|
|
17
|
-
self.lambda_max = lambda_max
|
|
18
|
-
self.lambda_i = lambda_max
|
|
19
|
-
self.n_ini_relax = n_ini_relax
|
|
20
|
-
self.rel_vel = rel_vel
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def converged(self):
|
|
24
|
-
if len(self.x) == 1:
|
|
25
|
-
return False
|
|
26
|
-
else:
|
|
27
|
-
if abs(self.x[-1]-self.x[-2]) < self.tol:
|
|
28
|
-
return True
|
|
29
|
-
else:
|
|
30
|
-
return False
|
|
31
|
-
|
|
32
|
-
def estimate_next_x(self, proposed_x):
|
|
33
|
-
x_next = (1 - self.lambda_i) * self.x[-1] + self.lambda_i * proposed_x
|
|
34
|
-
|
|
35
|
-
# Ajuste dinámico de lambda (reduce si hay oscilaciones)
|
|
36
|
-
if len(self.x)>self.n_ini_relax:
|
|
37
|
-
#if abs(x_next - self.x[-1]) > abs(self.x[-1]- self.x[-2]):
|
|
38
|
-
self.lambda_i = max(self.lambda_min, self.lambda_i * self.rel_vel) # Reducimos lambda
|
|
39
|
-
#else:
|
|
40
|
-
#self.lambda_i = min(self.lambda_max, self.lambda_i * 1/self.rel_vel) # Aumentamos lambda
|
|
41
|
-
|
|
42
|
-
self.x.append(x_next)
|
|
43
|
-
return x_next
|
|
44
|
-
|
|
45
|
-
def set_next_x(self, next_x):
|
|
46
|
-
self.x.append(next_x)
|
|
47
|
-
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import opensimula as osim
|
|
2
|
-
import math
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
def test_Iterative_process():
|
|
6
|
-
def y(x):
|
|
7
|
-
return math.cos(x)
|
|
8
|
-
x_i = 0.5
|
|
9
|
-
itera = osim.Iterative_process(x_i)
|
|
10
|
-
while not itera.converged():
|
|
11
|
-
x_i = itera.estimate_next_x(y(x_i))
|
|
12
|
-
|
|
13
|
-
assert x_i == pytest.approx(0.73908,abs=1e-4)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|