Skip to content

Commit f4bf392

Browse files
committed
refactor: unify tabular consumer time series interfaces
1 parent 5451eed commit f4bf392

7 files changed

Lines changed: 239 additions & 128 deletions

File tree

src/libecalc/domain/infrastructure/energy_components/legacy_consumer/tabulated/tabular_consumer_function.py

Lines changed: 34 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,27 @@
55
from libecalc.common.list.list_utils import array_to_list
66
from libecalc.common.logger import logger
77
from libecalc.common.units import Unit
8-
from libecalc.common.utils.rates import Rates
98
from libecalc.common.variables import ExpressionEvaluator
109
from libecalc.domain.infrastructure.energy_components.legacy_consumer.consumer_function import (
1110
ConsumerFunction,
1211
ConsumerFunctionResult,
1312
)
14-
from libecalc.domain.infrastructure.energy_components.legacy_consumer.consumer_function.utils import (
15-
apply_condition,
16-
apply_power_loss_factor,
17-
get_condition_from_expression,
18-
get_power_loss_factor_from_expression,
19-
)
20-
from libecalc.domain.infrastructure.energy_components.legacy_consumer.tabulated.common import (
21-
Variable,
22-
VariableExpression,
23-
)
2413
from libecalc.domain.infrastructure.energy_components.legacy_consumer.tabulated.tabular_energy_function import (
2514
TabularEnergyFunction,
2615
)
2716
from libecalc.domain.process.core.results import EnergyFunctionResult
28-
from libecalc.expression import Expression
17+
from libecalc.domain.time_series_power_loss_factor import TimeSeriesPowerLossFactor
18+
from libecalc.domain.time_series_variable import TimeSeriesVariable
2919

3020

3121
class TabularConsumerFunction(ConsumerFunction):
3222
"""
3323
Consumer function based on tabulated energy usage data.
3424
3525
This class evaluates energy usage (power or fuel) for a consumer by:
36-
- Evaluating variable expressions to obtain input values.
37-
- Interpolating tabular data using these values via `TabularEnergyFunction`.
38-
- Optionally applying a condition and a power loss factor.
26+
- Evaluating time series variables (with condition and rate conversion applied).
27+
- Interpolating tabular data using these variable values via `TabularEnergyFunction`.
28+
- Optionally applying a power loss factor.
3929
4030
The result is energy usage in [MW] (electricity) or [Sm3/day] (fuel).
4131
For electricity, power is also included in the result.
@@ -45,9 +35,8 @@ class TabularConsumerFunction(ConsumerFunction):
4535
data (list[list[float]]): Tabular data, one list per header.
4636
energy_usage_adjustment_constant (float): Constant to adjust energy usage.
4737
energy_usage_adjustment_factor (float): Factor to adjust energy usage.
48-
variables_expressions (list[VariableExpression]): Variable expressions to evaluate.
49-
condition_expression (Expression | None): Optional condition for evaluation.
50-
power_loss_factor_expression (Expression | None): Optional power loss factor expression.
38+
variables (list[TimeSeriesVariable]): Variables to evaluate and use for interpolation.
39+
power_loss_factor (TimeSeriesPowerLossFactor | None): Optional power loss factor.
5140
"""
5241

