OpenSimula 0.8.9__tar.gz → 0.8.10__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.
Files changed (74) hide show
  1. {opensimula-0.8.9/src/opensimula.egg-info → opensimula-0.8.10}/PKG-INFO +1 -1
  2. {opensimula-0.8.9 → opensimula-0.8.10}/pyproject.toml +1 -1
  3. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Building_surface.py +37 -6
  4. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/DX_unit.py +90 -4
  5. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/HVAC_MZW_system.py +29 -4
  6. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/HVAC_SZW_system.py +26 -6
  7. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Opening.py +6 -2
  8. {opensimula-0.8.9 → opensimula-0.8.10/src/opensimula.egg-info}/PKG-INFO +1 -1
  9. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_Case_600FF.py +3 -3
  10. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_Case_610.py +4 -4
  11. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_HVAC_DX_Equipment.py +21 -0
  12. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_HVAC_DX_system.py +11 -11
  13. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_HVAC_FAN_COIL_water_system.py +10 -10
  14. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_Project.py +39 -0
  15. {opensimula-0.8.9 → opensimula-0.8.10}/LICENSE +0 -0
  16. {opensimula-0.8.9 → opensimula-0.8.10}/README.md +0 -0
  17. {opensimula-0.8.9 → opensimula-0.8.10}/setup.cfg +0 -0
  18. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Child.py +0 -0
  19. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Component.py +0 -0
  20. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Iterative_process.py +0 -0
  21. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Message.py +0 -0
  22. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Parameter_container.py +0 -0
  23. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Parameters.py +0 -0
  24. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Project.py +0 -0
  25. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Simulation.py +0 -0
  26. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/Variable.py +0 -0
  27. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/__init__.py +0 -0
  28. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Building.py +0 -0
  29. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Calculator.py +0 -0
  30. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Chiller_heat_pump.py +0 -0
  31. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Construction.py +0 -0
  32. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Day_schedule.py +0 -0
  33. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Fan.py +0 -0
  34. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/File_data.py +0 -0
  35. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/File_met.py +0 -0
  36. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Frame.py +0 -0
  37. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Glazing.py +0 -0
  38. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/HVAC_DX_system.py +0 -0
  39. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/HVAC_perfect_system.py +0 -0
  40. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/HVAC_water_system.py +0 -0
  41. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Material.py +0 -0
  42. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Opening_type.py +0 -0
  43. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Pump.py +0 -0
  44. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Solar_surface.py +0 -0
  45. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Space.py +0 -0
  46. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Space_type.py +0 -0
  47. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Surface.py +0 -0
  48. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Test_component.py +0 -0
  49. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Water_coil.py +0 -0
  50. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Week_schedule.py +0 -0
  51. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/Year_schedule.py +0 -0
  52. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/components/__init__.py +0 -0
  53. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/editor/__init__.py +0 -0
  54. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/editor/schema.py +0 -0
  55. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/editor/static/editor.css +0 -0
  56. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/editor/static/editor.js +0 -0
  57. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/editor/validation.py +0 -0
  58. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/editor/widget.py +0 -0
  59. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/visual_3D/Environment_3D.py +0 -0
  60. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula/visual_3D/Polygon_3D.py +0 -0
  61. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula.egg-info/SOURCES.txt +0 -0
  62. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula.egg-info/dependency_links.txt +0 -0
  63. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula.egg-info/requires.txt +0 -0
  64. {opensimula-0.8.9 → opensimula-0.8.10}/src/opensimula.egg-info/top_level.txt +0 -0
  65. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_Calculator.py +0 -0
  66. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_Construction.py +0 -0
  67. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_File_data.py +0 -0
  68. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_File_met.py +0 -0
  69. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_HVAC_water_system_process.py +0 -0
  70. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_Iterative_process.py +0 -0
  71. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_Schedules.py +0 -0
  72. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_editor_schema.py +0 -0
  73. {opensimula-0.8.9 → opensimula-0.8.10}/test/test_editor_widget.py +0 -0
  74. {opensimula-0.8.9 → opensimula-0.8.10}/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.9
3
+ Version: 0.8.10
4
4
  Summary: Component-based time simulation environment in Python
5
5
  Author: Luis Pérez-Lombard, Ismael R. Maestre
6
6
  Author-email: "Juan F. Coronel" <jfc@us.es>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opensimula"
3
- version = "0.8.9"
3
+ version = "0.8.10"
4
4
  requires-python = ">=3.12"
