Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remove pydantic validation from dto classes #740

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions src/libecalc/core/models/model_input_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
(
Expand Down Expand Up @@ -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()


Expand Down
10 changes: 9 additions & 1 deletion src/libecalc/core/models/pump/pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should consider using asarray or asfarray? asarray won't copy when the input already is an array. asfarray will create a floating point ndarray, I'm not sure what that actually is array with dtype=float or something else.


# Ensure that the pump does not run when rate is <= 0.
stream_day_rate = np.where(rate > 0, rate, 0)

Expand Down
Original file line number Diff line number Diff line change
@@ -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__(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component is no longer necessary, but we would have to make sure not to use component_type directly when removing it. EnergyComponent has the same information as Component.

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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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__(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should look into removing BaseEquipment and BaseComponent. Instead we could create objects that contains several of these values if we think it makes sense to group them. Or just duplicate the init in each energy component.

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
Comment on lines +68 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move the generator specific stuff to generator set?


@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__(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also remove BaseConsumer I think. We don't want to use inheritance without being sure the abstraction makes sense, here it's just used to avoid having to specify the same params.

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
Expand All @@ -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
Loading