5342
def __init__(
@@ -56,9 +45,8 @@ def __init__(
5645
data: list[list[float]],
5746
energy_usage_adjustment_constant: float,
5847
energy_usage_adjustment_factor: float,
59-
variables_expressions: list[VariableExpression],
60-
condition_expression: Expression | None = None,
61-
power_loss_factor_expression: Expression | None = None,
48+
variables: list[TimeSeriesVariable],
49+
power_loss_factor: TimeSeriesPowerLossFactor | None = None,
6250
):
6351
"""Tabulated consumer function [MW] (energy) or [Sm3/day] (fuel)."""
6452
# Consistency of variables between tabulated_energy_function and variables_expressions must be validated up
@@ -69,21 +57,24 @@ def __init__(
6957
energy_usage_adjustment_constant=energy_usage_adjustment_constant,
7058
energy_usage_adjustment_factor=energy_usage_adjustment_factor,
7159
)
72-
self._variables_expressions = variables_expressions
60+
self._variables = variables
7361

74-
self._condition_expression = condition_expression
7562
# Typically used for power line loss subsea et.c.
76-
self._power_loss_factor_expression = power_loss_factor_expression
63+
self._power_loss_factor = power_loss_factor
7764

7865
def evaluate(
7966
self,
8067
expression_evaluator: ExpressionEvaluator,
8168
regularity: list[float],
8269
) -> ConsumerFunctionResult:
8370
"""
84-
Evaluates the consumer function for given input data.
71+
Evaluates the consumer function for the given input data.
8572
86-
See the class docstring for a detailed description of the evaluation process.
73+
Steps:
74+
1. Evaluates all variables (with condition and rate conversion).
75+
2. Interpolates the tabular energy function using these values.
76+
3. Optionally applies a power loss factor.
77+
4. Returns a result object with energy usage, validity, and related data.
8778
8879
Args:
8980
expression_evaluator (ExpressionEvaluator): Evaluator for variable and condition expressions.
@@ -93,76 +84,41 @@ def evaluate(
9384
ConsumerFunctionResult: Result containing energy usage, validity, and related data.
9485
"""
9586

96-
variables_for_calculation = []
97-
# If some of these are rates, we need to calculate stream day rate for use
98-
# Also take a copy of the calendar day rate and stream day rate for input to result object
99-
for variable in self._variables_expressions:
100-
variable_values = expression_evaluator.evaluate(variable.expression)
101-
if variable.name.lower() == "rate":
102-
variable_values = Rates.to_stream_day(
103-
calendar_day_rates=variable_values,
104-
regularity=regularity,
105-
)
106-
variables_for_calculation.append(Variable(name=variable.name, values=variable_values.tolist()))
107-
10887
energy_function_result = self.evaluate_variables(
109-
variables=variables_for_calculation,
88+
variables=self._variables,
11089
)
11190

112-
condition = get_condition_from_expression(
113-
condition_expression=self._condition_expression,
114-
expression_evaluator=expression_evaluator,
115-
)
116-
# for tabular, is_valid is based on energy_usage being NaN. This will also (correctly) change potential
117-
# invalid points to valid where the condition sets energy_usage to zero
118-
energy_function_result.energy_usage = array_to_list(
119-
apply_condition(
120-
input_array=np.asarray(energy_function_result.energy_usage),
121-
condition=condition,
122-
)
123-
)
124-
energy_function_result.power = (
125-
array_to_list(
126-
apply_condition(
127-
input_array=np.asarray(energy_function_result.power),
128-
condition=condition,
129-
)
91+
# Apply power loss factor if present
92+
if self._power_loss_factor is not None:
93+
energy_usage = self._power_loss_factor.apply(
94+
energy_usage=np.asarray(energy_function_result.energy_usage, dtype=np.float64)
13095
)
131-
if energy_function_result.power is not None
132-
else None
133-
)
134-
135-
power_loss_factor = get_power_loss_factor_from_expression(
136-
expression_evaluator=expression_evaluator,
137-
power_loss_factor_expression=self._power_loss_factor_expression,
138-
)
96+
power_loss_factor = self._power_loss_factor.get_values(length=len(energy_usage))
97+
else:
98+
energy_usage = energy_function_result.energy_usage
99+
power_loss_factor = None
139100

140101
return ConsumerFunctionResult(
141-
periods=expression_evaluator.get_periods(),
102+
periods=self._variables[0].get_periods(),
142103
is_valid=np.asarray(energy_function_result.is_valid),
143104
energy_function_result=energy_function_result,
144-
energy_usage_before_power_loss_factor=np.asarray(energy_function_result.energy_usage),
145-
power_loss_factor=power_loss_factor,
146-
energy_usage=apply_power_loss_factor(
147-
energy_usage=np.asarray(energy_function_result.energy_usage),
148-
power_loss_factor=power_loss_factor,
149-
),
105+
energy_usage_before_power_loss_factor=np.asarray(energy_function_result.energy_usage, dtype=np.float64),
106+
power_loss_factor=np.asarray(power_loss_factor, dtype=np.float64),
107+
energy_usage=np.asarray(energy_usage, dtype=np.float64),
150108
)
151109

152-
def evaluate_variables(self, variables: list[Variable]) -> EnergyFunctionResult:
110+
def evaluate_variables(self, variables: list[TimeSeriesVariable]) -> EnergyFunctionResult:
153111
"""
154-
Interpolates energy usage for the provided variable values.
155-
156-
See the class docstring for a detailed description of the evaluation process.
112+
Interpolates energy usage for the provided variables.
157113
158114
Args:
159-
variables (list[Variable]): List of variables with names and values for interpolation.
115+
variables (list[TimeSeriesVariable]): List of variables to evaluate and use for interpolation.
160116
161117
Returns:
162118
EnergyFunctionResult: Result containing energy usage, units, and power (if applicable).
163119
"""
164120

165-
variables_map_by_name = {variable.name: variable.values for variable in variables}
121+
variables_map_by_name = {variable.name: variable.get_values() for variable in variables}
166122
_check_variables_match_required(
167123
variables_to_evaluate=list(variables_map_by_name.keys()),
168124
required_variables=self._tabular_energy_function.required_variables,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from abc import ABC, abstractmethod
2+
3+
4+
class TimeSeriesVariable(ABC):
5+
@property
6+
@abstractmethod
7+
def name(self) -> str: ...
8+
9+
@abstractmethod
10+
def get_values(self) -> list[float]: ...
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import numpy as np
2+
3+
from libecalc.common.time_utils import Period, Periods
4+
from libecalc.common.utils.rates import Rates
5+
from libecalc.domain.infrastructure.energy_components.legacy_consumer.consumer_function.utils import (
6+
apply_condition,
7+
get_condition_from_expression,
8+
)
9+
from libecalc.domain.regularity import Regularity
10+
from libecalc.domain.time_series_variable import TimeSeriesVariable
11+
from libecalc.expression import Expression
12+
from libecalc.presentation.yaml.domain.time_series_expression import TimeSeriesExpression
13+
14+
15+
class ExpressionTimeSeriesVariable(TimeSeriesVariable):
16+
"""
17+
Wraps a time series expression for use as a variable in tabular consumer functions.
18+
Handles rate conversion and conditional masking.
19+
"""
20+
21+
def __init__(
22+
self,
23+
name: str,
24+
time_series_expression: TimeSeriesExpression,
25+
regularity: Regularity,
26+
is_rate: bool = True,
27+
condition_expression: Expression | dict[Period, Expression] | None = None,
28+
):
29+
self._name = name
30+
self._time_series_expression = time_series_expression
31+
self._regularity = regularity
32+
self._is_rate = is_rate
33+
self.condition = get_condition_from_expression(
34+
expression_evaluator=self._time_series_expression.expression_evaluator,
35+
condition_expression=condition_expression,
36+
)
37+
38+
@property
39+
def name(self) -> str:
40+
return self._name
41+
42+
@property
43+
def is_rate(self) -> bool:
44+
return self._is_rate
45+
46+
def get_values(self) -> list[float]:
47+
values: np.ndarray = np.asarray(self._time_series_expression.get_evaluated_expressions(), dtype=np.float64)
48+
# If some of these are rates, we need to calculate stream day rate for use
49+
# Also take a copy of the calendar day rate and stream day rate for input to result object
50+
51+
if self.is_rate:
52+
values = Rates.to_stream_day(
53+
calendar_day_rates=values,
54+
regularity=self._regularity.values,
55+
)
56+
57+
values = apply_condition(
58+
input_array=values,
59+
condition=self.condition,
60+
)
61+
return values.tolist()
62+
63+
def get_periods(self) -> Periods:
64+
"""
65+
Returns the periods associated with the time series expression.
66+
67+
"""
68+
return self._time_series_expression.expression_evaluator.get_periods()

src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Protocol, assert_never
2+
from typing import Protocol, assert_never, cast
33

44
from libecalc.common.chart_type import ChartType
55
from libecalc.common.consumption_type import ConsumptionType
@@ -32,7 +32,6 @@
3232
from libecalc.domain.infrastructure.energy_components.legacy_consumer.tabulated import (
3333
TabularConsumerFunction,
3434
)
35-
from libecalc.domain.infrastructure.energy_components.legacy_consumer.tabulated.common import VariableExpression
3635
from libecalc.domain.process.compressor.core import create_compressor_model
3736
from libecalc.domain.process.compressor.dto import (
3837
CompressorTrainSimplifiedWithKnownStages,
@@ -43,6 +42,7 @@
4342
from libecalc.domain.process.compressor.dto.model_types import CompressorModelTypes
4443
from libecalc.domain.process.pump.factory import create_pump_model
4544
from libecalc.domain.regularity import Regularity
45+
from libecalc.domain.time_series_variable import TimeSeriesVariable
4646
from libecalc.dto.utils.validators import convert_expression, convert_expressions
4747
from libecalc.expression import Expression
4848
from libecalc.expression.expression import InvalidExpressionError
@@ -53,6 +53,7 @@
5353
ExpressionTimeSeriesPowerLossFactor,
5454
)
5555
from libecalc.presentation.yaml.domain.expression_time_series_pressure import ExpressionTimeSeriesPressure
56+
from libecalc.presentation.yaml.domain.expression_time_series_variable import ExpressionTimeSeriesVariable
5657
from libecalc.presentation.yaml.domain.reference_service import ReferenceService
5758
from libecalc.presentation.yaml.domain.time_series_expression import TimeSeriesExpression
5859
from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords
@@ -256,7 +257,10 @@ def _map_direct(
256257
power_loss_factor=power_loss_factor,
257258
)
258259

259-
def _map_tabular(self, model: YamlEnergyUsageModelTabulated, consumes: ConsumptionType) -> TabularConsumerFunction:
260+
def _map_tabular(
261+
self, model: YamlEnergyUsageModelTabulated, consumes: ConsumptionType, period: Period
262+
) -> TabularConsumerFunction:
263+
period_regularity, period_evaluator = self._period_subsets[period]
260264
energy_model = self.__references.get_tabulated_model(model.energy_function)
261265
energy_usage_type = energy_model.get_energy_usage_type()
262266
energy_usage_type_as_consumption_type = (
@@ -267,22 +271,32 @@ def _map_tabular(self, model: YamlEnergyUsageModelTabulated, consumes: Consumpti
267271
raise InvalidConsumptionType(actual=energy_usage_type_as_consumption_type, expected=consumes)
268272

269273
condition = convert_expression(_map_condition(model))
270-
power_loss_factor = convert_expression(model.power_loss_factor)
274+
power_loss_factor_expression = TimeSeriesExpression(
275+
expressions=model.power_loss_factor, expression_evaluator=period_evaluator
276+
)
277+
power_loss_factor = ExpressionTimeSeriesPowerLossFactor(time_series_expression=power_loss_factor_expression)
278+
279+
variables = [
280+
ExpressionTimeSeriesVariable(
281+
name=variable.name,
282+
time_series_expression=TimeSeriesExpression(
283+
expressions=variable.expression,
284+
expression_evaluator=period_evaluator,
285+
),
286+
regularity=period_regularity,
287+
is_rate=(variable.name.lower() == "rate"),
288+
condition_expression=condition,
289+
)
290+
for variable in model.variables
291+
]
271292

272293
return TabularConsumerFunction(
273294
headers=energy_model.headers,
274295
data=energy_model.data,
275296
energy_usage_adjustment_constant=energy_model.energy_usage_adjustment_constant,
276297
energy_usage_adjustment_factor=energy_model.energy_usage_adjustment_factor,
277-
variables_expressions=[
278-
VariableExpression(
279-
name=variable.name,
280-
expression=convert_expression(variable.expression), # type: ignore[arg-type]
281-
)
282-
for variable in model.variables
283-
],
284-
condition_expression=condition, # type: ignore[arg-type]
285-
power_loss_factor_expression=power_loss_factor, # type: ignore[arg-type]
298+
variables=cast(list[TimeSeriesVariable], variables),
299+
power_loss_factor=power_loss_factor,
286300
)
287301

288302
def _map_pump(
@@ -584,7 +598,7 @@ def from_yaml_to_dto(
584598
elif isinstance(model, YamlEnergyUsageModelPumpSystem):
585599
mapped_model = self._map_pump_system(model, consumes=consumes)
586600
elif isinstance(model, YamlEnergyUsageModelTabulated):
587-
mapped_model = self._map_tabular(model=model, consumes=consumes)
601+
mapped_model = self._map_tabular(model=model, consumes=consumes, period=period)
588602
elif isinstance(model, YamlEnergyUsageModelCompressorTrainMultipleStreams):
589603
mapped_model = self._map_multiple_streams_compressor(model, consumes=consumes)
590604
else:

0 commit comments

Comments
 (0)