Skip to content

Commit 9fa2c2a

Browse files
committed
feat(yaml): introduce interstage types and segment constraints for process systems
Add YAML types to express process system topology (PROCESS_SYSTEMS) and operational scenarios (PROCESS_SIMULATIONS) with full separation of equipment and operation. Mapper is separate work. New process system types (all named, defined under PROCESS_SYSTEMS): - COMPRESSOR_STAGE: single compressor stage with inlet temperature - PRESSURE_DROP: any interstage element that introduces a pressure drop - MIXER: stream injection point (pure topology — no stream defined here) - SPLITTER: stream extraction point (pure topology — no rate defined here) - SERIAL: ordered list of named process system references; carries ANTI_SURGE (physical equipment choice: INDIVIDUAL_ASV or COMMON_ASV, defaults to INDIVIDUAL_ASV) - PARALLEL: parallel arrangement of named serial trains PROCESS_SIMULATIONS fields: - TARGET: single reference to the target process system - INLET_STREAM: reference to an inlet stream (serial target) - STREAM_DISTRIBUTION: COMMON_STREAM or INDIVIDUAL_STREAMS (parallel target) - COMMON_STREAM: SETTINGS list tried in order; first within capacity wins; each setting supports OVERFLOW between trains - INDIVIDUAL_STREAMS: one stream reference per train - MIXER_STREAMS: map of mixer name -> inlet stream reference - SPLITTER_RATES: map of splitter name -> rate (VALUE + UNIT) - CONSTRAINTS: map of item name or train name -> YamlSegmentConstraint Using the train name targets the train outlet regardless of last stage YamlSegmentConstraint per segment boundary: - OUTLET_PRESSURE: required; target pressure [bara] - PRESSURE_CONTROL: optional; defaults to DOWNSTREAM_CHOKE for the train outlet constraint and INDIVIDUAL_ASV_RATE for all intermediate segment constraints ANTI_SURGE is on the SERIAL train (physical equipment), not the constraint. Consistency between train ANTI_SURGE and constraint PRESSURE_CONTROL is validated at asset level (e.g. COMMON_ASV pressure control requires COMMON_ASV on the train). Cross-reference validation in YamlAsset (runs at parse time, before domain code): - Every constraint must have OUTLET_PRESSURE set - Every MIXER in the target train must have an entry in MIXER_STREAMS - Every SPLITTER in the target train must have an entry in SPLITTER_RATES - Constraint keys must be item names in the train or the train name itself - PRESSURE_CONTROL defaults applied before consistency validation New YamlAntiSurge enum in yaml_enums.py.
1 parent 90a14eb commit 9fa2c2a

3 files changed

Lines changed: 189 additions & 20 deletions

