ecoengine 3.2.0__tar.gz → 3.2.2__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 (80) hide show
  1. {ecoengine-3.2.0 → ecoengine-3.2.2}/PKG-INFO +2 -1
  2. {ecoengine-3.2.0 → ecoengine-3.2.2}/pyproject.toml +2 -1
  3. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/constants/constants.py +1 -1
  4. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/interfaces/EcosizerEngine.py +34 -5
  5. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/heating/PerformanceMap.py +8 -8
  6. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/InstantWHSystem.py +2 -2
  7. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/recirc_systems/SwingDualFuelSystem.py +2 -2
  8. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/recirc_systems/SwingERTrdOffSystem.py +3 -3
  9. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/recirc_systems/SwingSystem.py +3 -3
  10. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/rtp_systems/MP_RTPInSeriesSystem.py +52 -20
  11. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/rtp_systems/SP_RTPInParallelSystem.py +42 -10
  12. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/rtp_systems/SP_RTPInSeriesSystem.py +50 -15
  13. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine.egg-info/PKG-INFO +2 -1
  14. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine.egg-info/requires.txt +1 -0
  15. {ecoengine-3.2.0 → ecoengine-3.2.2}/README.md +0 -0
  16. {ecoengine-3.2.0 → ecoengine-3.2.2}/setup.cfg +0 -0
  17. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/__init__.py +0 -0
  18. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/constants/__init__.py +0 -0
  19. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/__init__.py +0 -0
  20. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/climate_data/DryBulbTemperatures_ByClimateZone.csv +0 -0
  21. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/climate_data/InletWaterTemperatures_ByClimateZone.csv +0 -0
  22. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/climate_data/WeatherStation_ClimateZone_Lookup.csv +0 -0
  23. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/climate_data/ZipCode_ClimateZone_Lookup.csv +0 -0
  24. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/climate_data/__init__.py +0 -0
  25. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/climate_data/kGperkWh_ByClimateZone.csv +0 -0
  26. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/__init__.py +0 -0
  27. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/apartment.json +0 -0
  28. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/elementary_school.json +0 -0
  29. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/food_service_a.json +0 -0
  30. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/food_service_b.json +0 -0
  31. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/junior_high.json +0 -0
  32. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/mens_dorm.json +0 -0
  33. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/motel.json +0 -0
  34. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/multi_family.json +0 -0
  35. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/nursing_home.json +0 -0
  36. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/office_building.json +0 -0
  37. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/senior_high.json +0 -0
  38. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/load_shapes/womens_dorm.json +0 -0
  39. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/__init__.py +0 -0
  40. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/maps.json +0 -0
  41. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH3_SP_bounds.pkl +0 -0
  42. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH3_SP_capacity_interpolator.pkl +0 -0
  43. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH3_SP_power_in_interpolator.pkl +0 -0
  44. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH4_MP_bounds.pkl +0 -0
  45. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH4_MP_capacity_interpolator.pkl +0 -0
  46. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH4_MP_power_in_interpolator.pkl +0 -0
  47. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/data/preformanceMaps/pkls/__init__.py +0 -0
  48. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/interfaces/Simulator.py +0 -0
  49. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/interfaces/__init__.py +0 -0
  50. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/__init__.py +0 -0
  51. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/building/Building.py +0 -0
  52. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/building/ClimateZone.py +0 -0
  53. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/building/UtilityCostTracker.py +0 -0
  54. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/building/__init__.py +0 -0
  55. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/__init__.py +0 -0
  56. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/heating/Controls.py +0 -0
  57. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/heating/WaterHeater.py +0 -0
  58. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/heating/__init__.py +0 -0
  59. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/storage/EnergyTank.py +0 -0
  60. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/storage/MixedStorageTank.py +0 -0
  61. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/storage/SlugOverlayTank.py +0 -0
  62. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/storage/StorageTank.py +0 -0
  63. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/storage/StratifiedTank.py +0 -0
  64. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/components/storage/__init__.py +0 -0
  65. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/DHWSystem.py +0 -0
  66. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/MPNoRecircSystem.py +0 -0
  67. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/__init__.py +0 -0
  68. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/recirc_systems/ParallelLoopSystem.py +0 -0
  69. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/recirc_systems/RecircSystem.py +0 -0
  70. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/recirc_systems/__init__.py +0 -0
  71. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/rtp_systems/MultiPassRTPSystem.py +0 -0
  72. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/rtp_systems/RTPSystem.py +0 -0
  73. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/rtp_systems/SinglePassRTPSystem.py +0 -0
  74. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/rtp_systems/__init__.py +0 -0
  75. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/dhwsystems/utils.py +0 -0
  76. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/simulation/SimulationRun.py +0 -0
  77. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine/objects/simulation/__init__.py +0 -0
  78. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine.egg-info/SOURCES.txt +0 -0
  79. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine.egg-info/dependency_links.txt +0 -0
  80. {ecoengine-3.2.0 → ecoengine-3.2.2}/src/ecoengine.egg-info/top_level.txt +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ecoengine
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Requires-Python: >=3.11
5
5
  Requires-Dist: numpy<3.0,>=2.4
6
6
  Requires-Dist: scipy<2.0,>=1.17
