Skip to content

Commit

Permalink
feat: add control margin to single speed compressor charts (#418)
Browse files Browse the repository at this point in the history
docs: update control margin keyword
  • Loading branch information
olelod authored Mar 22, 2024
1 parent 7a66d54 commit 472e592
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 75 deletions.
44 changes: 39 additions & 5 deletions docs/docs/about/references/keywords/CONTROL_MARGIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <model name>
TYPE: SINGLE_SPEED_COMPRESSOR_TRAIN
FLUID_MODEL: <reference to fluid model, must be defined in MODELS>
...
COMPRESSOR_TRAIN:
STAGES:
- INLET_TEMPERATURE: <inlet temperature in Celsius for stage>
COMPRESSOR_CHART: <reference to compressor chart model for first stage, must be defined in MODELS or FACILITY_INPUTS>
CONTROL_MARGIN: <Default value is zero>
CONTROL_MARGIN_UNIT: <FRACTION or PERCENTAGE, default is PERCENTAGE>
....
~~~~

### 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

Expand Down
57 changes: 55 additions & 2 deletions src/libecalc/core/models/chart/base.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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

Expand Down
22 changes: 5 additions & 17 deletions src/libecalc/core/models/compressor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.")
10 changes: 9 additions & 1 deletion src/libecalc/presentation/yaml/mappers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 472e592

Please sign in to comment.