Skip to content

Commit f7ae40a

Browse files
committed
chore: implement common asv compressor train solver
1 parent c662c35 commit f7ae40a

13 files changed

Lines changed: 397 additions & 54 deletions

File tree

src/ecalc_neqsim_wrapper/fluid_service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,12 @@ def mass_rate_to_standard_rate(
479479
standard_density = self._get_standard_density(fluid_model)
480480
return mass_rate_kg_per_h * 24.0 / standard_density
481481

482+
def volumetric_rate_to_standard_rate(
483+
self, fluid_model: FluidModel, volumetric_rate: float, density: float
484+
) -> float:
485+
mass_rate = volumetric_rate * density
486+
return self.mass_rate_to_standard_rate(fluid_model=fluid_model, mass_rate_kg_per_h=mass_rate)
487+
482488

483489
def get_fluid_service_stats() -> dict[str, dict]:
484490
"""Get cache statistics for the fluid service caches."""

src/libecalc/domain/process/compressor/core/train/stage.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,24 +99,12 @@ def remove_rate(self, outlet_stream_stage: FluidStream) -> FluidStream:
9999
stream=outlet_stream_stage,
100100
)
101101

102-
def evaluate(
102+
def get_compressor_inlet_stream(
103103
self,
104104
inlet_stream_stage: FluidStream,
105105
rates_out_of_splitter: list[float] | None = None,
106106
streams_in_to_mixer: list[FluidStream] | None = None,
107-
) -> CompressorTrainStageResultSingleTimeStep:
108-
"""Evaluates a compressor train stage given the conditions and rate of the inlet stream, and the speed
109-
of the shaft driving the compressor if given.
110-
111-
Args:
112-
inlet_stream_stage (FluidStream): The conditions of the inlet fluid stream. If there are several inlet streams,
113-
the first one is the stage inlet stream, the others enter the stage at the Mixer.
114-
rates_out_of_splitter (list[float] | None, optional): Additional rates to the Splitter if defined.
115-
streams_in_to_mixer (list[FluidStream] | None, optional): Additional streams to the Mixer if defined.
116-
117-
Returns:
118-
CompressorTrainStageResultSingleTimeStep: The result of the evaluation for the compressor stage
119-
"""
107+
):
120108
# First the stream passes through the Splitter (if defined)
121109
if self.splitter is not None:
122110
self.splitter.rates_out_of_splitter = rates_out_of_splitter
@@ -156,7 +144,32 @@ def evaluate(
156144
else:
157145
inlet_stream_after_liquid_remover = inlet_stream_after_temperature_setter
158146

159-
inlet_stream_compressor = inlet_stream_after_liquid_remover
147+
return inlet_stream_after_liquid_remover
148+
149+
def evaluate(
150+
self,
151+
inlet_stream_stage: FluidStream,
152+
rates_out_of_splitter: list[float] | None = None,
153+
streams_in_to_mixer: list[FluidStream] | None = None,
154+
) -> CompressorTrainStageResultSingleTimeStep:
155+
"""Evaluates a compressor train stage given the conditions and rate of the inlet stream, and the speed
156+
of the shaft driving the compressor if given.
157+
158+
Args:
159+
inlet_stream_stage (FluidStream): The conditions of the inlet fluid stream. If there are several inlet streams,
160+
the first one is the stage inlet stream, the others enter the stage at the Mixer.
161+
rates_out_of_splitter (list[float] | None, optional): Additional rates to the Splitter if defined.
162+
streams_in_to_mixer (list[FluidStream] | None, optional): Additional streams to the Mixer if defined.
163+
164+
Returns:
165+
CompressorTrainStageResultSingleTimeStep: The result of the evaluation for the compressor stage
166+
"""
167+
168+
inlet_stream_compressor = self.get_compressor_inlet_stream(
169+
inlet_stream_stage=inlet_stream_stage,
170+
rates_out_of_splitter=rates_out_of_splitter,
171+
streams_in_to_mixer=streams_in_to_mixer,
172+
)
160173

161174
# Then additional rate is added by the RateModifier (if defined),
162175
inlet_stream_compressor_including_asv = self.add_rate(
@@ -166,7 +179,7 @@ def evaluate(
166179
# Compressor
167180
self.compressor.validate_speed()
168181
self.compressor.set_rate_before_asv(
169-
rate_before_asv_m3_per_h=inlet_stream_after_liquid_remover.volumetric_rate_m3_per_hour
182+
rate_before_asv_m3_per_h=inlet_stream_compressor.volumetric_rate_m3_per_hour
170183
)
171184

172185
outlet_stream_compressor_including_asv = self.compress(

src/libecalc/domain/process/entities/process_units/choke.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def propagate_stream(self, inlet_stream: FluidStream) -> FluidStream:
2525
if self._pressure_change > 0.0:
2626
pressure_bara = inlet_stream.pressure_bara - self._pressure_change
2727
if pressure_bara < 0.0:
28-
raise OutsideCapacityError("Unable to produce an outlet stream, trying to choke to negative pressure.")
28+
raise OutsideCapacityError("Trying to choke to negative pressure.")
2929
else:
3030
# Delta pressure = 0, i.e. don't do anything
3131
return inlet_stream
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import abc
2+
from collections.abc import Callable
3+
4+
from libecalc.domain.process.compressor.core.train.utils.common import EPSILON
5+
from libecalc.domain.process.entities.process_units.recirculation_loop import RecirculationLoop
6+
from libecalc.domain.process.entities.shaft import Shaft
7+
from libecalc.domain.process.process_solver.boundary import Boundary
8+
from libecalc.domain.process.process_solver.float_constraint import FloatConstraint
9+
from libecalc.domain.process.process_solver.search_strategies import BinarySearchStrategy, ScipyRootFindingStrategy
10+
from libecalc.domain.process.process_solver.solver import Solution
11+
from libecalc.domain.process.process_solver.solvers.recirculation_solver import (
12+
RecirculationConfiguration,
13+
RecirculationSolver,
14+
)
15+
from libecalc.domain.process.process_solver.solvers.speed_solver import SpeedConfiguration, SpeedSolver
16+
from libecalc.domain.process.process_system.process_error import RateTooLowError
17+
from libecalc.domain.process.process_system.process_system import ProcessSystem
18+
from libecalc.domain.process.process_system.process_unit import ProcessUnit
19+
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream
20+
21+
22+
class CompressorStageProcessUnit(ProcessUnit):
23+
@abc.abstractmethod
24+
def get_speed_boundary(self) -> Boundary: ...
25+
26+
@abc.abstractmethod
27+
def get_maximum_standard_rate(self, inlet_stream: FluidStream) -> float:
28+
"""
29+
Maximum standard rate ignoring speed
30+
"""
31+
...
32+
33+
34+
class CommonASVSolver:
35+
def __init__(
36+
self,
37+
shaft: Shaft,
38+
compressors: list[CompressorStageProcessUnit],
39+
fluid_service: FluidService,
40+
) -> None:
41+
self._shaft = shaft
42+
self._compressors = compressors
43+
self._fluid_service = fluid_service
44+
self._root_finding_strategy = ScipyRootFindingStrategy()
45+
self._recirculation_loop = RecirculationLoop(
46+
inner_process=ProcessSystem(
47+
process_units=self._compressors,
48+
),
49+
fluid_service=self._fluid_service,
50+
)
51+
52+
def get_recirculation_loop(self) -> RecirculationLoop:
53+
return self._recirculation_loop
54+
55+
def get_initial_speed_boundary(self) -> Boundary:
56+
speed_boundaries = [compressor.get_speed_boundary() for compressor in self._compressors]
57+
max_speed = max(speed_boundary.max for speed_boundary in speed_boundaries)
58+
min_speed = min(speed_boundary.min for speed_boundary in speed_boundaries)
59+
return Boundary(
60+
min=min_speed,
61+
max=max_speed,
62+
)
63+
64+
def get_maximum_recirculation_rate(self, inlet_stream: FluidStream) -> float:
65+
first_compressor = self._compressors[0]
66+
max_rate = first_compressor.get_maximum_standard_rate(inlet_stream=inlet_stream)
67+
return max(0.0, max_rate - inlet_stream.standard_rate_sm3_per_day)
68+
69+
def get_initial_recirculation_rate_boundary(
70+
self, inlet_stream: FluidStream, minimum_recirculation_rate: float = EPSILON
71+
) -> Boundary:
72+
return Boundary(
73+
min=minimum_recirculation_rate,
74+
max=self.get_maximum_recirculation_rate(inlet_stream=inlet_stream) * (1 - EPSILON),
75+
)
76+
77+
def get_recirculation_solver(
78+
self,
79+
boundary: Boundary,
80+
target_pressure: FloatConstraint | None = None,
81+
) -> RecirculationSolver:
82+
return RecirculationSolver(
83+
root_finding_strategy=self._root_finding_strategy,
84+
search_strategy=BinarySearchStrategy(tolerance=10e-3),
85+
recirculation_rate_boundary=boundary,
86+
target_pressure=target_pressure,
87+
)
88+
89+
def get_recirculation_func(self, inlet_stream: FluidStream) -> Callable[[RecirculationConfiguration], FluidStream]:
90+
def recirculation_func(configuration: RecirculationConfiguration) -> FluidStream:
91+
self._recirculation_loop.set_recirculation_rate(configuration.recirculation_rate)
92+
return self._recirculation_loop.propagate_stream(inlet_stream=inlet_stream)
93+
94+
return recirculation_func
95+
96+
def find_common_asv_solution(
97+
self, pressure_constraint: FloatConstraint, inlet_stream: FluidStream
98+
) -> tuple[Solution[SpeedConfiguration], Solution[RecirculationConfiguration]]:
99+
speed_solver = SpeedSolver(
100+
search_strategy=BinarySearchStrategy(),
101+
root_finding_strategy=self._root_finding_strategy,
102+
boundary=self.get_initial_speed_boundary(),
103+
target_pressure=pressure_constraint.value,
104+
)
105+
recirculation_solver_to_capacity = self.get_recirculation_solver(
106+
boundary=self.get_initial_recirculation_rate_boundary(inlet_stream=inlet_stream)
107+
)
108+
recirculation_func = self.get_recirculation_func(inlet_stream=inlet_stream)
109+
110+
def speed_func(configuration: SpeedConfiguration) -> FluidStream:
111+
try:
112+
self._shaft.set_speed(configuration.speed)
113+
self._recirculation_loop.set_recirculation_rate(0)
114+
return self._recirculation_loop.propagate_stream(inlet_stream=inlet_stream)
115+
except RateTooLowError:
116+
solution = recirculation_solver_to_capacity.solve(recirculation_func)
117+
self._recirculation_loop.set_recirculation_rate(solution.configuration.recirculation_rate)
118+
return self._recirculation_loop.propagate_stream(inlet_stream=inlet_stream)
119+
120+
speed_solution = speed_solver.solve(speed_func)
121+
self._shaft.set_speed(speed_solution.configuration.speed)
122+
recirculation_solver_with_target_pressure = self.get_recirculation_solver(
123+
boundary=self.get_initial_recirculation_rate_boundary(
124+
inlet_stream=inlet_stream,
125+
),
126+
target_pressure=pressure_constraint,
127+
)
128+
recirculation_solution = recirculation_solver_with_target_pressure.solve(recirculation_func)
129+
# Return solution with all configurations
130+
return speed_solution, recirculation_solution
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from math import isclose
2+
3+
from libecalc.domain.process.compressor.core.train.utils.common import PRESSURE_CALCULATION_TOLERANCE
4+
5+
6+
class FloatConstraint:
7+
def __init__(self, value, abs_tol: float = PRESSURE_CALCULATION_TOLERANCE):
8+
self.value = float(value)
9+
self.abs_tol = abs_tol
10+
11+
def _is_close(self, other):
12+
try:
13+
other_val = float(other)
14+
except (TypeError, ValueError):
15+
return NotImplemented
16+
17+
return isclose(self.value, other_val, rel_tol=0, abs_tol=self.abs_tol)
18+
19+
def __eq__(self, other):
20+
return self._is_close(other)
21+
22+
def __ne__(self, other):
23+
return not self._is_close(other)
24+
25+
def __lt__(self, other):
26+
try:
27+
other_val = float(other)
28+
except (TypeError, ValueError):
29+
return NotImplemented
30+
# a < b if a is not close to b and a < b
31+
return not self._is_close(other) and self.value < other_val
32+
33+
def __le__(self, other):
34+
try:
35+
float(other)
36+
except (TypeError, ValueError):
37+
return NotImplemented
38+
# a <= b if a < b or a approx equal to b
39+
return self.__lt__(other) or self._is_close(other)
40+
41+
def __gt__(self, other):
42+
try:
43+
other_val = float(other)
44+
except (TypeError, ValueError):
45+
return NotImplemented
46+
# a > b if a is not close to b and a > b
47+
return not self._is_close(other) and self.value > other_val
48+
49+
def __ge__(self, other):
50+
try:
51+
float(other)
52+
except (TypeError, ValueError):
53+
return NotImplemented
54+
# a >= b if a > b or a approx equal to b
55+
return self.__gt__(other) or self._is_close(other)
56+
57+
def __repr__(self):
58+
return f"FloatConstraint({self.value}, rel_tol={self.rel_tol}, abs_tol={self.abs_tol})"

src/libecalc/domain/process/process_solver/solvers/recirculation_solver.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Literal
44

55
from libecalc.domain.process.process_solver.boundary import Boundary
6+
from libecalc.domain.process.process_solver.float_constraint import FloatConstraint
67
from libecalc.domain.process.process_solver.search_strategies import RootFindingStrategy, SearchStrategy
78
from libecalc.domain.process.process_solver.solver import Solution, Solver
89
from libecalc.domain.process.process_system.process_error import RateTooHighError, RateTooLowError
@@ -13,14 +14,17 @@
1314
class RecirculationConfiguration:
1415
recirculation_rate: float
1516

17+
def __post_init__(self):
18+
self.recirculation_rate = float(self.recirculation_rate)
19+
1620

1721
class RecirculationSolver(Solver):
1822
def __init__(
1923
self,
2024
search_strategy: SearchStrategy,
2125
root_finding_strategy: RootFindingStrategy,
2226
recirculation_rate_boundary: Boundary,
23-
target_pressure: float | None = None,
27+
target_pressure: FloatConstraint | None = None,
2428
):
2529
self._recirculation_rate_boundary = recirculation_rate_boundary
2630
self._target_pressure = target_pressure
@@ -74,14 +78,20 @@ def bool_func(x: float, mode: Literal["minimize", "maximize"]) -> tuple[bool, bo
7478
minimum_outlet_stream = func(RecirculationConfiguration(recirculation_rate=minimum_rate))
7579
if minimum_outlet_stream.pressure_bara <= target_pressure:
7680
# Highest possible pressure is too low
77-
return Solution(success=False, configuration=RecirculationConfiguration(recirculation_rate=minimum_rate))
81+
return Solution(
82+
success=minimum_outlet_stream.pressure_bara == target_pressure,
83+
configuration=RecirculationConfiguration(recirculation_rate=minimum_rate),
84+
)
7885
maximum_outlet_stream = func(RecirculationConfiguration(recirculation_rate=maximum_rate))
7986
if maximum_outlet_stream.pressure_bara >= target_pressure:
8087
# Lowest possible pressure is too high
81-
return Solution(success=False, configuration=RecirculationConfiguration(recirculation_rate=maximum_rate))
88+
return Solution(
89+
success=maximum_outlet_stream.pressure_bara == self._target_pressure,
90+
configuration=RecirculationConfiguration(recirculation_rate=maximum_rate),
91+
)
8292

8393
recirculation_rate = self._root_finding_strategy.find_root(
8494
boundary=Boundary(min=minimum_rate, max=maximum_rate),
85-
func=lambda x: func(RecirculationConfiguration(recirculation_rate=x)).pressure_bara - target_pressure,
95+
func=lambda x: func(RecirculationConfiguration(recirculation_rate=x)).pressure_bara - target_pressure.value,
8696
)
8797
return Solution(success=True, configuration=RecirculationConfiguration(recirculation_rate=recirculation_rate))

src/libecalc/domain/process/process_solver/solvers/speed_solver.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class SpeedConfiguration:
1616
speed: float
1717

1818

19-
class SpeedSolver(Solver[SpeedConfiguration]):
19+
class SpeedSolver(Solver):
2020
def __init__(
2121
self,
2222
search_strategy: SearchStrategy,
@@ -43,6 +43,9 @@ def get_outlet_stream(speed: float) -> FluidStream:
4343
configuration=max_speed_configuration,
4444
)
4545

46+
if maximum_speed_outlet_stream.pressure_bara < self._target_pressure:
47+
return Solution(success=False, configuration=SpeedConfiguration(self._boundary.max))
48+
4649
try:
4750
minimum_speed_configuration = SpeedConfiguration(speed=self._boundary.min)
4851
minimum_speed_outlet_stream = func(minimum_speed_configuration)
@@ -65,26 +68,26 @@ def bool_speed_func(x: float):
6568
)
6669
minimum_speed_configuration = SpeedConfiguration(speed=minimum_speed_within_capacity)
6770
minimum_speed_outlet_stream = func(minimum_speed_configuration)
68-
if (
71+
72+
if minimum_speed_outlet_stream.pressure_bara > self._target_pressure:
73+
# Solution 2, target pressure is too low
74+
return Solution(success=False, configuration=minimum_speed_configuration)
75+
76+
assert (
6977
minimum_speed_outlet_stream.pressure_bara
7078
<= self._target_pressure
7179
<= maximum_speed_outlet_stream.pressure_bara
72-
):
73-
# Solution 1, iterate on speed until target discharge pressure is found
74-
def root_speed_func(x: float) -> float:
75-
# We should be able to produce an outlet stream since we adjust minimum speed above,
76-
# or exit if max speed is not enough
77-
out = get_outlet_stream(speed=x)
78-
return out.pressure_bara - self._target_pressure
79-
80-
speed = self._root_finding_strategy.find_root(
81-
boundary=Boundary(min=minimum_speed_configuration.speed, max=self._boundary.max),
82-
func=root_speed_func,
83-
)
84-
return Solution(success=True, configuration=SpeedConfiguration(speed=speed))
85-
elif self._target_pressure < minimum_speed_outlet_stream.pressure_bara:
86-
# Solution 2, target pressure is too low
87-
return Solution(success=False, configuration=SpeedConfiguration(minimum_speed_configuration.speed))
80+
)
81+
82+
# Solution 1, iterate on speed until target discharge pressure is found
83+
def root_speed_func(x: float) -> float:
84+
# We should be able to produce an outlet stream since we adjust minimum speed above,
85+
# or exit if max speed is not enough
86+
out = get_outlet_stream(speed=x)
87+
return out.pressure_bara - self._target_pressure
8888

89-
# Solution 3, target discharge pressure is too high
90-
return Solution(success=False, configuration=SpeedConfiguration(self._boundary.max))
89+
speed = self._root_finding_strategy.find_root(
90+
boundary=Boundary(min=minimum_speed_configuration.speed, max=self._boundary.max),
91+
func=root_speed_func,
92+
)
93+
return Solution(success=True, configuration=SpeedConfiguration(speed=speed))

0 commit comments

Comments
 (0)