File tree

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
from libecalc.presentation.yaml.yaml_types import YamlBase
66
from libecalc.presentation.yaml.yaml_types.components.yaml_installation import YamlInstallation
77
from libecalc.presentation.yaml.yaml_types.components.yaml_process_system import (
8+
YamlInterstageMixer,
9+
YamlInterstageSplitter,
810
YamlProcessSimulation,
911
YamlProcessSystem,
1012
YamlProcessUnit,
13+
YamlSerialProcessSystem,
1114
)
15+
from libecalc.presentation.yaml.yaml_types.models.yaml_enums import YamlAntiSurge, YamlPressureControl
1216
from libecalc.presentation.yaml.yaml_types.facility_model.yaml_facility_model import YamlFacilityModel
1317
from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_fuel_type import YamlFuelType
1418
from libecalc.presentation.yaml.yaml_types.models import YamlConsumerModel, YamlFluidModel
@@ -191,3 +195,97 @@ def validate_unique_references(self):
191195
f" Duplicated references are: {', '.join(duplicated_references)}"
192196
)
193197
return self
198+
199+
@model_validator(mode="after")
200+
def validate_constraint_outlet_pressure(self):
201+
for sim in self.process_simulations:
202+
for key, constraint in sim.constraints.items():
203+
if constraint.outlet_pressure is None:
204+
raise ValueError(
205+
f"Constraint '{key}' in simulation '{sim.name}' must have OUTLET_PRESSURE set."
206+
)
207+
return self
208+
209+
@model_validator(mode="after")
210+
def apply_pressure_control_defaults(self):
211+
for sim in self.process_simulations:
212+
train = self.process_systems.get(sim.target)
213+
if not isinstance(train, YamlSerialProcessSystem):
214+
continue
215+
for key, constraint in sim.constraints.items():
216+
if constraint.pressure_control is None:
217+
constraint.pressure_control = (
218+
YamlPressureControl.DOWNSTREAM_CHOKE
219+
if key == train.name
220+
else YamlPressureControl.INDIVIDUAL_ASV_RATE
221+
)
222+
return self
223+
224+
@model_validator(mode="after")
225+
def validate_simulation_streams(self):
226+
for sim in self.process_simulations:
227+
train = self.process_systems.get(sim.target)
228+
if not isinstance(train, YamlSerialProcessSystem):
229+
continue
230+
for item_name in train.items:
231+
item = self.process_systems.get(item_name)
232+
if isinstance(item, YamlInterstageMixer) and item_name not in sim.mixer_streams:
233+
raise ValueError(
234+
f"Mixer '{item_name}' in train '{train.name}' has no stream defined "
235+
f"in MIXER_STREAMS of simulation '{sim.name}'."
236+
)
237+
if isinstance(item, YamlInterstageSplitter) and item_name not in sim.splitter_rates:
238+
raise ValueError(
239+
f"Splitter '{item_name}' in train '{train.name}' has no rate defined "
240+
f"in SPLITTER_RATES of simulation '{sim.name}'."
241+
)
242+
return self
243+
244+
@model_validator(mode="after")
245+
def validate_constraint_keys(self):
246+
for sim in self.process_simulations:
247+
train = self.process_systems.get(sim.target)
248+
if not isinstance(train, YamlSerialProcessSystem):
249+
continue
250+
valid_keys = set(train.items) | {train.name}
251+
for key in sim.constraints:
252+
if key not in valid_keys:
253+
raise ValueError(
254+
f"Constraint key '{key}' in simulation '{sim.name}' is not a valid item name "
255+
f"in train '{train.name}' or the train name itself. "
256+
f"Valid keys are: {', '.join(sorted(valid_keys))}."
257+
)
258+
return self
259+
260+
@model_validator(mode="after")
261+
def validate_anti_surge_pressure_control_consistency(self):
262+
for sim in self.process_simulations:
263+
train = self.process_systems.get(sim.target)
264+
if not isinstance(train, YamlSerialProcessSystem):
265+
continue
266+
for key, constraint in sim.constraints.items():
267+
if constraint.pressure_control is None:
268+
continue
269+
if (
270+
constraint.pressure_control == YamlPressureControl.COMMON_ASV
271+
and train.anti_surge != YamlAntiSurge.COMMON_ASV
272+
):
273+
raise ValueError(
274+
f"Constraint '{key}' in simulation '{sim.name}' uses PRESSURE_CONTROL "
275+
f"COMMON_ASV but train '{train.name}' has ANTI_SURGE {train.anti_surge.value}. "
276+
f"COMMON_ASV pressure control requires ANTI_SURGE COMMON_ASV on the train."
277+
)
278+
if (
279+
constraint.pressure_control in (
280+
YamlPressureControl.INDIVIDUAL_ASV_RATE,
281+
YamlPressureControl.INDIVIDUAL_ASV_PRESSURE,
282+
)
283+
and train.anti_surge != YamlAntiSurge.INDIVIDUAL_ASV
284+
):
285+
raise ValueError(
286+
f"Constraint '{key}' in simulation '{sim.name}' uses PRESSURE_CONTROL "
287+
f"{constraint.pressure_control.value} but train '{train.name}' has "
288+
f"ANTI_SURGE {train.anti_surge.value}. "
289+
f"Individual ASV pressure control requires ANTI_SURGE INDIVIDUAL_ASV on the train."
290+
)
291+
return self

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

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from typing import Annotated, Generic, Literal, TypeAlias, TypeVar
1+
from typing import Annotated, Literal, TypeAlias
22

