ecoengine 3.3.2__tar.gz → 3.3.3__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 (82) hide show
  1. {ecoengine-3.3.2 → ecoengine-3.3.3}/PKG-INFO +1 -1
  2. {ecoengine-3.3.2 → ecoengine-3.3.3}/pyproject.toml +1 -1
  3. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/building/UtilityCostTracker.py +21 -0
  4. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/simulation/SimulationRun.py +67 -13
  5. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine.egg-info/PKG-INFO +1 -1
  6. {ecoengine-3.3.2 → ecoengine-3.3.3}/README.md +0 -0
  7. {ecoengine-3.3.2 → ecoengine-3.3.3}/setup.cfg +0 -0
  8. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/__init__.py +0 -0
  9. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/constants/__init__.py +0 -0
  10. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/constants/constants.py +0 -0
  11. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/__init__.py +0 -0
  12. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/climate_data/DryBulbTemperatures_ByClimateZone.csv +0 -0
  13. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/climate_data/InletWaterTemperatures_ByClimateZone.csv +0 -0
  14. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/climate_data/WeatherStation_ClimateZone_Lookup.csv +0 -0
  15. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/climate_data/ZipCode_ClimateZone_Lookup.csv +0 -0
  16. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/climate_data/__init__.py +0 -0
  17. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/climate_data/kGperkWh_ByClimateZone.csv +0 -0
  18. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/__init__.py +0 -0
  19. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/apartment.json +0 -0
  20. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/elementary_school.json +0 -0
  21. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/food_service_a.json +0 -0
  22. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/food_service_b.json +0 -0
  23. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/junior_high.json +0 -0
  24. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/mens_dorm.json +0 -0
  25. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/motel.json +0 -0
  26. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/multi_family.json +0 -0
  27. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/nursing_home.json +0 -0
  28. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/office_building.json +0 -0
  29. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/senior_high.json +0 -0
  30. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/load_shapes/womens_dorm.json +0 -0
  31. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/__init__.py +0 -0
  32. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/maps.json +0 -0
  33. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH3_SP_bounds.pkl +0 -0
  34. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH3_SP_capacity_interpolator.pkl +0 -0
  35. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH3_SP_power_in_interpolator.pkl +0 -0
  36. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH4_MP_bounds.pkl +0 -0
  37. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH4_MP_capacity_interpolator.pkl +0 -0
  38. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/pkls/GenericHPWH4_MP_power_in_interpolator.pkl +0 -0
  39. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/data/preformanceMaps/pkls/__init__.py +0 -0
  40. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/interfaces/EcoBuilder.py +0 -0
  41. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/interfaces/EcosizerEngine.py +0 -0
  42. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/interfaces/Simulator.py +0 -0
  43. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/interfaces/__init__.py +0 -0
  44. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/interfaces/system_assembly_utils.py +0 -0
  45. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/__init__.py +0 -0
  46. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/building/Building.py +0 -0
  47. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/building/ClimateZone.py +0 -0
  48. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/building/__init__.py +0 -0
  49. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/__init__.py +0 -0
  50. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/heating/Controls.py +0 -0
  51. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/heating/PerformanceMap.py +0 -0
  52. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/heating/WaterHeater.py +0 -0
  53. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/heating/__init__.py +0 -0
  54. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/storage/EnergyTank.py +0 -0
  55. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/storage/MixedStorageTank.py +0 -0
  56. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/storage/SlugOverlayTank.py +0 -0
  57. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/storage/StorageTank.py +0 -0
  58. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/storage/StratifiedTank.py +0 -0
  59. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/components/storage/__init__.py +0 -0
  60. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/DHWSystem.py +0 -0
  61. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/InstantWHSystem.py +0 -0
  62. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/MPNoRecircSystem.py +0 -0
  63. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/__init__.py +0 -0
  64. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/recirc_systems/ParallelLoopSystem.py +0 -0
  65. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/recirc_systems/RecircSystem.py +0 -0
  66. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/recirc_systems/SwingDualFuelSystem.py +0 -0
  67. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/recirc_systems/SwingERTrdOffSystem.py +0 -0
  68. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/recirc_systems/SwingSystem.py +0 -0
  69. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/recirc_systems/__init__.py +0 -0
  70. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/rtp_systems/MP_RTPInSeriesSystem.py +0 -0
  71. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/rtp_systems/MultiPassRTPSystem.py +0 -0
  72. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/rtp_systems/RTPSystem.py +0 -0
  73. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/rtp_systems/SP_RTPInParallelSystem.py +0 -0
  74. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/rtp_systems/SP_RTPInSeriesSystem.py +0 -0
  75. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/rtp_systems/SinglePassRTPSystem.py +0 -0
  76. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/rtp_systems/__init__.py +0 -0
  77. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/dhwsystems/utils.py +0 -0
  78. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine/objects/simulation/__init__.py +0 -0
  79. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine.egg-info/SOURCES.txt +0 -0
  80. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine.egg-info/dependency_links.txt +0 -0
  81. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine.egg-info/requires.txt +0 -0
  82. {ecoengine-3.3.2 → ecoengine-3.3.3}/src/ecoengine.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ecoengine
