diff --git a/libecalc.iml b/libecalc.iml new file mode 100644 index 0000000000..8414ea83ad --- /dev/null +++ b/libecalc.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/libecalc/domain/process/dummy.py b/src/libecalc/domain/process/dummy.py new file mode 100644 index 0000000000..02e6226899 --- /dev/null +++ b/src/libecalc/domain/process/dummy.py @@ -0,0 +1,198 @@ +""" +Dummy process implementation for testing purposes. +""" +import uuid +from datetime import datetime + +from libecalc.domain.process.entities.process_units.compressor import Compressor +from libecalc.domain.process.entities.process_units.legacy_compressor.legacy_compressor import LegacyCompressor +from libecalc.domain.process.process_simulation import ProcessScenario, PressureControlConfig, AntiSurgeConfig, \ + Constraint, IndividualStreamDistributionConfig, ProcessPipeline, create_process_scenario_id, ProcessSimulation +from libecalc.domain.process.process_solver.anti_surge import anti_surge_strategy +from libecalc.domain.process.process_system.serial_process_system import SerialProcessSystem +from libecalc.domain.process.value_objects.fluid_stream.fluid_stream import SimpleStream +from libecalc.presentation.yaml.domain.time_series_expression import TimeSeriesExpression +from libecalc.presentation.yaml.mappers.fluid_mapper import MEDIUM_MW_19P4 + +""" +Prototyping... +""" +from ecalc_neqsim_wrapper import NeqSimFluidService +from libecalc.domain.process.compressor.core.train.stage import CompressorTrainStage +from libecalc.domain.process.entities.process_units.choke import Choke +from libecalc.domain.process.entities.process_units.rate_modifier.rate_modifier import RateModifier +from libecalc.domain.process.entities.process_units.temperature_setter import TemperatureSetter +from libecalc.domain.process.entities.shaft import Shaft, VariableSpeedShaft +from libecalc.domain.process.process_solver.boundary import Boundary +from libecalc.domain.process.process_system.process_error import OutsideCapacityError, RateTooHighError, RateTooLowError +from libecalc.domain.process.process_system.process_system import ProcessSystem, create_process_system_id, \ + ProcessSystemId +from libecalc.domain.process.process_system.process_unit import ProcessUnitId, create_process_unit_id +from libecalc.domain.process.value_objects.chart import ChartCurve +from libecalc.domain.process.value_objects.chart.chart import ChartData +from libecalc.domain.process.value_objects.chart.chart_area_flag import ChartAreaFlag +from libecalc.domain.process.value_objects.fluid_stream import FluidStream, FluidModel, EoSModel +from libecalc.presentation.yaml.mappers.charts.user_defined_chart_data import UserDefinedChartData + + +dummy_process_pipeline_id = uuid.uuid4() +process_scenario_id = create_process_scenario_id() +process_simulation_id = uuid.uuid4() + +"""def process_solution_dummy() -> ProcessSolution: + # Find solution! (For now we "say" that we only have 1 solution, and we return the first good solution we find (or no solution, if exhaustive search + # for a solution yields no solution) + + # TODO: We could and should load domain model from db, but for simplicity we wait + + + return ProcessSolution( + id=uuid.uuid4(), + process_problem_id=process_problem_id, + configuration={} + ) +""" + +def process_stream_dummy() -> SimpleStream: + fluid_model = FluidModel(eos_model=EoSModel.SRK, composition=MEDIUM_MW_19P4) + pressure = 20.0 + temperature_kelvin = 273.15 + 30 + standard_rate_m3_per_day = 4000000 + + return SimpleStream( + fluid_model=fluid_model, + pressure_bara=pressure, + temperature_kelvin=temperature_kelvin, + standard_rate_m3_per_day=standard_rate_m3_per_day, + ) + +def process_stream_distribution_dummy() -> IndividualStreamDistributionConfig: + return IndividualStreamDistributionConfig( + inlet_streams=[process_stream_dummy()] + ) + +def process_simulation_dummy() -> ProcessSimulation: + return ProcessSimulation( + id=process_simulation_id, + stream_distribution=process_stream_distribution_dummy(), + process_scenarios=[process_scenario_dummy()] + ) + +def process_scenario_dummy() -> ProcessScenario: + return ProcessScenario( + id=process_scenario_id, + process_pipeline_id=dummy_process_pipeline_id, + pressure_control_strategy=PressureControlConfig( + type="DOWNSTREAM_CHOKE" + ), + anti_surge_strategy=AntiSurgeConfig( + type="COMMON_ASV", + ), + constraint=Constraint( + outlet_pressure=200.0 + ), +# inlet_stream=process_system_dummy_stream() + ) + +def shaft_dummy() -> Shaft: + return VariableSpeedShaft( + speed_rpm=10500.0 + ) # TODO: Should not set speed here, but we may want to set min and max here ...(from data or explicit) + + +def chart_data_dummy() -> ChartData: + # TODO: 2 compressors use this chart data - is it sharable in db; but in domain objs it is VO? + # Should we enforce this in YAML too? + return UserDefinedChartData( + curves=[ + ChartCurve( + rate_actual_m3_hour=[3000.0, 3500.0, 4000.0, 4500.0], + polytropic_head_joule_per_kg=[8500.0, 8000.0, 7500.0, 6500.0], + efficiency_fraction=[0.72, 0.75, 0.74, 0.70], + speed_rpm=7500.0, + ), + ChartCurve( + rate_actual_m3_hour=[4100.0, 4600.0, 5000.0, 5500.0, 6000.0, 6500.0], + polytropic_head_joule_per_kg=[16500.0, 16500.0, 15500.0, 14500.0, 13500.0, 12000.0], + efficiency_fraction=[0.72, 0.73, 0.74, 0.74, 0.72, 0.70], + speed_rpm=10500.0, + ), + ], + control_margin=0.0, + ) + + +def compressors_dummy() -> list[Compressor]: + common_shaft = shaft_dummy() + return [ + Compressor( + process_unit_id=create_process_unit_id(), + compressor_chart=chart_data_dummy(), + fluid_service=NeqSimFluidService.instance(), + shaft=common_shaft, + ), + Compressor( + process_unit_id=create_process_unit_id(), + compressor_chart=chart_data_dummy(), + fluid_service=NeqSimFluidService.instance(), + shaft=common_shaft, + ) + ] + +def process_pipeline_dummy() -> ProcessPipeline: + # TODO: Process system ..? + propagators = [*compressors_dummy(), + Choke( # DownStreamChoke - default PressureControlMechanism when not specified + process_unit_id=create_process_unit_id(), + fluid_service=NeqSimFluidService.instance(), + pressure_change=0.0, # No need to choke...we meet outlet target pressure perfectly... + ), + ] + return ProcessPipeline( + id=dummy_process_pipeline_id, + stream_propagators=propagators + ) + + +def process_system_dummy_streams() -> dict[datetime, SimpleStream | FluidStream]: + fluid_model = FluidModel(eos_model=EoSModel.SRK, composition=MEDIUM_MW_19P4) + pressure = 20.0 + temperature_kelvin = 273.15 + 30 + standard_rates_m3_per_day = [ + 4000000, + 4000000, + 4000000, + 4000000, + 4500000, + 5000000, + 5500000, + 6000000, + 6000000, + 5500000, + 5000000, + 3000000, + 3000000, + 2000000, + 1000000, + 1000000, + 500000, + 500000, + 500000, + 200000, + 200000, + 0 + ] + # 1st of january every year from 2020 to 2040 + timestamps = [ + datetime(year, 1, 1) for year in range(2020, 2040) + ] + + return { + timestamp: SimpleStream( + fluid_model=fluid_model, + pressure_bara=pressure, + temperature_kelvin=temperature_kelvin, + standard_rate_m3_per_day=standard_rate, + ) + for timestamp, standard_rate in zip(timestamps, standard_rates_m3_per_day) + } \ No newline at end of file diff --git a/src/libecalc/domain/process/entities/process_units/choke.py b/src/libecalc/domain/process/entities/process_units/choke.py index 1766392cb8..a6ee5bcc5f 100644 --- a/src/libecalc/domain/process/entities/process_units/choke.py +++ b/src/libecalc/domain/process/entities/process_units/choke.py @@ -2,7 +2,6 @@ from libecalc.domain.process.process_system.process_unit import ProcessUnit, ProcessUnitId from libecalc.domain.process.value_objects.fluid_stream import FluidService, FluidStream - class Choke(ProcessUnit): def __init__( self, diff --git a/src/libecalc/domain/process/entities/shaft/shaft.py b/src/libecalc/domain/process/entities/shaft/shaft.py index 203ff369c7..c0e6384c2b 100644 --- a/src/libecalc/domain/process/entities/shaft/shaft.py +++ b/src/libecalc/domain/process/entities/shaft/shaft.py @@ -5,21 +5,27 @@ ShaftId = NewType("ShaftId", UUID) - def create_shaft_id() -> ShaftId: return ShaftId(uuid.uuid4()) - class Shaft(ABC): """Abstract base class for a shaft. Can be expanded to include more properties and methods as needed. Name, id, units connected to it, etc + + TODO: Instead of "that a compressor has a shaft", should we flip it and say that a shaft has compressors? Or the unit it is attached to? + ie, the shaft needs to know when a solver or algorithm changes it, and it needs to make sure that all connected units are updated w. same speed ... + should we then also have a "mechanical system", that is related to the shaft, motor, gear box etc? and also the compressor - but from a mechanical perspective """ def __init__(self, speed_rpm: float | None = None): - self._id = create_shaft_id() self._speed_rpm = speed_rpm + self._id = create_shaft_id() + # TODO: The speed should not be set when creating a shaft, it is a part of the config/state of the process system, + # and can be changed by the solver/algorithm. We can have a "config" or "state" object that is passed to the shaft, + # and the shaft can update it when the speed is changed. TOTHINK: Would shared state where solver can change, and system must read + # from state be a better solution - ie inspired by react state management? def get_id(self) -> ShaftId: return self._id diff --git a/src/libecalc/domain/process/process_simulation.py b/src/libecalc/domain/process/process_simulation.py index 65a803ded3..6819177c10 100644 --- a/src/libecalc/domain/process/process_simulation.py +++ b/src/libecalc/domain/process/process_simulation.py @@ -1,51 +1,157 @@ -from dataclasses import dataclass -from typing import Literal +import uuid +from dataclasses import dataclass, field +from typing import Literal, NewType +from uuid import UUID -from libecalc.domain.process.process_system.process_system import ProcessSystem, ProcessSystemId +from libecalc.domain.process.process_system.process_system import ProcessSystem +from libecalc.domain.process.process_system.process_unit import ProcessUnit +from libecalc.domain.process.process_system.stream_propagator import StreamPropagator from libecalc.domain.process.stream_distribution.common_stream_distribution import Overflow -from libecalc.domain.process.value_objects.fluid_stream.time_series_stream import TimeSeriesStream -from libecalc.presentation.yaml.domain.time_series_expression import TimeSeriesExpression +from libecalc.domain.process.value_objects.fluid_stream.fluid_stream import SimpleStream @dataclass class CommonStreamSettings: - rate_fractions: list[TimeSeriesExpression] + rate_fractions: list[str] # TimeSeriesExpression] overflow: list[Overflow] @dataclass -class CommonStreamDistributionConfig: - inlet_stream: TimeSeriesStream +class CommonStreamDistributionConfig: # TODO: Rather a strategy that splits a stream in a method according to a policy? + inlet_stream: SimpleStream # TimeseriesStream settings: list[CommonStreamSettings] @dataclass class IndividualStreamDistributionConfig: - inlet_streams: list[TimeSeriesStream] + inlet_streams: list[SimpleStream] # TimeseriesStream @dataclass -class Constraint: - process_system_id: ProcessSystemId - outlet_pressure: TimeSeriesExpression +class Constraint: # should this instead be more flexible wrt. matching one or more stream conditions? + outlet_pressure: float # TimeSeriesExpression @dataclass -class PressureControlConfig: - process_system_id: ProcessSystemId +class PressureControlConfig: # Spec type: Literal["UPSTREAM_CHOKE", "DOWNSTREAM_CHOKE", "COMMON_ASV", "INDIVIDUAL_ASV_RATE", "INDIVIDUAL_ASV_PRESSURE"] + # process_system_id @dataclass class AntiSurgeConfig: - process_system_id: ProcessSystemId type: Literal["INDIVIDUAL_ASV", "COMMON_ASV"] + # process_system_id + + +ProcessScenarioId = NewType("ProcessScenarioId", UUID) + + +def create_process_scenario_id() -> ProcessScenarioId: + return ProcessScenarioId(uuid.uuid4()) + + +@dataclass +class ProcessPipeline: # or simulator? + """ + A part of a process topology that is calculated independently + container propagators - ie systems or units ... + + the static physical stuff that we know a priori + TODO: subpipelines? + """ + + id: UUID + stream_propagators: list[StreamPropagator] + + def get_process_units(self) -> list[ProcessUnit]: + process_units = [] + for stream_propagator in self.stream_propagators: + match stream_propagator: + case ProcessSystem(): + process_units.extend(stream_propagator.get_process_units()) + case ProcessUnit(): + process_units.append(stream_propagator) + + return process_units + + +@dataclass +class ProcessScenario: # TODO: Rename to subproblem? + # can a scenario exist wo. a simulation? yes, e.g. get max rate ... + # given a physical pipeline (a confined problem, such as a compressor train), the user needs to define strategies to find a solution for the sub problem + pressure_control_strategy: PressureControlConfig + anti_surge_strategy: AntiSurgeConfig + constraint: Constraint # ie target pressure - and intermediate pressures ... + process_pipeline_id: UUID # embedded ref here now for convenience, but not a part of this aggr, so FK/ID later + id: ProcessScenarioId = field(default_factory=create_process_scenario_id) + + def get_id(self) -> ProcessScenarioId: + return self.id + + def get_constraint(self) -> Constraint: + return self.constraint + + def get_pressure_control_strategy(self) -> PressureControlConfig: + return self.pressure_control_strategy + + def get_anti_surge_strategy(self) -> AntiSurgeConfig: + return self.anti_surge_strategy @dataclass +class ProcessSimulation: # process_model? + """ + TODO: one or more subproblems, where we first need to find the stream distribution before looking at each subproblem separately + quit and notify as soon as we notice we are not able to find a solution, or always finish? + """ + + id: UUID + stream_distribution: ( + IndividualStreamDistributionConfig | CommonStreamDistributionConfig + ) # the inlet stream is only indirectly a part of sim, through the strategy. It could be separate, where the strategy is just a policy how to distr it + process_scenarios: list[ProcessScenario] # todo: subproblem? TODO: a part of aggr or not? + + def get_stream_distribution_config(self) -> IndividualStreamDistributionConfig | CommonStreamDistributionConfig: + return self.stream_distribution + + +"""@dataclass class ProcessSimulation: process_systems: list[ProcessSystem] pressure_control_strategies: list[PressureControlConfig] anti_surge_strategies: list[AntiSurgeConfig] constraints: list[Constraint] stream_distribution: IndividualStreamDistributionConfig | CommonStreamDistributionConfig +""" + +""" +@dataclass +class ProcessSubSolution: + id: UUID + process_subproblem_id: UUID + configuration: dict[UUID, dict[str, float]] + +@dataclass +class ProcessSolution: + #For a process problem, we have a stream distribution solution (or more?) along with configurations for each strategy + id: UUID + process_problem_id: UUID + process_sub_solutions: ProcessSubSolution # currently 1 solution, but may be more later ... + #stream_distribution_configuration: StreamDistributionSolution + #configuration: dict[UUID, dict[str, float]] #ProcessConfiguration later, just a very simple dict now. Possibly separate AggrRoot, but more like if we want to keep candidates, or we have to pick one from several solutions etc ... + # keep config even when not success? + #success: bool + #reason: str + +# run in a solver, which is a domain service + +@dataclass +class ProcessSimulation: + #Once we have found all solutions, or just have a configuration that we want to simulate, we simulate it + id: UUID + process_problem_id: UUID + process_solution_id: UUID + outlet_stream: FluidStream +# TODO: The simulation is run in a runner or simulator, which is a domain service +""" diff --git a/src/libecalc/domain/process/process_system/process_system.py b/src/libecalc/domain/process/process_system/process_system.py index e3740f8cde..5ea5702770 100644 --- a/src/libecalc/domain/process/process_system/process_system.py +++ b/src/libecalc/domain/process/process_system/process_system.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, NewType from uuid import UUID +from libecalc.domain.process.process_system.process_unit import ProcessUnit from libecalc.domain.process.process_system.stream_propagator import StreamPropagator if TYPE_CHECKING: diff --git a/src/libecalc/domain/process/value_objects/fluid_stream/fluid_stream.py b/src/libecalc/domain/process/value_objects/fluid_stream/fluid_stream.py index bd81186066..c4ea0883ff 100644 --- a/src/libecalc/domain/process/value_objects/fluid_stream/fluid_stream.py +++ b/src/libecalc/domain/process/value_objects/fluid_stream/fluid_stream.py @@ -19,6 +19,16 @@ from libecalc.domain.process.value_objects.fluid_stream.fluid_properties import FluidProperties from libecalc.domain.process.value_objects.fluid_stream.process_conditions import ProcessConditions +@dataclass(frozen=True) +class SimpleStream: + """ + basic properties and std rate + only used as supporting simple input, and will calc mass rate and rich fluid stream ASAP in factory... + """ + fluid_model: FluidModel + pressure_bara: float + temperature_kelvin: float + standard_rate_m3_per_day: float @dataclass(frozen=True) class FluidStream: diff --git a/src/libecalc/domain/shared/__init__.py b/src/libecalc/domain/shared/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/libecalc/presentation/yaml/mappers/process_simulation_mapper.py b/src/libecalc/presentation/yaml/mappers/process_simulation_mapper.py index a94d1a9f13..645f5a7157 100644 --- a/src/libecalc/presentation/yaml/mappers/process_simulation_mapper.py +++ b/src/libecalc/presentation/yaml/mappers/process_simulation_mapper.py @@ -1,3 +1,4 @@ +import uuid from collections.abc import Sequence from typing import Literal, assert_never @@ -469,9 +470,11 @@ def map_process_simulation(self, yaml_process_simulation: YamlProcessSimulation) assert_never(yaml_stream_distribution.method) return ProcessSimulation( - process_systems=process_systems, - constraints=constraints, - anti_surge_strategies=anti_surge_configs, - pressure_control_strategies=pressure_control_configs, + id=uuid.uuid4(), + process_scenarios=process_scenarios, + #process_systems=process_systems, + #constraints=constraints, + #anti_surge_strategies=anti_surge_configs, + #pressure_control_strategies=pressure_control_configs, stream_distribution=stream_distribution, ) diff --git a/src/libecalc/presentation/yaml/model.py b/src/libecalc/presentation/yaml/model.py index bee7277086..71d82af6cc 100644 --- a/src/libecalc/presentation/yaml/model.py +++ b/src/libecalc/presentation/yaml/model.py @@ -28,13 +28,23 @@ from libecalc.domain.process.compressor.core.sampled import CompressorModelSampled from libecalc.domain.process.compressor.core.train.base import CompressorTrainModel from libecalc.domain.process.core.results import CompressorTrainResult, PumpModelResult +from libecalc.domain.process.dummy import process_scenario_dummy, process_system_dummy_streams, process_pipeline_dummy, \ + process_simulation_dummy +from libecalc.domain.process.entities.process_units.compressor import Compressor from libecalc.domain.process.evaluation_input import ( CompressorEvaluationInput, CompressorSampledEvaluationInput, PumpEvaluationInput, ) +from libecalc.domain.process.process_simulation import ProcessScenario, ProcessPipeline from libecalc.domain.process.process_simulation import ProcessSimulation +from libecalc.domain.process.process_simulation import ProcessSimulation +from libecalc.domain.process.process_simulation import ProcessSimulation, ProcessPipeline +from libecalc.domain.process.process_simulation import ProcessScenario, ProcessPipeline +from libecalc.domain.process.process_simulation import ProcessScenario, ProcessPipeline +from libecalc.domain.process.process_system.process_system import ProcessSystem from libecalc.domain.process.pump.pump import PumpModel +from libecalc.domain.process.value_objects.fluid_stream.fluid_stream import SimpleStream, FluidStream from libecalc.domain.regularity import Regularity from libecalc.presentation.yaml.domain.category_service import CategoryService from libecalc.presentation.yaml.domain.container_info import ContainerInfo @@ -107,7 +117,7 @@ def get_fuel_usage(self) -> TimeSeriesStreamDayRate | None: return energy_usage -class YamlModel: +class YamlModel(ProcessSimulation): """ Class representing both the yaml and the resources. @@ -143,7 +153,7 @@ def __init__( def get_process_simulations(self) -> list[ProcessSimulation]: self.validate_for_run() - process_simulations = [] + process_simulations: list[ProcessSimulation] = [] facility_resources, _ = self._resource_service.get_facility_resources() mapper = ProcessSimulationMapper( expression_evaluator=self.get_expression_evaluator(), @@ -157,6 +167,19 @@ def get_process_simulations(self) -> list[ProcessSimulation]: return process_simulations + def get_process_pipeline(self) -> ProcessPipeline: + self.validate_for_run() + return process_pipeline_dummy() + + def get_process_simulation(self) -> ProcessSimulation: + self.validate_for_run() + return process_simulation_dummy() + """" + def get_process_solution(self) -> ProcessSolution: + self.validate_for_run() + return process_solution_dummy() + """ + def get_emitter(self, container_id: uuid.UUID) -> Emitter | None: for installation in self.get_installations(): for emitter in installation.get_emitters():