3-
from pydantic import Field
3+
from pydantic import Field, model_validator
44

55
from libecalc.presentation.yaml.yaml_types import YamlBase
66
from libecalc.presentation.yaml.yaml_types.components.yaml_expression_type import YamlExpressionType
77
from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_chart import UnitsField, YamlCurve, YamlUnits
88
from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_stages import YamlControlMarginUnits
9-
from libecalc.presentation.yaml.yaml_types.streams.yaml_inlet_stream import YamlInletStream
9+
from libecalc.presentation.yaml.yaml_types.models.yaml_enums import YamlAntiSurge, YamlPressureControl
10+
from libecalc.presentation.yaml.yaml_types.streams.yaml_inlet_stream import YamlInletStreamRate
1011
from libecalc.presentation.yaml.yaml_types.yaml_data_or_file import DataOrFile
1112

1213
StreamRef = str
@@ -63,25 +64,54 @@ class YamlCompressorStageProcessSystem(YamlBase):
6364
description="Inlet temperature in Celsius for stage",
6465
title="INLET_TEMPERATURE",
6566
)
66-
pressure_drop_ahead_of_stage: YamlExpressionType = Field(
67-
0.0,
68-
description="Pressure drop before compression stage [in bar]",
69-
title="PRESSURE_DROP_AHEAD_OF_STAGE",
70-
)
7167
compressor: CompressorReference | YamlCompressor
7268

7369

74-
TTarget = TypeVar("TTarget")
70+
class YamlInterstagePressureDrop(YamlBase):
71+
type: Literal["PRESSURE_DROP"]
72+
name: ProcessSystemReference
73+
pressure_drop: YamlExpressionType = Field(
74+
...,
75+
title="PRESSURE_DROP",
76+
description="Pressure drop across the choke [bar].",
77+
)
7578

7679

77-
class YamlItem(YamlBase, Generic[TTarget]):
78-
target: TTarget | ProcessSystemReference
80+
class YamlInterstageMixer(YamlBase):
81+
type: Literal["MIXER"]
82+
name: ProcessSystemReference
83+
84+
85+
class YamlInterstageSplitter(YamlBase):
86+
type: Literal["SPLITTER"]
87+
name: ProcessSystemReference
88+
89+
90+
YamlInterstageItem = Annotated[
91+
YamlInterstagePressureDrop | YamlInterstageMixer | YamlInterstageSplitter,
92+
Field(discriminator="type"),
93+
]
7994

8095

8196
class YamlSerialProcessSystem(YamlBase):
8297
type: Literal["SERIAL"]
8398
name: ProcessSystemReference
84-
items: list[YamlItem[YamlCompressorStageProcessSystem]]
99+
items: list[ProcessSystemReference]
100+
anti_surge: YamlAntiSurge = Field(
101+
default=YamlAntiSurge.INDIVIDUAL_ASV,
102+
title="ANTI_SURGE",
103+
description=(
104+
"Anti-surge strategy for the train. INDIVIDUAL_ASV means each compressor "
105+
"has its own recirculation valve. COMMON_ASV means a shared valve across the train. "
106+
"Defaults to INDIVIDUAL_ASV."
107+
),
108+
)
109+
110+
111+
class YamlParallelProcessSystem(YamlBase):
112+
type: Literal["PARALLEL"]
113+
name: ProcessSystemReference
114+
items: list[ProcessSystemReference]
85115

86116

87117
class YamlOverflow(YamlBase):
@@ -96,36 +126,67 @@ class YamlCommonStreamSetting(YamlBase):
96126

97127
class YamlCommonStreamDistribution(YamlBase):
98128
method: Literal["COMMON_STREAM"]
99-
inlet_stream: StreamRef | YamlInletStream
129+
inlet_stream: StreamRef
100130
settings: list[YamlCommonStreamSetting]
101131

102132

