diff --git a/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/fluid.py b/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/fluid.py index 36e6243cc..4b0a0852c 100644 --- a/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/fluid.py +++ b/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/fluid.py @@ -35,8 +35,6 @@ def __init__( existing_fluid: Initialize FluidStream from an existing (NeqSim) fluid. Warning: Be mindful of keeping fluid_model and existing fluid consistent. If not the fluid properties may be incorrect. """ self.fluid_model = fluid_model - self.initial_temperature_kelvin = temperature_kelvin - self.initial_pressure_bare = pressure_bara if not temperature_kelvin > 0: raise ValueError("FluidStream temperature needs to be above 0.") @@ -60,8 +58,8 @@ def __init__( self._enthalpy_joule_per_kg = _neqsim_fluid_stream.enthalpy_joule_per_kg if ( - pressure_bara == UnitConstants.STANDARD_PRESSURE_BARA - and temperature_kelvin == UnitConstants.STANDARD_TEMPERATURE_KELVIN + self._pressure_bara == UnitConstants.STANDARD_PRESSURE_BARA + and self._temperature_kelvin == UnitConstants.STANDARD_TEMPERATURE_KELVIN ): self.standard_conditions_density = _neqsim_fluid_stream.density self.molar_mass_kg_per_mol = _neqsim_fluid_stream.molar_mass @@ -72,8 +70,8 @@ def __init__( pressure_bara=UnitConstants.STANDARD_PRESSURE_BARA, eos_model=self.fluid_model.eos_model, ) - self.standard_conditions_density = _neqsim_fluid_at_standard_conditions.density + self.molar_mass_kg_per_mol = _neqsim_fluid_stream.molar_mass @property def pressure_bara(self) -> float: @@ -201,7 +199,10 @@ def set_new_pressure_and_temperature( new_temperature_kelvin=new_temperature_kelvin, remove_liquid=remove_liquid, ) - return FluidStream(existing_fluid=fluid_stream, fluid_model=self.fluid_model) + return FluidStream( + existing_fluid=fluid_stream, + fluid_model=self.fluid_model, + ) def set_new_pressure_and_enthalpy_change( self, new_pressure: float, enthalpy_change_joule_per_kg: float, remove_liquid: bool = True @@ -277,7 +278,5 @@ def mix_in_stream( return FluidStream( existing_fluid=mixed_neqsim_fluid_stream, - temperature_kelvin=temperature_kelvin, - pressure_bara=pressure_bara, fluid_model=dto.FluidModel(composition=mixed_fluid_composition, eos_model=self.fluid_model.eos_model), ) diff --git a/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/variable_speed_compressor_train_common_shaft_multiple_streams_and_pressures.py b/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/variable_speed_compressor_train_common_shaft_multiple_streams_and_pressures.py index 990c6f512..80ba50cfa 100644 --- a/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/variable_speed_compressor_train_common_shaft_multiple_streams_and_pressures.py +++ b/src/ecalc/libraries/libecalc/common/libecalc/core/models/compressor/train/variable_speed_compressor_train_common_shaft_multiple_streams_and_pressures.py @@ -91,6 +91,10 @@ def __init__( else: self.outlet_stream_connected_to_stage[stream.connected_to_stage_no].append(i) + # in rare cases we can end up with trying to mix two streams with zero mass rate, and need the fluid from the + # previous time step to recirculate. This will take care of that. + self.fluid_to_recirculate_in_stage_when_inlet_rate_is_zero = [None] * len(self.stages) + @staticmethod def _check_intermediate_pressure_stage_number_is_valid( _stage_number_intermediate_pressure: int, @@ -837,7 +841,7 @@ def calculate_compressor_train_given_rate_ps_speed( mass_rate_this_stage_kg_per_hour = mass_rate_previous_stage_kg_per_hour # take mass out before potential mixing with additional ingoing stream # assume that volume/mass is always taken out before additional volume/mass is potentially added, no matter - # what order the streams are defined in in the yaml file + # what order the streams are defined in the yaml file for stream_number in self.outlet_stream_connected_to_stage.get(stage_number): mass_rate_this_stage_kg_per_hour -= inlet_stream.standard_rate_to_mass_rate( std_rates_std_m3_per_day_per_stream[stream_number] @@ -853,15 +857,27 @@ def calculate_compressor_train_given_rate_ps_speed( mass_rate_additional_inlet_stream_kg_per_hour = additional_inlet_stream.standard_rate_to_mass_rate( float(std_rates_std_m3_per_day_per_stream[stream_number]) ) + if (mass_rate_this_stage_kg_per_hour > 0) or (mass_rate_additional_inlet_stream_kg_per_hour > 0): + inlet_stream = additional_inlet_stream.mix_in_stream( + other_fluid_stream=inlet_stream, + self_mass_rate=mass_rate_additional_inlet_stream_kg_per_hour, + other_mass_rate=mass_rate_this_stage_kg_per_hour, + temperature_kelvin=additional_inlet_stream.temperature_kelvin, + pressure_bara=additional_inlet_stream.pressure_bara, + ) + mass_rate_this_stage_kg_per_hour += mass_rate_additional_inlet_stream_kg_per_hour - inlet_stream = additional_inlet_stream.mix_in_stream( - other_fluid_stream=inlet_stream, - self_mass_rate=mass_rate_additional_inlet_stream_kg_per_hour, - other_mass_rate=mass_rate_this_stage_kg_per_hour, - temperature_kelvin=additional_inlet_stream.temperature_kelvin, - pressure_bara=additional_inlet_stream.pressure_bara, + if mass_rate_this_stage_kg_per_hour == 0: + fluid_to_recirculate = self.get_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + stage_number=stage_number + ) + if fluid_to_recirculate: + inlet_stream = fluid_to_recirculate + else: + raise ValueError( + f"Trying to recirculate fluid in stage {stage_number} without defining which " + f"composition the fluid should have." ) - mass_rate_this_stage_kg_per_hour += mass_rate_additional_inlet_stream_kg_per_hour stage_result = stage.evaluate( inlet_stream_stage=inlet_stream, @@ -879,7 +895,9 @@ def calculate_compressor_train_given_rate_ps_speed( ) mass_rate_previous_stage_kg_per_hour = mass_rate_this_stage_kg_per_hour - + self.set_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + stage_number=stage_number, fluid_stream=inlet_stream + ) return CompressorTrainResultSingleTimeStep( stage_results=stage_results, speed=speed, @@ -1122,19 +1140,27 @@ def _update_inlet_fluid_and_std_rates_for_last_subtrain( float(std_rates_std_m3_per_day_per_stream[stream_number]) ) # mix streams to get inlet stream for first compressor stage - inlet_stream = additional_inlet_stream.mix_in_stream( - other_fluid_stream=inlet_stream, - self_mass_rate=mass_rate_additional_inlet_stream, - other_mass_rate=inlet_mass_rate, - temperature_kelvin=self.stages[0].inlet_temperature_kelvin, - pressure_bara=additional_inlet_stream.pressure_bara, - ) + # if rate is 0 don't try to mix, + if (inlet_mass_rate > 0) or (mass_rate_additional_inlet_stream > 0): + inlet_stream = additional_inlet_stream.mix_in_stream( + other_fluid_stream=inlet_stream, + self_mass_rate=mass_rate_additional_inlet_stream, + other_mass_rate=inlet_mass_rate, + temperature_kelvin=additional_inlet_stream.temperature_kelvin, + pressure_bara=additional_inlet_stream.pressure_bara, + ) inlet_std_rate += std_rates_std_m3_per_day_per_stream[stream_number] inlet_mass_rate = inlet_stream.standard_rate_to_mass_rate(inlet_std_rate) # update inlet fluid for the subtrain with new composition - updated_inlet_fluid = FluidStream(fluid_model=inlet_stream.fluid_model) - + if inlet_mass_rate == 0: + fluid_to_recirculate = self.get_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero(stage_number=0) + if fluid_to_recirculate: + updated_inlet_fluid = fluid_to_recirculate + else: + raise ValueError("Trying to recirculate unknown fluid in compressor stage") + else: + updated_inlet_fluid = FluidStream(fluid_model=inlet_stream.fluid_model) updated_std_rates_std_m3_per_day_per_stream = [inlet_std_rate] updated_std_rates_std_m3_per_day_per_stream.extend( [ @@ -1284,6 +1310,18 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( + compressor_train_results_to_return_last_part.stage_results ) + for stage_number in range(len(self.stages)): + self.set_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + stage_number=stage_number, + fluid_stream=compressor_train_first_part.get_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + stage_number=stage_number + ) + if stage_number < stage_number_for_intermediate_pressure_target + else compressor_train_last_part.get_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + stage_number=stage_number - stage_number_for_intermediate_pressure_target + ), + ) + return CompressorTrainResultSingleTimeStep( speed=speed, stage_results=compressor_train_results_to_return_stage_results, @@ -1291,6 +1329,29 @@ def find_and_calculate_for_compressor_train_with_two_pressure_requirements( or compressor_train_results_to_return_last_part.failure_status, ) + def set_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + self, stage_number: int, fluid_stream: FluidStream + ) -> None: + """Keep track of what fluid passes through each compressor stage in the compressor train at a given calculation + step. This is done in case of the possibility of having a zero inlet rate at the next calculation step when + adding/subtracting ingoing/outgoing streams. + + Args: + stage_number: The stage number (zero index) + fluid_stream: The fluid stream passing through compressor stage number + """ + self.fluid_to_recirculate_in_stage_when_inlet_rate_is_zero[stage_number] = fluid_stream + + def get_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero(self, stage_number: int) -> FluidStream: + """Retrieve the fluid that passed through a given compressor stage in the compressor train at the previous + calculation step. + + Arge: + stage_number: The stage number (zero index) + + """ + return self.fluid_to_recirculate_in_stage_when_inlet_rate_is_zero[stage_number] + def split_rates_on_stage_number( compressor_train: VariableSpeedCompressorTrainCommonShaftMultipleStreamsAndPressures, @@ -1366,4 +1427,16 @@ def split_train_on_stage_number( ), ) + for stage_no in range(len(compressor_train.stages)): + if stage_no < stage_number: + compressor_train_first_part.set_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + stage_number=stage_no, + fluid_stream=compressor_train.get_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero(stage_no), + ) + else: + compressor_train_last_part.set_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero( + stage_number=stage_no - stage_number, + fluid_stream=compressor_train.get_fluid_to_recirculate_in_stage_when_inlet_rate_is_zero(stage_no), + ) + return compressor_train_first_part, compressor_train_last_part diff --git a/src/ecalc/libraries/libecalc/common/tests/core/models/compressor_modelling/test_variable_speed_compressor_train_multiple_streams.py b/src/ecalc/libraries/libecalc/common/tests/core/models/compressor_modelling/test_variable_speed_compressor_train_multiple_streams.py index 1f44a3fe1..01649ab40 100644 --- a/src/ecalc/libraries/libecalc/common/tests/core/models/compressor_modelling/test_variable_speed_compressor_train_multiple_streams.py +++ b/src/ecalc/libraries/libecalc/common/tests/core/models/compressor_modelling/test_variable_speed_compressor_train_multiple_streams.py @@ -219,6 +219,41 @@ def variable_speed_compressor_train_two_compressors_two_streams( ) +@pytest.fixture +def variable_speed_compressor_train_two_compressors_ingoning_and_outgoing_streams_between_compressors( + variable_speed_compressor_train_stage_dto, + dry_fluid, + rich_fluid, + mock_variable_speed_compressor_train_multiple_streams_and_pressures, +) -> VariableSpeedCompressorTrainCommonShaftMultipleStreamsAndPressures: + """Train with only two compressors, and standard medium fluid, on stream in per stage, no liquid off take.""" + fluid_streams = [ + FluidStreamObjectForMultipleStreams( + fluid=FluidStream(rich_fluid), + is_inlet_stream=True, + connected_to_stage_no=0, + ), + FluidStreamObjectForMultipleStreams( + is_inlet_stream=False, + connected_to_stage_no=1, + ), + FluidStreamObjectForMultipleStreams( + fluid=FluidStream(dry_fluid), + is_inlet_stream=True, + connected_to_stage_no=1, + ), + ] + return VariableSpeedCompressorTrainCommonShaftMultipleStreamsAndPressures( + streams=fluid_streams, + data_transfer_object=mock_variable_speed_compressor_train_multiple_streams_and_pressures.copy( + update={ + "stages": [variable_speed_compressor_train_stage_dto] * 2, + "pressure_control": dto.types.FixedSpeedPressureControl.DOWNSTREAM_CHOKE, + } + ), + ) + + @pytest.fixture def variable_speed_compressor_train_two_compressors_one_ingoing_and_one_outgoing_stream( variable_speed_compressor_train_stage_dto, @@ -649,3 +684,26 @@ def test_adjust_energy_usage( np.asarray(result_comparison_intermediate.energy_usage) + energy_usage_adjustment_constant, result_intermediate.energy_usage, ) + + +def test_recirculate_mixing_streams_with_zero_mass_rate( + variable_speed_compressor_train_two_compressors_ingoning_and_outgoing_streams_between_compressors, +): + result = variable_speed_compressor_train_two_compressors_ingoning_and_outgoing_streams_between_compressors.evaluate_rate_ps_pd( + rate=np.asarray( + [ + [3000000, 3000000, 3000000, 3000000, 3000000, 3000000], + [0, 3000000, 2500000, 3000000, 3000000, 3000000], + [0, 0, 500000, 0, 1000000, 0], + ] + ), + suction_pressure=np.asarray([30, 30, 30, 30, 30, 30]), + discharge_pressure=np.asarray([150, 150, 150, 150, 150, 150]), + ) + np.testing.assert_almost_equal(result.power[0], result.power[1], decimal=4) + np.testing.assert_almost_equal(result.power[2], result.power[3], decimal=4) + np.testing.assert_almost_equal(result.power[4], result.power[5], decimal=4) + assert result.recirculation_loss[0] < result.recirculation_loss[1] + assert result.recirculation_loss[2] < result.recirculation_loss[3] + assert result.recirculation_loss[4] < result.recirculation_loss[5] + assert result.power[0] < result.power[2] < result.power[4] # more and more of the heavy fluid