From 62c4cc2c8f9435b1392124a2e28e7ee6489443bb Mon Sep 17 00:00:00 2001 From: olelod Date: Fri, 21 Nov 2025 08:37:24 +0100 Subject: [PATCH] refactor: send streams and boundary conditions to compressor model calculations --- .../legacy_consumer/system/types.py | 34 +- .../domain/process/compressor/core/base.py | 35 +- .../core/sampled/compressor_model_sampled.py | 4 +- .../process/compressor/core/train/base.py | 220 +++-- .../train/compressor_train_common_shaft.py | 494 ++++++---- ...on_shaft_multiple_streams_and_pressures.py | 513 ++-------- .../simplified_train/simplified_train.py | 52 +- .../entities/process_units/mixer/mixer.py | 16 +- .../process_units/splitter/splitter.py | 2 +- .../temperature_setter/temperature_setter.py | 3 +- .../domain/expression_time_series_pressure.py | 2 +- .../test_compressor_train_common_shaft.py | 271 +++--- .../test_compressor_train_multiple_streams.py | 267 +++--- .../test_compressor_with_turbine.py | 20 +- .../test_simplified_compressor_train.py | 28 +- .../all_energy_usage_models_v3.json | 882 +++++++++--------- 16 files changed, 1305 insertions(+), 1538 deletions(-) diff --git a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py index 2a7505d37b..b632d15070 100644 --- a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py +++ b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py @@ -7,6 +7,7 @@ from libecalc.domain.process.compressor.core.base import CompressorWithTurbineModel from libecalc.domain.process.compressor.core.sampled import CompressorModelSampled from libecalc.domain.process.compressor.core.train.base import CompressorTrainModel +from libecalc.domain.process.compressor.core.train.utils.common import EPSILON from libecalc.domain.process.core.results import EnergyFunctionResult from libecalc.domain.process.pump.pump import PumpModel from libecalc.domain.process.value_objects.fluid_stream.fluid_factory import FluidFactoryInterface @@ -35,11 +36,14 @@ def get_max_standard_rate( ) -> NDArray[np.float64]: model = self._facility_model if isinstance(model, CompressorTrainModel): - return model.get_max_standard_rate( - suction_pressures=suction_pressure, - discharge_pressures=discharge_pressure, + assert self._fluid_factory is not None + model.set_evaluation_input( + suction_pressure=suction_pressure, + discharge_pressure=discharge_pressure, fluid_factory=self._fluid_factory, + rate=np.full_like(suction_pressure, EPSILON), ) + return model.get_max_standard_rate() elif isinstance(model, PumpModel): assert fluid_density is not None return model.get_max_standard_rates( @@ -49,20 +53,26 @@ def get_max_standard_rate( ) elif isinstance(model, CompressorWithTurbineModel): if isinstance(model.compressor_model, CompressorModelSampled): - return model.get_max_standard_rate( - suction_pressures=suction_pressure, - discharge_pressures=discharge_pressure, + model.set_evaluation_input( + suction_pressure=suction_pressure, + discharge_pressure=discharge_pressure, + rate=np.full_like(suction_pressure, EPSILON), ) - return model.get_max_standard_rate( - suction_pressures=suction_pressure, - discharge_pressures=discharge_pressure, + return model.get_max_standard_rate() + model.set_evaluation_input( + suction_pressure=suction_pressure, + discharge_pressure=discharge_pressure, fluid_factory=self._fluid_factory, + rate=np.full_like(suction_pressure, EPSILON), ) + return model.get_max_standard_rate() elif isinstance(model, CompressorModelSampled): - return model.get_max_standard_rate( - suction_pressures=suction_pressure, - discharge_pressures=discharge_pressure, + model.set_evaluation_input( + suction_pressure=suction_pressure, + discharge_pressure=discharge_pressure, + rate=np.full_like(suction_pressure, EPSILON), ) + return model.get_max_standard_rate() else: assert_never(model) diff --git a/src/libecalc/domain/process/compressor/core/base.py b/src/libecalc/domain/process/compressor/core/base.py index 587fa3e9c9..49286354c9 100644 --- a/src/libecalc/domain/process/compressor/core/base.py +++ b/src/libecalc/domain/process/compressor/core/base.py @@ -14,7 +14,6 @@ from libecalc.domain.process.compressor.core.train.utils.common import POWER_CALCULATION_TOLERANCE from libecalc.domain.process.compressor.core.train.utils.numeric_methods import find_root from libecalc.domain.process.core.results import CompressorTrainResult -from libecalc.domain.process.value_objects.fluid_stream.fluid_factory import FluidFactoryInterface class CompressorWithTurbineModel: @@ -97,39 +96,17 @@ def _calculate_remaining_capacity_in_train_given_standard_rate( return 0.0 # Return 0 if no power value available return float(energy_result.power.values[0]) - (max_power - POWER_CALCULATION_TOLERANCE) - def get_max_standard_rate( - self, - suction_pressures: NDArray[np.float64], - discharge_pressures: NDArray[np.float64], - fluid_factory: FluidFactoryInterface | None = None, - ) -> NDArray[np.float64]: + def get_max_standard_rate(self) -> NDArray[np.float64]: """Validate that the compressor has enough power to handle the set maximum standard rate. If there is insufficient power find new maximum rate. """ compressor_model = self.compressor_model - if fluid_factory is not None: - compressor_model._fluid_factory = fluid_factory - - max_standard_rate = compressor_model.get_max_standard_rate( - suction_pressures=suction_pressures, discharge_pressures=discharge_pressures - ) + suction_pressures = compressor_model._suction_pressure + discharge_pressures = compressor_model._discharge_pressure + assert suction_pressures is not None + assert discharge_pressures is not None + max_standard_rate = compressor_model.get_max_standard_rate() assert max_standard_rate is not None - # Check if the obtained results are within the maximum load that the turbine can deliver - if isinstance(compressor_model, CompressorTrainModel): - compressor_model.set_evaluation_input( - fluid_factory=compressor_model._fluid_factory, - rate=max_standard_rate, - suction_pressure=suction_pressures, - discharge_pressure=discharge_pressures, - ) - elif isinstance(compressor_model, CompressorModelSampled): - compressor_model.set_evaluation_input( - rate=max_standard_rate, - suction_pressure=suction_pressures, - discharge_pressure=discharge_pressures, - ) - else: - assert_never(compressor_model) results_max_standard_rate = compressor_model.evaluate() energy_result = results_max_standard_rate.get_energy_result() diff --git a/src/libecalc/domain/process/compressor/core/sampled/compressor_model_sampled.py b/src/libecalc/domain/process/compressor/core/sampled/compressor_model_sampled.py index 2950201086..f4776472c4 100644 --- a/src/libecalc/domain/process/compressor/core/sampled/compressor_model_sampled.py +++ b/src/libecalc/domain/process/compressor/core/sampled/compressor_model_sampled.py @@ -149,15 +149,13 @@ def get_consumption_type(self) -> ConsumptionType: def get_max_standard_rate( self, - suction_pressures: NDArray[np.float64] | None = None, - discharge_pressures: NDArray[np.float64] | None = None, ) -> NDArray[np.float64] | None: """Get max rate given suction pressure and a discharge pressure. :param suction_pressures: Suction pressure [bar] :param discharge_pressures: Discharge pressure [bar] """ - number_of_calculation_points = len(suction_pressures) if suction_pressures is not None else 1 + number_of_calculation_points = len(self._suction_pressure) if self._suction_pressure is not None else 1 if self.support_max_rate: if self._qhull_sampled.support_max_rate: return np.full( diff --git a/src/libecalc/domain/process/compressor/core/train/base.py b/src/libecalc/domain/process/compressor/core/train/base.py index 6d67cf6d36..6908f1df74 100644 --- a/src/libecalc/domain/process/compressor/core/train/base.py +++ b/src/libecalc/domain/process/compressor/core/train/base.py @@ -14,12 +14,12 @@ CompressorTrainStageResultSingleTimeStep, ) from libecalc.domain.process.compressor.core.train.stage import CompressorTrainStage -from libecalc.domain.process.compressor.core.train.train_evaluation_input import CompressorTrainEvaluationInput -from libecalc.domain.process.compressor.core.train.utils.common import EPSILON, PRESSURE_CALCULATION_TOLERANCE +from libecalc.domain.process.compressor.core.train.utils.common import PRESSURE_CALCULATION_TOLERANCE from libecalc.domain.process.core.results import CompressorTrainResult from libecalc.domain.process.core.results.compressor import TargetPressureStatus from libecalc.domain.process.value_objects.fluid_stream import FluidStream from libecalc.domain.process.value_objects.fluid_stream.fluid_factory import FluidFactoryInterface +from libecalc.infrastructure.neqsim_fluid_provider.neqsim_fluid_factory import NeqSimFluidFactory INVALID_MAX_RATE = np.nan @@ -90,11 +90,15 @@ def get_consumption_type(self) -> ConsumptionType: def set_evaluation_input( self, rate: NDArray[np.float64], - fluid_factory: FluidFactoryInterface | list[FluidFactoryInterface] | None, + fluid_factory: FluidFactoryInterface | list[FluidFactoryInterface], suction_pressure: NDArray[np.float64], discharge_pressure: NDArray[np.float64], intermediate_pressure: NDArray[np.float64] | None = None, ): + if rate is None: + raise ValueError("Rate is required for model") + if fluid_factory is None: + raise ValueError("Fluid factory is required for model") if suction_pressure is None: raise ValueError("Suction pressure is required for model") if discharge_pressure is None: @@ -106,6 +110,58 @@ def set_evaluation_input( self._intermediate_pressure = intermediate_pressure self._fluid_factory = fluid_factory + def _make_streams_and_boundary_conditions_for_single_timestep( + self, + rate_value: float | NDArray[np.float64], + suction_pressure_value: float, + intermediate_pressure_value: float | None, + discharge_pressure_value: float, + ) -> tuple[list[FluidStream | float], dict[str, float]]: + """ + Make streams and boundary conditions for a single time step. + + Args: + rate_value (NDArray[np.float64]): Rate in [Sm3/day]. + suction_pressure_value (float): Suction pressure in [bara]. + intermediate_pressure_value (float | None): Intermediate pressure in [bara], or None. + discharge_pressure_value (float): Discharge pressure in [bara]. + """ + # make streams (or rate of outgoing streams for multiple streams model) + if isinstance(self._fluid_factory, NeqSimFluidFactory): + # only one stream + assert isinstance(rate_value, np.float64) + streams = [ + self._fluid_factory.create_stream_from_standard_rate( + pressure_bara=suction_pressure_value, + temperature_kelvin=self.stages[0].inlet_temperature_kelvin, # for now use inlet temp of first stage + standard_rate_m3_per_day=rate_value, + ) + ] + else: + assert isinstance(self._fluid_factory, list) + assert isinstance(rate_value, np.ndarray) + streams = [] + for stream_index in range(len(self._fluid_factory)): + if self._fluid_factory[stream_index] is not None: + streams.append( + self._fluid_factory[stream_index].create_stream_from_standard_rate( + pressure_bara=suction_pressure_value, + temperature_kelvin=self.stages[0].inlet_temperature_kelvin, + standard_rate_m3_per_day=rate_value[stream_index], + ) + ) + else: + streams.append(rate_value[stream_index]) + + # make dictionary of constraints + boundary_conditions = { + "discharge_pressure": discharge_pressure_value, + } + if intermediate_pressure_value is not None: + boundary_conditions["interstage_pressure"] = intermediate_pressure_value + + return streams, boundary_conditions + def evaluate( self, ) -> CompressorTrainResult: @@ -133,12 +189,20 @@ def evaluate( Returns: CompressorTrainResult: The result of the compressor train evaluation. """ + # Make sure evaluation input is set + assert self._rate is not None, "Rate is required for model" + assert self._suction_pressure is not None, "Suction pressure is required for model" + assert self._discharge_pressure is not None, "Discharge pressure is required for model" + assert self._fluid_factory is not None, "Fluid factory is required for model" + logger.debug( f"Evaluating {type(self).__name__} given suction pressure, discharge pressure, " "and potential inter-stage pressure." ) train_results: list[CompressorTrainResultSingleTimeStep] = [] + max_standard_rate: list[float] = [] + for rate_value, suction_pressure_value, intermediate_pressure_value, discharge_pressure_value in zip( np.transpose(self._rate), self._suction_pressure, @@ -147,21 +211,31 @@ def evaluate( else [None] * len(self._suction_pressure), self._discharge_pressure, ): - if isinstance(rate_value, np.ndarray): - rate_value = list(rate_value) - constraints_rate = rate_value[0] - constraints_stream_rates = rate_value - else: - constraints_rate = rate_value - constraints_stream_rates = None - evaluation_constraints = CompressorTrainEvaluationInput( - rate=constraints_rate, - suction_pressure=suction_pressure_value, - discharge_pressure=discharge_pressure_value, - interstage_pressure=intermediate_pressure_value, - stream_rates=constraints_stream_rates, + streams, boundary_conditions = self._make_streams_and_boundary_conditions_for_single_timestep( + rate_value=rate_value, + suction_pressure_value=suction_pressure_value, + intermediate_pressure_value=intermediate_pressure_value, + discharge_pressure_value=discharge_pressure_value, + ) + train_results.append( + self.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, + ) ) - train_results.append(self.evaluate_given_constraints(constraints=evaluation_constraints)) + if self.calculate_max_rate: + try: + max_standard_rate.append( + self.get_max_standard_rate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, + ) + ) + except EcalcError as e: + logger.exception(e) + max_standard_rate.append(float("nan")) + else: + max_standard_rate.append(float("nan")) power_mw = np.array([result.power_megawatt for result in train_results]) power_mw_adjusted = np.where( @@ -170,13 +244,6 @@ def evaluate( power_mw, ) - max_standard_rate = np.full_like(self._suction_pressure, fill_value=INVALID_MAX_RATE, dtype=float) - if self.calculate_max_rate: - max_standard_rate = self.get_max_standard_rate( - suction_pressures=self._suction_pressure, - discharge_pressures=self._discharge_pressure, - ) - ( inlet_stream_condition, outlet_stream_condition, @@ -194,31 +261,43 @@ def evaluate( power=list(power_mw_adjusted), power_unit=Unit.MEGA_WATT, rate_sm3_day=cast(list, self._rate.tolist()), - max_standard_rate=cast(list, max_standard_rate.tolist()), + max_standard_rate=max_standard_rate, stage_results=stage_results, failure_status=[t.failure_status for t in train_results], turbine_result=None, ) @abstractmethod - def evaluate_given_constraints( - self, constraints: CompressorTrainEvaluationInput + def evaluate_single_timestep( + self, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: """ - Evaluate the compressor model based on the set constraints. + Evaluate the compressor model for a single time step given streams. - The constraints can be: - * Rate (train inlet) - * Additional rates for each stream (if multiple streams) - * Suction pressure (train inlet) - * Discharge pressure (train outlet) - * Intermediate pressure (inter-stage) - * Speed (if variable speed) + Args: + streams (list[FluidStream | float]): List of fluid streams involved in the evaluation. + boundary_conditions (dict[str, float): Dictionary of boundary conditions. + Returns: + CompressorTrainResultSingleTimeStep: The result of the compressor train evaluation. + """ + ... - The evaluation is done for a single time step. + @abstractmethod + def get_max_standard_rate_single_timestep( + self, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], + ) -> float: + """ + Evaluate the maximum standard rate for a single time step given streams and boundary conditions. - Return: - CompressorTrainResultSingleTimeStep: The result of the compressor train evaluation. + Args: + streams (list[FluidStream | float]): List of fluid streams involved in the evaluation. + boundary_conditions (dict[str, float]): Dictionary of boundary conditions. + Returns: + float: The maximum standard rate for the given conditions. """ ... @@ -256,17 +335,19 @@ def calculate_pressure_ratios_per_stage( def check_target_pressures( self, - constraints: CompressorTrainEvaluationInput, + boundary_conditions: dict[str, float], results: CompressorTrainResultSingleTimeStep | list[CompressorTrainStageResultSingleTimeStep], ) -> TargetPressureStatus: """Check to see how the calculated pressures compare to the required pressures Args: - constraints: The evaluation constraints given to the evaluation + boundary_conditions (dict[str, float | None]): Dictionary of boundary conditions. results: The results from the compressor train evaluation Returns: TargetPressureStatus: The status of the target pressures """ + discharge_pressure = boundary_conditions.get("discharge_pressure", None) + interstage_pressure = boundary_conditions.get("intermediate_pressure", None) if isinstance(results, list): train_suction_pressure = results[0].inlet_pressure calculated_discharge_pressure = results[-1].discharge_pressure @@ -276,7 +357,7 @@ def check_target_pressures( train_suction_pressure = results.suction_pressure calculated_discharge_pressure = results.discharge_pressure stage_suction_pressure = results.stage_results[0].inlet_stream.pressure_bara - if constraints.stream_rates is not None: + if interstage_pressure is not None: calculated_intermediate_pressure = ( results.stage_results[self.stage_number_interstage_pressure - 1].discharge_pressure if self.stage_number_interstage_pressure is not None @@ -287,28 +368,21 @@ def check_target_pressures( if stage_suction_pressure is not None: if (stage_suction_pressure / train_suction_pressure) - 1 > PRESSURE_CALCULATION_TOLERANCE: return TargetPressureStatus.ABOVE_TARGET_SUCTION_PRESSURE - if constraints.discharge_pressure is not None: - if (calculated_discharge_pressure / constraints.discharge_pressure) - 1 > PRESSURE_CALCULATION_TOLERANCE: + if discharge_pressure is not None: + if (calculated_discharge_pressure / discharge_pressure) - 1 > PRESSURE_CALCULATION_TOLERANCE: return TargetPressureStatus.ABOVE_TARGET_DISCHARGE_PRESSURE - if (constraints.discharge_pressure / calculated_discharge_pressure) - 1 > PRESSURE_CALCULATION_TOLERANCE: + if (discharge_pressure / calculated_discharge_pressure) - 1 > PRESSURE_CALCULATION_TOLERANCE: return TargetPressureStatus.BELOW_TARGET_DISCHARGE_PRESSURE - if constraints.interstage_pressure is not None and calculated_intermediate_pressure is not None: - if ( - calculated_intermediate_pressure / constraints.interstage_pressure - ) - 1 > PRESSURE_CALCULATION_TOLERANCE: + if interstage_pressure is not None and calculated_intermediate_pressure is not None: + if (calculated_intermediate_pressure / interstage_pressure) - 1 > PRESSURE_CALCULATION_TOLERANCE: return TargetPressureStatus.ABOVE_TARGET_INTERMEDIATE_PRESSURE - if ( - constraints.interstage_pressure / calculated_intermediate_pressure - ) - 1 > PRESSURE_CALCULATION_TOLERANCE: + if (interstage_pressure / calculated_intermediate_pressure) - 1 > PRESSURE_CALCULATION_TOLERANCE: return TargetPressureStatus.BELOW_TARGET_INTERMEDIATE_PRESSURE return TargetPressureStatus.TARGET_PRESSURES_MET def get_max_standard_rate( self, - suction_pressures: NDArray[np.float64], - discharge_pressures: NDArray[np.float64], - fluid_factory: FluidFactoryInterface | None = None, ) -> NDArray[np.float64]: """ Calculate the maximum standard volume rate [Sm3/day] that the compressor train can operate at. @@ -318,31 +392,39 @@ def get_max_standard_rate( operational constraints, including the maximum allowable power and the compressor chart limits. Args: - suction_pressures (float): The suction pressures in bara for each time step. - discharge_pressures (float): The discharge pressures in bara for each time step. - fluid_factory (FluidFactoryInterface): The fluid factory interface. Returns: NDArray[np.float64]: An array of maximum standard rates for each time step. If the maximum rate cannot be determined, it returns INVALID_MAX_RATE for that time step. """ - if fluid_factory is not None: - self._fluid_factory = fluid_factory - - max_standard_rate = np.full_like(suction_pressures, fill_value=INVALID_MAX_RATE, dtype=float) - for i, (suction_pressure_value, discharge_pressure_value) in enumerate( + # Make sure evaluation input is set + assert self._rate is not None, "Rate is required for model" + assert self._suction_pressure is not None, "Suction pressure is required for model" + assert self._discharge_pressure is not None, "Discharge pressure is required for model" + assert self._fluid_factory is not None, "Fluid factory is required for model" + + max_standard_rate = np.full_like(self._suction_pressure, float("nan")) + for i, (rate_value, suction_pressure_value, intermediate_pressure_value, discharge_pressure_value) in enumerate( zip( - suction_pressures, - discharge_pressures, + np.transpose(self._rate), + self._suction_pressure, + self._intermediate_pressure + if self._intermediate_pressure is not None + else [None] * len(self._suction_pressure), + self._discharge_pressure, ) ): - constraints = CompressorTrainEvaluationInput( - suction_pressure=suction_pressure_value, - discharge_pressure=discharge_pressure_value, - rate=EPSILON, + streams, boundary_conditions = self._make_streams_and_boundary_conditions_for_single_timestep( + rate_value=rate_value, + suction_pressure_value=suction_pressure_value, + intermediate_pressure_value=intermediate_pressure_value, + discharge_pressure_value=discharge_pressure_value, ) try: - max_standard_rate[i] = self._get_max_std_rate_single_timestep(constraints=constraints) + max_standard_rate[i] = self.get_max_standard_rate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, + ) except EcalcError as e: logger.exception(e) max_standard_rate[i] = float("nan") diff --git a/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft.py b/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft.py index e3f645196c..f1c0dac884 100644 --- a/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft.py +++ b/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft.py @@ -4,6 +4,7 @@ from libecalc.common.fixed_speed_pressure_control import FixedSpeedPressureControl from libecalc.common.logger import logger from libecalc.domain.component_validation_error import ( + DomainValidationException, ProcessChartTypeValidationException, ProcessDischargePressureValidationException, ) @@ -13,7 +14,6 @@ ) from libecalc.domain.process.compressor.core.train.base import CompressorTrainModel from libecalc.domain.process.compressor.core.train.stage import CompressorTrainStage -from libecalc.domain.process.compressor.core.train.train_evaluation_input import CompressorTrainEvaluationInput from libecalc.domain.process.compressor.core.train.utils.common import ( EPSILON, POWER_CALCULATION_TOLERANCE, @@ -27,7 +27,7 @@ from libecalc.domain.process.core.results.compressor import TargetPressureStatus from libecalc.domain.process.entities.shaft import Shaft, SingleSpeedShaft, VariableSpeedShaft from libecalc.domain.process.value_objects.chart.chart_area_flag import ChartAreaFlag -from libecalc.domain.process.value_objects.fluid_stream import ProcessConditions +from libecalc.domain.process.value_objects.fluid_stream import FluidStream, ProcessConditions class CompressorTrainCommonShaft(CompressorTrainModel): @@ -90,21 +90,55 @@ def __init__( self._validate_stages(stages) self._validate_shaft() + self.inlet_stream_connected_to_stage: dict[int, list[int]] = {key: [] for key in range(len(self.stages))} + self.outlet_stream_connected_to_stage: dict[int, list[int]] = {key: [] for key in range(len(self.stages))} + @property def is_variable_speed(self): return all(stage.compressor.compressor_chart.is_variable_speed for stage in self.stages) - def evaluate_given_constraints( + def evaluate_single_timestep( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], fixed_speed: float | None = None, ) -> CompressorTrainResultSingleTimeStep: - if constraints.rate > 0: # type: ignore[operator] + """Evaluate a single timestep on the fluid streams.""" + target_discharge_pressure = boundary_conditions.get("discharge_pressure", None) + target_interstage_pressure = boundary_conditions.get("interstage_pressure", None) + + assert target_discharge_pressure is not None + + # TODO: These checks should be done at a higher level, not here + # Check for at least one positive volumetric rate in an ingoing stream + standard_rates = [stream.standard_rate if isinstance(stream, FluidStream) else -stream for stream in streams] + if not all(x >= 0 for x in [sum(standard_rates[: i + 1]) for i in range(len(standard_rates))]): + raise DomainValidationException( + "You can not remove more fluid from the compressor train than what is being fed into it." + ) + + if ( + sum(stream.volumetric_rate for stream in streams if isinstance(stream, FluidStream)) > 0 + ): # any positive rate + if target_interstage_pressure is not None: + if fixed_speed is not None: + raise ValueError("You can not set a fixed speed when also setting an interstage pressure.") + return self.find_and_calculate_for_compressor_train_with_two_pressure_requirements( + stage_number_for_intermediate_pressure_target=self.stage_number_interstage_pressure, + streams=streams, + boundary_conditions=boundary_conditions, + pressure_control_first_part=self.pressure_control_first_part, + pressure_control_last_part=self.pressure_control_last_part, + ) if fixed_speed is None: - fixed_speed = self.find_fixed_shaft_speed_given_constraints(constraints=constraints) + fixed_speed = self.find_fixed_shaft_speed( + streams=streams, + boundary_conditions=boundary_conditions, + ) self.shaft.set_speed(fixed_speed) train_result = self.calculate_compressor_train( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) if train_result.target_pressure_status == TargetPressureStatus.TARGET_PRESSURES_MET: # Solution found @@ -116,21 +150,26 @@ def evaluate_given_constraints( elif self.pressure_control is None: return train_result else: - train_result = self.evaluate_with_pressure_control_given_constraints(constraints=constraints) + train_result = self.evaluate_with_pressure_control( + streams=streams, + boundary_conditions=boundary_conditions, + ) return train_result else: return CompressorTrainResultSingleTimeStep.create_empty(number_of_stages=len(self.stages)) def calculate_compressor_train( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], asv_rate_fraction: float = 0.0, asv_additional_mass_rate: float = 0.0, ) -> CompressorTrainResultSingleTimeStep: """Calculate compressor train result given inlet conditions and speed Args: - constraints (CompressorTrainEvaluationInput): The constraints for the evaluation. + streams (FluidStream): The inlet fluid stream to the compressor train. + boundary_conditions (dict[str, float]): The boundary conditions of the compressor train. asv_rate_fraction: asv_additional_mass_rate: @@ -138,25 +177,36 @@ def calculate_compressor_train( results including conditions and calculations for each stage and power. """ - if not self.shaft.speed_is_defined or constraints.rate is None or constraints.suction_pressure is None: + inlet_stream_train = streams[0] + assert isinstance(inlet_stream_train, FluidStream) + + additional_rates_to_splitter: list[float] = [] + additional_streams_to_mixer: list[FluidStream] = [] + + for stream in streams[1:]: + if isinstance(stream, FluidStream): + additional_streams_to_mixer.append(stream) + else: + additional_rates_to_splitter.append(stream) + + if ( + not self.shaft.speed_is_defined + or inlet_stream_train.volumetric_rate is None + or inlet_stream_train.pressure_bara is None + ): raise EcalcError( title="Missing required parameters", message="Compressor train calculation requires speed, rate and suction pressure to be set.", ) - # Initialize stream at inlet of first compressor stage using fluid properties and inlet conditions - - train_inlet_stream = self.train_inlet_stream( - pressure=constraints.suction_pressure, - temperature=self.stages[0].inlet_temperature_kelvin, - rate=constraints.rate, - ) stage_results: list[CompressorTrainStageResultSingleTimeStep] = [] - outlet_stream = train_inlet_stream + outlet_stream_stage = inlet_stream_train for stage in self.stages: - inlet_stream = outlet_stream + inlet_stream_stage = outlet_stream_stage stage_result = stage.evaluate( - inlet_stream_stage=inlet_stream, + inlet_stream_stage=inlet_stream_stage, + additional_rates_to_splitter=additional_rates_to_splitter, + additional_streams_to_mixer=additional_streams_to_mixer, speed=self.shaft.get_speed(), asv_rate_fraction=asv_rate_fraction, asv_additional_mass_rate=asv_additional_mass_rate, @@ -164,16 +214,17 @@ def calculate_compressor_train( stage_results.append(stage_result) # We need to recreate the domain object from the result object. This needs cleaning up. - outlet_stream = stage_result.outlet_stream + outlet_stream_stage = stage_result.outlet_stream + outlet_stream_train = outlet_stream_stage # check if target pressures are met target_pressure_status = self.check_target_pressures( - constraints=constraints, + boundary_conditions=boundary_conditions, results=stage_results, ) return CompressorTrainResultSingleTimeStep( - inlet_stream=train_inlet_stream, - outlet_stream=outlet_stream, + inlet_stream=inlet_stream_train, + outlet_stream=outlet_stream_train, stage_results=stage_results, speed=self.shaft.get_speed(), above_maximum_power=sum([stage_result.power_megawatt for stage_result in stage_results]) @@ -197,13 +248,13 @@ def _validate_stages(self, stages: list[CompressorTrainStage]): raise ProcessChartTypeValidationException(message=str(msg)) - def _get_max_std_rate_single_timestep( + def get_max_standard_rate_single_timestep( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], allow_asv: bool = False, ) -> float: """Calculate the max standard rate [Sm3/day] that the compressor train can operate at for a single time step. - The maximum rate can be found in 3 areas: 1. The compressor train can't reach the required target pressure regardless of speed -> Left of the chart. 2. The compressor train hits the required outlet pressure on the maximum speed curve -> On the max speed curve. @@ -233,37 +284,42 @@ def _get_max_std_rate_single_timestep( May be useful to add mass_rate_kg_per_hour to StageResultSingleCalculationPoint. Args: - constraints (CompressorTrainEvaluationInput: The constraints for the evaluation. + streams (list[FluidStream | float]): The inlet fluid streams to the compressor train. + boundary_conditions (dict[str, float]): The boundary conditions of the compressor train. allow_asv: Returns: Standard volume rate [Sm3/day] """ - inlet_density = self._fluid_factory.create_thermo_system( - pressure_bara=constraints.suction_pressure, # type: ignore[arg-type] - temperature_kelvin=self.stages[0].inlet_temperature_kelvin, - ).density + discharge_pressure = boundary_conditions.get("discharge_pressure", None) + inlet_density = streams[0].density def _calculate_train_result(mass_rate: float, speed: float) -> CompressorTrainResultSingleTimeStep: """Partial function of self.calculate_compressor_train_given_speed where we only pass mass_rate. """ self.shaft.set_speed(speed) + streams[0] = FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=mass_rate, + ) return self.calculate_compressor_train( - constraints=constraints.create_conditions_with_new_input( - new_rate=self._fluid_factory.mass_rate_to_standard_rate(mass_rate), # type: ignore[arg-type] - ) + streams=streams, + boundary_conditions=boundary_conditions, ) def _calculate_train_result_given_ps_pd(mass_rate: float) -> CompressorTrainResultSingleTimeStep: - """Partial function of self.evaluate_given_constraints + """Partial function of calculate compressor train where we only pass mass_rate. """ - return self.evaluate_given_constraints( - constraints=constraints.create_conditions_with_new_input( - new_rate=self._fluid_factory.mass_rate_to_standard_rate(mass_rate), # type: ignore[arg-type] - ) + streams[0] = FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=mass_rate, + ) + return self.calculate_compressor_train( + streams=streams, + boundary_conditions=boundary_conditions, ) def _calculate_train_result_given_speed_at_stone_wall( @@ -282,10 +338,13 @@ def _calculate_train_result_given_speed_at_stone_wall( maximum_number_of_iterations=20, ) + streams[0] = FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=_max_valid_mass_rate_at_given_speed, + ) return self.calculate_compressor_train( - constraints=constraints.create_conditions_with_new_input( - new_rate=self._fluid_factory.mass_rate_to_standard_rate(_max_valid_mass_rate_at_given_speed), # type: ignore[arg-type] - ) + streams=streams, + boundary_conditions=boundary_conditions, ) # Same as the partial functions above, but simpler syntax using partial() @@ -375,21 +434,18 @@ def _calculate_train_result_given_speed_at_stone_wall( result_max_mass_rate_at_max_speed = result_max_mass_rate_at_max_speed_first_stage # Solution scenario 1. Infeasible. Target pressure is too high. - if ( - constraints.discharge_pressure is not None - and result_min_mass_rate_at_max_speed.discharge_pressure < constraints.discharge_pressure - ): + if discharge_pressure is not None and result_min_mass_rate_at_max_speed.discharge_pressure < discharge_pressure: return 0.0 # Solution scenario 2. Solution is at maximum speed curve. elif ( - constraints.discharge_pressure is not None - and constraints.discharge_pressure >= result_max_mass_rate_at_max_speed.discharge_pressure + discharge_pressure is not None + and discharge_pressure >= result_max_mass_rate_at_max_speed.discharge_pressure ): """ Iterating along max speed curve for first stage. """ - target_discharge_pressure = constraints.discharge_pressure + target_discharge_pressure = discharge_pressure result_mass_rate = find_root( lower_bound=min_mass_rate_at_max_speed, upper_bound=max_mass_rate_at_max_speed, @@ -434,13 +490,13 @@ def _calculate_train_result_given_speed_at_stone_wall( result_max_mass_rate_at_min_speed = result_max_mass_rate_at_min_speed_first_stage if ( - constraints.discharge_pressure is not None + discharge_pressure is not None and result_max_mass_rate_at_max_speed.discharge_pressure - >= constraints.discharge_pressure + >= discharge_pressure >= result_max_mass_rate_at_min_speed.discharge_pressure ): # iterate along stone wall until target discharge pressure is reached - target_discharge_pressure = constraints.discharge_pressure + target_discharge_pressure = discharge_pressure result_speed = find_root( lower_bound=self.minimum_speed, upper_bound=self.maximum_speed, @@ -453,8 +509,8 @@ def _calculate_train_result_given_speed_at_stone_wall( # Solution scenario 5. Too high pressure even at min speed and max flow rate. elif ( - constraints.discharge_pressure is not None - and result_max_mass_rate_at_min_speed.discharge_pressure > constraints.discharge_pressure + discharge_pressure is not None + and result_max_mass_rate_at_min_speed.discharge_pressure > discharge_pressure ): return 0.0 else: @@ -466,47 +522,64 @@ def _calculate_train_result_given_speed_at_stone_wall( # If so, reduce rate such that power comes below maximum power maximum_power = self.maximum_power if not maximum_power: - result = self._fluid_factory.mass_rate_to_standard_rate(rate_to_return) - return float(result) - elif ( - self.evaluate_given_constraints( - constraints=constraints.create_conditions_with_new_input( - new_rate=self._fluid_factory.mass_rate_to_standard_rate(rate_to_return), # type: ignore[arg-type] + return FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=rate_to_return, + ).standard_rate + else: + # check against maximum power + streams[0] = FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=rate_to_return, + ) + if ( + self.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, + ).power_megawatt + > maximum_power + ): + # check if minimum_rate gives too high power consumption + streams[0] = FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=EPSILON, ) - ).power_megawatt - > maximum_power - ): - # check if minimum_rate gives too high power consumption - result_with_minimum_rate = self.evaluate_given_constraints( - constraints=constraints.create_conditions_with_new_input( - new_rate=EPSILON, + result_with_minimum_rate = self.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, ) - ) - if result_with_minimum_rate.power_megawatt > maximum_power: - return 0.0 # can't find solution + if result_with_minimum_rate.power_megawatt > maximum_power: + return 0.0 # can't find solution + else: + # iterate between rate with minimum power, and the previously found rate to return, to find the + # maximum rate that gives power consumption below maximum power + + return FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=find_root( + lower_bound=result_with_minimum_rate.stage_results[0].mass_rate_asv_corrected_kg_per_hour, + upper_bound=rate_to_return, + func=lambda x: self.evaluate_single_timestep( + streams=[ # type: ignore[arg-type] + FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=x, + ) + ] + + streams[1:], # type: ignore[operator] + boundary_conditions=boundary_conditions, + ).power_megawatt + - maximum_power * (1 - POWER_CALCULATION_TOLERANCE), + relative_convergence_tolerance=1e-3, + maximum_number_of_iterations=20, + ), + ).standard_rate else: - # iterate between rate with minimum power, and the previously found rate to return, to find the - # maximum rate that gives power consumption below maximum power - - result = self._fluid_factory.mass_rate_to_standard_rate( - find_root( - lower_bound=result_with_minimum_rate.stage_results[0].mass_rate_asv_corrected_kg_per_hour, - upper_bound=rate_to_return, - func=lambda x: self.evaluate_given_constraints( - constraints=constraints.create_conditions_with_new_input( - new_rate=self._fluid_factory.mass_rate_to_standard_rate(x), # type: ignore[arg-type] - ) - ).power_megawatt - - maximum_power * (1 - POWER_CALCULATION_TOLERANCE), - relative_convergence_tolerance=1e-3, - maximum_number_of_iterations=20, - ) - ) - return float(result) - else: - # maximum power defined, but found rate is below maximum power - result = self._fluid_factory.mass_rate_to_standard_rate(rate_to_return) - return float(result) + # maximum power defined, but found rate is below maximum power + return FluidStream( + thermo_system=streams[0].thermo_system, + mass_rate_kg_per_h=rate_to_return, + ).standard_rate def _validate_maximum_discharge_pressure(self): if self.maximum_discharge_pressure is not None and self.maximum_discharge_pressure < 0: @@ -528,13 +601,16 @@ def _validate_shaft(self): ) self.shaft.set_speed(self.minimum_speed) - def evaluate_with_pressure_control_given_constraints( - self, constraints: CompressorTrainEvaluationInput + def evaluate_with_pressure_control( + self, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: """ Args: - constraints: + streams: The inlet fluid stream to the compressor train. + boundary_conditions: The boundary conditions of the compressor train. Returns: CompressorTrainResultSingleTimeStep: The result of the compressor train evaluation. @@ -545,23 +621,28 @@ def evaluate_with_pressure_control_given_constraints( ) if self.pressure_control == FixedSpeedPressureControl.DOWNSTREAM_CHOKE: train_result = self._evaluate_train_with_downstream_choking( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) elif self.pressure_control == FixedSpeedPressureControl.UPSTREAM_CHOKE: train_result = self._evaluate_train_with_upstream_choking( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) elif self.pressure_control == FixedSpeedPressureControl.INDIVIDUAL_ASV_RATE: train_result = self._evaluate_train_with_individual_asv_rate( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) elif self.pressure_control == FixedSpeedPressureControl.INDIVIDUAL_ASV_PRESSURE: train_result = self._evaluate_train_with_individual_asv_pressure( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) elif self.pressure_control == FixedSpeedPressureControl.COMMON_ASV: train_result = self._evaluate_train_with_common_asv( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) else: raise ValueError(f"Pressure control {self.pressure_control} not supported") @@ -570,7 +651,8 @@ def evaluate_with_pressure_control_given_constraints( def _evaluate_train_with_downstream_choking( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: """ Evaluate a single-speed compressor train's total power given mass rate, suction pressure, and discharge pressure. @@ -578,40 +660,45 @@ def _evaluate_train_with_downstream_choking( This method assumes that the discharge pressure is controlled to meet the target using a downstream choke valve. Args: - constraints (CompressorTrainEvaluationInput): The constraints for the evaluation. + streams: The inlet fluid stream to the compressor train. + boundary_conditions: The boundary conditions of the compressor train. Returns: CompressorTrainResultSingleTimeStep: The result of the evaluation for a single time step. """ + discharge_pressure = boundary_conditions.get("discharge_pressure", None) train_result = self.calculate_compressor_train( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) if self.maximum_discharge_pressure is not None: if train_result.discharge_pressure * (1 + PRESSURE_CALCULATION_TOLERANCE) > self.maximum_discharge_pressure: + new_boundary_conditions = boundary_conditions.copy() + new_boundary_conditions["discharge_pressure"] = self.maximum_discharge_pressure new_train_result = self._evaluate_train_with_upstream_choking( - constraints=constraints.create_conditions_with_new_input( - new_discharge_pressure=self.maximum_discharge_pressure, - ), + streams=streams, + boundary_conditions=new_boundary_conditions, ) train_result.stage_results = new_train_result.stage_results train_result.outlet_stream = new_train_result.outlet_stream train_result.target_pressure_status = self.check_target_pressures( - constraints=constraints, + boundary_conditions=boundary_conditions, results=train_result, ) + # TODO: this could be replaced by actually having a choke valve model after the compressor train if train_result.target_pressure_status == TargetPressureStatus.ABOVE_TARGET_DISCHARGE_PRESSURE: # At this point, discharge_pressure must be set since we're checking target pressures - assert constraints.discharge_pressure is not None + assert discharge_pressure is not None train_result.outlet_stream = train_result.outlet_stream.create_stream_with_new_conditions( conditions=ProcessConditions( - pressure_bara=constraints.discharge_pressure, + pressure_bara=discharge_pressure, temperature_kelvin=train_result.outlet_stream.temperature_kelvin, ) ) train_result.target_pressure_status = self.check_target_pressures( - constraints=constraints, + boundary_conditions=boundary_conditions, results=train_result, ) @@ -619,7 +706,8 @@ def _evaluate_train_with_downstream_choking( def _evaluate_train_with_upstream_choking( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: """ Evaluate the compressor train's total power assuming upstream choking is used to control suction pressure. @@ -627,33 +715,32 @@ def _evaluate_train_with_upstream_choking( This method iteratively adjusts the suction pressure to achieve the target discharge pressure. Args: - constraints (CompressorTrainEvaluationInput): The constraints for the evaluation. + streams: The inlet fluid stream to the compressor train. + boundary_conditions: The boundary conditions of the compressor train. Returns: CompressorTrainResultSingleTimeStep: The result of the evaluation for a single time step. """ - assert constraints.rate is not None - assert constraints.suction_pressure is not None - - train_inlet_stream = self.train_inlet_stream( - pressure=constraints.suction_pressure, - temperature=self.stages[0].inlet_temperature_kelvin, - rate=constraints.rate, - ) def _calculate_train_result_given_inlet_pressure( inlet_pressure: float, ) -> CompressorTrainResultSingleTimeStep: """Note that we use outside variables for clarity and to avoid class instances.""" + _inlet_stream = streams[0].create_stream_with_new_conditions( + conditions=ProcessConditions( + pressure_bara=inlet_pressure, + temperature_kelvin=streams[0].temperature_kelvin, + ) + ) + _inlet_streams = [_inlet_stream] + streams[1:] return self.calculate_compressor_train( - constraints=constraints.create_conditions_with_new_input( - new_suction_pressure=inlet_pressure, - ), + streams=_inlet_streams, + boundary_conditions=boundary_conditions, ) # This method requires discharge_pressure to be set - assert constraints.discharge_pressure is not None - target_discharge_pressure = constraints.discharge_pressure + target_discharge_pressure = boundary_conditions.get("discharge_pressure", None) + assert target_discharge_pressure is not None result_inlet_pressure = find_root( lower_bound=EPSILON + self.stages[0].pressure_drop_ahead_of_stage, @@ -663,17 +750,18 @@ def _calculate_train_result_given_inlet_pressure( ) train_result = _calculate_train_result_given_inlet_pressure(inlet_pressure=result_inlet_pressure) - train_result.inlet_stream = train_inlet_stream + train_result.inlet_stream = streams[0] train_result.target_pressure_status = self.check_target_pressures( - constraints=constraints, + boundary_conditions=boundary_conditions, results=train_result, ) return train_result def _evaluate_train_with_individual_asv_rate( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: """ Evaluate the total power of a single-speed compressor train given suction pressure, discharge pressure, @@ -687,18 +775,22 @@ def _evaluate_train_with_individual_asv_rate( The ASV fraction that results in the target discharge pressure is found using a Newton iteration Args: - constraints (CompressorTrainEvaluationInput): The constraints for the evaluation. + streams: The inlet fluid stream to the compressor train. + boundary_conditions: The boundary conditions of the compressor train. Returns: CompressorTrainResultSingleTimeStep: The result of the evaluation for a single time step. """ + target_discharge_pressure = boundary_conditions.get("discharge_pressure", None) + assert target_discharge_pressure is not None def _calculate_train_result_given_asv_rate_margin( asv_rate_fraction: float, ) -> CompressorTrainResultSingleTimeStep: """Note that we use outside variables for clarity and to avoid class instances.""" return self.calculate_compressor_train( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, asv_rate_fraction=asv_rate_fraction, ) @@ -708,23 +800,19 @@ def _calculate_train_result_given_asv_rate_margin( asv_rate_fraction=minimum_asv_fraction ) if (train_result_for_minimum_asv_rate_fraction.chart_area_status == ChartAreaFlag.ABOVE_MAXIMUM_FLOW_RATE) or ( - constraints.discharge_pressure is not None - and constraints.discharge_pressure > train_result_for_minimum_asv_rate_fraction.discharge_pressure + target_discharge_pressure is not None + and target_discharge_pressure > train_result_for_minimum_asv_rate_fraction.discharge_pressure ): return train_result_for_minimum_asv_rate_fraction train_result_for_maximum_asv_rate_fraction = _calculate_train_result_given_asv_rate_margin( asv_rate_fraction=maximum_asv_fraction ) if ( - constraints.discharge_pressure is not None - and constraints.discharge_pressure < train_result_for_maximum_asv_rate_fraction.discharge_pressure + target_discharge_pressure is not None + and target_discharge_pressure < train_result_for_maximum_asv_rate_fraction.discharge_pressure ): return train_result_for_maximum_asv_rate_fraction - # This method requires discharge_pressure for the Newton iteration - assert constraints.discharge_pressure is not None - target_discharge_pressure = constraints.discharge_pressure - result_asv_rate_margin = find_root( lower_bound=0.0, upper_bound=1.0, @@ -737,7 +825,8 @@ def _calculate_train_result_given_asv_rate_margin( def _evaluate_train_with_individual_asv_pressure( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: """ Evaluate the compressor train's total power using individual ASV pressure control. @@ -746,29 +835,25 @@ def _evaluate_train_with_individual_asv_pressure( in the train. ASVs are independently adjusted to achieve the required discharge pressure for each compressor. Args: - constraints (CompressorTrainEvaluationInput): The constraints for the evaluation. + streams: The inlet fluid stream to the compressor train. + boundary_conditions: The boundary conditions of the compressor train. Returns: CompressorTrainResultSingleTimeStep: The result of the evaluation for a single time step. """ # This method requires both suction and discharge pressure to be set - assert constraints.suction_pressure is not None - assert constraints.discharge_pressure is not None - - # Multiple streams factories are lists (one per stream). - if isinstance(self._fluid_factory, list): - fluid_factory = self._fluid_factory[0] - else: - fluid_factory = self._fluid_factory - - inlet_stream_train = fluid_factory.create_stream_from_standard_rate( - pressure_bara=constraints.suction_pressure, - temperature_kelvin=self.stages[0].inlet_temperature_kelvin, - standard_rate_m3_per_day=constraints.rate, # type: ignore[arg-type] + target_discharge_pressure = boundary_conditions.get("discharge_pressure", None) + assert target_discharge_pressure is not None + + inlet_stream_train = streams[0].create_stream_with_new_conditions( + conditions=ProcessConditions( + pressure_bara=streams[0].pressure_bara, + temperature_kelvin=self.stages[0].inlet_temperature_kelvin, + ), ) pressure_ratio_per_stage = self.calculate_pressure_ratios_per_stage( - suction_pressure=constraints.suction_pressure, - discharge_pressure=constraints.discharge_pressure, + suction_pressure=inlet_stream_train.pressure_bara, + discharge_pressure=target_discharge_pressure, ) inlet_stream_stage = inlet_stream_train stage_results = [] @@ -783,7 +868,7 @@ def _evaluate_train_with_individual_asv_pressure( # check if target pressures are met target_pressure_status = self.check_target_pressures( - constraints=constraints, + boundary_conditions=boundary_conditions, results=stage_results, ) return CompressorTrainResultSingleTimeStep( @@ -796,7 +881,8 @@ def _evaluate_train_with_individual_asv_pressure( def _evaluate_train_with_common_asv( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: """ Evaluate the total power of a single-speed compressor train given suction pressure, discharge pressure, @@ -810,39 +896,41 @@ def _evaluate_train_with_common_asv( A Newton iteration is used to find the mass rate that results in the target discharge pressure. Args: - constraints (CompressorTrainEvaluationInput): The constraints for the evaluation. + streams: The inlet fluid stream to the compressor train. + boundary_conditions: The boundary conditions of the compressor train. Returns: CompressorTrainResultSingleTimeStep: The result of the evaluation for a single time step. """ + target_discharge_pressure = boundary_conditions.get("discharge_pressure", None) + # Multiple streams factories are lists (one per stream). - if isinstance(self._fluid_factory, list): - fluid_factory = self._fluid_factory[0] - else: - fluid_factory = self._fluid_factory - minimum_mass_rate_kg_per_hour = fluid_factory.standard_rate_to_mass_rate( - standard_rate_m3_per_day=constraints.rate, # type: ignore[arg-type] - ) + inlet_streams = streams.copy() + train_inlet_stream = inlet_streams[0] + + minimum_mass_rate_kg_per_hour = train_inlet_stream.mass_rate_kg_per_h + # Iterate on rate until pressures are met - density_train_inlet_fluid = fluid_factory.create_thermo_system( - pressure_bara=constraints.suction_pressure, # type: ignore[arg-type] - temperature_kelvin=self.stages[0].inlet_temperature_kelvin, - ).density + density_train_inlet_fluid = train_inlet_stream.density def _calculate_train_result_given_mass_rate( mass_rate_kg_per_hour: float, ) -> CompressorTrainResultSingleTimeStep: + inlet_streams[0] = FluidStream( + thermo_system=train_inlet_stream.thermo_system, + mass_rate_kg_per_h=mass_rate_kg_per_hour, + ) return self.calculate_compressor_train( - constraints=constraints.create_conditions_with_new_input( - new_rate=fluid_factory.mass_rate_to_standard_rate(mass_rate_kg_per_h=mass_rate_kg_per_hour), # type: ignore[arg-type] - ), + streams=inlet_streams, + boundary_conditions=boundary_conditions, ) def _calculate_train_result_given_additional_mass_rate( additional_mass_rate_kg_per_hour: float, ) -> CompressorTrainResultSingleTimeStep: return self.calculate_compressor_train( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, asv_additional_mass_rate=additional_mass_rate_kg_per_hour, ) @@ -851,7 +939,7 @@ def _calculate_train_result_given_additional_mass_rate( minimum_mass_rate = max( minimum_mass_rate_kg_per_hour, self.stages[0].compressor.compressor_chart.minimum_rate * density_train_inlet_fluid, - ) # type: ignore[type-var] + ) # note: we subtract EPSILON to avoid floating point issues causing the maximum mass rate to exceed chart area maximum rate after round-trip conversion (mass rate -> standard rat -> mass rate) maximum_mass_rate = ( self.stages[0].compressor.compressor_chart.maximum_rate * density_train_inlet_fluid * (1 - EPSILON) @@ -861,7 +949,7 @@ def _calculate_train_result_given_additional_mass_rate( # is already larger than the maximum mass rate, there is no need for optimization - just add result # with minimum_mass_rate_kg_per_hour (which will fail with above maximum flow rate) if minimum_mass_rate_kg_per_hour > maximum_mass_rate: - return _calculate_train_result_given_mass_rate(mass_rate_kg_per_hour=minimum_mass_rate_kg_per_hour) # type: ignore[arg-type] + return _calculate_train_result_given_mass_rate(mass_rate_kg_per_hour=minimum_mass_rate_kg_per_hour) train_result_for_minimum_mass_rate = _calculate_train_result_given_mass_rate( mass_rate_kg_per_hour=float(minimum_mass_rate) @@ -888,7 +976,7 @@ def _calculate_train_result_given_additional_mass_rate( # find the minimum additional_mass_rate that gives all points internal minimum_mass_rate = -maximize_x_given_boolean_condition_function( x_min=-maximum_mass_rate, - x_max=-minimum_mass_rate, # type: ignore[arg-type] + x_max=-minimum_mass_rate, bool_func=lambda x: _calculate_train_result_given_mass_rate( mass_rate_kg_per_hour=-x ).mass_rate_asv_corrected_is_constant_for_stages, @@ -903,20 +991,20 @@ def _calculate_train_result_given_additional_mass_rate( # If none of those give a valid results, the compressor train is poorly designed... inc = 0.1 train_result_for_mass_rate = _calculate_train_result_given_mass_rate( - mass_rate_kg_per_hour=minimum_mass_rate + inc * (maximum_mass_rate - minimum_mass_rate) # type: ignore[arg-type] + mass_rate_kg_per_hour=minimum_mass_rate + inc * (maximum_mass_rate - minimum_mass_rate) ) while not train_result_for_mass_rate.mass_rate_asv_corrected_is_constant_for_stages: inc += 0.1 if inc >= 1: logger.error("Single speed train with Common ASV pressure control has no solution!") train_result_for_mass_rate = _calculate_train_result_given_mass_rate( - mass_rate_kg_per_hour=minimum_mass_rate + inc * (maximum_mass_rate - minimum_mass_rate) # type: ignore[arg-type] + mass_rate_kg_per_hour=minimum_mass_rate + inc * (maximum_mass_rate - minimum_mass_rate) ) # found one solution, now find min and max minimum_mass_rate = -maximize_x_given_boolean_condition_function( - x_min=-(minimum_mass_rate + inc * (maximum_mass_rate - minimum_mass_rate)), # type: ignore[arg-type] - x_max=-minimum_mass_rate, # type: ignore[arg-type] + x_min=-(minimum_mass_rate + inc * (maximum_mass_rate - minimum_mass_rate)), + x_max=-minimum_mass_rate, bool_func=lambda x: _calculate_train_result_given_mass_rate( mass_rate_kg_per_hour=-x ).mass_rate_asv_corrected_is_constant_for_stages, @@ -939,24 +1027,23 @@ def _calculate_train_result_given_additional_mass_rate( mass_rate_kg_per_hour=maximum_mass_rate ) if ( - constraints.discharge_pressure is not None - and constraints.discharge_pressure > train_result_for_minimum_mass_rate.discharge_pressure + target_discharge_pressure is not None + and target_discharge_pressure > train_result_for_minimum_mass_rate.discharge_pressure ): # will never reach target pressure, too high return train_result_for_minimum_mass_rate if ( - constraints.discharge_pressure is not None - and constraints.discharge_pressure < train_result_for_maximum_mass_rate.discharge_pressure + target_discharge_pressure is not None + and target_discharge_pressure < train_result_for_maximum_mass_rate.discharge_pressure ): # will never reach target pressure, too low return train_result_for_maximum_mass_rate # This method requires discharge_pressure for the Newton iteration - assert constraints.discharge_pressure is not None - target_discharge_pressure = constraints.discharge_pressure + assert target_discharge_pressure is not None result_mass_rate = find_root( - lower_bound=minimum_mass_rate, # type: ignore[arg-type] + lower_bound=minimum_mass_rate, upper_bound=maximum_mass_rate, func=lambda x: _calculate_train_result_given_mass_rate(mass_rate_kg_per_hour=x).discharge_pressure - target_discharge_pressure, @@ -964,12 +1051,13 @@ def _calculate_train_result_given_additional_mass_rate( # This mass rate is the mass rate to use as mass rate after asv for each stage, # thus the asv in each stage should be set to correspond to this mass rate return _calculate_train_result_given_additional_mass_rate( - additional_mass_rate_kg_per_hour=(result_mass_rate - minimum_mass_rate_kg_per_hour) # type: ignore[arg-type] + additional_mass_rate_kg_per_hour=(result_mass_rate - minimum_mass_rate_kg_per_hour) ) - def find_fixed_shaft_speed_given_constraints( + def find_fixed_shaft_speed( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], lower_bound_for_speed: float | None = None, upper_bound_for_speed: float | None = None, ) -> float: @@ -990,13 +1078,17 @@ def find_fixed_shaft_speed_given_constraints( speed_1 = maximum speed for train, calculate f(speed_1) aka f_1 Args: - constraints (CompressorTrainEvaluationInput): The constraints for the evaluation. + streams (list[FluidStream| float]): The inlet streams in/out of the compressor train. + boundary_conditions (dict[str, float | None]): The boundary conditions for the evaluation. lower_bound_for_speed (float | None): The lower bound for the speed. If None, uses the minimum speed upper_bound_for_speed (float | None): The upper bound for the speed. If None, uses the maximum speed Returns: - The speed required to operate at to meet the given constraints. (Bounded by the minimu and maximum speed) + The speed required to operate at to meet the given boundary conditions. (Bounded by the minimum and maximum speed) """ + discharge_pressure = boundary_conditions.get("discharge_pressure") + assert discharge_pressure is not None + minimum_speed = ( lower_bound_for_speed if lower_bound_for_speed and lower_bound_for_speed > self.minimum_speed @@ -1011,7 +1103,8 @@ def find_fixed_shaft_speed_given_constraints( def _calculate_compressor_train(_speed: float) -> CompressorTrainResultSingleTimeStep: self.shaft.set_speed(_speed) return self.calculate_compressor_train( - constraints=constraints, + streams=streams, + boundary_conditions=boundary_conditions, ) train_result_for_minimum_speed = _calculate_compressor_train(_speed=minimum_speed) @@ -1031,13 +1124,13 @@ def _calculate_compressor_train(_speed: float) -> CompressorTrainResultSingleTim # Solution 1, iterate on speed until target discharge pressure is found if ( - constraints.discharge_pressure is not None + discharge_pressure is not None and train_result_for_minimum_speed.discharge_pressure - <= constraints.discharge_pressure + <= discharge_pressure <= train_result_for_maximum_speed.discharge_pressure ): # At this point, discharge_pressure is confirmed to be not None - target_discharge_pressure = constraints.discharge_pressure + target_discharge_pressure = discharge_pressure speed = find_root( lower_bound=minimum_speed, upper_bound=maximum_speed, @@ -1047,10 +1140,7 @@ def _calculate_compressor_train(_speed: float) -> CompressorTrainResultSingleTim return speed # Solution 2, target pressure is too low: - if ( - constraints.discharge_pressure is not None - and constraints.discharge_pressure < train_result_for_minimum_speed.discharge_pressure - ): + if discharge_pressure is not None and discharge_pressure < train_result_for_minimum_speed.discharge_pressure: return minimum_speed # Solution 3, target discharge pressure is too high diff --git a/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft_multiple_streams_and_pressures.py b/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft_multiple_streams_and_pressures.py index ab01025a0d..a23d40360d 100644 --- a/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft_multiple_streams_and_pressures.py +++ b/src/libecalc/domain/process/compressor/core/train/compressor_train_common_shaft_multiple_streams_and_pressures.py @@ -4,24 +4,18 @@ import numpy as np from numpy._typing import NDArray -from libecalc.common.errors.exceptions import IllegalStateException from libecalc.common.fixed_speed_pressure_control import FixedSpeedPressureControl from libecalc.common.logger import logger from libecalc.domain.component_validation_error import ProcessChartTypeValidationException from libecalc.domain.process.compressor.core.results import CompressorTrainResultSingleTimeStep from libecalc.domain.process.compressor.core.train.compressor_train_common_shaft import CompressorTrainCommonShaft from libecalc.domain.process.compressor.core.train.stage import CompressorTrainStage -from libecalc.domain.process.compressor.core.train.train_evaluation_input import CompressorTrainEvaluationInput from libecalc.domain.process.compressor.core.train.types import FluidStreamObjectForMultipleStreams from libecalc.domain.process.compressor.core.train.utils.common import EPSILON -from libecalc.domain.process.compressor.core.train.utils.numeric_methods import ( - maximize_x_given_boolean_condition_function, -) from libecalc.domain.process.core.results.compressor import TargetPressureStatus from libecalc.domain.process.entities.shaft import Shaft, VariableSpeedShaft -from libecalc.domain.process.value_objects.fluid_stream import FluidStream, ProcessConditions +from libecalc.domain.process.value_objects.fluid_stream import FluidStream from libecalc.domain.process.value_objects.fluid_stream.fluid_factory import FluidFactoryInterface -from libecalc.domain.process.value_objects.fluid_stream.fluid_model import FluidModel class CompressorTrainCommonShaftMultipleStreamsAndPressures(CompressorTrainCommonShaft): @@ -84,8 +78,6 @@ def __init__( self.streams = streams self.number_of_compressor_streams = len(self.streams) - self.inlet_stream_connected_to_stage: dict[int, list[int]] = {key: [] for key in range(len(self.stages))} - self.outlet_stream_connected_to_stage: dict[int, list[int]] = {key: [] for key in range(len(self.stages))} for i, stream in enumerate(self.streams): if stream.is_inlet_stream: self.inlet_stream_connected_to_stage[stream.connected_to_stage_no].append(i) @@ -111,7 +103,7 @@ def pressure_control_last_part(self) -> FixedSpeedPressureControl: def set_evaluation_input( self, rate: NDArray[np.float64], - fluid_factory: FluidFactoryInterface | list[FluidFactoryInterface] | None, + fluid_factory: FluidFactoryInterface | list[FluidFactoryInterface], suction_pressure: NDArray[np.float64], discharge_pressure: NDArray[np.float64], intermediate_pressure: NDArray[np.float64] | None = None, @@ -129,37 +121,6 @@ def set_evaluation_input( fluid_factory=fluid_factory, ) - @staticmethod - def _check_intermediate_pressure_stage_number_is_valid( - _stage_number_intermediate_pressure: int | None, - number_of_stages: int, - ): - """Fixme: Move to dto validation. - Validate that the intermediate pressure stage number is within the allowed range. - - The intermediate pressure stage number specifies the stage for which the intermediate pressure is defined as the inlet pressure. - This value must be greater than 0 (since stage 0 uses the suction pressure as its inlet) and less than the total number of stages. - The method raises an exception if the value is out of bounds. - - Args: - _stage_number_intermediate_pressure (int): The stage number for the intermediate pressure (zero-based). - number_of_stages (int): The total number of stages in the compressor train. - - Raises: - IllegalStateException: If the stage number is not within the valid range. - """ - if _stage_number_intermediate_pressure is None or ( - _stage_number_intermediate_pressure < 1 or _stage_number_intermediate_pressure > number_of_stages - 1 - ): - msg = ( - f"The stage number for the intermediate pressure must point to one of the intermediate stages and" - f" can not be smaller than 1, and can not be larger than 1 minus the number of stages, but is" - f" {_stage_number_intermediate_pressure} while the number of stages is {number_of_stages}." - f" You should not end up here, please contact support" - ) - logger.exception(msg) - raise IllegalStateException(msg) - def _validate_stages(self, stages: list[CompressorTrainStage]): min_speed_per_stage = [] max_speed_per_stage = [] @@ -174,363 +135,11 @@ def _validate_stages(self, stages: list[CompressorTrainStage]): raise ProcessChartTypeValidationException(message=str(msg)) - def train_inlet_stream( - self, - pressure: float, - temperature: float, - rate: float, - ) -> FluidStream: - """Find inlet stream given constraints. - - Args: - pressure (float): - temperature (float): - rate (float): - - Returns: - FluidStream: Inlet fluid stream at the compressor train inlet. - """ - assert isinstance(self._fluid_factory, list) # for mypy - return self._fluid_factory[0].create_stream_from_standard_rate( - pressure_bara=pressure, - temperature_kelvin=temperature, - standard_rate_m3_per_day=rate, - ) - - def evaluate_given_constraints( - self, - constraints: CompressorTrainEvaluationInput, - fixed_speed: float | None = None, - ) -> CompressorTrainResultSingleTimeStep: - """ - Evaluate the compressor train for the given operating constraints. - - This method simulates the compressor train using the provided constraints, which may include stream rates, - pressures, and shaft speed. It handles cases with and without intermediate pressure targets, determines the - appropriate shaft speed if not specified, and applies pressure control if needed. The method ensures mass - balance and returns an empty result if the configuration is invalid. - - Args: - constraints (CompressorTrainEvaluationInput): The operating constraints, including pressures, stream rates, - and optional intermediate pressure. - fixed_speed (float): A fixed constant speed for the compressor train. Takes away the ability to iterate - over speed to find a solution. Defaults to None. - - Returns: - CompressorTrainResultSingleTimeStep: The result of the simulation, including stage results, inlet/outlet streams, - speed, and pressure status. Returns an empty result if the configuration is invalid. - """ - if ( - constraints.stream_rates is None - or not self.check_that_ingoing_streams_are_larger_than_or_equal_to_outgoing_streams( - constraints.stream_rates - ) - ): - return CompressorTrainResultSingleTimeStep.create_empty(number_of_stages=len(self.stages)) - - # At this point, stream_rates is confirmed to be not None - assert constraints.stream_rates is not None - - inlet_rates = [constraints.stream_rates[i] for (i, stream) in enumerate(self.streams) if stream.is_inlet_stream] - # Enough with one positive ingoing stream. Compressors with possible zero rates will recirculate - positive_ingoing_streams = list(filter(lambda x: x > 0, list(inlet_rates))) - if not any(positive_ingoing_streams): - return CompressorTrainResultSingleTimeStep.create_empty(number_of_stages=len(self.stages)) - else: - if constraints.interstage_pressure is not None: - if fixed_speed is not None: - raise ValueError("You can not set a fixed speed when also setting an interstage pressure.") - self._check_intermediate_pressure_stage_number_is_valid( - _stage_number_intermediate_pressure=self.stage_number_interstage_pressure, - number_of_stages=len(self.stages), - ) - return self.find_and_calculate_for_compressor_train_with_two_pressure_requirements( - stage_number_for_intermediate_pressure_target=self.stage_number_interstage_pressure, - constraints=constraints, - pressure_control_first_part=self.pressure_control_first_part, - pressure_control_last_part=self.pressure_control_last_part, - ) - else: - if fixed_speed is None: - fixed_speed = self.find_fixed_shaft_speed_given_constraints(constraints=constraints) - self.shaft.set_speed(fixed_speed) - train_result = self.calculate_compressor_train( - constraints=constraints, - ) - - if train_result.target_pressure_status == TargetPressureStatus.TARGET_PRESSURES_MET: - # Solution found - return train_result - elif train_result.target_pressure_status is TargetPressureStatus.BELOW_TARGET_DISCHARGE_PRESSURE: - # Not able to reach the requested discharge pressure at the given speed - # Return result (with failure) at given speed - return train_result - elif self.pressure_control is None: - return train_result - else: - train_result = self.evaluate_with_pressure_control_given_constraints( - constraints=constraints, - ) - return train_result - - def check_that_ingoing_streams_are_larger_than_or_equal_to_outgoing_streams( - self, - std_rates_std_m3_per_day_per_stream: list[float], - ) -> bool: - """ - Check that, for each stage in the compressor train, the cumulative sum of ingoing streams up to that stage - is at least as large as the cumulative sum of outgoing streams. This ensures mass balance, as a compressor - train cannot generate fluid. - - Args: - std_rates_std_m3_per_day_per_stream (list[float]): Standard rates [Sm3/day] for each stream in the compressor train. - - Returns: - bool: True if, at every stage, the total ingoing volume is greater than or equal to the outgoing volume; False otherwise. - """ - sum_of_ingoing_volume_up_to_current_stage_number = 0 - sum_of_outgoing_volume_up_to_current_stage_number = 0 - for stage_number, _ in enumerate(self.stages): - sum_of_ingoing_volume_up_to_current_stage_number += sum( - [ - std_rates_std_m3_per_day_per_stream[inlet_stream_number] - for inlet_stream_number in self.inlet_stream_connected_to_stage[stage_number] - ] - ) - sum_of_outgoing_volume_up_to_current_stage_number += sum( - [ - std_rates_std_m3_per_day_per_stream[inlet_stream_number] - for inlet_stream_number in self.outlet_stream_connected_to_stage[stage_number] - ] - ) - if sum_of_outgoing_volume_up_to_current_stage_number > sum_of_ingoing_volume_up_to_current_stage_number: - logger.warning( - f"For stage number {stage_number}, the sum of the outgoing streams exceeds the sum of the ingoing " - f"streams. Rates will be set to zero and time step not calculated." - ) - return False - - return True - - def convert_to_rates_for_each_compressor_train_stages(self, rates_per_stream: list[float]) -> list[float]: - """ - Convert stream rates to per-stage rates for the compressor train. - - This method takes the rates for each stream (either mass rates [kg/h] or standard rates [Sm3/day]) - and computes the corresponding rate for each stage in the compressor train, accounting for streams - entering or leaving between stages. - - Args: - rates_per_stream (list[float]): Rates for each stream in the compressor train, either as mass rates [kg/h] - or standard rates [Sm3/day]. - - Returns: - list[float]: Rates for each stage in the compressor train, in the same units as the input. - """ - stage_rates = [] - current_compressor_stage = 0 - previous_compressor_stage = 0 - for i, stream in enumerate(self.streams): - # the next stage with an additional ingoing/outgoing stream. - # current_compressor_stage is not necessarily previous_compressor_stage + 1, unless streams are entering - # or leaving the train between all stages - current_compressor_stage = stream.connected_to_stage_no - if i == 0: - stage_rates.append(rates_per_stream[i]) - else: - # if no ingoing/outgoing streams between compressor train stages, - # keep the rate coming out of the previous compressor train stage - for _ in range(previous_compressor_stage, current_compressor_stage): - stage_rates.append(stage_rates[previous_compressor_stage]) - if stream.is_inlet_stream: # if there is an extra ingoing rate, then add it - stage_rates[current_compressor_stage] += rates_per_stream[i] - else: # if there is an extra outgoing rate, then add it - stage_rates[current_compressor_stage] -= rates_per_stream[i] - previous_compressor_stage = current_compressor_stage - - # if there are more stages after the last ingoing/outgoing stream keep the same rate - for _ in range(current_compressor_stage + 1, len(self.stages)): - stage_rates.append(stage_rates[previous_compressor_stage]) - - return stage_rates - - def _get_max_std_rate_single_timestep( - self, - constraints: CompressorTrainEvaluationInput, - allow_asv: bool = False, - ) -> float: - """ - Calculate the maximum standard volume rate [Sm3/day] for a single time step, given the current constraints. - - This method determines the highest possible rate for an ingoing stream that the compressor train can handle - under the specified operating conditions. It iteratively increases the stream rate until the compressor train - can no longer operate within valid limits, then returns the maximum valid rate found. - - Args: - constraints (CompressorTrainEvaluationInput): The operating constraints, including which stream to maximuze. - allow_asv (bool, optional): If True, allows anti-surge valve recirculation in the calculation. Defaults to False. - - Returns: - float: The maximum standard volume rate in Sm3/day for the specified stream. Returns 0.0 if no valid rate is found. - """ - constraints = constraints.create_conditions_with_new_input(new_stream_rates=[EPSILON] * len(self.streams)) - assert constraints.stream_rates is not None - - stream_to_maximize_connected_to_stage_no = self.streams[constraints.stream_to_maximize].connected_to_stage_no - - # if it is not an ingoing stream --> currently no calculations done - # Fixme: what should be returned? 0.0, NaN or something else? - if not self.streams[constraints.stream_to_maximize].is_inlet_stream: - return 0.0 - - std_rates_std_m3_per_day_per_stream = constraints.stream_rates.copy() - - def _calculate_train_result(std_rate_for_stream: float) -> CompressorTrainResultSingleTimeStep: - """Partial function of self.evaluate_given_constraints - where we only pass std_rate_per_stream. - """ - std_rates_std_m3_per_day_per_stream[constraints.stream_to_maximize] = std_rate_for_stream - return self.evaluate_given_constraints( - constraints=constraints.create_conditions_with_new_input( - new_rate=std_rates_std_m3_per_day_per_stream[0], - new_stream_rates=std_rates_std_m3_per_day_per_stream, - ), - ) - - train_result = self.evaluate_given_constraints( - constraints=constraints, - ) - if not train_result.is_valid: - zero_stream_result = _calculate_train_result(std_rate_for_stream=EPSILON) - if not zero_stream_result.is_valid: - return 0.0 - else: - return maximize_x_given_boolean_condition_function( - x_min=0.0, - x_max=float(constraints.stream_rates[constraints.stream_to_maximize]), - bool_func=lambda x: _calculate_train_result(std_rate_for_stream=x).is_valid, - ) - else: - max_rate_is_larger_than = std_rates_std_m3_per_day_per_stream[constraints.stream_to_maximize] - while train_result.is_valid: - max_rate_is_larger_than = train_result.stage_results[ - stream_to_maximize_connected_to_stage_no - ].standard_rate_asv_corrected_sm3_per_day - std_rates_std_m3_per_day_per_stream[constraints.stream_to_maximize] = max_rate_is_larger_than * 2 - train_result = self.evaluate_given_constraints( - constraints=constraints.create_conditions_with_new_input( - new_rate=std_rates_std_m3_per_day_per_stream[0], - new_stream_rates=std_rates_std_m3_per_day_per_stream, - ) - ) - return maximize_x_given_boolean_condition_function( - x_min=float(max_rate_is_larger_than), - x_max=float(max_rate_is_larger_than * 2), - bool_func=lambda x: _calculate_train_result(std_rate_for_stream=x).is_valid, - ) - - def calculate_compressor_train( - self, - constraints: CompressorTrainEvaluationInput, - asv_rate_fraction: float = 0.0, - asv_additional_mass_rate: float = 0.0, - ) -> CompressorTrainResultSingleTimeStep: - """ - Simulate the compressor train for the given inlet conditions, stream rates, and shaft speed. - - This method models the flow through each stage of the compressor train, accounting for multiple inlet and outlet - streams, anti-surge valve recirculation, and possible subtrain configurations. It computes the resulting outlet - stream, per-stage results (including conditions and power), and overall train performance. - - Typical usage scenarios: - 1. Standard train: self.streams[0] is the main inlet. - 2. First subtrain (with intermediate pressure target): self.streams[0] is the inlet. - 3. Last subtrain (after a split): self.streams[0] may be an entering or leaving stream, and the inlet fluid - is set from the first subtrain. - - Args: - constraints (CompressorTrainEvaluationInput): Pressures, stream rates, and speed for the simulation. - asv_rate_fraction (float, optional): Fraction of anti-surge valve recirculation. Defaults to 0.0. - asv_additional_mass_rate (float, optional): Additional mass rate for recirculation. Defaults to 0.0. - - Returns: - CompressorTrainResultSingleTimeStep: Object containing inlet/outlet streams, per-stage results, speed, - total power, and pressure status. - """ - # This multiple streams train also requires stream_rates to be set - assert constraints.stream_rates is not None - assert constraints.suction_pressure is not None - assert isinstance(self._fluid_factory, list) - - # Create fluid streams for ingoing streams - fluid_streams = [ - self._fluid_factory[i].create_stream_from_standard_rate( - pressure_bara=constraints.suction_pressure, - temperature_kelvin=self.stages[0].inlet_temperature_kelvin, - standard_rate_m3_per_day=constraints.stream_rates[i], - ) - for i, stream in enumerate(self.streams) - if stream.is_inlet_stream and self._fluid_factory[i] is not None - ] - - previous_stage_outlet_stream = train_inlet_stream = fluid_streams[0] - inlet_stream_counter = 1 - stage_results = [] - - for stage_number, stage in enumerate(self.stages): - stage_inlet_stream = previous_stage_outlet_stream - - additional_rates_to_splitter = [ - constraints.stream_rates[stream_number] - for stream_number in self.outlet_stream_connected_to_stage.get(stage_number, []) - ] - additional_streams_to_mixer = [] - for stream_number in self.inlet_stream_connected_to_stage.get(stage_number, []): - if stream_number > 0: - if inlet_stream_counter < len(fluid_streams): - additional_streams_to_mixer.append( - fluid_streams[inlet_stream_counter].create_stream_with_new_conditions( - conditions=ProcessConditions( - pressure_bara=stage_inlet_stream.pressure_bara, - temperature_kelvin=stage_inlet_stream.temperature_kelvin, - ) - ) - ) - inlet_stream_counter += 1 - - stage_results.append( - stage.evaluate( - inlet_stream_stage=stage_inlet_stream, - additional_rates_to_splitter=additional_rates_to_splitter, - additional_streams_to_mixer=additional_streams_to_mixer, - speed=self.shaft.get_speed(), - asv_rate_fraction=asv_rate_fraction, - asv_additional_mass_rate=asv_additional_mass_rate, - ) - ) - - previous_stage_outlet_stream = stage_results[-1].outlet_stream - - # check if target pressures are met - target_pressure_status = self.check_target_pressures(constraints=constraints, results=stage_results) - - return CompressorTrainResultSingleTimeStep( - inlet_stream=train_inlet_stream, - outlet_stream=previous_stage_outlet_stream, - stage_results=stage_results, - speed=self.shaft.get_speed(), - above_maximum_power=( - sum(stage_result.power_megawatt for stage_result in stage_results) > self.maximum_power - if self.maximum_power - else False - ), - target_pressure_status=target_pressure_status, - ) - def find_and_calculate_for_compressor_train_with_two_pressure_requirements( self, stage_number_for_intermediate_pressure_target: int | None, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], pressure_control_first_part: FixedSpeedPressureControl, pressure_control_last_part: FixedSpeedPressureControl, ) -> CompressorTrainResultSingleTimeStep: @@ -547,7 +156,8 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( Args: stage_number_for_intermediate_pressure_target (int): The stage index at which the intermediate pressure target applies. - constraints (CompressorTrainEvaluationInput): The operating constraints, including pressures and stream rates. + streams (list[FluidStream|float]): List of fluid streams or rates associated with the compressor train. + boundary_conditions (dict[str, float]): Dictionary containing boundary conditions such as discharge pressure and interstage pressure. pressure_control_first_part (FixedSpeedPressureControl): Pressure control strategy for the first sub-train. pressure_control_last_part (FixedSpeedPressureControl): Pressure control strategy for the second sub-train. @@ -556,7 +166,11 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( inlet/outlet streams, speed, and pressure status. """ # This method requires stream_rates to be set for splitting operations - assert constraints.stream_rates is not None + target_discharge_pressure = boundary_conditions.get("discharge_pressure", None) + target_interstage_pressure = boundary_conditions.get("interstage_pressure", None) + + assert target_discharge_pressure is not None + assert target_interstage_pressure is not None assert stage_number_for_intermediate_pressure_target is not None # Split train into two and calculate minimum speed to reach required pressures @@ -567,75 +181,61 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( pressure_control_last_part=pressure_control_last_part, ) - std_rates_first_part, std_rates_last_part = split_rates_on_stage_number( + streams_first_part, streams_last_part = split_streams_on_stage_number( compressor_train=self, - rates_per_stream=constraints.stream_rates, + streams=streams, stage_number=stage_number_for_intermediate_pressure_target, ) - constraints_first_part = CompressorTrainEvaluationInput( - suction_pressure=constraints.suction_pressure, - discharge_pressure=constraints.interstage_pressure, - rate=std_rates_first_part[0], - stream_rates=std_rates_first_part, - ) - constraints_last_part = CompressorTrainEvaluationInput( - suction_pressure=constraints.interstage_pressure, - discharge_pressure=constraints.discharge_pressure, - rate=std_rates_last_part[0], - stream_rates=std_rates_last_part, - ) - - compressor_train_first_part.find_fixed_shaft_speed_given_constraints( - constraints=constraints_first_part, + boundary_conditions_first_part = { + "discharge_pressure": target_interstage_pressure, + } + boundary_conditions_last_part = { + "discharge_pressure": target_discharge_pressure, + } + + compressor_train_first_part.find_fixed_shaft_speed( + streams=streams_first_part, + boundary_conditions=boundary_conditions_first_part, lower_bound_for_speed=self.minimum_speed, # Only search for a solution within the bounds of the upper_bound_for_speed=self.maximum_speed, # original, complete compressor train ) if math.isclose(compressor_train_first_part.shaft.get_speed(), self.minimum_speed, rel_tol=EPSILON): compressor_train_results_first_part_with_optimal_speed_result = ( - compressor_train_first_part.evaluate_with_pressure_control_given_constraints( - constraints=constraints_first_part, + compressor_train_first_part.evaluate_with_pressure_control( + streams=streams_first_part, + boundary_conditions=boundary_conditions_first_part, ) ) else: compressor_train_results_first_part_with_optimal_speed_result = ( compressor_train_first_part.calculate_compressor_train( - constraints=constraints_first_part, + streams=streams_first_part, + boundary_conditions=boundary_conditions_first_part, ) ) # set self.inlet_fluid based on outlet_stream_first_part - compressor_train_last_part.streams[0].fluid_model = FluidModel( - composition=compressor_train_results_first_part_with_optimal_speed_result.stage_results[ - -1 - ].outlet_stream.thermo_system.composition, - eos_model=compressor_train_results_first_part_with_optimal_speed_result.stage_results[ - -1 - ].outlet_stream.thermo_system.eos_model, - ) - - # Update fluid factory to match the new fluid model - # This ensures base class methods use the correct fluid properties/composition - assert isinstance(compressor_train_last_part._fluid_factory, list) # for mypy - compressor_train_last_part._fluid_factory[0] = compressor_train_last_part._fluid_factory[ - 0 - ].create_fluid_factory_from_fluid_model(compressor_train_last_part.streams[0].fluid_model) + streams_last_part[0] = compressor_train_results_first_part_with_optimal_speed_result.outlet_stream - compressor_train_last_part.find_fixed_shaft_speed_given_constraints( - constraints=constraints_last_part, + compressor_train_last_part.find_fixed_shaft_speed( + streams=streams_last_part, + boundary_conditions=boundary_conditions_last_part, lower_bound_for_speed=self.minimum_speed, upper_bound_for_speed=self.maximum_speed, ) if math.isclose(compressor_train_last_part.shaft.get_speed(), self.minimum_speed, rel_tol=EPSILON): compressor_train_results_last_part_with_optimal_speed_result = ( - compressor_train_last_part.evaluate_with_pressure_control_given_constraints( - constraints=constraints_last_part, + compressor_train_last_part.evaluate_with_pressure_control( + streams=streams_last_part, + boundary_conditions=boundary_conditions_last_part, ) ) else: compressor_train_results_last_part_with_optimal_speed_result = ( compressor_train_last_part.calculate_compressor_train( - constraints=constraints_last_part, + streams=streams_last_part, + boundary_conditions=boundary_conditions_last_part, ) ) @@ -647,8 +247,9 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( if compressor_train_first_part.shaft.get_speed() > compressor_train_last_part.shaft.get_speed(): compressor_train_last_part.shaft.set_speed(compressor_train_first_part.shaft.get_speed()) compressor_train_results_last_part_with_pressure_control = ( - compressor_train_last_part.evaluate_with_pressure_control_given_constraints( - constraints=constraints_last_part, + compressor_train_last_part.evaluate_with_pressure_control( + streams=streams_last_part, + boundary_conditions=boundary_conditions_last_part, ) ) compressor_train_results_to_return_first_part = ( @@ -659,8 +260,9 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( else: compressor_train_first_part.shaft.set_speed(compressor_train_last_part.shaft.get_speed()) compressor_train_results_first_part_with_pressure_control = ( - compressor_train_first_part.evaluate_with_pressure_control_given_constraints( - constraints=constraints_first_part, + compressor_train_first_part.evaluate_with_pressure_control( + streams=streams_first_part, + boundary_conditions=boundary_conditions_first_part, ) ) compressor_train_results_to_return_first_part = compressor_train_results_first_part_with_pressure_control @@ -687,7 +289,7 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( # check if target pressures are met target_pressure_status = self.check_target_pressures( - constraints=constraints, + boundary_conditions=boundary_conditions, results=train_result, ) @@ -696,11 +298,11 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( return train_result -def split_rates_on_stage_number( +def split_streams_on_stage_number( compressor_train: CompressorTrainCommonShaftMultipleStreamsAndPressures, - rates_per_stream: list[float], + streams: list[FluidStream | float], stage_number: int, -) -> tuple[list[float], list[float]]: +) -> tuple[list[FluidStream | float], list[FluidStream | float]]: """ Splits the stream rates for a compressor train into two lists at the specified stage number. @@ -709,7 +311,7 @@ def split_rates_on_stage_number( Args: compressor_train: The compressor train instance containing stream and stage information. - rates_per_stream: List of rates for each stream in the compressor train. + streams: The streams being processed. stage_number: The stage index at which to split the rates (0-based). Returns: @@ -717,21 +319,18 @@ def split_rates_on_stage_number( - List of rates for the first part (before the split stage). - List of rates for the last part (from the split stage onward). """ - rates_first_part = [ - rates_per_stream[i] - for i, stream in enumerate(compressor_train.streams) - if stream.connected_to_stage_no < stage_number + streams_first_part = [ + streams[i] for i, stream in enumerate(compressor_train.streams) if stream.connected_to_stage_no < stage_number ] - rates_last_part = [ - compressor_train.convert_to_rates_for_each_compressor_train_stages(rates_per_stream)[stage_number - 1] + streams_last_part = [ + streams[i] for i, stream in enumerate(compressor_train.streams) if stream.connected_to_stage_no >= stage_number ] + streams_last_part.insert( + 0, streams_first_part[0] + ) # just a placeholder for the stream coming out of first part after calculation - for i, stream in enumerate(compressor_train.streams): - if stream.connected_to_stage_no >= stage_number: - rates_last_part.append(rates_per_stream[i]) - - return rates_first_part, rates_last_part + return streams_first_part, streams_last_part def split_train_on_stage_number( diff --git a/src/libecalc/domain/process/compressor/core/train/simplified_train/simplified_train.py b/src/libecalc/domain/process/compressor/core/train/simplified_train/simplified_train.py index 684a181776..6776e5f703 100644 --- a/src/libecalc/domain/process/compressor/core/train/simplified_train/simplified_train.py +++ b/src/libecalc/domain/process/compressor/core/train/simplified_train/simplified_train.py @@ -9,7 +9,6 @@ ) from libecalc.domain.process.compressor.core.train.base import CompressorTrainModel from libecalc.domain.process.compressor.core.train.stage import CompressorTrainStage -from libecalc.domain.process.compressor.core.train.train_evaluation_input import CompressorTrainEvaluationInput from libecalc.domain.process.compressor.core.train.utils.enthalpy_calculations import ( calculate_enthalpy_change_head_iteration, calculate_polytropic_head_campbell, @@ -96,35 +95,23 @@ def __init__( calculate_max_rate=calculate_max_rate, ) - def evaluate_given_constraints( + def evaluate_single_timestep( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> CompressorTrainResultSingleTimeStep: - """ - Calculate pressure ratios, find maximum pressure ratio, number of compressors in - the train, and pressure ratio per stage. Calculate fluid mass rate per hour and - results per compressor in the train given mass rate and inter-stage pressures. - - Note: - - When the number of compressors in the train is not defined, the method determines - how many are needed based on the rate and pressure data used for evaluation. - - This approach may not work well with compressor systems, as the number of stages - may change with different rates. + """Evaluate a single timestep on the fluid streams.""" + assert len(streams) == 1 - Returns: - CompressorTrainResultSingleTimeStep: The result of the compressor train evaluation. - """ - assert constraints.suction_pressure is not None - assert constraints.discharge_pressure is not None - assert constraints.rate is not None + inlet_stream = streams[0] + discharge_pressure = boundary_conditions.get("discharge_pressure", None) + suction_pressure = inlet_stream.pressure_bara + assert isinstance(inlet_stream, FluidStream) + assert suction_pressure is not None + assert discharge_pressure is not None pressure_ratios_per_stage = self.calculate_pressure_ratios_per_stage( - suction_pressure=constraints.suction_pressure, discharge_pressure=constraints.discharge_pressure - ) - inlet_stream = self.train_inlet_stream( - pressure=constraints.suction_pressure, - temperature=self.stages[0].inlet_temperature_kelvin, - rate=constraints.rate, + suction_pressure=suction_pressure, discharge_pressure=discharge_pressure ) if inlet_stream.mass_rate_kg_per_h > 0: compressor_stages_result = [] @@ -147,7 +134,7 @@ def evaluate_given_constraints( if self.maximum_power else False, target_pressure_status=self.check_target_pressures( - constraints=constraints, + boundary_conditions=boundary_conditions, results=compressor_stages_result, ), inlet_stream=compressor_stages_result[0].inlet_stream, @@ -279,9 +266,10 @@ def calculate_compressor_stage_work_given_outlet_pressure( point_is_valid=~np.isnan(power_mw), # type: ignore[arg-type] # power_mw is set to np.NaN if invalid step. ) - def _get_max_std_rate_single_timestep( + def get_max_standard_rate_single_timestep( self, - constraints: CompressorTrainEvaluationInput, + streams: list[FluidStream | float], + boundary_conditions: dict[str, float], ) -> float: """Calculate maximum standard rate for a single timestep. @@ -290,14 +278,14 @@ def _get_max_std_rate_single_timestep( operational constraints, including the maximum allowable power and the compressor chart limits. Args: - constraints (CompressorTrainEvaluationInput): The input constraints for the compressor train evaluation + streams: + boundary_conditions: Returns: float: The maximum standard volume rate in Sm3/day. Returns NaN if the calculation fails. """ - - suction_pressure = constraints.suction_pressure - discharge_pressure = constraints.discharge_pressure + suction_pressure = streams[0].pressure_bara + discharge_pressure = boundary_conditions.get("discharge_pressure", None) assert suction_pressure is not None and discharge_pressure is not None, "Pressures should be defined" diff --git a/src/libecalc/domain/process/entities/process_units/mixer/mixer.py b/src/libecalc/domain/process/entities/process_units/mixer/mixer.py index be8bf843f8..987914bbb5 100644 --- a/src/libecalc/domain/process/entities/process_units/mixer/mixer.py +++ b/src/libecalc/domain/process/entities/process_units/mixer/mixer.py @@ -1,4 +1,4 @@ -from libecalc.domain.process.value_objects.fluid_stream import FluidStream, SimplifiedStreamMixing +from libecalc.domain.process.value_objects.fluid_stream import FluidStream, ProcessConditions, SimplifiedStreamMixing from libecalc.domain.process.value_objects.fluid_stream.mixing import StreamMixingStrategy @@ -20,4 +20,16 @@ def mix_streams(self, streams: list[FluidStream]) -> FluidStream: if self.number_of_inputs != len(streams): raise ValueError("Number of input streams must match the number of inputs defined for the mixer.") - return self.mixing_strategy.mix_streams(streams=streams) + # Today for simplicity we assume that the first stream decides the pressure and temperature of all streams entering the mixer + # This is how eCalc operates today. In the future we can implement more advanced mixing. + new_streams = [streams[0]] + [ + stream.create_stream_with_new_conditions( + conditions=ProcessConditions( + pressure_bara=streams[0].pressure_bara, + temperature_kelvin=streams[0].temperature_kelvin, + ), + ) + for stream in streams[1:] + ] + + return self.mixing_strategy.mix_streams(streams=new_streams) diff --git a/src/libecalc/domain/process/entities/process_units/splitter/splitter.py b/src/libecalc/domain/process/entities/process_units/splitter/splitter.py index ec9616e01d..3b51432909 100644 --- a/src/libecalc/domain/process/entities/process_units/splitter/splitter.py +++ b/src/libecalc/domain/process/entities/process_units/splitter/splitter.py @@ -11,7 +11,7 @@ def split_stream(self, stream: FluidStream, split_fractions: list[float]) -> lis Args: stream (FluidStream): The fluid stream to be split. - split_fractions (Sequence[float]): The fractions of the stream to go to the different output streams (0.0 to 1.0). + split_fractions (list(float)): The fractions of the stream to go to the different output streams (0.0 to 1.0). Returns: list[FluidStream]: A list containing the resulting FluidStreams. diff --git a/src/libecalc/domain/process/entities/process_units/temperature_setter/temperature_setter.py b/src/libecalc/domain/process/entities/process_units/temperature_setter/temperature_setter.py index ebb32a2d08..d614932b4e 100644 --- a/src/libecalc/domain/process/entities/process_units/temperature_setter/temperature_setter.py +++ b/src/libecalc/domain/process/entities/process_units/temperature_setter/temperature_setter.py @@ -10,7 +10,8 @@ def required_temperature_kelvin(self) -> float: return self._required_temperature_kelvin def set_temperature(self, stream: FluidStream) -> FluidStream: - if stream.temperature_kelvin > self.required_temperature_kelvin: + # Today we dont have a nice way of defining streams in the YAML file, so all streams are + if stream.temperature_kelvin != self.required_temperature_kelvin: return stream.create_stream_with_new_conditions( conditions=ProcessConditions( pressure_bara=stream.pressure_bara, diff --git a/src/libecalc/presentation/yaml/domain/expression_time_series_pressure.py b/src/libecalc/presentation/yaml/domain/expression_time_series_pressure.py index 95a3913dfa..61881dfa93 100644 --- a/src/libecalc/presentation/yaml/domain/expression_time_series_pressure.py +++ b/src/libecalc/presentation/yaml/domain/expression_time_series_pressure.py @@ -40,7 +40,7 @@ def _validate(self): if should_validate: # TODO: this comparison should in reality be <= 0, but since there are slight confusion around units # bara vs barg in the input data, we allow 0 for now. Will be tightened up in future. - if pressure < 0: + if pressure <= 0: raise InvalidPressureException(pressure, str(self._time_series_expression.get_expression())) def get_periods(self) -> Periods: diff --git a/tests/libecalc/core/models/compressor_modelling/test_compressor_train_common_shaft.py b/tests/libecalc/core/models/compressor_modelling/test_compressor_train_common_shaft.py index f299e6140e..34a5dbb889 100644 --- a/tests/libecalc/core/models/compressor_modelling/test_compressor_train_common_shaft.py +++ b/tests/libecalc/core/models/compressor_modelling/test_compressor_train_common_shaft.py @@ -8,7 +8,7 @@ from libecalc.domain.process.compressor.core.train.train_evaluation_input import CompressorTrainEvaluationInput from libecalc.domain.process.core.results.compressor import CompressorTrainCommonShaftFailureStatus from libecalc.domain.process.value_objects.chart.chart_area_flag import ChartAreaFlag -from libecalc.domain.process.value_objects.fluid_stream import FluidComposition +from libecalc.domain.process.value_objects.fluid_stream import FluidComposition, FluidStream from libecalc.domain.process.value_objects.fluid_stream.fluid_model import FluidModel, EoSModel from libecalc.infrastructure.neqsim_fluid_provider.neqsim_fluid_factory import NeqSimFluidFactory @@ -349,16 +349,21 @@ def test_calculate_single_speed_train(single_speed_compressor_train_common_shaft pressure_control=FixedSpeedPressureControl.DOWNSTREAM_CHOKE ) + streams = [ + fluid_factory_medium.create_stream_from_mass_rate( + pressure_bara=inlet_pressure_train_bara, + temperature_kelvin=compressor_train.stages[0].inlet_temperature_kelvin, + mass_rate_kg_per_h=mass_rate_kg_per_hour, + ) + ] compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, rate=1, suction_pressure=1, discharge_pressure=2 ) speed = compressor_train.stages[0].compressor.compressor_chart.curves[0].speed compressor_train.shaft.set_speed(speed) result = compressor_train.calculate_compressor_train( - constraints=CompressorTrainEvaluationInput( - rate=compressor_train._fluid_factory.mass_rate_to_standard_rate(mass_rate_kg_per_hour), - suction_pressure=inlet_pressure_train_bara, - ) + streams=streams, + boundary_conditions={}, ) # Stability tests @@ -382,9 +387,9 @@ def test_calculate_evaluate_rate_ps_pd_single_speed_train_with_max_rate( ) compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray(rate), - suction_pressure=np.asarray(suction_pressure), - discharge_pressure=np.asarray(discharge_pressure), + rate=np.asarray(rate, dtype=float), + suction_pressure=np.asarray(suction_pressure, dtype=float), + discharge_pressure=np.asarray(discharge_pressure, dtype=float), ) result = compressor_train.evaluate() @@ -400,9 +405,9 @@ def test_calculate_single_speed_train_zero_mass_rate( ) compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.array([0]), - suction_pressure=np.array([1]), - discharge_pressure=np.array([2]), + rate=np.array([0], dtype=float), + suction_pressure=np.array([1], dtype=float), + discharge_pressure=np.array([2], dtype=float), ) result = compressor_train.evaluate() @@ -499,9 +504,9 @@ def test_points_above_and_below_maximum_power(single_speed_compressor_train_comm ) compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([1800000, 2200000]), - suction_pressure=np.asarray([30, 30]), - discharge_pressure=np.asarray([80.0, 80.0]), + rate=np.asarray([1800000, 2200000], dtype=float), + suction_pressure=np.asarray([30, 30], dtype=float), + discharge_pressure=np.asarray([80.0, 80.0], dtype=float), ) result = compressor_train.evaluate() energy_result = result.get_energy_result() @@ -517,9 +522,9 @@ def test_single_point_within_capacity_one_compressor(self, variable_speed_compre compressor_train = variable_speed_compressor_train() compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([7000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([100.0]), + rate=np.asarray([7000], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([100.0], dtype=float), ) result = compressor_train.evaluate() assert result.mass_rate_kg_per_hr[0] == pytest.approx(240.61266437085808) @@ -532,9 +537,9 @@ def test_points_above_and_below_maximum_power(self, variable_speed_compressor_tr compressor_train = variable_speed_compressor_train(maximum_power=7.0) compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([3000000, 3500000]), - suction_pressure=np.asarray([30, 30]), - discharge_pressure=np.asarray([100.0, 100.0]), + rate=np.asarray([3000000, 3500000], dtype=float), + suction_pressure=np.asarray([30, 30], dtype=float), + discharge_pressure=np.asarray([100.0, 100.0], dtype=float), ) result = compressor_train.evaluate() @@ -557,9 +562,9 @@ def test_single_point_rate_too_high_no_pressure_control( compressor_train = variable_speed_compressor_train(pressure_control=None) compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([7000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([target_pressure]), + rate=np.asarray([7000], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([target_pressure], dtype=float), ) result = compressor_train.evaluate() assert not np.any(result.pressure_is_choked) @@ -576,9 +581,9 @@ def test_single_point_recirculate_on_minimum_speed_curve_one_compressor( ) compressor_train.set_evaluation_input( fluid_factory=fluid_factory, - rate=np.asarray([1500000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([55.0]), + rate=np.asarray([1500000], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([55.0], dtype=float), ) result = compressor_train.evaluate() @@ -588,18 +593,15 @@ def test_single_point_recirculate_on_minimum_speed_curve_one_compressor( np.testing.assert_allclose(result.get_energy_result().power.values[0], 2.5070, rtol=0.001) - def test_zero_rate_zero_pressure(self, variable_speed_compressor_train, fluid_model_medium): - """We want to get a result object when rate is zero regardless of invalid/zero pressures. To ensure - this we set pressure -> 1 when both rate and pressure is zero. This may happen when pressure is a function - of rate. - """ + def test_zero_rate(self, variable_speed_compressor_train, fluid_model_medium): + """We want to get a result object when rate is zero.""" fluid_factory = NeqSimFluidFactory(fluid_model=fluid_model_medium) compressor_train = variable_speed_compressor_train() compressor_train.set_evaluation_input( fluid_factory=fluid_factory, - rate=np.array([0, 1, 1]), - suction_pressure=np.array([0, 1, 1]), - discharge_pressure=np.array([0, 2, 2]), + rate=np.array([0, 1], dtype=float), + suction_pressure=np.array([1, 1], dtype=float), + discharge_pressure=np.array([2, 2], dtype=float), ) result = compressor_train.evaluate() @@ -607,7 +609,6 @@ def test_zero_rate_zero_pressure(self, variable_speed_compressor_train, fluid_mo assert result.failure_status == [ CompressorTrainCommonShaftFailureStatus.NO_FAILURE, CompressorTrainCommonShaftFailureStatus.NO_FAILURE, - CompressorTrainCommonShaftFailureStatus.NO_FAILURE, ] assert np.isnan(result.mass_rate_kg_per_hr[0]) @@ -615,9 +616,9 @@ def test_zero_rate_zero_pressure(self, variable_speed_compressor_train, fluid_mo assert np.isnan(result.outlet_stream.pressure[0]) energy_result = result.get_energy_result() - np.testing.assert_allclose(energy_result.energy_usage.values, np.array([0.0, 0.092847, 0.092847]), rtol=0.0001) + np.testing.assert_allclose(energy_result.energy_usage.values, np.array([0.0, 0.092847]), rtol=0.0001) assert energy_result.power.values[0] == 0 - assert energy_result.is_valid == [True, True, True] + assert energy_result.is_valid == [True, True] def test_single_point_within_capacity_one_compressor_add_constant( self, variable_speed_compressor_train, fluid_model_medium @@ -629,17 +630,17 @@ def test_single_point_within_capacity_one_compressor_add_constant( compressor_train.set_evaluation_input( fluid_factory=fluid_factory, - rate=np.asarray([3000000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([100.0]), + rate=np.asarray([3000000], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([100.0], dtype=float), ) result_comparison = compressor_train.evaluate() compressor_train_adjusted.set_evaluation_input( fluid_factory=fluid_factory, - rate=np.asarray([3000000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([100.0]), + rate=np.asarray([3000000], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([100.0], dtype=float), ) result = compressor_train_adjusted.evaluate() @@ -661,9 +662,9 @@ def test_single_point_downstream_choke( compressor_train = variable_speed_compressor_train() compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([3000000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([40.0]), + rate=np.asarray([3000000], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([40.0], dtype=float), ) result = compressor_train.evaluate() @@ -674,8 +675,8 @@ def test_single_point_downstream_choke( assert all(result.get_energy_result().is_valid) -def test_find_fixed_shaft_speed_given_constraints(): - func = CompressorTrainCommonShaft.find_fixed_shaft_speed_given_constraints +def test_find_fixed_shaft_speed(): + func = CompressorTrainCommonShaft.find_fixed_shaft_speed assert func # Depends on test case with real data to make sense # Will be done after asset test case is established @@ -683,17 +684,20 @@ def test_find_fixed_shaft_speed_given_constraints(): def test_calculate_compressor_train_given_speed_invalid(variable_speed_compressor_train, fluid_factory_medium): compressor_train = variable_speed_compressor_train() - compressor_train.set_evaluation_input( - fluid_factory=fluid_factory_medium, rate=1, suction_pressure=1, discharge_pressure=2 - ) # dummy values for rates and pressures, only need fluid factory + streams = [ + fluid_factory_medium.create_stream_from_mass_rate( + pressure_bara=1, + temperature_kelvin=300, + mass_rate_kg_per_h=1, + ) + ] + boundary_conditions = {"pressure": [1], "temperature": [1]} compressor_train.shaft.set_speed(1) with pytest.raises(IllegalStateException): _ = compressor_train.calculate_compressor_train( - constraints=CompressorTrainEvaluationInput( - suction_pressure=50, - rate=compressor_train._fluid_factory.mass_rate_to_standard_rate(6000000.0), - ) + streams=streams, + boundary_conditions=boundary_conditions, ) @@ -713,60 +717,59 @@ def test_find_and_calculate_for_compressor_shaft_speed_given_rate_ps_pd_invalid_ ) ) - compressor_train.set_evaluation_input( - fluid_factory=fluid_factory_medium, rate=1, suction_pressure=1, discharge_pressure=2 - ) # dummy values for rates and pressures, only need fluid factory + streams = [ + fluid_factory_medium.create_stream_from_mass_rate( + pressure_bara=20, + temperature_kelvin=293.15, + mass_rate_kg_per_h=mass_rate_kg_per_hour, + ) + ] + boundary_conditions = {"discharge_pressure": 400} - standard_rate = compressor_train._fluid_factory.mass_rate_to_standard_rate(mass_rate_kg_per_hour) # rate too large - result = compressor_train.evaluate_given_constraints( - constraints=CompressorTrainEvaluationInput( - rate=standard_rate, - suction_pressure=20, - discharge_pressure=400, - ) + result = compressor_train.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, ) assert result.chart_area_status == ChartAreaFlag.ABOVE_MAXIMUM_FLOW_RATE # Target pressure too large, but rate still too high - result = compressor_train.evaluate_given_constraints( - constraints=CompressorTrainEvaluationInput( - rate=standard_rate, - suction_pressure=20, - discharge_pressure=1000, - ) + boundary_conditions = {"discharge_pressure": 1000} + result = compressor_train.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, ) assert result.failure_status == CompressorTrainCommonShaftFailureStatus.ABOVE_MAXIMUM_FLOW_RATE # Target pressure too low -> but still possible because of downstream choke. However, the rate is still too high. - result = compressor_train.evaluate_given_constraints( - constraints=CompressorTrainEvaluationInput( - rate=standard_rate, - suction_pressure=20, - discharge_pressure=1, - ) + boundary_conditions = {"discharge_pressure": 1} + result = compressor_train.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, ) assert result.chart_area_status == ChartAreaFlag.ABOVE_MAXIMUM_FLOW_RATE assert not result.is_valid # Rate is too large, but point is below stonewall, i.e. rate is too large for resulting speed, but not too large # for maximum speed - result = compressor_train.evaluate_given_constraints( - constraints=CompressorTrainEvaluationInput( - rate=standard_rate, - suction_pressure=20, - discharge_pressure=600, - ) + boundary_conditions = {"discharge_pressure": 600} + result = compressor_train.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, ) assert result.chart_area_status == ChartAreaFlag.ABOVE_MAXIMUM_FLOW_RATE # Point where rate is recirculating - result = compressor_train.evaluate_given_constraints( - constraints=CompressorTrainEvaluationInput( - rate=1, - suction_pressure=20, - discharge_pressure=400, + streams = [ + fluid_factory_medium.create_stream_from_mass_rate( + pressure_bara=20, + temperature_kelvin=293.15, + mass_rate_kg_per_h=1, ) + ] + result = compressor_train.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, ) # Check that actual rate is equal to minimum rate for the speed assert result.stage_results[0].inlet_actual_rate_asv_corrected_m3_per_hour == pytest.approx( @@ -781,9 +784,9 @@ def test_variable_speed_compressor_train_vs_unisim_methane(variable_speed_compre compressor_train.set_evaluation_input( fluid_factory=fluid_factory, - rate=np.asarray([3537181, 5305772, 6720644, 5305772, 5305772]), - suction_pressure=np.asarray([40, 40, 40, 40, 40]), - discharge_pressure=np.asarray([100, 100, 100, 110, 70]), + rate=np.asarray([3537181, 5305772, 6720644, 5305772, 5305772], dtype=float), + suction_pressure=np.asarray([40, 40, 40, 40, 40], dtype=float), + discharge_pressure=np.asarray([100, 100, 100, 110, 70], dtype=float), ) result = compressor_train.evaluate() @@ -844,17 +847,17 @@ def test_adjustment_constant_and_factor_one_compressor(variable_speed_compressor compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([7000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([100.0]), + rate=np.asarray([7000], dtype=float), + suction_pressure=np.asarray([30.0], dtype=float), + discharge_pressure=np.asarray([100.0], dtype=float), ) result = compressor_train.evaluate() compressor_train_adjusted.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([7000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([100.0]), + rate=np.asarray([7000.0], dtype=float), + suction_pressure=np.asarray([30.0], dtype=float), + discharge_pressure=np.asarray([100.0], dtype=float), ) result_adjusted = compressor_train_adjusted.evaluate() @@ -868,39 +871,47 @@ def test_get_max_standard_rate_with_and_without_maximum_power(variable_speed_com compressor_train = variable_speed_compressor_train() compressor_train_max_power = variable_speed_compressor_train(maximum_power=7.0) - compressor_train.set_evaluation_input( - fluid_factory=fluid_factory_medium, rate=1, suction_pressure=30, discharge_pressure=100 - ) - compressor_train_max_power.set_evaluation_input( - fluid_factory=fluid_factory_medium, rate=1, suction_pressure=30, discharge_pressure=100 - ) - - max_standard_rate_without_maximum_power = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([100]), - ) - max_standard_rate_with_maximum_power = compressor_train_max_power.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([100]), - ) - compressor_train.set_evaluation_input( - fluid_factory=fluid_factory_medium, - rate=np.asarray([max_standard_rate_without_maximum_power], dtype=float), - suction_pressure=np.asarray([30], dtype=float), - discharge_pressure=np.asarray([100], dtype=float), - ) - power_at_max_standard_rate_without_maximum_power = compressor_train.evaluate().get_energy_result().power.values - - compressor_train_max_power.set_evaluation_input( - fluid_factory=fluid_factory_medium, - rate=np.asarray([max_standard_rate_with_maximum_power], dtype=float), - suction_pressure=np.asarray([30], dtype=float), - discharge_pressure=np.asarray([100], dtype=float), - ) - power_at_max_standard_rate_with_maximum_power = ( - compressor_train_max_power.evaluate().get_energy_result().power.values - ) + streams = [ + fluid_factory_medium.create_stream_from_mass_rate( + pressure_bara=30, + temperature_kelvin=compressor_train.stages[0].inlet_temperature_kelvin, + mass_rate_kg_per_h=1, + ) + ] + boundary_conditions = {"discharge_pressure": 100} + + max_standard_rate_without_maximum_power = compressor_train.get_max_standard_rate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, + ) + max_standard_rate_with_maximum_power = compressor_train_max_power.get_max_standard_rate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, + ) + + streams_rate_without_maximum_power = [ + fluid_factory_medium.create_stream_from_standard_rate( + pressure_bara=30, + temperature_kelvin=compressor_train.stages[0].inlet_temperature_kelvin, + standard_rate_m3_per_day=max_standard_rate_without_maximum_power, + ) + ] + streams_rate_with_maximum_power = [ + fluid_factory_medium.create_stream_from_standard_rate( + pressure_bara=30, + temperature_kelvin=compressor_train.stages[0].inlet_temperature_kelvin, + standard_rate_m3_per_day=max_standard_rate_with_maximum_power, + ) + ] + power_at_max_standard_rate_without_maximum_power = compressor_train.evaluate_single_timestep( + streams=streams_rate_without_maximum_power, + boundary_conditions=boundary_conditions, + ).power_megawatt + power_at_max_standard_rate_with_maximum_power = compressor_train_max_power.evaluate_single_timestep( + streams=streams_rate_with_maximum_power, + boundary_conditions=boundary_conditions, + ).power_megawatt assert max_standard_rate_without_maximum_power > max_standard_rate_with_maximum_power assert power_at_max_standard_rate_without_maximum_power > power_at_max_standard_rate_with_maximum_power - assert power_at_max_standard_rate_with_maximum_power[0] < compressor_train_max_power.maximum_power + assert power_at_max_standard_rate_with_maximum_power < compressor_train_max_power.maximum_power diff --git a/tests/libecalc/core/models/compressor_modelling/test_compressor_train_multiple_streams.py b/tests/libecalc/core/models/compressor_modelling/test_compressor_train_multiple_streams.py index 32ef38dbfb..fde43174c7 100644 --- a/tests/libecalc/core/models/compressor_modelling/test_compressor_train_multiple_streams.py +++ b/tests/libecalc/core/models/compressor_modelling/test_compressor_train_multiple_streams.py @@ -1,7 +1,9 @@ import numpy as np import pytest +from numpy._typing import NDArray from libecalc.common.fixed_speed_pressure_control import FixedSpeedPressureControl, InterstagePressureControl +from libecalc.domain.component_validation_error import DomainValidationException from libecalc.domain.process.compressor.core.train.compressor_train_common_shaft import CompressorTrainCommonShaft from libecalc.domain.process.compressor.core.train.compressor_train_common_shaft_multiple_streams_and_pressures import ( CompressorTrainCommonShaftMultipleStreamsAndPressures, @@ -14,9 +16,9 @@ from libecalc.domain.process.value_objects.fluid_stream.fluid_model import FluidModel from libecalc.infrastructure.neqsim_fluid_provider.neqsim_fluid_factory import NeqSimFluidFactory -DEFAULT_RATE = np.asarray([1]) -DEFAULT_SUCTION_PRESSURE = np.asarray([30]) -DEFAULT_DISCHARGE_PRESSURE = np.asarray([100]) +DEFAULT_RATE = 1 +DEFAULT_SUCTION_PRESSURE = 30 +DEFAULT_DISCHARGE_PRESSURE = 100 def calculate_relative_difference(value1, value2): @@ -98,13 +100,34 @@ def two_streams(fluid_model_medium) -> list[FluidStreamObjectForMultipleStreams] def set_evaluation_input( - fluid_factory_medium, compressor_train: CompressorTrainCommonShaft + fluid_factory_medium, + compressor_train: CompressorTrainCommonShaft, + rate: float = DEFAULT_RATE, + suction_pressure: float = DEFAULT_SUCTION_PRESSURE, + discharge_pressure: float = DEFAULT_DISCHARGE_PRESSURE, ) -> CompressorTrainCommonShaft: compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=DEFAULT_RATE, - suction_pressure=DEFAULT_SUCTION_PRESSURE, - discharge_pressure=DEFAULT_DISCHARGE_PRESSURE, + rate=np.array([rate], dtype=float), + suction_pressure=np.array([suction_pressure], dtype=float), + discharge_pressure=np.asarray([discharge_pressure], dtype=float), + ) + return compressor_train + + +def set_evaluation_input_multiple_streams( + fluid_factory_medium, + compressor_train: CompressorTrainCommonShaft, + rate: float = DEFAULT_RATE, + suction_pressure: float = DEFAULT_SUCTION_PRESSURE, + discharge_pressure: float = DEFAULT_DISCHARGE_PRESSURE, + number_of_streams: int = 1, +) -> CompressorTrainCommonShaft: + compressor_train.set_evaluation_input( + fluid_factory=fluid_factory_medium, + rate=np.ones((number_of_streams, 1), dtype=float) * rate, + suction_pressure=np.array([suction_pressure], dtype=float), + discharge_pressure=np.asarray([discharge_pressure], dtype=float), ) return compressor_train @@ -118,54 +141,41 @@ def test_get_maximum_standard_rate_max_speed_curve( compressor_train = variable_speed_compressor_train(nr_stages=2) compressor_train_multiple_streams = variable_speed_compressor_train_multiple_streams_and_pressures(nr_stages=2) compressor_train = set_evaluation_input(fluid_factory_medium, compressor_train) - compressor_train_multiple_streams = set_evaluation_input([fluid_factory_medium], compressor_train_multiple_streams) + compressor_train_multiple_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium], compressor_train_multiple_streams + ) """Values are pinned against self. Need QA.""" # Low pressure - outside right end of max speed curve - outside_curve = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([100]), - ) - outside_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([100]), - ) + outside_curve = compressor_train.get_max_standard_rate() + outside_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate() # Right edge of max speed curve (critical boundary) - right_end_of_max_speed_curve = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([295.1]), - ) - right_end_of_max_speed_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([295.1]), + compressor_train = set_evaluation_input(fluid_factory_medium, compressor_train, discharge_pressure=295.1) + compressor_train_multiple_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium], compressor_train_multiple_streams, discharge_pressure=295.1 ) + right_end_of_max_speed_curve = compressor_train.get_max_standard_rate() + right_end_of_max_speed_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate() + # Middle pressure - middle of max speed curve - middle_of_max_speed_curve = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([350]), - ) - middle_of_max_speed_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([350]), + compressor_train = set_evaluation_input(fluid_factory_medium, compressor_train, discharge_pressure=350) + compressor_train_multiple_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium], compressor_train_multiple_streams, discharge_pressure=350 ) + middle_of_max_speed_curve = compressor_train.get_max_standard_rate() + middle_of_max_speed_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate() # High pressure - left end of max speed curve - left_end_of_max_speed_curve = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([400]), - ) - left_end_of_max_speed_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate( - suction_pressures=np.asarray([30]), - discharge_pressures=np.asarray([400]), + compressor_train = set_evaluation_input(fluid_factory_medium, compressor_train, discharge_pressure=400) + compressor_train_multiple_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium], compressor_train_multiple_streams, discharge_pressure=400 ) + left_end_of_max_speed_curve = compressor_train.get_max_standard_rate() + left_end_of_max_speed_curve_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate() # Assert that variable speed and variable speed with one stream give same results - np.testing.assert_allclose(outside_curve, outside_curve_multiple_streams, rtol=0.01) - np.testing.assert_allclose(right_end_of_max_speed_curve, right_end_of_max_speed_curve_multiple_streams, rtol=0.01) - np.testing.assert_allclose(middle_of_max_speed_curve, middle_of_max_speed_curve_multiple_streams, rtol=0.01) - np.testing.assert_allclose(left_end_of_max_speed_curve, left_end_of_max_speed_curve_multiple_streams, rtol=0.01) # Verify pressure control: pressures at/beyond max rate point give same result np.testing.assert_allclose(right_end_of_max_speed_curve, outside_curve, rtol=0.01) @@ -187,38 +197,36 @@ def test_get_maximum_standard_rate_at_stone_wall( compressor_train_multiple_streams = variable_speed_compressor_train_multiple_streams_and_pressures( nr_stages=2, pressure_control=FixedSpeedPressureControl.INDIVIDUAL_ASV_PRESSURE ) - compressor_train = set_evaluation_input(fluid_factory_medium, compressor_train) - compressor_train_multiple_streams = set_evaluation_input([fluid_factory_medium], compressor_train_multiple_streams) + compressor_train = set_evaluation_input( + fluid_factory_medium, compressor_train, suction_pressure=30, discharge_pressure=50 + ) + compressor_train_multiple_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium], compressor_train_multiple_streams, suction_pressure=30, discharge_pressure=50 + ) """Values are pinned against self. Need QA.""" - below_stone_wall = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([50.0]), + below_stone_wall = compressor_train.get_max_standard_rate() + below_stone_wall_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate() + + compressor_train = set_evaluation_input( + fluid_factory_medium, compressor_train, suction_pressure=30, discharge_pressure=100 ) - below_stone_wall_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([50.0]), + compressor_train_multiple_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium], compressor_train_multiple_streams, suction_pressure=30, discharge_pressure=100 ) + maximum_rate_stone_wall_100 = compressor_train.get_max_standard_rate() - maximum_rate_stone_wall_100 = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([100.0]), - ) + maximum_rate_stone_wall_100_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate() - maximum_rate_stone_wall_100_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([100.0]), + compressor_train = set_evaluation_input( + fluid_factory_medium, compressor_train, suction_pressure=30, discharge_pressure=200 ) - - maximum_rate_stone_wall_200 = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([200.0]), + compressor_train_multiple_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium], compressor_train_multiple_streams, suction_pressure=30, discharge_pressure=200 ) + maximum_rate_stone_wall_200 = compressor_train.get_max_standard_rate() - maximum_rate_stone_wall_200_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([200.0]), - ) + maximum_rate_stone_wall_200_multiple_streams = compressor_train_multiple_streams.get_max_standard_rate() np.testing.assert_allclose(below_stone_wall, 0.0) np.testing.assert_allclose(maximum_rate_stone_wall_100, 3457025, rtol=0.01) @@ -266,31 +274,31 @@ def test_variable_speed_vs_variable_speed_multiple_streams_and_pressures( compressor_train_one_compressor.set_evaluation_input( fluid_factory=fluid_factory, - rate=np.asarray([3000000]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([100]), + rate=np.asarray([3000000], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([100], dtype=float), ) result_variable_speed_compressor_train_one_compressor = compressor_train_one_compressor.evaluate() compressor_train_two_compressors.set_evaluation_input( fluid_factory=fluid_factory, - rate=np.asarray([2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000]), - suction_pressure=np.asarray([30, 30, 30, 30, 30, 30, 30, 30]), - discharge_pressure=np.asarray([100.0, 110, 120, 130, 140, 150, 160, 170]), + rate=np.asarray([2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000], dtype=float), + suction_pressure=np.asarray([30, 30, 30, 30, 30, 30, 30, 30], dtype=float), + discharge_pressure=np.asarray([100.0, 110, 120, 130, 140, 150, 160, 170], dtype=float), ) result_variable_speed_compressor_train_two_compressors = compressor_train_two_compressors.evaluate() compressor_train_multiple_streams_one_compressor.set_evaluation_input( fluid_factory=[fluid_factory], - rate=np.asarray([[3000000]]), - suction_pressure=np.asarray([30]), - discharge_pressure=np.asarray([100]), + rate=np.asarray([[3000000]], dtype=float), + suction_pressure=np.asarray([30], dtype=float), + discharge_pressure=np.asarray([100], dtype=float), ) result_variable_speed_compressor_train_one_compressor_one_stream = ( compressor_train_multiple_streams_one_compressor.evaluate() ) compressor_train_multiple_streams_two_compressors.set_evaluation_input( fluid_factory=[fluid_factory], - rate=np.asarray([[2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000]]), + rate=np.asarray([[2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000, 2500000]], dtype=float), suction_pressure=np.asarray([30, 30, 30, 30, 30, 30, 30, 30], dtype=float), discharge_pressure=np.asarray([100.0, 110, 120, 130, 140, 150, 160, 170], dtype=float), ) @@ -338,7 +346,7 @@ def test_points_within_capacity_two_compressors_two_streams( fluid_streams=two_streams, ) compressor_train.set_evaluation_input( - fluid_factory=[fluid_factory], + fluid_factory=[fluid_factory, None], rate=np.asarray([[6000], [2000]]), suction_pressure=np.asarray([30]), discharge_pressure=np.asarray([110.0]), @@ -367,43 +375,40 @@ def test_get_maximum_standard_rate_too_high_pressure_ratio( nr_stages=2, fluid_streams=fluid_streams, ) - compressor_train = set_evaluation_input(fluid_factory_medium, compressor_train) - compressor_train_multiple_streams_one_stream = set_evaluation_input( - [fluid_factory_medium], compressor_train_multiple_streams_one_stream + compressor_train = set_evaluation_input( + fluid_factory_medium, compressor_train, suction_pressure=30.0, discharge_pressure=1000.0 + ) + compressor_train_multiple_streams_one_stream = set_evaluation_input_multiple_streams( + [fluid_factory_medium], + compressor_train_multiple_streams_one_stream, + suction_pressure=30.0, + discharge_pressure=1000.0, ) - compressor_train_multiple_streams_two_streams = set_evaluation_input( - [fluid_factory_medium, fluid_factory_medium], compressor_train_multiple_streams_two_streams + compressor_train_multiple_streams_two_streams = set_evaluation_input_multiple_streams( + [fluid_factory_medium, fluid_factory_medium], + compressor_train_multiple_streams_two_streams, + suction_pressure=30.0, + discharge_pressure=1000.0, + number_of_streams=2, ) """Values are pinned against self. Need QA.""" # Check point where head requirement is too high. ASV should make no difference here. - maximum_rate_max_not_existing = compressor_train.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([1000.0]), - ) + maximum_rate_max_not_existing = compressor_train.get_max_standard_rate() np.testing.assert_allclose(maximum_rate_max_not_existing, 0) # Same for multiple streams and pressures train with one stream - maximum_rate_max_not_existing = compressor_train_multiple_streams_one_stream.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([1000.0]), - ) + maximum_rate_max_not_existing = compressor_train_multiple_streams_one_stream.get_max_standard_rate() np.testing.assert_allclose(maximum_rate_max_not_existing, 0) # Same for multiple streams and pressures train with two streams - maximum_rate_max_not_existing = compressor_train_multiple_streams_two_streams.get_max_standard_rate( - suction_pressures=np.asarray([30.0]), - discharge_pressures=np.asarray([1000.0]), - ) + maximum_rate_max_not_existing = compressor_train_multiple_streams_two_streams.get_max_standard_rate() np.testing.assert_allclose(maximum_rate_max_not_existing, 0) -def test_zero_rate_zero_pressure_multiple_streams( +def test_zero_rate_multiple_streams( variable_speed_compressor_train_multiple_streams_and_pressures, fluid_model_medium, two_streams ): - """We want to get a result object when rate is zero regardless of invalid/zero pressures. To ensure - this we set pressure -> 1 when both rate and pressure is zero. This may happen when pressure is a function - of rate. - """ + """We want to get a result object when rate is zero regardless of pressures.""" fluid_streams = two_streams fluid_streams[1].fluid_model = fluid_model_medium fluid_streams[1].is_inlet_stream = True @@ -416,8 +421,8 @@ def test_zero_rate_zero_pressure_multiple_streams( compressor_train.set_evaluation_input( fluid_factory=[fluid_factory, fluid_factory], rate=np.array([[0, 1, 0, 1], [0, 1, 1, 0]]), - suction_pressure=np.array([0, 1, 1, 1]), - discharge_pressure=np.array([0, 5, 5, 5]), + suction_pressure=np.array([1, 1, 1, 1]), + discharge_pressure=np.array([5, 5, 5, 5]), ) result = compressor_train.evaluate() @@ -446,7 +451,6 @@ def test_different_volumes_of_ingoing_and_outgoing_streams( """Make sure that we get NOT_CALCULATED if the requested volume leaving the compressor train exceeds the volume entering the compressor train. """ - fluid_factory = fluid_factory_medium compressor_train = variable_speed_compressor_train_multiple_streams_and_pressures( nr_stages=2, fluid_streams=two_streams ) @@ -455,31 +459,32 @@ def test_different_volumes_of_ingoing_and_outgoing_streams( upstream_pressure_control=FixedSpeedPressureControl.UPSTREAM_CHOKE, ) - compressor_train.set_evaluation_input( - fluid_factory=[fluid_factory, fluid_factory], - rate=np.array([[0, 0, 100000], [0, 107000, 107000]]), - suction_pressure=np.array([1, 1, 1]), - intermediate_pressure=np.array([2, 2, 2]), - discharge_pressure=np.array([3, 3, 3]), - ) - result = compressor_train.evaluate() - - assert result.stage_results[0].chart_area_flags[0] == ChartAreaFlag.NOT_CALCULATED - assert result.stage_results[0].chart_area_flags[1] == ChartAreaFlag.NOT_CALCULATED - assert result.stage_results[0].chart_area_flags[2] == ChartAreaFlag.NOT_CALCULATED - - compressor_train.set_evaluation_input( - fluid_factory=[fluid_factory, fluid_factory], - rate=np.array([[0, 0, 100000], [0, 107000, 107000]]), - suction_pressure=np.array([1, 1, 1]), - intermediate_pressure=np.array([2, 2, 2]), - discharge_pressure=np.array([3, 3, 3]), - ) - result = compressor_train.evaluate() - - assert result.stage_results[0].chart_area_flags[0] == ChartAreaFlag.NOT_CALCULATED - assert result.stage_results[0].chart_area_flags[1] == ChartAreaFlag.NOT_CALCULATED - assert result.stage_results[0].chart_area_flags[2] == ChartAreaFlag.NOT_CALCULATED + rates_in = [0, 100000] + rates_out = [107000, 107000] + + boundary_conditions = { + "interstage_pressure": 2, + "discharge_pressure": 3, + } + for rate_in, rate_out in zip(rates_in, rates_out): + streams = [ + fluid_factory_medium.create_stream_from_standard_rate( + pressure_bara=1, + temperature_kelvin=compressor_train.stages[0].inlet_temperature_kelvin, + standard_rate_m3_per_day=rate_in, + ), + rate_out, + ] + with pytest.raises(DomainValidationException) as exec_info: + compressor_train.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, + ) + + assert ( + str(exec_info.value) + == "You can not remove more fluid from the compressor train than what is being fed into it." + ) def test_evaluate_variable_speed_compressor_train_multiple_streams_and_pressures_with_interstage_pressure( @@ -506,11 +511,11 @@ def test_evaluate_variable_speed_compressor_train_multiple_streams_and_pressures ) compressor_train.set_evaluation_input( - fluid_factory=[fluid_factory, fluid_factory], - rate=np.array([[1000000, 1200000, 1300000], [0, 107000, 107000]]), - suction_pressure=np.array([10, 10, 10]), - intermediate_pressure=np.array([30, 30, 30]), - discharge_pressure=np.array([90, 90, 90]), + fluid_factory=[fluid_factory, None], + rate=np.array([[1000000, 1200000, 1300000], [0, 107000, 107000]], dtype=float), + suction_pressure=np.array([10, 10, 10], dtype=float), + intermediate_pressure=np.array([30, 30, 30], dtype=float), + discharge_pressure=np.array([90, 90, 90], dtype=float), ) result = compressor_train.evaluate() @@ -628,7 +633,7 @@ def test_recirculate_mixing_streams_with_zero_mass_rate( fluid_streams=fluid_streams, ) compressor_train.set_evaluation_input( - fluid_factory=[fluid_factory_rich, fluid_factory_rich, fluid_factory_dry], + fluid_factory=[fluid_factory_rich, None, fluid_factory_dry], rate=np.asarray( [ [3000000, 3000000, 3000000, 3000000, 3000000, 3000000], diff --git a/tests/libecalc/core/models/compressor_modelling/test_compressor_with_turbine.py b/tests/libecalc/core/models/compressor_modelling/test_compressor_with_turbine.py index a1d9bcd2e1..b632eb8acf 100644 --- a/tests/libecalc/core/models/compressor_modelling/test_compressor_with_turbine.py +++ b/tests/libecalc/core/models/compressor_modelling/test_compressor_with_turbine.py @@ -49,29 +49,21 @@ def test_turbine_max_rate(turbine_factory, single_speed_compressor_train_unisim_ fluid_factory = NeqSimFluidFactory(FluidModel(composition=FluidComposition(methane=1.0), eos_model=EoSModel.SRK)) compressor_with_turbine_model = CompressorWithTurbineModel( turbine_model=turbine_factory( - loads=[1, 10], efficiency_fractions=[0.5, 0.5] + loads=[1, 12], efficiency_fractions=[0.5, 0.5] ), # Max power 10, make sure above power from compressor model compressor_energy_function=single_speed_compressor_train_unisim_methane, energy_usage_adjustment_constant=0, energy_usage_adjustment_factor=1, ) - rate = np.asarray([[1, 1, 1, 1, 1]]) - suction_pressure = np.asarray([40, 40, 40, 35, 60]) - discharge_pressure = np.asarray([200, 200, 200, 200, 200]) + rate = np.asarray([1, 1, 1], dtype=float) + suction_pressure = np.asarray([40, 35, 60], dtype=float) + discharge_pressure = np.asarray([90, 80, 90], dtype=float) compressor_with_turbine_model.set_evaluation_input( fluid_factory=fluid_factory, rate=rate, suction_pressure=suction_pressure, discharge_pressure=discharge_pressure, ) - max_rate = compressor_with_turbine_model.get_max_standard_rate( - suction_pressures=suction_pressure, - discharge_pressures=discharge_pressure, - ) + max_rate = compressor_with_turbine_model.get_max_standard_rate() - assert ( - max_rate.tolist() - == single_speed_compressor_train_unisim_methane.get_max_standard_rate( - suction_pressures=suction_pressure, discharge_pressures=discharge_pressure, fluid_factory=fluid_factory - ).tolist() - ) + assert max_rate.tolist() == single_speed_compressor_train_unisim_methane.get_max_standard_rate().tolist() diff --git a/tests/libecalc/core/models/compressor_modelling/test_simplified_compressor_train.py b/tests/libecalc/core/models/compressor_modelling/test_simplified_compressor_train.py index d9009476ae..03b51a32f2 100644 --- a/tests/libecalc/core/models/compressor_modelling/test_simplified_compressor_train.py +++ b/tests/libecalc/core/models/compressor_modelling/test_simplified_compressor_train.py @@ -97,9 +97,9 @@ def test_simplified_compressor_train_known_stage_and_maximum_power( ) compressor_train.set_evaluation_input( fluid_factory=fluid_factory_medium, - rate=np.asarray([8200000, 8400000]), - suction_pressure=np.asarray([25, 25]), - discharge_pressure=np.asarray([70, 70]), + rate=np.asarray([8200000, 8400000], dtype=float), + suction_pressure=np.asarray([25, 25], dtype=float), + discharge_pressure=np.asarray([70, 70], dtype=float), ) result = compressor_train.evaluate() assert result.failure_status == [ @@ -214,10 +214,7 @@ def test_compressor_train_simplified_known_stages_generic_chart( assert len(results.stage_results) == 2 - maximum_rates = simple_compressor_train_model.get_max_standard_rate( - suction_pressures=suction_pressures, - discharge_pressures=discharge_pressures, - ) + maximum_rates = simple_compressor_train_model.get_max_standard_rate() np.testing.assert_allclose( maximum_rates.tolist(), @@ -390,13 +387,18 @@ def test_evaluate_compressor_simplified_valid_points( compressor_results = [] for rate, suction_pressure, discharge_pressure in zip(rates, suction_pressures, discharge_pressures): + streams = [ + fluid_factory_medium.create_stream_from_standard_rate( + pressure_bara=suction_pressure, + temperature_kelvin=313.15, + standard_rate_m3_per_day=rate, + ) + ] + boundary_conditions = {"discharge_pressure": discharge_pressure} compressor_results.append( - compressor_train.evaluate_given_constraints( - constraints=CompressorTrainEvaluationInput( - discharge_pressure=discharge_pressure, - rate=rate, - suction_pressure=suction_pressure, - ) + compressor_train.evaluate_single_timestep( + streams=streams, + boundary_conditions=boundary_conditions, ) ) diff --git a/tests/libecalc/integration/snapshots/test_all_energy_usage_models/test_all_results/all_energy_usage_models_v3.json b/tests/libecalc/integration/snapshots/test_all_energy_usage_models/test_all_results/all_energy_usage_models_v3.json index 26e1a81a7f..fb02d85805 100644 --- a/tests/libecalc/integration/snapshots/test_all_energy_usage_models/test_all_results/all_energy_usage_models_v3.json +++ b/tests/libecalc/integration/snapshots/test_all_energy_usage_models/test_all_results/all_energy_usage_models_v3.json @@ -363,9 +363,9 @@ ], "unit": "MW", "values": [ - 299.948349, - 372.213936, - 348.935468, + 299.947854, + 372.213441, + 348.934973, 5.41469057 ] }, @@ -392,10 +392,10 @@ }, "unit": "GWh", "values": [ - 2627.54754, - 5888.14161, - 8944.81631, - 8944.81631 + 2627.5432, + 5888.13294, + 8944.80331, + 8944.80331 ] }, "power_electrical": { @@ -428,9 +428,9 @@ ], "unit": "MW", "values": [ - 267.722533, - 272.882547, - 274.048176, + 267.722038, + 272.882052, + 274.047681, 5.41469057 ] }, @@ -457,10 +457,10 @@ }, "unit": "GWh", "values": [ - 2345.24939, - 4735.7005, - 7136.36253, - 7136.36253 + 2345.24505, + 4735.69183, + 7136.34952, + 7136.34952 ] }, "power_mechanical": { @@ -46873,9 +46873,9 @@ ], "unit": "MW", "values": [ - 66.9645534, - 66.9645534, - 66.9645534, + 66.9640832, + 66.9640832, + 66.9640832, 0.0 ] }, @@ -46902,10 +46902,10 @@ }, "unit": "MWd", "values": [ - 24442.062, - 48884.124, - 73326.186, - 73326.186 + 24441.8904, + 48883.7808, + 73325.6711, + 73325.6711 ] }, "failure_status": [ @@ -46967,9 +46967,9 @@ }, "unit": "Am3/h", "values": [ - 5589.79142, - 5589.79142, - 5589.79142, + 5589.70003, + 5589.70003, + 5589.70003, NaN ] }, @@ -47331,9 +47331,9 @@ }, "unit": "Am3/h", "values": [ - 45.1053919, - 45.1053919, - 45.1053919, + 45.1065289, + 45.1065289, + 45.1065289, NaN ] }, @@ -47360,9 +47360,9 @@ }, "unit": "kg/m3", "values": [ - 316.29885, - 316.29885, - 316.29885, + 316.299649, + 316.299649, + 316.299649, NaN ] }, @@ -47389,9 +47389,9 @@ }, "unit": "N/A", "values": [ - 1.15506129, - 1.15506129, - 1.15506129, + 1.15506138, + 1.15506138, + 1.15506138, NaN ] }, @@ -47511,9 +47511,9 @@ ], "unit": "Sm3/d", "values": [ - 390000.0, - 390000.0, - 390000.0, + 390010.816, + 390010.816, + 390010.816, NaN ] }, @@ -47540,9 +47540,9 @@ }, "unit": "K", "values": [ - 363.679193, - 363.679193, - 363.679193, + 363.67817, + 363.67817, + 363.67817, NaN ] }, @@ -47569,9 +47569,9 @@ }, "unit": "N/A", "values": [ - 1.32530245, - 1.32530245, - 1.32530245, + 1.3253029, + 1.3253029, + 1.3253029, NaN ] } @@ -47627,9 +47627,9 @@ ], "unit": "MW", "values": [ - 66.9645534, - 66.9645534, - 66.9645534, + 66.9640832, + 66.9640832, + 66.9640832, 0.0 ] }, @@ -47656,10 +47656,10 @@ }, "unit": "GWh", "values": [ - 586.609488, - 1173.21898, - 1759.82846, - 1759.82846 + 586.605369, + 1173.21074, + 1759.81611, + 1759.81611 ] }, "rate": { @@ -48009,9 +48009,9 @@ ], "unit": "MW", "values": [ - 1.18467968, - 1.18467968, - 1.18467968, + 1.18467656, + 1.18467656, + 1.18467656, 0.0 ] }, @@ -48070,9 +48070,9 @@ }, "unit": "Am3/h", "values": [ - 5589.79142, - 5589.79142, - 5589.79142, + 5589.70003, + 5589.70003, + 5589.70003, NaN ] }, @@ -48099,9 +48099,9 @@ }, "unit": "Am3/h", "values": [ - 5589.79142, - 5589.79142, - 5589.79142, + 5589.70003, + 5589.70003, + 5589.70003, NaN ] }, @@ -48128,9 +48128,9 @@ }, "unit": "kg/m3", "values": [ - 6.02205953, - 6.02205953, - 6.02205953, + 6.022158, + 6.022158, + 6.022158, NaN ] }, @@ -48157,9 +48157,9 @@ }, "unit": "N/A", "values": [ - 1.23723208, - 1.23723208, - 1.23723208, + 1.23723196, + 1.23723196, + 1.23723196, NaN ] }, @@ -48207,9 +48207,9 @@ }, "unit": "bara", "values": [ - 6.9509867, - 6.9509867, - 6.9509867, + 6.95109801, + 6.95109801, + 6.95109801, NaN ] }, @@ -48337,9 +48337,9 @@ }, "unit": "N/A", "values": [ - 0.97979963, - 0.97979963, - 0.97979963, + 0.97979931, + 0.97979931, + 0.97979931, NaN ] } @@ -48456,9 +48456,9 @@ }, "unit": "Am3/h", "values": [ - 3343.1549, - 3343.1549, - 3343.1549, + 3343.09156, + 3343.09156, + 3343.09156, NaN ] }, @@ -48485,9 +48485,9 @@ }, "unit": "Am3/h", "values": [ - 3343.1549, - 3343.1549, - 3343.1549, + 3343.09156, + 3343.09156, + 3343.09156, NaN ] }, @@ -48514,9 +48514,9 @@ }, "unit": "kg/m3", "values": [ - 10.0689492, - 10.0689492, - 10.0689492, + 10.0691399, + 10.0691399, + 10.0691399, NaN ] }, @@ -48543,9 +48543,9 @@ }, "unit": "N/A", "values": [ - 1.20925661, - 1.20925661, - 1.20925661, + 1.20925651, + 1.20925651, + 1.20925651, NaN ] }, @@ -48593,9 +48593,9 @@ }, "unit": "bara", "values": [ - 14.036128, - 14.036128, - 14.036128, + 14.0363842, + 14.0363842, + 14.0363842, NaN ] }, @@ -48694,9 +48694,9 @@ }, "unit": "K", "values": [ - 365.872244, - 365.872244, - 365.872244, + 365.872133, + 365.872133, + 365.872133, NaN ] }, @@ -48723,9 +48723,9 @@ }, "unit": "N/A", "values": [ - 0.98072901, - 0.98072901, - 0.98072901, + 0.98072864, + 0.98072864, + 0.98072864, NaN ] } @@ -48773,9 +48773,9 @@ }, "unit": "frac", "values": [ - 0.702986, - 0.702986, - 0.702986, + 0.70298969, + 0.70298969, + 0.70298969, 1.0 ] }, @@ -48802,9 +48802,9 @@ }, "unit": "kJ/kg", "values": [ - 126.695968, - 126.695968, - 126.695968, + 126.695634, + 126.695634, + 126.695634, 0.0 ] }, @@ -48831,9 +48831,9 @@ }, "unit": "kJ/kg", "values": [ - 126.695968, - 126.695968, - 126.695968, + 126.695634, + 126.695634, + 126.695634, 0.0 ] }, @@ -48860,9 +48860,9 @@ }, "unit": "kJ/kg", "values": [ - 89.0654919, - 89.0654919, - 89.0654919, + 89.0657237, + 89.0657237, + 89.0657237, 0.0 ] }, @@ -48896,9 +48896,9 @@ ], "unit": "MW", "values": [ - 1.18467968, - 1.18467968, - 1.18467968, + 1.18467656, + 1.18467656, + 1.18467656, 0.0 ] }, @@ -49013,9 +49013,9 @@ }, "unit": "RPM", "values": [ - 9359.49918, - 9359.49918, - 9359.49918, + 9359.43594, + 9359.43594, + 9359.43594, NaN ] } @@ -49272,9 +49272,9 @@ ], "unit": "MW", "values": [ - 2.20431648, - 2.20431648, - 2.20431648, + 2.20429956, + 2.20429956, + 2.20429956, 0.0 ] }, @@ -49333,9 +49333,9 @@ }, "unit": "Am3/h", "values": [ - 4243.53118, - 4243.53118, - 4243.53118, + 4243.45064, + 4243.45064, + 4243.45064, NaN ] }, @@ -49362,9 +49362,9 @@ }, "unit": "Am3/h", "values": [ - 4243.53118, - 4243.53118, - 4243.53118, + 4243.45064, + 4243.45064, + 4243.45064, NaN ] }, @@ -49391,9 +49391,9 @@ }, "unit": "kg/m3", "values": [ - 11.9826327, - 11.9826327, - 11.9826327, + 11.9828602, + 11.9828602, + 11.9828602, NaN ] }, @@ -49420,9 +49420,9 @@ }, "unit": "N/A", "values": [ - 1.23685849, - 1.23685849, - 1.23685849, + 1.23685821, + 1.23685821, + 1.23685821, NaN ] }, @@ -49470,9 +49470,9 @@ }, "unit": "bara", "values": [ - 14.036128, - 14.036128, - 14.036128, + 14.0363842, + 14.0363842, + 14.0363842, NaN ] }, @@ -49600,9 +49600,9 @@ }, "unit": "N/A", "values": [ - 0.96231523, - 0.96231523, - 0.96231523, + 0.96231454, + 0.96231454, + 0.96231454, NaN ] } @@ -49719,9 +49719,9 @@ }, "unit": "Am3/h", "values": [ - 2204.17239, - 2204.17239, - 2204.17239, + 2204.14465, + 2204.14465, + 2204.14465, NaN ] }, @@ -49748,9 +49748,9 @@ }, "unit": "Am3/h", "values": [ - 2204.17239, - 2204.17239, - 2204.17239, + 2204.14465, + 2204.14465, + 2204.14465, NaN ] }, @@ -49777,9 +49777,9 @@ }, "unit": "kg/m3", "values": [ - 23.0692825, - 23.0692825, - 23.0692825, + 23.0695729, + 23.0695729, + 23.0695729, NaN ] }, @@ -49806,9 +49806,9 @@ }, "unit": "N/A", "values": [ - 1.20191251, - 1.20191251, - 1.20191251, + 1.20191248, + 1.20191248, + 1.20191248, NaN ] }, @@ -49856,9 +49856,9 @@ }, "unit": "bara", "values": [ - 34.1112684, - 34.1112684, - 34.1112684, + 34.1116318, + 34.1116318, + 34.1116318, NaN ] }, @@ -49957,9 +49957,9 @@ }, "unit": "K", "values": [ - 381.211959, - 381.211959, - 381.211959, + 381.211444, + 381.211444, + 381.211444, NaN ] }, @@ -49986,9 +49986,9 @@ }, "unit": "N/A", "values": [ - 0.96671168, - 0.96671168, - 0.96671168, + 0.96671113, + 0.96671113, + 0.96671113, NaN ] } @@ -50036,9 +50036,9 @@ }, "unit": "frac", "values": [ - 0.74559913, - 0.74559913, - 0.74559913, + 0.74559742, + 0.74559742, + 0.74559742, 1.0 ] }, @@ -50065,9 +50065,9 @@ }, "unit": "kJ/kg", "values": [ - 156.061869, - 156.061869, - 156.061869, + 156.060671, + 156.060671, + 156.060671, 0.0 ] }, @@ -50094,9 +50094,9 @@ }, "unit": "kJ/kg", "values": [ - 156.061869, - 156.061869, - 156.061869, + 156.060671, + 156.060671, + 156.060671, 0.0 ] }, @@ -50123,9 +50123,9 @@ }, "unit": "kJ/kg", "values": [ - 116.359593, - 116.359593, - 116.359593, + 116.358434, + 116.358434, + 116.358434, 0.0 ] }, @@ -50159,9 +50159,9 @@ ], "unit": "MW", "values": [ - 2.20431648, - 2.20431648, - 2.20431648, + 2.20429956, + 2.20429956, + 2.20429956, 0.0 ] }, @@ -50276,9 +50276,9 @@ }, "unit": "RPM", "values": [ - 9359.49918, - 9359.49918, - 9359.49918, + 9359.43594, + 9359.43594, + 9359.43594, NaN ] } @@ -50314,9 +50314,9 @@ ], "unit": "MW", "values": [ - 2.70484167, - 2.70484167, - 2.70484167, + 2.7048313, + 2.7048313, + 2.7048313, NaN ] }, @@ -50535,9 +50535,9 @@ ], "unit": "MW", "values": [ - 5.09081496, - 5.09081496, - 5.09081496, + 5.09077249, + 5.09077249, + 5.09077249, 0.0 ] }, @@ -50596,9 +50596,9 @@ }, "unit": "Am3/h", "values": [ - 1647.72737, - 1647.72737, - 1647.72737, + 1647.70807, + 1647.70807, + 1647.70807, NaN ] }, @@ -50625,9 +50625,9 @@ }, "unit": "Am3/h", "values": [ - 3515.66179, - 3515.66179, - 3515.66179, + 3515.63858, + 3515.63858, + 3515.63858, NaN ] }, @@ -50654,9 +50654,9 @@ }, "unit": "kg/m3", "values": [ - 30.8598841, - 30.8598841, - 30.8598841, + 30.8602456, + 30.8602456, + 30.8602456, NaN ] }, @@ -50683,9 +50683,9 @@ }, "unit": "N/A", "values": [ - 1.21419, - 1.21419, - 1.21419, + 1.21418957, + 1.21418957, + 1.21418957, NaN ] }, @@ -50733,9 +50733,9 @@ }, "unit": "bara", "values": [ - 34.1112684, - 34.1112684, - 34.1112684, + 34.1116318, + 34.1116318, + 34.1116318, NaN ] }, @@ -50805,9 +50805,9 @@ ], "unit": "Sm3/d", "values": [ - 2965786.7, - 2965786.7, - 2965786.7, + 2965801.86, + 2965801.86, + 2965801.86, NaN ] }, @@ -50863,9 +50863,9 @@ }, "unit": "N/A", "values": [ - 0.9092228, - 0.9092228, - 0.9092228, + 0.90922186, + 0.90922186, + 0.90922186, NaN ] } @@ -50951,9 +50951,9 @@ }, "unit": "kg/h", "values": [ - 108492.915, - 108492.915, - 108492.915, + 108493.47, + 108493.47, + 108493.47, NaN ] }, @@ -50982,9 +50982,9 @@ }, "unit": "Am3/h", "values": [ - 833.031388, - 833.031388, - 833.031388, + 833.028324, + 833.028324, + 833.028324, NaN ] }, @@ -51011,9 +51011,9 @@ }, "unit": "Am3/h", "values": [ - 1777.3915, - 1777.3915, - 1777.3915, + 1777.39405, + 1777.39405, + 1777.39405, NaN ] }, @@ -51040,9 +51040,9 @@ }, "unit": "kg/m3", "values": [ - 61.0405278, - 61.0405278, - 61.0405278, + 61.0407523, + 61.0407523, + 61.0407523, NaN ] }, @@ -51069,9 +51069,9 @@ }, "unit": "N/A", "values": [ - 1.1786613, - 1.1786613, - 1.1786613, + 1.17866135, + 1.17866135, + 1.17866135, NaN ] }, @@ -51119,9 +51119,9 @@ }, "unit": "bara", "values": [ - 89.9999999, - 89.9999999, - 89.9999999, + 89.9999998, + 89.9999998, + 89.9999998, NaN ] }, @@ -51191,9 +51191,9 @@ ], "unit": "Sm3/d", "values": [ - 2965786.7, - 2965786.7, - 2965786.7, + 2965801.86, + 2965801.86, + 2965801.86, NaN ] }, @@ -51220,9 +51220,9 @@ }, "unit": "K", "values": [ - 392.043846, - 392.043846, - 392.043846, + 392.042813, + 392.042813, + 392.042813, NaN ] }, @@ -51249,9 +51249,9 @@ }, "unit": "N/A", "values": [ - 0.93968641, - 0.93968641, - 0.93968641, + 0.93968544, + 0.93968544, + 0.93968544, NaN ] } @@ -51299,9 +51299,9 @@ }, "unit": "frac", "values": [ - 0.72352698, - 0.72352698, - 0.72352698, + 0.72352704, + 0.72352704, + 0.72352704, 1.0 ] }, @@ -51328,9 +51328,9 @@ }, "unit": "kJ/kg", "values": [ - 168.922863, - 168.922863, - 168.922863, + 168.92059, + 168.92059, + 168.92059, 0.0 ] }, @@ -51357,9 +51357,9 @@ }, "unit": "kJ/kg", "values": [ - 168.922863, - 168.922863, - 168.922863, + 168.92059, + 168.92059, + 168.92059, 0.0 ] }, @@ -51386,9 +51386,9 @@ }, "unit": "kJ/kg", "values": [ - 122.220249, - 122.220249, - 122.220249, + 122.218615, + 122.218615, + 122.218615, 0.0 ] }, @@ -51422,9 +51422,9 @@ ], "unit": "MW", "values": [ - 5.09081496, - 5.09081496, - 5.09081496, + 5.09077249, + 5.09077249, + 5.09077249, 0.0 ] }, @@ -51539,9 +51539,9 @@ }, "unit": "RPM", "values": [ - 9359.49918, - 9359.49918, - 9359.49918, + 9359.43594, + 9359.43594, + 9359.43594, NaN ] } @@ -51577,9 +51577,9 @@ ], "unit": "MW", "values": [ - 14.9938623, - 14.9938623, - 14.9938623, + 14.993972, + 14.993972, + 14.993972, NaN ] }, @@ -51798,9 +51798,9 @@ ], "unit": "MW", "values": [ - 15.6633028, - 15.6633028, - 15.6633028, + 15.663422, + 15.663422, + 15.663422, 0.0 ] }, @@ -51859,9 +51859,9 @@ }, "unit": "Am3/h", "values": [ - 150.257359, - 150.257359, - 150.257359, + 150.257368, + 150.257368, + 150.257368, NaN ] }, @@ -51888,9 +51888,9 @@ }, "unit": "Am3/h", "values": [ - 3515.66179, - 3515.66179, - 3515.66179, + 3515.63858, + 3515.63858, + 3515.63858, NaN ] }, @@ -51917,9 +51917,9 @@ }, "unit": "kg/m3", "values": [ - 94.9489841, - 94.9489841, - 94.9489841, + 94.9516114, + 94.9516114, + 94.9516114, NaN ] }, @@ -51946,9 +51946,9 @@ }, "unit": "N/A", "values": [ - 1.15372456, - 1.15372456, - 1.15372456, + 1.1537228, + 1.1537228, + 1.1537228, NaN ] }, @@ -51996,9 +51996,9 @@ }, "unit": "bara", "values": [ - 90.0, - 90.0, - 90.0, + 90.0020705, + 90.0020705, + 90.0020705, NaN ] }, @@ -52032,9 +52032,9 @@ ], "unit": "Sm3/d", "values": [ - 390000.0, - 390000.0, - 390000.0, + 390010.816, + 390010.816, + 390010.816, NaN ] }, @@ -52068,9 +52068,9 @@ ], "unit": "Sm3/d", "values": [ - 9125064.54, - 9125064.54, - 9125064.54, + 9125256.8, + 9125256.8, + 9125256.8, NaN ] }, @@ -52126,9 +52126,9 @@ }, "unit": "N/A", "values": [ - 0.78300095, - 0.78300095, - 0.78300095, + 0.78299743, + 0.78299743, + 0.78299743, NaN ] } @@ -52185,9 +52185,9 @@ }, "unit": "kg/h", "values": [ - 14266.7836, - 14266.7836, - 14266.7836, + 14267.1792, + 14267.1792, + 14267.1792, NaN ] }, @@ -52214,9 +52214,9 @@ }, "unit": "kg/h", "values": [ - 333808.515, - 333808.515, - 333808.515, + 333815.548, + 333815.548, + 333815.548, NaN ] }, @@ -52245,9 +52245,9 @@ }, "unit": "Am3/h", "values": [ - 85.4345624, - 85.4345624, - 85.4345624, + 85.4356066, + 85.4356066, + 85.4356066, NaN ] }, @@ -52274,9 +52274,9 @@ }, "unit": "Am3/h", "values": [ - 1998.96384, - 1998.96384, - 1998.96384, + 1998.97494, + 1998.97494, + 1998.97494, NaN ] }, @@ -52303,9 +52303,9 @@ }, "unit": "kg/m3", "values": [ - 166.990773, - 166.990773, - 166.990773, + 166.993363, + 166.993363, + 166.993363, NaN ] }, @@ -52332,9 +52332,9 @@ }, "unit": "N/A", "values": [ - 1.1512064, - 1.1512064, - 1.1512064, + 1.15120612, + 1.15120612, + 1.15120612, NaN ] }, @@ -52382,9 +52382,9 @@ }, "unit": "bara", "values": [ - 253.896718, - 253.896718, - 253.896718, + 253.899895, + 253.899895, + 253.899895, NaN ] }, @@ -52418,9 +52418,9 @@ ], "unit": "Sm3/d", "values": [ - 390000.0, - 390000.0, - 390000.0, + 390010.816, + 390010.816, + 390010.816, NaN ] }, @@ -52454,9 +52454,9 @@ ], "unit": "Sm3/d", "values": [ - 9125064.54, - 9125064.54, - 9125064.54, + 9125256.8, + 9125256.8, + 9125256.8, NaN ] }, @@ -52483,9 +52483,9 @@ }, "unit": "K", "values": [ - 392.724472, - 392.724472, - 392.724472, + 392.723137, + 392.723137, + 392.723137, NaN ] }, @@ -52512,9 +52512,9 @@ }, "unit": "N/A", "values": [ - 0.97410803, - 0.97410803, - 0.97410803, + 0.97410858, + 0.97410858, + 0.97410858, NaN ] } @@ -52562,9 +52562,9 @@ }, "unit": "frac", "values": [ - 0.72352698, - 0.72352698, - 0.72352698, + 0.72352704, + 0.72352704, + 0.72352704, 1.0 ] }, @@ -52591,9 +52591,9 @@ }, "unit": "kJ/kg", "values": [ - 168.922863, - 168.922863, - 168.922863, + 168.92059, + 168.92059, + 168.92059, 0.0 ] }, @@ -52620,9 +52620,9 @@ }, "unit": "kJ/kg", "values": [ - 168.922863, - 168.922863, - 168.922863, + 168.92059, + 168.92059, + 168.92059, 0.0 ] }, @@ -52649,9 +52649,9 @@ }, "unit": "kJ/kg", "values": [ - 122.220249, - 122.220249, - 122.220249, + 122.218615, + 122.218615, + 122.218615, 0.0 ] }, @@ -52685,9 +52685,9 @@ ], "unit": "MW", "values": [ - 15.6633028, - 15.6633028, - 15.6633028, + 15.663422, + 15.663422, + 15.663422, 0.0 ] }, @@ -52802,9 +52802,9 @@ }, "unit": "RPM", "values": [ - 9359.49918, - 9359.49918, - 9359.49918, + 9359.43594, + 9359.43594, + 9359.43594, NaN ] } @@ -52840,9 +52840,9 @@ ], "unit": "MW", "values": [ - 41.151999, - 41.151999, - 41.151999, + 41.1514625, + 41.1514625, + 41.1514625, NaN ] }, @@ -53061,9 +53061,9 @@ ], "unit": "MW", "values": [ - 41.8214395, - 41.8214395, - 41.8214395, + 41.8209126, + 41.8209126, + 41.8209126, 0.0 ] }, @@ -53122,9 +53122,9 @@ }, "unit": "Am3/h", "values": [ - 56.2755979, - 56.2755979, - 56.2755979, + 56.2767389, + 56.2767389, + 56.2767389, NaN ] }, @@ -53151,9 +53151,9 @@ }, "unit": "Am3/h", "values": [ - 3515.66179, - 3515.66179, - 3515.66179, + 3515.63858, + 3515.63858, + 3515.63858, NaN ] }, @@ -53180,9 +53180,9 @@ }, "unit": "kg/m3", "values": [ - 253.51634, - 253.51634, - 253.51634, + 253.51823, + 253.51823, + 253.51823, NaN ] }, @@ -53209,9 +53209,9 @@ }, "unit": "N/A", "values": [ - 1.13656523, - 1.13656523, - 1.13656523, + 1.1365656, + 1.1365656, + 1.1365656, NaN ] }, @@ -53259,9 +53259,9 @@ }, "unit": "bara", "values": [ - 253.896718, - 253.896718, - 253.896718, + 253.899895, + 253.899895, + 253.899895, NaN ] }, @@ -53295,9 +53295,9 @@ ], "unit": "Sm3/d", "values": [ - 390000.0, - 390000.0, - 390000.0, + 390010.816, + 390010.816, + 390010.816, NaN ] }, @@ -53331,9 +53331,9 @@ ], "unit": "Sm3/d", "values": [ - 24364167.6, - 24364167.6, - 24364167.6, + 24364188.5, + 24364188.5, + 24364188.5, NaN ] }, @@ -53389,9 +53389,9 @@ }, "unit": "N/A", "values": [ - 0.83596611, - 0.83596611, - 0.83596611, + 0.83597044, + 0.83597044, + 0.83597044, NaN ] } @@ -53448,9 +53448,9 @@ }, "unit": "kg/h", "values": [ - 14266.7836, - 14266.7836, - 14266.7836, + 14267.1792, + 14267.1792, + 14267.1792, NaN ] }, @@ -53477,9 +53477,9 @@ }, "unit": "kg/h", "values": [ - 891277.708, - 891277.708, - 891277.708, + 891278.472, + 891278.472, + 891278.472, NaN ] }, @@ -53508,9 +53508,9 @@ }, "unit": "Am3/h", "values": [ - 45.1053919, - 45.1053919, - 45.1053919, + 45.1065289, + 45.1065289, + 45.1065289, NaN ] }, @@ -53537,9 +53537,9 @@ }, "unit": "Am3/h", "values": [ - 2817.83417, - 2817.83417, - 2817.83417, + 2817.82947, + 2817.82947, + 2817.82947, NaN ] }, @@ -53566,9 +53566,9 @@ }, "unit": "kg/m3", "values": [ - 316.29885, - 316.29885, - 316.29885, + 316.299649, + 316.299649, + 316.299649, NaN ] }, @@ -53595,9 +53595,9 @@ }, "unit": "N/A", "values": [ - 1.15506129, - 1.15506129, - 1.15506129, + 1.15506138, + 1.15506138, + 1.15506138, NaN ] }, @@ -53681,9 +53681,9 @@ ], "unit": "Sm3/d", "values": [ - 390000.0, - 390000.0, - 390000.0, + 390010.816, + 390010.816, + 390010.816, NaN ] }, @@ -53717,9 +53717,9 @@ ], "unit": "Sm3/d", "values": [ - 24364167.6, - 24364167.6, - 24364167.6, + 24364188.5, + 24364188.5, + 24364188.5, NaN ] }, @@ -53746,9 +53746,9 @@ }, "unit": "K", "values": [ - 363.679193, - 363.679193, - 363.679193, + 363.67817, + 363.67817, + 363.67817, NaN ] }, @@ -53775,9 +53775,9 @@ }, "unit": "N/A", "values": [ - 1.32530245, - 1.32530245, - 1.32530245, + 1.3253029, + 1.3253029, + 1.3253029, NaN ] } @@ -53825,9 +53825,9 @@ }, "unit": "frac", "values": [ - 0.72352698, - 0.72352698, - 0.72352698, + 0.72352704, + 0.72352704, + 0.72352704, 1.0 ] }, @@ -53854,9 +53854,9 @@ }, "unit": "kJ/kg", "values": [ - 168.922863, - 168.922863, - 168.922863, + 168.92059, + 168.92059, + 168.92059, 0.0 ] }, @@ -53883,9 +53883,9 @@ }, "unit": "kJ/kg", "values": [ - 168.922863, - 168.922863, - 168.922863, + 168.92059, + 168.92059, + 168.92059, 0.0 ] }, @@ -53912,9 +53912,9 @@ }, "unit": "kJ/kg", "values": [ - 122.220249, - 122.220249, - 122.220249, + 122.218615, + 122.218615, + 122.218615, 0.0 ] }, @@ -53948,9 +53948,9 @@ ], "unit": "MW", "values": [ - 41.8214395, - 41.8214395, - 41.8214395, + 41.8209126, + 41.8209126, + 41.8209126, 0.0 ] }, @@ -54065,9 +54065,9 @@ }, "unit": "RPM", "values": [ - 9359.49918, - 9359.49918, - 9359.49918, + 9359.43594, + 9359.43594, + 9359.43594, NaN ] } @@ -60199,9 +60199,9 @@ ], "unit": "MW", "values": [ - 299.948349, - 372.213936, - 348.935468, + 299.947854, + 372.213441, + 348.934973, 5.41469057 ] }, @@ -60228,10 +60228,10 @@ }, "unit": "GWh", "values": [ - 2627.54754, - 5888.14161, - 8944.81631, - 8944.81631 + 2627.5432, + 5888.13294, + 8944.80331, + 8944.80331 ] }, "power_electrical": { @@ -60264,9 +60264,9 @@ ], "unit": "MW", "values": [ - 267.722533, - 272.882547, - 274.048176, + 267.722038, + 272.882052, + 274.047681, 5.41469057 ] }, @@ -60293,10 +60293,10 @@ }, "unit": "GWh", "values": [ - 2345.24939, - 4735.7005, - 7136.36253, - 7136.36253 + 2345.24505, + 4735.69183, + 7136.34952, + 7136.34952 ] }, "power_mechanical": { @@ -60634,9 +60634,9 @@ ], "unit": "MW", "values": [ - 267.722533, - 272.882547, - 274.048176, + 267.722038, + 272.882052, + 274.047681, 5.41469057 ] }, @@ -60670,9 +60670,9 @@ ], "unit": "MW", "values": [ - 732.277467, - 727.117453, - 725.951824, + 732.277962, + 727.117948, + 725.952319, 994.585309 ] }, @@ -60699,10 +60699,10 @@ }, "unit": "GWh", "values": [ - 2345.24939, - 4735.7005, - 7136.36253, - 7136.36253 + 2345.24505, + 4735.69183, + 7136.34952, + 7136.34952 ] } }, @@ -64280,9 +64280,9 @@ ], "unit": "MW", "values": [ - 70.4890036, - 70.4890036, - 70.4890036, + 70.4885087, + 70.4885087, + 70.4885087, 0.0 ] }, @@ -64309,10 +64309,10 @@ }, "unit": "MWd", "values": [ - 25728.4863, - 51456.9726, - 77185.459, - 77185.459 + 25728.3057, + 51456.6113, + 77184.917, + 77184.917 ] }, "id": "variable_speed_compressor_train_multiple_input_streams_and_interstage_pressure", @@ -64397,9 +64397,9 @@ ], "unit": "MW", "values": [ - 70.4890036, - 70.4890036, - 70.4890036, + 70.4885087, + 70.4885087, + 70.4885087, 0.0 ] }, @@ -64426,10 +64426,10 @@ }, "unit": "GWh", "values": [ - 617.483672, - 1234.96734, - 1852.45101, - 1852.45101 + 617.479336, + 1234.95867, + 1852.43801, + 1852.43801 ] }, "rate_exceeds_maximum": { @@ -64491,9 +64491,9 @@ ], "unit": "MW", "values": [ - 58.8507029, - 58.8507029, - 58.8507029, + 58.8502657, + 58.8502657, + 58.8502657, NaN ] },