103133
class YamlIndividualStreamDistribution(YamlBase):
104134
method: Literal["INDIVIDUAL_STREAMS"]
105-
inlet_streams: list[StreamRef | YamlInletStream]
135+
inlet_streams: list[StreamRef]
106136

107137

108138
YamlStreamDistribution = Annotated[
109139
YamlCommonStreamDistribution | YamlIndividualStreamDistribution, Field(discriminator="method")
110140
]
111141

112142

113-
class YamlProcessConstraints(YamlBase):
143+
class YamlSegmentConstraint(YamlBase):
114144
outlet_pressure: YamlExpressionType | None = Field(
115145
None,
116146
title="OUTLET_PRESSURE",
117147
description="Target outlet pressure [bara].",
118148
)
149+
pressure_control: YamlPressureControl | None = Field(
150+
None,
151+
title="PRESSURE_CONTROL",
152+
description=(
153+
"Pressure control strategy for the segment ending at this constraint. "
154+
"Defaults to DOWNSTREAM_CHOKE for the train outlet (constraint keyed by train name) "
155+
"and INDIVIDUAL_ASV_RATE for all intermediate segment constraints."
156+
),
157+
)
119158

120159

121160
class YamlProcessSimulation(YamlBase):
122161
name: str
123-
targets: list[YamlItem[YamlSerialProcessSystem]] = Field(..., title="TARGETS")
124-
stream_distribution: YamlStreamDistribution
125-
constraints: dict[ProcessSystemReference, YamlProcessConstraints] = Field(
162+
target: ProcessSystemReference = Field(..., title="TARGET")
163+
inlet_stream: StreamRef | None = Field(
164+
None,
165+
title="INLET_STREAM",
166+
description="Reference to an inlet stream defined in INLET_STREAMS, for a serial target.",
167+
)
168+
stream_distribution: YamlStreamDistribution | None = Field(
169+
None,
170+
title="STREAM_DISTRIBUTION",
171+
description="Stream distribution for a parallel target.",
172+
)
173+
mixer_streams: dict[ProcessSystemReference, StreamRef] = Field(
174+
default_factory=dict,
175+
title="MIXER_STREAMS",
176+
description="References to inlet streams for mixer items in the target train, keyed by mixer name.",
177+
)
178+
splitter_rates: dict[ProcessSystemReference, YamlInletStreamRate] = Field(
179+
default_factory=dict,
180+
title="SPLITTER_RATES",
181+
description="Extraction rates for splitter items in the target train, keyed by splitter name.",
182+
)
183+
constraints: dict[ProcessSystemReference, YamlSegmentConstraint] = Field(
126184
default_factory=dict,
127185
title="CONSTRAINTS",
128-
description="Optional constraints per process system reference.",
186+
description=(
187+
"Constraints keyed by process system item name or the train name itself. "
188+
"Using the train name as a key targets the train outlet regardless of which stage is last."
189+
),
129190
)
130191

131192

@@ -135,6 +196,11 @@ class YamlProcessSimulation(YamlBase):
135196
]
136197

137198
YamlProcessSystem = Annotated[
138-
YamlSerialProcessSystem | YamlCompressorStageProcessSystem,
199+
YamlSerialProcessSystem
200+
| YamlParallelProcessSystem
201+
| YamlCompressorStageProcessSystem
202+
| YamlInterstagePressureDrop
203+
| YamlInterstageSplitter
204+
| YamlInterstageMixer,
139205
Field(discriminator="type"),
140206
]

src/libecalc/presentation/yaml/yaml_types/models/yaml_enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ class YamlPressureControl(enum.Enum):
2727
INDIVIDUAL_ASV_PRESSURE = "INDIVIDUAL_ASV_PRESSURE"
2828
INDIVIDUAL_ASV_RATE = "INDIVIDUAL_ASV_RATE"
2929
COMMON_ASV = "COMMON_ASV"
30+
31+
32+
class YamlAntiSurge(enum.Enum):
33+
INDIVIDUAL_ASV = "INDIVIDUAL_ASV"
34+
COMMON_ASV = "COMMON_ASV"

0 commit comments

Comments
 (0)