Skip to content

Commit 414f957

Browse files
committed
feat(libecalc): add closed-form liquid pump process simulation
Introduce a liquid pump process graph under a dedicated PUMP_PROCESS_SIMULATIONS YAML keyword, kept separate from the gas FluidStream pipeline. - Extend the closed-form Pump result with efficiency, specific shaft work, shaft power and speed at the operating point (PumpEvaluationResult). - Add PumpProcessSimulation: a fixed liquid graph of inlet, recirculation mixer, pump, recirculation splitter, choke and outlet with typed LiquidProcessUnit entities, connections and a recirculation loop. The domain is time-agnostic: evaluate() maps a sequence of PumpOperatingInput to a liquid stream on every connection plus the pump result, in order. - Add the PUMP_PROCESS_SIMULATIONS "TYPE: PUMP" YAML root, its mapper and YamlModel.get_pump_process_simulations(). Refs: equinor/ecalc-internal#2059
1 parent d91dd6b commit 414f957

14 files changed

Lines changed: 876 additions & 100 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from libecalc.common.errors.ecalc_validation_error import EcalcValidationException
2+
from libecalc.common.errors.exceptions import InvalidResourceException
3+
from libecalc.common.time_utils import Period
4+
from libecalc.common.variables import ExpressionEvaluator
5+
from libecalc.domain.process.value_objects.chart.chart import ChartData
6+
from libecalc.domain.regularity import Regularity
7+
from libecalc.domain.resource import Resources
8+
from libecalc.presentation.yaml.domain.expression_time_series_flow_rate import ExpressionTimeSeriesFlowRate
9+
from libecalc.presentation.yaml.domain.expression_time_series_fluid_density import ExpressionTimeSeriesFluidDensity
10+
from libecalc.presentation.yaml.domain.expression_time_series_pressure import ExpressionTimeSeriesPressure
11+
from libecalc.presentation.yaml.domain.reference_service import ReferenceService
12+
from libecalc.presentation.yaml.domain.time_series_expression import TimeSeriesExpression
13+
from libecalc.presentation.yaml.mappers.charts.user_defined_chart_data import UserDefinedChartData
14+
from libecalc.presentation.yaml.yaml_types.facility_model.yaml_facility_model import (
15+
YamlPumpChartSingleSpeed,
16+
)
17+
from libecalc.presentation.yaml.yaml_types.process.yaml_process_simulation import YamlPumpProcessSimulation
18+
from libecalc.process.pump.liquid_stream import LiquidStream
19+
from libecalc.process.pump.pump import Pump
20+
from libecalc.process.pump.pump_process_simulation import (
21+
PumpOperatingInput,
22+
PumpProcessSimulation,
23+
)
24+
25+
26+
class PumpProcessSimulationMapper:
27+
def __init__(
28+
self,
29+
expression_evaluator: ExpressionEvaluator,
30+
reference_service: ReferenceService,
31+
resources: Resources,
32+
process_simulation_period: Period,
33+
):
34+
self._expression_evaluator = expression_evaluator.get_subset_for_period(process_simulation_period)
35+
self._reference_service = reference_service
36+
self._resources = resources
37+
38+
def map(
39+
self,
40+
yaml_process_simulation: YamlPumpProcessSimulation,
41+
) -> tuple[PumpProcessSimulation, list[PumpOperatingInput], list[Period]]:
42+
chart_data = self._get_chart_data(yaml_process_simulation.pump_model.chart)
43+
pump = Pump(
44+
pump_chart=chart_data,
45+
minimum_flow_rate_m3_per_hour=yaml_process_simulation.pump_model.minimum_flow_rate,
46+
)
47+
simulation = PumpProcessSimulation(
48+
pump=pump,
49+
name=yaml_process_simulation.name,
50+
)
51+
52+
regularity = Regularity(
53+
expression_evaluator=self._expression_evaluator,
54+
target_period=self._expression_evaluator.get_period(),
55+
)
56+
rate = ExpressionTimeSeriesFlowRate(
57+
time_series_expression=TimeSeriesExpression(
58+
expression_evaluator=self._expression_evaluator,
59+
expression=yaml_process_simulation.inlet.rate,
60+
),
61+
regularity=regularity,
62+
)
63+
rate_values = rate.get_stream_day_values()
64+
suction_pressure = ExpressionTimeSeriesPressure(
65+
time_series_expression=TimeSeriesExpression(
66+
expression_evaluator=self._expression_evaluator,
67+
expression=yaml_process_simulation.inlet.pressure,
68+
),
69+
)
70+
discharge_pressure = ExpressionTimeSeriesPressure(
71+
time_series_expression=TimeSeriesExpression(
72+
expression_evaluator=self._expression_evaluator,
73+
expression=yaml_process_simulation.required_discharge_pressure,
74+
),
75+
)
76+
density = ExpressionTimeSeriesFluidDensity(
77+
time_series_expression=TimeSeriesExpression(
78+
expression_evaluator=self._expression_evaluator,
79+
expression=yaml_process_simulation.inlet.density,
80+
)
81+
)
82+
83+
suction_values = suction_pressure.get_values()
84+
discharge_values = discharge_pressure.get_values()
85+
density_values = density.get_values()
86+
87+
# The domain is time-agnostic: build one physical input per period and keep the period
88+
# vector here in the presentation layer, paired with the inputs (and the results) by index.
89+
periods = list(rate.get_periods())
90+
operating_inputs = [
91+
self._to_operating_input(
92+
rate_value=rate_value,
93+
suction_pressure_bara=suction,
94+
required_discharge_pressure_bara=discharge,
95+
density_kg_per_m3=density_value,
96+
)
97+
for rate_value, suction, discharge, density_value in zip(
98+
rate_values,
99+
suction_values,
100+
discharge_values,
101+
density_values,
102+
strict=True,
103+
)
104+
]
105+
if len(periods) != len(operating_inputs):
106+
raise ValueError("Pump period vector and input vector length mismatch.")
107+
return simulation, operating_inputs, periods
108+
109+
def _to_operating_input(
110+
self,
111+
rate_value: float,
112+
suction_pressure_bara: float,
113+
required_discharge_pressure_bara: float,
114+
density_kg_per_m3: float,
115+
) -> PumpOperatingInput:
116+
# Suction pressure and density describe the physical inlet fluid; they must be positive
117+
# whether or not the pump runs.
118+
self._require_positive(suction_pressure_bara, "suction pressure [bara]")
119+
self._require_positive(density_kg_per_m3, "inlet density [kg/m3]")
120+
inlet_stream = LiquidStream.from_volumetric_rate(
121+
volumetric_rate_m3_per_day=rate_value,
122+
pressure_bara=suction_pressure_bara,
123+
density_kg_per_m3=density_kg_per_m3,
124+
)
125+
126+
if rate_value > 0:
127+
self._require_positive(required_discharge_pressure_bara, "required discharge pressure [bara]")
128+
discharge = required_discharge_pressure_bara
129+
else:
130+
# Pump off (zero rate): the required discharge is a meaningless duty target. Keep the
131+
# user's value when it is a valid absolute pressure, otherwise fall back to the inlet
132+
# pressure (the pump delivers no head, so the outlet equals the inlet).
133+
discharge = (
134+
required_discharge_pressure_bara if required_discharge_pressure_bara > 0 else suction_pressure_bara
135+
)
136+
137+
return PumpOperatingInput(
138+
inlet_stream=inlet_stream,
139+
required_discharge_pressure_bara=discharge,
140+
)
141+
142+
@staticmethod
143+
def _require_positive(value: float, subject: str) -> None:
144+
if value <= 0:
145+
raise EcalcValidationException(f"Pump {subject} must be greater than 0; got {value}.")
146+
147+
def _get_chart_data(self, reference: str) -> ChartData:
148+
model = self._reference_service.get_pump_model(reference)
149+
if model.head_margin != 0.0:
150+
raise EcalcValidationException(
151+
"HEAD_MARGIN is not supported by the new pump process domain "
152+
"(points above the maximum head are flagged infeasible instead of snapped to it)."
153+
)
154+
resource = self._resources.get(model.file)
155+
if resource is None:
156+
raise EcalcValidationException(f"Pump chart resource '{model.file}' was not found.")
157+
try:
158+
return UserDefinedChartData.from_resource(
159+
resource,
160+
units=model.units,
161+
is_single_speed=isinstance(model, YamlPumpChartSingleSpeed),
162+
)
163+
except InvalidResourceException as error:
164+
raise EcalcValidationException(str(error)) from error

