from abc import ABC, abstractmethod
from dataclasses import dataclass
from shilps.components import Component, 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
inv_cost_per_kw: 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
inv_cost_per_kw: float = None
bat_kw_to_kwh: float = None
[docs]
@dataclass
class PVSystem(GainComponent):
snom_KVA: float = None
solar_irradiance: TSParameter = None
[docs]
@dataclass
class TemplatePVSystem(PVSystem):
"""
Template for a PVSystem component.
"""
snom_MVA: Range = None
inv_cost_per_kw: float = None
[docs]
@dataclass
class EconomicData(Component):
"""
Economic data for a hydrogen plant.
"""
inv_cost_per_kw: 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)
cls.register_serializable_parameter("location", tuple)
def __init__(self, name: str = None, batteries: Dict[int, TemplateESS]=None,
electrolizers: Dict[int, TemplateElectrolizer]=None,
pv_systems: Dict[int, TemplatePVSystem]=None,
location: tuple = None
) -> None:
super().__init__()
self.name = name
self.locations = location
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