Skip to content

Commit 6e7b4ea

Browse files
frodehkjsolaas
andauthored
refactor: introduce FeasibilitySolver and StreamDistributionItem (#1457)
* refactor: introduce FeasibilitySolver and StreamDistributionItem Refs: equinor/ecalc-internal#1634 equinor/ecalc-internal#1635 --------- Co-authored-by: Jostein Solaas <33114722+jsolaas@users.noreply.github.com>
1 parent 90a14eb commit 6e7b4ea

4 files changed

Lines changed: 117 additions & 10 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from libecalc.domain.process.entities.process_units.compressor import Compressor
2+
from libecalc.domain.process.process_solver.float_constraint import FloatConstraint
3+
from libecalc.domain.process.process_solver.outlet_pressure_solver import OutletPressureSolver
4+
from libecalc.domain.process.process_solver.process_runner import ProcessRunner
5+
from libecalc.domain.process.value_objects.fluid_stream import FluidStream
6+
7+
8+
class FeasibilitySolver:
9+
"""
10+
Calculates how much of a given inlet rate exceeds what a compressor train
11+
can handle for a target pressure.
12+
13+
Orchestrates OutletPressureSolver and queries compressor charts to find
14+
the feasible rate — the excess is redirected via stream distribution.
15+
"""
16+
17+
def __init__(
18+
self,
19+
outlet_pressure_solver: OutletPressureSolver,
20+
compressors: list[Compressor],
21+
runner: ProcessRunner,
22+
):
23+
self._solver = outlet_pressure_solver
24+
self._compressors = compressors
25+
self._runner = runner
26+
27+
def get_excess_rate(
28+
self,
29+
inlet_stream: FluidStream,
30+
target_pressure: FloatConstraint,
31+
) -> float:
32+
"""
33+
Rate [sm³/day] that exceeds what this train can handle.
34+
35+
This is the amount that must be redirected (e.g. via overflow)
36+
to another train in the stream distribution.
37+
"""
38+
feasible = self._find_feasible_rate(inlet_stream, target_pressure)
39+
excess_rate = max(0.0, inlet_stream.standard_rate_sm3_per_day - feasible)
40+
return excess_rate
41+
42+
def _find_feasible_rate(
43+
self,
44+
inlet_stream: FluidStream,
45+
target_pressure: FloatConstraint,
46+
) -> float:
47+
"""Highest standard rate [sm³/day] for which the train can meet target_pressure.
48+
49+
Returns the full inlet rate if the solver succeeds, otherwise finds the
50+
bottleneck compressor's stone wall limit at the current operating point.
51+
"""
52+
solution = self._solver.find_solution(target_pressure, inlet_stream)
53+
54+
if solution.success:
55+
# The train can handle the full rate — no need to search for a bottleneck.
56+
return inlet_stream.standard_rate_sm3_per_day
57+
58+
# Apply the configuration before querying compressor charts.
59+
self._runner.apply_configurations(solution.configuration)
60+
61+
# Search for compressor with the lowest max rate
62+
min_max_rate = float("inf")
63+
for compressor in self._compressors:
64+
compressor_inlet = self._runner.run(
65+
inlet_stream=inlet_stream,
66+
to_id=compressor.get_id(),
67+
)
68+
max_rate = compressor.get_maximum_standard_rate(compressor_inlet)
69+
min_max_rate = min(min_max_rate, max_rate)
70+
71+
feasible_rate = max(0.0, min_max_rate)
72+
73+
return feasible_rate

src/libecalc/domain/process/stream_distribution/common_stream_distribution.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream
1111

1212

13-
class HasCapacity(abc.ABC):
13+
class HasExcessRate(abc.ABC):
1414
@abc.abstractmethod
15-
def get_unhandled_rate(self, rate: float, pressure: float) -> float: ...
15+
def get_excess_rate(self, inlet_stream: FluidStream) -> float: ...
1616

1717

1818
T = TypeVar("T", bound=Hashable)
@@ -28,7 +28,7 @@ class CommonStreamDistribution(StreamDistribution, Generic[T]):
2828
def __init__(
2929
self,
3030
inlet_stream: FluidStream,
31-
items: dict[T, HasCapacity],
31+
items: dict[T, HasExcessRate],
3232
rate_fractions: list[float],
3333
overflows: list[Overflow[T]],
3434
fluid_service: FluidService,
@@ -69,9 +69,15 @@ def _adjust_for_overflow(self) -> dict[T, float]:
6969
current_rate = rate + overflow_rate
7070
if overflow is not None:
7171
item = self._items[item_id]
72-
unhandled_rate = item.get_unhandled_rate(current_rate, self._inlet_stream.pressure_bara)
73-
handled_rate = current_rate - unhandled_rate
74-
overflow_map[overflow.to_id].append(unhandled_rate)
72+
stream = self._fluid_service.create_stream_from_standard_rate(
73+
fluid_model=self._inlet_stream.fluid_model,
74+
standard_rate_m3_per_day=current_rate,
75+
temperature_kelvin=self._inlet_stream.temperature_kelvin,
76+
pressure_bara=self._inlet_stream.pressure_bara,
77+
)
78+
excess_rate = item.get_excess_rate(stream)
79+
handled_rate = current_rate - excess_rate
80+
overflow_map[overflow.to_id].append(excess_rate)
7581
adjusted_rates[item_id] = handled_rate
7682
else:
7783
adjusted_rates[item_id] = current_rate
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from libecalc.domain.process.process_solver.feasibility_solver import FeasibilitySolver
2+
from libecalc.domain.process.process_solver.float_constraint import FloatConstraint
3+
from libecalc.domain.process.stream_distribution.common_stream_distribution import HasExcessRate
4+
from libecalc.domain.process.stream_distribution.priorities_stream_distribution import HasValidity
5+
from libecalc.domain.process.value_objects.fluid_stream import FluidStream
6+
7+
8+
class StreamDistributionItem(HasExcessRate, HasValidity):
9+
"""Connects a compressor train's solver to the stream distribution system."""
10+
11+
def __init__(
12+
self,
13+
feasibility_solver: FeasibilitySolver,
14+
target_pressure: FloatConstraint,
15+
):
16+
self._feasibility_solver = feasibility_solver
17+
self._target_pressure = target_pressure
18+
19+
def is_valid(self, inlet_stream: FluidStream) -> bool:
20+
"""Can the train operate at these inlet conditions?"""
21+
return self.get_excess_rate(inlet_stream) == 0.0
22+
23+
def get_excess_rate(self, inlet_stream: FluidStream) -> float:
24+
"""How much rate (sm³/day) exceeds this train's capacity?"""
25+
return self._feasibility_solver.get_excess_rate(
26+
inlet_stream=inlet_stream, target_pressure=self._target_pressure
27+
)

tests/libecalc/application/test_stream_distribution.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@
44
from inline_snapshot import snapshot
55
from libecalc.domain.process.stream_distribution.common_stream_distribution import (
66
CommonStreamDistribution,
7-
HasCapacity,
7+
HasExcessRate,
88
Overflow,
99
)
1010
from libecalc.domain.component_validation_error import DomainValidationException
11+
from libecalc.domain.process.value_objects.fluid_stream import FluidStream
1112

1213

13-
class Item(HasCapacity):
14+
class Item(HasExcessRate):
1415
def __init__(self, capacity: float):
1516
self.id = uuid4()
1617
self._capacity = capacity
1718

18-
def get_unhandled_rate(self, rate: float, pressure: float) -> float:
19-
return max(0.0, rate - self._capacity)
19+
def get_excess_rate(self, inlet_stream: FluidStream) -> float:
20+
return max(0.0, inlet_stream.standard_rate_sm3_per_day - self._capacity)
2021

2122

2223
class TestCommonStreamDistribution:

0 commit comments

Comments
 (0)