5
5
  authors = [
6
6
  { name="Juan F. Coronel", email="jfc@us.es" },
@@ -12,6 +12,8 @@ from opensimula.Parameters import (
12
12
  from opensimula.Variable import Variable
13
13
  from opensimula.visual_3D.Polygon_3D import Polygon_3D
14
14
 
15
+ SIGMA = 5.670374419e-8 # Stefan-Boltzmann constant, W/m²K⁴
16
+
15
17
 
16
18
  class Building_surface(Surface):
17
19
  def __init__(self, name, project):
@@ -266,8 +268,10 @@ class Building_surface(Surface):
266
268
  self.parameter("ground_material").component.parameter("specific_heat").value
267
269
  )
268
270
  d_p = math.sqrt((self._k_g_ * periodo_segundos) / (math.pi * rho_g * c_p_g))
269
- self._U_hat_ext_ = self._G_w_ * self._k_g_ / math.sqrt(
270
- (0.457 * self._B_prime_ + self._d_t_) ** 2 + d_p**2
271
+ # Periodic external heat transfer coefficient, UNE-EN ISO 13370:2017 (H_pe), per m²
272
+ P = self.perimeter * self.parameter("exterior_perimeter_fraction").value
273
+ self._U_hat_ext_ = (
274
+ self._G_w_ * 0.37 * P * self._k_g_ * math.log(d_p / self._d_t_ + 1) / self.area
271
275
  )
272
276
 
273
277
  def _create_openings_list_(self):
@@ -331,7 +335,7 @@ class Building_surface(Surface):
331
335
  # Meterological data
332
336
  hor_sol_dif = self._file_met.variable("sol_diffuse").values[time_i]
333
337
  hor_sol_dir = self._file_met.variable("sol_direct").values[time_i]
334
- T_sky = self._file_met.variable("sky_temperature").values[time_i]
338
+ self._T_sky = self._file_met.variable("sky_temperature").values[time_i]
335
339
 
336
340
  # Diffuse solar radiation
337
341
  E_dif_sunny = self._file_met.solar_diffuse_rad(
@@ -360,17 +364,44 @@ class Building_surface(Surface):
360
364
  q_sol0 = self.radiant_property("alpha", "solar_diffuse", 0) * (E_dif + E_dir)
361
365
  self.variable("q_sol0").values[time_i] = q_sol0
362
366
  # Mean radiant temperature
363
- T_rm = self._F_sky * T_sky + (1 - self._F_sky) * self._T_ext
367
+ T_rm = self._F_sky * self._T_sky + (1 - self._F_sky) * self._T_ext
364
368
  self.variable("T_rm").values[time_i] = T_rm
365
- # f_0
366
369
  h_rd = self.H_RD * self.radiant_property("alpha", "long_wave", 0)
370
+ T_s0 = self.variable("T_s0").values[time_i - 1] if time_i > 0 else self._T_ext
371
+ self._q_lw_corr = self.long_wave_correction(
372
+ T_s0, self.radiant_property("alpha", "long_wave", 0)
373
+ )
374
+ # f_0
367
375
  self.f_0 = self.area * (
368
376
  -self._p_0_
369
377
  - self.parameter("h_cv").value[0] * self._T_ext
370
378
  - h_rd * T_rm
371
379
  - q_sol0
380
+ - self._q_lw_corr
372
381
  )
373
382
 
383
+ def long_wave_correction(self, T_s0, epsilon):
384
+ """Exterior long wave gain (W/m²) not included in the linearized exchange
385
+
386
+ The building matrices linearize the long wave exchange with the sky and the ground
387
+ with H_RD (20 °C); the difference with the exact exchange is added explicitly, with
388
+ the surface temperature of the previous time step.
389
+
390
+ Args:
391
+ T_s0 (float): exterior surface temperature (°C)
392
+ epsilon (float): long wave emissivity of the exterior surface
393
+
394
+ Returns:
395
+ float: exact minus linearized long wave gain (W/m²)
396
+ """
397
+ T_rm = self._F_sky * self._T_sky + (1 - self._F_sky) * self._T_ext
398
+ exact = epsilon * SIGMA * (
399
+ self._F_sky * (self._T_sky + 273.15) ** 4
400
+ + (1 - self._F_sky) * (self._T_ext + 273.15) ** 4
401
+ - (T_s0 + 273.15) ** 4
402
+ )
403
+ return exact - self.H_RD * epsilon * (T_rm - T_s0)
404
+
374
405
  def _calculate_direct_sunny_fraction_(self, time_i):
375
406
  if self.project().parameter("shadow_calculation").value == "INSTANT":
376
407
  direct_sunny_fraction = self.project().env_3D.get_direct_sunny_fraction(
@@ -426,7 +457,7 @@ class Building_surface(Surface):
426
457
  self.variable("q_lwt0").values[time_i] = h_rd * (
427
458
  self.variable("T_rm").values[time_i]
428
459
  - self.variable("T_s0").values[time_i]
429
- )
460
+ ) + self._q_lw_corr
430
461
  self.variable("q_lwt1").values[time_i] = (
431
462
  -self.variable("q_cd1").values[time_i]
432
463
  - self.variable("q_cv1").values[time_i]
@@ -1,7 +1,9 @@
1
1
  from opensimula.Message import Message
2
2
  from opensimula.Parameters import Parameter_float, Parameter_float_list, Parameter_math_exp, Parameter_options, Parameter_boolean
3
3
  from opensimula.Component import Component
4
- from scipy.optimize import fsolve
4
+ from scipy.optimize import fsolve, brentq
5
+ import math
6
+ import psychrolib as sicro
5
7
 
6
8
  class DX_unit(Component):
7
9
  def __init__(self, name, project):
@@ -24,7 +26,7 @@ class DX_unit(Component):
24
26
  self.add_parameter(Parameter_math_exp("heating_capacity_expression", "1", "frac"))
25
27
  self.add_parameter(Parameter_math_exp("heating_power_expression", "1", "frac"))
26
28
  self.add_parameter(Parameter_math_exp("COP_expression", "1", "frac"))
27
- self.add_parameter(Parameter_options("dry_coil_model", "SENSIBLE", ["TOTAL", "SENSIBLE"]))
29
+ self.add_parameter(Parameter_options("dry_coil_model", "SENSIBLE", ["TOTAL", "SENSIBLE", "ADP"]))
28
30
  self.add_parameter(Parameter_options("indoor_fan_operation", "CONTINUOUS", ["CONTINUOUS", "CYCLING"]))
29
31
  self.add_parameter(Parameter_boolean("power_dry_coil_correction", True))
30
32
  self.add_parameter(Parameter_float_list("expression_max_values", [60,30,60,30,1.5,1], "-"))
@@ -41,6 +43,11 @@ class DX_unit(Component):
41
43
  errors.append(Message(msg, "ERROR"))
42
44
  return errors
43
45
 
46
+ def pre_simulation(self, n_time_steps, delta_t):
47
+ super().pre_simulation(n_time_steps, delta_t)
48
+ self._A_0 = None # Bypass factor constant, calculated at the first cooling call
49
+ self._T_iwb_dry = None
50
+
44
51
  def get_heating_load(self,T_idb,T_iwb,T_odb,T_owb,F_air,Q_required):
45
52
  """
46
53
  Q_required: Required heating load
@@ -118,7 +125,11 @@ class DX_unit(Component):
118
125
  sensible_capacity = self.parameter("nominal_sensible_cooling_capacity").value
119
126
  f = self.parameter("sensible_cooling_capacity_expression").evaluate(var_dic)
120
127
  sensible_capacity = sensible_capacity * f
121
- if (sensible_capacity > total_capacity):
128
+ self._T_iwb_dry = None
129
+ if self.parameter("dry_coil_model").value == "ADP":
130
+ total_capacity, sensible_capacity = self._adp_capacities(
131
+ T_idb, T_iwb, T_odb, T_owb, F_air, total_capacity, sensible_capacity)
132
+ elif (sensible_capacity > total_capacity):
122
133
  if self.parameter("dry_coil_model").value == "SENSIBLE":
123
134
  total_capacity = sensible_capacity
124
135
  elif self.parameter("dry_coil_model").value == "TOTAL":
@@ -164,7 +175,10 @@ class DX_unit(Component):
164
175
 
165
176
  def _get_correct_cooling_power(self,total_capacity, sensible_capacity, var_dic):
166
177
  power = self.parameter("nominal_cooling_power").value
167
- if (sensible_capacity == total_capacity and self.parameter("power_dry_coil_correction").value):
178
+ if self.parameter("dry_coil_model").value == "ADP":
179
+ if self._T_iwb_dry is not None and self.parameter("power_dry_coil_correction").value:
180
+ var_dic["T_iwb"] = self._T_iwb_dry
181
+ elif (sensible_capacity == total_capacity and self.parameter("power_dry_coil_correction").value):
168
182
  T_iwb_min = self._get_min_T_iwb(var_dic)
169
183
  var_dic["T_iwb"] = T_iwb_min
170
184
  f = self.parameter("cooling_power_expression").evaluate(var_dic)
@@ -180,6 +194,78 @@ class DX_unit(Component):
180
194
  root = fsolve(func, var_dic["T_iwb"],xtol=1e-3)
181
195
  return root[0]
182
196
 
197
+ # Apparatus dew point (ADP) / bypass factor (BF) dry coil model:
198
+ # BF = exp(-A_0/m), A_0 from the nominal capacities; the coil is dry when the humidity
199
+ # ratio of the ADP is not below the inlet one.
200
+ def _adp_capacities(self, T_idb, T_iwb, T_odb, T_owb, F_air, total_capacity, sensible_capacity):
201
+ p = self._sim_.props["ATM_PRESSURE"]
202
+ if getattr(self, "_A_0", None) is None: # also used outside a simulation
203
+ self._A_0 = self._get_A_0(p)
204
+ w_in = sicro.GetHumRatioFromTWetBulb(T_idb, min(T_iwb, T_idb), p)
205
+ m_air = self._air_mass_flow(T_idb, w_in, F_air, p)
206
+ w_adp, SHR = self._adp_state(T_idb, w_in, m_air, total_capacity, p)
207
+ if w_adp >= w_in: # Dry coil: capacity at the wet bulb of the dry/wet boundary
208
+ T_iwb_dry = self._get_dry_T_iwb(T_idb, T_iwb, T_odb, T_owb, F_air, m_air, p)
209
+ var_dic = self._var_state_dic([T_idb, T_iwb_dry, T_odb, T_owb, F_air, 1])
210
+ total_capacity = (self.parameter("nominal_total_cooling_capacity").value
211
+ * self.parameter("total_cooling_capacity_expression").evaluate(var_dic))
212
+ self._T_iwb_dry = T_iwb_dry
213
+ return (total_capacity, total_capacity)
214
+ # Wet coil: the latent capacity cannot exceed that of the ADP model
215
+ sensible_capacity = max(sensible_capacity, total_capacity * SHR)
216
+ if sensible_capacity > total_capacity:
217
+ total_capacity = sensible_capacity
218
+ return (total_capacity, sensible_capacity)
219
+
220
+ def _get_A_0(self, p):
221
+ T_idb, T_iwb, _ = self.parameter("nominal_cooling_conditions").value
222
+ Q_tot = self.parameter("nominal_total_cooling_capacity").value
223
+ Q_sen = self.parameter("nominal_sensible_cooling_capacity").value
224
+ w_in = sicro.GetHumRatioFromTWetBulb(T_idb, T_iwb, p)
225
+ m_air = self._air_mass_flow(T_idb, w_in, 1, p)
226
+ h_in = sicro.GetMoistAirEnthalpy(T_idb, w_in)
227
+ h_out = h_in - Q_tot / m_air
228
+ c_p = sicro.GetMoistAirEnthalpy(1, w_in) - sicro.GetMoistAirEnthalpy(0, w_in)
229
+ T_out = T_idb - Q_sen / (m_air * c_p)
230
+ w_out = sicro.GetHumRatioFromEnthalpyAndTDryBulb(h_out, T_out)
231
+ # ADP: the line through inlet and outlet states meets the saturation curve
232
+ def func(T):
233
+ return sicro.GetSatHumRatio(T, p) - (w_in + (w_out - w_in) * (T - T_idb) / (T_out - T_idb))
234
+ T_adp = brentq(func, -40, T_out)
235
+ BF = (T_out - T_adp) / (T_idb - T_adp)
236
+ return -math.log(BF) * m_air
237
+
238
+ def _air_mass_flow(self, T_idb, w_in, F_air, p):
239
+ return self.parameter("nominal_air_flow").value * F_air / sicro.GetMoistAirVolume(T_idb, w_in, p)
240
+
241
+ def _adp_state(self, T_idb, w_in, m_air, total_capacity, p):
242
+ """Humidity ratio of the ADP and sensible heat ratio for the given total capacity"""
243
+ BF = math.exp(-self._A_0 / m_air)
244
+ h_in = sicro.GetMoistAirEnthalpy(T_idb, w_in)
245
+ h_adp = h_in - total_capacity / m_air / (1 - BF)
246
+ if h_adp >= sicro.GetSatAirEnthalpy(T_idb, p): # No capacity: dry
247
+ return (sicro.GetSatHumRatio(T_idb, p), 1)
248
+ if h_adp <= sicro.GetSatAirEnthalpy(-60, p):
249
+ return (0, 0)
250
+ T_adp = brentq(lambda T: sicro.GetSatAirEnthalpy(T, p) - h_adp, -60, T_idb)
251
+ w_adp = sicro.GetSatHumRatio(T_adp, p)
252
+ h_tin_wadp = sicro.GetMoistAirEnthalpy(T_idb, w_adp)
253
+ SHR = min((h_tin_wadp - h_adp) / (h_in - h_adp), 1)
254
+ return (w_adp, SHR)
255
+
256
+ def _get_dry_T_iwb(self, T_idb, T_iwb, T_odb, T_owb, F_air, m_air, p):
257
+ """Inlet wet bulb temperature at which the coil starts to be wet"""
258
+ def func(T_wb):
259
+ var_dic = self._var_state_dic([T_idb, T_wb, T_odb, T_owb, F_air, 1])
260
+ total = (self.parameter("nominal_total_cooling_capacity").value
261
+ * self.parameter("total_cooling_capacity_expression").evaluate(var_dic))
262
+ w = sicro.GetHumRatioFromTWetBulb(T_idb, T_wb, p)
263
+ return self._adp_state(T_idb, w, m_air, total, p)[0] - w
264
+ T_wb_min = min(T_iwb, T_idb)
265
+ if func(T_idb) >= 0: # Dry even at saturation
266
+ return T_idb
267
+ return brentq(func, T_wb_min, T_idb, xtol=1e-3)
268
+
183
269
  def get_fan_power(self, f_load):
184
270
  if self.parameter("indoor_fan_operation").value == "CONTINUOUS":
185
271
  return self.parameter("indoor_fan_power").value
@@ -58,6 +58,7 @@ class HVAC_MZW_system(Component): # HVAC Multizone Water system
58
58
  )
59
59
  )
60
60
  self.add_parameter(Parameter_math_exp("outdoor_air_fraction", "0", "frac"))
61
+ self.add_parameter(Parameter_math_exp("outdoor_air_flow", "0", "m³/s"))
61
62
  self.add_parameter(Parameter_variable_list("input_variables", []))
62
63
  self.add_parameter(Parameter_math_exp("supply_heating_setpoint", "10", "°C"))
63
64
  self.add_parameter(Parameter_math_exp("supply_cooling_setpoint", "15", "°C"))
@@ -214,6 +215,16 @@ class HVAC_MZW_system(Component): # HVAC Multizone Water system
214
215
  f"{self.parameter('name').value}, must define its reheat water system."
215
216
  )
216
217
  errors.append(Message(msg, "ERROR"))
218
+ # Test minimum outdoor air given only once
219
+ if _not_zero(self.parameter("outdoor_air_fraction").value) and _not_zero(
220
+ self.parameter("outdoor_air_flow").value
221
+ ):
222
+ msg = f"{self.parameter('name').value}, outdoor_air_fraction and outdoor_air_flow cannot be used at the same time."
223
+ errors.append(Message(msg, "ERROR"))
224
+ # VAV systems get the outdoor air from the return air flow
225
+ if self.parameter("vav").value and _not_zero(self.parameter("outdoor_air_flow").value):
226
+ msg = f"{self.parameter('name').value}, outdoor_air_flow cannot be used in VAV systems."
227
+ errors.append(Message(msg, "ERROR"))
217
228
  # Test file_met defined
218
229
  if self.project().parameter("simulation_file_met").value == "not_defined":
219
230
  msg = f"{self.parameter('name').value}, file_met must be defined in the project 'simulation_file_met'."
@@ -286,6 +297,7 @@ class HVAC_MZW_system(Component): # HVAC Multizone Water system
286
297
  # outdoor air fraction
287
298
  self.F_OA_min = self.parameter("outdoor_air_fraction").evaluate(var_dic)
288
299
  self.F_OA = self.F_OA_min
300
+ self.V_OA = self.parameter("outdoor_air_flow").evaluate(var_dic)
289
301
  # setpoints
290
302
  self.T_heat_sp = self.parameter("supply_heating_setpoint").evaluate(var_dic)
291
303
  self.variable("supply_heating_setpoint").values[time_index] = self.T_heat_sp
@@ -350,20 +362,26 @@ class HVAC_MZW_system(Component): # HVAC Multizone Water system
350
362
  # Calculate F_OA based on return air flow
351
363
  if self.is_vav:
352
364
  self.F_OA = (self.air_flow * self.F_flow_total - v_return) / (self.air_flow* self.F_flow_total)
365
+ elif self.V_OA > 0:
366
+ self._set_outdoor_air_from_flow(rho_ZA)
353
367
 
354
- # Return fan
355
- if self.return_fan is None: # No return fan
356
- self.m_return = self.air_flow * (1 - self.F_OA) * rho_ZA
368
+ # Zone mass balance: the minimum outdoor air is exhausted from the zones
369
+ self.m_return = self.m_air_supply * (1 - self.F_OA)
370
+ if self.return_fan is None or self.m_return <= 0: # No return fan
357
371
  self.T_RA = self.T_ZA
358
372
  else:
359
373
  # Calculate flow fraction in return fan
360
- self.m_return = v_return * rho_ZA
361
374
  Q_return_fan = self._get_fan_power(
362
375
  "return", self.F_load, v_return/self.return_air_flow
363
376
  )
364
377
  self.T_RA = Q_return_fan / (self.m_return * self.props["C_PA"]) + self.T_ZA
365
378
 
366
379
 
380
+ def _set_outdoor_air_from_flow(self, rho_ZA):
381
+ # Outdoor air flow given at zone air conditions (the air exhausted from the zones)
382
+ self.F_OA_min = min(self.V_OA * rho_ZA / self.m_air_supply, 1)
383
+ self.F_OA = self.F_OA_min
384
+
367
385
  def _calculate_mixed_air(self, f_oa):
368
386
  # Mixed air
369
387
  self.T_MA, self.w_MA, self.T_MAwb = self._mix_air(f_oa, self.T_OA, self.w_OA, self.T_RA, self.w_RA)
@@ -717,3 +735,10 @@ class HVAC_MZW_system(Component): # HVAC Multizone Water system
717
735
  self.variable("T_ADP").values[time_index] = self.T_ADP
718
736
  self.variable("epsilon").values[time_index] = self.epsilon
719
737
  self.variable("epsilon_adp").values[time_index] = self.epsilon_adp
738
+
739
+
740
+ def _not_zero(expression):
741
+ try:
742
+ return float(expression) != 0
743
+ except ValueError: # Expression with variables
744
+ return True
@@ -30,6 +30,7 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
30
30
  self.add_parameter(Parameter_float("air_flow", 1, "m³/s", min=0))
31
31
  self.add_parameter(Parameter_float("return_air_flow", 1, "m³/s", min=0))
32
32
  self.add_parameter(Parameter_math_exp("outdoor_air_fraction", "0", "frac"))
33
+ self.add_parameter(Parameter_math_exp("outdoor_air_flow", "0", "m³/s"))
33
34
  self.add_parameter(Parameter_variable_list("input_variables", []))
34
35
  self.add_parameter(Parameter_math_exp("heating_setpoint", "20", "°C"))
35
36
  self.add_parameter(Parameter_math_exp("cooling_setpoint", "25", "°C"))
@@ -146,6 +147,12 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
146
147
  f"{self.parameter('name').value}, must define its heating water system."
147
148
  )
148
149
  errors.append(Message(msg, "ERROR"))
150
+ # Test minimum outdoor air given only once
151
+ if _not_zero(self.parameter("outdoor_air_fraction").value) and _not_zero(
152
+ self.parameter("outdoor_air_flow").value
153
+ ):
154
+ msg = f"{self.parameter('name').value}, outdoor_air_fraction and outdoor_air_flow cannot be used at the same time."
155
+ errors.append(Message(msg, "ERROR"))
149
156
  # Test file_met defined
150
157
  if self.project().parameter("simulation_file_met").value == "not_defined":
151
158
  msg = f"{self.parameter('name').value}, file_met must be defined in the project 'simulation_file_met'."
@@ -199,6 +206,7 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
199
206
  # outdoor air fraction
200
207
  self.F_OA_min = self.parameter("outdoor_air_fraction").evaluate(var_dic)
201
208
  self.F_OA = self.F_OA_min
209
+ self.V_OA = self.parameter("outdoor_air_flow").evaluate(var_dic)
202
210
  # setpoints
203
211
  self.T_heat_sp = self.parameter("heating_setpoint").evaluate(var_dic)
204
212
  self.variable("heating_setpoint").values[time_index] = self.T_heat_sp
@@ -218,10 +226,6 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
218
226
  self.epsilon_adp = 0
219
227
  # Initial mass flows
220
228
  self.m_air_supply = self.air_flow * self.props["RHO_A"]
221
- if self.return_fan is None: # No return fan
222
- self.v_air_return = self.air_flow * (1 - self.F_OA)
223
- else: # Return fan
224
- self.v_air_return = self.return_air_flow
225
229
 
226
230
  def iteration(self, time_index, date, daylight_saving, n_iter):
227
231
  super().iteration(time_index, date, daylight_saving, n_iter)
@@ -245,8 +249,11 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
245
249
  self.T_ZA, self.w_ZA / 1000, self.props["ATM_PRESSURE"]
246
250
  )
247
251
  self.w_RA = self.w_ZA
248
- self.m_air_return = self.v_air_return * rho_ZA
249
- if self.return_fan is None: # No return fan
252
+ if self.V_OA > 0:
253
+ self._set_outdoor_air_from_flow(rho_ZA)
254
+ # Zone mass balance: the minimum outdoor air is exhausted from the zone
255
+ self.m_air_return = self.m_air_supply * (1 - self.F_OA_min)
256
+ if self.return_fan is None or self.m_air_return <= 0: # No return fan
250
257
  self.Q_return_fan_required = 0
251
258
  self.T_RA = self.T_ZA
252
259
  else:
@@ -258,6 +265,12 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
258
265
  self.m_air_return * self.props["C_PA"]
259
266
  )
260
267
 
268
+ def _set_outdoor_air_from_flow(self, rho_ZA):
269
+ # Outdoor air flow given at zone air conditions (the air exhausted from the zone)
270
+ self.F_OA_min = min(self.V_OA * rho_ZA / self.m_air_supply, 1)
271
+ if self.parameter("economizer").value == "NO" or self.F_OA < self.F_OA_min:
272
+ self.F_OA = self.F_OA_min
273
+
261
274
  def _calculate_required_Q(self):
262
275
  K_t, F_t = self.space.get_thermal_equation(False)
263
276
  Q_supply_fan = self._get_fan_power("supply", self.F_load)
@@ -583,3 +596,10 @@ class HVAC_SZW_system(Component): # HVAC Single Zone Water system
583
596
  self.variable("T_ADP").values[time_index] = self.T_ADP
584
597
  self.variable("epsilon").values[time_index] = self.epsilon
585
598
  self.variable("epsilon_adp").values[time_index] = self.epsilon_adp
599
+
600
+
601
+ def _not_zero(expression):
602
+ try:
603
+ return float(expression) != 0
604
+ except ValueError: # Expression with variables
605
+ return True
@@ -228,8 +228,12 @@ class Opening(Component):
228
228
  T_rm = surface.variable("T_rm").values[time_i]
229
229
  self.variable("T_rm").values[time_i] = T_rm
230
230
  h_rd = self.H_RD * self.radiant_property("alpha", "long_wave", 0)
231
+ T_s0 = self.variable("T_s0").values[time_i - 1] if time_i > 0 else self._T_ext
232
+ self._q_lw_corr = surface.long_wave_correction(
233
+ T_s0, self.radiant_property("alpha", "long_wave", 0)
234
+ )
231
235
  self.f_0 = self.area * (
232
- -self.parameter("h_cv").value[0] * self._T_ext - h_rd * T_rm
236
+ -self.parameter("h_cv").value[0] * self._T_ext - h_rd * T_rm - self._q_lw_corr
233
237
  )
234
238
  # q_sol0 will be corrected by the building
235
239
 
@@ -312,7 +316,7 @@ class Opening(Component):
312
316
  self.variable("q_lwt0").values[time_i] = h_rd * (
313
317
  self.variable("T_rm").values[time_i]
314
318
  - self.variable("T_s0").values[time_i]
315
- )
319
+ ) + self._q_lw_corr
316
320
  self.variable("q_lwt1").values[time_i] = (
317
321
  +self.variable("q_cd").values[time_i]
318
322
  - self.variable("q_cv1").values[time_i]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opensimula
3
- Version: 0.8.9
3
+ Version: 0.8.10
4
4
  Summary: Component-based time simulation environment in Python
5
5
  Author: Luis Pérez-Lombard, Ismael R. Maestre
6
6
  Author-email: "Juan F. Coronel" <jfc@us.es>
@@ -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.161302336706985)
323
- assert T.max() == pytest.approx(65.67212712041555)
324
- assert T.min() == pytest.approx(-13.387879316775047)
322
+ assert T.mean() == pytest.approx(25.393174594921604)
323
+ assert T.max() == pytest.approx(65.59282242450941)
324
+ assert T.min() == pytest.approx(-12.419790814021622)
@@ -349,10 +349,10 @@ 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.600215347025868)
353
- assert annual_cooling == pytest.approx(4.1577184451246785)
354
- assert peak_heating == pytest.approx(3.253717666525836)
355
- assert peak_cooling == pytest.approx(5.451313222438749)
352
+ assert annual_heating == pytest.approx(4.463690912071373)
353
+ assert annual_cooling == pytest.approx(4.184622421932999)
354
+ assert peak_heating == pytest.approx(3.1410488100839884)
355
+ assert peak_cooling == pytest.approx(5.470013279897169)
356
356
 
357
357
 
358
358
  def test_interpolated_sunny_fraction():
@@ -47,3 +47,24 @@ def test_points():
47
47
  assert dry_coil_Q[2] == pytest.approx(0,rel=1e-2)
48
48
 
49
49
 
50
+
51
+
52
+ def test_adp_dry_coil_model():
53
+ sim = osim.Simulation()
54
+ pro = sim.new_project("pro")
55
+ pro.read_dict(case_dict)
56
+ dx = pro.component("HVAC_equipment")
57
+ dx.parameter("dry_coil_model").value = "ADP"
58
+
59
+ # Nominal conditions: the bypass factor reproduces the nominal sensible heat ratio
60
+ Q_eq, Q_sen, Q_lat, F_load = dx.get_cooling_load(26.7, 19.4, 35, 25, 1, -6136)
61
+ assert Q_sen / (Q_sen + Q_lat) == pytest.approx(6136 / 7951, rel=2e-2)
62
+ # Dry inlet air (26.7 °C, 13 °C wet bulb): dry coil, no latent load
63
+ Q_eq, Q_sen, Q_lat, F_load = dx.get_cooling_load(26.7, 13, 35, 25, 1, -3000)
64
+ assert Q_lat == 0
65
+ assert dx._T_iwb_dry > 13
66
+ # The latent capacity goes to 0 continuously approaching the dry/wet boundary
67
+ T_wb = dx._T_iwb_dry
68
+ lat = [dx.get_cooling_load(26.7, T_wb + dT, 35, 25, 1, -3000)[2] for dT in (0.05, 0.5, 1)]
69
+ assert 0 <= lat[0] < lat[1] < lat[2]
70
+ assert lat[0] < 0.1 * lat[2]
@@ -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.6308525834349497)
377
- assert annual_cooling == pytest.approx(4.979106872785461)
376
+ assert annual_heating == pytest.approx(3.505853012164859)
377
+ assert annual_cooling == pytest.approx(5.011342208430301)
378
378
  assert peak_heating == pytest.approx(2.749886298191046)
379
- assert peak_cooling == pytest.approx(5.697679866477179)
380
- assert power == pytest.approx(14.566525249640708)
381
- assert annual_latent == pytest.approx(0.05448532861090071)
379
+ assert peak_cooling == pytest.approx(5.716379241482701)
380
+ assert power == pytest.approx(14.432399666361574)
381
+ assert annual_latent == pytest.approx(0.054370526548591265)
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.546162001592287)
404
- assert annual_cooling == pytest.approx(4.064545377360391)
405
- assert peak_heating == pytest.approx(4.013157255693249)
406
- assert peak_cooling == pytest.approx(5.378485230257034)
407
- assert power == pytest.approx(16.706104000254026)
408
- assert annual_latent == pytest.approx(0.07588788128726585)
403
+ assert annual_heating == pytest.approx(6.413918255026011)
404
+ assert annual_cooling == pytest.approx(4.0843062420685365)
405
+ assert peak_heating == pytest.approx(3.9487898560161283)
406
+ assert peak_cooling == pytest.approx(5.397096853455196)
407
+ assert power == pytest.approx(16.614980023519852)
408
+ assert annual_latent == pytest.approx(0.0753997559884013)
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.06385026130900037, rel=1e-5)
380
- assert annual_cooling == pytest.approx(4.84214845930499, rel=1e-5)
381
- assert annual_humidification == pytest.approx(0.5802388327831097, rel=1e-5)
379
+ assert annual_heating == pytest.approx(0.05794164810570399, rel=1e-5)
380
+ assert annual_cooling == pytest.approx(4.841634385823313, rel=1e-5)
381
+ assert annual_humidification == pytest.approx(0.5815140376898394, rel=1e-5)
382
382
  assert annual_dehumidification == pytest.approx(0.0)
383
- assert peak_heating == pytest.approx(2.52316442850628, rel=1e-5)
384
- assert peak_cooling == pytest.approx(5.531297059385839, rel=1e-5)
383
+ assert peak_heating == pytest.approx(2.4139930539173866, rel=1e-5)
384
+ assert peak_cooling == pytest.approx(5.52532910869739, 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.06564630834915086, rel=1e-5)
393
- assert cooling_generation == pytest.approx(-4.935146136630207, rel=1e-5)
392
+ assert heating_generation == pytest.approx(0.05987756110752762, rel=1e-5)
393
+ assert cooling_generation == pytest.approx(-4.934717594594343, 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(50.53477212415523, rel=1e-5)
398
+ assert T_WGO.max() == pytest.approx(50.848459465012894, rel=1e-5)
399
399
  assert T_WGO.min() == pytest.approx(7.0)
400
- assert T_WGI.max() == pytest.approx(50.57068717973014, rel=1e-5)
401
- assert T_WGI.min() == pytest.approx(7.041926599097343, rel=1e-5)
400
+ assert T_WGI.max() == pytest.approx(50.876345651587265, rel=1e-5)
401
+ assert T_WGI.min() == pytest.approx(7.041357495923282, rel=1e-5)
@@ -1,3 +1,4 @@
1
+ import pytest
1
2
  import opensimula as osim
2
3
 
3
4
  p1_dic = {
@@ -220,3 +221,41 @@ def test_opening_surface_type():
220
221
  assert surface_type_errors() == []
221
222
  wall.parameter("surface_type").value = "UNDERGROUND"
222
223
  assert len(surface_type_errors()) == 1
224
+
225
+
226
+ def test_outdoor_air_flow_or_fraction():
227
+ sim = osim.Simulation()
228
+ sim.console_print = False
229
+ p1 = sim.new_project("Project 1")
230
+ szw = p1.new_component("HVAC_SZW_system", "szw")
231
+ mzw = p1.new_component("HVAC_MZW_system", "mzw")
232
+
233
+ def outdoor_air_errors():
234
+ return [e for e in p1.check() if "outdoor_air" in e.text]
235
+
236
+ assert outdoor_air_errors() == []
237
+ for system in (szw, mzw):
238
+ system.parameter("outdoor_air_fraction").value = "0.3"
239
+ system.parameter("outdoor_air_flow").value = 0.1
240
+ assert len(outdoor_air_errors()) == 2
241
+ for system in (szw, mzw):
242
+ system.parameter("outdoor_air_fraction").value = 0
243
+ mzw.parameter("vav").value = True
244
+ assert len(outdoor_air_errors()) == 1
245
+
246
+
247
+ def test_long_wave_correction():
248
+ sim = osim.Simulation()
249
+ p1 = sim.new_project("Project 1")
250
+ wall = p1.new_component("Building_surface", "wall")
251
+ wall._F_sky = 0.5
252
+ # Around 20 °C the linearized exchange is exact
253
+ wall._T_sky, wall._T_ext = 19.0, 20.0
254
+ assert abs(wall.long_wave_correction(20.5, 0.9)) < 0.01
255
+ # In winter the linearized exchange (H_RD at 20 °C) overestimates the loss to the sky
256
+ wall._T_sky, wall._T_ext = -20.0, 0.0
257
+ T_s0 = -2.0
258
+ T_rm = 0.5 * -20.0 + 0.5 * 0.0
259
+ linear = wall.H_RD * 0.9 * (T_rm - T_s0)
260
+ assert wall.long_wave_correction(T_s0, 0.9) > 0
261
+ assert wall.long_wave_correction(T_s0, 0.9) / -linear == pytest.approx(0.29, abs=0.02)
File without changes
File without changes
File without changes