diff --git a/src/libecalc/core/models/compressor/sampled/compressor_model_sampled.py b/src/libecalc/core/models/compressor/sampled/compressor_model_sampled.py index 8f61c04527..fecfa51a4c 100644 --- a/src/libecalc/core/models/compressor/sampled/compressor_model_sampled.py +++ b/src/libecalc/core/models/compressor/sampled/compressor_model_sampled.py @@ -167,8 +167,15 @@ def evaluate_rate_ps_pd( """ # subtract an epsilon to make robust comparison. if rate is not None: + # Ensure rate is a NumPy array + rate = np.array(rate, dtype=np.float64) # To avoid bringing rate below zero. rate = np.where(rate > 0, rate - EPSILON, rate) + if suction_pressure is not None: + suction_pressure = np.array(suction_pressure, dtype=np.float64) + + if discharge_pressure is not None: + discharge_pressure = np.array(discharge_pressure, dtype=np.float64) number_of_data_points = 0 if rate is not None: diff --git a/src/libecalc/core/models/model_input_validation.py b/src/libecalc/core/models/model_input_validation.py index 39cd480abe..2ce0945180 100644 --- a/src/libecalc/core/models/model_input_validation.py +++ b/src/libecalc/core/models/model_input_validation.py @@ -29,12 +29,18 @@ def validate_model_input( NDArray[np.float64], list[ModelInputFailureStatus], ]: + # Ensure input is a NumPy array + rate = np.array(rate, dtype=np.float64) + suction_pressure = np.array(suction_pressure, dtype=np.float64) + discharge_pressure = np.array(discharge_pressure, dtype=np.float64) + indices_to_validate = _find_indices_to_validate(rate=rate) validated_failure_status = [ModelInputFailureStatus.NO_FAILURE] * len(suction_pressure) validated_rate = rate.copy() validated_suction_pressure = suction_pressure.copy() validated_discharge_pressure = discharge_pressure.copy() if intermediate_pressure is not None: + intermediate_pressure = np.array(intermediate_pressure, dtype=np.float64) validated_intermediate_pressure = intermediate_pressure if len(indices_to_validate) >= 1: ( @@ -82,6 +88,7 @@ def _find_indices_to_validate(rate: NDArray[np.float64]) -> list[int]: For a 1D array, this means returning the indices where rate is positive. For a 2D array, this means returning the indices where at least one rate is positive (along 0-axis). """ + rate = np.atleast_1d(rate) # Ensure rate is at least 1D return np.where(np.any(rate != 0, axis=0) if np.ndim(rate) == 2 else rate != 0)[0].tolist() diff --git a/src/libecalc/core/models/pump/pump.py b/src/libecalc/core/models/pump/pump.py index 7d35549af0..511fc753ad 100644 --- a/src/libecalc/core/models/pump/pump.py +++ b/src/libecalc/core/models/pump/pump.py @@ -63,9 +63,13 @@ def evaluate_streams( @staticmethod def _calculate_head( - ps: NDArray[np.float64], pd: NDArray[np.float64], density: Union[NDArray[np.float64], float] + ps: Union[NDArray[np.float64], list[float]], + pd: Union[NDArray[np.float64], list[float]], + density: Union[NDArray[np.float64], float], ) -> NDArray[np.float64]: """:return: Head in joule per kg [J/kg]""" + ps = np.array(ps, dtype=np.float64) + pd = np.array(pd, dtype=np.float64) return np.array(Unit.BARA.to(Unit.PASCAL)(pd - ps) / density) @staticmethod @@ -288,6 +292,10 @@ def evaluate_rate_ps_pd_density( :param discharge_pressures: :param fluid_density: """ + # Ensure rate is a NumPy array + rate = np.array(rate, dtype=np.float64) + fluid_density = np.array(fluid_density, dtype=np.float64) + # Ensure that the pump does not run when rate is <= 0. stream_day_rate = np.where(rate > 0, rate, 0) diff --git a/src/libecalc/domain/infrastructure/energy_components/asset/asset.py b/src/libecalc/domain/infrastructure/energy_components/asset/asset.py index 20cc273052..e7fd13fe54 100644 --- a/src/libecalc/domain/infrastructure/energy_components/asset/asset.py +++ b/src/libecalc/domain/infrastructure/energy_components/asset/asset.py @@ -1,26 +1,28 @@ from typing import Literal -from pydantic import Field - from libecalc.application.energy.energy_component import EnergyComponent from libecalc.common.component_type import ComponentType from libecalc.common.string.string_utils import generate_id from libecalc.domain.infrastructure.energy_components.base.component_dto import Component from libecalc.domain.infrastructure.energy_components.installation.installation import Installation from libecalc.dto.component_graph import ComponentGraph -from libecalc.dto.utils.validators import ComponentNameStr class Asset(Component, EnergyComponent): + def __init__( + self, + name: str, + installations: list[Installation], + component_type: Literal[ComponentType.ASSET] = ComponentType.ASSET, + ): + self.name = name + self.installations = installations + self.component_type = component_type + @property def id(self): return generate_id(self.name) - name: ComponentNameStr - - installations: list[Installation] = Field(default_factory=list) - component_type: Literal[ComponentType.ASSET] = ComponentType.ASSET - def is_fuel_consumer(self) -> bool: return True diff --git a/src/libecalc/domain/infrastructure/energy_components/base/component_dto.py b/src/libecalc/domain/infrastructure/energy_components/base/component_dto.py index 67fe1a8eaa..f3882becfe 100644 --- a/src/libecalc/domain/infrastructure/energy_components/base/component_dto.py +++ b/src/libecalc/domain/infrastructure/energy_components/base/component_dto.py @@ -1,28 +1,31 @@ -from abc import ABC, abstractmethod -from typing import Optional +from __future__ import annotations -from pydantic import ConfigDict, Field, field_validator -from pydantic_core.core_schema import ValidationInfo +from abc import ABC, abstractmethod +from typing import Optional, Union +from libecalc.application.energy.emitter import Emitter +from libecalc.application.energy.energy_component import EnergyComponent from libecalc.common.component_type import ComponentType from libecalc.common.consumption_type import ConsumptionType from libecalc.common.string.string_utils import generate_id from libecalc.common.time_utils import Period from libecalc.common.units import Unit from libecalc.common.utils.rates import RateType +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) from libecalc.domain.infrastructure.energy_components.utils import _convert_keys_in_dictionary_from_str_to_periods -from libecalc.dto.base import EcalcBaseModel from libecalc.dto.fuel_type import FuelType from libecalc.dto.types import ConsumerUserDefinedCategoryType from libecalc.dto.utils.validators import ( - ComponentNameStr, ExpressionType, validate_temporal_model, ) from libecalc.expression import Expression -class Component(EcalcBaseModel, ABC): +class Component(ABC): component_type: ComponentType @property @@ -31,13 +34,11 @@ def id(self) -> str: ... class BaseComponent(Component, ABC): - name: ComponentNameStr + def __init__(self, name: str, regularity: dict[Period, Expression]): + self.name = name + self.regularity = self.check_regularity(regularity) + validate_temporal_model(self.regularity) - regularity: dict[Period, Expression] - - _validate_base_temporal_model = field_validator("regularity")(validate_temporal_model) - - @field_validator("regularity", mode="before") @classmethod def check_regularity(cls, regularity): if isinstance(regularity, dict) and len(regularity.values()) > 0: @@ -46,61 +47,116 @@ def check_regularity(cls, regularity): class BaseEquipment(BaseComponent, ABC): - user_defined_category: dict[Period, ConsumerUserDefinedCategoryType] = Field(..., validate_default=True) + def __init__( + self, + name: str, + regularity: dict[Period, Expression], + user_defined_category: dict[Period, ConsumerUserDefinedCategoryType], + component_type: ComponentType, + energy_usage_model: Optional[dict[Period, Expression]] = None, + fuel: Optional[dict[Period, FuelType]] = None, + generator_set_model: Optional[dict[Period, Union[BaseEquipment, Emitter, EnergyComponent]]] = None, + consumers: Optional[list[BaseEquipment]] = None, + cable_loss: Optional[Expression] = None, + max_usage_from_shore: Optional[Expression] = None, + ): + super().__init__(name, regularity) + self.user_defined_category = self.check_user_defined_category(user_defined_category, name) + self.energy_usage_model = energy_usage_model + self.component_type = component_type + self.fuel = fuel + self.generator_set_model = generator_set_model + self.consumers = consumers + self.cable_loss = cable_loss + self.max_usage_from_shore = max_usage_from_shore @property def id(self) -> str: return generate_id(self.name) - @field_validator("user_defined_category", mode="before") - def check_user_defined_category(cls, user_defined_category, info: ValidationInfo): + @classmethod + def check_user_defined_category( + cls, user_defined_category, name: str + ): # TODO: Check if this is needed. Should be handled in yaml validation """Provide which value and context to make it easier for user to correct wrt mandatory changes.""" + if isinstance(user_defined_category, dict) and len(user_defined_category.values()) > 0: user_defined_category = _convert_keys_in_dictionary_from_str_to_periods(user_defined_category) for user_category in user_defined_category.values(): if user_category not in list(ConsumerUserDefinedCategoryType): - name_context_str = "" - if (name := info.data.get("name")) is not None: - name_context_str = f"with the name {name}" - - raise ValueError( - f"CATEGORY: {user_category} is not allowed for {cls.__name__} {name_context_str}. Valid categories are: {[(consumer_user_defined_category.value) for consumer_user_defined_category in ConsumerUserDefinedCategoryType]}" + msg = ( + f"CATEGORY: {user_category} is not allowed for {cls.__name__}. Valid categories are: " + f"{[(consumer_user_defined_category.value) for consumer_user_defined_category in ConsumerUserDefinedCategoryType]}" ) + raise ComponentValidationException( + errors=[ + ModelValidationError( + name=name, + message=str(msg), + ) + ] + ) return user_defined_category class BaseConsumer(BaseEquipment, ABC): """Base class for all consumers.""" - consumes: ConsumptionType - fuel: Optional[dict[Period, FuelType]] = None + def __init__( + self, + name: str, + regularity: dict[Period, Expression], + consumes: ConsumptionType, + user_defined_category: dict[Period, ConsumerUserDefinedCategoryType], + component_type: ComponentType, + energy_usage_model: Optional[dict[Period, Expression]] = None, + fuel: Optional[dict[Period, FuelType]] = None, + ): + super().__init__(name, regularity, user_defined_category, component_type, energy_usage_model, fuel) + + self.fuel = self.validate_fuel_exist(name=self.name, fuel=fuel, consumes=consumes) + self.consumes = consumes - @field_validator("fuel", mode="before") @classmethod - def validate_fuel_exist(cls, fuel, info: ValidationInfo): + def validate_fuel_exist(cls, name: str, fuel: Optional[dict[Period, FuelType]], consumes: ConsumptionType): """ Make sure fuel is set if consumption type is FUEL. """ if isinstance(fuel, dict) and len(fuel.values()) > 0: fuel = _convert_keys_in_dictionary_from_str_to_periods(fuel) - if info.data.get("consumes") == ConsumptionType.FUEL and (fuel is None or len(fuel) < 1): - msg = f"Missing fuel for fuel consumer '{info.data.get('name')}'" - raise ValueError(msg) + if consumes == ConsumptionType.FUEL and (fuel is None or len(fuel) < 1): + msg = "Missing fuel for fuel consumer" + raise ComponentValidationException( + errors=[ + ModelValidationError( + name=name, + message=str(msg), + ) + ], + ) return fuel -class ExpressionTimeSeries(EcalcBaseModel): - value: ExpressionType - unit: Unit - type: Optional[RateType] = None +class ExpressionTimeSeries: + def __init__(self, value: ExpressionType, unit: Unit, type: Optional[RateType] = None): + self.value = value + self.unit = unit + self.type = type -class ExpressionStreamConditions(EcalcBaseModel): - rate: Optional[ExpressionTimeSeries] = None - pressure: Optional[ExpressionTimeSeries] = None - temperature: Optional[ExpressionTimeSeries] = None - fluid_density: Optional[ExpressionTimeSeries] = None +class ExpressionStreamConditions: + def __init__( + self, + rate: Optional[ExpressionTimeSeries] = None, + pressure: Optional[ExpressionTimeSeries] = None, + temperature: Optional[ExpressionTimeSeries] = None, + fluid_density: Optional[ExpressionTimeSeries] = None, + ): + self.rate = rate + self.pressure = pressure + self.temperature = temperature + self.fluid_density = fluid_density ConsumerID = str @@ -110,13 +166,13 @@ class ExpressionStreamConditions(EcalcBaseModel): SystemStreamConditions = dict[ConsumerID, dict[StreamID, ExpressionStreamConditions]] -class Crossover(EcalcBaseModel): - model_config = ConfigDict(populate_by_name=True) - - stream_name: Optional[str] = Field(None) - from_component_id: str - to_component_id: str +class Crossover: + def __init__(self, from_component_id: str, to_component_id: str, stream_name: Optional[str] = None): + self.stream_name = stream_name + self.from_component_id = from_component_id + self.to_component_id = to_component_id -class SystemComponentConditions(EcalcBaseModel): - crossover: list[Crossover] +class SystemComponentConditions: + def __init__(self, crossover: list[Crossover]): + self.crossover = crossover diff --git a/src/libecalc/domain/infrastructure/energy_components/common.py b/src/libecalc/domain/infrastructure/energy_components/common.py index fed67e8fd3..c0ab6e4f37 100644 --- a/src/libecalc/domain/infrastructure/energy_components/common.py +++ b/src/libecalc/domain/infrastructure/energy_components/common.py @@ -1,6 +1,4 @@ -from typing import Annotated, Literal, Optional, TypeVar, Union - -from pydantic import ConfigDict, Field +from typing import Literal, Optional, TypeVar, Union from libecalc.common.component_type import ComponentType from libecalc.domain.infrastructure.energy_components.asset.asset import Asset @@ -14,10 +12,9 @@ from libecalc.domain.infrastructure.energy_components.generator_set.generator_set_dto import GeneratorSet from libecalc.domain.infrastructure.energy_components.installation.installation import Installation from libecalc.domain.infrastructure.energy_components.pump.component_dto import PumpComponent -from libecalc.dto.base import EcalcBaseModel from libecalc.expression import Expression -Consumer = Annotated[Union[FuelConsumer, ElectricityConsumer], Field(discriminator="consumes")] +Consumer = Union[FuelConsumer, ElectricityConsumer] ComponentDTO = Union[ Asset, @@ -31,36 +28,46 @@ ] -class CompressorOperationalSettings(EcalcBaseModel): - rate: Expression - inlet_pressure: Expression - outlet_pressure: Expression - +class CompressorOperationalSettings: + def __init__(self, rate: Expression, inlet_pressure: Expression, outlet_pressure: Expression): + self.rate = rate + self.inlet_pressure = inlet_pressure + self.outlet_pressure = outlet_pressure -class PumpOperationalSettings(EcalcBaseModel): - rate: Expression - inlet_pressure: Expression - outlet_pressure: Expression - fluid_density: Expression +class PumpOperationalSettings: + def __init__( + self, rate: Expression, inlet_pressure: Expression, outlet_pressure: Expression, fluid_density: Expression + ): + self.rate = rate + self.inlet_pressure = inlet_pressure + self.outlet_pressure = outlet_pressure + self.fluid_density = fluid_density -class Stream(EcalcBaseModel): - model_config = ConfigDict(populate_by_name=True) - stream_name: Optional[str] = Field(None) - from_component_id: str - to_component_id: str +class Stream: + def __init__(self, from_component_id: str, to_component_id: str, stream_name: Optional[str] = None): + self.stream_name = stream_name + self.from_component_id = from_component_id + self.to_component_id = to_component_id ConsumerComponent = TypeVar("ConsumerComponent", bound=Union[CompressorComponent, PumpComponent]) class TrainComponent(BaseConsumer): - component_type: Literal[ComponentType.TRAIN_V2] = Field( - ComponentType.TRAIN_V2, - title="TYPE", - description="The type of the component", - alias="TYPE", - ) - stages: list[ConsumerComponent] - streams: list[Stream] + component_type: Literal[ComponentType.TRAIN_V2] = ComponentType.TRAIN_V2 + + def __init__( + self, + name: str, + regularity: dict, + consumes, + user_defined_category: dict, + component_type: ComponentType, + stages: list, + streams: list, + ): + super().__init__(name, regularity, consumes, user_defined_category, component_type) + self.stages = stages + self.streams = streams diff --git a/src/libecalc/domain/infrastructure/energy_components/component_validation_error.py b/src/libecalc/domain/infrastructure/energy_components/component_validation_error.py new file mode 100644 index 0000000000..50deebe996 --- /dev/null +++ b/src/libecalc/domain/infrastructure/energy_components/component_validation_error.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from typing import Optional + +import yaml + +from libecalc.presentation.yaml.file_context import FileContext +from libecalc.presentation.yaml.validation_errors import Location + + +@dataclass +class ModelValidationError: + message: str + name: Optional[str] = None + location: Optional[Location] = None + data: Optional[dict] = None + file_context: Optional[FileContext] = None + + @property + def yaml(self) -> Optional[str]: + if self.data is None: + return None + + return yaml.dump(self.data, sort_keys=False).strip() + + def error_message(self): + msg = "" + if self.file_context is not None: + msg += f"Object starting on line {self.file_context.start.line_number}\n" + yaml = self.yaml + if yaml is not None: + msg += "...\n" + msg += yaml + msg += "\n...\n\n" + + if self.location is not None and not self.location.is_empty(): + msg += f"Location: {self.location.as_dot_separated()}\n" + + if self.name is not None: + msg += f"Name: {self.name}\n" + + msg += f"Message: {self.message}\n" + return msg + + def __str__(self): + return self.error_message() + + +class ComponentValidationException(Exception): + def __init__(self, errors: list[ModelValidationError]): + self._errors = errors + + def errors(self) -> list[ModelValidationError]: + return self._errors diff --git a/src/libecalc/domain/infrastructure/energy_components/consumer_system/consumer_system_dto.py b/src/libecalc/domain/infrastructure/energy_components/consumer_system/consumer_system_dto.py index e52fd433e3..674db9df33 100644 --- a/src/libecalc/domain/infrastructure/energy_components/consumer_system/consumer_system_dto.py +++ b/src/libecalc/domain/infrastructure/energy_components/consumer_system/consumer_system_dto.py @@ -1,8 +1,6 @@ from collections import defaultdict from typing import Literal, Optional, Union -from pydantic import Field - from libecalc.application.energy.component_energy_context import ComponentEnergyContext from libecalc.application.energy.emitter import Emitter from libecalc.application.energy.energy_component import EnergyComponent @@ -33,6 +31,10 @@ SystemComponentConditions, SystemStreamConditions, ) +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) from libecalc.domain.infrastructure.energy_components.compressor import Compressor from libecalc.domain.infrastructure.energy_components.compressor.component_dto import CompressorComponent from libecalc.domain.infrastructure.energy_components.consumer_system.consumer_system import ( @@ -41,20 +43,36 @@ from libecalc.domain.infrastructure.energy_components.fuel_model.fuel_model import FuelModel from libecalc.domain.infrastructure.energy_components.pump import Pump from libecalc.domain.infrastructure.energy_components.pump.component_dto import PumpComponent +from libecalc.dto import FuelType from libecalc.dto.component_graph import ComponentGraph +from libecalc.dto.types import ConsumerUserDefinedCategoryType from libecalc.expression import Expression class ConsumerSystem(BaseConsumer, Emitter, EnergyComponent): - component_type: Literal[ComponentType.CONSUMER_SYSTEM_V2] = Field( - ComponentType.CONSUMER_SYSTEM_V2, - title="TYPE", - description="The type of the component", - ) - - component_conditions: SystemComponentConditions - stream_conditions_priorities: Priorities[SystemStreamConditions] - consumers: Union[list[CompressorComponent], list[PumpComponent]] + def __init__( + self, + name: str, + user_defined_category: dict[Period, ConsumerUserDefinedCategoryType], + regularity: dict[Period, Expression], + consumes: ConsumptionType, + component_conditions: SystemComponentConditions, + stream_conditions_priorities: Priorities[SystemStreamConditions], + consumers: Union[list[CompressorComponent], list[PumpComponent]], + fuel: Optional[dict[Period, FuelType]] = None, + component_type: Literal[ComponentType.CONSUMER_SYSTEM_V2] = ComponentType.CONSUMER_SYSTEM_V2, + ): + super().__init__( + component_type=component_type, + name=name, + user_defined_category=user_defined_category, + regularity=regularity, + consumes=consumes, + fuel=fuel, + ) + self.component_conditions = component_conditions + self.stream_conditions_priorities = stream_conditions_priorities + self.consumers = consumers def is_fuel_consumer(self) -> bool: return self.consumes == ConsumptionType.FUEL @@ -193,34 +211,40 @@ def evaluate_stream_conditions( periods=expression_evaluator.get_periods(), values=list( expression_evaluator.evaluate( - Expression.setup_from_expression(stream_conditions.rate.value) + Expression.setup_from_expression(stream_conditions["rate"].value) ) ), - unit=stream_conditions.rate.unit, + unit=stream_conditions["rate"].unit, ) - if stream_conditions.rate is not None + if stream_conditions and "rate" in stream_conditions and stream_conditions["rate"] is not None else None, pressure=TimeSeriesFloat( periods=expression_evaluator.get_periods(), values=list( expression_evaluator.evaluate( - expression=Expression.setup_from_expression(stream_conditions.pressure.value) + expression=Expression.setup_from_expression(stream_conditions["pressure"].value) ) ), - unit=stream_conditions.pressure.unit, + unit=stream_conditions["pressure"].unit, ) - if stream_conditions.pressure is not None + if stream_conditions + and "pressure" in stream_conditions + and stream_conditions["pressure"] is not None else None, fluid_density=TimeSeriesFloat( periods=expression_evaluator.get_periods(), values=list( expression_evaluator.evaluate( - expression=Expression.setup_from_expression(stream_conditions.fluid_density.value) + expression=Expression.setup_from_expression( + stream_conditions["fluid_density"].value + ) ) ), - unit=stream_conditions.fluid_density.unit, + unit=stream_conditions["fluid_density"].unit, ) - if stream_conditions.fluid_density is not None + if stream_conditions + and "fluid_density" in stream_conditions + and stream_conditions["fluid_density"] is not None else None, ) for stream_name, stream_conditions in streams_conditions.items() @@ -241,16 +265,23 @@ def create_consumer( model_for_period = energy_usage_model if model_for_period is None: - raise ValueError(f"Could not find model for consumer {consumer.name} at timestep {period}") + raise ComponentValidationException( + errors=[ + ModelValidationError( + name=consumer.name, + message=f"Could not find model at timestep {period}", + ) + ] + ) - if consumer.component_type == ComponentType.COMPRESSOR: + if consumer.component_type in {ComponentType.COMPRESSOR, ComponentType.COMPRESSOR_V2}: return Compressor( id=consumer.id, compressor_model=create_compressor_model( compressor_model_dto=model_for_period, ), ) - elif consumer.component_type == ComponentType.PUMP: + elif consumer.component_type in {ComponentType.PUMP, ComponentType.PUMP_V2}: return Pump( id=consumer.id, pump_model=create_pump_model( diff --git a/src/libecalc/domain/infrastructure/energy_components/electricity_consumer/electricity_consumer.py b/src/libecalc/domain/infrastructure/energy_components/electricity_consumer/electricity_consumer.py index 0ea74a54ae..b4cb926c2c 100644 --- a/src/libecalc/domain/infrastructure/energy_components/electricity_consumer/electricity_consumer.py +++ b/src/libecalc/domain/infrastructure/energy_components/electricity_consumer/electricity_consumer.py @@ -1,7 +1,5 @@ from typing import Literal -from pydantic import field_validator - from libecalc.application.energy.component_energy_context import ComponentEnergyContext from libecalc.application.energy.energy_component import EnergyComponent from libecalc.common.component_type import ComponentType @@ -21,28 +19,31 @@ check_model_energy_usage_type, ) from libecalc.dto.models import ElectricEnergyUsageModel +from libecalc.dto.types import ConsumerUserDefinedCategoryType from libecalc.dto.utils.validators import validate_temporal_model +from libecalc.expression import Expression class ElectricityConsumer(BaseConsumer, EnergyComponent): - component_type: Literal[ - ComponentType.COMPRESSOR, - ComponentType.PUMP, - ComponentType.GENERIC, - ComponentType.PUMP_SYSTEM, - ComponentType.COMPRESSOR_SYSTEM, - ] - consumes: Literal[ConsumptionType.ELECTRICITY] = ConsumptionType.ELECTRICITY - energy_usage_model: dict[ - Period, - ElectricEnergyUsageModel, - ] - - _validate_el_consumer_temporal_model = field_validator("energy_usage_model")(validate_temporal_model) - - _check_model_energy_usage = field_validator("energy_usage_model")( - lambda data: check_model_energy_usage_type(data, EnergyUsageType.POWER) - ) + def __init__( + self, + name: str, + regularity: dict[Period, Expression], + user_defined_category: dict[Period, ConsumerUserDefinedCategoryType], + component_type: Literal[ + ComponentType.COMPRESSOR, + ComponentType.PUMP, + ComponentType.GENERIC, + ComponentType.PUMP_SYSTEM, + ComponentType.COMPRESSOR_SYSTEM, + ], + energy_usage_model: dict[Period, ElectricEnergyUsageModel], + consumes: Literal[ConsumptionType.ELECTRICITY] = ConsumptionType.ELECTRICITY, + ): + super().__init__(name, regularity, consumes, user_defined_category, component_type, energy_usage_model, None) + self.energy_usage_model = self.check_energy_usage_model(energy_usage_model) + self._validate_el_consumer_temporal_model(self.energy_usage_model) + self._check_model_energy_usage(self.energy_usage_model) def is_fuel_consumer(self) -> bool: return False @@ -83,12 +84,19 @@ def evaluate_energy_usage( return consumer_results - @field_validator("energy_usage_model", mode="before") - @classmethod - def check_energy_usage_model(cls, energy_usage_model): + @staticmethod + def check_energy_usage_model(energy_usage_model: dict[Period, ElectricEnergyUsageModel]): """ Make sure that temporal models are converted to Period objects if they are strings """ if isinstance(energy_usage_model, dict) and len(energy_usage_model.values()) > 0: energy_usage_model = _convert_keys_in_dictionary_from_str_to_periods(energy_usage_model) return energy_usage_model + + @staticmethod + def _validate_el_consumer_temporal_model(energy_usage_model: dict[Period, ElectricEnergyUsageModel]): + validate_temporal_model(energy_usage_model) + + @staticmethod + def _check_model_energy_usage(energy_usage_model: dict[Period, ElectricEnergyUsageModel]): + check_model_energy_usage_type(energy_usage_model, EnergyUsageType.POWER) diff --git a/src/libecalc/domain/infrastructure/energy_components/fuel_consumer/fuel_consumer.py b/src/libecalc/domain/infrastructure/energy_components/fuel_consumer/fuel_consumer.py index cac74245d7..fc1540f349 100644 --- a/src/libecalc/domain/infrastructure/energy_components/fuel_consumer/fuel_consumer.py +++ b/src/libecalc/domain/infrastructure/energy_components/fuel_consumer/fuel_consumer.py @@ -1,8 +1,5 @@ from typing import Literal, Optional -from pydantic import field_validator -from pydantic_core.core_schema import ValidationInfo - from libecalc.application.energy.component_energy_context import ComponentEnergyContext from libecalc.application.energy.emitter import Emitter from libecalc.application.energy.energy_component import EnergyComponent @@ -27,23 +24,31 @@ ) from libecalc.dto.fuel_type import FuelType from libecalc.dto.models import FuelEnergyUsageModel +from libecalc.dto.types import ConsumerUserDefinedCategoryType from libecalc.dto.utils.validators import validate_temporal_model +from libecalc.expression import Expression class FuelConsumer(BaseConsumer, Emitter, EnergyComponent): - component_type: Literal[ - ComponentType.COMPRESSOR, - ComponentType.GENERIC, - ComponentType.COMPRESSOR_SYSTEM, - ] - consumes: Literal[ConsumptionType.FUEL] = ConsumptionType.FUEL - fuel: dict[Period, FuelType] - energy_usage_model: dict[Period, FuelEnergyUsageModel] - - _validate_fuel_consumer_temporal_models = field_validator("energy_usage_model", "fuel")(validate_temporal_model) - _check_model_energy_usage = field_validator("energy_usage_model")( - lambda data: check_model_energy_usage_type(data, EnergyUsageType.FUEL) - ) + def __init__( + self, + name: str, + regularity: dict[Period, Expression], + user_defined_category: dict[Period, ConsumerUserDefinedCategoryType], + component_type: Literal[ + ComponentType.COMPRESSOR, + ComponentType.GENERIC, + ComponentType.COMPRESSOR_SYSTEM, + ], + fuel: dict[Period, FuelType], + energy_usage_model: dict[Period, FuelEnergyUsageModel], + consumes: Literal[ConsumptionType.FUEL] = ConsumptionType.FUEL, + ): + super().__init__(name, regularity, consumes, user_defined_category, component_type, energy_usage_model, fuel) + self.fuel = self.check_fuel(fuel) + self.energy_usage_model = self.check_energy_usage_model(energy_usage_model) + self._validate_fuel_consumer_temporal_models(self.energy_usage_model, self.fuel) + self._check_model_energy_usage(self.energy_usage_model) def is_fuel_consumer(self) -> bool: return True @@ -100,9 +105,8 @@ def evaluate_emissions( fuel_rate=fuel_usage.values, ) - @field_validator("energy_usage_model", mode="before") - @classmethod - def check_energy_usage_model(cls, energy_usage_model, info: ValidationInfo): + @staticmethod + def check_energy_usage_model(energy_usage_model: dict[Period, FuelEnergyUsageModel]): """ Make sure that temporal models are converted to Period objects if they are strings """ @@ -110,12 +114,22 @@ def check_energy_usage_model(cls, energy_usage_model, info: ValidationInfo): energy_usage_model = _convert_keys_in_dictionary_from_str_to_periods(energy_usage_model) return energy_usage_model - @field_validator("fuel", mode="before") - @classmethod - def check_fuel(cls, fuel): + @staticmethod + def check_fuel(fuel: dict[Period, FuelType]): """ Make sure that temporal models are converted to Period objects if they are strings """ if isinstance(fuel, dict) and len(fuel.values()) > 0: fuel = _convert_keys_in_dictionary_from_str_to_periods(fuel) return fuel + + @staticmethod + def _validate_fuel_consumer_temporal_models( + energy_usage_model: dict[Period, FuelEnergyUsageModel], fuel: dict[Period, FuelType] + ): + validate_temporal_model(energy_usage_model) + validate_temporal_model(fuel) + + @staticmethod + def _check_model_energy_usage(energy_usage_model: dict[Period, FuelEnergyUsageModel]): + check_model_energy_usage_type(energy_usage_model, EnergyUsageType.FUEL) diff --git a/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set.py b/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set.py index ba4c254469..0503f6ae86 100644 --- a/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set.py +++ b/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set.py @@ -18,6 +18,10 @@ from libecalc.common.variables import ExpressionEvaluator from libecalc.core.models.generator import GeneratorModelSampled from libecalc.core.result import GeneratorSetResult +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) class Genset: @@ -48,7 +52,14 @@ def evaluate( assert power_requirement.unit == Unit.MEGA_WATT if not len(power_requirement) == len(expression_evaluator.get_periods()): - raise ValueError("length of power_requirement does not match the time vector.") + raise ComponentValidationException( + errors=[ + ModelValidationError( + name=self.name, + message="length of power_requirement does not match the time vector.", + ) + ] + ) # Compute fuel consumption from power rate. fuel_rate = self.evaluate_fuel_rate( diff --git a/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set_dto.py b/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set_dto.py index 441a1f7791..13b3df33ff 100644 --- a/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set_dto.py +++ b/src/libecalc/domain/infrastructure/energy_components/generator_set/generator_set_dto.py @@ -1,7 +1,4 @@ -from typing import Annotated, Literal, Optional, Union - -from pydantic import Field, field_validator, model_validator -from pydantic_core.core_schema import ValidationInfo +from typing import Literal, Optional, Union from libecalc.application.energy.component_energy_context import ComponentEnergyContext from libecalc.application.energy.emitter import Emitter @@ -15,43 +12,58 @@ from libecalc.core.result import EcalcModelResult from libecalc.core.result.emission import EmissionResult from libecalc.domain.infrastructure.energy_components.base.component_dto import BaseEquipment +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) from libecalc.domain.infrastructure.energy_components.consumer_system.consumer_system_dto import ConsumerSystem from libecalc.domain.infrastructure.energy_components.electricity_consumer.electricity_consumer import ( ElectricityConsumer, ) +from libecalc.domain.infrastructure.energy_components.fuel_consumer.fuel_consumer import FuelConsumer from libecalc.domain.infrastructure.energy_components.fuel_model.fuel_model import FuelModel from libecalc.domain.infrastructure.energy_components.generator_set.generator_set import Genset from libecalc.domain.infrastructure.energy_components.utils import _convert_keys_in_dictionary_from_str_to_periods from libecalc.dto.component_graph import ComponentGraph from libecalc.dto.fuel_type import FuelType from libecalc.dto.models import GeneratorSetSampled +from libecalc.dto.types import ConsumerUserDefinedCategoryType from libecalc.dto.utils.validators import ( ExpressionType, validate_temporal_model, ) -from libecalc.presentation.yaml.ltp_validation import ( - validate_generator_set_power_from_shore, -) +from libecalc.expression import Expression class GeneratorSet(BaseEquipment, Emitter, EnergyComponent): - component_type: Literal[ComponentType.GENERATOR_SET] = ComponentType.GENERATOR_SET - fuel: dict[Period, FuelType] - generator_set_model: dict[Period, GeneratorSetSampled] - consumers: list[ - Annotated[ - Union[ElectricityConsumer, ConsumerSystem], - Field(discriminator="component_type"), - ] - ] = Field(default_factory=list) - cable_loss: Optional[ExpressionType] = Field( - None, - title="CABLE_LOSS", - description="Power loss in cables from shore. " "Used to calculate onshore delivery/power supply onshore.", - ) - max_usage_from_shore: Optional[ExpressionType] = Field( - None, title="MAX_USAGE_FROM_SHORE", description="The peak load/effect that is expected for one hour, per year." - ) + def __init__( + self, + name: str, + user_defined_category: dict[Period, ConsumerUserDefinedCategoryType], + generator_set_model: dict[Period, GeneratorSetSampled], + regularity: dict[Period, Expression], + consumers: list[Union[ElectricityConsumer, ConsumerSystem]] = None, + fuel: dict[Period, FuelType] = None, + cable_loss: Optional[ExpressionType] = None, + max_usage_from_shore: Optional[ExpressionType] = None, + component_type: Literal[ComponentType.GENERATOR_SET] = ComponentType.GENERATOR_SET, + ): + super().__init__( + name, + regularity, + user_defined_category, + ComponentType.GENERATOR_SET, + generator_set_model=generator_set_model, + fuel=fuel, + ) + self.generator_set_model = self.check_generator_set_model(generator_set_model) + self.fuel = self.check_fuel(fuel) + self.consumers = consumers if consumers is not None else [] + self.cable_loss = cable_loss + self.max_usage_from_shore = max_usage_from_shore + self.component_type = component_type + self._validate_genset_temporal_models(self.generator_set_model, self.fuel) + self.check_consumers() def is_fuel_consumer(self) -> bool: return True @@ -118,29 +130,21 @@ def evaluate_emissions( fuel_rate=fuel_usage.values, ) - _validate_genset_temporal_models = field_validator("generator_set_model", "fuel")(validate_temporal_model) + @staticmethod + def _validate_genset_temporal_models( + generator_set_model: dict[Period, GeneratorSetSampled], fuel: dict[Period, FuelType] + ): + validate_temporal_model(generator_set_model) + validate_temporal_model(fuel) - @field_validator("user_defined_category", mode="before") - @classmethod - def check_mandatory_category_for_generator_set(cls, user_defined_category, info: ValidationInfo): - """This could be handled automatically with Pydantic, but I want to inform the users in a better way, in - particular since we introduced a breaking change for this to be mandatory for GeneratorSets in v7.2. - """ - if user_defined_category is None or user_defined_category == "": - raise ValueError(f"CATEGORY is mandatory and must be set for '{info.data.get('name', cls.__name__)}'") - - return user_defined_category - - @field_validator("generator_set_model", mode="before") - @classmethod - def check_generator_set_model(cls, generator_set_model, info: ValidationInfo): + @staticmethod + def check_generator_set_model(generator_set_model: dict[Period, GeneratorSetSampled]): if isinstance(generator_set_model, dict) and len(generator_set_model.values()) > 0: generator_set_model = _convert_keys_in_dictionary_from_str_to_periods(generator_set_model) return generator_set_model - @field_validator("fuel", mode="before") - @classmethod - def check_fuel(cls, fuel, info: ValidationInfo): + @staticmethod + def check_fuel(fuel: dict[Period, FuelType]): """ Make sure that temporal models are converted to Period objects if they are strings """ @@ -148,16 +152,20 @@ def check_fuel(cls, fuel, info: ValidationInfo): fuel = _convert_keys_in_dictionary_from_str_to_periods(fuel) return fuel - @model_validator(mode="after") - def check_power_from_shore(self): - _check_power_from_shore_attributes = validate_generator_set_power_from_shore( - cable_loss=self.cable_loss, - max_usage_from_shore=self.max_usage_from_shore, - model_fields=self.model_fields, - category=self.user_defined_category, - ) + def check_consumers(self): + errors: list[ModelValidationError] = [] + for consumer in self.consumers: + if isinstance(consumer, FuelConsumer): + errors.append( + ModelValidationError( + name=consumer.name, + message="The consumer is not an electricity consumer. " + "Generators can not have fuel consumers.", + ) + ) - return self + if errors: + raise ComponentValidationException(errors=errors) def get_graph(self) -> ComponentGraph: graph = ComponentGraph() diff --git a/src/libecalc/domain/infrastructure/energy_components/installation/installation.py b/src/libecalc/domain/infrastructure/energy_components/installation/installation.py index 8048a5b520..7046bd8f48 100644 --- a/src/libecalc/domain/infrastructure/energy_components/installation/installation.py +++ b/src/libecalc/domain/infrastructure/energy_components/installation/installation.py @@ -1,13 +1,14 @@ -from typing import Annotated, Literal, Optional, Union - -from pydantic import Field, field_validator, model_validator -from pydantic_core.core_schema import ValidationInfo +from typing import Optional, Union from libecalc.application.energy.energy_component import EnergyComponent from libecalc.common.component_type import ComponentType from libecalc.common.string.string_utils import generate_id from libecalc.common.time_utils import Period from libecalc.domain.infrastructure.energy_components.base.component_dto import BaseComponent +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) from libecalc.domain.infrastructure.energy_components.consumer_system.consumer_system_dto import ConsumerSystem from libecalc.domain.infrastructure.energy_components.fuel_consumer.fuel_consumer import FuelConsumer from libecalc.domain.infrastructure.energy_components.generator_set.generator_set_dto import GeneratorSet @@ -25,17 +26,26 @@ class Installation(BaseComponent, EnergyComponent): - component_type: Literal[ComponentType.INSTALLATION] = ComponentType.INSTALLATION - - user_defined_category: Optional[InstallationUserDefinedCategoryType] = Field(default=None, validate_default=True) - hydrocarbon_export: dict[Period, Expression] - fuel_consumers: list[ - Annotated[ - Union[GeneratorSet, FuelConsumer, ConsumerSystem], - Field(discriminator="component_type"), - ] - ] = Field(default_factory=list) - venting_emitters: list[YamlVentingEmitter] = Field(default_factory=list) + def __init__( + self, + name: str, + regularity: dict[Period, Expression], + hydrocarbon_export: dict[Period, Expression], + fuel_consumers: list[Union[GeneratorSet, FuelConsumer, ConsumerSystem]], + venting_emitters: Optional[list[YamlVentingEmitter]] = None, + user_defined_category: Optional[InstallationUserDefinedCategoryType] = None, + ): + super().__init__(name, regularity) + self.hydrocarbon_export = self.convert_expression_installation(hydrocarbon_export) + self.regularity = self.convert_expression_installation(regularity) + self.fuel_consumers = fuel_consumers + self.user_defined_category = user_defined_category + self.component_type = ComponentType.INSTALLATION + self.validate_installation_temporal_model() + + if venting_emitters is None: + venting_emitters = [] + self.venting_emitters = venting_emitters def is_fuel_consumer(self) -> bool: return True @@ -60,36 +70,27 @@ def get_name(self) -> str: def id(self) -> str: return generate_id(self.name) - _validate_installation_temporal_model = field_validator("hydrocarbon_export")(validate_temporal_model) - - _convert_expression_installation = field_validator("regularity", "hydrocarbon_export", mode="before")( - convert_expression - ) - - @field_validator("user_defined_category", mode="before") - def check_user_defined_category(cls, user_defined_category, info: ValidationInfo): - # Provide which value and context to make it easier for user to correct wrt mandatory changes. - if user_defined_category is not None: - if user_defined_category not in list(InstallationUserDefinedCategoryType): - name_context_str = "" - if (name := info.data.get("name")) is not None: - name_context_str = f"with the name {name}" - - raise ValueError( - f"CATEGORY: {user_defined_category} is not allowed for {cls.__name__} {name_context_str}. Valid categories are: {[str(installation_user_defined_category.value) for installation_user_defined_category in InstallationUserDefinedCategoryType]}" - ) + def validate_installation_temporal_model(self): + return validate_temporal_model(self.hydrocarbon_export) - return user_defined_category + def convert_expression_installation(self, data): + # Implement the conversion logic here + return convert_expression(data) - @model_validator(mode="after") def check_fuel_consumers_or_venting_emitters_exist(self): try: if self.fuel_consumers or self.venting_emitters: return self except AttributeError: - raise ValueError( - 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.", + raise ComponentValidationException( + errors=[ + ModelValidationError( + name=self.name, + message=f"Keywords are missing:\n It is required to specify at least one of the keywords " + f"{EcalcYamlKeywords.fuel_consumers}, {EcalcYamlKeywords.generator_sets} or " + f"{EcalcYamlKeywords.installation_venting_emitters} in the model.", + ) + ] ) from None def get_graph(self) -> ComponentGraph: diff --git a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/consumer_function/results.py b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/consumer_function/results.py index 17370cc8ee..d9d0bc888e 100644 --- a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/consumer_function/results.py +++ b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/consumer_function/results.py @@ -1,55 +1,111 @@ from __future__ import annotations +import copy from abc import abstractmethod from enum import Enum from typing import Literal, Optional, Union import numpy as np -from pydantic import BaseModel, ConfigDict +from numpy.typing import NDArray from libecalc.common.logger import logger from libecalc.common.time_utils import Periods from libecalc.core.models.results.base import EnergyFunctionResult -from libecalc.core.utils.array_type import PydanticNDArray +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) from libecalc.domain.infrastructure.energy_components.legacy_consumer.consumer_function.types import ( ConsumerFunctionType, ) -class ConsumerFunctionResultBase(BaseModel): +class ConsumerFunctionResultBase: """Result object for ConsumerFunction. Units: energy_usage [MW] """ - typ: ConsumerFunctionType - - periods: Periods - is_valid: PydanticNDArray - energy_usage: PydanticNDArray - energy_usage_before_power_loss_factor: Optional[PydanticNDArray] = None - condition: Optional[PydanticNDArray] = None - power_loss_factor: Optional[PydanticNDArray] = None - energy_function_result: Optional[Union[EnergyFunctionResult, list[EnergyFunctionResult]]] = None - - # New! to support fuel to power rate...for e.g. compressors emulating turbine - power: Optional[PydanticNDArray] = None - model_config = ConfigDict(arbitrary_types_allowed=True) + def __init__( + self, + typ: ConsumerFunctionType, + periods: Periods, + is_valid: NDArray, + energy_usage: NDArray, + energy_usage_before_power_loss_factor: Optional[NDArray] = None, + condition: Optional[NDArray] = None, + power_loss_factor: Optional[NDArray] = None, + energy_function_result: Optional[Union[EnergyFunctionResult, list[EnergyFunctionResult]]] = None, + # New! to support fuel to power rate...for e.g. compressors emulating turbine + power: Optional[NDArray] = None, + ): + self.typ = typ + self.periods = periods + self.is_valid = is_valid + self.energy_usage = energy_usage + self.energy_usage_before_power_loss_factor = energy_usage_before_power_loss_factor + self.condition = condition + self.power_loss_factor = power_loss_factor + self.energy_function_result = energy_function_result + self.power = power @abstractmethod def extend(self, other: object) -> ConsumerFunctionResultBase: ... + def model_copy(self, deep: bool = False) -> ConsumerFunctionResultBase: + if deep: + return copy.deepcopy(self) + return copy.copy(self) + class ConsumerFunctionResult(ConsumerFunctionResultBase): - typ: Literal[ConsumerFunctionType.SINGLE] = ConsumerFunctionType.SINGLE # type: ignore[valid-type] + def __init__( + self, + periods: Periods, + is_valid: NDArray, + energy_usage: NDArray, + typ: Literal[ConsumerFunctionType.SINGLE] = ConsumerFunctionType.SINGLE, # type: ignore[valid-type] + energy_usage_before_power_loss_factor: Optional[NDArray] = None, + condition: Optional[NDArray] = None, + power_loss_factor: Optional[NDArray] = None, + energy_function_result: Optional[Union[EnergyFunctionResult, list[EnergyFunctionResult]]] = None, + power: Optional[NDArray] = None, + ): + super().__init__( + typ, + periods, + is_valid, + energy_usage, + energy_usage_before_power_loss_factor, + condition, + power_loss_factor, + energy_function_result, + power, + ) + self.typ = typ + self.periods = periods + self.is_valid = is_valid + self.energy_usage = energy_usage + self.energy_usage_before_power_loss_factor = energy_usage_before_power_loss_factor + self.condition = condition + self.power_loss_factor = power_loss_factor + self.energy_function_result = energy_function_result + self.power = power def extend(self, other) -> ConsumerFunctionResult: """This is used when merging different time slots when the energy function of a consumer changes over time.""" if not isinstance(self, type(other)): msg = "Mixing CONSUMER_SYSTEM with non-CONSUMER_SYSTEM is no longer supported." logger.warning(msg) - raise ValueError(msg) + raise ComponentValidationException( + errors=[ + ModelValidationError( + name=self.__repr_name__(), + message=msg, + ) + ] + ) for attribute, values in self.__dict__.items(): other_values = other.__getattribute__(attribute) diff --git a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/operational_setting.py b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/operational_setting.py index 8a38e88d70..2c7018b632 100644 --- a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/operational_setting.py +++ b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/operational_setting.py @@ -5,16 +5,14 @@ import numpy as np from numpy.typing import NDArray -from pydantic import BaseModel, ConfigDict, model_validator from libecalc.common.errors.exceptions import EcalcError, IncompatibleDataError from libecalc.common.logger import logger from libecalc.common.utils.rates import Rates -from libecalc.core.utils.array_type import PydanticNDArray from libecalc.expression import Expression -class ConsumerSystemOperationalSettingExpressions(BaseModel): +class ConsumerSystemOperationalSettingExpressions: """Each index of a setting is aligned with a consumer. The first consumer has rate self.rates[0], etc. cross_overs: Defines what consumer to send exceeding rates to (Warning! index starts at 1!). @@ -28,17 +26,25 @@ class ConsumerSystemOperationalSettingExpressions(BaseModel): Note that circular references is not possible. """ - rates: list[Expression] - suction_pressures: list[Expression] - discharge_pressures: list[Expression] - cross_overs: Optional[list[int]] = None - fluid_densities: Optional[list[Expression]] = None + def __init__( + self, + rates: list[Expression], + suction_pressures: list[Expression], + discharge_pressures: list[Expression], + cross_overs: Optional[list[int]] = None, + fluid_densities: Optional[list[Expression]] = None, + ): + self.rates = rates + self.suction_pressures = suction_pressures + self.discharge_pressures = discharge_pressures + self.cross_overs = cross_overs + self.fluid_densities = fluid_densities + self.check_list_length() @property def number_of_consumers(self): return len(self.rates) - @model_validator(mode="after") def check_list_length(self): def _log_error(field: str, field_values: list[Any], n_rates) -> None: msg = ( @@ -68,20 +74,36 @@ class CompressorSystemOperationalSettingExpressions(ConsumerSystemOperationalSet class PumpSystemOperationalSettingExpressions(ConsumerSystemOperationalSettingExpressions): - fluid_densities: list[Expression] + def __init__( + self, + rates: list[Expression], + suction_pressures: list[Expression], + fluid_densities: list[Expression], + discharge_pressures: list[Expression], + cross_overs: Optional[list[int]] = None, + ): + super().__init__(rates, suction_pressures, discharge_pressures, cross_overs) + self.fluid_densities = fluid_densities -class ConsumerSystemOperationalSetting(BaseModel): +class ConsumerSystemOperationalSetting: """Warning! The methods below are fragile to changes in attribute names and types.""" - rates: list[PydanticNDArray] - suction_pressures: list[PydanticNDArray] - discharge_pressures: list[PydanticNDArray] - cross_overs: Optional[list[int]] = None - fluid_densities: Optional[list[PydanticNDArray]] = None - model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) + def __init__( + self, + rates: list[NDArray[np.float64]], + suction_pressures: list[NDArray[np.float64]], + discharge_pressures: list[NDArray[np.float64]], + cross_overs: Optional[list[int]] = None, + fluid_densities: Optional[list[NDArray[np.float64]]] = None, + ): + self.rates = rates + self.suction_pressures = suction_pressures + self.discharge_pressures = discharge_pressures + self.cross_overs = cross_overs + self.fluid_densities = fluid_densities + self.check_list_length() - @model_validator(mode="after") def check_list_length(self): def _log_error(field: str, field_values: list[Any], n_rates: int) -> None: error_message = ( @@ -142,9 +164,25 @@ def set_rates_after_cross_over( data.update({"rates": rates_after_cross_over}) return self.__class__(**data) + def model_copy(self, update: dict) -> ConsumerSystemOperationalSetting: + """Create a copy of the current model with updates.""" + new_model = deepcopy(self) + for key, value in update.items(): + setattr(new_model, key, value) + return new_model + class CompressorSystemOperationalSetting(ConsumerSystemOperationalSetting): ... class PumpSystemOperationalSetting(ConsumerSystemOperationalSetting): - fluid_densities: list[PydanticNDArray] + def __init__( + self, + rates: list[NDArray[np.float64]], + suction_pressures: list[NDArray[np.float64]], + discharge_pressures: list[NDArray[np.float64]], + fluid_densities: list[NDArray[np.float64]], + cross_overs: Optional[list[int]] = None, + ): + super().__init__(rates, suction_pressures, discharge_pressures, cross_overs, fluid_densities) + self.fluid_densities = fluid_densities diff --git a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/results.py b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/results.py index 4417085bb3..4e3396bf05 100644 --- a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/results.py +++ b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/results.py @@ -1,17 +1,19 @@ from __future__ import annotations from enum import Enum -from typing import Literal, Optional, Union +from typing import Optional, Union import numpy as np from numpy.typing import NDArray -from pydantic import BaseModel, ConfigDict from libecalc.common.logger import logger from libecalc.common.time_utils import Periods -from libecalc.core.models.results import CompressorTrainResult, PumpModelResult +from libecalc.core.models.results import CompressorTrainResult, EnergyFunctionResult, PumpModelResult from libecalc.core.result.results import ConsumerModelResult -from libecalc.core.utils.array_type import PydanticNDArray +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) from libecalc.domain.infrastructure.energy_components.legacy_consumer.consumer_function.results import ( ConsumerFunctionResultBase, ) @@ -23,16 +25,17 @@ ) -class ConsumerSystemComponentResult(BaseModel): - name: str - consumer_model_result: Union[PumpModelResult, CompressorTrainResult] +class ConsumerSystemComponentResult: + def __init__(self, name: str, consumer_model_result: Union[PumpModelResult, CompressorTrainResult]): + self.name = name + self.consumer_model_result = consumer_model_result @property def energy_usage(self) -> list[Optional[float]]: return self.consumer_model_result.energy_usage @property - def power(self) -> PydanticNDArray: + def power(self) -> NDArray: if self.consumer_model_result.power is not None: return np.asarray(self.consumer_model_result.power) else: @@ -44,7 +47,8 @@ def rate(self) -> list[Optional[float]]: class PumpResult(ConsumerSystemComponentResult): - consumer_model_result: PumpModelResult + def __init__(self, name: str, consumer_model_result: PumpModelResult): + super().__init__(name, consumer_model_result) @property def fluid_density(self): @@ -52,15 +56,16 @@ def fluid_density(self): class CompressorResult(ConsumerSystemComponentResult): - consumer_model_result: Union[ConsumerModelResult, CompressorTrainResult] + def __init__(self, name: str, consumer_model_result: Union[ConsumerModelResult, CompressorTrainResult]): + super().__init__(name, consumer_model_result) -class ConsumerSystemOperationalSettingResult(BaseModel): - consumer_results: list[ConsumerSystemComponentResult] - model_config = ConfigDict(frozen=True) +class ConsumerSystemOperationalSettingResult: + def __init__(self, consumer_results: list[ConsumerSystemComponentResult]): + self.consumer_results = consumer_results @property - def total_energy_usage(self) -> PydanticNDArray: + def total_energy_usage(self) -> NDArray: total_energy_usage = np.sum( [np.asarray(result.energy_usage) for result in self.consumer_results], axis=0, @@ -68,7 +73,7 @@ def total_energy_usage(self) -> PydanticNDArray: return np.array(total_energy_usage) @property - def total_power(self) -> PydanticNDArray: + def total_power(self) -> NDArray: total_power = np.sum( [np.asarray(result.power) for result in self.consumer_results], axis=0, @@ -76,7 +81,7 @@ def total_power(self) -> PydanticNDArray: return np.array(total_power) @property - def indices_outside_capacity(self) -> PydanticNDArray: + def indices_outside_capacity(self) -> NDArray: invalid_indices = np.full_like(self.total_energy_usage, fill_value=0) for result in self.consumer_results: @@ -110,21 +115,52 @@ class ConsumerSystemConsumerFunctionResult(ConsumerFunctionResultBase): data for the energy usage of each consumer in the system in this operational setting. """ - typ: Literal[ConsumerFunctionType.SYSTEM] = ConsumerFunctionType.SYSTEM # type: ignore[valid-type] - - operational_setting_used: PydanticNDArray # integers in the range of number of operational settings - operational_settings: list[list[ConsumerSystemOperationalSetting]] - operational_settings_results: list[list[ConsumerSystemOperationalSettingResult]] - consumer_results: list[list[ConsumerSystemComponentResult]] - cross_over_used: Optional[PydanticNDArray] = ( - None # 0 or 1 whether cross over is used for this result (1=True, 0=False) - ) + def __init__( + self, + operational_setting_used: NDArray, + operational_settings: list[list[ConsumerSystemOperationalSetting]], + operational_settings_results: list[list[ConsumerSystemOperationalSettingResult]], + consumer_results: list[list[ConsumerSystemComponentResult]], + cross_over_used: Optional[NDArray] = None, + # 0 or 1 whether cross over is used for this result (1=True, 0=False) + periods: Periods = None, + is_valid: NDArray = None, + energy_usage: NDArray = None, + energy_usage_before_power_loss_factor: Optional[NDArray] = None, + condition: Optional[NDArray] = None, + power_loss_factor: Optional[NDArray] = None, + energy_function_result: Optional[Union[EnergyFunctionResult, list[EnergyFunctionResult]]] = None, + power: Optional[NDArray] = None, + ): + super().__init__( + typ=ConsumerFunctionType.SYSTEM, + periods=periods, + is_valid=is_valid, + energy_usage=energy_usage, + energy_usage_before_power_loss_factor=energy_usage_before_power_loss_factor, + condition=condition, + power_loss_factor=power_loss_factor, + energy_function_result=energy_function_result, + power=power, + ) + self.operational_setting_used = operational_setting_used + self.operational_settings = operational_settings + self.operational_settings_results = operational_settings_results + self.consumer_results = consumer_results + self.cross_over_used = cross_over_used def extend(self, other) -> ConsumerSystemConsumerFunctionResult: if not isinstance(self, type(other)): - msg = f"{self.__repr_name__()} Mixing CONSUMER_SYSTEM with non-CONSUMER_SYSTEM is no longer supported." + msg = "Mixing CONSUMER_SYSTEM with non-CONSUMER_SYSTEM is no longer supported." logger.warning(msg) - raise ValueError(msg) + raise ComponentValidationException( + errors=[ + ModelValidationError( + name=self.__repr_name__(), + message=msg, + ) + ] + ) for attribute, values in self.__dict__.items(): other_values = other.__getattribute__(attribute) diff --git a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py index 2e0b001bc1..cfc20355d0 100644 --- a/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py +++ b/src/libecalc/domain/infrastructure/energy_components/legacy_consumer/system/types.py @@ -1,12 +1,10 @@ from typing import Union -from pydantic import BaseModel, ConfigDict - from libecalc.core.models.compressor.base import CompressorModel from libecalc.core.models.pump import PumpModel -class ConsumerSystemComponent(BaseModel): - name: str - facility_model: Union[PumpModel, CompressorModel] - model_config = ConfigDict(arbitrary_types_allowed=True) +class ConsumerSystemComponent: + def __init__(self, name: str, facility_model: Union[PumpModel, CompressorModel]): + self.name = name + self.facility_model = facility_model diff --git a/src/libecalc/domain/infrastructure/energy_components/utils.py b/src/libecalc/domain/infrastructure/energy_components/utils.py index 02fedcab5a..79fc39c59d 100644 --- a/src/libecalc/domain/infrastructure/energy_components/utils.py +++ b/src/libecalc/domain/infrastructure/energy_components/utils.py @@ -3,13 +3,23 @@ from libecalc.common.energy_usage_type import EnergyUsageType from libecalc.common.time_utils import Period +from libecalc.domain.infrastructure.energy_components.component_validation_error import ( + ComponentValidationException, + ModelValidationError, +) from libecalc.dto.models import ConsumerFunction def check_model_energy_usage_type(model_data: dict[Period, ConsumerFunction], energy_type: EnergyUsageType): for model in model_data.values(): if model.energy_usage_type != energy_type: - raise ValueError(f"Model does not consume {energy_type.value}") + raise ComponentValidationException( + errors=[ + ModelValidationError( + message=f"Model does not consume {energy_type.value}", + ) + ] + ) return model_data diff --git a/src/libecalc/presentation/yaml/ltp_validation.py b/src/libecalc/presentation/yaml/ltp_validation.py index c5a8ae5fea..f57dd30130 100644 --- a/src/libecalc/presentation/yaml/ltp_validation.py +++ b/src/libecalc/presentation/yaml/ltp_validation.py @@ -9,21 +9,20 @@ def validate_generator_set_power_from_shore( cable_loss: ExpressionType, max_usage_from_shore: ExpressionType, - model_fields: dict, category: Union[dict, ConsumerUserDefinedCategoryType], ): if cable_loss is not None or max_usage_from_shore is not None: - feedback_text = ( - f"{model_fields['cable_loss'].title} and " f"{model_fields['max_usage_from_shore'].title} are only valid" - ) + CABLE_LOSS = "CABLE_LOSS" + MAX_USAGE_FROM_SHORE = "MAX_USAGE_FROM_SHORE" + feedback_text = f"{CABLE_LOSS} and " f"{MAX_USAGE_FROM_SHORE} are only valid" if cable_loss is None: - feedback_text = f"{model_fields['max_usage_from_shore'].title} is only valid" + feedback_text = f"{MAX_USAGE_FROM_SHORE} is only valid" if max_usage_from_shore is None: - feedback_text = f"{model_fields['cable_loss'].title} is only valid" + feedback_text = f"{CABLE_LOSS} is only valid" if isinstance(category, ConsumerUserDefinedCategoryType): if category is not ConsumerUserDefinedCategoryType.POWER_FROM_SHORE: - message = f"{feedback_text} for the category {ConsumerUserDefinedCategoryType.POWER_FROM_SHORE.value}, not for {category}." + message = f"{feedback_text} for the category {ConsumerUserDefinedCategoryType.POWER_FROM_SHORE.value}, not for {category.value}." raise ValueError(message) else: if ConsumerUserDefinedCategoryType.POWER_FROM_SHORE not in category.values(): diff --git a/src/libecalc/presentation/yaml/mappers/component_mapper.py b/src/libecalc/presentation/yaml/mappers/component_mapper.py index dfcdc02fae..af05783b1a 100644 --- a/src/libecalc/presentation/yaml/mappers/component_mapper.py +++ b/src/libecalc/presentation/yaml/mappers/component_mapper.py @@ -152,6 +152,7 @@ def from_yaml_to_dto( fuel=fuel, energy_usage_model=energy_usage_model, component_type=_get_component_type(energy_usage_model), + consumes=consumes, ) except ValidationError as e: raise DtoValidationError(data=data.model_dump(), validation_error=e) from e @@ -166,6 +167,7 @@ def from_yaml_to_dto( ), energy_usage_model=energy_usage_model, component_type=_get_component_type(energy_usage_model), + consumes=consumes, ) except ValidationError as e: raise DtoValidationError(data=data.model_dump(), validation_error=e) from e @@ -226,6 +228,7 @@ def from_yaml_to_dto( user_defined_category=user_defined_category, cable_loss=cable_loss, max_usage_from_shore=max_usage_from_shore, + component_type=ComponentType.GENERATOR_SET, # TODO: Check if this is correct. Why isnĀ“t component_type set for YamlGeneratorSet? ) except ValidationError as e: raise DtoValidationError(data=data.model_dump(), validation_error=e) from e diff --git a/src/libecalc/presentation/yaml/yaml_types/components/system/yaml_consumer_system.py b/src/libecalc/presentation/yaml/yaml_types/components/system/yaml_consumer_system.py index 7fdf6e9f5c..96a35e2461 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/system/yaml_consumer_system.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/system/yaml_consumer_system.py @@ -1,12 +1,17 @@ from datetime import datetime from typing import Annotated, Generic, Literal, Optional, TypeVar, Union -from pydantic import ConfigDict, Field, TypeAdapter +from pydantic import ConfigDict, Field from libecalc.common.component_type import ComponentType from libecalc.common.consumption_type import ConsumptionType +from libecalc.common.priorities import Priorities from libecalc.common.time_utils import Period, define_time_model_for_period -from libecalc.domain.infrastructure.energy_components.base.component_dto import Crossover, SystemComponentConditions +from libecalc.domain.infrastructure.energy_components.base.component_dto import ( + Crossover, + SystemComponentConditions, + SystemStreamConditions, +) from libecalc.domain.infrastructure.energy_components.consumer_system.consumer_system_dto import ConsumerSystem from libecalc.dto import FuelType from libecalc.expression import Expression @@ -68,6 +73,22 @@ class YamlConsumerSystem(YamlConsumerBase, Generic[TYamlConsumer]): consumers: list[TYamlConsumer] + @staticmethod + def convert_yaml_priorities(yaml_priorities: YamlPriorities) -> Priorities[SystemStreamConditions]: + priorities: Priorities[SystemStreamConditions] = {} + for priority_id, consumer_map in yaml_priorities.items(): + priorities[priority_id] = {} + for consumer_id, stream_conditions in consumer_map.items(): + priorities[priority_id][consumer_id] = { + stream_name: SystemStreamConditions( + rate=stream_conditions.rate, + pressure=stream_conditions.pressure, + fluid_density=stream_conditions.fluid_density, + ) + for stream_name, stream_conditions in stream_conditions.items() + } + return priorities + def to_dto( self, regularity: dict[datetime, Expression], @@ -114,7 +135,7 @@ def to_dto( regularity=regularity, consumes=consumes, component_conditions=component_conditions, - stream_conditions_priorities=TypeAdapter(YamlPriorities).dump_python( + stream_conditions_priorities=self.convert_yaml_priorities( self.stream_conditions_priorities ), # TODO: unnecessary, but we should remove the need to have dto here (two very similar classes) consumers=consumers, diff --git a/src/libecalc/presentation/yaml/yaml_types/components/yaml_compressor.py b/src/libecalc/presentation/yaml/yaml_types/components/yaml_compressor.py index b556ba7278..c8cc3dc4f0 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/yaml_compressor.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/yaml_compressor.py @@ -55,4 +55,5 @@ def to_dto( self.energy_usage_model, target_period=target_period ).items() }, + component_type=self.component_type, ) diff --git a/src/libecalc/presentation/yaml/yaml_types/components/yaml_generator_set.py b/src/libecalc/presentation/yaml/yaml_types/components/yaml_generator_set.py index 0e5e1f3a7a..6d7df964fc 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/yaml_generator_set.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/yaml_generator_set.py @@ -73,7 +73,6 @@ def check_power_from_shore(self): _check_power_from_shore_attributes = validate_generator_set_power_from_shore( cable_loss=self.cable_loss, max_usage_from_shore=self.max_usage_from_shore, - model_fields=self.model_fields, category=self.category, ) return self diff --git a/src/libecalc/presentation/yaml/yaml_types/components/yaml_pump.py b/src/libecalc/presentation/yaml/yaml_types/components/yaml_pump.py index 4a3b44d3b0..a2758915cd 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/yaml_pump.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/yaml_pump.py @@ -52,4 +52,5 @@ def to_dto( self.energy_usage_model, target_period=target_period ).items() }, + component_type=self.component_type, ) diff --git a/tests/libecalc/core/consumers/conftest.py b/tests/libecalc/core/consumers/conftest.py index e2a1e2e190..0efaf441d5 100644 --- a/tests/libecalc/core/consumers/conftest.py +++ b/tests/libecalc/core/consumers/conftest.py @@ -115,6 +115,7 @@ def genset_2mw_dto(fuel_dto, direct_el_consumer, generator_set_sampled_model_2mw }, consumers=[direct_el_consumer], regularity={Period(datetime(1900, 1, 1)): Expression.setup_from_expression(1)}, + component_type=ComponentType.GENERATOR_SET, ) @@ -129,4 +130,5 @@ def genset_1000mw_late_startup_dto(fuel_dto, direct_el_consumer, generator_set_s }, consumers=[direct_el_consumer], regularity={Period(datetime(1900, 1, 1)): Expression.setup_from_expression(1)}, + component_type=ComponentType.GENERATOR_SET, ) diff --git a/tests/libecalc/core/consumers/system/test_operational_setting.py b/tests/libecalc/core/consumers/system/test_operational_setting.py index 362a3d79b4..1bed384af7 100644 --- a/tests/libecalc/core/consumers/system/test_operational_setting.py +++ b/tests/libecalc/core/consumers/system/test_operational_setting.py @@ -23,7 +23,6 @@ def test_consumer_system_operational_settings_expression(): assert operational_settings_expression.number_of_consumers == number_of_consumers -@patch.multiple(ConsumerSystemOperationalSetting, __abstractmethods__=set()) class TestConsumerSystemOperationalSetting: def test_operational_setting(self): operational_settings = ConsumerSystemOperationalSetting( diff --git a/tests/libecalc/core/consumers/system/test_system_utils.py b/tests/libecalc/core/consumers/system/test_system_utils.py index 15efa55382..80d500cb59 100644 --- a/tests/libecalc/core/consumers/system/test_system_utils.py +++ b/tests/libecalc/core/consumers/system/test_system_utils.py @@ -329,9 +329,9 @@ def test_assemble_operational_setting_from_model_result_list(): operational_settings=operational_settings, setting_number_used_per_timestep=setting_number_used_per_timestep ) - assert result.rates[0].tolist() == [0, 31, 22, 13, 4] - assert result.rates[1].tolist() == [0, 31, 22, 13, 4] - assert result.suction_pressures[0].tolist() == [0, 31, 22, 13, 4] - assert result.suction_pressures[1].tolist() == [0, 31, 22, 13, 4] - assert result.discharge_pressures[0].tolist() == [0, 31, 22, 13, 4] - assert result.discharge_pressures[1].tolist() == [0, 31, 22, 13, 4] + assert np.array(result.rates[0]).tolist() == [0, 31, 22, 13, 4] + assert np.array(result.rates[1]).tolist() == [0, 31, 22, 13, 4] + assert np.array(result.suction_pressures[0]).tolist() == [0, 31, 22, 13, 4] + assert np.array(result.suction_pressures[1]).tolist() == [0, 31, 22, 13, 4] + assert np.array(result.discharge_pressures[0]).tolist() == [0, 31, 22, 13, 4] + assert np.array(result.discharge_pressures[1]).tolist() == [0, 31, 22, 13, 4] diff --git a/tests/libecalc/dto/test_electricity_consumer.py b/tests/libecalc/dto/test_electricity_consumer.py index fa412dd876..33d4a52412 100644 --- a/tests/libecalc/dto/test_electricity_consumer.py +++ b/tests/libecalc/dto/test_electricity_consumer.py @@ -1,19 +1,19 @@ from datetime import datetime import pytest -from pydantic import ValidationError from libecalc import dto from libecalc.domain.infrastructure import ElectricityConsumer from libecalc.common.component_type import ComponentType from libecalc.common.energy_usage_type import EnergyUsageType from libecalc.common.time_utils import Period +from libecalc.domain.infrastructure.energy_components.component_validation_error import ComponentValidationException from libecalc.expression import Expression class TestElectricityConsumer: def test_invalid_energy_usage(self): - with pytest.raises(ValidationError) as e: + with pytest.raises(ComponentValidationException) as e: ElectricityConsumer( name="Test", component_type=ComponentType.GENERIC, @@ -25,7 +25,7 @@ def test_invalid_energy_usage(self): }, regularity={Period(datetime(1900, 1, 1)): Expression.setup_from_expression(1)}, ) - assert "Model does not consume POWER" in str(e.value) + assert "Model does not consume POWER" in str(e.value.errors()[0]) def test_valid_electricity_consumer(self): # Should not raise ValidationError diff --git a/tests/libecalc/dto/test_fuel_consumer.py b/tests/libecalc/dto/test_fuel_consumer.py index 2ce4e75720..e17178125e 100644 --- a/tests/libecalc/dto/test_fuel_consumer.py +++ b/tests/libecalc/dto/test_fuel_consumer.py @@ -10,6 +10,7 @@ from libecalc.common.component_type import ComponentType from libecalc.common.energy_usage_type import EnergyUsageType from libecalc.common.time_utils import Period +from libecalc.domain.infrastructure.energy_components.component_validation_error import ComponentValidationException from libecalc.expression import Expression regularity = {Period(datetime(2000, 1, 1)): Expression.setup_from_expression(1)} @@ -93,7 +94,7 @@ def get_fuel_consumer( class TestFuelConsumer: def test_missing_fuel(self): - with pytest.raises(ValidationError) as exc_info: + with pytest.raises(ComponentValidationException) as exc_info: FuelConsumer( name="test", fuel={}, @@ -107,4 +108,4 @@ def test_missing_fuel(self): regularity=regularity, user_defined_category="category", ) - assert "Missing fuel for fuel consumer 'test'" in str(exc_info.value) + assert "Name: test\nMessage: Missing fuel for fuel consumer" in str(exc_info.value.errors()[0]) diff --git a/tests/libecalc/dto/test_generator_set.py b/tests/libecalc/dto/test_generator_set.py index 2ae1c9ea08..4cfe71eb4a 100644 --- a/tests/libecalc/dto/test_generator_set.py +++ b/tests/libecalc/dto/test_generator_set.py @@ -6,6 +6,7 @@ import libecalc.dto.fuel_type from libecalc import dto from libecalc.domain.infrastructure import GeneratorSet, FuelConsumer +from libecalc.domain.infrastructure.energy_components.component_validation_error import ComponentValidationException from libecalc.dto.models import GeneratorSetSampled from libecalc.common.component_type import ComponentType from libecalc.common.consumption_type import ConsumptionType @@ -14,6 +15,8 @@ from libecalc.common.time_utils import Period from libecalc.dto.types import ConsumerUserDefinedCategoryType from libecalc.expression import Expression +from libecalc.presentation.yaml.yaml_types.components.yaml_generator_set import YamlGeneratorSet +from libecalc.testing.yaml_builder import YamlGeneratorSetBuilder class TestGeneratorSetSampled: @@ -60,6 +63,7 @@ def test_valid(self): emissions=[], ) }, + component_type=ComponentType.GENERATOR_SET, ) assert generator_set_dto.generator_set_model == { Period(datetime(1900, 1, 1)): dto.GeneratorSetSampled( @@ -71,6 +75,7 @@ def test_valid(self): } def test_genset_should_fail_with_fuel_consumer(self): + """This validation is done in the dto layer""" fuel = libecalc.dto.fuel_type.FuelType( name="fuel", emissions=[], @@ -89,7 +94,7 @@ def test_genset_should_fail_with_fuel_consumer(self): regularity={Period(datetime(2000, 1, 1)): Expression.setup_from_expression(1)}, user_defined_category={Period(datetime(2000, 1, 1)): ConsumerUserDefinedCategoryType.MISCELLANEOUS}, ) - with pytest.raises(ValidationError): + with pytest.raises(ComponentValidationException): GeneratorSet( name="Test", user_defined_category={Period(datetime(1900, 1, 1)): ConsumerUserDefinedCategoryType.MISCELLANEOUS}, @@ -97,54 +102,38 @@ def test_genset_should_fail_with_fuel_consumer(self): regularity={}, consumers=[fuel_consumer], fuel={}, + component_type=ComponentType.GENERATOR_SET, ) def test_power_from_shore_wrong_category(self): """ Check that CABLE_LOSS and MAX_USAGE_FROM_SHORE are only allowed if generator set category is POWER-FROM-SHORE + This validation is done in the yaml layer. """ # Check for CABLE_LOSS with pytest.raises(ValueError) as exc_info: - GeneratorSet( - name="Test", - user_defined_category={Period(datetime(1900, 1, 1)): ConsumerUserDefinedCategoryType.BOILER}, - generator_set_model={}, - regularity={Period(datetime(1900, 1, 1)): Expression.setup_from_expression(1)}, - consumers=[], - fuel={}, - cable_loss=0, - ) + YamlGeneratorSetBuilder().with_test_data().with_category("BOILER").with_cable_loss(0).validate() assert ("CABLE_LOSS is only valid for the category POWER-FROM-SHORE, not for BOILER") in str(exc_info.value) # Check for MAX_USAGE_FROM_SHORE with pytest.raises(ValueError) as exc_info: - GeneratorSet( - name="Test", - user_defined_category={Period(datetime(1900, 1, 1)): ConsumerUserDefinedCategoryType.BOILER}, - generator_set_model={}, - regularity={Period(datetime(1900, 1, 1)): Expression.setup_from_expression(1)}, - consumers=[], - fuel={}, - max_usage_from_shore=20, - ) + YamlGeneratorSetBuilder().with_test_data().with_category("BOILER").with_max_usage_from_shore(20).validate() assert ("MAX_USAGE_FROM_SHORE is only valid for the category POWER-FROM-SHORE, not for BOILER") in str( exc_info.value ) + # Check for CABLE_LOSS and MAX_USAGE_FROM_SHORE with pytest.raises(ValueError) as exc_info: - GeneratorSet( - name="Test", - user_defined_category={Period(datetime(1900, 1, 1)): ConsumerUserDefinedCategoryType.BOILER}, - generator_set_model={}, - regularity={Period(datetime(1900, 1, 1)): Expression.setup_from_expression(1)}, - consumers=[], - fuel={}, - max_usage_from_shore=20, - cable_loss=0, - ) + ( + YamlGeneratorSetBuilder() + .with_test_data() + .with_category("BOILER") + .with_cable_loss(0) + .with_max_usage_from_shore(20) + ).validate() assert ( "CABLE_LOSS and MAX_USAGE_FROM_SHORE are only valid for the category POWER-FROM-SHORE, not for BOILER"