From 9f605fa335e76286d1755f4d6dc2c9ca3459eeb1 Mon Sep 17 00:00:00 2001 From: Kjetil Brakstad Date: Fri, 16 Jan 2026 12:16:56 +0100 Subject: [PATCH] feat: add MECHANICAL_EFFICIENCY to compressor train types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add mechanical_efficiency field (0 < η ≤ 1, default 1.0) to all train types - Shaft domain class owns efficiency, calculates shaft_power = gas_power / η - Mutual exclusivity with deprecated POWER_ADJUSTMENT_* params - Add docs, migration guide, and parametrized tests --- .../migration_guides/mechanical_efficiency.md | 101 ++++++++++++ ...del_with_multiple_streams_and_pressures.md | 5 +- ...d_variable_speed_compressor_train_model.md | 15 +- .../single_speed_compressor_train_model.md | 8 +- .../variable_speed_compressor_train_model.md | 6 +- ...del_with_multiple_streams_and_pressures.md | 1 + .../about/references/MECHANICAL_EFFICIENCY.md | 84 ++++++++++ .../references/POWER_ADJUSTMENT_CONSTANT.md | 14 +- .../references/POWER_ADJUSTMENT_FACTOR.md | 14 +- .../domain/process/compressor/core/results.py | 13 ++ .../process/compressor/core/train/stage.py | 19 ++- .../domain/process/entities/shaft/shaft.py | 24 ++- .../data/all_energy_usage_models.yaml | 2 +- .../yaml/mappers/consumer_function_mapper.py | 76 +++++++-- .../presentation/yaml/yaml_keywords.py | 3 + .../models/yaml_compressor_trains.py | 141 +++++++++++++++- .../all_energy_usage_models_v3.json | 156 +++++++++--------- ...t_yaml_mechanical_efficiency_validation.py | 132 +++++++++++++++ 18 files changed, 684 insertions(+), 130 deletions(-) create mode 100644 docs/docs/about/migration_guides/mechanical_efficiency.md create mode 100644 docs/docs/about/references/MECHANICAL_EFFICIENCY.md create mode 100644 tests/libecalc/presentation/yaml/yaml_types/models/test_yaml_mechanical_efficiency_validation.py diff --git a/docs/docs/about/migration_guides/mechanical_efficiency.md b/docs/docs/about/migration_guides/mechanical_efficiency.md new file mode 100644 index 0000000000..5e1b135e8d --- /dev/null +++ b/docs/docs/about/migration_guides/mechanical_efficiency.md @@ -0,0 +1,101 @@ +--- +title: Mechanical Efficiency Migration +description: Migrating from POWER_ADJUSTMENT_FACTOR to MECHANICAL_EFFICIENCY +sidebar_position: -16 +--- + +# Migrating to MECHANICAL_EFFICIENCY + +This guide explains how to migrate from the deprecated `POWER_ADJUSTMENT_FACTOR` and `POWER_ADJUSTMENT_CONSTANT` parameters to the new `MECHANICAL_EFFICIENCY` parameter. + +## Why Migrate? + +The legacy power adjustment parameters were empirical corrections without clear physical meaning. The new approach provides: + +- **Physical clarity**: Mechanical efficiency represents real-world losses in bearings, gearboxes, and seals +- **Better documentation**: Efficiency values can be traced to equipment specifications +- **Future compatibility**: Legacy parameters will be removed in a future version + +## Conversion Formula + +$$ +\eta_{mechanical} = \frac{1}{\text{POWER\_ADJUSTMENT\_FACTOR}} +$$ + +For example: `POWER_ADJUSTMENT_FACTOR: 1.05` → `MECHANICAL_EFFICIENCY: 0.952` + +:::note +`POWER_ADJUSTMENT_CONSTANT` has no direct equivalent. If you used a constant offset, consult the eCalc team. +::: + +## Migration Examples + +### All Compressor Train Types + +For all compressor train types (`VARIABLE_SPEED_COMPRESSOR_TRAIN`, `SINGLE_SPEED_COMPRESSOR_TRAIN`, `VARIABLE_SPEED_COMPRESSOR_TRAIN_MULTIPLE_STREAMS_AND_PRESSURES`, and `SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN`), set `MECHANICAL_EFFICIENCY` directly on the train model: + +**Before:** +```yaml +MODELS: + - NAME: export_compressor + TYPE: VARIABLE_SPEED_COMPRESSOR_TRAIN + POWER_ADJUSTMENT_FACTOR: 1.05 + FLUID_MODEL: medium_gas + COMPRESSOR_TRAIN: + STAGES: + - INLET_TEMPERATURE: 30 + COMPRESSOR_CHART: compressor_chart_ref +``` + +**After:** +```yaml +MODELS: + - NAME: export_compressor + TYPE: VARIABLE_SPEED_COMPRESSOR_TRAIN + MECHANICAL_EFFICIENCY: 0.952 # = 1/1.05, accounts for 5% mechanical losses + FLUID_MODEL: medium_gas + COMPRESSOR_TRAIN: + STAGES: + - INLET_TEMPERATURE: 30 + COMPRESSOR_CHART: compressor_chart_ref +``` + +### Simplified Trains + +The same pattern applies to `SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN`: + +**Before:** +```yaml +MODELS: + - NAME: simple_train + TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN + POWER_ADJUSTMENT_FACTOR: 1.08 + FLUID_MODEL: medium_gas + COMPRESSOR_TRAIN: + STAGES: + - INLET_TEMPERATURE: 30 + COMPRESSOR_CHART: generic_chart +``` + +**After:** +```yaml +MODELS: + - NAME: simple_train + TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN + MECHANICAL_EFFICIENCY: 0.926 # = 1/1.08 + FLUID_MODEL: medium_gas + COMPRESSOR_TRAIN: + STAGES: + - INLET_TEMPERATURE: 30 + COMPRESSOR_CHART: generic_chart +``` + +## Troubleshooting + +**Cannot specify both MECHANICAL_EFFICIENCY and POWER_ADJUSTMENT_FACTOR**: Remove the legacy parameter when using `MECHANICAL_EFFICIENCY`. The two approaches are mutually exclusive. + +**MECHANICAL_EFFICIENCY must be in range (0, 1]**: Ensure your value is greater than 0 and at most 1.0. Use the conversion formula above if migrating from `POWER_ADJUSTMENT_FACTOR`. + +## Further Reading + +- [MECHANICAL_EFFICIENCY Reference](/about/references/MECHANICAL_EFFICIENCY.md) diff --git a/docs/docs/about/modelling/setup/installations/compressor_models_in_calculations/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md b/docs/docs/about/modelling/setup/installations/compressor_models_in_calculations/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md index 0c17d07371..ca9e803cb0 100644 --- a/docs/docs/about/modelling/setup/installations/compressor_models_in_calculations/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md +++ b/docs/docs/about/modelling/setup/installations/compressor_models_in_calculations/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md @@ -26,9 +26,12 @@ ENERGY_USAGE_MODEL: SUCTION_PRESSURE: DISCHARGE_PRESSURE: INTERSTAGE_CONTROL_PRESSURE: - POWER_ADJUSTMENT_CONSTANT: ~~~~~~~~ +:::warning Deprecated Parameters +The `POWER_ADJUSTMENT_CONSTANT` parameter is deprecated. Use `MECHANICAL_EFFICIENCY` on the train model definition instead. +::: + The number of elements in [RATE_PER_STREAM](/about/references/RATE_PER_STREAM.md) must correspond to the number of streams defined for the model referenced in [COMPRESSOR_TRAIN_MODEL](/about/references/COMPRESSOR_TRAIN_MODEL.md). diff --git a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/simplified_variable_speed_compressor_train_model.md b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/simplified_variable_speed_compressor_train_model.md index c8b6360bf6..322dc0417a 100644 --- a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/simplified_variable_speed_compressor_train_model.md +++ b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/simplified_variable_speed_compressor_train_model.md @@ -27,13 +27,17 @@ stages are calculated at run-time based on input data. MODELS: - NAME: TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN - FLUID_MODEL: COMPRESSOR_TRAIN: - POWER_ADJUSTMENT_CONSTANT: + MECHANICAL_EFFICIENCY: 0 and ≤ 1.> MAXIMUM_POWER: - CALCULATE_MAX_RATE: + CALCULATE_MAX_RATE: ~~~~~~~~ +:::warning Deprecated Parameters +The `POWER_ADJUSTMENT_CONSTANT` and `POWER_ADJUSTMENT_FACTOR` parameters are deprecated. Use `MECHANICAL_EFFICIENCY` instead. +::: + ### Simplified compressor train model with known compressor stages When the compressor stages are known, each stage is defined with a compressor chart and an inlet temperature: @@ -42,6 +46,7 @@ MODELS: - NAME: TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN FLUID_MODEL: + MECHANICAL_EFFICIENCY: COMPRESSOR_TRAIN: STAGES: - INLET_TEMPERATURE: @@ -49,7 +54,6 @@ MODELS: - INLET_TEMPERATURE: COMPRESSOR_CHART: - ... and so forth for each stage in the train - POWER_ADJUSTMENT_CONSTANT: MAXIMUM_POWER: ~~~~~~~~ @@ -68,11 +72,11 @@ MODELS: - NAME: TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN FLUID_MODEL: + MECHANICAL_EFFICIENCY: COMPRESSOR_TRAIN: MAXIMUM_PRESSURE_RATIO_PER_STAGE: COMPRESSOR_CHART: INLET_TEMPERATURE: - POWER_ADJUSTMENT_CONSTANT: ~~~~~~~~ ## Examples @@ -106,6 +110,7 @@ MODELS: - NAME: simplified_compressor_model TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN FLUID_MODEL: fluid_model_1 + MECHANICAL_EFFICIENCY: 0.95 # Account for 5% mechanical losses in the drivetrain COMPRESSOR_TRAIN: STAGES: - INLET_TEMPERATURE: 30 diff --git a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/single_speed_compressor_train_model.md b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/single_speed_compressor_train_model.md index 8750f13148..4d1960d048 100644 --- a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/single_speed_compressor_train_model.md +++ b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/single_speed_compressor_train_model.md @@ -24,11 +24,15 @@ This means that a single speed compressor model needs the following to be define The following keywords are optional for a single speed compressor model: +- [MECHANICAL_EFFICIENCY](/about/references/MECHANICAL_EFFICIENCY.md) - Mechanical efficiency of the drivetrain (default 1.0) - [MAXIMUM_DISCHARGE_PRESSURE](/about/references/MAXIMUM_DISCHARGE_PRESSURE.md) -- [POWER_ADJUSTMENT_CONSTANT](/about/references/POWER_ADJUSTMENT_CONSTANT.md) - [MAXIMUM_POWER](/about/references/MAXIMUM_POWER.md) - [CALCULATE_MAX_RATE](/about/references/CALCULATE_MAX_RATE.md) +:::warning Deprecated Parameters +The `POWER_ADJUSTMENT_CONSTANT` and `POWER_ADJUSTMENT_FACTOR` parameters are deprecated. Use `MECHANICAL_EFFICIENCY` instead. +::: + The model is defined under the main keyword [MODELS](/about/references/MODELS.md) in the format ## Format @@ -40,6 +44,7 @@ MODELS: FLUID_MODEL: PRESSURE_CONTROL: MAXIMUM_DISCHARGE_PRESSURE: + MECHANICAL_EFFICIENCY: COMPRESSOR_TRAIN: STAGES: - INLET_TEMPERATURE: @@ -53,7 +58,6 @@ MODELS: CONTROL_MARGIN_UNIT: PRESSURE_DROP_AHEAD_OF_STAGE: - ... and so forth for each stage in the train - POWER_ADJUSTMENT_CONSTANT: MAXIMUM_POWER: CALCULATE_MAX_RATE: ~~~~~~~~ diff --git a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model.md b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model.md index 8a3757d924..4972f102f6 100644 --- a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model.md +++ b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model.md @@ -45,6 +45,7 @@ MODELS: - NAME: TYPE: VARIABLE_SPEED_COMPRESSOR_TRAIN FLUID_MODEL: + MECHANICAL_EFFICIENCY: COMPRESSOR_TRAIN: STAGES: - INLET_TEMPERATURE: @@ -58,9 +59,12 @@ MODELS: PRESSURE_DROP_AHEAD_OF_STAGE: CONTROL_MARGIN_UNIT: - ... and so forth for each stage in the train - POWER_ADJUSTMENT_CONSTANT: MAXIMUM_POWER: CALCULATE_MAX_RATE: PRESSURE_CONTROL: ~~~~~~~~ +:::warning Deprecated Parameters +The `POWER_ADJUSTMENT_CONSTANT` and `POWER_ADJUSTMENT_FACTOR` parameters are deprecated. Use `MECHANICAL_EFFICIENCY` instead. +::: + diff --git a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md index a143e9f4c0..47493da3cf 100644 --- a/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md +++ b/docs/docs/about/modelling/setup/models/compressor_modelling/compressor_models_types/variable_speed_compressor_train_model_with_multiple_streams_and_pressures.md @@ -53,6 +53,7 @@ MODELS: CONTROL_MARGIN_UNIT: PRESSURE_DROP_AHEAD_OF_STAGE: - ... + MECHANICAL_EFFICIENCY: MAXIMUM_POWER: ~~~~~~~~ diff --git a/docs/docs/about/references/MECHANICAL_EFFICIENCY.md b/docs/docs/about/references/MECHANICAL_EFFICIENCY.md new file mode 100644 index 0000000000..392fd7df16 --- /dev/null +++ b/docs/docs/about/references/MECHANICAL_EFFICIENCY.md @@ -0,0 +1,84 @@ +# MECHANICAL_EFFICIENCY + +[MODELS](/about/references/MODELS.md) / +[MECHANICAL_EFFICIENCY](/about/references/MECHANICAL_EFFICIENCY.md) + +## Description + +`MECHANICAL_EFFICIENCY` defines the ratio of useful mechanical work output to the total mechanical work input for the compressor drivetrain. It accounts for power losses in bearings, gearboxes, seals, and couplings. + +The mechanical efficiency is used to calculate the shaft power required from the driver: + +$$ +P_{shaft} = \frac{P_{gas}}{\eta_{mechanical}} +$$ + +Where: +- $P_{shaft}$ is the shaft power (input from driver) in MW +- $P_{gas}$ is the gas power (thermodynamic work on gas) in MW +- $\eta_{mechanical}$ is the mechanical efficiency (0 < η ≤ 1) + +## Format + +~~~~yaml +MODELS: + - NAME: + TYPE: + MECHANICAL_EFFICIENCY: # Optional, default 1.0 + ... +~~~~ + +## Example + +~~~~yaml +MODELS: + - NAME: export_compressor + TYPE: VARIABLE_SPEED_COMPRESSOR_TRAIN + MECHANICAL_EFFICIENCY: 0.95 # 95% efficiency, 5% losses + FLUID_MODEL: medium_gas + COMPRESSOR_TRAIN: + STAGES: + - INLET_TEMPERATURE: 30 + COMPRESSOR_CHART: compressor_chart_ref +~~~~ + +## Value Range + +| Constraint | Value | Description | +|------------|-------|-------------| +| Minimum | > 0 | Must be positive (exclusive) | +| Maximum | ≤ 1.0 | Cannot exceed 100% efficiency | +| Default | 1.0 | No mechanical losses (ideal drivetrain) | + +### Typical Values + +| Drive Configuration | Typical Range | Typical Losses | Notes | +|---------------------|---------------|----------------|-------| +| Direct drive | 0.96 - 0.98 | 2-4% | Bearings (1-2%) + seals/couplings (1-2%) | +| With gearbox | 0.93 - 0.96 | 4-7% | Adds gearbox losses (2-3%) | + +:::note +These are approximate ranges for the **total** mechanical efficiency of the drive train. The actual value depends on equipment design, operating conditions, and manufacturer data. A typical default of ~0.95 (5% losses) covers a common configuration with gearbox. +::: + +## Physical Interpretation + +The mechanical efficiency accounts for power losses in the drivetrain between +the driver (motor/turbine) and the compressor, including: + +- **Bearing losses**: Friction in shaft support bearings +- **Gearbox losses**: Friction and churning in gear systems (if present) +- **Seal and coupling losses**: Minor friction in auxiliary components + +:::note Constant efficiency assumption +In reality, frictional losses scale approximately with the square of rotational speed. +However, eCalc assumes a **constant** mechanical efficiency across all operating points. +Choose a representative value for typical operating conditions, or consult vendor data +for the expected speed range. +::: + +## See Also + +- [EFFICIENCY](/about/references/EFFICIENCY.md) +- [POLYTROPIC_EFFICIENCY](/about/references/POLYTROPIC_EFFICIENCY.md) +- [Mechanical Efficiency Migration Guide](/about/migration_guides/mechanical_efficiency.md) diff --git a/docs/docs/about/references/POWER_ADJUSTMENT_CONSTANT.md b/docs/docs/about/references/POWER_ADJUSTMENT_CONSTANT.md index 4f17b6a631..b0770bdacd 100644 --- a/docs/docs/about/references/POWER_ADJUSTMENT_CONSTANT.md +++ b/docs/docs/about/references/POWER_ADJUSTMENT_CONSTANT.md @@ -3,6 +3,10 @@ [MODELS](/about/references/MODELS.md) / [POWER_ADJUSTMENT_CONSTANT](/about/references/POWER_ADJUSTMENT_CONSTANT.md) +:::warning Deprecated +This parameter is deprecated and will be removed in a future version. Use [MECHANICAL_EFFICIENCY](/about/references/MECHANICAL_EFFICIENCY.md) instead for a physically meaningful way to model mechanical losses. +::: + ## Description Optional constant MW adjustment added to the model. Only added if (electrical) POWER > 0. Can be used in combination with [POWER_ADJUSTMENT_FACTOR](/about/references/POWER_ADJUSTMENT_FACTOR.md). @@ -18,6 +22,7 @@ MODELS: ## Example +**Deprecated usage:** ~~~~~yaml MODELS: - NAME: simple_compressor @@ -26,11 +31,4 @@ MODELS: POWER_ADJUSTMENT_CONSTANT: 10 #MW ~~~~~ -~~~~~yaml -MODELS: - - NAME: simple_compressor - TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN - ... - POWER_ADJUSTMENT_CONSTANT: 10 #MW - POWER_ADJUSTMENT_FACTOR: 1.2 -~~~~~ \ No newline at end of file +**Recommended migration:** See the [Mechanical Efficiency Migration Guide](/about/migration_guides/mechanical_efficiency.md). \ No newline at end of file diff --git a/docs/docs/about/references/POWER_ADJUSTMENT_FACTOR.md b/docs/docs/about/references/POWER_ADJUSTMENT_FACTOR.md index dd40a017b7..c666e53d4c 100644 --- a/docs/docs/about/references/POWER_ADJUSTMENT_FACTOR.md +++ b/docs/docs/about/references/POWER_ADJUSTMENT_FACTOR.md @@ -3,6 +3,10 @@ [MODELS](/about/references/MODELS.md) / [POWER_ADJUSTMENT_FACTOR](/about/references/POWER_ADJUSTMENT_FACTOR.md) +:::warning Deprecated +This parameter is deprecated and will be removed in a future version. Use [MECHANICAL_EFFICIENCY](/about/references/MECHANICAL_EFFICIENCY.md) instead for a physically meaningful way to model mechanical losses. +::: + ## Description Optional factor adjusting the power in the model. The power is multiplied by this factor. Can be used in combination with [POWER_ADJUSTMENT_CONSTANT](/about/references/POWER_ADJUSTMENT_CONSTANT.md). @@ -18,6 +22,7 @@ MODELS: ## Example +**Deprecated usage:** ~~~~~yaml MODELS: - NAME: simple_compressor @@ -26,11 +31,4 @@ MODELS: POWER_ADJUSTMENT_FACTOR: 1.2 ~~~~~ -~~~~~yaml -MODELS: - - NAME: simple_compressor - TYPE: SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN - ... - POWER_ADJUSTMENT_CONSTANT: 10 #MW - POWER_ADJUSTMENT_FACTOR: 1.2 -~~~~~ +**Recommended migration:** See the [Mechanical Efficiency Migration Guide](/about/migration_guides/mechanical_efficiency.md). diff --git a/src/libecalc/domain/process/compressor/core/results.py b/src/libecalc/domain/process/compressor/core/results.py index fc66aef38c..564d932d23 100644 --- a/src/libecalc/domain/process/compressor/core/results.py +++ b/src/libecalc/domain/process/compressor/core/results.py @@ -24,6 +24,11 @@ class CompressorTrainStageResultSingleTimeStep: head [J/kg] Polytropic efficiency (0, 1] power [MW] + + Power terminology: + - gas_power_megawatt: Thermodynamic power transferred to the gas (from enthalpy change) + - shaft_power_megawatt: Mechanical power on the shaft = gas_power / mechanical_efficiency + - power_megawatt: Shaft power (for backward compatibility, equals shaft_power_megawatt) """ def __init__( @@ -43,6 +48,8 @@ def __init__( rate_exceeds_maximum: bool | None = None, pressure_is_choked: bool | None = None, head_exceeds_maximum: bool | None = None, + gas_power_megawatt: float | None = None, + shaft_power_megawatt: float | None = None, ): self.inlet_stream = inlet_stream self.outlet_stream = outlet_stream @@ -59,6 +66,10 @@ def __init__( self.pressure_is_choked = pressure_is_choked self.head_exceeds_maximum = head_exceeds_maximum self.point_is_valid = bool(point_is_valid) + # New power fields for gas/shaft power distinction + # Default to power_megawatt for backward compatibility if not specified + self.gas_power_megawatt = gas_power_megawatt if gas_power_megawatt is not None else power_megawatt + self.shaft_power_megawatt = shaft_power_megawatt if shaft_power_megawatt is not None else power_megawatt @classmethod def create_empty(cls) -> CompressorTrainStageResultSingleTimeStep: @@ -78,6 +89,8 @@ def create_empty(cls) -> CompressorTrainStageResultSingleTimeStep: pressure_is_choked=False, head_exceeds_maximum=False, point_is_valid=True, + gas_power_megawatt=0.0, + shaft_power_megawatt=0.0, ) @property diff --git a/src/libecalc/domain/process/compressor/core/train/stage.py b/src/libecalc/domain/process/compressor/core/train/stage.py index 73efc3c30d..73868d8d24 100644 --- a/src/libecalc/domain/process/compressor/core/train/stage.py +++ b/src/libecalc/domain/process/compressor/core/train/stage.py @@ -69,6 +69,16 @@ def __init__( def fluid_service(self) -> FluidService: return self._fluid_service + @property + def shaft(self): + """Get shaft from compressor (delegate to compressor).""" + return self.compressor.shaft + + @property + def mechanical_efficiency(self) -> float: + """Get mechanical efficiency from shaft, defaulting to 1.0 if no shaft.""" + return self.shaft.mechanical_efficiency if self.shaft else 1.0 + @property def remove_liquid_after_cooling(self) -> bool: return self.liquid_remover is not None @@ -187,10 +197,13 @@ def evaluate( outlet_stream_compressor = self.rate_modifier.remove_rate(outlet_stream_compressor_including_asv) enthalpy_change = operational_point.polytropic_head_joule_per_kg / operational_point.polytropic_efficiency - power_megawatt = calculate_power_in_megawatt( + # Gas power: thermodynamic power transferred to the gas + gas_power_megawatt = calculate_power_in_megawatt( enthalpy_change_joule_per_kg=enthalpy_change, mass_rate_kg_per_hour=inlet_stream_compressor_including_asv.mass_rate_kg_per_h, ) + # Shaft power: mechanical power on shaft = gas_power / mechanical_efficiency + shaft_power_megawatt = gas_power_megawatt / self.mechanical_efficiency return CompressorTrainStageResultSingleTimeStep( inlet_stream=inlet_stream_compressor, @@ -201,7 +214,9 @@ def evaluate( polytropic_efficiency=operational_point.polytropic_efficiency, chart_area_flag=chart_area_flag, polytropic_enthalpy_change_kJ_per_kg=enthalpy_change / 1000, - power_megawatt=power_megawatt, + power_megawatt=shaft_power_megawatt, # Backward compatible: power = shaft power + gas_power_megawatt=gas_power_megawatt, + shaft_power_megawatt=shaft_power_megawatt, point_is_valid=operational_point.is_valid, polytropic_enthalpy_change_before_choke_kJ_per_kg=enthalpy_change / 1000, ) diff --git a/src/libecalc/domain/process/entities/shaft/shaft.py b/src/libecalc/domain/process/entities/shaft/shaft.py index 2fe552a18a..d4c831a1a0 100644 --- a/src/libecalc/domain/process/entities/shaft/shaft.py +++ b/src/libecalc/domain/process/entities/shaft/shaft.py @@ -4,13 +4,27 @@ class Shaft(ABC): """Abstract base class for a shaft. - Can be expanded to include more properties and methods as needed. - Name, id, units connected to it, etc + A shaft is a physical rotating component that connects the driver (turbine/motor) + to a compressor. The shaft owns mechanical efficiency, which accounts for + losses in bearings, gearbox, and couplings. + + Attributes: + mechanical_efficiency: Fraction of shaft power that becomes gas power. + Constraint: 0 < η_mech ≤ 1. Default is 1.0 (no losses). + speed_rpm: The rotational speed of the shaft in RPM. """ - def __init__(self, speed_rpm: float | None = None): + def __init__(self, mechanical_efficiency: float = 1.0, speed_rpm: float | None = None): + if not (0 < mechanical_efficiency <= 1): + raise ValueError(f"Mechanical efficiency must be in the range (0, 1], got {mechanical_efficiency}") + self._mechanical_efficiency = mechanical_efficiency self._speed_rpm = speed_rpm + @property + def mechanical_efficiency(self) -> float: + """Fraction of shaft power that becomes gas power (0 < η ≤ 1).""" + return self._mechanical_efficiency + @abstractmethod def set_speed(self, value: float) -> None: pass @@ -29,6 +43,8 @@ def speed_is_defined(self) -> bool: class SingleSpeedShaft(Shaft): + """Shaft with fixed rotational speed. Once set, speed cannot be changed.""" + def set_speed(self, value: float): if self._speed_rpm is None: self._speed_rpm = value @@ -37,5 +53,7 @@ def set_speed(self, value: float): class VariableSpeedShaft(Shaft): + """Shaft with variable rotational speed. Speed can be changed at runtime.""" + def set_speed(self, value: float): self._speed_rpm = value diff --git a/src/libecalc/fixtures/cases/all_energy_usage_models/data/all_energy_usage_models.yaml b/src/libecalc/fixtures/cases/all_energy_usage_models/data/all_energy_usage_models.yaml index 66a3539b6a..d0eea3c289 100644 --- a/src/libecalc/fixtures/cases/all_energy_usage_models/data/all_energy_usage_models.yaml +++ b/src/libecalc/fixtures/cases/all_energy_usage_models/data/all_energy_usage_models.yaml @@ -177,6 +177,7 @@ MODELS: TYPE: VARIABLE_SPEED_COMPRESSOR_TRAIN FLUID_MODEL: medium_gas PRESSURE_CONTROL: DOWNSTREAM_CHOKE + MECHANICAL_EFFICIENCY: 0.95 # 5% mechanical losses (bearings, seals, couplings) COMPRESSOR_TRAIN: STAGES: # stages for train with predefined number of compressors - INLET_TEMPERATURE: 30 @@ -187,7 +188,6 @@ MODELS: COMPRESSOR_CHART: predefined_compressor_chart CONTROL_MARGIN: 0 CONTROL_MARGIN_UNIT: PERCENTAGE # PERCENTAGE is default, FRACTION is other possible value - POWER_ADJUSTMENT_CONSTANT: 1 - NAME: variable_speed_compressor_train_predefined_variable_speed_chart_user_defined_fluid_composition TYPE: VARIABLE_SPEED_COMPRESSOR_TRAIN FLUID_MODEL: user_defined_fluid_composition diff --git a/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py b/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py index 1b2d760e31..df90929409 100644 --- a/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py +++ b/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py @@ -352,6 +352,11 @@ def _create_compressor_train_stage( interstage_pressure_control: InterstagePressureControl | None = None, control_margin: float | None = None, ) -> CompressorTrainStage: + """Create a compressor train stage. + + Args: + shaft: Optional shaft for mechanical efficiency. If None, compressor has no shaft (η=1.0 implicit). + """ chart_data = self._get_compressor_chart(compressor_chart_reference, control_margin) return CompressorTrainStage( @@ -378,6 +383,25 @@ def _create_compressor_train_stage( ), ) + def _create_shaft( + self, + mechanical_efficiency: float, + is_variable_speed: bool, + ): + """Create a Shaft domain object from mechanical efficiency value. + + Args: + mechanical_efficiency: Mechanical efficiency value (0 < η ≤ 1) + is_variable_speed: Whether to create VariableSpeedShaft or SingleSpeedShaft + + Returns: + Shaft instance with mechanical efficiency + """ + if is_variable_speed: + return VariableSpeedShaft(mechanical_efficiency=mechanical_efficiency) + else: + return SingleSpeedShaft(mechanical_efficiency=mechanical_efficiency) + def _create_variable_speed_compressor_train( self, model: YamlVariableSpeedCompressorTrain ) -> tuple[CompressorTrainCommonShaft, FluidModel]: @@ -385,11 +409,16 @@ def _create_variable_speed_compressor_train( fluid_model = self._get_fluid_model(fluid_model_reference) train_spec = model.compressor_train - shaft = VariableSpeedShaft() # Get the fluid service singleton fluid_service = NeqSimFluidService.instance() + # Create shared shaft for all compressors in the train + shared_shaft = self._create_shaft( + mechanical_efficiency=model.mechanical_efficiency, + is_variable_speed=True, + ) + # The stages are pre defined, known stages_data = train_spec.stages @@ -409,7 +438,7 @@ def _create_variable_speed_compressor_train( )[0], remove_liquid_after_cooling=True, fluid_service=fluid_service, - shaft=shaft, + shaft=shared_shaft, pressure_drop_ahead_of_stage=stage.pressure_drop_ahead_of_stage, control_margin=control_margin, ) @@ -420,7 +449,7 @@ def _create_variable_speed_compressor_train( compressor_model = CompressorTrainCommonShaft( stages=stages, - shaft=shaft, + shaft=shared_shaft, fluid_service=fluid_service, energy_usage_adjustment_constant=model.power_adjustment_constant, energy_usage_adjustment_factor=model.power_adjustment_factor, @@ -437,11 +466,16 @@ def _create_single_speed_compressor_train( fluid_model = self._get_fluid_model(fluid_model_reference) train_spec = model.compressor_train - shaft = SingleSpeedShaft() # Get the fluid service singleton fluid_service = NeqSimFluidService.instance() + # Create shared shaft for all compressors in the train + shared_shaft = self._create_shaft( + mechanical_efficiency=model.mechanical_efficiency, + is_variable_speed=False, + ) + stages: list[CompressorTrainStage] = [ self._create_compressor_train_stage( compressor_chart_reference=stage.compressor_chart, @@ -451,7 +485,7 @@ def _create_single_speed_compressor_train( )[0], remove_liquid_after_cooling=True, fluid_service=fluid_service, - shaft=shaft, + shaft=shared_shaft, pressure_drop_ahead_of_stage=stage.pressure_drop_ahead_of_stage, control_margin=convert_control_margin_to_fraction( stage.control_margin, @@ -474,7 +508,7 @@ def _create_single_speed_compressor_train( compressor_model = CompressorTrainCommonShaft( stages=stages, - shaft=shaft, + shaft=shared_shaft, fluid_service=fluid_service, pressure_control=pressure_control, maximum_discharge_pressure=maximum_discharge_pressure, @@ -537,7 +571,6 @@ def _create_simplified_model_with_prepared_stages( fluid_model = self._get_fluid_model(model.fluid_model) train_spec = model.compressor_train - shaft = SingleSpeedShaft() # Not used for simplified trains, but required by compressor if isinstance(train_spec, YamlUnknownCompressorStages): assert operational_data is not None @@ -575,6 +608,9 @@ def _create_simplified_model_with_prepared_stages( # Get the fluid service singleton fluid_service = NeqSimFluidService.instance() + # Get mechanical efficiency for per-stage shafts + mechanical_efficiency = model.mechanical_efficiency + stages: list[CompressorTrainStage] = [] if operational_data is None: # Expect only generic from design point @@ -586,10 +622,13 @@ def _create_simplified_model_with_prepared_stages( [yaml_stage.inlet_temperature], input_unit=Unit.CELSIUS, )[0] + # Create per-stage shaft with train-level mechanical efficiency + # Simplified trains have independent stages, each with its own shaft + stage_shaft = VariableSpeedShaft(mechanical_efficiency=mechanical_efficiency) stages.append( CompressorTrainStage( - rate_modifier=RateModifier(chart, shaft=shaft), - compressor=Compressor(chart, fluid_service=fluid_service, shaft=shaft), + rate_modifier=RateModifier(chart, shaft=stage_shaft), + compressor=Compressor(chart, fluid_service=fluid_service, shaft=stage_shaft), temperature_setter=TemperatureSetter( required_temperature_kelvin=inlet_temperature_kelvin, fluid_service=fluid_service ), @@ -647,10 +686,13 @@ def _create_simplified_model_with_prepared_stages( stage_inlet_pressure = stage_outlet_pressure + # Create per-stage shaft with train-level mechanical efficiency + # Simplified trains have independent stages, each with its own shaft + stage_shaft = VariableSpeedShaft(mechanical_efficiency=mechanical_efficiency) stages.append( CompressorTrainStage( - rate_modifier=RateModifier(chart, shaft=shaft), - compressor=Compressor(chart, fluid_service=fluid_service, shaft=shaft), + rate_modifier=RateModifier(chart, shaft=stage_shaft), + compressor=Compressor(chart, fluid_service=fluid_service, shaft=stage_shaft), temperature_setter=TemperatureSetter( required_temperature_kelvin=inlet_temperature_kelvin, fluid_service=fluid_service ), @@ -674,11 +716,15 @@ def _create_variable_speed_compressor_train_multiple_streams_and_pressures( ) -> tuple[CompressorTrainCommonShaft, list[FluidModel | None]]: stream_references = {stream.name for stream in model.streams} - shaft = VariableSpeedShaft() - # Get the fluid service singleton fluid_service = NeqSimFluidService.instance() + # Create shared shaft for all compressors in the train + shared_shaft = self._create_shaft( + mechanical_efficiency=model.mechanical_efficiency, + is_variable_speed=True, + ) + stream_to_stage_map: dict[str, int] = {} for stage_index, stage_config in enumerate(model.stages): for stream_reference in stage_config.stream or []: @@ -688,7 +734,7 @@ def _create_variable_speed_compressor_train_multiple_streams_and_pressures( stages = [ self._create_compressor_train_stage( fluid_service=fluid_service, - shaft=shaft, + shaft=shared_shaft, number_of_mixer_ports_this_stage=( sum( 1 @@ -751,7 +797,7 @@ def _create_variable_speed_compressor_train_multiple_streams_and_pressures( energy_usage_adjustment_constant=model.power_adjustment_constant, energy_usage_adjustment_factor=model.power_adjustment_factor, stages=stages, - shaft=shaft, + shaft=shared_shaft, fluid_service=fluid_service, calculate_max_rate=False, maximum_power=model.maximum_power, diff --git a/src/libecalc/presentation/yaml/yaml_keywords.py b/src/libecalc/presentation/yaml/yaml_keywords.py index 1981acb1b6..ef4a1442c2 100644 --- a/src/libecalc/presentation/yaml/yaml_keywords.py +++ b/src/libecalc/presentation/yaml/yaml_keywords.py @@ -179,6 +179,9 @@ class EcalcYamlKeywords: models_power_adjustment_factor_mw = "POWER_ADJUSTMENT_FACTOR" models_maximum_power = "MAXIMUM_POWER" + # Mechanical efficiency + mechanical_efficiency = "MECHANICAL_EFFICIENCY" + facility_adjustment = "ADJUSTMENT" facility_adjustment_factor = "FACTOR" facility_adjustment_constant = "CONSTANT" diff --git a/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_trains.py b/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_trains.py index 8d54650336..f705a89e09 100644 --- a/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_trains.py +++ b/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_trains.py @@ -1,3 +1,4 @@ +import logging from typing import Annotated, Literal, Union from pydantic import Field, model_validator @@ -22,6 +23,8 @@ YamlPressureControl, ) +logger = logging.getLogger(__name__) + class YamlCompressorTrainBase(YamlBase): name: ModelName = Field( @@ -65,15 +68,45 @@ class YamlSingleSpeedCompressorTrain(YamlCompressorTrainBase): ) power_adjustment_constant: float = Field( 0.0, - description="Constant to adjust power usage in MW", + description="[DEPRECATED] Constant to adjust power usage in MW. Use MECHANICAL_EFFICIENCY instead.", title="POWER_ADJUSTMENT_CONSTANT", ) power_adjustment_factor: float = Field( 1.0, - description="Factor to adjust power usage in MW", + description="[DEPRECATED] Factor to adjust power usage in MW. Use MECHANICAL_EFFICIENCY instead.", title="POWER_ADJUSTMENT_FACTOR", ) fluid_model: FluidModelReference = Field(..., description="Reference to a fluid model", title="FLUID_MODEL") + mechanical_efficiency: float = Field( + 1.0, + description="Mechanical efficiency of the compressor train shaft. " + "Value must be between 0 (exclusive) and 1 (inclusive). Typical values: 0.93-0.97.", + title="MECHANICAL_EFFICIENCY", + gt=0.0, + le=1.0, + ) + + @model_validator(mode="after") + def validate_mechanical_efficiency_and_legacy_params(self): + """Validate mutual exclusivity of MECHANICAL_EFFICIENCY and legacy power adjustment parameters.""" + has_mechanical_efficiency = self.mechanical_efficiency != 1.0 + has_legacy_factor = self.power_adjustment_factor != 1.0 + has_legacy_constant = self.power_adjustment_constant != 0.0 + + if has_mechanical_efficiency and (has_legacy_factor or has_legacy_constant): + raise ValueError( + f"Model '{self.name}': Cannot specify both MECHANICAL_EFFICIENCY and POWER_ADJUSTMENT_FACTOR/POWER_ADJUSTMENT_CONSTANT. " + "Use MECHANICAL_EFFICIENCY instead of legacy power adjustment parameters." + ) + + if has_legacy_factor or has_legacy_constant: + logger.warning( + f"Model '{self.name}': POWER_ADJUSTMENT_FACTOR and POWER_ADJUSTMENT_CONSTANT are deprecated " + "and will be removed in a future version. Use MECHANICAL_EFFICIENCY instead. " + "See migration guide: https://equinor.github.io/ecalc/about/migration_guides/mechanical_efficiency" + ) + + return self def to_dto(self): raise NotImplementedError @@ -103,15 +136,45 @@ class YamlVariableSpeedCompressorTrain(YamlCompressorTrainBase): ) power_adjustment_constant: float = Field( 0.0, - description="Constant to adjust power usage in MW", + description="[DEPRECATED] Constant to adjust power usage in MW. Use MECHANICAL_EFFICIENCY instead.", title="POWER_ADJUSTMENT_CONSTANT", ) power_adjustment_factor: float = Field( 1.0, - description="Factor to adjust power usage in MW", + description="[DEPRECATED] Factor to adjust power usage in MW. Use MECHANICAL_EFFICIENCY instead.", title="POWER_ADJUSTMENT_FACTOR", ) fluid_model: FluidModelReference = Field(..., description="Reference to a fluid model", title="FLUID_MODEL") + mechanical_efficiency: float = Field( + 1.0, + description="Mechanical efficiency of the compressor train shaft. " + "Value must be between 0 (exclusive) and 1 (inclusive). Typical values: 0.93-0.97.", + title="MECHANICAL_EFFICIENCY", + gt=0.0, + le=1.0, + ) + + @model_validator(mode="after") + def validate_mechanical_efficiency_and_legacy_params(self): + """Validate mutual exclusivity of MECHANICAL_EFFICIENCY and legacy power adjustment parameters.""" + has_mechanical_efficiency = self.mechanical_efficiency != 1.0 + has_legacy_factor = self.power_adjustment_factor != 1.0 + has_legacy_constant = self.power_adjustment_constant != 0.0 + + if has_mechanical_efficiency and (has_legacy_factor or has_legacy_constant): + raise ValueError( + f"Model '{self.name}': Cannot specify both MECHANICAL_EFFICIENCY and POWER_ADJUSTMENT_FACTOR/POWER_ADJUSTMENT_CONSTANT. " + "Use MECHANICAL_EFFICIENCY instead of legacy power adjustment parameters." + ) + + if has_legacy_factor or has_legacy_constant: + logger.warning( + f"Model '{self.name}': POWER_ADJUSTMENT_FACTOR and POWER_ADJUSTMENT_CONSTANT are deprecated " + "and will be removed in a future version. Use MECHANICAL_EFFICIENCY instead. " + "See migration guide: https://equinor.github.io/ecalc/about/migration_guides/mechanical_efficiency" + ) + + return self def to_dto(self): raise NotImplementedError @@ -135,6 +198,14 @@ class YamlSimplifiedVariableSpeedCompressorTrain(YamlCompressorTrainBase): title="CALCULATE_MAX_RATE", ) fluid_model: FluidModelReference = Field(..., description="Reference to a fluid model", title="FLUID_MODEL") + mechanical_efficiency: float = Field( + 1.0, + description="Mechanical efficiency of the compressors. Applied to each stage. " + "Value must be between 0 (exclusive) and 1 (inclusive). Typical values: 0.93-0.97.", + title="MECHANICAL_EFFICIENCY", + gt=0.0, + le=1.0, + ) power_adjustment_constant: float = Field( 0.0, description="Constant to adjust power usage in MW", @@ -149,6 +220,34 @@ class YamlSimplifiedVariableSpeedCompressorTrain(YamlCompressorTrainBase): def to_dto(self): raise NotImplementedError + @model_validator(mode="after") + def validate_mechanical_efficiency_and_legacy_params(self) -> "YamlSimplifiedVariableSpeedCompressorTrain": + """Validate mutual exclusivity between MECHANICAL_EFFICIENCY and legacy POWER_ADJUSTMENT_* params.""" + has_mechanical_efficiency = self.mechanical_efficiency != 1.0 + has_legacy_constant = self.power_adjustment_constant != 0.0 + has_legacy_factor = self.power_adjustment_factor != 1.0 + + # Mutual exclusivity check + if has_mechanical_efficiency and (has_legacy_constant or has_legacy_factor): + raise ValueError( + "MECHANICAL_EFFICIENCY cannot be used together with POWER_ADJUSTMENT_CONSTANT or " + "POWER_ADJUSTMENT_FACTOR. Use MECHANICAL_EFFICIENCY instead of the deprecated parameters." + ) + + # Deprecation warnings for legacy params + if has_legacy_constant: + logger.warning( + f"POWER_ADJUSTMENT_CONSTANT is deprecated for '{self.name}'. " + f"Use MECHANICAL_EFFICIENCY instead. See migration guide for conversion." + ) + if has_legacy_factor: + logger.warning( + f"POWER_ADJUSTMENT_FACTOR is deprecated for '{self.name}'. " + f"Use MECHANICAL_EFFICIENCY (= 1 / POWER_ADJUSTMENT_FACTOR) instead." + ) + + return self + @model_validator(mode="after") def check_compressor_chart(self, info: ValidationInfo): if info.context is not None: @@ -222,14 +321,22 @@ class YamlVariableSpeedCompressorTrainMultipleStreamsAndPressures(YamlCompressor ) power_adjustment_constant: float = Field( 0.0, - description="Constant to adjust power usage in MW", + description="[DEPRECATED] Constant to adjust power usage in MW. Use MECHANICAL_EFFICIENCY instead.", title="POWER_ADJUSTMENT_CONSTANT", ) power_adjustment_factor: float = Field( 1.0, - description="Factor to adjust power usage in MW", + description="[DEPRECATED] Factor to adjust power usage in MW. Use MECHANICAL_EFFICIENCY instead.", title="POWER_ADJUSTMENT_FACTOR", ) + mechanical_efficiency: float = Field( + 1.0, + description="Mechanical efficiency of the compressor train shaft. " + "Value must be between 0 (exclusive) and 1 (inclusive). Typical values: 0.93-0.97.", + title="MECHANICAL_EFFICIENCY", + gt=0.0, + le=1.0, + ) def to_dto(self): raise NotImplementedError @@ -247,6 +354,28 @@ def check_interstage_control_pressure(self): raise ValueError("Only one stage can have interstage control pressure defined.") return self + @model_validator(mode="after") + def validate_mechanical_efficiency_and_legacy_params(self): + """Validate mutual exclusivity of MECHANICAL_EFFICIENCY and legacy power adjustment parameters.""" + has_mechanical_efficiency = self.mechanical_efficiency != 1.0 + has_legacy_factor = self.power_adjustment_factor != 1.0 + has_legacy_constant = self.power_adjustment_constant != 0.0 + + if has_mechanical_efficiency and (has_legacy_factor or has_legacy_constant): + raise ValueError( + f"Model '{self.name}': Cannot specify both MECHANICAL_EFFICIENCY and POWER_ADJUSTMENT_FACTOR/POWER_ADJUSTMENT_CONSTANT. " + "Use MECHANICAL_EFFICIENCY instead of legacy power adjustment parameters." + ) + + if has_legacy_factor or has_legacy_constant: + logger.warning( + f"Model '{self.name}': POWER_ADJUSTMENT_FACTOR and POWER_ADJUSTMENT_CONSTANT are deprecated " + "and will be removed in a future version. Use MECHANICAL_EFFICIENCY instead. " + "See migration guide: https://equinor.github.io/ecalc/about/migration_guides/mechanical_efficiency" + ) + + return self + YamlCompressorTrain = Union[ YamlVariableSpeedCompressorTrain, 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 2c94adadcb..67ff35bc8a 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.95632, - 372.22471, - 348.94521, + 300.01029, + 372.27868, + 348.99919, 5.4146906 ] }, @@ -392,10 +392,10 @@ }, "unit": "GWh", "values": [ - 2627.6173, - 5888.3058, - 8945.0659, - 8945.0659 + 2628.0901, + 5889.2514, + 8946.4843, + 8946.4843 ] }, "power_electrical": { @@ -428,9 +428,9 @@ ], "unit": "MW", "values": [ - 267.7295, - 272.88952, - 274.05514, + 267.78347, + 272.94349, + 274.10912, 5.4146906 ] }, @@ -457,10 +457,10 @@ }, "unit": "GWh", "values": [ - 2345.3104, - 4735.8226, - 7136.5457, - 7136.5457 + 2345.7832, + 4736.7682, + 7137.964, + 7137.964 ] }, "power_mechanical": { @@ -54108,9 +54108,9 @@ ], "unit": "MW", "values": [ - 21.02546, - 21.02546, - 21.02546, + 21.079432, + 21.079432, + 21.079432, 0.0 ] }, @@ -54137,10 +54137,10 @@ }, "unit": "MWd", "values": [ - 7674.2929, - 15348.586, - 23022.879, - 23022.879 + 7693.9926, + 15387.985, + 23081.978, + 23081.978 ] }, "failure_status": [ @@ -54862,9 +54862,9 @@ ], "unit": "MW", "values": [ - 21.02546, - 21.02546, - 21.02546, + 21.079432, + 21.079432, + 21.079432, 0.0 ] }, @@ -54891,10 +54891,10 @@ }, "unit": "GWh", "values": [ - 184.18303, - 368.36606, - 552.54909, - 552.54909 + 184.65582, + 369.31164, + 553.96746, + 553.96746 ] }, "rate": { @@ -55280,9 +55280,9 @@ ], "unit": "MW", "values": [ - 6.3317606, - 6.3317606, - 6.3317606, + 6.6650111, + 6.6650111, + 6.6650111, 0.0 ] }, @@ -56167,9 +56167,9 @@ ], "unit": "MW", "values": [ - 6.3317606, - 6.3317606, - 6.3317606, + 6.6650111, + 6.6650111, + 6.6650111, 0.0 ] }, @@ -56543,9 +56543,9 @@ ], "unit": "MW", "values": [ - 13.693699, - 13.693699, - 13.693699, + 14.414421, + 14.414421, + 14.414421, 0.0 ] }, @@ -57430,9 +57430,9 @@ ], "unit": "MW", "values": [ - 13.693699, - 13.693699, - 13.693699, + 14.414421, + 14.414421, + 14.414421, 0.0 ] }, @@ -60199,9 +60199,9 @@ ], "unit": "MW", "values": [ - 299.95632, - 372.22471, - 348.94521, + 300.01029, + 372.27868, + 348.99919, 5.4146906 ] }, @@ -60228,10 +60228,10 @@ }, "unit": "GWh", "values": [ - 2627.6173, - 5888.3058, - 8945.0659, - 8945.0659 + 2628.0901, + 5889.2514, + 8946.4843, + 8946.4843 ] }, "power_electrical": { @@ -60264,9 +60264,9 @@ ], "unit": "MW", "values": [ - 267.7295, - 272.88952, - 274.05514, + 267.78347, + 272.94349, + 274.10912, 5.4146906 ] }, @@ -60293,10 +60293,10 @@ }, "unit": "GWh", "values": [ - 2345.3104, - 4735.8226, - 7136.5457, - 7136.5457 + 2345.7832, + 4736.7682, + 7137.964, + 7137.964 ] }, "power_mechanical": { @@ -60634,9 +60634,9 @@ ], "unit": "MW", "values": [ - 267.7295, - 272.88952, - 274.05514, + 267.78347, + 272.94349, + 274.10912, 5.4146906 ] }, @@ -60670,9 +60670,9 @@ ], "unit": "MW", "values": [ - 732.2705, - 727.11048, - 725.94486, + 732.21653, + 727.05651, + 725.89088, 994.58531 ] }, @@ -60699,10 +60699,10 @@ }, "unit": "GWh", "values": [ - 2345.3104, - 4735.8226, - 7136.5457, - 7136.5457 + 2345.7832, + 4736.7682, + 7137.964, + 7137.964 ] } }, @@ -64533,9 +64533,9 @@ ], "unit": "MW", "values": [ - 21.02546, - 21.02546, - 21.02546, + 21.079432, + 21.079432, + 21.079432, 0.0 ] }, @@ -64562,10 +64562,10 @@ }, "unit": "GWh", "values": [ - 184.18303, - 368.36606, - 552.54909, - 552.54909 + 184.65582, + 369.31164, + 553.96746, + 553.96746 ] }, "id": "variable_speed_compressor_train_predefined_charts", @@ -64650,9 +64650,9 @@ ], "unit": "MW", "values": [ - 21.02546, - 21.02546, - 21.02546, + 21.079432, + 21.079432, + 21.079432, 0.0 ] }, @@ -64679,10 +64679,10 @@ }, "unit": "GWh", "values": [ - 184.18303, - 368.36606, - 552.54909, - 552.54909 + 184.65582, + 369.31164, + 553.96746, + 553.96746 ] }, "rate_exceeds_maximum": { diff --git a/tests/libecalc/presentation/yaml/yaml_types/models/test_yaml_mechanical_efficiency_validation.py b/tests/libecalc/presentation/yaml/yaml_types/models/test_yaml_mechanical_efficiency_validation.py new file mode 100644 index 0000000000..df8fb55821 --- /dev/null +++ b/tests/libecalc/presentation/yaml/yaml_types/models/test_yaml_mechanical_efficiency_validation.py @@ -0,0 +1,132 @@ +"""Tests for MECHANICAL_EFFICIENCY on all compressor train types.""" + +import pytest +from pydantic import ValidationError + +from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_stages import ( + YamlCompressorStage, + YamlCompressorStageWithMarginAndPressureDrop, +) +from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_trains import ( + YamlCompressorStages, + YamlSimplifiedVariableSpeedCompressorTrain, + YamlSingleSpeedCompressorTrain, + YamlVariableSpeedCompressorTrain, +) + + +@pytest.fixture +def common_shaft_stages(): + """Minimal stages for single/variable speed trains.""" + stage = YamlCompressorStageWithMarginAndPressureDrop( + compressor_chart="chart_ref", + inlet_temperature=30.0, + control_margin=0.0, + control_margin_unit="PERCENTAGE", + ) + return YamlCompressorStages(stages=[stage]) + + +@pytest.fixture +def simplified_stages(): + """Minimal stages for simplified trains.""" + return YamlCompressorStages[YamlCompressorStage]( + stages=[YamlCompressorStage(inlet_temperature=30, compressor_chart="chart_ref")], + ) + + +def create_train(train_class, stages, **kwargs): + """Factory to create any train type with common defaults.""" + type_map = { + YamlSingleSpeedCompressorTrain: "SINGLE_SPEED_COMPRESSOR_TRAIN", + YamlVariableSpeedCompressorTrain: "VARIABLE_SPEED_COMPRESSOR_TRAIN", + YamlSimplifiedVariableSpeedCompressorTrain: "SIMPLIFIED_VARIABLE_SPEED_COMPRESSOR_TRAIN", + } + return train_class( + name="test_train", + type=type_map[train_class], + compressor_train=stages, + fluid_model="fluid_ref", + **kwargs, + ) + + +class TestMechanicalEfficiencyValidation: + """Tests for MECHANICAL_EFFICIENCY field validation across all train types.""" + + @pytest.mark.parametrize( + "train_class,stages_fixture", + [ + (YamlSingleSpeedCompressorTrain, "common_shaft_stages"), + (YamlVariableSpeedCompressorTrain, "common_shaft_stages"), + (YamlSimplifiedVariableSpeedCompressorTrain, "simplified_stages"), + ], + ) + def test_valid_mechanical_efficiency(self, train_class, stages_fixture, request): + """Valid mechanical efficiency within range (0, 1].""" + stages = request.getfixturevalue(stages_fixture) + train = create_train(train_class, stages, mechanical_efficiency=0.95) + assert train.mechanical_efficiency == 0.95 + + @pytest.mark.parametrize( + "train_class,stages_fixture", + [ + (YamlSingleSpeedCompressorTrain, "common_shaft_stages"), + (YamlVariableSpeedCompressorTrain, "common_shaft_stages"), + (YamlSimplifiedVariableSpeedCompressorTrain, "simplified_stages"), + ], + ) + def test_default_mechanical_efficiency(self, train_class, stages_fixture, request): + """Default mechanical efficiency is 1.0 (no losses).""" + stages = request.getfixturevalue(stages_fixture) + train = create_train(train_class, stages) + assert train.mechanical_efficiency == 1.0 + + @pytest.mark.parametrize( + "train_class,stages_fixture", + [ + (YamlSingleSpeedCompressorTrain, "common_shaft_stages"), + (YamlVariableSpeedCompressorTrain, "common_shaft_stages"), + (YamlSimplifiedVariableSpeedCompressorTrain, "simplified_stages"), + ], + ) + @pytest.mark.parametrize("invalid_value", [0.0, -0.1, 1.1]) + def test_invalid_mechanical_efficiency(self, train_class, stages_fixture, invalid_value, request): + """Invalid mechanical efficiency values should fail validation.""" + stages = request.getfixturevalue(stages_fixture) + with pytest.raises(ValidationError): + create_train(train_class, stages, mechanical_efficiency=invalid_value) + + +class TestMutualExclusivityWithLegacyParams: + """Tests that MECHANICAL_EFFICIENCY cannot be used with legacy POWER_ADJUSTMENT_* params.""" + + @pytest.mark.parametrize( + "train_class,stages_fixture", + [ + (YamlSingleSpeedCompressorTrain, "common_shaft_stages"), + (YamlVariableSpeedCompressorTrain, "common_shaft_stages"), + (YamlSimplifiedVariableSpeedCompressorTrain, "simplified_stages"), + ], + ) + def test_cannot_combine_with_power_adjustment_factor(self, train_class, stages_fixture, request): + """Cannot use both MECHANICAL_EFFICIENCY and POWER_ADJUSTMENT_FACTOR.""" + stages = request.getfixturevalue(stages_fixture) + with pytest.raises(ValidationError) as exc_info: + create_train(train_class, stages, mechanical_efficiency=0.95, power_adjustment_factor=1.05) + assert "cannot" in str(exc_info.value).lower() + + @pytest.mark.parametrize( + "train_class,stages_fixture", + [ + (YamlSingleSpeedCompressorTrain, "common_shaft_stages"), + (YamlVariableSpeedCompressorTrain, "common_shaft_stages"), + (YamlSimplifiedVariableSpeedCompressorTrain, "simplified_stages"), + ], + ) + def test_cannot_combine_with_power_adjustment_constant(self, train_class, stages_fixture, request): + """Cannot use both MECHANICAL_EFFICIENCY and POWER_ADJUSTMENT_CONSTANT.""" + stages = request.getfixturevalue(stages_fixture) + with pytest.raises(ValidationError) as exc_info: + create_train(train_class, stages, mechanical_efficiency=0.95, power_adjustment_constant=1.0) + assert "cannot" in str(exc_info.value).lower()