3
- Version: 3.3.2
3
+ Version: 3.3.3
4
4
  Requires-Python: >=3.11
5
5
  Requires-Dist: numpy<3.0,>=2.4
6
6
  Requires-Dist: scipy<2.0,>=1.17
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ecoengine"
7
- version = "3.3.2"
7
+ version = "3.3.3"
8
8
  requires-python = ">=3.11"
9
9
 
10
10
  # Ranges are bounded on purpose, and the shared ones are kept in sync with
@@ -260,6 +260,13 @@ class UtilityCostTracker:
260
260
  )
261
261
 
262
262
  uc = UtilityCostTracker.from_csv("utility_rates.csv")
263
+
264
+ Demand interval
265
+ ---------------
266
+ ``demand_interval_min`` (default 15) is the averaging window for demand
267
+ charges: the billed demand of a period is the highest average kW over any
268
+ ``demand_interval_min``-minute window that lies entirely inside that
269
+ period (see ``SimulationRun.get_max_demand_kw_by_period``).
263
270
  """
264
271
 
265
272
  def __init__(
@@ -272,7 +279,12 @@ class UtilityCostTracker:
272
279
  monthly_base_charge: float,
273
280
  energy_charge_by_hour: list[float] | None = None,
274
281
  include_discount: bool = False,
282
+ demand_interval_min: int = 15,
275
283
  ) -> None:
284
+ if not isinstance(demand_interval_min, int) or isinstance(demand_interval_min, bool) or demand_interval_min <= 0:
285
+ raise ValueError(
286
+ f"demand_interval_min must be a positive integer number of minutes, got {demand_interval_min!r}."
287
+ )
276
288
  self.demand_period_chart = demand_period_chart
277
289
  self.demand_charge_map = demand_charge_map
278
290
  self.energy_charge_map = energy_charge_map
@@ -281,6 +293,7 @@ class UtilityCostTracker:
281
293
  self.monthly_base_charge = monthly_base_charge
282
294
  self.energy_charge_by_hour: list[float] = energy_charge_by_hour or []
283
295
  self.include_discount = include_discount
296
+ self.demand_interval_min = demand_interval_min
284
297
 
285
298
  # ------------------------------------------------------------------
286
299
  # Factory constructors
@@ -303,6 +316,7 @@ class UtilityCostTracker:
303
316
  dscnt_end_hour=None,
304
317
  discnt_demand_charge=None,
305
318
  discnt_energy_charge=None,
319
+ demand_interval_min: int = 15,
306
320
  ) -> UtilityCostTracker:
307
321
  """
308
322
  Build a UtilityCostTracker from rate parameters.
@@ -348,6 +362,8 @@ class UtilityCostTracker:
348
362
  Discount demand charge [$/kW].
349
363
  discnt_energy_charge : float or list[float] or None
350
364
  Discount energy rate [$/kWh].
365
+ demand_interval_min : int
366
+ Averaging window for demand charges [minutes]. Default 15.
351
367
 
352
368
  Returns
353
369
  -------
