diff --git a/src/libecalc/domain/process/process_solver/feasibility_solver.py b/src/libecalc/domain/process/process_solver/feasibility_solver.py new file mode 100644 index 0000000000..77bc59bd05 --- /dev/null +++ b/src/libecalc/domain/process/process_solver/feasibility_solver.py @@ -0,0 +1,73 @@ +from libecalc.domain.process.entities.process_units.compressor import Compressor +from libecalc.domain.process.process_solver.float_constraint import FloatConstraint +from libecalc.domain.process.process_solver.outlet_pressure_solver import OutletPressureSolver +from libecalc.domain.process.process_solver.process_runner import ProcessRunner +from libecalc.domain.process.value_objects.fluid_stream import FluidStream + + +class FeasibilitySolver: + """ + Calculates how much of a given inlet rate exceeds what a compressor train + can handle for a target pressure. + + Orchestrates OutletPressureSolver and queries compressor charts to find + the feasible rate — the excess is redirected via stream distribution. + """ + + def __init__( + self, + outlet_pressure_solver: OutletPressureSolver, + compressors: list[Compressor], + runner: ProcessRunner, + ): + self._solver = outlet_pressure_solver + self._compressors = compressors + self._runner = runner + + def get_excess_rate( + self, + inlet_stream: FluidStream, + target_pressure: FloatConstraint, + ) -> float: + """ + Rate [sm³/day] that exceeds what this train can handle. + + This is the amount that must be redirected (e.g. via overflow) + to another train in the stream distribution. + """ + feasible = self._find_feasible_rate(inlet_stream, target_pressure) + excess_rate = max(0.0, inlet_stream.standard_rate_sm3_per_day - feasible) + return excess_rate + + def _find_feasible_rate( + self, + inlet_stream: FluidStream, + target_pressure: FloatConstraint, + ) -> float: + """Highest standard rate [sm³/day] for which the train can meet target_pressure. + + Returns the full inlet rate if the solver succeeds, otherwise finds the + bottleneck compressor's stone wall limit at the current operating point. + """ + solution = self._solver.find_solution(target_pressure, inlet_stream) + + if solution.success: + # The train can handle the full rate — no need to search for a bottleneck. + return inlet_stream.standard_rate_sm3_per_day + + # Apply the configuration before querying compressor charts. + self._runner.apply_configurations(solution.configuration) + + # Search for compressor with the lowest max rate + min_max_rate = float("inf") + for compressor in self._compressors: + compressor_inlet = self._runner.run( + inlet_stream=inlet_stream, + to_id=compressor.get_id(), + ) + max_rate = compressor.get_maximum_standard_rate(compressor_inlet) + min_max_rate = min(min_max_rate, max_rate) + + feasible_rate = max(0.0, min_max_rate) + + return feasible_rate diff --git a/src/libecalc/domain/process/stream_distribution/common_stream_distribution.py b/src/libecalc/domain/process/stream_distribution/common_stream_distribution.py index 8efc5f2309..3accc0b023 100644 --- a/src/libecalc/domain/process/stream_distribution/common_stream_distribution.py +++ b/src/libecalc/domain/process/stream_distribution/common_stream_distribution.py @@ -10,9 +10,9 @@ from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream -class HasCapacity(abc.ABC): +class HasExcessRate(abc.ABC): @abc.abstractmethod - def get_unhandled_rate(self, rate: float, pressure: float) -> float: ... + def get_excess_rate(self, inlet_stream: FluidStream) -> float: ... T = TypeVar("T", bound=Hashable) @@ -28,7 +28,7 @@ class CommonStreamDistribution(StreamDistribution, Generic[T]): def __init__( self, inlet_stream: FluidStream, - items: dict[T, HasCapacity], + items: dict[T, HasExcessRate], rate_fractions: list[float], overflows: list[Overflow[T]], fluid_service: FluidService, @@ -69,9 +69,15 @@ def _adjust_for_overflow(self) -> dict[T, float]: current_rate = rate + overflow_rate if overflow is not None: item = self._items[item_id] - unhandled_rate = item.get_unhandled_rate(current_rate, self._inlet_stream.pressure_bara) - handled_rate = current_rate - unhandled_rate - overflow_map[overflow.to_id].append(unhandled_rate) + stream = self._fluid_service.create_stream_from_standard_rate( + fluid_model=self._inlet_stream.fluid_model, + standard_rate_m3_per_day=current_rate, + temperature_kelvin=self._inlet_stream.temperature_kelvin, + pressure_bara=self._inlet_stream.pressure_bara, + ) + excess_rate = item.get_excess_rate(stream) + handled_rate = current_rate - excess_rate + overflow_map[overflow.to_id].append(excess_rate) adjusted_rates[item_id] = handled_rate else: adjusted_rates[item_id] = current_rate diff --git a/src/libecalc/presentation/yaml/mappers/process_simulation_mapper.py b/src/libecalc/presentation/yaml/mappers/process_simulation_mapper.py new file mode 100644 index 0000000000..ba39456817 --- /dev/null +++ b/src/libecalc/presentation/yaml/mappers/process_simulation_mapper.py @@ -0,0 +1,27 @@ +from libecalc.domain.process.process_solver.feasibility_solver import FeasibilitySolver +from libecalc.domain.process.process_solver.float_constraint import FloatConstraint +from libecalc.domain.process.stream_distribution.common_stream_distribution import HasExcessRate +from libecalc.domain.process.stream_distribution.priorities_stream_distribution import HasValidity +from libecalc.domain.process.value_objects.fluid_stream import FluidStream + + +class StreamDistributionItem(HasExcessRate, HasValidity): + """Connects a compressor train's solver to the stream distribution system.""" + + def __init__( + self, + feasibility_solver: FeasibilitySolver, + target_pressure: FloatConstraint, + ): + self._feasibility_solver = feasibility_solver + self._target_pressure = target_pressure + + def is_valid(self, inlet_stream: FluidStream) -> bool: + """Can the train operate at these inlet conditions?""" + return self.get_excess_rate(inlet_stream) == 0.0 + + def get_excess_rate(self, inlet_stream: FluidStream) -> float: + """How much rate (sm³/day) exceeds this train's capacity?""" + return self._feasibility_solver.get_excess_rate( + inlet_stream=inlet_stream, target_pressure=self._target_pressure + ) diff --git a/tests/libecalc/application/test_stream_distribution.py b/tests/libecalc/application/test_stream_distribution.py index 5d9b903f5c..69af9b8e2e 100644 --- a/tests/libecalc/application/test_stream_distribution.py +++ b/tests/libecalc/application/test_stream_distribution.py @@ -4,19 +4,20 @@ from inline_snapshot import snapshot from libecalc.domain.process.stream_distribution.common_stream_distribution import ( CommonStreamDistribution, - HasCapacity, + HasExcessRate, Overflow, ) from libecalc.domain.component_validation_error import DomainValidationException +from libecalc.domain.process.value_objects.fluid_stream import FluidStream -class Item(HasCapacity): +class Item(HasExcessRate): def __init__(self, capacity: float): self.id = uuid4() self._capacity = capacity - def get_unhandled_rate(self, rate: float, pressure: float) -> float: - return max(0.0, rate - self._capacity) + def get_excess_rate(self, inlet_stream: FluidStream) -> float: + return max(0.0, inlet_stream.standard_rate_sm3_per_day - self._capacity) class TestCommonStreamDistribution: