From 472e592774e41bfab23754738f3290ca142caebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Petter=20L=C3=B8d=C3=B8en?= Date: Fri, 22 Mar 2024 13:04:55 +0100 Subject: [PATCH] feat: add control margin to single speed compressor charts (#418) docs: update control margin keyword --- .../references/keywords/CONTROL_MARGIN.md | 44 ++++++++++++-- src/libecalc/core/models/chart/base.py | 57 ++++++++++++++++++- .../chart/single_speed_compressor_chart.py | 11 +++- .../chart/variable_speed_compressor_chart.py | 53 ++--------------- src/libecalc/core/models/compressor/utils.py | 22 ++----- .../presentation/yaml/mappers/model.py | 10 +++- .../test_compressor_chart.py | 38 ++++++++++++- 7 files changed, 160 insertions(+), 75 deletions(-) diff --git a/docs/docs/about/references/keywords/CONTROL_MARGIN.md b/docs/docs/about/references/keywords/CONTROL_MARGIN.md index 1c4e5ef66b..c817e72f0f 100644 --- a/docs/docs/about/references/keywords/CONTROL_MARGIN.md +++ b/docs/docs/about/references/keywords/CONTROL_MARGIN.md @@ -6,18 +6,52 @@ ## Description -This keyword defines the surge control margin for a variable speed compressor chart. +This keyword defines the surge control margin for a single speed compressor chart or a variable speed compressor chart. -The `CONTROL_MARGIN` behaves as an alternate to the minimum flow line: The input will be 'cropped' to only include points to the right of the control line - modelling recirculation (ASV) from the correct control line. +The `CONTROL_MARGIN` behaves as an alternate to the minimum flow line: For each chart curve (a single speed chart will have one, a variable speed chart will have at least two) the input will be 'cropped' to only include points to the right of the control line - modelling recirculation (ASV) from the correct control line. The `CONTROL_MARGIN` is given as a percentage or fraction ([CONTROL_MARGIN_UNIT](/about/references/keywords/CONTROL_MARGIN_UNIT.md)) of the rate difference between minimum- and maximum flow, for the given speed. It is used to calculate the increase in minimum flow for each individual speed curve. -It is defined when setting up the stages in a [Variable speed compressor train model](/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model.md) or [Variable speed compressor train model with multiple streams and pressures](/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md). - -It is currently only possible to define a surge control margin for variable speed compressors. +It is defined when setting up the stages in a [Single speed compressor train model](/about/modelling/setup/models/compressor_modelling/compressor_models_types/single_speed_compressor_train_model.md), [Variable speed compressor train model](/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model.md) or [Variable speed compressor train model with multiple streams and pressures](/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md). See [Surge control margin for variable speed compressor chart](/about/modelling/setup/models/compressor_modelling/compressor_charts/index.md) for more details. +## Use in [Single speed compressor train model](/about/modelling/setup/models/compressor_modelling/compressor_models_types/single_speed_compressor_train_model.md) +### Format + +~~~~yaml +MODELS: + - NAME: + TYPE: SINGLE_SPEED_COMPRESSOR_TRAIN + FLUID_MODEL: + ... + COMPRESSOR_TRAIN: + STAGES: + - INLET_TEMPERATURE: + COMPRESSOR_CHART: + CONTROL_MARGIN: + CONTROL_MARGIN_UNIT: + .... +~~~~ + +### Example +~~~~yaml +MODELS: + - NAME: compressor_model + TYPE: SINGLE_SPEED_COMPRESSOR_TRAIN + FLUID_MODEL: fluid_model + ... + COMPRESSOR_TRAIN: + STAGES: + - INLET_TEMPERATURE: 20 + COMPRESSOR_CHART: 1_stage_chart + CONTROL_MARGIN: 0.1 + CONTROL_MARGIN_UNIT: FRACTION + .... +~~~~ + + +>>>>>>> Stashed changes ## Use in [Variable speed compressor train model](/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model.md) ### Format diff --git a/src/libecalc/core/models/chart/base.py b/src/libecalc/core/models/chart/base.py index 8d9a7ca5ed..72fcae605f 100644 --- a/src/libecalc/core/models/chart/base.py +++ b/src/libecalc/core/models/chart/base.py @@ -1,11 +1,14 @@ +from copy import deepcopy from typing import List, Optional, Tuple import numpy as np from numpy.typing import NDArray from scipy.interpolate import interp1d from shapely.geometry import LineString, Point +from typing_extensions import Self from libecalc import dto +from libecalc.common.logger import logger class ChartCurve: @@ -124,8 +127,8 @@ def rate_head_and_efficiency_at_maximum_rate(self) -> Tuple[float, float, float] return self.rate[-1], self.head[-1], self.efficiency[-1] def get_distance_and_efficiency_from_closest_point_on_curve(self, rate: float, head: float) -> Tuple[float, float]: - """Compute the closest distance from a point (rate,head) to the (interpolated) curve and corresponding efficiency for - that closest point. + """Compute the closest distance from a point (rate,head) to the (interpolated) curve and corresponding + efficiency for that closest point. """ head_linestring = LineString([(x, y) for x, y in zip(self.rate_values, self.head_values)]) p = Point(rate, head) @@ -136,3 +139,53 @@ def get_distance_and_efficiency_from_closest_point_on_curve(self, rate: float, h distance = -distance efficiency = float(self.efficiency_as_function_of_rate(closest_interpolated_point.x)) return distance, efficiency + + def adjust_for_control_margin(self, control_margin: Optional[float]) -> Self: + """Adjusts the chart curve with respect to the given control margin. + + Args: + control_margin: a fraction on the interval [0, 1] + + Returns: + Chart curve where lowest fraction (given by control margin) of rates have been removed and the + head/efficiency updated accordingly for the new minimum rate point on the curve + """ + if control_margin is None: + return deepcopy(self) + + def _get_new_point(x: List[float], y: List[float], new_x_value) -> float: + """Set up simple interpolation and get a point estimate on y based on the new x point.""" + return interp1d(x=x, y=y, fill_value=(np.min(y), np.max(y)), bounds_error=False, assume_sorted=False)( + new_x_value + ) + + logger.warning( + "The CONTROL_MARGIN functionality is experimental. Usage in an operational setting is not recommended. " + "Any usage of this functionality is at your own risk." + ) + adjust_minimum_rate_by = (np.max(self.rate) - np.min(self.rate)) * control_margin + new_minimum_rate = np.min(self.rate) + adjust_minimum_rate_by + rate_head_efficiency_array = np.vstack((self.rate, self.head, self.efficiency)) + # remove points with rate less than the new minimum rate (i.e. chop off left part of chart curve) + rate_head_efficiency_array = rate_head_efficiency_array[:, rate_head_efficiency_array[0, :] > new_minimum_rate] + + new_rate_head_efficiency_point = [ + new_minimum_rate, + _get_new_point(x=self.rate, y=self.head, new_x_value=new_minimum_rate), # Head as a function of rate + _get_new_point( # Efficiency as a function of rate + x=self.rate, y=self.efficiency, new_x_value=new_minimum_rate + ), + ] + + rate_head_efficiency_array = np.c_[ + new_rate_head_efficiency_point, + rate_head_efficiency_array, + ] + + new_chart_curve = deepcopy(self) + + new_chart_curve.rate_actual_m3_hour = rate_head_efficiency_array[0, :].tolist() + new_chart_curve.polytropic_head_joule_per_kg = rate_head_efficiency_array[1, :].tolist() + new_chart_curve.efficiency_fraction = rate_head_efficiency_array[2, :].tolist() + + return new_chart_curve diff --git a/src/libecalc/core/models/compressor/train/chart/single_speed_compressor_chart.py b/src/libecalc/core/models/compressor/train/chart/single_speed_compressor_chart.py index 2803a37ff7..427f385a4b 100644 --- a/src/libecalc/core/models/compressor/train/chart/single_speed_compressor_chart.py +++ b/src/libecalc/core/models/compressor/train/chart/single_speed_compressor_chart.py @@ -1,4 +1,5 @@ -from typing import Tuple +from copy import deepcopy +from typing import Optional, Tuple from libecalc.core.models.chart import SingleSpeedChart from libecalc.core.models.compressor.train.chart.types import ( @@ -15,6 +16,14 @@ class SingleSpeedCompressorChart(SingleSpeedChart): Chart may be used with or without efficiency values. """ + def get_chart_adjusted_for_control_margin(self, control_margin: Optional[float]) -> SingleSpeedChart: + """Sets a new minimum rate and corresponding head and efficiency for each curve in a compressor chart.""" + if control_margin is None: + return deepcopy(self) + new_chart = deepcopy(self) + + return new_chart.adjust_for_control_margin(control_margin=control_margin) + def _evaluate_point_validity_chart_area_flag_and_adjusted_rate( self, rate: float, diff --git a/src/libecalc/core/models/compressor/train/chart/variable_speed_compressor_chart.py b/src/libecalc/core/models/compressor/train/chart/variable_speed_compressor_chart.py index 095baa332c..812dded719 100644 --- a/src/libecalc/core/models/compressor/train/chart/variable_speed_compressor_chart.py +++ b/src/libecalc/core/models/compressor/train/chart/variable_speed_compressor_chart.py @@ -1,16 +1,15 @@ from __future__ import annotations from copy import deepcopy -from typing import List, Optional, Tuple, Union +from typing import Optional, Tuple, Union import numpy as np from numpy.typing import NDArray from scipy.interpolate import interp1d -from libecalc import dto from libecalc.common.errors.exceptions import IllegalStateException from libecalc.common.logger import logger -from libecalc.core.models.chart import ChartCurve, VariableSpeedChart +from libecalc.core.models.chart import VariableSpeedChart from libecalc.core.models.compressor.train.chart.types import ( CompressorChartHeadEfficiencyResultSinglePoint, CompressorChartResult, @@ -26,52 +25,10 @@ def get_chart_adjusted_for_control_margin(self, control_margin: Optional[float]) if control_margin is None: return deepcopy(self) - def _get_new_point(x: List[float], y: List[float], new_x_value) -> float: - """Set up simple interpolation and get a point estimate on y based on the new x point.""" - return interp1d(x=x, y=y, fill_value=(np.min(y), np.max(y)), bounds_error=False, assume_sorted=False)( - new_x_value - ) - - logger.warning( - "The CONTROL_MARGIN functionality is experimental. Usage in an operational setting is not recommended. " - "Any usage of this functionality is at your own risk." - ) - new_curves = [] - for curve in self.curves: - adjust_minimum_rate_by = (np.max(curve.rate) - np.min(curve.rate)) * control_margin - new_minimum_rate = np.min(curve.rate) + adjust_minimum_rate_by - rate_head_efficiency_array = np.vstack((curve.rate, curve.head, curve.efficiency)) - # remove points with rate less than the new minimum rate (i.e. chop off left part of chart curve) - rate_head_efficiency_array = rate_head_efficiency_array[ - :, rate_head_efficiency_array[0, :] > new_minimum_rate - ] - - new_rate_head_efficiency_point = [ - new_minimum_rate, - _get_new_point(x=curve.rate, y=curve.head, new_x_value=new_minimum_rate), # Head as a function of rate - _get_new_point( # Efficiency as a function of rate - x=curve.rate, y=curve.efficiency, new_x_value=new_minimum_rate - ), - ] - - rate_head_efficiency_array = np.c_[ - new_rate_head_efficiency_point, - rate_head_efficiency_array, - ] - - new_curves.append( - ChartCurve( - dto.ChartCurve( - rate_actual_m3_hour=rate_head_efficiency_array[0, :].tolist(), - polytropic_head_joule_per_kg=rate_head_efficiency_array[1, :].tolist(), - efficiency_fraction=rate_head_efficiency_array[2, :].tolist(), - speed_rpm=curve.speed_rpm, - ) - ) - ) - new_chart = deepcopy(self) - new_chart.curves = new_curves + new_chart.curves = [ + curve.adjust_for_control_margin(control_margin=control_margin) for curve in new_chart.curves + ] return new_chart diff --git a/src/libecalc/core/models/compressor/utils.py b/src/libecalc/core/models/compressor/utils.py index c48bb446d4..5183846791 100644 --- a/src/libecalc/core/models/compressor/utils.py +++ b/src/libecalc/core/models/compressor/utils.py @@ -53,19 +53,7 @@ def _create_undefined_compressor_train_stage( ) -def _create_single_speed_compressor_train_stage( - stage_data: dto.CompressorStage, -) -> CompressorTrainStage: - compressor_chart = _create_compressor_chart(stage_data.compressor_chart) - return CompressorTrainStage( - compressor_chart=compressor_chart, - inlet_temperature_kelvin=stage_data.inlet_temperature_kelvin, - pressure_drop_ahead_of_stage=stage_data.pressure_drop_before_stage, - remove_liquid_after_cooling=stage_data.remove_liquid_after_cooling, - ) - - -def _create_variable_speed_compressor_train_stage( +def _create_compressor_train_stage( stage_data: dto.CompressorStage, ) -> CompressorTrainStage: compressor_chart = _create_compressor_chart(stage_data.compressor_chart) @@ -84,10 +72,10 @@ def _create_variable_speed_compressor_train_stage( def map_compressor_train_stage_to_domain(stage_dto: dto.CompressorStage) -> CompressorTrainStage: """Todo: Add multiple streams and pressures here.""" if isinstance(stage_dto, dto.CompressorStage): - if isinstance(stage_dto.compressor_chart, (dto.VariableSpeedChart, dto.GenericChartFromDesignPoint)): - return _create_variable_speed_compressor_train_stage(stage_dto) - elif isinstance(stage_dto.compressor_chart, dto.SingleSpeedChart): - return _create_single_speed_compressor_train_stage(stage_dto) + if isinstance( + stage_dto.compressor_chart, (dto.VariableSpeedChart, dto.GenericChartFromDesignPoint, dto.SingleSpeedChart) + ): + return _create_compressor_train_stage(stage_dto) elif isinstance(stage_dto.compressor_chart, dto.GenericChartFromInput): return _create_undefined_compressor_train_stage(stage_dto) raise ValueError(f"Compressor stage typ {stage_dto.type_} has not been implemented.") diff --git a/src/libecalc/presentation/yaml/mappers/model.py b/src/libecalc/presentation/yaml/mappers/model.py index b0cc428711..7ba2c4c5fa 100644 --- a/src/libecalc/presentation/yaml/mappers/model.py +++ b/src/libecalc/presentation/yaml/mappers/model.py @@ -423,7 +423,15 @@ def _single_speed_compressor_train_mapper( pressure_drop_before_stage=stage.get( EcalcYamlKeywords.models_type_compressor_train_pressure_drop_ahead_of_stage, 0.0 ), - control_margin=0, + control_margin=convert_control_margin_to_fraction( + stage.get(EcalcYamlKeywords.models_type_compressor_train_stage_control_margin, 0.0), + YAML_UNIT_MAPPING[ + stage.get( + EcalcYamlKeywords.models_type_compressor_train_stage_control_margin_unit, + EcalcYamlKeywords.models_type_compressor_train_stage_control_margin_unit_percentage, + ) + ], + ), ) for stage in stages_data ] diff --git a/src/tests/libecalc/core/models/compressor_modelling/test_compressor_chart.py b/src/tests/libecalc/core/models/compressor_modelling/test_compressor_chart.py index 9f51bee577..07d505272a 100644 --- a/src/tests/libecalc/core/models/compressor_modelling/test_compressor_chart.py +++ b/src/tests/libecalc/core/models/compressor_modelling/test_compressor_chart.py @@ -2,7 +2,10 @@ import pytest from libecalc import dto from libecalc.common.errors.exceptions import IllegalStateException -from libecalc.core.models.compressor.train.chart import VariableSpeedCompressorChart +from libecalc.core.models.compressor.train.chart import ( + SingleSpeedCompressorChart, + VariableSpeedCompressorChart, +) from libecalc.dto.types import ChartAreaFlag from pytest import approx @@ -307,6 +310,39 @@ def test_calculate_scaling_factors_for_speed_below_and_above(): ) == (0.5, 0.5) +def test_single_speed_compressor_chart_control_margin(): + """When adjusting the chart using a control margin, we multiply the average of the minimum rate for all curves + by the control margin factor and multiply by the margin: + + Here: + minimum rates are [1, 4] => average = 2 + control margin = 0.1 (10 %) + Result: 2 * 0.1 = 0.25 + + This is used to move eash minimum rate to the "right" by 0.25. + + :return: + """ + compressor_chart = SingleSpeedCompressorChart( + dto.ChartCurve( + speed_rpm=1, + rate_actual_m3_hour=[1, 2, 3], + polytropic_head_joule_per_kg=[4, 5, 6], + efficiency_fraction=[0.7, 0.8, 0.9], + ), + ) + control_margin = 0.1 + compressor_chart_adjusted = compressor_chart.get_chart_adjusted_for_control_margin(control_margin=control_margin) + + adjust_minimum_rate_by = ( + compressor_chart.rate_actual_m3_hour[-1] - compressor_chart.rate_actual_m3_hour[0] + ) * control_margin + + new_minimum_rate = compressor_chart.rate_actual_m3_hour[0] + adjust_minimum_rate_by + + assert compressor_chart_adjusted.rate_actual_m3_hour[0] == new_minimum_rate + + def test_variable_speed_compressor_chart_control_margin(): """When adjusting the chart using a control margin, we multiply the average of the minimum rate for all curves by the control margin factor and multiply by the margin: