From ef21d6fa273344f7faebcaf9f5f300ad1774417e Mon Sep 17 00:00:00 2001 From: Jostein Solaas Date: Tue, 24 Sep 2024 19:13:09 +0200 Subject: [PATCH] refactor!: use yaml objects in mapper BREAKING CHANGE: H2O no longer supported in fluid composition, use 'water' --- .../about/migration_guides/v8.22_to_v8.23.md | 45 ++ docs/docs/changelog/next.md | 2 + .../yaml/energy_model_validation.py | 54 +- .../yaml/mappers/component_mapper.py | 131 +++-- .../yaml/mappers/consumer_function_mapper.py | 226 ++++----- .../yaml/mappers/create_references.py | 79 +-- .../yaml/mappers/facility_input.py | 89 ++-- .../presentation/yaml/mappers/fluid_mapper.py | 55 +- .../yaml/mappers/fuel_and_emission_mapper.py | 33 +- .../presentation/yaml/mappers/model.py | 469 +++++++----------- src/libecalc/presentation/yaml/parse_input.py | 4 +- .../yaml/yaml_models/pyyaml_yaml_model.py | 28 +- .../yaml/yaml_models/yaml_model.py | 10 +- .../legacy/energy_usage_model/common.py | 8 +- .../legacy/yaml_electricity_consumer.py | 10 +- .../components/legacy/yaml_fuel_consumer.py | 10 +- .../yaml/yaml_types/components/yaml_asset.py | 4 +- .../components/yaml_installation.py | 1 + .../facility_model/yaml_facility_model.py | 2 +- .../yaml/yaml_types/models/__init__.py | 2 +- .../models/yaml_compressor_chart.py | 18 +- .../models/yaml_compressor_stages.py | 4 +- .../models/yaml_compressor_trains.py | 2 +- .../yaml/yaml_types/models/yaml_fluid.py | 1 - .../yaml/yaml_types/yaml_data_or_file.py | 5 +- .../input/mappers/test_consumer_chart.py | 40 +- .../input/mappers/test_energy_usage_model.py | 23 +- .../input/mappers/test_facility_input.py | 31 +- .../input/mappers/test_fueltype_mapper.py | 50 -- .../input/mappers/test_model_mapper.py | 124 ++--- .../libecalc/input/mappers/test_utils.py | 28 +- .../libecalc/input/test_create_references.py | 45 +- 32 files changed, 760 insertions(+), 873 deletions(-) create mode 100644 docs/docs/about/migration_guides/v8.22_to_v8.23.md delete mode 100644 src/tests/libecalc/input/mappers/test_fueltype_mapper.py diff --git a/docs/docs/about/migration_guides/v8.22_to_v8.23.md b/docs/docs/about/migration_guides/v8.22_to_v8.23.md new file mode 100644 index 0000000000..4d610d6f63 --- /dev/null +++ b/docs/docs/about/migration_guides/v8.22_to_v8.23.md @@ -0,0 +1,45 @@ +--- +title: v8.22 to v8.23 +description: v8.22 to v8.23 migration +sidebar_position: -11 +--- + +# v8.22 to v8.23 + +In this migration guide you will find: + +1. [YAML changes](#yaml-migration) + +## Yaml migration + +### Migration overview + +This doc guides you through migrating an existing eCalcâ„¢ model from version v8.22 to v8.23. + +We try to make this as easy as possible, and provide a step-by-step migration guide. + +### 1. Changes to COMPOSITION +- `H2O` is no longer allowed in a fluid composition, `water` should be used instead + +```yaml +MODELS: +- NAME: + TYPE: FLUID + FLUID_MODEL_TYPE: COMPOSITION + EOS_MODEL: + COMPOSITION: + # This is old + H2O: + # This is new + water: + nitrogen: + CO2: + methane: + ethane: + propane: + i_butane: + n_butane: + i_pentane: + n_pentane: + n_hexane: +``` diff --git a/docs/docs/changelog/next.md b/docs/docs/changelog/next.md index 7b13ee57b1..4cb2be0581 100644 --- a/docs/docs/changelog/next.md +++ b/docs/docs/changelog/next.md @@ -37,3 +37,5 @@ sidebar_position: -1002 Main changes: - Economics has been removed. TAX, PRICE and QUOTA keywords will now give an error. - Misplaced keywords will now cause an error instead of being ignored. + +- H2O is no longer supported in composition. Use 'water' instead. \ No newline at end of file diff --git a/src/libecalc/presentation/yaml/energy_model_validation.py b/src/libecalc/presentation/yaml/energy_model_validation.py index 02086966b4..d598e9d950 100644 --- a/src/libecalc/presentation/yaml/energy_model_validation.py +++ b/src/libecalc/presentation/yaml/energy_model_validation.py @@ -1,17 +1,45 @@ -import datetime +from datetime import datetime +from typing import Dict, Union -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords +from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model import ( + YamlEnergyUsageModelCompressor, + YamlEnergyUsageModelCompressorSystem, + YamlEnergyUsageModelCompressorTrainMultipleStreams, + YamlEnergyUsageModelDirect, + YamlEnergyUsageModelTabulated, +) -def validate_energy_usage_models(model: dict, consumer_name: str): - energy_models = [] - for key, value in model.items(): - if isinstance(key, datetime.date) and value[EcalcYamlKeywords.type] not in energy_models: - energy_models.append(value[EcalcYamlKeywords.type]) +def validate_energy_usage_models( + model: Union[ + YamlEnergyUsageModelDirect, + YamlEnergyUsageModelCompressor, + YamlEnergyUsageModelCompressorSystem, + YamlEnergyUsageModelTabulated, + YamlEnergyUsageModelCompressorTrainMultipleStreams, + Dict[ + datetime, + Union[ + YamlEnergyUsageModelDirect, + YamlEnergyUsageModelCompressor, + YamlEnergyUsageModelCompressorSystem, + YamlEnergyUsageModelTabulated, + YamlEnergyUsageModelCompressorTrainMultipleStreams, + ], + ], + ], + consumer_name: str, +): + if isinstance(model, dict): + # Temporal model since dict + energy_model_types = [] + for value in model.values(): + if value.type not in energy_model_types: + energy_model_types.append(value.type) - if len(energy_models) > 1: - energy_models_list = ", ".join(energy_models) - raise ValueError( - "Energy model type cannot change over time within a single consumer." - f" The model type is changed for '{consumer_name}': {energy_models_list}", - ) + if len(energy_model_types) > 1: + energy_models_list = ", ".join(energy_model_types) + raise ValueError( + "Energy model type cannot change over time within a single consumer." + f" The model type is changed for '{consumer_name}': {energy_models_list}", + ) diff --git a/src/libecalc/presentation/yaml/mappers/component_mapper.py b/src/libecalc/presentation/yaml/mappers/component_mapper.py index c5eaa8a27b..fe0e36cae1 100644 --- a/src/libecalc/presentation/yaml/mappers/component_mapper.py +++ b/src/libecalc/presentation/yaml/mappers/component_mapper.py @@ -1,12 +1,13 @@ from datetime import datetime from typing import Dict, Optional, Union -from pydantic import TypeAdapter, ValidationError +from pydantic import ValidationError from libecalc.common.component_type import ComponentType from libecalc.common.consumer_type import ConsumerType from libecalc.common.consumption_type import ConsumptionType from libecalc.common.energy_model_type import EnergyModelType +from libecalc.common.errors.exceptions import InvalidReferenceException from libecalc.common.logger import logger from libecalc.common.time_utils import Period, define_time_model_for_period from libecalc.dto import ConsumerFunction, FuelType @@ -22,14 +23,14 @@ DtoValidationError, ) from libecalc.presentation.yaml.yaml_entities import References -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords from libecalc.presentation.yaml.yaml_models.yaml_model import YamlValidator +from libecalc.presentation.yaml.yaml_types.components.legacy.yaml_electricity_consumer import YamlElectricityConsumer +from libecalc.presentation.yaml.yaml_types.components.legacy.yaml_fuel_consumer import YamlFuelConsumer from libecalc.presentation.yaml.yaml_types.components.system.yaml_consumer_system import ( YamlConsumerSystem, ) -from libecalc.presentation.yaml.yaml_types.emitters.yaml_venting_emitter import ( - YamlVentingEmitter, -) +from libecalc.presentation.yaml.yaml_types.components.yaml_generator_set import YamlGeneratorSet +from libecalc.presentation.yaml.yaml_types.components.yaml_installation import YamlInstallation energy_usage_model_to_component_type_map = { ConsumerType.PUMP: ComponentType.PUMP, @@ -94,63 +95,60 @@ def __init__(self, references: References, target_period: Period): def from_yaml_to_dto( self, - data: Dict, + data: Union[YamlFuelConsumer, YamlElectricityConsumer, YamlConsumerSystem], regularity: Dict[datetime, Expression], consumes: ConsumptionType, default_fuel: Optional[str] = None, ) -> Consumer: - component_type = data.get(EcalcYamlKeywords.type) - if component_type is not None and component_type != ComponentType.CONSUMER_SYSTEM_V2: + component_type = data.component_type + if component_type not in [ComponentType.CONSUMER_SYSTEM_V2.value, "FUEL_CONSUMER", "ELECTRICITY_CONSUMER"]: # We have type here for v2, check that type is valid raise DataValidationError( - data=data, - message=f"Invalid component type '{component_type}' for component with name '{data.get(EcalcYamlKeywords.name)}'", + data=data.model_dump(), + message=f"Invalid component type '{component_type}' for component with name '{data.name}'", ) fuel = None if consumes == ConsumptionType.FUEL: + consumer_fuel = data.fuel if hasattr(data, "fuel") else None # YamlConsumerSystem does not have fuel try: - fuel = _resolve_fuel( - data.get(EcalcYamlKeywords.fuel), default_fuel, self.__references, target_period=self._target_period - ) - except ValueError as e: + fuel = _resolve_fuel(consumer_fuel, default_fuel, self.__references, target_period=self._target_period) + except InvalidReferenceException as e: raise DataValidationError( - data=data, - message=f"Fuel '{data.get(EcalcYamlKeywords.fuel)}' does not exist", + data=data.model_dump(), + message=f"Fuel '{consumer_fuel or default_fuel}' does not exist", error_key="fuel", ) from e - if component_type is not None: - if component_type == ComponentType.CONSUMER_SYSTEM_V2: - try: - compressor_system_yaml = YamlConsumerSystem(**data) - return compressor_system_yaml.to_dto( - consumes=consumes, - regularity=regularity, - references=self.__references, - target_period=self._target_period, - fuel=fuel, - ) - except ValidationError as e: - raise DtoValidationError(data=data, validation_error=e) from e + if isinstance(data, YamlConsumerSystem): + try: + return data.to_dto( + consumes=consumes, + regularity=regularity, + references=self.__references, + target_period=self._target_period, + fuel=fuel, + ) + except ValidationError as e: + raise DtoValidationError(data=data.model_dump(), validation_error=e) from e energy_usage_model = resolve_reference( - data.get(EcalcYamlKeywords.energy_usage_model), + data.energy_usage_model, references=self.__references.models, - ) + ) # TODO: This is never a reference? So resolve_reference always return the same value, remove try: energy_usage_model = self.__energy_usage_model_mapper.from_yaml_to_dto(energy_usage_model) except ValidationError as e: - raise DtoValidationError(data=data, validation_error=e) from e + raise DtoValidationError(data=data.model_dump(), validation_error=e) from e if consumes == ConsumptionType.FUEL: try: - fuel_consumer_name = data.get(EcalcYamlKeywords.name) + fuel_consumer_name = data.name return FuelConsumer( name=fuel_consumer_name, user_defined_category=define_time_model_for_period( - data.get(EcalcYamlKeywords.user_defined_tag), target_period=self._target_period + data.category, target_period=self._target_period ), regularity=regularity, fuel=fuel, @@ -158,21 +156,21 @@ def from_yaml_to_dto( component_type=_get_component_type(energy_usage_model), ) except ValidationError as e: - raise DtoValidationError(data=data, validation_error=e) from e + raise DtoValidationError(data=data.model_dump(), validation_error=e) from e else: try: - electricity_consumer_name = data.get(EcalcYamlKeywords.name) + electricity_consumer_name = data.name return ElectricityConsumer( name=electricity_consumer_name, regularity=regularity, user_defined_category=define_time_model_for_period( - data.get(EcalcYamlKeywords.user_defined_tag), target_period=self._target_period + data.category, target_period=self._target_period ), energy_usage_model=energy_usage_model, component_type=_get_component_type(energy_usage_model), ) except ValidationError as e: - raise DtoValidationError(data=data, validation_error=e) from e + raise DtoValidationError(data=data.model_dump(), validation_error=e) from e class GeneratorSetMapper: @@ -183,22 +181,20 @@ def __init__(self, references: References, target_period: Period): def from_yaml_to_dto( self, - data: Dict, + data: YamlGeneratorSet, regularity: Dict[datetime, Expression], default_fuel: Optional[str] = None, ) -> GeneratorSet: try: - fuel = _resolve_fuel( - data.get(EcalcYamlKeywords.fuel), default_fuel, self.__references, target_period=self._target_period - ) + fuel = _resolve_fuel(data.fuel, default_fuel, self.__references, target_period=self._target_period) except ValueError as e: raise DataValidationError( - data=data, - message=f"Fuel '{data.get(EcalcYamlKeywords.fuel)}' does not exist", + data=data.model_dump(), + message=f"Fuel '{data.fuel}' does not exist", # TODO: What if default fuel does not exist? error_key="fuel", ) from e generator_set_model_data = define_time_model_for_period( - data.get(EcalcYamlKeywords.el2fuel), target_period=self._target_period + data.electricity2fuel, target_period=self._target_period ) generator_set_model = { start_time: resolve_reference( @@ -217,17 +213,15 @@ def from_yaml_to_dto( regularity=regularity, consumes=ConsumptionType.ELECTRICITY, ) - for consumer in data.get(EcalcYamlKeywords.consumers, []) + for consumer in data.consumers or [] ] - user_defined_category = define_time_model_for_period( - data.get(EcalcYamlKeywords.user_defined_tag), target_period=self._target_period - ) + user_defined_category = define_time_model_for_period(data.category, target_period=self._target_period) - cable_loss = convert_expression(data.get(EcalcYamlKeywords.cable_loss)) - max_usage_from_shore = convert_expression(data.get(EcalcYamlKeywords.max_usage_from_shore)) + cable_loss = convert_expression(data.cable_loss) + max_usage_from_shore = convert_expression(data.max_usage_from_shore) try: - generator_set_name = data.get(EcalcYamlKeywords.name) + generator_set_name = data.name return GeneratorSet( name=generator_set_name, fuel=fuel, @@ -239,7 +233,7 @@ def from_yaml_to_dto( max_usage_from_shore=max_usage_from_shore, ) except ValidationError as e: - raise DtoValidationError(data=data, validation_error=e) from e + raise DtoValidationError(data=data.model_dump(), validation_error=e) from e class InstallationMapper: @@ -249,13 +243,13 @@ def __init__(self, references: References, target_period: Period): self.__generator_set_mapper = GeneratorSetMapper(references=references, target_period=target_period) self.__consumer_mapper = ConsumerMapper(references=references, target_period=target_period) - def from_yaml_to_dto(self, data: Dict) -> Installation: - fuel_data = data.get(EcalcYamlKeywords.fuel) + def from_yaml_to_dto(self, data: YamlInstallation) -> Installation: + fuel_data = data.fuel regularity = define_time_model_for_period( - convert_expression(data.get(EcalcYamlKeywords.regularity, 1)), target_period=self._target_period + convert_expression(data.regularity or 1), target_period=self._target_period ) - installation_name = data.get(EcalcYamlKeywords.name) + installation_name = data.name generator_sets = [ self.__generator_set_mapper.from_yaml_to_dto( @@ -263,7 +257,7 @@ def from_yaml_to_dto(self, data: Dict) -> Installation: regularity=regularity, default_fuel=fuel_data, ) - for generator_set in data.get(EcalcYamlKeywords.generator_sets, []) + for generator_set in data.generator_sets or [] ] fuel_consumers = [ self.__consumer_mapper.from_yaml_to_dto( @@ -272,22 +266,11 @@ def from_yaml_to_dto(self, data: Dict) -> Installation: consumes=ConsumptionType.FUEL, default_fuel=fuel_data, ) - for fuel_consumer in data.get(EcalcYamlKeywords.fuel_consumers, []) + for fuel_consumer in data.fuel_consumers or [] ] - venting_emitters = [] - for venting_emitter in data.get(EcalcYamlKeywords.installation_venting_emitters, []): - try: - venting_emitter = TypeAdapter(YamlVentingEmitter).validate_python(venting_emitter) - venting_emitters.append(venting_emitter) - except ValidationError as e: - raise DtoValidationError(data=venting_emitter, validation_error=e) from e - hydrocarbon_export = define_time_model_for_period( - data.get( - EcalcYamlKeywords.hydrocarbon_export, - Expression.setup_from_expression(0), - ), + data.hydrocarbon_export or Expression.setup_from_expression(0), target_period=self._target_period, ) @@ -297,11 +280,11 @@ def from_yaml_to_dto(self, data: Dict) -> Installation: regularity=regularity, hydrocarbon_export=hydrocarbon_export, fuel_consumers=[*generator_sets, *fuel_consumers], - venting_emitters=venting_emitters, - user_defined_category=data.get(EcalcYamlKeywords.user_defined_tag), + venting_emitters=data.venting_emitters or [], + user_defined_category=data.category, ) except ValidationError as e: - raise DtoValidationError(data=data, validation_error=e) from e + raise DtoValidationError(data=data.model_dump(), validation_error=e) from e class EcalcModelMapper: diff --git a/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py b/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py index 1ca5cb91fe..7ed890e3e1 100644 --- a/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py +++ b/src/libecalc/presentation/yaml/mappers/consumer_function_mapper.py @@ -1,9 +1,8 @@ from datetime import datetime -from typing import Dict, List, Optional, Set, Union +from typing import Dict, List, Optional, Protocol, Set, Union from libecalc.common.energy_model_type import EnergyModelType from libecalc.common.energy_usage_type import EnergyUsageType -from libecalc.common.logger import logger from libecalc.common.time_utils import Period, define_time_model_for_period from libecalc.common.utils.rates import RateType from libecalc.dto import ( @@ -15,7 +14,6 @@ CompressorWithTurbine, ConsumerFunction, DirectConsumerFunction, - EnergyModel, PumpConsumerFunction, PumpSystemConsumerFunction, PumpSystemOperationalSetting, @@ -28,9 +26,21 @@ from libecalc.presentation.yaml.mappers.utils import ( resolve_reference, ) -from libecalc.presentation.yaml.validation_errors import DataValidationError from libecalc.presentation.yaml.yaml_entities import References from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords +from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model import ( + YamlElectricityEnergyUsageModel, + YamlEnergyUsageModelCompressor, + YamlEnergyUsageModelCompressorSystem, + YamlEnergyUsageModelCompressorTrainMultipleStreams, + YamlEnergyUsageModelDirect, + YamlEnergyUsageModelPump, + YamlEnergyUsageModelPumpSystem, + YamlEnergyUsageModelTabulated, + YamlFuelEnergyUsageModel, +) +from libecalc.presentation.yaml.yaml_types.components.yaml_expression_type import YamlExpressionType +from libecalc.presentation.yaml.yaml_types.yaml_temporal_model import YamlTemporalModel def _handle_condition_list(conditions: List[str]): @@ -38,25 +48,17 @@ def _handle_condition_list(conditions: List[str]): return " {*} ".join(conditions_with_parentheses) -def _map_condition(energy_usage_model: dict) -> Optional[Union[str, int, float]]: - if EcalcYamlKeywords.condition in energy_usage_model and EcalcYamlKeywords.conditions in energy_usage_model: - raise DataValidationError( - data=energy_usage_model, - message=f"Either {EcalcYamlKeywords.condition} or {EcalcYamlKeywords.conditions}" - f" should be specified, not both.", - ) +class ConditionedModel(Protocol): + condition: YamlExpressionType + conditions: List[YamlExpressionType] - if EcalcYamlKeywords.condition in energy_usage_model: - condition_value = energy_usage_model.get(EcalcYamlKeywords.condition) - if isinstance(condition_value, list): - logger.warning( - f"Usage of list with '{EcalcYamlKeywords.condition}'" - f" keyword is deprecated. Use '{EcalcYamlKeywords.conditions}' instead." - ) - return _handle_condition_list(condition_value) + +def _map_condition(energy_usage_model: ConditionedModel) -> Optional[Union[str, int, float]]: + if energy_usage_model.condition: + condition_value = energy_usage_model.condition return condition_value - elif EcalcYamlKeywords.conditions in energy_usage_model: - return _handle_condition_list(energy_usage_model.get(EcalcYamlKeywords.conditions, [])) + elif energy_usage_model.conditions: + return _handle_condition_list(energy_usage_model.conditions) else: return None @@ -92,19 +94,19 @@ def _get_compressor_train_energy_usage_type( def _compressor_system_mapper( - energy_usage_model: Dict, references: References = None + energy_usage_model: YamlEnergyUsageModelCompressorSystem, references: References = None ) -> CompressorSystemConsumerFunction: compressors = [] compressor_power_usage_type = set() - for compressor in energy_usage_model.get(EcalcYamlKeywords.compressor_system_compressors, []): + for compressor in energy_usage_model.compressors: compressor_train = resolve_reference( - value=compressor.get(EcalcYamlKeywords.compressor_system_compressor_sampled_data), + value=compressor.compressor_model, references=references.models, ) compressors.append( CompressorSystemCompressor( - name=compressor.get(EcalcYamlKeywords.name), + name=compressor.name, compressor_train=compressor_train, ) ) @@ -124,101 +126,84 @@ def _compressor_system_mapper( return CompressorSystemConsumerFunction( energy_usage_type=energy_usage_type, compressors=compressors, - power_loss_factor=energy_usage_model.get(EcalcYamlKeywords.power_loss_factor), + power_loss_factor=energy_usage_model.power_loss_factor, condition=_map_condition(energy_usage_model), - total_system_rate=energy_usage_model.get(EcalcYamlKeywords.consumer_system_total_system_rate), + total_system_rate=energy_usage_model.total_system_rate, operational_settings=[ CompressorSystemOperationalSetting( - rates=operational_setting.get(EcalcYamlKeywords.consumer_system_operational_settings_rates), - rate_fractions=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_rate_fractions - ), - suction_pressure=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_suction_pressure - ), - suction_pressures=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_suction_pressures - ), - discharge_pressure=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_discharge_pressure - ), - discharge_pressures=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_discharge_pressures - ), - crossover=operational_setting.get(EcalcYamlKeywords.consumer_system_operational_settings_crossover), + rates=operational_setting.rates, + rate_fractions=operational_setting.rate_fractions, + suction_pressure=operational_setting.suction_pressure, + suction_pressures=operational_setting.suction_pressures, + discharge_pressure=operational_setting.discharge_pressure, + discharge_pressures=operational_setting.discharge_pressures, + crossover=operational_setting.crossover, ) - for operational_setting in energy_usage_model.get(EcalcYamlKeywords.consumer_system_operational_settings) + for operational_setting in energy_usage_model.operational_settings ], ) -def _pump_system_mapper(energy_usage_model: Dict, references: References = None) -> PumpSystemConsumerFunction: +def _pump_system_mapper( + energy_usage_model: YamlEnergyUsageModelPumpSystem, references: References = None +) -> PumpSystemConsumerFunction: """Remove references from pump system and map yaml to DTO :param energy_usage_model: dict representing PumpSystem :return: """ pumps = [] - for pump in energy_usage_model.get(EcalcYamlKeywords.pump_system_pumps, []): + for pump in energy_usage_model.pumps: pump_model = resolve_reference( - pump.get(EcalcYamlKeywords.pump_system_pump_model), + pump.chart, references=references.models, ) - pumps.append(PumpSystemPump(name=pump.get(EcalcYamlKeywords.name), pump_model=pump_model)) + pumps.append(PumpSystemPump(name=pump.name, pump_model=pump_model)) return PumpSystemConsumerFunction( - power_loss_factor=energy_usage_model.get(EcalcYamlKeywords.power_loss_factor), + power_loss_factor=energy_usage_model.power_loss_factor, condition=_map_condition(energy_usage_model), pumps=pumps, - fluid_density=energy_usage_model.get(EcalcYamlKeywords.pump_system_fluid_density), - total_system_rate=energy_usage_model.get(EcalcYamlKeywords.consumer_system_total_system_rate), + fluid_density=energy_usage_model.fluid_density, + total_system_rate=energy_usage_model.total_system_rate, operational_settings=[ PumpSystemOperationalSetting( - fluid_densities=operational_setting.get( - EcalcYamlKeywords.pump_system_operational_settings_fluid_densities - ), - rates=operational_setting.get(EcalcYamlKeywords.consumer_system_operational_settings_rates), - rate_fractions=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_rate_fractions - ), - suction_pressure=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_suction_pressure - ), - suction_pressures=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_suction_pressures - ), - discharge_pressure=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_discharge_pressure - ), - discharge_pressures=operational_setting.get( - EcalcYamlKeywords.consumer_system_operational_settings_discharge_pressures - ), - crossover=operational_setting.get(EcalcYamlKeywords.consumer_system_operational_settings_crossover), + fluid_densities=operational_setting.fluid_densities, + rates=operational_setting.rates, + rate_fractions=operational_setting.rate_fractions, + suction_pressure=operational_setting.suction_pressure, + suction_pressures=operational_setting.suction_pressures, + discharge_pressure=operational_setting.discharge_pressure, + discharge_pressures=operational_setting.discharge_pressures, + crossover=operational_setting.crossover, ) - for operational_setting in energy_usage_model.get(EcalcYamlKeywords.consumer_system_operational_settings) + for operational_setting in energy_usage_model.operational_settings ], ) -def _direct_mapper(energy_usage_model: Dict, references: References = None) -> DirectConsumerFunction: +def _direct_mapper( + energy_usage_model: YamlEnergyUsageModelDirect, references: References = None +) -> DirectConsumerFunction: """Change type to match DTOs, then pass the dict on to DTO to automatically create the correct DTO. :param energy_usage_model: :return: """ - is_power_consumer = EcalcYamlKeywords.load in energy_usage_model + is_power_consumer = energy_usage_model.load is not None return DirectConsumerFunction( energy_usage_type=EnergyUsageType.POWER if is_power_consumer else EnergyUsageType.FUEL, - load=energy_usage_model.get(EcalcYamlKeywords.load), - fuel_rate=energy_usage_model.get(EcalcYamlKeywords.fuel_rate), + load=energy_usage_model.load, + fuel_rate=energy_usage_model.fuel_rate, condition=_map_condition(energy_usage_model), - power_loss_factor=energy_usage_model.get(EcalcYamlKeywords.power_loss_factor), - consumption_rate_type=energy_usage_model.get(EcalcYamlKeywords.direct_consumer_consumption_rate_type) - or RateType.STREAM_DAY, + power_loss_factor=energy_usage_model.power_loss_factor, + consumption_rate_type=energy_usage_model.consumption_rate_type or RateType.STREAM_DAY, ) -def _tabulated_mapper(energy_usage_model: Dict, references: References = None) -> TabulatedConsumerFunction: +def _tabulated_mapper( + energy_usage_model: YamlEnergyUsageModelTabulated, references: References = None +) -> TabulatedConsumerFunction: energy_model = resolve_reference( - energy_usage_model.get(EcalcYamlKeywords.energy_model), + energy_usage_model.energy_function, references.models, ) return TabulatedConsumerFunction( @@ -226,48 +211,46 @@ def _tabulated_mapper(energy_usage_model: Dict, references: References = None) - if EnergyUsageType.POWER.value in energy_model.headers else EnergyUsageType.FUEL, condition=_map_condition(energy_usage_model), - power_loss_factor=energy_usage_model.get(EcalcYamlKeywords.power_loss_factor), + power_loss_factor=energy_usage_model.power_loss_factor, model=energy_model, variables=[ Variables( - name=variable.get(EcalcYamlKeywords.name), - expression=variable.get(EcalcYamlKeywords.variable_expression), + name=variable.name, + expression=variable.expression, ) - for variable in energy_usage_model.get(EcalcYamlKeywords.variables, []) + for variable in energy_usage_model.variables ], ) -def _pump_mapper(energy_usage_model: Dict, references: References = None) -> PumpConsumerFunction: +def _pump_mapper(energy_usage_model: YamlEnergyUsageModelPump, references: References = None) -> PumpConsumerFunction: energy_model = resolve_reference( - energy_usage_model.get(EcalcYamlKeywords.energy_model), + energy_usage_model.energy_function, references=references.models, ) return PumpConsumerFunction( - power_loss_factor=energy_usage_model.get(EcalcYamlKeywords.power_loss_factor), + power_loss_factor=energy_usage_model.power_loss_factor, condition=_map_condition(energy_usage_model), - rate_standard_m3_day=energy_usage_model.get(EcalcYamlKeywords.consumer_function_rate), - suction_pressure=energy_usage_model.get(EcalcYamlKeywords.consumer_function_suction_pressure), - discharge_pressure=energy_usage_model.get(EcalcYamlKeywords.consumer_function_discharge_pressure), - fluid_density=energy_usage_model.get(EcalcYamlKeywords.pump_function_fluid_density), + rate_standard_m3_day=energy_usage_model.rate, + suction_pressure=energy_usage_model.suction_pressure, + discharge_pressure=energy_usage_model.discharge_pressure, + fluid_density=energy_usage_model.fluid_density, model=energy_model, ) def _variable_speed_compressor_train_multiple_streams_and_pressures_mapper( - energy_usage_model: Dict, references: References = None + energy_usage_model: YamlEnergyUsageModelCompressorTrainMultipleStreams, references: References = None ) -> CompressorConsumerFunction: compressor_train_model = resolve_reference( - energy_usage_model.get(EcalcYamlKeywords.models_type_compressor_train_compressor_train_model), + energy_usage_model.compressor_train_model, references=references.models, ) - rates_per_stream_config = energy_usage_model.get(EcalcYamlKeywords.models_type_compressor_train_rate_per_stream) rates_per_stream = [ - Expression.setup_from_expression(value=rate_expression) for rate_expression in rates_per_stream_config + Expression.setup_from_expression(value=rate_expression) + for rate_expression in energy_usage_model.rate_per_stream ] - interstage_control_pressure_config = energy_usage_model.get( - EcalcYamlKeywords.models_type_compressor_train_interstage_control_pressure - ) + interstage_control_pressure_config = energy_usage_model.interstage_control_pressure if isinstance(compressor_train_model, CompressorWithTurbine): require_interstage_pressure_variable_expression = ( compressor_train_model.compressor_train.has_interstage_pressure @@ -279,13 +262,13 @@ def _variable_speed_compressor_train_multiple_streams_and_pressures_mapper( if require_interstage_pressure_variable_expression and interstage_control_pressure_config is None: raise ValueError( f"Energy model" - f" {energy_usage_model.get(EcalcYamlKeywords.models_type_compressor_train_compressor_train_model)}" + f" {energy_usage_model.compressor_train_model}" f" requires {EcalcYamlKeywords.models_type_compressor_train_interstage_control_pressure} to be defined" ) elif not require_interstage_pressure_variable_expression and interstage_control_pressure_config is not None: raise ValueError( f"Energy model" - f" {energy_usage_model.get(EcalcYamlKeywords.models_type_compressor_train_compressor_train_model)}" + f" {energy_usage_model.compressor_train_model}" f" does not accept {EcalcYamlKeywords.models_type_compressor_train_interstage_control_pressure}" f" to be defined" ) @@ -297,23 +280,23 @@ def _variable_speed_compressor_train_multiple_streams_and_pressures_mapper( ) return CompressorConsumerFunction( energy_usage_type=_get_compressor_train_energy_usage_type(compressor_train_model), - power_loss_factor=energy_usage_model.get(EcalcYamlKeywords.power_loss_factor), + power_loss_factor=energy_usage_model.power_loss_factor, condition=_map_condition(energy_usage_model), rate_standard_m3_day=rates_per_stream, suction_pressure=Expression.setup_from_expression( - value=energy_usage_model.get(EcalcYamlKeywords.consumer_function_suction_pressure) - ), - discharge_pressure=Expression.setup_from_expression( - value=energy_usage_model.get(EcalcYamlKeywords.consumer_function_discharge_pressure) + value=energy_usage_model.suction_pressure, ), + discharge_pressure=Expression.setup_from_expression(value=energy_usage_model.discharge_pressure), model=compressor_train_model, interstage_control_pressure=interstage_control_pressure, ) -def _compressor_mapper(energy_usage_model: Dict, references: References = None) -> CompressorConsumerFunction: +def _compressor_mapper( + energy_usage_model: YamlEnergyUsageModelCompressor, references: References = None +) -> CompressorConsumerFunction: energy_model = resolve_reference( - energy_usage_model.get(EcalcYamlKeywords.energy_model), + energy_usage_model.energy_function, references=references.models, ) @@ -321,11 +304,11 @@ def _compressor_mapper(energy_usage_model: Dict, references: References = None) return CompressorConsumerFunction( energy_usage_type=compressor_train_energy_usage_type, - power_loss_factor=energy_usage_model.get(EcalcYamlKeywords.power_loss_factor), + power_loss_factor=energy_usage_model.power_loss_factor, condition=_map_condition(energy_usage_model), - rate_standard_m3_day=energy_usage_model.get(EcalcYamlKeywords.consumer_function_rate), - suction_pressure=energy_usage_model.get(EcalcYamlKeywords.consumer_function_suction_pressure), - discharge_pressure=energy_usage_model.get(EcalcYamlKeywords.consumer_function_discharge_pressure), + rate_standard_m3_day=energy_usage_model.rate, + suction_pressure=energy_usage_model.suction_pressure, + discharge_pressure=energy_usage_model.discharge_pressure, model=energy_model, ) @@ -347,13 +330,22 @@ def __init__(self, references: References, target_period: Period): self._target_period = target_period @staticmethod - def create_model(model: Dict, references: References = None): - model_creator = _consumer_function_mapper.get(model[EcalcYamlKeywords.type]) + def create_model( + model: Union[YamlFuelEnergyUsageModel, YamlElectricityEnergyUsageModel], + references: References = None, + ): + model_creator = _consumer_function_mapper.get(model.type) if model_creator is None: - raise ValueError(f"Unknown model type: {model.get(EcalcYamlKeywords.type)}") + raise ValueError(f"Unknown model type: {model.type}") return model_creator(model, references) - def from_yaml_to_dto(self, data: EnergyModel) -> Optional[Dict[datetime, ConsumerFunction]]: + def from_yaml_to_dto( + self, + data: Union[ + YamlTemporalModel[YamlFuelEnergyUsageModel], + YamlTemporalModel[YamlElectricityEnergyUsageModel], + ], + ) -> Optional[Dict[datetime, ConsumerFunction]]: if data is None: return None diff --git a/src/libecalc/presentation/yaml/mappers/create_references.py b/src/libecalc/presentation/yaml/mappers/create_references.py index d39fe5dc22..6415b595c3 100644 --- a/src/libecalc/presentation/yaml/mappers/create_references.py +++ b/src/libecalc/presentation/yaml/mappers/create_references.py @@ -1,8 +1,9 @@ -from typing import Dict, List +from typing import Dict, Iterable, List, Protocol, Union from libecalc.common.errors.exceptions import EcalcError from libecalc.common.logger import logger from libecalc.common.string.string_utils import get_duplicates +from libecalc.dto import EnergyModel from libecalc.presentation.yaml.energy_model_validation import ( validate_energy_usage_models, ) @@ -12,10 +13,14 @@ from libecalc.presentation.yaml.resource import Resources from libecalc.presentation.yaml.yaml_entities import References from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords -from libecalc.presentation.yaml.yaml_models.pyyaml_yaml_model import PyYamlYamlModel +from libecalc.presentation.yaml.yaml_models.yaml_model import YamlValidator +from libecalc.presentation.yaml.yaml_types.components.legacy.yaml_electricity_consumer import YamlElectricityConsumer +from libecalc.presentation.yaml.yaml_types.components.legacy.yaml_fuel_consumer import YamlFuelConsumer +from libecalc.presentation.yaml.yaml_types.components.system.yaml_consumer_system import YamlConsumerSystem +from libecalc.presentation.yaml.yaml_types.models import YamlConsumerModel -def create_references(configuration: PyYamlYamlModel, resources: Resources) -> References: +def create_references(configuration: YamlValidator, resources: Resources) -> References: """Create references-lookup used throughout the yaml. :param resources: list of resources containing data for the FILE reference in facility input @@ -24,18 +29,16 @@ def create_references(configuration: PyYamlYamlModel, resources: Resources) -> R """ facility_input_mapper = FacilityInputMapper(resources=resources) facility_inputs_from_files = { - facility_input.get(EcalcYamlKeywords.name): facility_input_mapper.from_yaml_to_dto(facility_input) - for facility_input in configuration.facility_inputs_raise_if_invalid + facility_input.name: facility_input_mapper.from_yaml_to_dto(facility_input) + for facility_input in configuration.facility_inputs } models = create_model_references( - models_yaml_config=configuration.models_raise_if_invalid, + models_yaml_config=configuration.models, facility_inputs=facility_inputs_from_files, resources=resources, ) - duplicated_fuel_names = get_duplicates( - [fuel_data.get(EcalcYamlKeywords.name) for fuel_data in configuration.fuel_types] - ) + duplicated_fuel_names = get_duplicates([fuel_data.name for fuel_data in configuration.fuel_types]) if len(duplicated_fuel_names) > 0: raise EcalcError( @@ -44,12 +47,12 @@ def create_references(configuration: PyYamlYamlModel, resources: Resources) -> R f" Duplicated names are: {', '.join(duplicated_fuel_names)}", ) - fuel_types_emissions = [list(fuel_data[EcalcYamlKeywords.emissions]) for fuel_data in configuration.fuel_types] + fuel_types_emissions = [fuel_data.emissions for fuel_data in configuration.fuel_types] # Check each fuel for duplicated emissions duplicated_emissions = [] for emissions in fuel_types_emissions: - duplicated_emissions.append(get_duplicates([emission.get(EcalcYamlKeywords.name) for emission in emissions])) + duplicated_emissions.append(get_duplicates([emission.name for emission in emissions])) duplicated_emissions_names = ",".join(name for string in duplicated_emissions for name in string if len(string) > 0) @@ -60,26 +63,21 @@ def create_references(configuration: PyYamlYamlModel, resources: Resources) -> R f"Duplicated names are: {duplicated_emissions_names}", ) - fuel_types = { - fuel_data.get(EcalcYamlKeywords.name): FuelMapper.from_yaml_to_dto(fuel_data) - for fuel_data in configuration.fuel_types - } + fuel_types = {fuel_data.name: FuelMapper.from_yaml_to_dto(fuel_data) for fuel_data in configuration.fuel_types} + # TODO: Move this validation to installation consumers_installations = [] for installation in configuration.installations: - if installation.get(EcalcYamlKeywords.generator_sets) is not None: + if installation.generator_sets is not None: consumers_installations.append( - [ - consumer - for consumers in installation[EcalcYamlKeywords.generator_sets] - for consumer in consumers[EcalcYamlKeywords.consumers] - ] + [consumer for generator_set in installation.generator_sets for consumer in generator_set.consumers] ) - if installation.get(EcalcYamlKeywords.fuel_consumers) is not None: - consumers_installations.append(list(installation[EcalcYamlKeywords.fuel_consumers])) + if installation.fuel_consumers is not None: + consumers_installations.append(installation.fuel_consumers) check_multiple_energy_models(consumers_installations) + # TODO: End move to installation return References( models=models, @@ -87,7 +85,9 @@ def create_references(configuration: PyYamlYamlModel, resources: Resources) -> R ) -def check_multiple_energy_models(consumers_installations: List[List[Dict]]): +def check_multiple_energy_models( + consumers_installations: List[List[Union[YamlFuelConsumer, YamlConsumerSystem, YamlElectricityConsumer]]], +): """ Check for different energy model types within one consumer. Raises value error if different energy model types found within one consumer. @@ -102,20 +102,23 @@ def check_multiple_energy_models(consumers_installations: List[List[Dict]]): for consumer in consumers: # Check if key exists: ENERGY_USAGE_MODEL. # Consumer system v2 has different structure/naming: test fails when looking for key ENERGY_USAGE_MODEL - if EcalcYamlKeywords.energy_usage_model in consumer: - model = consumer[EcalcYamlKeywords.energy_usage_model] - if isinstance(model, dict): - validate_energy_usage_models(model, consumer[EcalcYamlKeywords.name]) + if isinstance(consumer, (YamlFuelConsumer, YamlElectricityConsumer)): + model = consumer.energy_usage_model + validate_energy_usage_models(model, consumer.name) -def create_model_references(models_yaml_config, facility_inputs: Dict, resources: Resources): +def create_model_references( + models_yaml_config: Iterable[YamlConsumerModel], + facility_inputs: Dict[str, EnergyModel], + resources: Resources, +): sorted_models = _sort_models(models_yaml_config) models_map = facility_inputs model_mapper = ModelMapper(resources=resources) for model in sorted_models: - model_reference = model.get(EcalcYamlKeywords.name) + model_reference = model.name if model_reference in models_map: raise EcalcError( title="Duplicate reference", @@ -142,18 +145,20 @@ def create_model_references(models_yaml_config, facility_inputs: Dict, resources } -def _model_parsing_order(model) -> int: - model_type = model.get(EcalcYamlKeywords.type) +class SortableModel(Protocol): + name: str + type: str + + +def _model_parsing_order(model: SortableModel) -> int: + model_type = model.type try: return _model_parsing_order_map[model_type] except KeyError as e: - lines_reference = f"Check lines {model.start_mark.line} to {model.end_mark.line} in Yaml-file." - none_msg = f"Have you remembered to include the {EcalcYamlKeywords.type} keyword?" if model_type is None else "" - msg = f"{model[EcalcYamlKeywords.name]}:\nUnknown model type {model_type}. {none_msg}\n{lines_reference}" - + msg = f"{model.name}:\nUnknown model type {model_type}." logger.exception(msg + f": {e}") raise EcalcError(title="Invalid model", message=msg) from e -def _sort_models(models): +def _sort_models(models: Iterable[SortableModel]): return sorted(models, key=_model_parsing_order) diff --git a/src/libecalc/presentation/yaml/mappers/facility_input.py b/src/libecalc/presentation/yaml/mappers/facility_input.py index be357e1f37..dbfbd2fcdd 100644 --- a/src/libecalc/presentation/yaml/mappers/facility_input.py +++ b/src/libecalc/presentation/yaml/mappers/facility_input.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Union +from typing import List, Optional, Union from pydantic import TypeAdapter, ValidationError @@ -10,12 +10,12 @@ from libecalc.dto import CompressorSampled as CompressorTrainSampledDTO from libecalc.dto import EnergyModel, GeneratorSetSampled, PumpModel, TabulatedData from libecalc.presentation.yaml.mappers.utils import ( + YAML_UNIT_MAPPING, chart_curves_as_resource_to_dto_format, convert_efficiency_to_fraction, convert_head_to_joule_per_kg, convert_rate_to_am3_per_hour, get_single_speed_chart_data, - get_units_from_chart_config, ) from libecalc.presentation.yaml.resource import Resource, Resources from libecalc.presentation.yaml.validation_errors import ( @@ -25,6 +25,14 @@ ValidationValueError, ) from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords +from libecalc.presentation.yaml.yaml_types.facility_model.yaml_facility_model import ( + YamlCompressorTabularModel, + YamlFacilityAdjustment, + YamlFacilityModel, + YamlFacilityModelBase, + YamlPumpChartSingleSpeed, + YamlPumpChartVariableSpeed, +) # Used here to make pydantic understand which object to instantiate. EnergyModelUnionType = Union[GeneratorSetSampled, TabulatedData, CompressorTrainSampledDTO] @@ -40,12 +48,16 @@ PUMP_CHART_TYPES = [ChartType.SINGLE_SPEED, ChartType.VARIABLE_SPEED] -def _get_adjustment_constant(data: Dict) -> float: - return data.get(EcalcYamlKeywords.facility_adjustment, {}).get(EcalcYamlKeywords.facility_adjustment_constant) or 0 +def _get_adjustment_constant(data: YamlFacilityModelBase) -> float: + if data.adjustment is None: + return YamlFacilityAdjustment().constant + return data.adjustment.constant -def _get_adjustment_factor(data: Dict) -> float: - return data.get(EcalcYamlKeywords.facility_adjustment, {}).get(EcalcYamlKeywords.facility_adjustment_factor) or 1 +def _get_adjustment_factor(data: YamlFacilityModelBase) -> float: + if data.adjustment is None: + return YamlFacilityAdjustment().factor + return data.adjustment.factor def _get_column_or_none(resource: Resource, header: str) -> Optional[List[Union[float, int, str]]]: @@ -56,7 +68,7 @@ def _get_column_or_none(resource: Resource, header: str) -> Optional[List[Union[ def _create_compressor_train_sampled_dto_model_data( - resource: Resource, facility_data, **kwargs + resource: Resource, facility_data: YamlCompressorTabularModel, **kwargs ) -> CompressorTrainSampledDTO: # kwargs just to allow this to be used with _default_facility_to_dto_model_data which needs type until we have # replaced _default_facility_to_dto_model_data and have separate functions for all types @@ -94,9 +106,10 @@ def _create_compressor_train_sampled_dto_model_data( ) -def _create_pump_model_single_speed_dto_model_data(resource: Resource, facility_data, **kwargs) -> PumpModel: - units = get_units_from_chart_config(chart_config=facility_data) - resource_name = facility_data.get(EcalcYamlKeywords.file) +def _create_pump_model_single_speed_dto_model_data( + resource: Resource, facility_data: YamlPumpChartSingleSpeed, **kwargs +) -> PumpModel: + resource_name = facility_data.file chart_data = get_single_speed_chart_data(resource=resource, resource_name=resource_name) @@ -104,17 +117,17 @@ def _create_pump_model_single_speed_dto_model_data(resource: Resource, facility_ speed_rpm=chart_data.speed, efficiency_fraction=convert_efficiency_to_fraction( efficiency_values=chart_data.efficiency, - input_unit=units[EcalcYamlKeywords.consumer_chart_efficiency], + input_unit=YAML_UNIT_MAPPING[facility_data.units.efficiency], ), rate_actual_m3_hour=convert_rate_to_am3_per_hour( - rate_values=chart_data.rate, input_unit=units[EcalcYamlKeywords.consumer_chart_rate] + rate_values=chart_data.rate, input_unit=YAML_UNIT_MAPPING[facility_data.units.rate] ), polytropic_head_joule_per_kg=convert_head_to_joule_per_kg( - head_values=chart_data.head, input_unit=units[EcalcYamlKeywords.consumer_chart_head] + head_values=chart_data.head, input_unit=YAML_UNIT_MAPPING[facility_data.units.head] ), ) - head_margin = facility_data.get(EcalcYamlKeywords.pump_system_head_margin, 0.0) + head_margin = facility_data.head_margin return PumpModel( chart=chart, @@ -124,30 +137,32 @@ def _create_pump_model_single_speed_dto_model_data(resource: Resource, facility_ ) -def _create_pump_chart_variable_speed_dto_model_data(resource: Resource, facility_data, **kwargs) -> PumpModel: - units = get_units_from_chart_config(chart_config=facility_data) - - resource_name = facility_data.get(EcalcYamlKeywords.file) +def _create_pump_chart_variable_speed_dto_model_data( + resource: Resource, facility_data: YamlPumpChartVariableSpeed, **kwargs +) -> PumpModel: + resource_name = facility_data.file curves_data = chart_curves_as_resource_to_dto_format(resource=resource, resource_name=resource_name) curves: List[ChartCurveDTO] = [ ChartCurveDTO( speed_rpm=curve["speed"], rate_actual_m3_hour=convert_rate_to_am3_per_hour( - rate_values=curve["rate"], input_unit=units[EcalcYamlKeywords.consumer_chart_rate] + rate_values=curve["rate"], + input_unit=YAML_UNIT_MAPPING[facility_data.units.rate], ), polytropic_head_joule_per_kg=convert_head_to_joule_per_kg( - head_values=curve["head"], input_unit=units[EcalcYamlKeywords.consumer_chart_head] + head_values=curve["head"], + input_unit=YAML_UNIT_MAPPING[facility_data.units.head], ), efficiency_fraction=convert_efficiency_to_fraction( efficiency_values=curve["efficiency"], - input_unit=units[EcalcYamlKeywords.consumer_chart_efficiency], + input_unit=YAML_UNIT_MAPPING[facility_data.units.efficiency], ), ) for curve in curves_data ] - head_margin = facility_data.get(EcalcYamlKeywords.pump_system_head_margin, 0.0) + head_margin = facility_data.head_margin return PumpModel( chart=VariableSpeedChartDTO(curves=curves), @@ -158,7 +173,7 @@ def _create_pump_chart_variable_speed_dto_model_data(resource: Resource, facilit def _default_facility_to_dto_model_data( - resource: Resource, typ: EnergyModelType, facility_data: Dict + resource: Resource, typ: EnergyModelType, facility_data: YamlFacilityModelBase ) -> EnergyModelUnionType: resource_headers = resource.get_headers() resource_data = [resource.get_column(header) for header in resource_headers] @@ -185,28 +200,18 @@ class FacilityInputMapper: def __init__(self, resources: Resources): self.__resources = resources - def from_yaml_to_dto(self, data: Dict) -> EnergyModel: - resource_name = data.get(EcalcYamlKeywords.file) - resource = self.__resources.get(data.get(EcalcYamlKeywords.file)) + def from_yaml_to_dto(self, data: YamlFacilityModel) -> EnergyModel: + resource = self.__resources.get(data.file) if resource is None: - # This should have been validated elsewhere. - if resource_name is None: - raise DataValidationError( - data, - message=f"Missing file in '{data.get(EcalcYamlKeywords.name)}'", - error_key=EcalcYamlKeywords.file, - dump_flow_style=DumpFlowStyle.BLOCK, - ) - raise DataValidationError( - data, - f"Unable to find resource '{data.get(EcalcYamlKeywords.file)}'", + data.model_dump(), + f"Unable to find resource '{data.file}'", error_key=EcalcYamlKeywords.file, dump_flow_style=DumpFlowStyle.BLOCK, ) - typ = energy_model_type_map.get(data.get(EcalcYamlKeywords.type)) + typ = energy_model_type_map.get(data.type) try: return facility_input_to_dto_map.get(typ, _default_facility_to_dto_model_data)( @@ -215,19 +220,19 @@ def from_yaml_to_dto(self, data: Dict) -> EnergyModel: facility_data=data, ) except ValidationError as ve: - raise DtoValidationError(data=data, validation_error=ve) from ve + raise DtoValidationError(data=data.model_dump(), validation_error=ve) from ve except ValidationValueError as vve: raise DataValidationError( - data=data, + data=data.model_dump(), message=str(vve), error_key=vve.key, dump_flow_style=DumpFlowStyle.BLOCK, ) from vve except InvalidResourceException as e: - message = f"Invalid resource '{resource_name}'. Reason: {str(e)}" + message = f"Invalid resource '{data.file}'. Reason: {str(e)}" raise DataValidationError( - data=data, + data=data.model_dump(), message=message, error_key=EcalcYamlKeywords.file, dump_flow_style=DumpFlowStyle.BLOCK, diff --git a/src/libecalc/presentation/yaml/mappers/fluid_mapper.py b/src/libecalc/presentation/yaml/mappers/fluid_mapper.py index 42eadd303b..4b2efffeba 100644 --- a/src/libecalc/presentation/yaml/mappers/fluid_mapper.py +++ b/src/libecalc/presentation/yaml/mappers/fluid_mapper.py @@ -1,10 +1,11 @@ from typing import Any, Dict -from libecalc.common.logger import logger from libecalc.dto import FluidComposition, FluidModel from libecalc.dto.types import EoSModel from libecalc.presentation.yaml.resource import Resources from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords +from libecalc.presentation.yaml.yaml_types.models import YamlFluidModel +from libecalc.presentation.yaml.yaml_types.models.yaml_fluid import YamlCompositionFluidModel, YamlPredefinedFluidModel """ Some "standard" predefined compositions to choose from @@ -13,7 +14,6 @@ used for increasing molecular weight. """ - ULTRA_DRY_MW_17P1 = FluidComposition( nitrogen=1.140841432, CO2=0.607605069, @@ -91,17 +91,17 @@ } -def fluid_model_mapper(model_config: Dict, input_models: Dict[str, Any], resources: Resources): - fluid_model_type = model_config.get(EcalcYamlKeywords.models_type_fluid_model_type) +def fluid_model_mapper(model_config: YamlFluidModel, input_models: Dict[str, Any], resources: Resources): + fluid_model_type = model_config.fluid_model_type mapper = _fluid_model_map.get(fluid_model_type) if mapper is None: raise ValueError(f"Fluid model type {fluid_model_type} not supported") return mapper(model_config=model_config) -def _predefined_fluid_model_mapper(model_config: Dict) -> FluidModel: - predefined_composition_type = model_config.get(EcalcYamlKeywords.models_type_fluid_predefined_gas_type) - eos_model_type = model_config.get(EcalcYamlKeywords.models_type_fluid_eos_model) +def _predefined_fluid_model_mapper(model_config: YamlPredefinedFluidModel) -> FluidModel: + predefined_composition_type = model_config.gas_type + eos_model_type = model_config.eos_model return FluidModel( eos_model=_eos_model_mapper.get(eos_model_type), composition=_predefined_fluid_composition_mapper.get(predefined_composition_type), @@ -109,36 +109,27 @@ def _predefined_fluid_model_mapper(model_config: Dict) -> FluidModel: def _composition_fluid_model_mapper( - model_config: Dict, + model_config: YamlCompositionFluidModel, ) -> FluidModel: - user_defined_composition = model_config.get(EcalcYamlKeywords.composition) + user_defined_composition = model_config.composition if user_defined_composition is None: raise ValueError("User defined composition not found in Yaml keywords") - if EcalcYamlKeywords.composition_H2O in user_defined_composition: - """ - This is a work to allow both H2O and water to be specified as fluid definition - """ - - # Fixme: Remove in version 9 - logger.warning( - "DeprecationWarning: H2O is deprecated as fluid composition. Use 'water' instead. " - "Will be removed in the next version." - ) - if EcalcYamlKeywords.composition_water in user_defined_composition: - user_defined_composition[EcalcYamlKeywords.composition_water] += user_defined_composition[ - EcalcYamlKeywords.composition_H2O - ] - else: - user_defined_composition[EcalcYamlKeywords.composition_water] = user_defined_composition[ - EcalcYamlKeywords.composition_H2O - ] - del user_defined_composition[ - EcalcYamlKeywords.composition_H2O - ] # Need to remove this in order to put it into the DTO - eos_model_type = model_config.get(EcalcYamlKeywords.models_type_fluid_eos_model) + eos_model_type = model_config.eos_model return FluidModel( eos_model=_eos_model_mapper.get(eos_model_type), - composition=user_defined_composition, + composition=FluidComposition( + water=user_defined_composition.water, + nitrogen=user_defined_composition.nitrogen, + CO2=user_defined_composition.CO2, + methane=user_defined_composition.methane, + ethane=user_defined_composition.ethane, + propane=user_defined_composition.propane, + i_butane=user_defined_composition.i_butane, + n_butane=user_defined_composition.n_butane, + i_pentane=user_defined_composition.i_pentane, + n_pentane=user_defined_composition.n_pentane, + n_hexane=user_defined_composition.n_hexane, + ), ) diff --git a/src/libecalc/presentation/yaml/mappers/fuel_and_emission_mapper.py b/src/libecalc/presentation/yaml/mappers/fuel_and_emission_mapper.py index 008090be69..e6b8289a5e 100644 --- a/src/libecalc/presentation/yaml/mappers/fuel_and_emission_mapper.py +++ b/src/libecalc/presentation/yaml/mappers/fuel_and_emission_mapper.py @@ -1,37 +1,24 @@ -from typing import Dict - from pydantic import ValidationError -from ecalc_cli.logger import logger from libecalc.dto import Emission, FuelType from libecalc.presentation.yaml.validation_errors import DtoValidationError -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords - - -class EmissionMapper: - @staticmethod - def from_yaml_to_dto(data: Dict) -> Emission: - if data.get("TAX") or data.get("QUOTA"): - logger.warning("Emission tax and quota are deprecated. It will have no effect.") - return Emission( - name=data.get(EcalcYamlKeywords.name), - factor=data.get(EcalcYamlKeywords.emission_factor), - ) +from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_fuel_type import YamlFuelType class FuelMapper: @staticmethod - def from_yaml_to_dto(fuel: Dict) -> FuelType: + def from_yaml_to_dto(fuel: YamlFuelType) -> FuelType: try: - if fuel.get("PRICE"): - logger.warning("Fuel price is deprecated. It will have no effect.") - return FuelType( - name=fuel.get(EcalcYamlKeywords.name), - user_defined_category=fuel.get(EcalcYamlKeywords.user_defined_tag), + name=fuel.name, + user_defined_category=fuel.category, emissions=[ - EmissionMapper.from_yaml_to_dto(emission) for emission in fuel.get(EcalcYamlKeywords.emissions, []) + Emission( + name=emission.name, + factor=emission.factor, + ) + for emission in fuel.emissions ], ) except ValidationError as e: - raise DtoValidationError(data=fuel, validation_error=e) from e + raise DtoValidationError(data=fuel.model_dump(), validation_error=e) from e diff --git a/src/libecalc/presentation/yaml/mappers/model.py b/src/libecalc/presentation/yaml/mappers/model.py index 5ea7e8bf8e..4fc7d19695 100644 --- a/src/libecalc/presentation/yaml/mappers/model.py +++ b/src/libecalc/presentation/yaml/mappers/model.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Union, cast from pydantic import ValidationError @@ -32,7 +32,6 @@ convert_rate_to_am3_per_hour, convert_temperature_to_kelvin, get_single_speed_chart_data, - get_units_from_chart_config, resolve_reference, ) from libecalc.presentation.yaml.resource import Resource, Resources @@ -42,34 +41,51 @@ ValidationValueError, ) from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords +from libecalc.presentation.yaml.yaml_types.models import ( + YamlCompressorChart, + YamlCompressorWithTurbine, + YamlConsumerModel, + YamlTurbine, +) +from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_chart import ( + YamlCurve, + YamlGenericFromDesignPointChart, + YamlGenericFromInputChart, + YamlSingleSpeedChart, + YamlVariableSpeedChart, +) +from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_stages import ( + YamlCompressorStageMultipleStreams, + YamlUnknownCompressorStages, +) from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_trains import ( - YamlCompatibleTrainsControlMargin, - YamlCompatibleTrainsPressureDropAheadOfStage, + YamlMultipleStreamsStream, + YamlSimplifiedVariableSpeedCompressorTrain, + YamlSingleSpeedCompressorTrain, + YamlVariableSpeedCompressorTrain, + YamlVariableSpeedCompressorTrainMultipleStreamsAndPressures, ) +from libecalc.presentation.yaml.yaml_types.yaml_data_or_file import YamlFile -def _compressor_chart_mapper(model_config: Dict, input_models: Dict[str, Any], resources: Resources) -> CompressorChart: - chart_type = model_config.get(EcalcYamlKeywords.consumer_chart_type) +def _compressor_chart_mapper( + model_config: YamlCompressorChart, input_models: Dict[str, Any], resources: Resources +) -> CompressorChart: + chart_type = model_config.chart_type mapper = _compressor_chart_map.get(chart_type) if mapper is None: raise ValueError(f"Unknown chart type {chart_type}") return mapper(model_config=model_config, resources=resources) -def _pressure_control_mapper(model_config: Dict) -> FixedSpeedPressureControl: - pressure_control_data = model_config.get( - EcalcYamlKeywords.models_type_compressor_train_pressure_control, - EcalcYamlKeywords.models_type_compressor_train_pressure_control_downstream_choke, - ) - if pressure_control_data == EcalcYamlKeywords.models_type_compressor_train_pressure_control_none: - pressure_control = None - else: - pressure_control = FixedSpeedPressureControl(pressure_control_data) - if pressure_control not in SUPPORTED_PRESSURE_CONTROLS_SINGLE_SPEED_COMPRESSOR_TRAIN: - raise ValueError( - f"Pressure control {pressure_control} not supported, should be one of {', '.join(SUPPORTED_PRESSURE_CONTROLS_SINGLE_SPEED_COMPRESSOR_TRAIN)}" - ) - return pressure_control +def _pressure_control_mapper( + model_config: Union[ + YamlVariableSpeedCompressorTrain, + YamlSingleSpeedCompressorTrain, + YamlVariableSpeedCompressorTrainMultipleStreamsAndPressures, + ], +) -> FixedSpeedPressureControl: + return FixedSpeedPressureControl(model_config.pressure_control.value) # TODO: Could this be none previously? def _get_curve_data_from_resource(resource: Resource, speed: float = 0.0): @@ -81,43 +97,13 @@ def _get_curve_data_from_resource(resource: Resource, speed: float = 0.0): } -def _single_speed_compressor_chart_mapper(model_config: Dict, resources: Resources) -> SingleSpeedChartDTO: - units = get_units_from_chart_config(chart_config=model_config) - curve_config = model_config.get(EcalcYamlKeywords.consumer_chart_curve) - name = model_config.get(EcalcYamlKeywords.name) +def _single_speed_compressor_chart_mapper( + model_config: YamlSingleSpeedChart, resources: Resources +) -> SingleSpeedChartDTO: + curve_config = model_config.curve - # Check if user has used CURVES (reserved for variable speed compressors) - # instead of CURVE (should be used for single speed compressors), - # and give clear error message. - if EcalcYamlKeywords.consumer_chart_curves in model_config: - raise DataValidationError( - data=model_config, - message=f"Compressor model {name}:\n" - f"The keyword {EcalcYamlKeywords.consumer_chart_curves} should only be used for " - f"variable speed compressor models.\n" - f"{name} is a single speed compressor model and should use the keyword " - f"{EcalcYamlKeywords.consumer_chart_curve}.", - ) - - if EcalcYamlKeywords.consumer_chart_curve not in model_config: - raise DataValidationError( - data=model_config, - message=f"The keyword {EcalcYamlKeywords.consumer_chart_curve} is not specified " - f"for the compressor model {name}.\n" - f"{EcalcYamlKeywords.consumer_chart_curve} is a required keyword for " - f"single speed compressor models.", - ) - - if not isinstance(curve_config, dict): - raise DataValidationError( - data=model_config, - message=f"Compressor model {name}:" - f"{EcalcYamlKeywords.consumer_chart_curve}" - f" should be an object. Type given: {type(curve_config)}.", - ) - - if EcalcYamlKeywords.file in curve_config: - resource_name = curve_config.get(EcalcYamlKeywords.file) + if isinstance(curve_config, YamlFile): + resource_name = curve_config.file resource = resources.get(resource_name) chart_data = get_single_speed_chart_data(resource=resource, resource_name=resource_name) @@ -128,86 +114,67 @@ def _single_speed_compressor_chart_mapper(model_config: Dict, resources: Resourc "efficiency": chart_data.efficiency, } else: + curve_config = cast(YamlCurve, curve_config) curve_data = { # Default to speed = 1 unless specified. This does not affect any calculations # but ensures we always have speed to handle charts in a generic way. - "speed": curve_config.get(EcalcYamlKeywords.consumer_chart_speed, 1), - "rate": curve_config.get(EcalcYamlKeywords.consumer_chart_rate), - "head": curve_config.get(EcalcYamlKeywords.consumer_chart_head), - "efficiency": curve_config.get(EcalcYamlKeywords.consumer_chart_efficiency), + "speed": curve_config.speed, + "rate": curve_config.rate, + "head": curve_config.head, + "efficiency": curve_config.efficiency, } return SingleSpeedChartDTO( speed_rpm=curve_data["speed"], rate_actual_m3_hour=convert_rate_to_am3_per_hour( - rate_values=curve_data["rate"], input_unit=units[EcalcYamlKeywords.consumer_chart_rate] + rate_values=curve_data["rate"], input_unit=YAML_UNIT_MAPPING[model_config.units.rate] ), polytropic_head_joule_per_kg=convert_head_to_joule_per_kg( - head_values=curve_data["head"], input_unit=units[EcalcYamlKeywords.consumer_chart_head] + head_values=curve_data["head"], input_unit=YAML_UNIT_MAPPING[model_config.units.head] ), efficiency_fraction=convert_efficiency_to_fraction( efficiency_values=curve_data["efficiency"], - input_unit=units[EcalcYamlKeywords.consumer_chart_efficiency], + input_unit=YAML_UNIT_MAPPING[model_config.units.efficiency], ), ) -def _variable_speed_compressor_chart_mapper(model_config: Dict, resources: Resources) -> VariableSpeedChartDTO: - units = get_units_from_chart_config(chart_config=model_config) - - curve_config = model_config.get(EcalcYamlKeywords.consumer_chart_curves) - - name = model_config.get(EcalcYamlKeywords.name) - - # Check if user has used CURVE (reserved for single speed compressors) - # instead of CURVES (should be used for variable speed compressors), - # and give clear error message. - if EcalcYamlKeywords.consumer_chart_curve in model_config: - raise DataValidationError( - data=model_config, - message=f"Compressor model {name}:\n" - f"The keyword {EcalcYamlKeywords.consumer_chart_curve} should only be used for " - f"single speed compressor models.\n" - f"{name} is a variable speed compressor model and should use the keyword " - f"{EcalcYamlKeywords.consumer_chart_curves}.", - ) +def _variable_speed_compressor_chart_mapper( + model_config: YamlVariableSpeedChart, resources: Resources +) -> VariableSpeedChartDTO: + curve_config = model_config.curves - if EcalcYamlKeywords.consumer_chart_curves not in model_config: - raise DataValidationError( - data=model_config, - message=f"The keyword {EcalcYamlKeywords.consumer_chart_curves} is not specified " - f"for the compressor model {name}.\n" - f"{EcalcYamlKeywords.consumer_chart_curves} is a required keyword for " - f"variable speed compressor models.", - ) - - if isinstance(curve_config, dict) and EcalcYamlKeywords.file in curve_config: - resource_name = curve_config.get(EcalcYamlKeywords.file) + if isinstance(curve_config, YamlFile): + resource_name = curve_config.file resource = resources.get(resource_name) curves_data = chart_curves_as_resource_to_dto_format(resource=resource, resource_name=resource_name) else: + curve_config = cast(List[YamlCurve], curve_config) curves_data = [ { - "speed": curve.get(EcalcYamlKeywords.consumer_chart_speed), - "rate": curve.get(EcalcYamlKeywords.consumer_chart_rate), - "head": curve.get(EcalcYamlKeywords.consumer_chart_head), - "efficiency": curve.get(EcalcYamlKeywords.consumer_chart_efficiency), + "speed": curve.speed, + "rate": curve.rate, + "head": curve.head, + "efficiency": curve.efficiency, } - for curve in model_config.get(EcalcYamlKeywords.consumer_chart_curves) + for curve in curve_config ] + units = model_config.units + curves: List[ChartCurveDTO] = [ ChartCurveDTO( speed_rpm=curve["speed"], rate_actual_m3_hour=convert_rate_to_am3_per_hour( - rate_values=curve["rate"], input_unit=units[EcalcYamlKeywords.consumer_chart_rate] + rate_values=curve["rate"], + input_unit=YAML_UNIT_MAPPING[units.rate], ), polytropic_head_joule_per_kg=convert_head_to_joule_per_kg( - head_values=curve["head"], input_unit=units[EcalcYamlKeywords.consumer_chart_head] + head_values=curve["head"], input_unit=YAML_UNIT_MAPPING[units.head] ), efficiency_fraction=convert_efficiency_to_fraction( efficiency_values=curve["efficiency"], - input_unit=units[EcalcYamlKeywords.consumer_chart_efficiency], + input_unit=YAML_UNIT_MAPPING[units.efficiency], ), ) for curve in curves_data @@ -216,36 +183,37 @@ def _variable_speed_compressor_chart_mapper(model_config: Dict, resources: Resou return VariableSpeedChartDTO(curves=curves) -def _generic_from_input_compressor_chart_mapper(model_config: Dict, resources: Resources) -> GenericChartFromInput: - units = get_units_from_chart_config( - chart_config=model_config, units_to_include=[EcalcYamlKeywords.consumer_chart_efficiency] - ) - polytropic_efficiency = model_config.get(EcalcYamlKeywords.consumer_chart_polytropic_efficiency) +def _generic_from_input_compressor_chart_mapper( + model_config: YamlGenericFromInputChart, resources: Resources +) -> GenericChartFromInput: + units = model_config.units + + polytropic_efficiency = model_config.polytropic_efficiency polytropic_efficiency_fraction = convert_efficiency_to_fraction( efficiency_values=[polytropic_efficiency], - input_unit=units[EcalcYamlKeywords.consumer_chart_efficiency], + input_unit=YAML_UNIT_MAPPING[units.efficiency], )[0] return GenericChartFromInput(polytropic_efficiency_fraction=polytropic_efficiency_fraction) def _generic_from_design_point_compressor_chart_mapper( - model_config: Dict, resources: Resources + model_config: YamlGenericFromDesignPointChart, resources: Resources ) -> GenericChartFromDesignPoint: - units = get_units_from_chart_config(chart_config=model_config) - design_rate = model_config.get(EcalcYamlKeywords.consumer_chart_design_rate) - design_polytropic_head = model_config.get(EcalcYamlKeywords.consumer_chart_design_head) - polytropic_efficiency = model_config.get(EcalcYamlKeywords.consumer_chart_polytropic_efficiency) + design_rate = model_config.design_rate + design_polytropic_head = model_config.design_head + polytropic_efficiency = model_config.polytropic_efficiency + units = model_config.units design_rate_actual_m3_per_hour = convert_rate_to_am3_per_hour( - rate_values=[design_rate], input_unit=units[EcalcYamlKeywords.consumer_chart_rate] + rate_values=[design_rate], input_unit=YAML_UNIT_MAPPING[units.rate] )[0] design_polytropic_head_joule_per_kg = convert_head_to_joule_per_kg( - head_values=[design_polytropic_head], input_unit=units[EcalcYamlKeywords.consumer_chart_head] + head_values=[design_polytropic_head], input_unit=YAML_UNIT_MAPPING[units.head] )[0] polytropic_efficiency_fraction = convert_efficiency_to_fraction( efficiency_values=[polytropic_efficiency], - input_unit=units[EcalcYamlKeywords.consumer_chart_efficiency], + input_unit=YAML_UNIT_MAPPING[units.efficiency], )[0] return GenericChartFromDesignPoint( @@ -283,12 +251,12 @@ def _replace_compressor_chart_with_reference(stage_spec, input_models) -> Dict: def _variable_speed_compressor_train_multiple_streams_and_pressures_stream_mapper( - stream_config: Dict, + stream_config: YamlMultipleStreamsStream, input_models: Dict[str, Any], ) -> MultipleStreamsAndPressureStream: - reference_name = stream_config.get(EcalcYamlKeywords.name) - stream_type = stream_config.get(EcalcYamlKeywords.type) - fluid_model_reference = stream_config.get(EcalcYamlKeywords.models_type_fluid_model) + reference_name = stream_config.name + stream_type = stream_config.type + fluid_model_reference = stream_config.fluid_model if fluid_model_reference is not None: fluid_model = resolve_reference(value=fluid_model_reference, references=input_models) return MultipleStreamsAndPressureStream( @@ -304,24 +272,19 @@ def _variable_speed_compressor_train_multiple_streams_and_pressures_stream_mappe def _variable_speed_compressor_train_multiple_streams_and_pressures_stage_mapper( - stage_config: Dict, + stage_config: YamlCompressorStageMultipleStreams, stream_references: List[str], input_models: Dict[str, Any], ) -> MultipleStreamsCompressorStage: - compressor_chart_reference = stage_config.get(EcalcYamlKeywords.models_type_compressor_train_compressor_chart) + compressor_chart_reference = stage_config.compressor_chart compressor_chart = resolve_reference(value=compressor_chart_reference, references=input_models) inlet_temperature_kelvin = convert_temperature_to_kelvin( - [stage_config.get(EcalcYamlKeywords.models_type_compressor_train_inlet_temperature)], + [stage_config.inlet_temperature], input_unit=Unit.CELSIUS, )[0] - pressure_drop_before_stage = stage_config.get( - EcalcYamlKeywords.models_type_compressor_train_pressure_drop_ahead_of_stage, 0.0 - ) - control_margin = stage_config.get(EcalcYamlKeywords.models_type_compressor_train_stage_control_margin, 0.0) - control_margin_unit = stage_config.get( - EcalcYamlKeywords.models_type_compressor_train_stage_control_margin_unit, - EcalcYamlKeywords.models_type_compressor_train_stage_control_margin_unit_percentage, - ) + pressure_drop_before_stage = stage_config.pressure_drop_ahead_of_stage + control_margin = stage_config.control_margin + control_margin_unit = stage_config.control_margin_unit control_margin_fraction = convert_control_margin_to_fraction( control_margin, YAML_UNIT_MAPPING[control_margin_unit], @@ -334,43 +297,39 @@ def _variable_speed_compressor_train_multiple_streams_and_pressures_stage_mapper "pressure_drop_before_stage": pressure_drop_before_stage, "control_margin": control_margin_fraction, } - stream_references_this_stage = stage_config.get(EcalcYamlKeywords.models_type_compressor_train_stream) + stream_references_this_stage = ( + stage_config.stream + ) # TODO: seems to be a bug if stream is a single string? Should we remove that option? if stream_references_this_stage is not None: stream_reference_not_present = [ stream_ref for stream_ref in stream_references_this_stage if stream_ref not in stream_references ] if any(stream_reference_not_present): raise ValueError(f"Streams {', '.join(stream_reference_not_present)} not properly defined") - mapped_stage.update({"stream_reference": stream_references_this_stage}) - interstage_pressure_control_config = stage_config.get( - EcalcYamlKeywords.models_type_compressor_train_interstage_control_pressure - ) + mapped_stage["stream_reference"] = stream_references_this_stage + interstage_pressure_control_config = stage_config.interstage_control_pressure if interstage_pressure_control_config is not None: interstage_pressure_control = InterstagePressureControl( - upstream_pressure_control=interstage_pressure_control_config.get( - EcalcYamlKeywords.models_type_compressor_train_upstream_pressure_control - ), - downstream_pressure_control=interstage_pressure_control_config.get( - EcalcYamlKeywords.models_type_compressor_train_downstream_pressure_control - ), + upstream_pressure_control=interstage_pressure_control_config.upstream_pressure_control, + downstream_pressure_control=interstage_pressure_control_config.downstream_pressure_control, ) - mapped_stage.update({"interstage_pressure_control": interstage_pressure_control}) + mapped_stage["interstage_pressure_control"] = interstage_pressure_control return MultipleStreamsCompressorStage.model_validate(mapped_stage) def _variable_speed_compressor_train_multiple_streams_and_pressures_mapper( - model_config: Dict, + model_config: YamlVariableSpeedCompressorTrainMultipleStreamsAndPressures, input_models: Dict[str, Any], resources: Resources, ) -> VariableSpeedCompressorTrainMultipleStreamsAndPressures: - streams_config = model_config.get(EcalcYamlKeywords.models_type_compressor_train_streams) + streams_config = model_config.streams streams = [ _variable_speed_compressor_train_multiple_streams_and_pressures_stream_mapper( stream_config, input_models=input_models ) for stream_config in streams_config ] - stages_config = model_config.get(EcalcYamlKeywords.models_type_compressor_train_stages) + stages_config = model_config.stages stages = [ _variable_speed_compressor_train_multiple_streams_and_pressures_stage_mapper( stage_config, stream_references=[stream.name for stream in streams], input_models=input_models @@ -382,11 +341,11 @@ def _variable_speed_compressor_train_multiple_streams_and_pressures_mapper( return VariableSpeedCompressorTrainMultipleStreamsAndPressures( streams=streams, stages=stages, - energy_usage_adjustment_constant=model_config.get(EcalcYamlKeywords.models_power_adjustment_constant_mw, 0), - energy_usage_adjustment_factor=model_config.get(EcalcYamlKeywords.models_power_adjustment_factor_mw, 1), - calculate_max_rate=model_config.get(EcalcYamlKeywords.calculate_max_rate, False), + energy_usage_adjustment_constant=model_config.power_adjustment_constant, + energy_usage_adjustment_factor=model_config.power_adjustment_factor, + calculate_max_rate=False, # TODO: Not supported? pressure_control=pressure_control, - maximum_power=model_config.get(EcalcYamlKeywords.models_maximum_power, None), + maximum_power=model_config.maximum_power, ) @@ -401,59 +360,37 @@ def _variable_speed_compressor_train_multiple_streams_and_pressures_mapper( def _single_speed_compressor_train_mapper( - model_config: Dict, + model_config: YamlSingleSpeedCompressorTrain, input_models: Dict[str, Any], resources: Resources, ) -> SingleSpeedCompressorTrain: - fluid_model_reference: str = model_config.get(EcalcYamlKeywords.models_type_fluid_model) + fluid_model_reference = model_config.fluid_model fluid_model = input_models.get(fluid_model_reference) if fluid_model is None: raise DataValidationError( - data=model_config, message=f"Fluid model reference {fluid_model_reference} not found." + data=model_config.model_dump(), message=f"Fluid model reference {fluid_model_reference} not found." ) - train_spec = model_config.get(EcalcYamlKeywords.models_type_compressor_train) - if train_spec is None: - raise DataValidationError( - data=model_config, - message=f"Missing keyword {EcalcYamlKeywords.models_type_compressor_train}" - f" for {model_config.get(EcalcYamlKeywords.type)} {model_config.get(EcalcYamlKeywords.name)}", - ) - # The stages are pre defined, known - stages_data = train_spec.get(EcalcYamlKeywords.models_type_compressor_train_stages) - if stages_data is None: - raise DataValidationError( - data=model_config, - message=f"Missing keyword {EcalcYamlKeywords.models_type_compressor_train_stages}" - f" for {model_config.get(EcalcYamlKeywords.type)} {model_config.get(EcalcYamlKeywords.name)}", - ) + train_spec = model_config.compressor_train + stages: List[CompressorStage] = [ CompressorStage( - compressor_chart=input_models.get( - stage.get(EcalcYamlKeywords.models_type_compressor_train_compressor_chart) - ), + compressor_chart=input_models.get(stage.compressor_chart), inlet_temperature_kelvin=convert_temperature_to_kelvin( - [stage.get(EcalcYamlKeywords.models_type_compressor_train_inlet_temperature)], + [stage.inlet_temperature], input_unit=Unit.CELSIUS, )[0], remove_liquid_after_cooling=True, - pressure_drop_before_stage=stage.get( - EcalcYamlKeywords.models_type_compressor_train_pressure_drop_ahead_of_stage, 0.0 - ), + pressure_drop_before_stage=stage.pressure_drop_ahead_of_stage, 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, - ) - ], + stage.control_margin, + YAML_UNIT_MAPPING[stage.control_margin_unit], ), ) - for stage in stages_data + for stage in train_spec.stages ] pressure_control = _pressure_control_mapper(model_config) - maximum_discharge_pressure = model_config.get(EcalcYamlKeywords.maximum_discharge_pressure) + maximum_discharge_pressure = model_config.maximum_discharge_pressure if maximum_discharge_pressure and pressure_control != FixedSpeedPressureControl.DOWNSTREAM_CHOKE: raise ValueError( f"Setting maximum discharge pressure for single speed compressor train is currently" @@ -466,68 +403,48 @@ def _single_speed_compressor_train_mapper( stages=stages, pressure_control=pressure_control, maximum_discharge_pressure=maximum_discharge_pressure, - energy_usage_adjustment_constant=model_config.get(EcalcYamlKeywords.models_power_adjustment_constant_mw, 0), - energy_usage_adjustment_factor=model_config.get(EcalcYamlKeywords.models_power_adjustment_factor_mw, 1), - calculate_max_rate=model_config.get(EcalcYamlKeywords.calculate_max_rate, False), - maximum_power=model_config.get(EcalcYamlKeywords.models_maximum_power, None), + energy_usage_adjustment_constant=model_config.power_adjustment_constant, + energy_usage_adjustment_factor=model_config.power_adjustment_factor, + calculate_max_rate=model_config.calculate_max_rate, + maximum_power=model_config.maximum_power, ) def _variable_speed_compressor_train_mapper( - model_config: Dict, + model_config: YamlVariableSpeedCompressorTrain, input_models: Dict[str, Any], resources: Resources, ) -> VariableSpeedCompressorTrain: - fluid_model_reference: str = model_config.get(EcalcYamlKeywords.models_type_fluid_model) + fluid_model_reference: str = model_config.fluid_model fluid_model = input_models.get(fluid_model_reference) if fluid_model is None: raise DataValidationError( - data=model_config, message=f"Fluid model reference {fluid_model_reference} not found." + data=model_config.model_dump(), message=f"Fluid model reference {fluid_model_reference} not found." ) - train_spec = model_config.get(EcalcYamlKeywords.models_type_compressor_train) - if train_spec is None: - raise DataValidationError( - data=model_config, - message=f"Missing keyword {EcalcYamlKeywords.models_type_compressor_train}" - f" for {model_config.get(EcalcYamlKeywords.type)} {model_config.get(EcalcYamlKeywords.name)}", - ) + train_spec = model_config.compressor_train + # The stages are pre defined, known - stages_data = train_spec.get(EcalcYamlKeywords.models_type_compressor_train_stages) - if stages_data is None: - raise DataValidationError( - data=model_config, - message=f"Missing keyword {EcalcYamlKeywords.models_type_compressor_train_stages}" - f" for {model_config.get(EcalcYamlKeywords.type)} {model_config.get(EcalcYamlKeywords.name)}", - ) + stages_data = train_spec.stages stages: List[CompressorStage] = [] for stage in stages_data: 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, - ) - ], + stage.control_margin, + YAML_UNIT_MAPPING[stage.control_margin_unit], ) - compressor_chart: VariableSpeedChartDTO = input_models.get( - stage.get(EcalcYamlKeywords.models_type_compressor_train_compressor_chart) - ) + compressor_chart: VariableSpeedChartDTO = input_models.get(stage.compressor_chart) stages.append( CompressorStage( compressor_chart=compressor_chart, inlet_temperature_kelvin=convert_temperature_to_kelvin( - [stage.get(EcalcYamlKeywords.models_type_compressor_train_inlet_temperature)], + [stage.inlet_temperature], input_unit=Unit.CELSIUS, )[0], remove_liquid_after_cooling=True, - pressure_drop_before_stage=stage.get( - EcalcYamlKeywords.models_type_compressor_train_pressure_drop_ahead_of_stage, 0.0 - ), + pressure_drop_before_stage=stage.pressure_drop_ahead_of_stage, control_margin=control_margin, ) ) @@ -536,139 +453,101 @@ def _variable_speed_compressor_train_mapper( return VariableSpeedCompressorTrain( fluid_model=fluid_model, stages=stages, - energy_usage_adjustment_constant=model_config.get(EcalcYamlKeywords.models_power_adjustment_constant_mw, 0), - energy_usage_adjustment_factor=model_config.get(EcalcYamlKeywords.models_power_adjustment_factor_mw, 1), - calculate_max_rate=model_config.get(EcalcYamlKeywords.calculate_max_rate, False), + energy_usage_adjustment_constant=model_config.power_adjustment_constant, + energy_usage_adjustment_factor=model_config.power_adjustment_factor, + calculate_max_rate=model_config.calculate_max_rate, pressure_control=pressure_control, - maximum_power=model_config.get(EcalcYamlKeywords.models_maximum_power, None), + maximum_power=model_config.maximum_power, ) def _simplified_variable_speed_compressor_train_mapper( - model_config: Dict, + model_config: YamlSimplifiedVariableSpeedCompressorTrain, input_models: Dict[str, Any], resources: Resources, ) -> Union[ CompressorTrainSimplifiedWithKnownStages, CompressorTrainSimplifiedWithUnknownStages, ]: - fluid_model_reference: str = model_config.get(EcalcYamlKeywords.models_type_fluid_model) + fluid_model_reference: str = model_config.fluid_model fluid_model = input_models.get(fluid_model_reference) if fluid_model is None: raise ValueError(f"Fluid model reference {fluid_model_reference} not found.") - train_spec: dict = model_config.get(EcalcYamlKeywords.models_type_compressor_train) + train_spec = model_config.compressor_train - if EcalcYamlKeywords.models_type_compressor_train_stages in train_spec: + if not isinstance(train_spec, YamlUnknownCompressorStages): # The stages are pre defined, known - stages = train_spec.get(EcalcYamlKeywords.models_type_compressor_train_stages) - for stage in stages: - if stage.get(EcalcYamlKeywords.models_type_compressor_train_stage_control_margin): - name = model_config.get(EcalcYamlKeywords.name) - raise ValueError( - f"{name}: {EcalcYamlKeywords.models_type_compressor_train_stage_control_margin}" - f" is not allowed for {model_config.get(EcalcYamlKeywords.type)}. " - f"{EcalcYamlKeywords.models_type_compressor_train_stage_control_margin} " - f"is only supported for the following train-types: " - f"{', '.join(YamlCompatibleTrainsControlMargin)}." - ) - if stage.get(EcalcYamlKeywords.models_type_compressor_train_pressure_drop_ahead_of_stage): - name = model_config.get(EcalcYamlKeywords.name) - raise ValueError( - f"{name}: {EcalcYamlKeywords.models_type_compressor_train_pressure_drop_ahead_of_stage}" - f" is not allowed for {model_config.get(EcalcYamlKeywords.type)}. " - f"{EcalcYamlKeywords.models_type_compressor_train_pressure_drop_ahead_of_stage} " - f"is only supported for the following train-types: " - f"{', '.join(YamlCompatibleTrainsPressureDropAheadOfStage)}." - ) + stages = train_spec.stages return CompressorTrainSimplifiedWithKnownStages( fluid_model=fluid_model, stages=[ CompressorStage( inlet_temperature_kelvin=convert_temperature_to_kelvin( - [stage.get(EcalcYamlKeywords.models_type_compressor_train_inlet_temperature)], + [stage.inlet_temperature], input_unit=Unit.CELSIUS, )[0], - compressor_chart=input_models.get( - stage.get(EcalcYamlKeywords.models_type_compressor_train_compressor_chart) - ), + compressor_chart=input_models.get(stage.compressor_chart), pressure_drop_before_stage=0, control_margin=0, remove_liquid_after_cooling=True, ) for stage in stages ], - energy_usage_adjustment_constant=model_config.get(EcalcYamlKeywords.models_power_adjustment_constant_mw, 0), - energy_usage_adjustment_factor=model_config.get(EcalcYamlKeywords.models_power_adjustment_factor_mw, 1), - calculate_max_rate=model_config.get(EcalcYamlKeywords.calculate_max_rate, False), - maximum_power=model_config.get(EcalcYamlKeywords.models_maximum_power, None), + energy_usage_adjustment_constant=model_config.power_adjustment_constant, + energy_usage_adjustment_factor=model_config.power_adjustment_factor, + calculate_max_rate=model_config.calculate_max_rate, + maximum_power=model_config.maximum_power, ) else: # The stages are unknown, not defined - compressor_chart_reference = train_spec[EcalcYamlKeywords.models_type_compressor_train_compressor_chart] + compressor_chart_reference = train_spec.compressor_chart return CompressorTrainSimplifiedWithUnknownStages( fluid_model=fluid_model, stage=CompressorStage( compressor_chart=input_models.get(compressor_chart_reference), inlet_temperature_kelvin=convert_temperature_to_kelvin( - [train_spec.get(EcalcYamlKeywords.models_type_compressor_train_inlet_temperature)], + [train_spec.inlet_temperature], input_unit=Unit.CELSIUS, )[0], pressure_drop_before_stage=0, remove_liquid_after_cooling=True, ), - energy_usage_adjustment_constant=model_config.get(EcalcYamlKeywords.models_power_adjustment_constant_mw, 0), - energy_usage_adjustment_factor=model_config.get(EcalcYamlKeywords.models_power_adjustment_factor_mw, 1), - calculate_max_rate=model_config.get(EcalcYamlKeywords.calculate_max_rate, False), - maximum_pressure_ratio_per_stage=train_spec.get( - EcalcYamlKeywords.models_type_compressor_train_maximum_pressure_ratio_per_stage - ), - maximum_power=model_config.get(EcalcYamlKeywords.models_maximum_power, None), + energy_usage_adjustment_constant=model_config.power_adjustment_constant, + energy_usage_adjustment_factor=model_config.power_adjustment_factor, + calculate_max_rate=model_config.calculate_max_rate, + maximum_pressure_ratio_per_stage=train_spec.maximum_pressure_ratio_per_stage, + maximum_power=model_config.maximum_power, ) -def _turbine_mapper(model_config: Dict, input_models: Dict[str, Any], resources: Resources) -> Turbine: - energy_usage_adjustment_constant = model_config.get(EcalcYamlKeywords.models_power_adjustment_constant_mw, 0) - energy_usage_adjustment_factor = model_config.get(EcalcYamlKeywords.models_power_adjustment_factor_mw, 1) - +def _turbine_mapper(model_config: YamlTurbine, input_models: Dict[str, Any], resources: Resources) -> Turbine: return Turbine( - lower_heating_value=model_config.get(EcalcYamlKeywords.fuel_lower_heating_value), - turbine_loads=model_config.get(EcalcYamlKeywords.models_turbine_efficiency_table_load_values), - turbine_efficiency_fractions=model_config.get( - EcalcYamlKeywords.models_turbine_efficiency_table_efficiency_values - ), - energy_usage_adjustment_constant=energy_usage_adjustment_constant, - energy_usage_adjustment_factor=energy_usage_adjustment_factor, + lower_heating_value=model_config.lower_heating_value, + turbine_loads=model_config.turbine_loads, + turbine_efficiency_fractions=model_config.turbine_efficiencies, + energy_usage_adjustment_constant=model_config.power_adjustment_constant, + energy_usage_adjustment_factor=model_config.power_adjustment_factor, ) def _compressor_with_turbine_mapper( - model_config: Dict, input_models: Dict[str, Any], resources: Resources + model_config: YamlCompressorWithTurbine, input_models: Dict[str, Any], resources: Resources ) -> CompressorWithTurbine: - compressor_train_model_reference = model_config.get(EcalcYamlKeywords.models_compressor_model) - turbine_model_reference = model_config.get(EcalcYamlKeywords.models_turbine_model) compressor_train_model = resolve_reference( - value=compressor_train_model_reference, + value=model_config.compressor_model, references=input_models, ) turbine_model = resolve_reference( - value=turbine_model_reference, + value=model_config.turbine_model, references=input_models, ) - for attr_reference, attr in ( - (compressor_train_model_reference, compressor_train_model), - (turbine_model_reference, turbine_model), - ): - if attr is None: - raise ValueError(f"{attr_reference} not found in input models") - energy_usage_adjustment_constant = model_config.get(EcalcYamlKeywords.models_power_adjustment_constant_mw, 0) - energy_usage_adjustment_factor = model_config.get(EcalcYamlKeywords.models_power_adjustment_factor_mw, 1) return CompressorWithTurbine( compressor_train=compressor_train_model, turbine=turbine_model, - energy_usage_adjustment_constant=energy_usage_adjustment_constant, - energy_usage_adjustment_factor=energy_usage_adjustment_factor, + energy_usage_adjustment_constant=model_config.power_adjustment_constant, + energy_usage_adjustment_factor=model_config.power_adjustment_factor, ) @@ -689,22 +568,22 @@ def __init__(self, resources: Resources): self.__resources = resources @staticmethod - def create_model(model: Dict, input_models: Dict[str, Any], resources: Resources): - model_creator = _model_mapper.get(model.get(EcalcYamlKeywords.type)) + def create_model(model: YamlConsumerModel, input_models: Dict[str, Any], resources: Resources): + model_creator = _model_mapper.get(model.type) if model_creator is None: - raise ValueError(f"Unknown model type: {model.get(EcalcYamlKeywords.type)}") + raise ValueError(f"Unknown model type: {model.name}") return model_creator(model_config=model, input_models=input_models, resources=resources) - def from_yaml_to_dto(self, model_config: Dict, input_models: Dict[str, Any]) -> EnergyModel: + def from_yaml_to_dto(self, model_config: YamlConsumerModel, input_models: Dict[str, Any]) -> EnergyModel: try: model_data = ModelMapper.create_model( model=model_config, input_models=input_models, resources=self.__resources ) return model_data except ValidationError as ve: - raise DtoValidationError(data=model_config, validation_error=ve) from ve + raise DtoValidationError(data=model_config.model_dump(), validation_error=ve) from ve except ValidationValueError as vve: raise DataValidationError( - data=model_config, + data=model_config.model_dump(), message=str(vve), ) from vve diff --git a/src/libecalc/presentation/yaml/parse_input.py b/src/libecalc/presentation/yaml/parse_input.py index 8d3588b5ef..b7dfbdff00 100644 --- a/src/libecalc/presentation/yaml/parse_input.py +++ b/src/libecalc/presentation/yaml/parse_input.py @@ -5,12 +5,12 @@ from libecalc.presentation.yaml.mappers.component_mapper import EcalcModelMapper from libecalc.presentation.yaml.mappers.create_references import create_references from libecalc.presentation.yaml.resource import Resources -from libecalc.presentation.yaml.yaml_models.pyyaml_yaml_model import PyYamlYamlModel +from libecalc.presentation.yaml.yaml_models.yaml_model import YamlValidator DEFAULT_START_TIME = datetime(1900, 1, 1) -def map_yaml_to_dto(configuration: PyYamlYamlModel, resources: Resources) -> Asset: +def map_yaml_to_dto(configuration: YamlValidator, resources: Resources) -> Asset: # TODO: Replace configuration type with YamlValidator references = create_references(configuration, resources) target_period = Period( diff --git a/src/libecalc/presentation/yaml/yaml_models/pyyaml_yaml_model.py b/src/libecalc/presentation/yaml/yaml_models/pyyaml_yaml_model.py index ce71f2a7a3..6b332c1b6d 100644 --- a/src/libecalc/presentation/yaml/yaml_models/pyyaml_yaml_model.py +++ b/src/libecalc/presentation/yaml/yaml_models/pyyaml_yaml_model.py @@ -2,7 +2,7 @@ import re from copy import deepcopy from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional, TextIO, Type, Union +from typing import Any, Dict, Iterable, Iterator, List, Optional, TextIO, Type, Union import yaml from pydantic import TypeAdapter @@ -38,10 +38,12 @@ ) from libecalc.presentation.yaml.yaml_models.yaml_model import YamlConfiguration, YamlValidator from libecalc.presentation.yaml.yaml_types.components.yaml_asset import YamlAsset +from libecalc.presentation.yaml.yaml_types.components.yaml_installation import YamlInstallation from libecalc.presentation.yaml.yaml_types.facility_model.yaml_facility_model import ( YamlFacilityModel, ) -from libecalc.presentation.yaml.yaml_types.models import YamlModel as YamlModelsModel +from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_fuel_type import YamlFuelType +from libecalc.presentation.yaml.yaml_types.models import YamlConsumerModel from libecalc.presentation.yaml.yaml_types.time_series.yaml_time_series import ( YamlTimeSeriesCollection, ) @@ -368,11 +370,11 @@ def facility_inputs(self) -> List[YamlFacilityModel]: return facility_inputs @property - def models(self) -> List[YamlModelsModel]: + def models(self) -> List[YamlConsumerModel]: models = [] for model in self._internal_datamodel.get(EcalcYamlKeywords.models, []): try: - models.append(TypeAdapter(YamlModelsModel).validate_python(model)) + models.append(TypeAdapter(YamlConsumerModel).validate_python(model)) except PydanticValidationError: pass @@ -413,11 +415,23 @@ def models_raise_if_invalid(self): @property def fuel_types(self): - return self._internal_datamodel.get(EcalcYamlKeywords.fuel_types, []) + fuel_types = [] + for fuel_type in self._internal_datamodel.get(EcalcYamlKeywords.fuel_types, []): + try: + fuel_types.append(TypeAdapter(YamlFuelType).validate_python(fuel_type)) + except PydanticValidationError: + pass + return fuel_types @property - def installations(self): - return self._internal_datamodel.get(EcalcYamlKeywords.installations, []) + def installations(self) -> Iterable[YamlInstallation]: + installations = [] + for installation in self._internal_datamodel.get(EcalcYamlKeywords.installations, []): + try: + installations.append(TypeAdapter(YamlInstallation).validate_python(installation)) + except PydanticValidationError: + pass + return installations @property def start(self) -> Optional[datetime.datetime]: diff --git a/src/libecalc/presentation/yaml/yaml_models/yaml_model.py b/src/libecalc/presentation/yaml/yaml_models/yaml_model.py index bcff0354c0..d40521d571 100644 --- a/src/libecalc/presentation/yaml/yaml_models/yaml_model.py +++ b/src/libecalc/presentation/yaml/yaml_models/yaml_model.py @@ -11,6 +11,9 @@ ) from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords from libecalc.presentation.yaml.yaml_types.components.yaml_asset import YamlAsset +from libecalc.presentation.yaml.yaml_types.components.yaml_installation import YamlInstallation +from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_fuel_type import YamlFuelType +from libecalc.presentation.yaml.yaml_types.models import YamlConsumerModel from libecalc.presentation.yaml.yaml_types.time_series.yaml_time_series import ( YamlTimeSeriesCollection, ) @@ -61,16 +64,17 @@ def time_series(self) -> List[YamlTimeSeriesCollection]: @property @abc.abstractmethod - def models(self): + def models(self) -> Iterable[YamlConsumerModel]: pass + @property @abc.abstractmethod - def fuel_types(self): + def fuel_types(self) -> Iterable[YamlFuelType]: pass @property @abc.abstractmethod - def installations(self) -> Iterable: + def installations(self) -> Iterable[YamlInstallation]: pass @property diff --git a/src/libecalc/presentation/yaml/yaml_types/components/legacy/energy_usage_model/common.py b/src/libecalc/presentation/yaml/yaml_types/components/legacy/energy_usage_model/common.py index 94180bf245..1f0bab40b4 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/legacy/energy_usage_model/common.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/legacy/energy_usage_model/common.py @@ -1,6 +1,6 @@ from typing import List -from pydantic import Field +from pydantic import Field, model_validator from libecalc.presentation.yaml.yaml_types import YamlBase from libecalc.presentation.yaml.yaml_types.components.yaml_expression_type import ( @@ -25,3 +25,9 @@ class EnergyUsageModelCommon(YamlBase): description="A factor that may be added to account for power line losses.\n\n$ECALC_DOCS_KEYWORDS_URL/POWERLOSSFACTOR", alias="POWERLOSSFACTOR", ) + + @model_validator(mode="after") + def check_mutually_exclusive_condition(self): + if self.conditions is not None and self.condition is not None: + raise ValueError("Either CONDITION or CONDITIONS should be specified, not both.") + return self diff --git a/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_electricity_consumer.py b/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_electricity_consumer.py index 1d6f7c5cfb..03588e6ca9 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_electricity_consumer.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_electricity_consumer.py @@ -6,7 +6,6 @@ from libecalc.presentation.yaml.energy_model_validation import ( validate_energy_usage_models, ) -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords from libecalc.presentation.yaml.yaml_types import YamlBase from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model import ( YamlElectricityEnergyUsageModel, @@ -41,8 +40,7 @@ class YamlElectricityConsumer(YamlBase): "\n\n$ECALC_DOCS_KEYWORDS_URL/ENERGY_USAGE_MODEL", ) - @model_validator(mode="before") - def check_energy_usage_models(cls, data): - model = data[EcalcYamlKeywords.energy_usage_model] - _check_multiple_energy_usage_models = validate_energy_usage_models(model, data[EcalcYamlKeywords.name]) - return data + @model_validator(mode="after") + def check_energy_usage_models(self): + _check_multiple_energy_usage_models = validate_energy_usage_models(self.energy_usage_model, self.name) + return self diff --git a/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_fuel_consumer.py b/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_fuel_consumer.py index ba8fc82457..615d38b9fc 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_fuel_consumer.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/legacy/yaml_fuel_consumer.py @@ -6,7 +6,6 @@ from libecalc.presentation.yaml.energy_model_validation import ( validate_energy_usage_models, ) -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords from libecalc.presentation.yaml.yaml_types import YamlBase from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model import ( YamlFuelEnergyUsageModel, @@ -47,8 +46,7 @@ class YamlFuelConsumer(YamlBase): description="The fuel used by the consumer." "\n\n$ECALC_DOCS_KEYWORDS_URL/FUEL", ) - @model_validator(mode="before") - def check_energy_usage_models(cls, data): - model = data[EcalcYamlKeywords.energy_usage_model] - _check_multiple_energy_usage_models = validate_energy_usage_models(model, data[EcalcYamlKeywords.name]) - return data + @model_validator(mode="after") + def check_energy_usage_models(self): + _check_multiple_energy_usage_models = validate_energy_usage_models(self.energy_usage_model, self.name) + return self diff --git a/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py b/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py index a364b9639d..d7275cb3b2 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py @@ -10,7 +10,7 @@ YamlFacilityModel, ) from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_fuel_type import YamlFuelType -from libecalc.presentation.yaml.yaml_types.models import YamlModel +from libecalc.presentation.yaml.yaml_types.models import YamlConsumerModel from libecalc.presentation.yaml.yaml_types.time_series.yaml_time_series import ( YamlTimeSeriesCollection, ) @@ -39,7 +39,7 @@ class YamlAsset(YamlBase): description="Defines input files which characterize various facility elements." "\n\n$ECALC_DOCS_KEYWORDS_URL/FACILITY_INPUTS", ) - models: List[YamlModel] = Field( + models: List[YamlConsumerModel] = Field( None, title="MODELS", description="Defines input files which characterize various facility elements." diff --git a/src/libecalc/presentation/yaml/yaml_types/components/yaml_installation.py b/src/libecalc/presentation/yaml/yaml_types/components/yaml_installation.py index 0450690c8b..5989039197 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/yaml_installation.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/yaml_installation.py @@ -91,3 +91,4 @@ def check_some_consumer_or_emitter(self): f"Keywords are missing:\n It is required to specify at least one of the keywords " f"{EcalcYamlKeywords.fuel_consumers}, {EcalcYamlKeywords.generator_sets} or {EcalcYamlKeywords.installation_venting_emitters} in the model.", ) from None + return self diff --git a/src/libecalc/presentation/yaml/yaml_types/facility_model/yaml_facility_model.py b/src/libecalc/presentation/yaml/yaml_types/facility_model/yaml_facility_model.py index cd9cbff452..543c1552e4 100644 --- a/src/libecalc/presentation/yaml/yaml_types/facility_model/yaml_facility_model.py +++ b/src/libecalc/presentation/yaml/yaml_types/facility_model/yaml_facility_model.py @@ -91,7 +91,7 @@ class YamlPumpChartUnits(YamlBase): class YamlPumpChartBase(YamlFacilityModelBase): head_margin: float = Field( - None, + 0.0, title="HEAD_MARGIN", description="Adjustment of the head margin for power calibration.\n\n$ECALC_DOCS_KEYWORDS_URL/HEAD_MARGIN", ) diff --git a/src/libecalc/presentation/yaml/yaml_types/models/__init__.py b/src/libecalc/presentation/yaml/yaml_types/models/__init__.py index b95a80b480..0be8dc7c5e 100644 --- a/src/libecalc/presentation/yaml/yaml_types/models/__init__.py +++ b/src/libecalc/presentation/yaml/yaml_types/models/__init__.py @@ -15,7 +15,7 @@ from libecalc.presentation.yaml.yaml_types.models.yaml_fluid import YamlFluidModel from libecalc.presentation.yaml.yaml_types.models.yaml_turbine import YamlTurbine -YamlModel = Annotated[ +YamlConsumerModel = Annotated[ Union[ YamlCompressorChart, YamlCompressorWithTurbine, diff --git a/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_chart.py b/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_chart.py index bf251a30d3..283cbf4295 100644 --- a/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_chart.py +++ b/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_chart.py @@ -14,23 +14,23 @@ class YamlCurve(YamlBase): - speed: float = None + speed: float = 1 rate: List[float] head: List[float] efficiency: List[float] -class YamlRateUnits(enum.Enum): +class YamlRateUnits(str, enum.Enum): AM3_PER_HOUR = "AM3_PER_HOUR" -class YamlHeadUnits(enum.Enum): +class YamlHeadUnits(str, enum.Enum): M = "M" KJ_PER_KG = "KJ_PER_KG" JOULE_PER_KG = "JOULE_PER_KG" -class YamlEfficiencyUnits(enum.Enum): +class YamlEfficiencyUnits(str, enum.Enum): FRACTION = "FRACTION" PERCENTAGE = "PERCENTAGE" @@ -53,6 +53,10 @@ class YamlUnits(YamlBase): ) +def UnitsField() -> Field: + return Field(default_factory=lambda: YamlUnits(), title="UNITS", description="Defines the units") + + class YamlSingleSpeedChart(YamlBase): name: ModelName = Field( ..., @@ -66,7 +70,7 @@ class YamlSingleSpeedChart(YamlBase): ) chart_type: Literal[YamlChartType.SINGLE_SPEED] = YamlChartType.SINGLE_SPEED curve: DataOrFile[YamlCurve] = Field(..., description="One single compressor chart curve.", title="CURVE") - units: YamlUnits = None + units: YamlUnits = UnitsField() class YamlVariableSpeedChart(YamlBase): @@ -84,7 +88,7 @@ class YamlVariableSpeedChart(YamlBase): curves: DataOrFile[List[YamlCurve]] = Field( ..., description="Compressor chart curves, one per speed.", title="CURVES" ) - units: YamlUnits = None + units: YamlUnits = UnitsField() class YamlGenericFromInputChart(YamlBase): @@ -104,7 +108,7 @@ class YamlGenericFromInputChart(YamlBase): description="Polytropic efficiency for compressor chart", title="POLYTROPIC_EFFICIENCY", ) - units: YamlUnits = None + units: YamlUnits = UnitsField() class YamlGenericFromDesignPointChart(YamlBase): diff --git a/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_stages.py b/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_stages.py index 9c537b2dcd..dda88dbadb 100644 --- a/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_stages.py +++ b/src/libecalc/presentation/yaml/yaml_types/models/yaml_compressor_stages.py @@ -12,7 +12,7 @@ ) -class YamlControlMarginUnits(enum.Enum): +class YamlControlMarginUnits(str, enum.Enum): FRACTION = "FRACTION" PERCENTAGE = "PERCENTAGE" @@ -45,7 +45,7 @@ class YamlCompressorStage(YamlBase): class YamlCompressorStageWithMarginAndPressureDrop(YamlCompressorStage): pressure_drop_ahead_of_stage: Optional[float] = Field( - None, + 0.0, description="Pressure drop before compression stage [in bar]", title="PRESSURE_DROP_AHEAD_OF_STAGE", ) 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 1be3b219da..c818410e37 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 @@ -88,7 +88,7 @@ class YamlVariableSpeedCompressorTrain(YamlCompressorTrainBase): title="COMPRESSOR_TRAIN", ) pressure_control: YamlPressureControl = Field( - None, + YamlPressureControl.DOWNSTREAM_CHOKE, description="Method for pressure control", title="PRESSURE_CONTROL", ) diff --git a/src/libecalc/presentation/yaml/yaml_types/models/yaml_fluid.py b/src/libecalc/presentation/yaml/yaml_types/models/yaml_fluid.py index 83a24e632a..18dd39a9f9 100644 --- a/src/libecalc/presentation/yaml/yaml_types/models/yaml_fluid.py +++ b/src/libecalc/presentation/yaml/yaml_types/models/yaml_fluid.py @@ -48,7 +48,6 @@ class YamlComposition(BaseModel): model_config = ConfigDict(extra="forbid") CO2: float = 0.0 - H2O: float = 0.0 ethane: float = 0.0 i_butane: float = 0.0 i_pentane: float = 0.0 diff --git a/src/libecalc/presentation/yaml/yaml_types/yaml_data_or_file.py b/src/libecalc/presentation/yaml/yaml_types/yaml_data_or_file.py index 2b024f7517..56fd487d05 100644 --- a/src/libecalc/presentation/yaml/yaml_types/yaml_data_or_file.py +++ b/src/libecalc/presentation/yaml/yaml_types/yaml_data_or_file.py @@ -21,7 +21,10 @@ class YamlFile(YamlBase): def file_or_data_discriminator(data): - if not isinstance(data, dict): + if isinstance(data, YamlFile): + return "file" + elif not isinstance(data, dict): + # Not json/dict and not YamlFile -> Should be TData return "data" lower_case_keys = [str(key).lower() for key in data.keys()] diff --git a/src/tests/libecalc/input/mappers/test_consumer_chart.py b/src/tests/libecalc/input/mappers/test_consumer_chart.py index cf9a5f4944..5420560815 100644 --- a/src/tests/libecalc/input/mappers/test_consumer_chart.py +++ b/src/tests/libecalc/input/mappers/test_consumer_chart.py @@ -14,6 +14,12 @@ from libecalc.presentation.yaml.validation_errors import ResourceValidationError from libecalc.presentation.yaml.yaml_entities import MemoryResource from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords +from libecalc.presentation.yaml.yaml_types.facility_model.yaml_facility_model import ( + YamlPumpChartSingleSpeed, + YamlPumpChartUnits, +) +from libecalc.presentation.yaml.yaml_types.models.yaml_compressor_chart import YamlSingleSpeedChart, YamlUnits +from libecalc.presentation.yaml.yaml_types.yaml_data_or_file import YamlFile @pytest.fixture @@ -70,12 +76,12 @@ def chart_resource_unequal_speed(): @pytest.fixture def pump_chart(): - return { - "NAME": "pumpchart", - "FILE": "pumpchart.csv", - "TYPE": "PUMP_CHART_SINGLE_SPEED", - "UNITS": {"EFFICIENCY": "PERCENTAGE", "RATE": "AM3_PER_HOUR", "HEAD": "M"}, - } + return YamlPumpChartSingleSpeed( + name="pumpchart", + file="pumpchart.csv", + type="PUMP_CHART_SINGLE_SPEED", + units=YamlPumpChartUnits(efficiency="PERCENTAGE", rate="AM3_PER_HOUR", head="M"), + ) class TestSingleSpeedChart: @@ -132,17 +138,17 @@ def test_invalid_unequal_speed(self, pump_chart, chart_resource_unequal_speed): @pytest.fixture def compressor_chart(): - return { - "NAME": "compressorchart", - "TYPE": "COMPRESSOR_CHART", - "CHART_TYPE": "SINGLE_SPEED", - "UNITS": { - "EFFICIENCY": "PERCENTAGE", - "RATE": "AM3_PER_HOUR", - "HEAD": "KJ_PER_KG", - }, - "CURVE": {"FILE": "compressorchart.csv"}, - } + return YamlSingleSpeedChart( + name="compressorchart", + type="COMPRESSOR_CHART", + chart_type="SINGLE_SPEED", + units=YamlUnits( + efficiency="PERCENTAGE", + rate="AM3_PER_HOUR", + head="KJ_PER_KG", + ), + curve=YamlFile(file="compressorchart.csv"), + ) class TestCompressorChartSingleSpeed: diff --git a/src/tests/libecalc/input/mappers/test_energy_usage_model.py b/src/tests/libecalc/input/mappers/test_energy_usage_model.py index cb44b05b2e..f9b7df6738 100644 --- a/src/tests/libecalc/input/mappers/test_energy_usage_model.py +++ b/src/tests/libecalc/input/mappers/test_energy_usage_model.py @@ -1,7 +1,9 @@ import io from datetime import datetime +from typing import Union import pytest +from pydantic import TypeAdapter, ValidationError import libecalc.common.energy_usage_type import libecalc.common.utils.rates @@ -14,9 +16,13 @@ from libecalc.presentation.yaml.mappers.consumer_function_mapper import ( ConsumerFunctionMapper, ) -from libecalc.presentation.yaml.validation_errors import DataValidationError from libecalc.presentation.yaml.yaml_entities import References, ResourceStream from libecalc.presentation.yaml.yaml_models.pyyaml_yaml_model import PyYamlYamlModel +from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model import ( + YamlElectricityEnergyUsageModel, + YamlFuelEnergyUsageModel, +) +from libecalc.presentation.yaml.yaml_types.yaml_temporal_model import YamlTemporalModel SINGLE_SPEED_PUMP_CHART = dto.PumpModel( chart=SingleSpeedChartDTO( @@ -152,7 +158,9 @@ def test_energy_usage_model_valid(self, yaml_text, expected_model_dto, reference read_yaml = PyYamlYamlModel.read_yaml(ResourceStream(name="main.yaml", stream=io.StringIO(yaml_text))) model_dto = ConsumerFunctionMapper( references, target_period=Period(start=datetime.min, end=datetime.max) - ).from_yaml_to_dto(read_yaml) + ).from_yaml_to_dto( + TypeAdapter(Union[YamlFuelEnergyUsageModel, YamlElectricityEnergyUsageModel]).validate_python(read_yaml) + ) model_dto_without_default_date = next(iter(model_dto.values())) assert model_dto_without_default_date == expected_model_dto @@ -172,9 +180,12 @@ def test_condition_and_conditions_error(self): ), ) ) - with pytest.raises(DataValidationError) as exc_info: - ConsumerFunctionMapper( - References(), target_period=Period(start=datetime.min, end=datetime.max) - ).from_yaml_to_dto(read_yaml) + with pytest.raises(ValidationError) as exc_info: + TypeAdapter( + Union[ + YamlTemporalModel[YamlFuelEnergyUsageModel], + YamlTemporalModel[YamlElectricityEnergyUsageModel], + ], + ).validate_python(read_yaml) assert "Either CONDITION or CONDITIONS should be specified, not both." in str(exc_info.value) diff --git a/src/tests/libecalc/input/mappers/test_facility_input.py b/src/tests/libecalc/input/mappers/test_facility_input.py index f9e0da7e1d..1d0b3e1ac0 100644 --- a/src/tests/libecalc/input/mappers/test_facility_input.py +++ b/src/tests/libecalc/input/mappers/test_facility_input.py @@ -1,21 +1,8 @@ -from typing import Dict - -import pytest - from libecalc import dto from libecalc.common.energy_model_type import EnergyModelType from libecalc.presentation.yaml.mappers.facility_input import FacilityInputMapper -from libecalc.presentation.yaml.validation_errors import DtoValidationError from libecalc.presentation.yaml.yaml_entities import MemoryResource -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords - - -def _create_facility_input_data(typ: str, file: str, name: str) -> Dict: - return { - EcalcYamlKeywords.type: typ, - EcalcYamlKeywords.file: file, - EcalcYamlKeywords.name: name, - } +from libecalc.presentation.yaml.yaml_types.facility_model.yaml_facility_model import YamlGeneratorSetModel class TestFacilityInputMapper: @@ -23,10 +10,10 @@ def test_generator_set_sampled(self): resources = {"generator_file_path": MemoryResource(headers=["POWER", "FUEL"], data=[[0, 0.4, 1], [0, 0.7, 1]])} facility_input_mapper = FacilityInputMapper(resources=resources) generator_set_sampled = facility_input_mapper.from_yaml_to_dto( - _create_facility_input_data( - typ="ELECTRICITY2FUEL", + YamlGeneratorSetModel( + type="ELECTRICITY2FUEL", file="generator_file_path", - name="genset,", + name="genset", ) ) @@ -34,13 +21,3 @@ def test_generator_set_sampled(self): assert generator_set_sampled.typ == EnergyModelType.GENERATOR_SET_SAMPLED assert generator_set_sampled.headers == ["POWER", "FUEL"] assert generator_set_sampled.data == [[0, 0.4, 1], [0, 0.7, 1]] - - def test_invalid_model(self): - # pydantic does not match type, then validate. So this test will only fail because the data does not match - # any of the EnergyModels. See https://github.com/samuelcolvin/pydantic/issues/619 for status on discriminated - # unions, i.e. specify a discriminator so that we don't have to validate against all models - facility_input_mapper = FacilityInputMapper(resources={"some-file": MemoryResource(headers=[], data=[])}) - with pytest.raises(DtoValidationError): - facility_input_mapper.from_yaml_to_dto( - _create_facility_input_data(name="something", typ="wrong_type", file="some-file") - ) diff --git a/src/tests/libecalc/input/mappers/test_fueltype_mapper.py b/src/tests/libecalc/input/mappers/test_fueltype_mapper.py deleted file mode 100644 index 197acf26c8..0000000000 --- a/src/tests/libecalc/input/mappers/test_fueltype_mapper.py +++ /dev/null @@ -1,50 +0,0 @@ -import libecalc.dto.fuel_type -from libecalc import dto -from libecalc.dto.types import FuelTypeUserDefinedCategoryType -from libecalc.expression import Expression -from libecalc.presentation.yaml.mappers.fuel_and_emission_mapper import FuelMapper -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords - - -class TestMapFuelType: - def test_valid_implicit(self): - fuel_dict = {EcalcYamlKeywords.name: "diesel"} - expected_fueltype = libecalc.dto.fuel_type.FuelType(name="diesel", emissions=[]) - - assert expected_fueltype == FuelMapper.from_yaml_to_dto(fuel_dict) - - def test_valid_fuller_fueltype(self): - fuel_dict = { - EcalcYamlKeywords.name: "diesel", - EcalcYamlKeywords.user_defined_tag: FuelTypeUserDefinedCategoryType.DIESEL, - } - expected_fueltype = libecalc.dto.fuel_type.FuelType( - name="diesel", - user_defined_category=FuelTypeUserDefinedCategoryType.DIESEL, - ) - - assert expected_fueltype == FuelMapper.from_yaml_to_dto(fuel_dict) - - def test_valid_full_fueltype(self): - fuel_dict = { - EcalcYamlKeywords.name: "diesel", - EcalcYamlKeywords.user_defined_tag: FuelTypeUserDefinedCategoryType.DIESEL, - EcalcYamlKeywords.emissions: [ - { - EcalcYamlKeywords.name: "co2", - EcalcYamlKeywords.emission_factor: 1.0, - } - ], - } - expected_fueltype = libecalc.dto.fuel_type.FuelType( - name="diesel", - user_defined_category=FuelTypeUserDefinedCategoryType.DIESEL, - emissions=[ - dto.Emission( - name="co2", - factor=Expression.setup_from_expression(value=1.0), - ) - ], - ) - - assert expected_fueltype == FuelMapper.from_yaml_to_dto(fuel_dict) diff --git a/src/tests/libecalc/input/mappers/test_model_mapper.py b/src/tests/libecalc/input/mappers/test_model_mapper.py index 4f1e74d8fd..4ae0c8ffc3 100644 --- a/src/tests/libecalc/input/mappers/test_model_mapper.py +++ b/src/tests/libecalc/input/mappers/test_model_mapper.py @@ -3,6 +3,7 @@ from typing import Dict, Optional, cast import pytest +from pydantic import TypeAdapter from libecalc import dto from libecalc.common.time_utils import Frequency, Period @@ -12,6 +13,7 @@ from libecalc.presentation.yaml.yaml_entities import MemoryResource, ResourceStream from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords from libecalc.presentation.yaml.yaml_models.yaml_model import ReaderType, YamlConfiguration, YamlValidator +from libecalc.presentation.yaml.yaml_types.models import YamlCompressorChart from tests.libecalc.input.test_yaml_model import DirectResourceService @@ -179,67 +181,75 @@ def test_predefined_variable_speed_compressor_chart_from_yaml_to_dto(): } model_mapper = ModelMapper(resources=resources) + + def create_compressor_chart(data: Dict) -> YamlCompressorChart: + return TypeAdapter(YamlCompressorChart).validate_python(data) + variable_speed_compressor_chart_curves_spec_in_csv = model_mapper.from_yaml_to_dto( - model_config={ - "NAME": "predefined_compressor_chart_curves_from_file", - "TYPE": "COMPRESSOR_CHART", - "CHART_TYPE": "VARIABLE_SPEED", - "UNITS": {"RATE": "AM3_PER_HOUR", "HEAD": "M", "EFFICIENCY": "FRACTION"}, - "CURVES": {"FILE": "einput/predefined_compressor_chart_curves.csv"}, - }, + model_config=create_compressor_chart( + { + "NAME": "predefined_compressor_chart_curves_from_file", + "TYPE": "COMPRESSOR_CHART", + "CHART_TYPE": "VARIABLE_SPEED", + "UNITS": {"RATE": "AM3_PER_HOUR", "HEAD": "M", "EFFICIENCY": "FRACTION"}, + "CURVES": {"FILE": "einput/predefined_compressor_chart_curves.csv"}, + } + ), input_models={}, ) variable_speed_compressor_chart_curves_spec_in_yaml = model_mapper.from_yaml_to_dto( - model_config={ - "NAME": "predefined_compressor_chart", - "TYPE": "COMPRESSOR_CHART", - "CHART_TYPE": "VARIABLE_SPEED", - "UNITS": {"RATE": "AM3_PER_HOUR", "HEAD": "M", "EFFICIENCY": "FRACTION"}, - "CURVES": [ - { - "SPEED": 7689, - "RATE": [2900.0666, 3503.8068, 4002.5554, 4595.0148], - "HEAD": [8412.9156, 7996.2541, 7363.8161, 6127.1702], - "EFFICIENCY": [0.723, 0.7469, 0.7449, 0.7015], - }, - { - "SPEED": 8787, - "RATE": [3305.5723, 4000.1546, 4499.2342, 4996.8728, 5241.9892], - "HEAD": [10950.9557, 10393.3867, 9707.491, 8593.8586, 7974.6002], - "EFFICIENCY": [0.7241, 0.7449, 0.7464, 0.722, 0.7007], - }, - { - "SPEED": 9886, - "RATE": [3708.8713, 4502.2531, 4993.5959, 5507.8114, 5924.3308], - "HEAD": [13845.3808, 13182.6922, 12425.3699, 11276.3984, 10054.3539], - "EFFICIENCY": [0.723, 0.7473, 0.748, 0.7306, 0.704], - }, - { - "SPEED": 10435, - "RATE": [3928.0389, 4507.4654, 5002.1249, 5498.9912, 6248.5937], - "HEAD": [15435.484, 14982.7351, 14350.2222, 13361.3245, 11183.0276], - "EFFICIENCY": [0.7232, 0.7437, 0.7453, 0.7414, 0.701], - }, - { - "SPEED": 10984, - "RATE": [4138.6974, 5002.4758, 5494.3704, 6008.6962, 6560.148], - "HEAD": [17078.8952, 16274.9249, 15428.5063, 14261.7156, 12382.7538], - "EFFICIENCY": [0.7226, 0.7462, 0.7468, 0.7349, 0.7023], - }, - { - "SPEED": 11533, - "RATE": [4327.9175, 4998.517, 5505.8851, 6027.6167, 6506.9064, 6908.2832], - "HEAD": [18882.3055, 18235.1912, 17531.6259, 16489.7195, 15037.1474, 13618.7919], - "EFFICIENCY": [0.7254, 0.7444, 0.745, 0.7466, 0.7266, 0.7019], - }, - { - "SPEED": 10767, - "RATE": [4052.9057, 4500.6637, 4999.41, 5492.822, 6000.6263, 6439.4876], - "HEAD": [16447, 16081, 15546, 14640, 13454, 11973], - "EFFICIENCY": [0.724, 0.738, 0.7479, 0.74766, 0.7298, 0.7014], - }, - ], - }, + model_config=create_compressor_chart( + { + "NAME": "predefined_compressor_chart", + "TYPE": "COMPRESSOR_CHART", + "CHART_TYPE": "VARIABLE_SPEED", + "UNITS": {"RATE": "AM3_PER_HOUR", "HEAD": "M", "EFFICIENCY": "FRACTION"}, + "CURVES": [ + { + "SPEED": 7689, + "RATE": [2900.0666, 3503.8068, 4002.5554, 4595.0148], + "HEAD": [8412.9156, 7996.2541, 7363.8161, 6127.1702], + "EFFICIENCY": [0.723, 0.7469, 0.7449, 0.7015], + }, + { + "SPEED": 8787, + "RATE": [3305.5723, 4000.1546, 4499.2342, 4996.8728, 5241.9892], + "HEAD": [10950.9557, 10393.3867, 9707.491, 8593.8586, 7974.6002], + "EFFICIENCY": [0.7241, 0.7449, 0.7464, 0.722, 0.7007], + }, + { + "SPEED": 9886, + "RATE": [3708.8713, 4502.2531, 4993.5959, 5507.8114, 5924.3308], + "HEAD": [13845.3808, 13182.6922, 12425.3699, 11276.3984, 10054.3539], + "EFFICIENCY": [0.723, 0.7473, 0.748, 0.7306, 0.704], + }, + { + "SPEED": 10435, + "RATE": [3928.0389, 4507.4654, 5002.1249, 5498.9912, 6248.5937], + "HEAD": [15435.484, 14982.7351, 14350.2222, 13361.3245, 11183.0276], + "EFFICIENCY": [0.7232, 0.7437, 0.7453, 0.7414, 0.701], + }, + { + "SPEED": 10984, + "RATE": [4138.6974, 5002.4758, 5494.3704, 6008.6962, 6560.148], + "HEAD": [17078.8952, 16274.9249, 15428.5063, 14261.7156, 12382.7538], + "EFFICIENCY": [0.7226, 0.7462, 0.7468, 0.7349, 0.7023], + }, + { + "SPEED": 11533, + "RATE": [4327.9175, 4998.517, 5505.8851, 6027.6167, 6506.9064, 6908.2832], + "HEAD": [18882.3055, 18235.1912, 17531.6259, 16489.7195, 15037.1474, 13618.7919], + "EFFICIENCY": [0.7254, 0.7444, 0.745, 0.7466, 0.7266, 0.7019], + }, + { + "SPEED": 10767, + "RATE": [4052.9057, 4500.6637, 4999.41, 5492.822, 6000.6263, 6439.4876], + "HEAD": [16447, 16081, 15546, 14640, 13454, 11973], + "EFFICIENCY": [0.724, 0.738, 0.7479, 0.74766, 0.7298, 0.7014], + }, + ], + } + ), input_models={}, ) assert variable_speed_compressor_chart_curves_spec_in_csv == variable_speed_compressor_chart_curves_spec_in_yaml diff --git a/src/tests/libecalc/input/mappers/test_utils.py b/src/tests/libecalc/input/mappers/test_utils.py index 4b66b4677d..6e3a0d83c1 100644 --- a/src/tests/libecalc/input/mappers/test_utils.py +++ b/src/tests/libecalc/input/mappers/test_utils.py @@ -1,3 +1,6 @@ +from dataclasses import dataclass +from typing import List + from libecalc.common.units import Unit from libecalc.presentation.yaml.mappers.consumer_function_mapper import _map_condition from libecalc.presentation.yaml.mappers.utils import ( @@ -7,38 +10,31 @@ convert_rate_to_am3_per_hour, convert_temperature_to_kelvin, ) -from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords + + +@dataclass +class ConditionedModel: + condition: str = None + conditions: List[str] = None class TestMapCondition: def test_valid_single(self): condition = "5 {+} SIM1;COL1" - assert condition == _map_condition({EcalcYamlKeywords.condition: condition}) - - def test_valid_single_list(self): - """Test that deprecated list in 'CONDITION' still works. Fixme: Remove when making breaking changes. - :return: - """ - conditions = [ - "5 {+} SIM1;COL1", - "6 {+} SIM1;COL2", - ] - assert f"({conditions[0]}) {{*}} ({conditions[1]})" == _map_condition({EcalcYamlKeywords.condition: conditions}) + assert condition == _map_condition(ConditionedModel(condition=condition)) def test_valid_multiple(self): conditions = [ "5 {+} SIM1;COL1", "6 {+} SIM1;COL2", ] - assert f"({conditions[0]}) {{*}} ({conditions[1]})" == _map_condition( - {EcalcYamlKeywords.conditions: conditions} - ) + assert f"({conditions[0]}) {{*}} ({conditions[1]})" == _map_condition(ConditionedModel(conditions=conditions)) def test_valid_multiple_with_single_item(self): conditions = [ "5 {+} SIM1;COL1", ] - assert f"({conditions[0]})" == _map_condition({EcalcYamlKeywords.conditions: conditions}) + assert f"({conditions[0]})" == _map_condition(ConditionedModel(conditions=conditions)) def test_convert_rate_to_am3_per_hour(): diff --git a/src/tests/libecalc/input/test_create_references.py b/src/tests/libecalc/input/test_create_references.py index ed8cfe8ce1..d4483e8f00 100644 --- a/src/tests/libecalc/input/test_create_references.py +++ b/src/tests/libecalc/input/test_create_references.py @@ -1,39 +1,32 @@ -from libecalc.presentation.yaml.mappers.create_references import _sort_models +from dataclasses import dataclass + +from libecalc.presentation.yaml.mappers.create_references import SortableModel, _sort_models from libecalc.presentation.yaml.yaml_keywords import EcalcYamlKeywords +@dataclass +class Model(SortableModel): + name: str + type: str + + class TestCreateReferences: @staticmethod def test_sort_models(): models_data = [ - { - EcalcYamlKeywords.name: "a", - EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_with_turbine, - }, - { - EcalcYamlKeywords.name: "b", - EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_train_simplified, - }, - {EcalcYamlKeywords.name: "c", EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_turbine}, - { - EcalcYamlKeywords.name: "d", - EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_with_turbine, - }, - {EcalcYamlKeywords.name: "e", EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_chart}, - { - EcalcYamlKeywords.name: "f", - EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_train_simplified, - }, - {EcalcYamlKeywords.name: "g", EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_chart}, - { - EcalcYamlKeywords.name: "h", - EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_train_simplified, - }, - {EcalcYamlKeywords.name: "i", EcalcYamlKeywords.type: EcalcYamlKeywords.models_type_compressor_chart}, + Model(name="a", type=EcalcYamlKeywords.models_type_compressor_with_turbine), + Model(name="b", type=EcalcYamlKeywords.models_type_compressor_train_simplified), + Model(name="c", type=EcalcYamlKeywords.models_type_turbine), + Model(name="d", type=EcalcYamlKeywords.models_type_compressor_with_turbine), + Model(name="e", type=EcalcYamlKeywords.models_type_compressor_chart), + Model(name="f", type=EcalcYamlKeywords.models_type_compressor_train_simplified), + Model(name="g", type=EcalcYamlKeywords.models_type_compressor_chart), + Model(name="h", type=EcalcYamlKeywords.models_type_compressor_train_simplified), + Model(name="i", type=EcalcYamlKeywords.models_type_compressor_chart), ] sorted_models = _sort_models(models_data) - assert [sorted_model[EcalcYamlKeywords.name] for sorted_model in sorted_models] == [ + assert [sorted_model.name for sorted_model in sorted_models] == [ "c", "e", "g",