src/libecalc/presentation/yaml/model.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from libecalc.presentation.yaml.domain.time_series_resource import TimeSeriesResource
4646
from libecalc.presentation.yaml.mappers.component_mapper import EcalcModelMapper
4747
from libecalc.presentation.yaml.mappers.process_simulation_mapper import ProcessSimulationMapper
48+
from libecalc.presentation.yaml.mappers.pump_process_simulation_mapper import PumpProcessSimulationMapper
4849
from libecalc.presentation.yaml.mappers.variables_mapper import map_yaml_to_variables
4950
from libecalc.presentation.yaml.mappers.variables_mapper.get_global_time_vector import (
5051
InvalidEndDate,
@@ -68,6 +69,10 @@
6869
YamlModelValidationContextNames,
6970
)
7071
from libecalc.process.process_pipeline.process_pipeline import ProcessPipeline
72+
from libecalc.process.pump.pump_process_simulation import (
73+
PumpOperatingInput,
74+
PumpProcessSimulation,
75+
)
7176

7277
DEFAULT_START_TIME = datetime(1900, 1, 1)
7378

@@ -165,6 +170,22 @@ def get_process_simulations(self) -> tuple[list[ProcessPipeline], list[ProcessSi
165170

166171
return process_pipelines, process_simulations
167172

173+
def get_pump_process_simulations(
174+
self,
175+
) -> list[tuple[PumpProcessSimulation, list[PumpOperatingInput], list[Period]]]:
176+
self.validate_for_run()
177+
facility_resources, _ = self._resource_service.get_facility_resources()
178+
mapper = PumpProcessSimulationMapper(
179+
expression_evaluator=self.get_expression_evaluator(),
180+
process_simulation_period=self.period,
181+
resources=facility_resources,
182+
reference_service=self._get_reference_service(),
183+
)
184+
return [
185+
mapper.map(yaml_pump_process_simulation)
186+
for yaml_pump_process_simulation in self._configuration.pump_process_simulations
187+
]
188+
168189
def get_periods(self) -> list[Period]:
169190
"""
170191
Get the global timevector for this model

src/libecalc/presentation/yaml/yaml_models/pyyaml_yaml_model.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
YamlEcalcEvent,
3737
YamlProcessEvent,
3838
YamlProcessSimulation,
39+
YamlPumpProcessSimulation,
3940
)
4041
from libecalc.presentation.yaml.yaml_types.process.yaml_process_units import YamlProcessUnit
4142
from libecalc.presentation.yaml.yaml_types.streams.yaml_inlet_stream import YamlInletStream
@@ -52,6 +53,7 @@
5253
_PROCESS_SIMULATIONS_KEY = "PROCESS_SIMULATIONS"
5354
_ECALC_EVENTS_KEY = "ECALC_EVENTS"
5455
_PROCESS_EVENTS_KEY = "PROCESS_EVENTS"
56+
_PUMP_PROCESS_SIMULATIONS_KEY = "PUMP_PROCESS_SIMULATIONS"
5557
_NEW_SECTIONS_WITH_FILE_REFS: tuple[str, ...] = (
5658
"PROCESS_UNITS",
5759
"PROCESS_PIPELINES",
@@ -506,6 +508,17 @@ def process_events(self) -> list[YamlProcessEvent]:
506508
pass
507509
return process_events
508510

511+
@property
512+
def pump_process_simulations(self) -> list[YamlPumpProcessSimulation]:
513+
pump_process_simulations: list[YamlPumpProcessSimulation] = []
514+
adapter = TypeAdapter(YamlPumpProcessSimulation)
515+
for pump_process_simulation in self._get_yaml_list_or_empty(_PUMP_PROCESS_SIMULATIONS_KEY):
516+
try:
517+
pump_process_simulations.append(adapter.validate_python(pump_process_simulation))
518+
except PydanticValidationError:
519+
pass
520+
return pump_process_simulations
521+
509522
@property
510523
def start(self) -> datetime.datetime | None:
511524
start_value = self._internal_datamodel.get(EcalcYamlKeywords.start)

src/libecalc/presentation/yaml/yaml_models/yaml_model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
YamlEcalcEvent,
2222
YamlProcessEvent,
2323
YamlProcessSimulation,
24+
YamlPumpProcessSimulation,
2425
)
2526
from libecalc.presentation.yaml.yaml_types.process.yaml_process_units import YamlProcessUnit
2627
from libecalc.presentation.yaml.yaml_types.streams.yaml_inlet_stream import YamlInletStream
@@ -112,6 +113,11 @@ def ecalc_events(self) -> list[YamlEcalcEvent]:
112113
def process_events(self) -> list[YamlProcessEvent]:
113114
pass
114115

116+
@property
117+
@abc.abstractmethod
118+
def pump_process_simulations(self) -> Iterable[YamlPumpProcessSimulation]:
119+
pass
120+
115121
@property
116122
@abc.abstractmethod
117123
def installations(self) -> Iterable[YamlInstallation]:

src/libecalc/presentation/yaml/yaml_reference_service.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
)
2727
from libecalc.presentation.yaml.yaml_types.models.yaml_enums import YamlModelType
2828
from libecalc.presentation.yaml.yaml_types.process.yaml_process_pipeline import YamlProcessPipeline
29-
from libecalc.presentation.yaml.yaml_types.process.yaml_process_simulation import YamlProcessSimulation
29+
from libecalc.presentation.yaml.yaml_types.process.yaml_process_simulation import (
30+
YamlProcessSimulation,
31+
YamlPumpProcessSimulation,
32+
)
3033
from libecalc.presentation.yaml.yaml_types.process.yaml_process_units import YamlProcessUnit
3134
from libecalc.presentation.yaml.yaml_types.streams.yaml_inlet_stream import YamlInletStream
3235

@@ -35,7 +38,13 @@
3538
YamlModel = YamlConsumerModel | YamlFacilityModel
3639

3740
ReferenceType = (
38-
YamlModel | YamlFuelType | YamlInletStream | YamlProcessPipeline | YamlProcessSimulation | YamlProcessUnit
41+
YamlModel
42+
| YamlFuelType
43+
| YamlInletStream
44+
| YamlProcessPipeline
45+
| YamlProcessSimulation
46+
| YamlPumpProcessSimulation
47+
| YamlProcessUnit
3948
)
4049

4150
# Some models are referenced by other models, for example a compressor model will reference compressor chart models
@@ -122,6 +131,12 @@ def __init__(
122131
references[process_simulation.name] = process_simulation
123132
reference_yaml_context[process_simulation.name] = process_simulation_path
124133

134+
pump_process_simulations_path = YamlPath(keys=("PUMP_PROCESS_SIMULATIONS",))
135+
for pump_process_simulation_index, pump_process_simulation in enumerate(configuration.pump_process_simulations):
136+
pump_process_simulation_path = pump_process_simulations_path.append(pump_process_simulation_index)
137+
references[pump_process_simulation.name] = pump_process_simulation
138+
reference_yaml_context[pump_process_simulation.name] = pump_process_simulation_path
139+
125140
fluid_models_path = YamlPath(keys=("FLUID_MODELS",))
126141

127142
for fluid_model_key, fluid_model in configuration.fluid_models.items():

src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
YamlEcalcEvent,
1313
YamlProcessEvent,
1414
YamlProcessSimulation,
15+
YamlPumpProcessSimulation,
1516
)
1617
from libecalc.presentation.yaml.yaml_types.process.yaml_process_units import YamlProcessUnit
1718
from libecalc.presentation.yaml.yaml_types.streams.yaml_inlet_stream import YamlInletStream
@@ -93,6 +94,11 @@ class YamlAsset(YamlBase):
9394
title="PROCESS_EVENTS",
9495
description="Defines process-specific events that reference global eCalc events.",
9596
)
97+
pump_process_simulations: list[YamlPumpProcessSimulation] = Field(
98+
default_factory=list,
99+
title="PUMP_PROCESS_SIMULATIONS",
100+
description="Defines one or more liquid pump process simulations to be run.",
101+
)
96102
installations: list[YamlInstallation] = Field(
97103
...,
98104
title="INSTALLATIONS",
@@ -189,6 +195,10 @@ def validate_unique_references(self):
189195
for process_simulation in self.process_simulations:
190196
references.append(process_simulation.name)
191197

198+
if self.pump_process_simulations is not None:
199+
for pump_process_simulation in self.pump_process_simulations:
200+
references.append(pump_process_simulation.name)
201+
192202
# TODO: Add ecalc events references?
193203

194204
if self.fluid_models is not None:

src/libecalc/presentation/yaml/yaml_types/process/yaml_process_references.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
type ProcessUnitReference = str
88
type EcalcEventReference = str
99
type ProcessEventReference = str
10+
type PumpChartReference = str

0 commit comments

Comments
 (0)