Skip to content

Commit f70693e

Browse files
committed
fix(libecalc): catch InsufficientInletPressureError in finders
Handle InsufficientInletPressureError in ShaftSpeedFinder, RecirculationLoopRateFinder, and UpstreamChokeDeltaPressureFinder so it doesn't crash during root-finding. Add from_error classmethod to InsufficientInletPressureFailure.
1 parent 45df459 commit f70693e

6 files changed

Lines changed: 119 additions & 24 deletions

File tree

src/libecalc/process/process_solver/finders/choke_delta_pressure_finders.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
from libecalc.domain.process.compressor.core.train.utils.common import PRESSURE_CALCULATION_TOLERANCE
44
from libecalc.process.fluid_stream.fluid_stream import FluidStream
5-
from libecalc.process.process_pipeline.process_error import CompressorStonewallError
5+
from libecalc.process.process_pipeline.process_error import CompressorStonewallError, InsufficientInletPressureError
66
from libecalc.process.process_solver.boundary import Boundary
77
from libecalc.process.process_solver.configuration import ChokeConfiguration
88
from libecalc.process.process_solver.finder import Finder, Finding
99
from libecalc.process.process_solver.search_strategies import RootFindingStrategy
1010
from libecalc.process.process_solver.solver import (
1111
CompressorStonewallFailure,
12+
InsufficientInletPressureFailure,
1213
TargetDirection,
1314
TargetPressureUnreachableFailure,
1415
)
@@ -38,30 +39,38 @@ def outlet_pressure(delta_pressure: float) -> float:
3839

3940
# Upstream choking increase the rate (lower pressure, higher volume)
4041
# If we choke too much, to rate will exceed the capacity / stonewall
41-
# If so, bisect to the highest ΔP that doesn't exceed stonewall
42-
stonewall_error: CompressorStonewallError | None = None
42+
# or downstream pressure drop becomes infeasible.
43+
# If so, bisect to the highest ΔP that doesn't exceed capacity.
44+
capacity_error: CompressorStonewallError | InsufficientInletPressureError | None = None
4345
try:
4446
outlet_pressure(self._boundary.max)
4547
search_max = self._boundary.max
46-
except CompressorStonewallError as e:
47-
stonewall_error = e
48+
except (CompressorStonewallError, InsufficientInletPressureError) as e:
49+
capacity_error = e
4850
lo, hi = self._boundary.min, self._boundary.max
4951
while hi - lo > PRESSURE_CALCULATION_TOLERANCE:
5052
mid = (lo + hi) / 2
5153
try:
5254
outlet_pressure(mid)
5355
lo = mid
54-
except CompressorStonewallError as bisect_error:
55-
stonewall_error = bisect_error
56+
except (CompressorStonewallError, InsufficientInletPressureError) as bisect_error:
57+
capacity_error = bisect_error
5658
hi = mid
5759
search_max = lo
5860

5961
max_pressure = outlet_pressure(search_max)
6062
closest = ChokeConfiguration(delta_pressure=search_max)
6163

