Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/libecalc/domain/process/entities/process_units/choke.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from typing import Final

from libecalc.domain.process.process_pipeline.process_error import OutsideCapacityError
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.process_pipeline.process_unit import GasProcessUnit, ProcessUnitId
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream


class Choke(ProcessUnit):
class Choke(GasProcessUnit):
def __init__(
self,
fluid_service: FluidService,
process_unit_id: ProcessUnitId | None = None,
pressure_change: float = 0.0,
):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or GasProcessUnit._create_id()
self._pressure_change = pressure_change
self._fluid_service = fluid_service

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
calculate_outlet_pressure_and_stream,
)
from libecalc.domain.process.process_pipeline.process_error import RateTooHighError, RateTooLowError
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.process_pipeline.process_unit import GasProcessUnit, ProcessUnitId
from libecalc.domain.process.process_solver.boundary import Boundary
from libecalc.domain.process.value_objects.chart.chart import ChartData
from libecalc.domain.process.value_objects.chart.compressor import CompressorChart
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream


class Compressor(ProcessUnit):
class Compressor(GasProcessUnit):
def __init__(
self,
compressor_chart: ChartData,
fluid_service: FluidService,
process_unit_id: ProcessUnitId | None = None,
):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or GasProcessUnit._create_id()
self._compressor_chart = CompressorChart(compressor_chart)
self._fluid_service = fluid_service
self._speed: float | None = None
Expand Down Expand Up @@ -86,6 +86,14 @@ def set_speed(self, speed: float) -> None:
"""Set the rotational speed used for chart lookup."""
self._speed = speed

@property
def minimum_speed(self) -> float:
return self._compressor_chart.minimum_speed

@property
def maximum_speed(self) -> float:
return self._compressor_chart.maximum_speed

def get_minimum_standard_rate(self, inlet_stream: FluidStream) -> float:
"""Minimum standard volumetric rate [sm³/day] at current speed.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
from typing import Final

from libecalc.common.units import UnitConstants
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.value_objects.fluid_stream import FluidStream
from libecalc.common.utils.ecalc_uuid import ecalc_id_generator
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnitId
from libecalc.domain.process.value_objects.stream_protocol import MixableStream


class DirectMixer(ProcessUnit):
class DirectMixer:
def __init__(self, mix_rate: float = 0, process_unit_id: ProcessUnitId | None = None):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnitId(ecalc_id_generator())
self._mix_rate = mix_rate

def get_id(self) -> ProcessUnitId:
return self._id

def propagate_stream(self, inlet_stream: FluidStream) -> FluidStream:
added_mass_kg_per_h = (
self._mix_rate * inlet_stream.standard_density_gas_phase_after_flash / UnitConstants.HOURS_PER_DAY
)
def propagate_stream(self, inlet_stream: MixableStream) -> MixableStream:
added_mass_kg_per_h = self._mix_rate * inlet_stream.standard_density / UnitConstants.HOURS_PER_DAY
return inlet_stream.with_mass_rate(inlet_stream.mass_rate_kg_per_h + added_mass_kg_per_h)

def get_mix_rate(self) -> float:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
from typing import Final

from libecalc.common.units import UnitConstants
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.value_objects.fluid_stream import FluidStream
from libecalc.common.utils.ecalc_uuid import ecalc_id_generator
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnitId
from libecalc.domain.process.value_objects.stream_protocol import MixableStream


class DirectSplitter(ProcessUnit):
class DirectSplitter:
def __init__(self, process_unit_id: ProcessUnitId | None = None, split_rate: float = 0):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnitId(ecalc_id_generator())
self._split_rate = split_rate

def get_id(self) -> ProcessUnitId:
return self._id

def propagate_stream(self, inlet_stream: FluidStream) -> FluidStream:
removed_mass_kg_per_h = (
self._split_rate * inlet_stream.standard_density_gas_phase_after_flash / UnitConstants.HOURS_PER_DAY
)
def propagate_stream(self, inlet_stream: MixableStream) -> MixableStream:
removed_mass_kg_per_h = self._split_rate * inlet_stream.standard_density / UnitConstants.HOURS_PER_DAY
return inlet_stream.with_mass_rate(inlet_stream.mass_rate_kg_per_h - removed_mass_kg_per_h)

def set_split_rate(self, split_rate: float):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Final

from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.process_pipeline.process_unit import GasProcessUnit, ProcessUnitId
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream
from libecalc.domain.process.value_objects.fluid_stream.constants import ThermodynamicConstants


class LiquidRemover(ProcessUnit):
class LiquidRemover(GasProcessUnit):
def __init__(self, fluid_service: FluidService, process_unit_id: ProcessUnitId | None = None):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or GasProcessUnit._create_id()
self._fluid_service = fluid_service

def get_id(self) -> ProcessUnitId:
Expand Down
6 changes: 3 additions & 3 deletions src/libecalc/domain/process/entities/process_units/mixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
from libecalc.domain.process.entities.process_units.simplified_stream_mixer.simplified_stream_mixer import (
SimplifiedStreamMixer,
)
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.process_pipeline.process_unit import GasProcessUnit, ProcessUnitId
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream


class Mixer(ProcessUnit):
class Mixer(GasProcessUnit):
"""Mixes one external stream into the through-stream.

