Skip to content

Commit

Permalink
feat: add support for temporal operational settings in v2
Browse files Browse the repository at this point in the history
* Add support for temporal operational settings in the new consumer system v2
* Add tests for different combinations/permutations of ecalc models for v2, with temporal models for both consumers and operational settings.
* Change to evaluate consumer systems by timestep instead of by period (in order to make sure e.g. overlapping periods are handled correctly. may change to periods later, as it would be easier to know that the logic is handled correctly by doing by timesteps initially. This also allows us to compare different permuations with the same data to be comparable.
* Fixes small bug related to handling regularity, where fallback to regularity 1 occured instead of using actualy regularity in some cases. To be followed up with improvements in follow-up PRs.
* Typos in pump_system, where compressor_system names and references were being used.
  • Loading branch information
TeeeJay authored Sep 18, 2023
1 parent 9428979 commit f2b217a
Show file tree
Hide file tree
Showing 20 changed files with 16,873 additions and 333 deletions.
39 changes: 36 additions & 3 deletions src/ecalc/libraries/libecalc/common/libecalc/common/utils/rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ def for_period(self, period: Period) -> Self:
unit=self.unit,
)

def for_timestep(self, current_timestep: datetime) -> Self:
"""
Get the timeseries data for the single timestep given
:param current_timestep:
:return: A timeseries with a single step/value corresponding to the timestep given
"""
timestep_index = self.timesteps.index(current_timestep)

return self.__class__(
timesteps=self.timesteps[timestep_index : timestep_index + 1],
values=self.values[timestep_index : timestep_index + 1],
unit=self.unit,
)