@@ -414,6 +430,7 @@ class UtilityCostTracker:
414
430
  is_discount_map=is_discount_map,
415
431
  monthly_base_charge=monthly_base_charge,
416
432
  include_discount=include_discount,
433
+ demand_interval_min=demand_interval_min,
417
434
  )
418
435
 
419
436
  @classmethod
@@ -421,6 +438,7 @@ class UtilityCostTracker:
421
438
  cls,
422
439
  csv_path: str | None = None,
423
440
  csv_file: TextIOWrapper | None = None,
441
+ demand_interval_min: int = 15,
424
442
  ) -> UtilityCostTracker:
425
443
  """
426
444
  Build a UtilityCostTracker from an 8760-row CSV file.
@@ -439,6 +457,8 @@ class UtilityCostTracker:
439
457
  ----------
440
458
  csv_path : str or None
441
459
  csv_file : TextIOWrapper or None
460
+ demand_interval_min : int
461
+ Averaging window for demand charges [minutes]. Default 15.
442
462
 
443
463
  Returns
444
464
  -------
@@ -554,6 +574,7 @@ class UtilityCostTracker:
554
574
  is_discount_map=is_discount_map,
555
575
  monthly_base_charge=monthly_base_charge,
556
576
  energy_charge_by_hour=energy_charge_by_hour,
577
+ demand_interval_min=demand_interval_min,
557
578
  )
558
579
 
559
580
  # ------------------------------------------------------------------
@@ -1,5 +1,8 @@
1
+ import math
1
2
  import numbers
2
3
 
4
+ import numpy as np
5
+
3
6
  from ecoengine.constants.constants import _RHO_CP
4
7
 
5
8
  _ANNUAL_DURATION_MIN = 365 * 24 * 60 # 525600
@@ -693,11 +696,71 @@ class SimulationRun:
693
696
 
694
697
  return fig
695
698
 
699
+ def get_max_demand_kw_by_period(self, uc) -> dict[int, float]:
700
+ """
701
+ Return the billed demand [kW] of each demand period: the highest
702
+ average combined kW (primary + TM / gas) over any
703
+ ``uc.demand_interval_min``-minute window that lies entirely inside
704
+ that period.
705
+
706
+ Windows slide at the finer of the simulation timestep and the demand
707
+ interval: each timestep is split into sub-steps of
708
+ ``gcd(timestep_min, demand_interval_min)`` minutes (e.g. 5 min for a
709
+ 10-min simulation with a 15-min interval), and every window start on
710
+ that grid is tried. A window that spans two different demand periods
711
+ is ignored. A period with no complete window inside the simulated
712
+ time gets no entry, so it has no demand charge.
713
+
714
+ Parameters
715
+ ----------
716
+ uc : UtilityCostTracker
717
+
718
+ Returns
719
+ -------
720
+ dict[int, float]
721
+ ``{period_key: max windowed average kW}``.
722
+ """
723
+ n_steps = len(self.heater_power_in_kw)
724
+ interval_min = uc.demand_interval_min
725
+ sub_min = math.gcd(self.timestep_min, interval_min)
726
+ win = interval_min // sub_min # sub-steps per window
727
+
728
+ # Power and demand period on the sub-step grid
729
+ kw = np.repeat(
730
+ np.array([self._combined_kw(i) for i in range(n_steps)], dtype=float),
731
+ self.timestep_min // sub_min,
732
+ )
733
+ n_sub = len(kw)
734
+ if n_sub < win:
735
+ return {}
736
+ chart = np.asarray(uc.demand_period_chart)
737
+ hours = (np.arange(n_sub) * sub_min) // 60
738
+ periods = chart[hours % len(chart)]
739
+
740
+ # Average kW of the window starting at each sub-step
741
+ csum = np.concatenate(([0.0], np.cumsum(kw)))
742
+ n_win = n_sub - win + 1
743
+ avg_kw = (csum[win:] - csum[:n_win]) / win
744
+
745
+ # A window is valid when its period never changes inside it
746
+ changes = np.concatenate(([0], np.cumsum(periods[1:] != periods[:-1])))
747
+ starts = np.arange(n_win)
748
+ valid = changes[starts + win - 1] == changes[starts]
749
+
750
+ start_periods = periods[:n_win]
751
+ max_kw_by_period: dict[int, float] = {}
752
+ for period in np.unique(start_periods[valid]):
753
+ mask = valid & (start_periods == period)
754
+ max_kw_by_period[int(period)] = float(avg_kw[mask].max())
755
+ return max_kw_by_period
756
+
696
757
  def get_annual_utility_cost(self, uc) -> float:
697
758
  """