The external stream is set via `set_stream` before propagation. This models
a sidestream injection point in an interstage manifold.
"""

def __init__(self, fluid_service: FluidService, process_unit_id: ProcessUnitId | None = None):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or GasProcessUnit._create_id()
self._mixer = SimplifiedStreamMixer(fluid_service)
self._external_stream: FluidStream | None = None

Expand Down
173 changes: 173 additions & 0 deletions src/libecalc/domain/process/entities/process_units/pump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from libecalc.domain.process.process_pipeline.liquid_process_unit import LiquidProcessUnit
from libecalc.domain.process.process_pipeline.process_error import RateTooHighError, RateTooLowError
from libecalc.domain.process.process_pipeline.process_unit import ProcessUnitId
from libecalc.domain.process.process_solver.boundary import Boundary
from libecalc.domain.process.value_objects.chart.chart import Chart, ChartData
from libecalc.domain.process.value_objects.liquid_stream import LiquidStream

_BARA_TO_PASCAL = 1e5


class Pump(LiquidProcessUnit):
"""Pump stage operating on a liquid stream.

Physics:
head [J/kg] = pressure_rise [Pa] / density [kg/m³]
power [MW] = density × head × Q [m³/s] / efficiency / 1e6

For single-speed pumps the chart has one curve — no set_speed() needed.
For variable-speed pumps, set_speed() must be called before propagate_stream().
"""

def __init__(
self,
process_unit_id: ProcessUnitId,
pump_chart: ChartData,
) -> None:
self._id = process_unit_id
self._pump_chart = Chart(pump_chart)
self._speed: float | None = None
self._last_head_joule_per_kg: float | None = None
self._last_efficiency: float | None = None
self._last_mass_rate_kg_per_s: float | None = None

def get_id(self) -> ProcessUnitId:
return self._id

def set_speed(self, speed: float) -> None:
self._speed = speed

@property
def minimum_flow_rate(self) -> float:
"""Minimum volumetric flow rate [m³/h] at current speed."""
return float(self._pump_chart.minimum_rate_as_function_of_speed(self.speed))

@property
def maximum_flow_rate(self) -> float:
"""Maximum volumetric flow rate [m³/h] at current speed."""
return float(self._pump_chart.maximum_rate_as_function_of_speed(self.speed))

def get_recirculation_range(self, inlet_stream: LiquidStream) -> Boundary:
"""How much recirculation (Sm³/day) is needed/available to stay within pump capacity.

Standard ≈ actual for liquids; min/max rates [m³/h] × 24 → Sm³/day.
"""
min_sm3_day = self.minimum_flow_rate * 24.0
max_sm3_day = self.maximum_flow_rate * 24.0
inlet_sm3_day = inlet_stream.standard_rate_sm3_per_day
return Boundary(
min=max(0.0, min_sm3_day - inlet_sm3_day),
max=max(0.0, max_sm3_day - inlet_sm3_day),
)

@property
def speed(self) -> float:
if self._speed is None:
if not self._pump_chart.is_variable_speed:
return self._pump_chart.minimum_speed
raise ValueError("Speed not set. Variable-speed Pump must be registered on a Shaft.")
return self._speed

def propagate_stream(self, inlet_stream: LiquidStream) -> LiquidStream:
"""Propagate liquid stream through the pump.

