from abc import ABC, abstractmethod
from dataclasses import dataclass
from shilps.components import Component, DataTimeSeries, Entity, PVGenerator, TSParameter, InvData, TimeConfig, TSParameter
from typing import Union, List, Self, Dict
import numpy as np
[docs]
class CustomAttribute(ABC):
[docs]
@abstractmethod
def copy(self) -> Self:
pass
[docs]
@abstractmethod
def to_dict(self) -> dict:
pass
[docs]
@classmethod
@abstractmethod
def from_dict(cls, input):
pass
[docs]
@dataclass
class Range(CustomAttribute):
min: float
max: float
[docs]
def to_dict(self) -> dict:
return {'min': self.min, 'max': self.max}
[docs]
@classmethod
def from_dict(cls, input: Dict[str, float]):
return cls(**input)
[docs]
def copy(self) -> Self:
return Range(self.min, self.max)
[docs]
class Gain(CustomAttribute):
def __init__(self, breakpoints: List[float], slopes: List[float], intercepts: List[float]) -> None:
self.breakpoints = breakpoints
self.slopes = slopes
self.intercepts = intercepts
[docs]
def to_dict(self) -> Dict:
return {'breakpoints': self.breakpoints, 'slopes': self.slopes, 'intercepts': self.intercepts}
[docs]
def from_dict(cls, input: Dict[str, List[float]]):
return cls(**input)
[docs]
def copy(self) -> Self:
return Gain(self.breakpoints.copy(), self.slopes.copy(), self.intercepts.copy())
[docs]
def piecewise_linear(self, x):
"""
Evaluate a piecewise linear function.
Parameters
----------
x : array_like
Input array.
breakpoints : list or array_like
Breakpoints of the piecewise function.
slopes : list or array_like
Slopes of the linear pieces.
intercepts : list or array_like
Intercepts of the linear pieces.
Returns
-------
np.ndarray
Output array where each element is the result of the piecewise linear function.
"""
x = np.asarray(x)
conditions = [np.logical_and(self.breakpoints[i] <= x, x < self.breakpoints[i + 1]) for i in range(len(self.breakpoints) - 1)]
conditions.append(x >= self.breakpoints[-1]) # Handle the last interval including the right endpoint
functions = [lambda x, i=i: self.slopes[i] * x + self.intercepts[i] for i in range(len(self.slopes))]
functions.append(lambda x: self.slopes[-1] * x + self.intercepts[-1]) # Handle the last interval
return np.piecewise(x, conditions, functions)
@property
def domain(self):
return (self.breakpoints[0], self.breakpoints[-1])
def __call__(self, x):
return self.piecewise_linear(x)
[docs]
@dataclass(slots=True)
class GainComponent(Component):
gain: Gain = None
[docs]
@dataclass(slots=True)
class Electrolizer(GainComponent):
"""
Represents a hydrogen electrolizer.
Attributes:
-----------
pnom_kW: float
Nominal
pmin_pu: float
Minimum power consumption in per unit.
pmax_pu: float
Maximum power consumption in per unit.
"""
pnom_kW: float = None
pmin_pu: float = None
pmax_pu: float = None
[docs]
@dataclass
class TemplateElectrolizer(Electrolizer):
"""
Represents a hydrogen electrolizer with fixed efficiency.
Attributes:
-----------
pnom_kW: float
Nominal
pmin_pu: float
Minimum power consumption in per unit.
pmax_pu: float
Maximum power consumption in per unit.
"""
pnom_kW: Union[float, List[float]] = None
[docs]
@dataclass
class ESS(GainComponent):
"""
Energy storage system component. It considers a fixed rate of charge and
fixed efficiency.
Attributes:
-----------
pnom_kW: float
Nominal active power (kW).
soc_nom_kWh: float
Nominal state of charge (kWh).
"""
pnom_kW: float = None
soc_nom_kWh: float = None
[docs]
@dataclass
class TemplateESS(ESS):
"""
Template storage system component. It considers a fixed rate of charge and
fixed efficiency.
Attributes:
-----------
pnom_kW: float
Nominal active power (kW).
soc_nom_kWh: float
Nominal state of charge (kWh).
efficiency: float
Round-trip efficiency.
"""
pnom_kW: Range = None
soc_nom_kWh: Range = None
[docs]
@dataclass
class PVSystem(GainComponent):
snom_KVA: float = None
solar_irradiance: TSParameter = None
@dataclass
class TemplatePVSystem(PVSystem):
snom_KVA: Range = None
[docs]
@dataclass
class TemplatePVSystem(PVSystem):
"""
Template for a PVSystem component.
"""
snom_MVA: Range = None
inv_cost: float = None
[docs]
class PlantTemplate(Entity):
"""
Template for a hydrogen plant.
"""
@classmethod
def _initialize_class(cls):
cls.register_serializable_component("batteries", TemplateESS)
cls.register_serializable_component("electrolizers", TemplateElectrolizer)
cls.register_serializable_component("pv_systems", TemplatePVSystem)
cls.register_serializable_parameter("name", str)
def __init__(self, name: str = None, batteries: Dict[int, TemplateESS]=None,
electrolizers: Dict[int, TemplateElectrolizer]=None,
pv_systems: Dict[int, TemplatePVSystem]=None
) -> None:
super().__init__()
self.name = name
self.batteries = batteries if batteries is not None else {}
self.electrolizers = electrolizers if electrolizers is not None else {}
self.pv_systems = pv_systems if pv_systems is not None else {}
[docs]
def tsnames(self) -> List[str]:
"""Return the time series names of the problem data.
Returns:
--------
- List[str], the time series names.
"""
tsnames = []
for pv_system in self.pv_systems.values():
tsnames.append(pv_system.solar_irradiance.tsname)
return tsnames