6264
if max_pressure > self._target_pressure:
63-
if stonewall_error is not None:
64-
return Finding(configuration=closest, failure=CompressorStonewallFailure.from_error(stonewall_error))
65+
if capacity_error is not None:
66+
if isinstance(capacity_error, CompressorStonewallError):
67+
return Finding(configuration=closest, failure=CompressorStonewallFailure.from_error(capacity_error))
68+
if isinstance(capacity_error, InsufficientInletPressureError):
69+
return Finding(
70+
configuration=closest,
71+
failure=InsufficientInletPressureFailure.from_error(capacity_error),
72+
)
73+
raise capacity_error
6574
return Finding(
6675
configuration=closest,
6776
failure=TargetPressureUnreachableFailure(

src/libecalc/process/process_solver/finders/recirculation_loop_rate_finder.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
from typing import Literal
33

44
from libecalc.process.fluid_stream.fluid_stream import FluidStream
5-
from libecalc.process.process_pipeline.process_error import CompressorStonewallError, CompressorSurgeError
5+
from libecalc.process.process_pipeline.process_error import (
6+
CompressorStonewallError,
7+
CompressorSurgeError,
8+
InsufficientInletPressureError,
9+
)
610
from libecalc.process.process_solver.boundary import Boundary
711
from libecalc.process.process_solver.configuration import RecirculationConfiguration
812
from libecalc.process.process_solver.finder import Finder, Finding
913
from libecalc.process.process_solver.float_constraint import FloatConstraint
1014
from libecalc.process.process_solver.search_strategies import Bisect, BisectResult, RootFindingStrategy
1115
from libecalc.process.process_solver.solver import (
1216
CompressorStonewallFailure,
17+
InsufficientInletPressureFailure,
1318
TargetDirection,
1419
TargetPressureUnreachableFailure,
1520
)
@@ -37,6 +42,12 @@ def find(self, func: Callable[[RecirculationConfiguration], FluidStream]) -> Fin
3742
configuration=RecirculationConfiguration(recirculation_rate=self._recirculation_rate_boundary.min),
3843
failure=CompressorStonewallFailure.from_error(e),
3944
)
45+
except InsufficientInletPressureError as e:
46+
# Pressure infeasible at minimum recirculation; adding more flow won't fix pressure.
47+
return Finding(
48+
configuration=RecirculationConfiguration(recirculation_rate=self._recirculation_rate_boundary.min),
49+
failure=InsufficientInletPressureFailure.from_error(e),
50+
)
4051

4152
target_pressure = self._target_pressure
4253
if target_pressure is None:
@@ -126,5 +137,5 @@ def _bool_func(
126137
return BisectResult(higher=mode != "minimize", accepted=True)
127138
except CompressorSurgeError:
128139
return BisectResult(higher=True, accepted=False)
129-
except CompressorStonewallError:
140+
except (CompressorStonewallError, InsufficientInletPressureError):
130141
return BisectResult(higher=False, accepted=False)

src/libecalc/process/process_solver/finders/shaft_speed_finder.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33

44
from libecalc.domain.process.compressor.core.exceptions import CompressorThermodynamicCalculationError
55
from libecalc.process.fluid_stream.fluid_stream import FluidStream
6-
from libecalc.process.process_pipeline.process_error import CompressorStonewallError, CompressorSurgeError
6+
from libecalc.process.process_pipeline.process_error import (
7+
CompressorStonewallError,
8+
CompressorSurgeError,
9+
InsufficientInletPressureError,
10+
)
711
from libecalc.process.process_solver.boundary import Boundary
812
from libecalc.process.process_solver.configuration import SpeedConfiguration
913
from libecalc.process.process_solver.finder import Finder, Finding
1014
from libecalc.process.process_solver.search_strategies import Bisect, BisectResult, RootFindingStrategy
1115
from libecalc.process.process_solver.solver import (
1216
CompressorStonewallFailure,
1317
CompressorSurgeFailure,
18+
InsufficientInletPressureFailure,
1419
TargetDirection,
1520
TargetPressureUnreachableFailure,
1621
ThermodynamicCalculationFailure,
@@ -60,6 +65,12 @@ def find(self, func: Callable[[SpeedConfiguration], FluidStream]) -> Finding[Spe
6065
boundary=Boundary(min=self._boundary.min, max=valid_max),
6166
target_pressure=self._target_pressure,
6267
).find(func)
68+
except InsufficientInletPressureError as e:
69+
logger.debug(f"Insufficient inlet pressure at maximum speed: {max_speed_configuration}")
70+
return Finding(
71+
configuration=max_speed_configuration,
72+
failure=InsufficientInletPressureFailure.from_error(e),
73+
)
6374

6475
if maximum_speed_outlet_stream.pressure_bara < self._target_pressure:
6576
return Finding(
@@ -81,6 +92,13 @@ def find(self, func: Callable[[SpeedConfiguration], FluidStream]) -> Finding[Spe
8192
min_config = SpeedConfiguration(speed=self._boundary.min)
8293
logger.debug(f"No solution found for minimum speed: {min_config}")
8394
return Finding(configuration=min_config, failure=CompressorStonewallFailure.from_error(e))
95+
except InsufficientInletPressureError as e:
96+
min_config = SpeedConfiguration(speed=self._boundary.min)
97+
logger.debug(f"Insufficient inlet pressure at minimum speed: {min_config}")
98+
return Finding(
99+
configuration=min_config,
100+
failure=InsufficientInletPressureFailure.from_error(e),
101+
)
84102

85103
if minimum_speed_outlet_stream.pressure_bara > self._target_pressure:
86104
return Finding(
@@ -116,30 +134,40 @@ def _eos_ok(func: Callable[[SpeedConfiguration], FluidStream], speed: float) ->
116134
try:
117135
func(SpeedConfiguration(speed=speed))
118136
return True
119-
except (CompressorThermodynamicCalculationError, CompressorStonewallError, CompressorSurgeError):
137+
except (
138+
CompressorThermodynamicCalculationError,
139+
CompressorStonewallError,
140+
CompressorSurgeError,
141+
InsufficientInletPressureError,
142+
):
120143
return False
121144

122145
def _find_min_within_capacity_speed(
123146
self, func: Callable[[SpeedConfiguration], FluidStream]
124147
) -> tuple[SpeedConfiguration, FluidStream]:
125148
"""Return the lowest speed configuration within flow capacity, and its outlet stream.
126149
127-
``CompressorStonewallError`` and ``CompressorThermodynamicCalculationError`` at the boundary
128-
minimum are recoverable: higher speed raises the stonewall limit or enters the valid
129-
EOS range; search upward.
150+
``CompressorStonewallError``, ``CompressorThermodynamicCalculationError``, and
151+
``InsufficientInletPressureError`` at the boundary minimum are recoverable:
152+
higher speed raises the stonewall limit or enters the valid EOS/pressure range;
153+
search upward.
130154
"""
131155
minimum_speed_configuration = SpeedConfiguration(speed=self._boundary.min)
132156
try:
133157
minimum_result = func(minimum_speed_configuration)
134158
return minimum_speed_configuration, minimum_result
135-
except (CompressorStonewallError, CompressorThermodynamicCalculationError) as e:
159+
except (CompressorStonewallError, CompressorThermodynamicCalculationError, InsufficientInletPressureError) as e:
136160
logger.debug(f"No solution found for minimum speed: {self._boundary.min}", exc_info=e)
137161

138162
def bool_speed_func(x: float) -> BisectResult:
139163
try:
140164
func(SpeedConfiguration(speed=x))
141165
return BisectResult(higher=False, accepted=True)
142-
except (CompressorStonewallError, CompressorThermodynamicCalculationError):
166+
except (
167+
CompressorStonewallError,
168+
CompressorThermodynamicCalculationError,
169+
InsufficientInletPressureError,
170+
):
143171
return BisectResult(higher=True, accepted=False)
144172
except CompressorSurgeError:
145173
return BisectResult(higher=False, accepted=False)

src/libecalc/process/process_solver/pipeline_section_solver.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
CompressorStonewallFailure,
1919
CompressorSurgeFailure,
2020
ConvergenceFailure,
21+
InsufficientInletPressureFailure,
2122
Solution,
2223
TargetDirection,
2324
TargetPressureUnreachableFailure,
@@ -112,7 +113,13 @@ def _find_solution(
112113
)
113114

114115
if isinstance(
115-
speed_finding.failure, (CompressorStonewallFailure, CompressorSurgeFailure, ThermodynamicCalculationFailure)
116+
speed_finding.failure,
117+
(
118+
CompressorStonewallFailure,
119+
CompressorSurgeFailure,
120+
ThermodynamicCalculationFailure,
121+
InsufficientInletPressureFailure,
122+
),
116123
):
117124
return Solution(configuration=[shaft_config], failure=speed_finding.failure)
118125

src/libecalc/process/process_solver/solver.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ class InsufficientInletPressureFailure(SolverFailure):
107107
inlet_pressure_bara: float | None = None
108108
required_delta_pressure_bara: float | None = None
109109

110+
@classmethod
111+
def from_error(cls, e: InsufficientInletPressureError) -> Self:
112+
return cls(
113+
process_unit_id=e.process_unit_id,
114+
inlet_pressure_bara=e.inlet_pressure_bara,
115+
required_delta_pressure_bara=e.required_delta_pressure_bara,
116+
)
117+
110118

111119
@dataclass
112120
class OfftakeExceedsInletFailure(SolverFailure):
@@ -131,11 +139,7 @@ def process_error_to_failure(e: ProcessError) -> SolverFailure:
131139
process_unit_id=e.process_unit_id, available_rate=e.available_rate, offtake_rate=e.offtake_rate
132140
)
133141
if isinstance(e, InsufficientInletPressureError):
134-
return InsufficientInletPressureFailure(
135-
process_unit_id=e.process_unit_id,
136-
inlet_pressure_bara=e.inlet_pressure_bara,
137-
required_delta_pressure_bara=e.required_delta_pressure_bara,
138-
)
142+
return InsufficientInletPressureFailure.from_error(e)
139143
if isinstance(e, CompressorThermodynamicCalculationError):
140144
return ThermodynamicCalculationFailure(reason=str(e))
141145
if isinstance(e, CompressorStonewallError):
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Regression: InsufficientInletPressureError must not crash finders."""
2+
3+
from libecalc.process.process_pipeline.process_error import InsufficientInletPressureError
4+
from libecalc.process.process_pipeline.process_unit import ProcessUnit
5+
from libecalc.process.process_solver.boundary import Boundary
6+
from libecalc.process.process_solver.finders.shaft_speed_finder import ShaftSpeedFinder, SpeedConfiguration
7+
from libecalc.process.process_solver.solver import InsufficientInletPressureFailure
8+
9+
DROPPER_ID = ProcessUnit._create_id()
10+
11+
12+
def test_shaft_speed_finder_catches_insufficient_inlet_pressure(
13+
search_strategy_factory,
14+
root_finding_strategy,
15+
):
16+
"""ShaftSpeedFinder returns a failure when func raises InsufficientInletPressureError."""
17+
18+
def func(_config: SpeedConfiguration):
19+
raise InsufficientInletPressureError(
20+
process_unit_id=DROPPER_ID,
21+
inlet_pressure_bara=25.0,
22+
required_delta_pressure_bara=75.0,
23+
)
24+
25+
finder = ShaftSpeedFinder(
26+
search_strategy=search_strategy_factory(tolerance=1.0),
27+
root_finding_strategy=root_finding_strategy,
28+
boundary=Boundary(min=50, max=100),
29+
target_pressure=40.0,
30+
)
31+
32+
finding = finder.find(func)
33+
34+
assert finding.failure is not None
35+
assert isinstance(finding.failure, InsufficientInletPressureFailure)
36+
assert finding.failure.process_unit_id == DROPPER_ID

0 commit comments

Comments
 (0)