7
7
  Requires-Dist: plotly<8.0,>=7.0
8
+ Requires-Dist: kaleido>=1.3.0
8
9
  Provides-Extra: docs
9
10
  Requires-Dist: sphinx>=6; extra == "docs"
10
11
  Requires-Dist: furo; extra == "docs"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ecoengine"
7
- version = "3.2.0"
7
+ version = "3.2.2"
8
8
  requires-python = ">=3.11"
9
9
 
10
10
  # Ranges are bounded on purpose, and the shared ones are kept in sync with
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "numpy>=2.4,<3.0",
33
33
  "scipy>=1.17,<2.0",
34
34
  "plotly>=7.0,<8.0",
35
+ "kaleido>=1.3.0",
35
36
  ]
36
37
 
37
38
  [project.optional-dependencies]
@@ -2,4 +2,4 @@
2
2
  _RHO_CP: float = 8.353535
3
3
 
4
4
  # Unit conversion: 1 W = 3.412142 BTU/hr (equivalently, 1 kW = 3.412142 kBTU/hr)
5
- _W_TO_KBTUH: float = 3.412142
5
+ _BTUH_PER_W: float = 3.412142
@@ -5,6 +5,7 @@ import os
5
5
  import warnings
6
6
  from .Simulator import simulate_3day as _simulate_3day, simulate_annual as _simulate_annual
7
7
  from ecoengine.objects.building.ClimateZone import ClimateZone as _ClimateZone
8
+ from ecoengine.constants.constants import _BTUH_PER_W
8
9
 
9
10
  _BUNDLED_MAPS_PATH = os.path.join(os.path.dirname(__file__), "../data/preformanceMaps/maps.json")
10
11
  _WS_LOOKUP = os.path.join(os.path.dirname(__file__), "../data/climate_data/WeatherStation_ClimateZone_Lookup.csv")