Computes outlet pressure from hydraulic head and tracks shaft power.
Raises RateTooLowError / RateTooHighError if the operating point is
outside the pump chart envelope at the current speed.
"""
rate = inlet_stream.volumetric_rate_m3_per_hour
density = inlet_stream.density_kg_per_m3

head, efficiency = self._head_and_efficiency_at_rate(rate)

min_rate = float(self._pump_chart.minimum_rate_as_function_of_head(head))
max_rate = float(self._pump_chart.maximum_rate_as_function_of_head(head))

if rate < min_rate:
raise RateTooLowError(
actual_rate=rate,
boundary_rate=min_rate,
process_unit_id=self._id,
)

if rate > max_rate:
raise RateTooHighError(
actual_rate=rate,
boundary_rate=max_rate,
process_unit_id=self._id,
)

self._last_head_joule_per_kg = head
self._last_efficiency = efficiency
self._last_mass_rate_kg_per_s = inlet_stream.mass_rate_kg_per_h / 3600.0

pressure_rise_bara = head * density / _BARA_TO_PASCAL
outlet_pressure = inlet_stream.pressure_bara + pressure_rise_bara

return inlet_stream.with_pressure(outlet_pressure)

@property
def minimum_speed(self) -> float:
return self._pump_chart.minimum_speed

@property
def maximum_speed(self) -> float:
return self._pump_chart.maximum_speed

@property
def last_shaft_power_mw(self) -> float:
"""Shaft power [MW] from the most recent propagate_stream() call.

P = ṁ [kg/s] × head [J/kg] / η / 1e6
"""
if (
self._last_head_joule_per_kg is None
or self._last_efficiency is None
or self._last_mass_rate_kg_per_s is None
):
raise ValueError("No result available — call propagate_stream() first.")
return self._last_mass_rate_kg_per_s * self._last_head_joule_per_kg / self._last_efficiency / 1e6

@property
def last_head_joule_per_kg(self) -> float:
"""Hydraulic head [J/kg] from the most recent propagate_stream() call."""
if self._last_head_joule_per_kg is None:
raise ValueError("No result available — call propagate_stream() first.")
return self._last_head_joule_per_kg

@property
def last_efficiency(self) -> float:
"""Pump efficiency [-] from the most recent propagate_stream() call."""
if self._last_efficiency is None:
raise ValueError("No result available — call propagate_stream() first.")
return self._last_efficiency

def _head_and_efficiency_at_rate(self, rate_m3_per_hour: float) -> tuple[float, float]:
"""Head [J/kg] and efficiency [-] at current speed and flow rate."""
curve = self._pump_chart.get_curve_by_speed(self.speed)
if curve is not None:
return (
float(curve.head_as_function_of_rate(rate_m3_per_hour)),
float(curve.efficiency_as_function_of_rate(rate_m3_per_hour)),
)

# Variable speed between two curves — linear interpolation
curves_below = [c for c in self._pump_chart.curves if c.speed <= self.speed]
curves_above = [c for c in self._pump_chart.curves if c.speed >= self.speed]
if not curves_below or not curves_above:
raise ValueError(
f"Speed {self.speed} rpm is outside the pump chart range "
f"[{self._pump_chart.minimum_speed}, {self._pump_chart.maximum_speed}]."
)

c_low = curves_below[-1]
c_high = curves_above[0]
alpha = (self.speed - c_low.speed) / (c_high.speed - c_low.speed)

head = (1 - alpha) * float(c_low.head_as_function_of_rate(rate_m3_per_hour)) + alpha * float(
c_high.head_as_function_of_rate(rate_m3_per_hour)
)
efficiency = (1 - alpha) * float(c_low.efficiency_as_function_of_rate(rate_m3_per_hour)) + alpha * float(
c_high.efficiency_as_function_of_rate(rate_m3_per_hour)
)
return head, efficiency
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Final

from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.process_pipeline.process_unit import GasProcessUnit, ProcessUnitId
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream


class Splitter(ProcessUnit):
class Splitter(GasProcessUnit):
"""Removes a fixed standard rate from the through-stream.

The split rate is set via `set_rate` before propagation. This models a gas
Expand All @@ -13,7 +13,7 @@ class Splitter(ProcessUnit):
"""

def __init__(self, fluid_service: FluidService, process_unit_id: ProcessUnitId | None = None, rate: float = 0.0):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or GasProcessUnit._create_id()
self._fluid_service = fluid_service
self._rate = rate

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from typing import Final

from libecalc.domain.process.process_pipeline.process_unit import ProcessUnit, ProcessUnitId
from libecalc.domain.process.process_pipeline.process_unit import GasProcessUnit, ProcessUnitId
from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream


class TemperatureSetter(ProcessUnit):
class TemperatureSetter(GasProcessUnit):
def __init__(
self,
required_temperature_kelvin: float,
fluid_service: FluidService,
process_unit_id: ProcessUnitId | None = None,
):
self._id: Final[ProcessUnitId] = process_unit_id or ProcessUnit._create_id()
self._id: Final[ProcessUnitId] = process_unit_id or GasProcessUnit._create_id()
self._required_temperature_kelvin = required_temperature_kelvin
self._fluid_service = fluid_service

Expand Down
Loading
Loading