You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
793 lines
25 KiB
Python
793 lines
25 KiB
Python
import warnings
|
|
from functools import partial, lru_cache
|
|
from numbers import Number
|
|
from itertools import count
|
|
|
|
import numpy as np
|
|
from numpy.polynomial import Polynomial
|
|
from scipy.optimize import minimize_scalar
|
|
|
|
from .converters import *
|
|
|
|
|
|
class Asset:
|
|
"""Generic class for producing/consuming assets. Specific asset classes can
|
|
inherit from this class.
|
|
|
|
Parameters:
|
|
-----------
|
|
max_power : int/float
|
|
Maximum asset power in MW electric
|
|
min_power : int/float
|
|
Minimium asset load in MW electric
|
|
|
|
Usage:
|
|
------
|
|
Use the set_load and get_load methods to set and get asset status in MW.
|
|
|
|
Convention is negative values for inputs (consumption) and positive
|
|
values for outputs (production).
|
|
"""
|
|
|
|
_freq_to_multiplier = {"H": 1, "15T": (1 / 4), "1T": (1 / 60)}
|
|
_ids = count(0)
|
|
|
|
def __init__(self, name, max_power, min_power):
|
|
if min_power > max_power:
|
|
raise ValueError("'min_power' can not be larger than 'max_power'.")
|
|
|
|
self.name = name
|
|
self.id = next(self._ids)
|
|
self.max_power = max_power
|
|
self.min_power = min_power
|
|
self.modes = {"max": max_power, "min": min_power}
|
|
|
|
def __repr__(self):
|
|
return f"{self.__class__.__name__}(self, max_power={self.max_power}, min_power={self.min_power})"
|
|
|
|
def set_load(self, load):
|
|
"""Set Asset load in MW.
|
|
|
|
Convention is negative value for consumption and positive value
|
|
for production. Subclasses might use a different convention if
|
|
this seems more intiutive.
|
|
|
|
Returns the load that is set in MW.
|
|
"""
|
|
if load < self.min_power or load > self.max_power:
|
|
warnings.warn(
|
|
f"Chosen Asset load for {self.name} is out of range. "
|
|
f"Should be between {self.min_power} and {self.max_power}. "
|
|
f"Function will return boundary load level for now."
|
|
)
|
|
load = min(max(load, self.min_power), self.max_power)
|
|
return load
|
|
|
|
def set_mode(self, mode):
|
|
""" """
|
|
load = self.modes[mode]
|
|
return self.set_load(load)
|
|
|
|
def MW_to_MWh(self, MW):
|
|
"""Performs conversion from MW to MWh using the time_factor variable."""
|
|
return MW * self.time_factor
|
|
|
|
def MWh_to_MW(self, MWh):
|
|
"""Performs conversion from MWh to MW using the time_factor variable."""
|
|
return MWh / self.time_factor
|
|
|
|
def set_freq(self, freq):
|
|
"""
|
|
Function that aligns time frequency between Model and Asset.
|
|
Can be '1T', '15T' or 'H'
|
|
The time_factor variable is used in subclasses to perform MW to MWh conversions.
|
|
"""
|
|
self.freq = freq
|
|
self.time_factor = Asset._freq_to_multiplier[freq]
|
|
|
|
def set_financials(
|
|
self, capex, opex, devex, lifetime=None, depreciate=True, salvage_value=0
|
|
):
|
|
"""Set financial data of the asset."""
|
|
self.capex = capex
|
|
self.opex = opex
|
|
self.devex = devex
|
|
self.lifetime = lifetime
|
|
self.depreciate = depreciate
|
|
self.salvage_value = salvage_value
|
|
|
|
|
|
class Eboiler(Asset):
|
|
"""Subclass for an E-boiler."""
|
|
|
|
def __init__(self, name, max_power, min_power=0, efficiency=0.99):
|
|
super().__init__(name, min_power=-max_power, max_power=-min_power)
|
|
self.efficiency = efficiency
|
|
self.max_thermal_output = max_power * 0.99
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"{self.__class__.__name__}(name={self.name}, max_power={self.max_power}, "
|
|
f"min_power={self.min_power}, efficiency={self.efficiency})"
|
|
)
|
|
|
|
def set_load(self, load):
|
|
"""Set load in MWe, returns (load, heat_output) in MWe and MWth
|
|
|
|
Convention is negative numbers for consumption.
|
|
Inserting a positive value will return an exception.
|
|
"""
|
|
|
|
if load > 0:
|
|
raise ValueError(
|
|
f"Eboiler.set_load() only accepts negative numbers by convention. "
|
|
f"{load} was inserted."
|
|
)
|
|
|
|
load = super().set_load(load)
|
|
heat_output = -load * self.efficiency
|
|
return (load, heat_output)
|
|
|
|
def set_heat_output(self, heat_output):
|
|
"""Set heat output in MWth, returns tuple (heat_output, eload) in MW"""
|
|
load = -heat_output / self.efficiency
|
|
load, heat_output = self.set_load(load)
|
|
return heat_output, load
|
|
|
|
|
|
class Heatpump(Asset):
|
|
"""Subclass for a Heatpump.
|
|
|
|
Use cop parameter to set fixed COP (float/int) or COP curve (func).
|
|
COP curve should take load in MWhe and return COP.
|
|
|
|
Parameters:
|
|
-----------
|
|
max_th_power : numeric
|
|
Maximum thermal output in MW (positive value)
|
|
cop_curve : numeric or list or function
|
|
3 ways to set the COP of the Heatpump:
|
|
(1) Fixed COP based on [numeric] value.
|
|
(2) Polynomial with coefficients based on [list] input.
|
|
|
|
Input coeficients in format [c0, c1, c2, ..., c(n)],
|
|
will generate Polynomial p(x) = c0 + c1*x + c2*x^2 ... cn*x^n,
|
|
where x = % thermal load (in % of thermal capacity) as decimal value.
|
|
|
|
Example:
|
|
cop=[1, 2, 3, 4] will result in following COP curve:
|
|
p(x) = 1 + 2x + 3x**2 + 4x**3,
|
|
|
|
(3) [function] in format func(*args, **kwargs)
|
|
Function should return a Polynomial that takes 'load_perc' as parameter.
|
|
|
|
min_th_power : numeric
|
|
Minimum thermal output in MW (positive value)
|
|
|
|
Notes:
|
|
------
|
|
Sign convention:
|
|
Thermal power outputs have positive values
|
|
Electric power inputs have negative values
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
max_th_power,
|
|
cop_curve,
|
|
min_th_power=0,
|
|
):
|
|
if max_th_power < 0 or min_th_power < 0:
|
|
raise ValueError("Thermal power can not have negative values.")
|
|
|
|
if min_th_power > max_th_power:
|
|
raise ValueError("'min_th_power' can not be larger than 'max_th_power'.")
|
|
|
|
self.name = name
|
|
self.max_th_power = max_th_power
|
|
self.min_th_power = min_th_power
|
|
self.cop_curve = self._set_cop_curve(cop_curve)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"{self.__class__.__name__}(name='{self.name}', max_thermal_power={self.max_th_power}, "
|
|
f"cop_curve={self.cop_curve}, min_th_power={self.min_th_power})"
|
|
)
|
|
|
|
# Is turning everything into a Polynomial the best solution here?
|
|
@staticmethod
|
|
@lru_cache(maxsize=None)
|
|
def _set_cop_curve(cop_curve):
|
|
"""Generate COP curve function based on different inputtypes.
|
|
|
|
Returns a function that takes *args **kwargs and returns a Polynomial.
|
|
"""
|
|
if isinstance(cop_curve, list):
|
|
|
|
def func(*args, **kwargs):
|
|
return Polynomial(cop_curve)
|
|
|
|
return func
|
|
|
|
return cop_curve
|
|
|
|
@lru_cache(maxsize=None)
|
|
def get_cop(self, heat_output, Tsink=None, Tsource=None):
|
|
"""Get COP corresponding to certain load.
|
|
|
|
Parameters:
|
|
-----------
|
|
heat_output : numeric
|
|
Thermal load in MW
|
|
Tsink : numeric
|
|
Sink temperature in degrees celcius
|
|
Tsource : numeric
|
|
Source temperature in degrees celcius
|
|
|
|
Notes:
|
|
------
|
|
Sign convention:
|
|
Positive values for thermal load
|
|
Negative values for electric load
|
|
"""
|
|
load_perc = heat_output / self.max_th_power
|
|
cop_curve = self.cop_curve
|
|
|
|
if not callable(cop_curve):
|
|
return cop_curve
|
|
else:
|
|
return cop_curve(Tsink=Tsink, Tsource=Tsource)(load_perc)
|
|
|
|
def th_to_el_power(self, heat_output, Tsink=None, Tsource=None):
|
|
if not self.min_th_power <= heat_output <= self.max_th_power:
|
|
warnings.warn(
|
|
f"Chosen heat output is out of range [{self.min_th_power} - {self.max_th_power}]. "
|
|
"Heat output is being limited to the closest boundary."
|
|
)
|
|
heat_output = min(max(heat_output, self.min_th_power), self.max_th_power)
|
|
|
|
cop = self.get_cop(heat_output=heat_output, Tsink=Tsink, Tsource=Tsource)
|
|
return -heat_output / cop
|
|
|
|
def set_load(self, *args, **kwargs):
|
|
raise NotImplementedError(
|
|
"Directly setting the electric load of the heatpump is not possible (yet). "
|
|
"Functionality will be implemented if there is a specific usecase for it."
|
|
)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def set_heat_output(self, heat_output, Tsink=None, Tsource=None):
|
|
"""Set heat output in MWth, returns load of heatpump as tuple (MWe, MWth)"""
|
|
if not self.min_th_power <= heat_output <= self.max_th_power:
|
|
warnings.warn(
|
|
f"Chosen heat output is out of range [{self.min_th_power} - {self.max_th_power}]. "
|
|
"Heat output is being limited to the closest boundary."
|
|
)
|
|
heat_output = min(max(heat_output, self.min_th_power), self.max_th_power)
|
|
|
|
if Tsink is not None and Tsource is not None and Tsink <= Tsource:
|
|
raise ValueError(f"Tsource '{Tsource}' can not be higher than '{Tsink}'.")
|
|
|
|
cop = self.get_cop(heat_output=heat_output, Tsink=Tsink, Tsource=Tsource)
|
|
e_load = -heat_output / cop
|
|
return e_load, heat_output
|
|
|
|
def _cost_function(self, x, c1, c2, c3, Tsink=None, Tsource=None):
|
|
"""Objective function for set_opt_load function.
|
|
|
|
x = heatpump thermal load in MW
|
|
c1 = electricity_cost
|
|
c2 = alt_heat_price
|
|
c3 = demand
|
|
"""
|
|
return (
|
|
x / self.get_cop(heat_output=x, Tsink=Tsink, Tsource=Tsource) * c1
|
|
+ (c3 - x) * c2
|
|
)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def set_opt_load(
|
|
self,
|
|
electricity_cost,
|
|
alt_heat_price,
|
|
demand,
|
|
Tsink=None,
|
|
Tsource=None,
|
|
tolerance=0.01,
|
|
):
|
|
"""Set optimal load of Heatpump with minimal total heat costs.
|
|
|
|
Function uses np.minimize_scalar to minimize cost function.
|
|
|
|
Parameters:
|
|
-----------
|
|
electricity_cost:
|
|
Cost of input electricity in €/MWh(e)
|
|
alt_heat_price:
|
|
Price of heat from alternative source in €/MWh(th)
|
|
demand:
|
|
Heat demand in MW(th)
|
|
|
|
Returns:
|
|
--------
|
|
Optimal load of heatpump as tuple (MWe, MWth)
|
|
"""
|
|
c1 = electricity_cost
|
|
c2 = alt_heat_price
|
|
c3 = demand
|
|
|
|
cop_curve = self.cop_curve
|
|
if isinstance(cop_curve, Number):
|
|
if c1 / cop_curve <= c2:
|
|
return self.max_th_power
|
|
else:
|
|
return self.min_th_power
|
|
|
|
obj_func = partial(
|
|
self._cost_function, c1=c1, c2=c2, c3=c3, Tsink=Tsink, Tsource=Tsource
|
|
)
|
|
|
|
low_bound = 0
|
|
up_bound = min(c3, self.max_th_power)
|
|
|
|
opt_th_load = minimize_scalar(
|
|
obj_func,
|
|
bounds=(low_bound, up_bound),
|
|
method="bounded",
|
|
options={"xatol": tolerance},
|
|
).x
|
|
opt_e_load, opt_th_load = self.set_heat_output(
|
|
opt_th_load, Tsink=Tsink, Tsource=Tsource
|
|
)
|
|
|
|
return opt_e_load, opt_th_load
|
|
|
|
|
|
class Battery(Asset):
|
|
"""Subclass for a Battery.
|
|
|
|
Battery is modeled as follows:
|
|
- Rated power is power in MW that battery can
|
|
import from and export to the grid
|
|
- Efficiency loss is applied at charging, meaning that
|
|
SoC increase when charging is lower than the SoC decrease
|
|
when discharging
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
rated_power,
|
|
rated_capacity,
|
|
roundtrip_eff,
|
|
min_soc=0,
|
|
max_soc=1,
|
|
soc_at_start=None,
|
|
cycle_lifetime=None,
|
|
):
|
|
super().__init__(name=name, max_power=rated_power, min_power=-rated_power)
|
|
self.capacity = rated_capacity
|
|
self.min_soc = min_soc
|
|
self.max_soc = max_soc
|
|
self.min_chargelevel = min_soc * self.capacity
|
|
self.max_chargelevel = max_soc * self.capacity
|
|
self.rt_eff = roundtrip_eff
|
|
self.one_way_eff = np.sqrt(roundtrip_eff)
|
|
self.cycle_count = 0
|
|
self.cycle_lifetime = cycle_lifetime
|
|
|
|
soc_at_start = min_soc if soc_at_start is None else soc_at_start
|
|
self.set_chargelevel(soc_at_start * self.capacity)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"Battery(self, rated_power={self.max_power}, rated_capacity={self.capacity}, "
|
|
f"roundtrip_eff={self.rt_eff}, min_soc={self.min_soc}, max_soc={self.max_soc})"
|
|
)
|
|
|
|
def get_soc(self):
|
|
"""Get the SoC in % (decimal value)"""
|
|
return self.chargelevel / self.capacity
|
|
|
|
def set_chargelevel(self, chargelevel):
|
|
"""Set the chargelevel in MWh. Will automatically change the SoC accordingly."""
|
|
# if round(chargelevel,2) < round(self.min_chargelevel,2) or round(chargelevel,2) > round(self.max_chargelevel,2):
|
|
# raise ValueError(
|
|
# f"Tried to set Charge Level to {chargelevel}. "
|
|
# f"Charge Level must be a value between "
|
|
# f"{self.min_chargelevel} and {self.max_chargelevel} (in MWh)"
|
|
# )
|
|
|
|
self.chargelevel = chargelevel
|
|
|
|
def set_load(self, load):
|
|
"""Set load of the battery.
|
|
|
|
Use negative values for charging and positive values for discharging.
|
|
Returns actual chargespeed, considering technical limitations of the battery.
|
|
|
|
Note: We currently assume all efficiency losses occur during charging (no losses during discharge)
|
|
"""
|
|
if not hasattr(self, "freq"):
|
|
raise AttributeError(
|
|
"Time frequency of the model is not defined. "
|
|
"Assign asset to a CaseStudy or use Asset.freq(). "
|
|
"to set de time frequency and try again."
|
|
)
|
|
|
|
load = super().set_load(load)
|
|
|
|
unbound_charging = self.MW_to_MWh(load)
|
|
|
|
if load < 0:
|
|
unbound_charging *= self.rt_eff
|
|
|
|
chargelevel = self.chargelevel
|
|
max_charging = chargelevel - self.max_chargelevel
|
|
max_discharging = chargelevel - self.min_chargelevel
|
|
|
|
bound_charging = min(max(unbound_charging, max_charging), max_discharging)
|
|
newcl = chargelevel - bound_charging
|
|
self.set_chargelevel(newcl)
|
|
|
|
if bound_charging < 0:
|
|
bound_charging /= self.rt_eff
|
|
|
|
self.cycle_count += abs(bound_charging / (self.capacity * 2))
|
|
|
|
return self.MWh_to_MW(bound_charging)
|
|
|
|
def charge(self, chargespeed):
|
|
"""Charge the battery with given chargespeed.
|
|
|
|
Redirects to Battery.set_load().
|
|
Returns load (negative value for charging).
|
|
"""
|
|
chargespeed = self.max_power if chargespeed == "max" else chargespeed
|
|
|
|
if chargespeed < 0:
|
|
raise ValueError(
|
|
f"Chargespeed should be always be a positive value by convention. "
|
|
f"Inserted {chargespeed}."
|
|
)
|
|
|
|
chargespeed = self.set_load(-chargespeed)
|
|
|
|
return chargespeed
|
|
|
|
def discharge(self, dischargespeed):
|
|
"""Discharge the battery by given amount.
|
|
|
|
Redirects to Battery.set_load().
|
|
Returns load (positive value for discharging).
|
|
"""
|
|
dischargespeed = self.max_power if dischargespeed == "max" else dischargespeed
|
|
|
|
if dischargespeed < 0:
|
|
raise ValueError(
|
|
f"Dischargespeed should be always be a positive value by convention. "
|
|
f"Inserted {dischargespeed}."
|
|
)
|
|
|
|
dischargespeed = self.set_load(dischargespeed)
|
|
|
|
return dischargespeed
|
|
|
|
def get_cost_per_cycle(self, cycle_lifetime):
|
|
return self.capex / self.cycle_lifetime
|
|
|
|
|
|
class EV(Battery):
|
|
def __init__(
|
|
self,
|
|
name,
|
|
rated_power,
|
|
rated_capacity,
|
|
roundtrip_eff,
|
|
min_soc=0,
|
|
max_soc=1,
|
|
soc_at_start=None,
|
|
id=None,
|
|
):
|
|
super().__init__(
|
|
name,
|
|
rated_power,
|
|
rated_capacity,
|
|
roundtrip_eff,
|
|
min_soc,
|
|
max_soc,
|
|
soc_at_start,
|
|
)
|
|
if id:
|
|
self.id = id
|
|
|
|
|
|
class HotWaterStorage(Battery):
|
|
"""Subclass for a storage asset.
|
|
|
|
Parameters:
|
|
-----------
|
|
rated_capacity : int/float
|
|
Rated capacity in MWh
|
|
min_buffer_level_perc : float
|
|
Minimum buffer level in %
|
|
buffer_level_at_start : float
|
|
Buffer level at start in %
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
rated_power,
|
|
# capacity_per_volume,
|
|
# volume,
|
|
temperature,
|
|
min_storagelevel,
|
|
initial_storagelevel=None,
|
|
):
|
|
# rated_capacity = capacity_per_volume * volume
|
|
rated_capacity = 3
|
|
|
|
if not initial_storagelevel:
|
|
initial_storagelevel = min_storagelevel
|
|
soc_at_start = initial_storagelevel / rated_capacity
|
|
max_storagelevel = rated_capacity
|
|
min_soc = min_storagelevel / rated_capacity
|
|
max_soc = max_storagelevel / rated_capacity
|
|
self.temperature = temperature
|
|
|
|
super().__init__(
|
|
name=name,
|
|
rated_power=rated_power,
|
|
rated_capacity=rated_capacity,
|
|
roundtrip_eff=1,
|
|
min_soc=min_soc,
|
|
max_soc=max_soc,
|
|
soc_at_start=soc_at_start,
|
|
)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"{self.__class__.__name__}(name={self.name}, rated_power={self.max_power}, capacity={self.capacity}, "
|
|
f"temperature={self.temperature}, min_storagelevel={self.min_chargelevel})"
|
|
)
|
|
|
|
@property
|
|
def charging_power_limit(self):
|
|
max_charging_energy = self.max_chargelevel - self.chargelevel
|
|
# return min(self.MWh_to_MW(max_charging_energy), -self.min_power)
|
|
return 0.5
|
|
|
|
@property
|
|
def discharging_power_limit(self):
|
|
max_discharging_energy = self.chargelevel - self.min_chargelevel
|
|
# return min(self.MWh_to_MW(max_discharging_energy), self.max_power)
|
|
return 1
|
|
|
|
|
|
class GasBoiler(Asset):
|
|
"""Representation of a Gas-fired boiler.
|
|
|
|
name : str
|
|
Unique name of the asset
|
|
max_th_output : numeric
|
|
Maximum thermal output in MW thermal
|
|
efficiency : float
|
|
Thermal efficiency of the gasboiler as decimal value.
|
|
min_th_output : numeric
|
|
Minimum thermal output in MW thermal
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
max_th_output,
|
|
min_th_output=0,
|
|
efficiency=0.9,
|
|
):
|
|
super().__init__(name=name, max_power=max_th_output, min_power=min_th_output)
|
|
self.efficiency = efficiency
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"{self.__class__.__name__}(name={self.name}, max_power={self.max_power}, "
|
|
f"min_power={self.min_power}, efficiency={self.efficiency})"
|
|
)
|
|
|
|
def set_load(self, *args, **kwargs):
|
|
raise NotImplementedError(
|
|
"Gasboiler does not have electric load. "
|
|
"Use Gasboiler.set_heat_output() instead."
|
|
)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def set_heat_output(self, output):
|
|
"""Redirect to Gasboiler.set_load()"""
|
|
heat_output = super().set_load(output)
|
|
gas_input = -heat_output / self.efficiency
|
|
return heat_output, gas_input
|
|
|
|
|
|
class Electrolyser(Asset):
|
|
def __init__(
|
|
self,
|
|
name,
|
|
rated_power,
|
|
kwh_per_kg=60,
|
|
min_flex_load_in_perc=15,
|
|
):
|
|
min_flex_power = min_flex_load_in_perc / 100 * rated_power
|
|
|
|
super().__init__(name=name, max_power=-min_flex_power, min_power=-rated_power)
|
|
|
|
self.rated_power = rated_power
|
|
self.min_flex_load = min_flex_load_in_perc
|
|
self.min_flex_power = self.min_flex_load / 100 * self.rated_power
|
|
self.kwh_per_kg = kwh_per_kg
|
|
self.kg_per_MWh = 1000 / self.kwh_per_kg
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"Electrolyser(name={self.name}, rated_power={self.rated_power}, "
|
|
f"kwh_per_kg={self.kwh_per_kg}, flex_range_in_perc=[{self.min_flex_load}, "
|
|
f"{self.max_flex_load}])"
|
|
)
|
|
|
|
def set_load(self, load):
|
|
"""Set load of the Electrolyser in MW."""
|
|
if not hasattr(self, "freq"):
|
|
raise AttributeError(
|
|
"Time frequency of the model is not defined. "
|
|
"Assign asset to a CaseStudy or use Asset.freq(). "
|
|
"to set de time frequency and try again."
|
|
)
|
|
|
|
load = -abs(load)
|
|
load = super().set_load(load)
|
|
|
|
h2_output_kg = self.MW_to_MWh(-load) * self.kg_per_MWh
|
|
return load, h2_output_kg
|
|
|
|
|
|
class Battolyser(Asset):
|
|
def __init__(
|
|
self,
|
|
name,
|
|
rated_power,
|
|
rated_capacity,
|
|
rt_eff,
|
|
soc_at_start=None,
|
|
):
|
|
super().__init__(name=name, max_power=rated_power, min_power=-rated_power)
|
|
|
|
self.capacity = rated_capacity
|
|
self.min_soc = 0.05
|
|
self.max_soc = 1.00
|
|
self.min_chargelevel = self.min_soc * self.capacity
|
|
self.max_chargelevel = self.max_soc * self.capacity
|
|
self.rt_eff = rt_eff
|
|
self.cycle_count = 0
|
|
|
|
soc_at_start = self.min_soc if soc_at_start is None else soc_at_start
|
|
self.set_chargelevel(soc_at_start * self.capacity)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"Battolyser(name={self.name}, rated_power={self.max_power}, "
|
|
f"rated_capacity={self.capacity}, rt_eff={self.rt_eff})"
|
|
)
|
|
|
|
def get_soc(self):
|
|
"""Get the SoC in % (decimal value)"""
|
|
return self.chargelevel / self.capacity
|
|
|
|
def set_chargelevel(self, chargelevel):
|
|
"""Set the chargelevel in MWh. Will automatically change the SoC accordingly."""
|
|
if chargelevel < self.min_chargelevel or chargelevel > self.max_chargelevel:
|
|
raise ValueError(
|
|
f"Tried to set Charge Level to {chargelevel}. "
|
|
f"Charge Level must be a value between "
|
|
f"{self.min_chargelevel} and {self.max_chargelevel} (in MWh)"
|
|
)
|
|
self.chargelevel = chargelevel
|
|
|
|
def set_load(self, load):
|
|
"""Set load of the Battolyser in MW.
|
|
|
|
Use negative values for charging and positive values for discharging.
|
|
Returns actual chargespeed, considering technical limitations of the battery.
|
|
|
|
Note: We currently assume all efficiency losses occur during discharging
|
|
(no losses during charging)
|
|
"""
|
|
if not hasattr(self, "freq"):
|
|
raise AttributeError(
|
|
"Time frequency of the model is not defined. "
|
|
"Assign asset to a CaseStudy or use Asset.freq(). "
|
|
"to set de time frequency and try again."
|
|
)
|
|
|
|
load = super().set_load(load)
|
|
|
|
unbound_charging = self.MW_to_MWh(load)
|
|
if load > 0:
|
|
unbound_charging /= self.rt_eff
|
|
|
|
chargelevel = self.chargelevel
|
|
max_charging = chargelevel - self.max_chargelevel
|
|
max_discharging = chargelevel - self.min_chargelevel
|
|
bound_charging = min(max(unbound_charging, max_charging), max_discharging)
|
|
newcl = chargelevel - bound_charging
|
|
self.set_chargelevel(newcl)
|
|
|
|
if bound_charging > 0:
|
|
bound_charging *= self.rt_eff
|
|
charging_power = self.MWh_to_MW(bound_charging)
|
|
h2_power = -self.MWh_to_MW(max(bound_charging - unbound_charging, 0))
|
|
self.cycle_count += abs(bound_charging / (self.capacity * 2))
|
|
return charging_power, h2_power
|
|
|
|
def charge(self, chargespeed):
|
|
"""Charge the battery with given chargespeed.
|
|
|
|
Redirects to Battery.set_load().
|
|
Returns load (negative value for charging).
|
|
"""
|
|
chargespeed = self.max_power if chargespeed == "max" else chargespeed
|
|
|
|
if chargespeed < 0:
|
|
raise ValueError(
|
|
f"Chargespeed should be always be a positive value by convention. "
|
|
f"Inserted {chargespeed}."
|
|
)
|
|
|
|
chargespeed, h2_prod_in_MW = self.set_load(-chargespeed)
|
|
|
|
return chargespeed, h2_prod_in_MW
|
|
|
|
def discharge(self, dischargespeed):
|
|
"""Discharge the battery by given amount.
|
|
|
|
Redirects to Battery.set_load().
|
|
Returns load (positive value for discharging).
|
|
"""
|
|
dischargespeed = self.max_power if dischargespeed == "max" else dischargespeed
|
|
|
|
if dischargespeed < 0:
|
|
raise ValueError(
|
|
f"Dischargespeed should be always be a positive value by convention. "
|
|
f"Inserted {dischargespeed}."
|
|
)
|
|
|
|
dischargespeed = self.set_load(dischargespeed)[0]
|
|
return dischargespeed
|
|
|
|
|
|
##Added by Shahla, very similar to Hotwaterstorage
|
|
class HeatBuffer(Battery):
|
|
"""Subclass for a storage asset.
|
|
|
|
Parameters:
|
|
-----------
|
|
rated_capacity : int/float
|
|
Rated capacity in MWh
|
|
min_buffer_level_perc : float
|
|
Minimum buffer level in %
|
|
buffer_level_at_start : float
|
|
Buffer level at start in %
|
|
"""
|
|
|
|
def __init__(
|
|
self, name, rated_capacity, min_buffer_level_perc, buffer_level_at_start
|
|
):
|
|
super().__init__(
|
|
name=name,
|
|
rated_power=100,
|
|
rated_capacity=rated_capacity,
|
|
roundtrip_eff=1,
|
|
min_soc=min_buffer_level_perc,
|
|
max_soc=1,
|
|
soc_at_start=buffer_level_at_start,
|
|
)
|