@@ -229,7 +230,7 @@ def get_hpwh_output_capacity(
229
230
  capacity_kbtuh *= num_heaters * (1.0 - defrost_derate)
230
231
 
231
232
  if return_as_kw:
232
- return capacity_kbtuh / 3.41214 # kBTU/hr → kW
233
+ return capacity_kbtuh / _BTUH_PER_W # kBTU/hr → kW
233
234
  return capacity_kbtuh
234
235
 
235
236
 
@@ -810,6 +811,7 @@ class EcosizerEngine:
810
811
  self._dhw_system = None
811
812
 
812
813
  # Build and size immediately
814
+ self.fallback = False
813
815
  self.build()
814
816
 
815
817
  # ------------------------------------------------------------------
@@ -1113,7 +1115,7 @@ class EcosizerEngine:
1113
1115
  self._require_dual_fuel_sizing_params()
1114
1116
  with warnings.catch_warnings():
1115
1117
  warnings.simplefilter("ignore")
1116
- return SP_RTPInSeriesSystem.from_size(
1118
+ sprtp_in_series_system = SP_RTPInSeriesSystem.from_size(
1117
1119
  building = self._building,
1118
1120
  supply_temp_f = self.supply_temp_f,
1119
1121
  storage_temp_f = self.storage_temp_f,
@@ -1128,6 +1130,11 @@ class EcosizerEngine:
1128
1130
  control_map = control_map,
1129
1131
  load_shift_fract_total_vol = ls_fract,
1130
1132
  )
1133
+ if not sprtp_in_series_system.fallback_system is None:
1134
+ self.fallback = True
1135
+ return sprtp_in_series_system.fallback_system
1136
+ else:
1137
+ return sprtp_in_series_system
1131
1138
 
1132
1139
  if self.schematic == "sprtp_in_parallel":
1133
1140
  from ecoengine.objects.dhwsystems.rtp_systems.SP_RTPInParallelSystem import SP_RTPInParallelSystem
@@ -1135,7 +1142,7 @@ class EcosizerEngine:
1135
1142
  self._require_dual_fuel_sizing_params()
1136
1143
  with warnings.catch_warnings():
1137
1144
  warnings.simplefilter("ignore")
1138
- return SP_RTPInParallelSystem.from_size(
1145
+ sprtp_in_parallel_system = SP_RTPInParallelSystem.from_size(
1139
1146
  building = self._building,
1140
1147
  supply_temp_f = self.supply_temp_f,
1141
1148
  storage_temp_f = self.storage_temp_f,
@@ -1151,6 +1158,11 @@ class EcosizerEngine:
1151
1158
  load_shift_fract_total_vol = ls_fract,
1152
1159
  gas_controls = self._build_gas_controls(),
1153
1160
  )
1161
+ if not sprtp_in_parallel_system.fallback_system is None:
1162
+ self.fallback = True
1163
+ return sprtp_in_parallel_system.fallback_system
1164
+ else:
1165
+ return sprtp_in_parallel_system
1154
1166
 
1155
1167
  if self.schematic == "mprtp_in_series":
1156
1168
  from ecoengine.objects.dhwsystems.rtp_systems.MP_RTPInSeriesSystem import MP_RTPInSeriesSystem
@@ -1162,7 +1174,7 @@ class EcosizerEngine:
1162
1174
  # engine-level setting (see the plain 'multi_pass_rtp' branch above,
1163
1175
  # which hardcodes 14); MP_RTPInSeriesSystem defaults to the same
1164
1176
  # value via _MPRTP_MAX_DAILY_RUN_HR.
1165
- return MP_RTPInSeriesSystem.from_size(
1177
+ mprtp_in_series_system = MP_RTPInSeriesSystem.from_size(
1166
1178
  building = self._building,
1167
1179
  supply_temp_f = self.supply_temp_f,
1168
1180
  storage_temp_f = self.storage_temp_f,
@@ -1176,6 +1188,11 @@ class EcosizerEngine:
1176
1188
  control_map = control_map,
1177
1189
  drawdown_fract = self.drawdown_fract,
1178
1190
  )
1191
+ if not mprtp_in_series_system.fallback_system is None:
1192
+ self.fallback = True
1193
+ return mprtp_in_series_system.fallback_system
1194
+ else:
1195
+ return mprtp_in_series_system
1179
1196
 
1180
1197
  if self.schematic == "swing_dual_fuel":
1181
1198
  from ecoengine.objects.dhwsystems.recirc_systems.SwingDualFuelSystem import SwingDualFuelSystem
@@ -1696,6 +1713,18 @@ class EcosizerEngine:
1696
1713
  if hasattr(sys, "_gas_capacity_kbtuh") and sys._gas_capacity_kbtuh is not None:
1697
1714
  result["gas_capacity_kbtuh"] = sys._gas_capacity_kbtuh
1698
1715
  return result
1716
+
1717
+ def was_fallback_operation(self) -> bool:
1718
+ """
1719
+ Returns if flag for fallback operation was set (only applicable for supplemental heating systems that were undersized but still
1720
+ successfully completed a simulation)
1721
+
1722
+ Returns
1723
+ -------
1724
+ bool
1725
+ True if an undersized base system succesfully passed simulation and sized no additional heating
1726
+ """
1727
+ return self.fallback
1699
1728
 
1700
1729
  # ------------------------------------------------------------------
1701
1730
  # Simulation
@@ -1905,7 +1934,7 @@ class EcosizerEngine:
1905
1934
  control_schedule, control_map = self._build_control_map()
1906
1935
  is_ls = "shed" in control_map
1907
1936
 
1908
- if self.schematic in _DUAL_FUEL_SCHEMATICS:
1937
+ if self.schematic in _DUAL_FUEL_SCHEMATICS and not self.fallback:
1909
1938
  title = get_sizing_curve_title(self.schematic)
1910
1939
  # Gas backup curve has its own x/y semantics (backup storage vs.
1911
1940
  # backup capacity across ASHRAE windows) — _build_sizing_curve_figure
@@ -5,7 +5,7 @@ import math
5
5
  import os
6
6
  import pickle
7
7
 
8
- from ecoengine.constants.constants import _W_TO_KBTUH
8
+ from ecoengine.constants.constants import _BTUH_PER_W
9
9
 
10
10
  # Bundled data directory — used as a fallback when perf_map_dir is not supplied.
11
11
  # Will be removed once the data is fully externalised.
@@ -221,7 +221,7 @@ class PerformanceMap:
221
221
  pwr_kw = self.get_power_in_kw(oat_f, outlet_temp_f, inlet_temp_f)
222
222
  if cap_kbtuh is None or pwr_kw is None or pwr_kw <= 0:
223
223
  return None
224
- return (cap_kbtuh / _W_TO_KBTUH) / pwr_kw
224
+ return (cap_kbtuh / _BTUH_PER_W) / pwr_kw
225
225
 
226
226
  def is_within_operating_bounds(self, oat_f: float) -> bool:
227
227
  """Return True if the conditions are within the map's valid operating range."""
@@ -427,7 +427,7 @@ class PklPerformanceMap(PerformanceMap):
427
427
  def _er_per_unit_kw(self) -> float:
428
428
  """kW per unit at COP=1 (Electric Resistance fallback)."""
429
429
  if self._nominal_kbtuh is not None:
430
- return (self._nominal_kbtuh / self.num_units) / _W_TO_KBTUH
430
+ return (self._nominal_kbtuh / self.num_units) / _BTUH_PER_W
431
431
  return self._default_out_low_kw
432
432
 
433
433
  def _get_per_unit_kw(
@@ -477,7 +477,7 @@ class PklPerformanceMap(PerformanceMap):
477
477
  ) -> float:
478
478
  inlet, outlet = self._resolve_temps(outlet_temp_f, inlet_temp_f)
479
479
  out_kw, _ = self._get_per_unit_kw(oat_f, inlet, outlet)
480
- return out_kw * self.num_units * _W_TO_KBTUH
480
+ return out_kw * self.num_units * _BTUH_PER_W
481
481
 
482
482
  def get_capacity_kw(
483
483
  self,
@@ -485,7 +485,7 @@ class PklPerformanceMap(PerformanceMap):
485
485
  outlet_temp_f: float,
486
486
  inlet_temp_f: float | None = None,
487
487
  ) -> float:
488
- return self.get_capacity_kbtuh(oat_f, outlet_temp_f, inlet_temp_f) / _W_TO_KBTUH
488
+ return self.get_capacity_kbtuh(oat_f, outlet_temp_f, inlet_temp_f) / _BTUH_PER_W
489
489
 
490
490
  def get_power_in_kw(
491
491
  self,
@@ -589,7 +589,7 @@ class HPWHsimPerformanceMap(PerformanceMap):
589
589
  entry = self._perfmap[0]
590
590
  input_kw = self._quad(entry["inputPower_coeffs"], self._design_inlet_f) / 1000.0
591
591
  cop = max(self._quad(entry["COP_coeffs"], self._design_inlet_f), 0.0)
592
- return max(cop * input_kw * _W_TO_KBTUH, 0.0)
592
+ return max(cop * input_kw * _BTUH_PER_W, 0.0)
593
593
  return 0.0
594
594
 
595
595
  def _get_per_unit_kbtuh(
@@ -642,7 +642,7 @@ class HPWHsimPerformanceMap(PerformanceMap):
642
642
 
643
643
  output_kw = cop * input_kw
644
644
  # Convert kW to kBTU/hr
645
- return output_kw * _W_TO_KBTUH, input_kw * _W_TO_KBTUH
645
+ return output_kw * _BTUH_PER_W, input_kw * _BTUH_PER_W
646
646
 
647
647
  # ------------------------------------------------------------------
648
648
  # Public interface
@@ -666,7 +666,7 @@ class HPWHsimPerformanceMap(PerformanceMap):
666
666
  ) -> float:
667
667
  inlet_temp_f = inlet_temp_f if inlet_temp_f is not None else self._design_inlet_f
668
668
  _, inp_kbtuh = self._get_per_unit_kbtuh(oat_f, inlet_temp_f, outlet_temp_f)
669
- return inp_kbtuh / _W_TO_KBTUH * self.num_units
669
+ return inp_kbtuh / _BTUH_PER_W * self.num_units
670
670
 
671
671
  def is_within_operating_bounds(self, oat_f: float) -> bool:
672
672
  return oat_f >= self.oat_min
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from ecoengine.constants.constants import _RHO_CP, _W_TO_KBTUH
3
+ from ecoengine.constants.constants import _RHO_CP, _BTUH_PER_W
4
4
  from .DHWSystem import DHWSystem
5
5
 
6
6
 
@@ -119,7 +119,7 @@ class InstantWHSystem(DHWSystem):
119
119
  "demand_supplyT_gal": demand_supplyT_gal,
120
120
  "usable_volume_supplyT_gal": 0.0,
121
121
  "heater_output_kbtuh": capacity_kbtuh,
122
- "heater_power_in_kw": capacity_kbtuh / _W_TO_KBTUH, # COP of 1.0
122
+ "heater_power_in_kw": capacity_kbtuh / _BTUH_PER_W, # COP of 1.0
123
123
  "oat_f": oat_f,
124
124
  "inlet_water_temp_f": inlet_temp_f,
125
125
  "tank_temps_f": [self.supply_temp_f] * 6,
@@ -7,7 +7,7 @@ from ecoengine.objects.components.heating.WaterHeater import WaterHeater
7
7
  from ecoengine.objects.components.storage.MixedStorageTank import MixedStorageTank
8
8
  from ecoengine.objects.components.storage.StratifiedTank import StratifiedTank
9
9
  from ecoengine.objects.dhwsystems.DHWSystem import DHWSystem
10
- from ecoengine.constants.constants import _RHO_CP, _W_TO_KBTUH
10
+ from ecoengine.constants.constants import _RHO_CP, _BTUH_PER_W
11
11
  from ecoengine.objects.dhwsystems.utils import (
12
12
  mixing_valve_behavior,
13
13
  mixing_valve_behavior_swing,
@@ -509,5 +509,5 @@ class SwingDualFuelSystem(SwingSystem):
509
509
  # TM panel data (consumed by SimulationRun for the swing-tank subplot)
510
510
  "tm_tank_temp_f": swing_mid_temp_f,
511
511
  "tm_heater_output_kbtuh": tm_kbtuh,
512
- "tm_heater_input_kw": tm_kbtuh / _W_TO_KBTUH,
512
+ "tm_heater_input_kw": tm_kbtuh / _BTUH_PER_W,
513
513
  }
@@ -10,7 +10,7 @@ from ecoengine.objects.components.storage.MixedStorageTank import MixedStorageTa
10
10
  from ecoengine.objects.components.storage.StratifiedTank import StratifiedTank
11
11
  from ecoengine.objects.dhwsystems.DHWSystem import _get_peak_indices
12
12
  from .SwingSystem import SwingSystem, _ELEMENT_DEADBAND_F, _hr_to_min
13
- from ecoengine.constants.constants import _RHO_CP, _W_TO_KBTUH
13
+ from ecoengine.constants.constants import _RHO_CP, _BTUH_PER_W
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from ecoengine.objects.building.Building import Building
@@ -520,7 +520,7 @@ class SwingERTrdOffSystem(SwingSystem):
520
520
 
521
521
  def get_er_capacity_kw(self) -> float:
522
522
  """Return the additional ER capacity added to the TM element [kW]."""
523
- return self.get_er_capacity_kbtuh() / _W_TO_KBTUH
523
+ return self.get_er_capacity_kbtuh() / _BTUH_PER_W
524
524
 
525
525
  # ------------------------------------------------------------------
526
526
  # ER sizing curve (trade-off plot)
@@ -591,7 +591,7 @@ class SwingERTrdOffSystem(SwingSystem):
591
591
  total_tm_i = self._minimum_tm_capacity_kbtuh
592
592
  er_needed = bool(self._er_capacity_kbtuh and self._er_capacity_kbtuh > 0)
593
593
 
594
- er_cap_kw.append(round(total_tm_i / _W_TO_KBTUH, 0))
594
+ er_cap_kw.append(round(total_tm_i / _BTUH_PER_W, 0))
595
595
  fract_covered.append(i)
596
596
  i -= 10.0
597
597
 
@@ -9,7 +9,7 @@ from ecoengine.objects.components.storage.StratifiedTank import StratifiedTank
9
9
  from ecoengine.objects.components.storage.MixedStorageTank import MixedStorageTank
10
10
  from ecoengine.objects.dhwsystems.DHWSystem import _get_peak_indices, StorageVolumeTooSmallError
11
11
  from .RecircSystem import RecircSystem
12
- from ecoengine.constants.constants import _RHO_CP, _W_TO_KBTUH
12
+ from ecoengine.constants.constants import _RHO_CP, _BTUH_PER_W
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from ecoengine.objects.building.Building import Building
@@ -373,7 +373,7 @@ class SwingSystem(RecircSystem):
373
373
  tm_safety_factor × recirc_loss_kbtuh
374
374
  """
375
375
  recirc_loss_btuhr = self.get_recirc_loss_kbtuh() * 1000.0
376
- vol_required = recirc_loss_btuhr / (_WATTS_PER_GAL * _W_TO_KBTUH)
376
+ vol_required = recirc_loss_btuhr / (_WATTS_PER_GAL * _BTUH_PER_W)
377
377
 
378
378
  if building is not None:
379
379
  peak_draw_gal_per_min = (
@@ -1038,5 +1038,5 @@ class SwingSystem(RecircSystem):
1038
1038
  # TM panel data (consumed by SimulationRun for the swing-tank subplot)
1039
1039
  "tm_tank_temp_f": swing_mid_temp_f,
1040
1040
  "tm_heater_output_kbtuh": tm_kbtuh,
1041
- "tm_heater_input_kw": tm_kbtuh / _W_TO_KBTUH, # assume COP of 1
1041
+ "tm_heater_input_kw": tm_kbtuh / _BTUH_PER_W, # assume COP of 1
1042
1042
  }
@@ -6,7 +6,7 @@ from ecoengine.objects.components.heating.Controls import Controls
6
6
  from ecoengine.objects.components.heating.WaterHeater import WaterHeater
7
7
  from ecoengine.objects.components.storage.MixedStorageTank import MixedStorageTank
8
8
  from ecoengine.objects.components.storage.SlugOverlayTank import SlugOverlayTank
9
- from ecoengine.constants.constants import _W_TO_KBTUH
9
+ from ecoengine.constants.constants import _BTUH_PER_W
10
10
  from ecoengine.objects.dhwsystems.rtp_systems.SP_RTPInSeriesSystem import _GAS_DEADBAND_F
11
11
  from ..utils import (
12
12
  mixing_valve_behavior,
@@ -111,7 +111,10 @@ class MP_RTPInSeriesSystem(MultiPassRTPSystem):
111
111
  MP_RTPInSeriesSystem
112
112
  """
113
113
  # ensure shed turns primary system completely off
114
- control_map = set_dual_fuel_shed_controls(control_map)
114
+ saved_shed_state = None
115
+ if 'shed' in control_map:
116
+ saved_shed_state = control_map.get("shed")
117
+ control_map = set_dual_fuel_shed_controls(control_map)
115
118
 
116
119
  system = cls(
117
120
  water_heaters=[],
@@ -124,7 +127,8 @@ class MP_RTPInSeriesSystem(MultiPassRTPSystem):
124
127
  defrost_factor=defrost_factor,
125
128
  tm_safety_factor=tm_safety_factor,
126
129
  )
127
-
130
+ system.saved_shed_state = saved_shed_state
131
+ system.fallback_system = None
128
132
  system._minimum_capacity_kbtuh = nominal_capacity_kbtuh
129
133
  system._minimum_storage_storageT_gal = nominal_storage_gal
130
134
 
@@ -194,24 +198,52 @@ class MP_RTPInSeriesSystem(MultiPassRTPSystem):
194
198
  """
195
199
  _WINDOW_MIN = 30
196
200
 
197
- self.outage_volume_gal, self.outage_temp_delta_f = size_supplemental_heating_and_storage(
198
- primary_system=self,
199
- building=building,
200
- nominal_capacity_kbtuh=nominal_capacity_kbtuh,
201
- )
202
-
203
- gas_capacity_kbtuh, gas_storage_vol_gal = self._gas_backup_from_window(_WINDOW_MIN)
204
- # TODO add thermal efficiency
201
+ try:
202
+ self.outage_volume_gal, self.outage_temp_delta_f = size_supplemental_heating_and_storage(
203
+ primary_system=self,
204
+ building=building,
205
+ nominal_capacity_kbtuh=nominal_capacity_kbtuh,
206
+ )
207
+ gas_capacity_kbtuh, gas_storage_vol_gal = self._gas_backup_from_window(_WINDOW_MIN)
208
+ # TODO add thermal efficiency
205
209
 
206
- self._in_series_storage_vol_gal = gas_storage_vol_gal
207
- self._in_series_capacity_kbtuh = gas_capacity_kbtuh
210
+ self._in_series_storage_vol_gal = gas_storage_vol_gal
211
+ self._in_series_capacity_kbtuh = gas_capacity_kbtuh
208
212
 
209
- self.gas_water_heater = WaterHeater.from_nominal_capacity(
210
- nominal_capacity_kbtuh=gas_capacity_kbtuh,
211
- control_schedule=["normal"] * 24,
212
- control_map={"normal": gas_controls},
213
- )
214
- self.gas_storage_tank = MixedStorageTank(total_volume_gal=gas_storage_vol_gal)
213
+ self.gas_water_heater = WaterHeater.from_nominal_capacity(
214
+ nominal_capacity_kbtuh=gas_capacity_kbtuh,
215
+ control_schedule=["normal"] * 24,
216
+ control_map={"normal": gas_controls},
217
+ )
218
+ self.gas_storage_tank = MixedStorageTank(total_volume_gal=gas_storage_vol_gal)
219
+ except ValueError as e:
220
+ # Compare against base class to cover window where system is undersized but still passing the simulation
221
+ control_map = self.water_heaters[0].control_map
222
+ if 'shed' in control_map:
223
+ control_map['shed'] = self.saved_shed_state
224
+ comparison_system = MultiPassRTPSystem.from_size(
225
+ building = building,
226
+ supply_temp_f = self.supply_temp_f,
227
+ storage_temp_f = self.storage_temp_f,
228
+ return_temp_f = self.return_temp_f,
229
+ return_flow_gpm = self.return_flow_gpm,
230
+ max_daily_run_hr = 14,
231
+ defrost_factor = self.defrost_factor,
232
+ tm_safety_factor = self.tm_safety_factor,
233
+ control_schedule = self.water_heaters[0].control_schedule,
234
+ control_map = control_map,
235
+ drawdown_fract = self.storage_tank.drawdown_fract,
236
+ )
237
+ result = {
238
+ "min_capacity_kbtuh": comparison_system._minimum_capacity_kbtuh,
239
+ "min_storage_storageT_gal": comparison_system._minimum_storage_storageT_gal,
240
+ }
241
+ if result["min_capacity_kbtuh"] >= nominal_capacity_kbtuh and result["min_storage_storageT_gal"] >= nominal_storage_gal:
242
+ # redirect user to base model
243
+ self.fallback_system = comparison_system
244
+ return
245
+ else:
246
+ raise e
215
247
 
216
248
  # ------------------------------------------------------------------
217
249
  # Gas backup sizing helpers
@@ -409,6 +441,6 @@ class MP_RTPInSeriesSystem(MultiPassRTPSystem):
409
441
  "mode": mode,
410
442
  "tm_tank_temp_f": self.gas_storage_tank.get_temperature_at_fraction(1.0),
411
443
  "tm_heater_output_kbtuh": gas_kbtuh,
412
- "tm_heater_input_kw": gas_kbtuh / _W_TO_KBTUH,
444
+ "tm_heater_input_kw": gas_kbtuh / _BTUH_PER_W,
413
445
  "delivery_temp_f": gas_top_t,
414
446
  }
@@ -6,7 +6,7 @@ import numpy as np
6
6
  from ecoengine.objects.components.heating.Controls import Controls
7
7
  from ecoengine.objects.components.heating.WaterHeater import WaterHeater
8
8
  from ecoengine.objects.components.storage.EnergyTank import EnergyTank
9
- from ecoengine.constants.constants import _RHO_CP, _W_TO_KBTUH
9
+ from ecoengine.constants.constants import _RHO_CP, _BTUH_PER_W
10
10
  from ..utils import (
11
11
  mixing_valve_behavior,
12
12
  size_supplemental_heating_and_storage,
@@ -154,7 +154,10 @@ class SP_RTPInParallelSystem(SinglePassRTPSystem):
154
154
  SP_RTPInParallelSystem
155
155
  """
156
156
  # ensure shed turns primary system completely off
157
- control_map = set_dual_fuel_shed_controls(control_map)
157
+ saved_shed_state = None
158
+ if 'shed' in control_map:
159
+ saved_shed_state = control_map.get("shed")
160
+ control_map = set_dual_fuel_shed_controls(control_map)
158
161
 
159
162
  system = cls(
160
163
  water_heaters=[],
@@ -184,6 +187,9 @@ class SP_RTPInParallelSystem(SinglePassRTPSystem):
184
187
  control_map=control_map,
185
188
  )]
186
189
 
190
+ system.saved_shed_state = saved_shed_state
191
+ system.fallback_system = None
192
+
187
193
  # Gas backup controls: on at supply_temp, off at supply_temp + deadband
188
194
  system._size_gas_backup(
189
195
  building=building,
@@ -265,13 +271,39 @@ class SP_RTPInParallelSystem(SinglePassRTPSystem):
265
271
  0.0, max(self.supply_temp_f - t for t in sim_run.tank_temps_f[-1])
266
272
  )
267
273
  if sim_run.outage_minutes <= self._MIN_OUTAGE_MIN and max_deficit_f <= self._MIN_DEFICIT_F:
268
- raise ValueError(
269
- "The primary system is already adequately sized: "
270
- f"outage duration was {sim_run.outage_minutes} min "
271
- f"(threshold {self._MIN_OUTAGE_MIN} min) and max temperature deficit "
272
- f"was {max_deficit_f:.2f} °F (threshold {self._MIN_DEFICIT_F:.1f} °F). "
273
- "No gas backup is required."
274
- )
274
+ control_map = self.water_heaters[0].control_map
275
+ if 'shed' in control_map:
276
+ control_map['shed'] = self.saved_shed_state
277
+ comparison_system = SinglePassRTPSystem.from_size(
278
+ building = building,
279
+ supply_temp_f = self.supply_temp_f,
280
+ storage_temp_f = self.storage_temp_f,
281
+ return_temp_f = self.return_temp_f,
282
+ return_flow_gpm = self.return_flow_gpm,
283
+ max_daily_run_hr = self.max_daily_run_hr,
284
+ defrost_factor = self.defrost_factor,
285
+ tm_safety_factor = self.tm_safety_factor,
286
+ control_schedule = self.water_heaters[0].control_schedule,
287
+ control_map = control_map,
288
+ load_shift_fract_total_vol = 1.0,
289
+ )
290
+ result = {
291
+ "min_capacity_kbtuh": comparison_system._minimum_capacity_kbtuh,
292
+ "min_storage_storageT_gal": comparison_system._minimum_storage_storageT_gal,
293
+ }
294
+ if result["min_capacity_kbtuh"] >= nominal_capacity_kbtuh and result["min_storage_storageT_gal"] >= nominal_storage_gal:
295
+ # redirect user to base model
296
+ self.fallback_system = comparison_system
297
+ return
298
+ else:
299
+
300
+ raise ValueError(
301
+ "The primary system is already adequately sized: "
302
+ f"outage duration was {sim_run.outage_minutes} min "
303
+ f"(threshold {self._MIN_OUTAGE_MIN} min) and max temperature deficit "
304
+ f"was {max_deficit_f:.2f} °F (threshold {self._MIN_DEFICIT_F:.1f} °F). "
305
+ "No gas backup is required."
306
+ )
275
307
 
276
308
  # The amount of storage (supply temp gallons) the gas is able to keep in a worst case scenario
277
309
  design_inlet = self._require_design_inlet_temp(building)
@@ -462,6 +494,6 @@ class SP_RTPInParallelSystem(SinglePassRTPSystem):
462
494
  "tank_temps_f": tank_temps_f,
463
495
  "mode": mode,
464
496
  "tm_heater_output_kbtuh": gas_kbtuh,
465
- "tm_heater_input_kw": gas_kbtuh / _W_TO_KBTUH,
497
+ "tm_heater_input_kw": gas_kbtuh / _BTUH_PER_W,
466
498
  "draw_thru_system_gal": draw_gal,
467
499
  }
@@ -6,7 +6,7 @@ from ecoengine.objects.components.heating.Controls import Controls
6
6
  from ecoengine.objects.components.heating.WaterHeater import WaterHeater
7
7
  from ecoengine.objects.components.storage.EnergyTank import EnergyTank
8
8
  from ecoengine.objects.components.storage.MixedStorageTank import MixedStorageTank
9
- from ecoengine.constants.constants import _RHO_CP, _W_TO_KBTUH
9
+ from ecoengine.constants.constants import _RHO_CP, _BTUH_PER_W
10
10
  from ..utils import (
11
11
  mixing_valve_behavior,
12
12
  size_supplemental_heating_and_storage,
@@ -114,7 +114,10 @@ class SP_RTPInSeriesSystem(SinglePassRTPSystem):
114
114
  SP_RTPInSeriesSystem
115
115
  """
116
116
  # ensure shed turns primary system completely off
117
- control_map = set_dual_fuel_shed_controls(control_map)
117
+ saved_shed_state = None
118
+ if 'shed' in control_map:
119
+ saved_shed_state = control_map.get("shed")
120
+ control_map = set_dual_fuel_shed_controls(control_map)
118
121
 
119
122
  system = cls(
120
123
  water_heaters=[],
@@ -127,6 +130,8 @@ class SP_RTPInSeriesSystem(SinglePassRTPSystem):
127
130
  defrost_factor=defrost_factor,
128
131
  tm_safety_factor=tm_safety_factor,
129
132
  )
133
+ system.saved_shed_state = saved_shed_state
134
+ system.fallback_system = None
130
135
 
131
136
  system._minimum_capacity_kbtuh = nominal_capacity_kbtuh
132
137
  system._minimum_storage_storageT_gal = nominal_storage_gal
@@ -180,9 +185,9 @@ class SP_RTPInSeriesSystem(SinglePassRTPSystem):
180
185
  ----------
181
186
  building : Building
182
187
  nominal_capacity_kbtuh : float
183
- Primary HPWH capacity ceiling [kBTU/hr].
188
+ Primary HPWH capacity of the original undersized system [kBTU/hr].
184
189
  nominal_storage_gal : float
185
- Primary storage volume ceiling [gal at storageT].
190
+ Primary storage volume of the original undersized system [gal at storageT].
186
191
  gas_controls : Controls
187
192
  Controls for the gas backup water heater.
188
193
 
@@ -193,17 +198,47 @@ class SP_RTPInSeriesSystem(SinglePassRTPSystem):
193
198
  and max deficit < 2 °F), meaning no gas backup is needed.
194
199
  """
195
200
  _WINDOW_MIN = 30
201
+ try:
202
+ # --- 1 & 2. Sizing simulation → outage arrays (raises ValueError if no backup needed) ---
203
+ self.outage_volume_gal, self.outage_temp_delta_f = size_supplemental_heating_and_storage(
204
+ primary_system=self,
205
+ building=building,
206
+ nominal_capacity_kbtuh=nominal_capacity_kbtuh,
207
+ )
196
208
 
197
- # --- 1 & 2. Sizing simulation outage arrays (raises ValueError if no backup needed) ---
198
- self.outage_volume_gal, self.outage_temp_delta_f = size_supplemental_heating_and_storage(
199
- primary_system=self,
200
- building=building,
201
- nominal_capacity_kbtuh=nominal_capacity_kbtuh,
202
- )
203
-
204
- # --- 3. Size gas backup components at the default 30-minute window ---
205
- gas_capacity_kbtuh, gas_storage_vol_gal = self._gas_backup_from_window(_WINDOW_MIN)
206
- # TODO add thermal efficiency
209
+ # --- 3. Size gas backup components at the default 30-minute window ---
210
+ gas_capacity_kbtuh, gas_storage_vol_gal = self._gas_backup_from_window(_WINDOW_MIN)
211
+ # TODO add thermal efficiency
212
+ except ValueError as e:
213
+ # Compare against base class to cover window where system is undersized but still passing the simulation
214
+ control_map = self.water_heaters[0].control_map
215
+ if 'shed' in control_map:
216
+ control_map['shed'] = self.saved_shed_state
217
+ comparison_system = SinglePassRTPSystem.from_size(
218
+ building = building,
219
+ supply_temp_f = self.supply_temp_f,
220
+ storage_temp_f = self.storage_temp_f,
221
+ return_temp_f = self.return_temp_f,
222
+ return_flow_gpm = self.return_flow_gpm,
223
+ max_daily_run_hr = self.max_daily_run_hr,
224
+ defrost_factor = self.defrost_factor,
225
+ tm_safety_factor = self.tm_safety_factor,
226
+ control_schedule = self.water_heaters[0].control_schedule,
227
+ control_map = control_map,
228
+ load_shift_fract_total_vol = 1.0,
229
+ )
230
+ result = {
231
+ "min_capacity_kbtuh": comparison_system._minimum_capacity_kbtuh,
232
+ "min_storage_storageT_gal": comparison_system._minimum_storage_storageT_gal,
233
+ }
234
+ if result["min_capacity_kbtuh"] >= nominal_capacity_kbtuh and result["min_storage_storageT_gal"] >= nominal_storage_gal:
235
+ # redirect user to base model
236
+ self.fallback_system = comparison_system
237
+ return
238
+ else:
239
+ raise e
240
+
241
+
207
242
 
208
243
  self._in_series_storage_vol_gal = gas_storage_vol_gal
209
244
  self._in_series_capacity_kbtuh = gas_capacity_kbtuh
@@ -393,6 +428,6 @@ class SP_RTPInSeriesSystem(SinglePassRTPSystem):
393
428
  "mode": mode,
394
429
  "tm_tank_temp_f": self.gas_storage_tank.get_temperature_at_fraction(1.0),
395
430
  "tm_heater_output_kbtuh": gas_kbtuh,
396
- "tm_heater_input_kw": gas_kbtuh / _W_TO_KBTUH,
431
+ "tm_heater_input_kw": gas_kbtuh / _BTUH_PER_W,
397
432
  "delivery_temp_f": gas_top_t,
398
433
  }
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ecoengine
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Requires-Python: >=3.11
5
5
  Requires-Dist: numpy<3.0,>=2.4
6
6
  Requires-Dist: scipy<2.0,>=1.17
7
7
  Requires-Dist: plotly<8.0,>=7.0
8
+ Requires-Dist: kaleido>=1.3.0
8
9
  Provides-Extra: docs
9
10
  Requires-Dist: sphinx>=6; extra == "docs"
10
11
  Requires-Dist: furo; extra == "docs"
@@ -1,6 +1,7 @@
1
1
  numpy<3.0,>=2.4
2
2
  scipy<2.0,>=1.17
3
3
  plotly<8.0,>=7.0
4
+ kaleido>=1.3.0
4
5
 
5
6
  [docs]
6
7
  sphinx>=6
File without changes
File without changes