698
759
  Compute total annual utility cost from this simulation run.
699
760
 
700
- Cost = energy charges + demand charges (max kW per period) + yearly base.
761
+ Cost = energy charges + demand charges + yearly base. Demand charges
762
+ use each period's highest ``uc.demand_interval_min``-minute average kW
763
+ (see ``get_max_demand_kw_by_period``).
701
764
 
702
765
  Parameters
703
766
  ----------
@@ -711,17 +774,13 @@ class SimulationRun:
711
774
  """
712
775
  hours_per_step = self.timestep_min / 60.0
713
776
  energy_cost = 0.0
714
- max_kw_by_period: dict[int, float] = {}
715
777
 
716
778
  for i in range(len(self.heater_power_in_kw)):
717
779
  kw = self._combined_kw(i)
718
780
  rate = uc.get_energy_charge_at_step(i, self.timestep_min)
719
781
  energy_cost += kw * hours_per_step * rate
720
782
 
721
- period = uc.get_demand_period_at_step(i, self.timestep_min)
722
- if period not in max_kw_by_period or kw > max_kw_by_period[period]:
723
- max_kw_by_period[period] = kw
724
-
783
+ max_kw_by_period = self.get_max_demand_kw_by_period(uc)
725
784
  demand_cost = sum(
726
785
  uc.get_demand_charge_for_period(p, max_kw)
727
786
  for p, max_kw in max_kw_by_period.items()
@@ -745,7 +804,6 @@ class SimulationRun:
745
804
  """
746
805
  hours_per_step = self.timestep_min / 60.0
747
806
  energy_by_month = [0.0] * 12
748
- max_kw_by_period: dict[int, float] = {}
749
807
 
750
808
  for i in range(len(self.heater_power_in_kw)):
751
809
  kw = self._combined_kw(i)
@@ -753,9 +811,7 @@ class SimulationRun:
753
811
  energy_by_month[_step_to_month(i, self.timestep_min)] += (
754
812
  kw * hours_per_step * rate
755
813
  )
756
- period = uc.get_demand_period_at_step(i, self.timestep_min)
757
- if period not in max_kw_by_period or kw > max_kw_by_period[period]:
758
- max_kw_by_period[period] = kw
814
+ max_kw_by_period = self.get_max_demand_kw_by_period(uc)
759
815
 
760
816
  # Assign each period's demand charge to its calendar month.
761
817
  # Period key = month * num_periods + offset → month = key // num_periods.
@@ -786,7 +842,6 @@ class SimulationRun:
786
842
  hours_per_step = self.timestep_min / 60.0
787
843
  energy_peak = [0.0] * 12
788
844
  energy_off_peak = [0.0] * 12
789
- max_kw_by_period: dict[int, float] = {}
790
845
 
791
846
  for i in range(len(self.heater_power_in_kw)):
792
847
  kw = self._combined_kw(i)
@@ -798,8 +853,7 @@ class SimulationRun:
798
853
  energy_peak[month] += cost
799
854
  else:
800
855
  energy_off_peak[month] += cost
801
- if period not in max_kw_by_period or kw > max_kw_by_period[period]:
802
- max_kw_by_period[period] = kw
856
+ max_kw_by_period = self.get_max_demand_kw_by_period(uc)
803
857
 
804
858
  num_periods = 3 if uc.include_discount else 2
805
859
  demand_peak = [0.0] * 12
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ecoengine
3
- Version: 3.3.2
3
+ Version: 3.3.3
4
4
  Requires-Python: >=3.11
5
5
  Requires-Dist: numpy<3.0,>=2.4
6
6
  Requires-Dist: scipy<2.0,>=1.17
File without changes
File without changes