def to_unit(self, unit: Unit) -> Self:
if unit == self.unit:
return self.copy()
Expand Down Expand Up @@ -586,11 +600,13 @@ class TimeSeriesRate(TimeSeries[float]):
"""

rate_type: Optional[RateType] = RateType.STREAM_DAY
regularity: Optional[List[float]]
regularity: Optional[List[float]] # TODO: Consider to set explicitly as a fallback to 1 may easily lead to errors

@validator("regularity", pre=True, always=True)
def set_regularity(cls, regularity: Optional[List[float]], values: Dict[str, Any]) -> List[float]:
if regularity is not None:
if (
regularity is not None and regularity != []
): # TODO: Current workaround. To be handled when regularity is handled correctly
return regularity
try:
return [1] * len(values["values"])
Expand Down Expand Up @@ -663,6 +679,21 @@ def for_period(self, period: Period) -> Self:
rate_type=self.rate_type,
)

def for_timestep(self, current_timestep: datetime) -> Self:
"""
Get the timeseries data for the single timestep given
:param current_timestep:
:return: A timeseries with a single step/value corresponding to the timestep given
"""
timestep_index = self.timesteps.index(current_timestep)
return self.__class__(
timesteps=self.timesteps[timestep_index : timestep_index + 1],
values=self.values[timestep_index : timestep_index + 1],
regularity=self.regularity[timestep_index : timestep_index + 1], # type: ignore
unit=self.unit,
rate_type=self.rate_type,
)

def to_calendar_day(self) -> Self:
"""Convert rates to calendar day rates."""
if self.rate_type == RateType.CALENDAR_DAY:
Expand Down Expand Up @@ -826,4 +857,6 @@ def reindex(self, new_time_vector: Iterable[datetime]) -> TimeSeriesRate:
Ensure to map correct value to correct timestep in the final resulting time vector.
"""
reindex_values = self.reindex_time_vector(new_time_vector)
return TimeSeriesRate(timesteps=new_time_vector, values=reindex_values.tolist(), unit=self.unit)
return TimeSeriesRate(
timesteps=new_time_vector, values=reindex_values.tolist(), unit=self.unit, regularity=self.regularity
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,20 @@ def __init__(self, id: str, energy_usage_model: Dict[datetime, dto.CompressorMod
self._operational_settings: Optional[CompressorOperationalSettings] = None

def get_max_rate(self, operational_settings: CompressorOperationalSettings) -> List[float]:
"""
For each timestep, get the maximum rate that this compressor can handle, given
the operational settings given, such as in -and outlet pressures (current conditions)
:param operational_settings:
:return:
"""
results = []
for period, compressor in self._temporal_model.items():
operational_settings_this_period = operational_settings.get_subset_from_period(period)
for timestep in operational_settings.timesteps:
compressor = self._temporal_model.get_model(timestep)
operational_settings_this_timestep = operational_settings.get_subset_for_timestep(timestep)
results.extend(
compressor.get_max_standard_rate(
suction_pressures=np.asarray(operational_settings_this_period.inlet_pressure.values),
discharge_pressures=np.asarray(operational_settings_this_period.outlet_pressure.values),
suction_pressures=np.asarray(operational_settings_this_timestep.inlet_pressure.values),
discharge_pressures=np.asarray(operational_settings_this_timestep.outlet_pressure.values),
).tolist()
)
return results
Expand All @@ -64,22 +71,23 @@ def evaluate(
"""
self._operational_settings = operational_settings

# Regularity is the same for all rate vectors.
# In case of cross-overs or multiple streams, there may be multiple rate vectors.
regularity = operational_settings.stream_day_rates[0].regularity

model_results = []
evaluated_timesteps = []
for period, compressor in self._temporal_model.items():
operational_settings_this_period = operational_settings.get_subset_from_period(period)
evaluated_timesteps.extend(operational_settings_this_period.timesteps)

# TODO: This is a false assumption and will be dealt with shortly (that the regularity is the same
# for all timesteps, and only taken for the first timestep)
evaluated_regularity = operational_settings.stream_day_rates[0].regularity
for timestep in operational_settings.timesteps:
compressor = self._temporal_model.get_model(timestep)
operational_settings_for_timestep = operational_settings.get_subset_for_timestep(timestep)
evaluated_timesteps.extend(operational_settings_for_timestep.timesteps)
if isinstance(compressor, VariableSpeedCompressorTrainCommonShaftMultipleStreamsAndPressures):
raise NotImplementedError("Need to implement this")
elif issubclass(type(compressor), CompressorModel):
model_result = compressor.evaluate_rate_ps_pd(
rate=np.sum([rate.values for rate in operational_settings_this_period.stream_day_rates], axis=0),
suction_pressure=np.asarray(operational_settings_this_period.inlet_pressure.values),
discharge_pressure=np.asarray(operational_settings_this_period.outlet_pressure.values),
rate=np.sum([rate.values for rate in operational_settings_for_timestep.stream_day_rates], axis=0),
suction_pressure=np.asarray(operational_settings_for_timestep.inlet_pressure.values),
discharge_pressure=np.asarray(operational_settings_for_timestep.outlet_pressure.values),
)
model_results.append(model_result)

Expand All @@ -94,7 +102,7 @@ def evaluate(
values=aggregated_result.energy_usage,
timesteps=evaluated_timesteps,
unit=aggregated_result.energy_usage_unit,
regularity=regularity,
regularity=evaluated_regularity,
)

if energy_usage.unit == Unit.STANDARD_CUBIC_METER_PER_DAY:
Expand All @@ -106,7 +114,7 @@ def evaluate(
values=aggregated_result.power,
timesteps=evaluated_timesteps,
unit=aggregated_result.power_unit,
regularity=regularity,
regularity=evaluated_regularity,
).fill_nan(0.0),
energy_usage=energy_usage.fill_nan(0.0),
is_valid=TimeSeriesBoolean(
Expand All @@ -117,7 +125,7 @@ def evaluate(
values=aggregated_result.recirculation_loss,
timesteps=evaluated_timesteps,
unit=Unit.MEGA_WATT,
regularity=regularity,
regularity=evaluated_regularity,
),
rate_exceeds_maximum=TimeSeriesBoolean(
values=aggregated_result.rate_exceeds_maximum,
Expand Down Expand Up @@ -149,15 +157,15 @@ def evaluate(
timesteps=evaluated_timesteps,
values=aggregated_result.power,
unit=aggregated_result.power_unit,
regularity=regularity,
regularity=evaluated_regularity,
)
if aggregated_result.power is not None
else None,
energy_usage=TimeSeriesRate(
timesteps=evaluated_timesteps,
values=aggregated_result.energy_usage,
unit=aggregated_result.energy_usage_unit,
regularity=regularity,
regularity=evaluated_regularity,
),
energy_usage_unit=aggregated_result.energy_usage_unit,
rate_sm3_day=aggregated_result.rate_sm3_day,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
CompressorOperationalSettings,
)
from libecalc.dto.core_specs.pump.operational_settings import PumpOperationalSettings
from libecalc.dto.core_specs.system.operational_settings import (
EvaluatedCompressorSystemOperationalSettings,
EvaluatedPumpSystemOperationalSettings,
)
from numpy.typing import NDArray

Consumer = TypeVar("Consumer", bound=Union[Compressor, Pump])
Expand Down Expand Up @@ -66,7 +70,11 @@ def __init__(self, id: str, consumers: List[ConsumerComponent]):

def _get_operational_settings_adjusted_for_crossover(
self,
operational_setting: SystemOperationalSettings,
operational_setting: Union[
SystemOperationalSettings,
EvaluatedPumpSystemOperationalSettings,
EvaluatedCompressorSystemOperationalSettings,
],
variables_map: VariablesMap,
) -> List[ConsumerOperationalSettings]:
"""
Expand Down Expand Up @@ -105,11 +113,14 @@ def _get_operational_settings_adjusted_for_crossover(

crossover_rates_map[crossover_to].append(list(crossover_rate))

# TODO: Assume regularity same for all now (false assumption, to be handled shortly)
regularity = consumer_operational_settings.stream_day_rates[0].regularity
consumer_operational_settings.stream_day_rates = [
TimeSeriesRate(
values=rate,
timesteps=variables_map.time_vector,
unit=Unit.STANDARD_CUBIC_METER_PER_DAY,
regularity=regularity,
)
for rate in [*rates, *crossover_rates_map[consumer_index]]
]
Expand All @@ -122,7 +133,15 @@ def _get_operational_settings_adjusted_for_crossover(
def evaluate(
self,
variables_map: VariablesMap,
temporal_operational_settings: TemporalModel[List[SystemOperationalSettings]],
temporal_operational_settings: TemporalModel[
List[
Union[
SystemOperationalSettings,
EvaluatedPumpSystemOperationalSettings,
EvaluatedCompressorSystemOperationalSettings,
]
]
],
) -> EcalcModelResult:
"""
Evaluating a consumer system that may be composed of both consumers and other consumer systems. It will default
Expand All @@ -138,34 +157,37 @@ def evaluate(
operational_settings_used = TimeSeriesInt(
timesteps=variables_map.time_vector, values=[0] * len(variables_map.time_vector), unit=Unit.NONE
)
operational_settings_results: Dict[int, Dict[str, EcalcModelResult]] = defaultdict(
operational_settings_results: Dict[datetime, Dict[int, Dict[str, EcalcModelResult]]] = defaultdict(
dict
) # map operational settings index and consumer id to consumer result.

for period, operational_settings in temporal_operational_settings.items():
variables_map_for_period = variables_map.get_subset_from_period(period)
start_index, end_index = period.get_timestep_indices(variables_map.time_vector)
for timestep_index, timestep in enumerate(variables_map.time_vector):
variables_map_for_timestep = variables_map.get_subset_for_timestep(timestep)
operational_settings_results[timestep] = defaultdict(dict)

operational_settings = temporal_operational_settings.get_model(timestep)
for operational_setting_index, operational_setting in enumerate(operational_settings):
operational_setting_for_timestep = operational_setting.for_timestep(timestep)
adjusted_operational_settings: List[
ConsumerOperationalSettings
] = self._get_operational_settings_adjusted_for_crossover(
operational_setting=operational_setting,
variables_map=variables_map_for_period,
operational_setting=operational_setting_for_timestep,
variables_map=variables_map_for_timestep,
)

for consumer, adjusted_operational_setting in zip(self._consumers, adjusted_operational_settings):
consumer_result = consumer.evaluate(adjusted_operational_setting)
operational_settings_results[operational_setting_index][consumer.id] = consumer_result
operational_settings_results[timestep][operational_setting_index][consumer.id] = consumer_result

consumer_results = operational_settings_results[operational_setting_index].values()
consumer_results = operational_settings_results[timestep][operational_setting_index].values()

# Check if consumers are valid for this operational setting, should be valid for all consumers
all_consumer_results_valid = reduce(
operator.mul, [consumer_result.component_result.is_valid for consumer_result in consumer_results]
)
all_consumer_results_valid_indices = np.nonzero(all_consumer_results_valid.values)[0]
all_consumer_results_valid_indices_period_shifted = [
axis_indices + start_index for axis_indices in all_consumer_results_valid_indices
axis_indices + timestep_index for axis_indices in all_consumer_results_valid_indices
]

# Remove already valid indices, so we don't overwrite operational setting used with the latest valid
Expand Down Expand Up @@ -219,7 +241,7 @@ def evaluate(
def collect_consumer_results(
self,
operational_settings_used: TimeSeriesInt,
operational_settings_results: Dict[int, Dict[str, EcalcModelResult]],
operational_settings_results: Dict[datetime, Dict[int, Dict[str, EcalcModelResult]]],
) -> List[Union[CompressorResult, PumpResult]]:
"""
Merge consumer results into a single result per consumer based on the operational settings used. I.e. pick results
Expand All @@ -232,17 +254,14 @@ def collect_consumer_results(
"""
consumer_results: Dict[str, Union[CompressorResult, PumpResult]] = {}
for unique_operational_setting in set(operational_settings_used.values):
operational_settings_used_indices = [
index
for index, operational_setting_used in enumerate(operational_settings_used.values)
if operational_setting_used == unique_operational_setting
]
for consumer in self._consumers:
for consumer in self._consumers:
for timestep_index, timestep in enumerate(operational_settings_used.timesteps):
operational_setting_used = operational_settings_used.values[timestep_index]
prev_result = consumer_results.get(consumer.id)
consumer_result_subset = operational_settings_results[unique_operational_setting][
consumer_result_subset = operational_settings_results[timestep][operational_setting_used][
consumer.id
].component_result.get_subset(operational_settings_used_indices)
].component_result

if prev_result is None:
consumer_results[consumer.id] = consumer_result_subset
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,21 @@ def __init__(self, id: str, energy_usage_model: Dict[datetime, dto.PumpModel]):
self._operational_settings = None

def get_max_rate(self, operational_settings: PumpOperationalSettings) -> List[float]:
"""
For each timestep, get the maximum rate that this pump can handle, given
the operational settings given, such as in -and outlet pressures and fluid density (current conditions)
:param operational_settings:
:return:
"""
results = []
for period, pump in self._temporal_model.items():
operational_settings_this_period = operational_settings.get_subset_from_period(period)
for timestep in operational_settings.timesteps:
pump = self._temporal_model.get_model(timestep)
operational_settings_this_timestep = operational_settings.get_subset_for_timestep(timestep)
results.extend(
pump.get_max_standard_rate(
suction_pressures=np.asarray(operational_settings_this_period.inlet_pressure.values),
discharge_pressures=np.asarray(operational_settings_this_period.outlet_pressure.values),
fluid_density=np.asarray(operational_settings_this_period.fluid_density.values),
suction_pressures=np.asarray(operational_settings_this_timestep.inlet_pressure.values),
discharge_pressures=np.asarray(operational_settings_this_timestep.outlet_pressure.values),
fluid_density=np.asarray(operational_settings_this_timestep.fluid_density.values),
).tolist()
)
return results
Expand All @@ -58,18 +65,20 @@ def evaluate(

# Regularity is the same for all rate vectors.
# In case of cross-overs or multiple streams, there may be multiple rate vectors.
# TODO: False assumption, to be handled shortly
regularity = operational_settings.stream_day_rates[0].regularity

model_results = []
evaluated_timesteps = []
for period, pump in self._temporal_model.items():
operational_settings_this_period = operational_settings.get_subset_from_period(period)
evaluated_timesteps.extend(operational_settings_this_period.timesteps)
for timestep in operational_settings.timesteps:
pump = self._temporal_model.get_model(timestep)
operational_settings_for_timestep = operational_settings.get_subset_for_timestep(timestep)
evaluated_timesteps.extend(operational_settings_for_timestep.timesteps)
model_result = pump.evaluate_rate_ps_pd_density(
rate=np.sum([rate.values for rate in operational_settings_this_period.stream_day_rates], axis=0),
suction_pressures=np.asarray(operational_settings_this_period.inlet_pressure.values),
discharge_pressures=np.asarray(operational_settings_this_period.outlet_pressure.values),
fluid_density=np.asarray(operational_settings_this_period.fluid_density.values),
rate=np.sum([rate.values for rate in operational_settings_for_timestep.stream_day_rates], axis=0),
suction_pressures=np.asarray(operational_settings_for_timestep.inlet_pressure.values),
discharge_pressures=np.asarray(operational_settings_for_timestep.outlet_pressure.values),
fluid_density=np.asarray(operational_settings_for_timestep.fluid_density.values),
)
model_results.append(model_result)

Expand Down
Loading

0 comments on commit f2b217a

Please sign in to comment.