mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-19 04:11:57 +00:00
feat(devices): add load-dependent DC-to-AC efficiency curve for GENETIC inverters (#1375)
* feat(devices): add load-dependent DC-to-AC efficiency curve for GENETIC inverters Real inverters are much less efficient at low load than at rated power (e.g. 0.80 at 2 % vs. 0.93 at 7 % load). A constant dc_to_ac_efficiency overrates night-time battery discharge into a small base load and, with it, arbitrage and grid charging. Add two optional inverter settings for the GENETIC optimizer: - dc_to_ac_efficiency_curve: list of (load_fraction, efficiency) points, load fraction = AC power / max_power_w. Points must be strictly increasing in [0, 1] with efficiencies in (0, 1]; at least two points. Linear interpolation, clamped at both ends. When set, the curve replaces dc_to_ac_efficiency. - dc_to_ac_efficiency_reference_load_fraction (default 0.06): load at which the curve values stored energy without a specific conversion. Battery discharges in Inverter.process_energy() use the efficiency at the AC energy of each conversion. The end-of-horizon value, the terminal and tail value curves and the AC charge break-even penalty use Inverter.reference_dc_to_ac_efficiency. Without a curve, dc_to_ac_efficiency_at() and the reference efficiency return dc_to_ac_efficiency itself, so results are bit-identical to the previous model. GENETIC0 is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(devices): evaluate the efficiency curve at the delivered AC load When the battery limits a discharge (low state of charge or its power limit), the conversion ran with the efficiency of the requested AC energy. With a 1000 Wh battery at 10 % SoC, a 1000 W inverter and the curve [(0, .8), (.5, .95), (1, .95)], a request of 500 Wh AC returned 95 Wh AC from the available 100 Wh DC, although the curve gives 82.5 % at that load. The delivered AC energy now solves a = dc * efficiency(a / rated) exactly on the piecewise-linear curve (solve_ac_for_dc_energy), bounded by the request. Unlimited discharges and inverters without a curve are unchanged. Regression tests use a real Battery: low SoC and a power-limited discharge, each with 60- and 15-minute slots, battery discharge losses, the load path through process_energy, and constant efficiency. * docs: regenerate OpenAPI for the efficiency-curve fix --------- Co-authored-by: Christin <christin@BigBrother.bikini.bottom.zone> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Andreas <drbacke@gmx.de>
This commit is contained in:
co-authored by
Christin
Claude Opus 5.5
Andreas
parent
560370281f
commit
7f26ea3f3f
@@ -134,6 +134,137 @@ def validate_home_appliance_load_definition(
|
||||
raise ValueError("load_profile_power_w must not contain negative values.")
|
||||
|
||||
|
||||
def validate_efficiency_curve(
|
||||
curve: Optional[list[tuple[float, float]]],
|
||||
) -> Optional[list[tuple[float, float]]]:
|
||||
"""Validate a load-dependent conversion efficiency curve.
|
||||
|
||||
A curve is a list of ``(load_fraction, efficiency)`` points. The load
|
||||
fraction is the converted power relative to the rated power of the device
|
||||
(0.0 = idle, 1.0 = rated power), the efficiency is the conversion
|
||||
efficiency at that load.
|
||||
|
||||
Rules:
|
||||
- At least two points.
|
||||
- Load fractions are finite, within [0, 1] and strictly increasing
|
||||
(sorted, no duplicates).
|
||||
- Efficiencies are finite and within (0, 1].
|
||||
|
||||
Args:
|
||||
curve: The curve points, or None for no curve.
|
||||
|
||||
Returns:
|
||||
The unchanged curve, or None.
|
||||
|
||||
Raises:
|
||||
ValueError: If the curve violates one of the rules.
|
||||
"""
|
||||
if curve is None:
|
||||
return None
|
||||
if len(curve) < 2:
|
||||
raise ValueError("An efficiency curve needs at least two points.")
|
||||
previous_fraction: Optional[float] = None
|
||||
for fraction, efficiency in curve:
|
||||
if not math.isfinite(fraction) or not 0.0 <= fraction <= 1.0:
|
||||
raise ValueError(f"Efficiency curve load fraction {fraction} is outside of [0, 1].")
|
||||
if not math.isfinite(efficiency) or not 0.0 < efficiency <= 1.0:
|
||||
raise ValueError(f"Efficiency curve efficiency {efficiency} is outside of (0, 1].")
|
||||
if previous_fraction is not None and fraction <= previous_fraction:
|
||||
raise ValueError(
|
||||
"Efficiency curve load fractions must be strictly increasing "
|
||||
f"(sorted, no duplicates); got {fraction} after {previous_fraction}."
|
||||
)
|
||||
previous_fraction = fraction
|
||||
return curve
|
||||
|
||||
|
||||
def interpolate_efficiency_curve(curve: list[tuple[float, float]], load_fraction: float) -> float:
|
||||
"""Return the efficiency of a curve at a load fraction.
|
||||
|
||||
Interpolates linearly between the curve points. Load fractions below the
|
||||
first or above the last point are clamped to the efficiency of that point.
|
||||
|
||||
Args:
|
||||
curve: Points as validated by ``validate_efficiency_curve``.
|
||||
load_fraction: Converted power relative to the rated power.
|
||||
|
||||
Returns:
|
||||
The efficiency at ``load_fraction``.
|
||||
"""
|
||||
first_fraction, first_efficiency = curve[0]
|
||||
if load_fraction <= first_fraction:
|
||||
return first_efficiency
|
||||
for fraction, efficiency in curve[1:]:
|
||||
if load_fraction <= fraction:
|
||||
return first_efficiency + (load_fraction - first_fraction) / (
|
||||
fraction - first_fraction
|
||||
) * (efficiency - first_efficiency)
|
||||
first_fraction, first_efficiency = fraction, efficiency
|
||||
return first_efficiency
|
||||
|
||||
|
||||
def solve_ac_for_dc_energy(
|
||||
curve: list[tuple[float, float]],
|
||||
dc_wh: float,
|
||||
rated_wh: float,
|
||||
upper_ac_wh: float,
|
||||
) -> float:
|
||||
"""Return the AC energy a DC energy yields through a load-dependent conversion.
|
||||
|
||||
The efficiency of a curve depends on the AC load actually delivered, so for a
|
||||
given DC energy the delivered AC energy ``a`` is the solution of
|
||||
``a = dc_wh * efficiency(a / rated_wh)``. The curve is piecewise linear and
|
||||
clamped at both ends, so the equation is solved exactly on each piece.
|
||||
|
||||
Args:
|
||||
curve: Points as validated by ``validate_efficiency_curve``.
|
||||
dc_wh: DC energy available to the conversion in one slot.
|
||||
rated_wh: AC energy at rated power in one slot (load fraction 1.0).
|
||||
upper_ac_wh: Upper bound of the result, e.g. the requested AC energy.
|
||||
|
||||
Returns:
|
||||
The largest consistent AC energy that does not exceed ``upper_ac_wh``.
|
||||
"""
|
||||
if dc_wh <= 0.0 or rated_wh <= 0.0 or upper_ac_wh <= 0.0:
|
||||
return 0.0
|
||||
tolerance = 1e-9
|
||||
best = -1.0
|
||||
|
||||
def consider(ac_wh: float, low: float, high: float) -> None:
|
||||
nonlocal best
|
||||
fraction = ac_wh / rated_wh
|
||||
if (
|
||||
low - tolerance <= fraction <= high + tolerance
|
||||
and 0.0 < ac_wh <= upper_ac_wh * (1.0 + tolerance)
|
||||
and ac_wh > best
|
||||
):
|
||||
best = ac_wh
|
||||
|
||||
first_fraction, first_efficiency = curve[0]
|
||||
last_fraction, last_efficiency = curve[-1]
|
||||
# Clamped ends: constant efficiency below the first and above the last point.
|
||||
consider(dc_wh * first_efficiency, float("-inf"), first_fraction)
|
||||
consider(dc_wh * last_efficiency, last_fraction, float("inf"))
|
||||
for (x0, e0), (x1, e1) in zip(curve, curve[1:]):
|
||||
slope = (e1 - e0) / (x1 - x0)
|
||||
# a = dc * (e0 + slope * (a / rated - x0)) => a * (1 - dc*slope/rated) = dc * (e0 - slope*x0)
|
||||
denominator = 1.0 - dc_wh * slope / rated_wh
|
||||
if abs(denominator) < 1e-12:
|
||||
continue
|
||||
consider(dc_wh * (e0 - slope * x0) / denominator, x0, x1)
|
||||
if best >= 0.0:
|
||||
return min(best, upper_ac_wh)
|
||||
# No piece holds a solution within the bound (only possible numerically at a
|
||||
# kink): converge on it from the bound.
|
||||
ac_wh = min(upper_ac_wh, dc_wh)
|
||||
for _ in range(50):
|
||||
next_ac_wh = min(dc_wh * interpolate_efficiency_curve(curve, ac_wh / rated_wh), upper_ac_wh)
|
||||
if abs(next_ac_wh - ac_wh) <= tolerance:
|
||||
break
|
||||
ac_wh = next_ac_wh
|
||||
return ac_wh
|
||||
|
||||
|
||||
class ConsumerScheduleMode(StrEnum):
|
||||
"""Schedule mode of a flexible consumer (home appliance).
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import Field
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from akkudoktoreos.devices.devicesabc import (
|
||||
interpolate_efficiency_curve,
|
||||
solve_ac_for_dc_energy,
|
||||
validate_efficiency_curve,
|
||||
)
|
||||
from akkudoktoreos.devices.genetic.battery import Battery
|
||||
from akkudoktoreos.optimization.genetic.geneticdevices import DeviceParameters
|
||||
from akkudoktoreos.prediction.interpolator import get_eos_load_interpolator
|
||||
@@ -44,6 +49,37 @@ class InverterParameters(DeviceParameters):
|
||||
"examples": [0.95, 1.0],
|
||||
},
|
||||
)
|
||||
dc_to_ac_efficiency_curve: Optional[list[tuple[float, float]]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Load-dependent efficiency of DC to AC conversion as a list of "
|
||||
"(load_fraction, efficiency) points. The load fraction is the AC power "
|
||||
"of the conversion relative to max_power_wh [0.0 ... 1.0]; points must be "
|
||||
"strictly increasing in load fraction, efficiencies in (0, 1]. Values are "
|
||||
"interpolated linearly and clamped to the first/last point. When set, the "
|
||||
"curve replaces dc_to_ac_efficiency. "
|
||||
"Default None (constant dc_to_ac_efficiency)."
|
||||
),
|
||||
"examples": [None, [[0.02, 0.8], [0.07, 0.93], [0.3, 0.96], [1.0, 0.95]]],
|
||||
},
|
||||
)
|
||||
dc_to_ac_efficiency_reference_load_fraction: float = Field(
|
||||
default=0.06,
|
||||
ge=0,
|
||||
le=1,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Load fraction at which dc_to_ac_efficiency_curve is evaluated where "
|
||||
"stored battery energy is valued without a specific conversion "
|
||||
"(e.g. remaining battery energy at the end of the horizon, AC charge "
|
||||
"break-even). Should represent the typical load the battery serves, "
|
||||
"e.g. the night-time base load. Only used with dc_to_ac_efficiency_curve. "
|
||||
"Default 0.06."
|
||||
),
|
||||
"examples": [0.06, 0.1],
|
||||
},
|
||||
)
|
||||
max_ac_charge_power_w: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
@@ -69,6 +105,13 @@ class InverterParameters(DeviceParameters):
|
||||
},
|
||||
)
|
||||
|
||||
@field_validator("dc_to_ac_efficiency_curve")
|
||||
@classmethod
|
||||
def _validate_dc_to_ac_efficiency_curve(
|
||||
cls, value: Optional[list[tuple[float, float]]]
|
||||
) -> Optional[list[tuple[float, float]]]:
|
||||
return validate_efficiency_curve(value)
|
||||
|
||||
|
||||
class Inverter:
|
||||
def __init__(
|
||||
@@ -93,11 +136,32 @@ class Inverter:
|
||||
self.max_power_wh = self.parameters.max_power_wh * self.slot_duration_h
|
||||
self.dc_to_ac_efficiency = self.parameters.dc_to_ac_efficiency
|
||||
self.ac_to_dc_efficiency = self.parameters.ac_to_dc_efficiency
|
||||
self.dc_to_ac_efficiency_curve = self.parameters.dc_to_ac_efficiency_curve
|
||||
# DC-to-AC efficiency used to value stored battery energy where no single
|
||||
# conversion is known (end-of-horizon value, AC charge break-even). With
|
||||
# a curve it is the efficiency at the configured reference load.
|
||||
self.reference_dc_to_ac_efficiency = self.dc_to_ac_efficiency_at(
|
||||
self.parameters.dc_to_ac_efficiency_reference_load_fraction * self.max_power_wh
|
||||
)
|
||||
# This value remains a power [W]. GeneticSimulation converts it into a
|
||||
# slot-independent charge-factor limit.
|
||||
self.max_ac_charge_power_w = self.parameters.max_ac_charge_power_w
|
||||
self.ac_charge_limits_total_charge = self.parameters.ac_charge_limits_total_charge
|
||||
|
||||
def dc_to_ac_efficiency_at(self, ac_wh: float) -> float:
|
||||
"""Return the DC-to-AC efficiency of a conversion delivering ``ac_wh`` in one slot.
|
||||
|
||||
Without ``dc_to_ac_efficiency_curve`` this is the constant
|
||||
``dc_to_ac_efficiency``. With a curve, the efficiency is interpolated at
|
||||
the load fraction ``ac_wh / max_power_wh`` of this slot. ``max_power_wh``
|
||||
is already scaled to the slot duration, so the fraction equals the mean
|
||||
AC power relative to the rated power.
|
||||
"""
|
||||
if self.dc_to_ac_efficiency_curve is None:
|
||||
return self.dc_to_ac_efficiency
|
||||
load_fraction = ac_wh / self.max_power_wh if self.max_power_wh > 0 else 0.0
|
||||
return interpolate_efficiency_curve(self.dc_to_ac_efficiency_curve, load_fraction)
|
||||
|
||||
def ac_charge_factor(self, factor: float) -> float:
|
||||
"""Return the AC charge factor the inverter can actually execute.
|
||||
|
||||
@@ -161,9 +225,26 @@ class Inverter:
|
||||
if not self.battery or requested_ac_wh <= 0.0:
|
||||
return 0.0, 0.0
|
||||
|
||||
dc_request = requested_ac_wh / self.dc_to_ac_efficiency
|
||||
# With an efficiency curve, the efficiency is taken at the requested AC
|
||||
# energy of this conversion.
|
||||
dc_to_ac_efficiency = self.dc_to_ac_efficiency_at(requested_ac_wh)
|
||||
dc_request = requested_ac_wh / dc_to_ac_efficiency
|
||||
battery_discharge_dc, discharge_losses = self.battery.discharge_energy(dc_request, hour)
|
||||
battery_discharge_ac = battery_discharge_dc * self.dc_to_ac_efficiency
|
||||
if self.dc_to_ac_efficiency_curve is not None and battery_discharge_dc < dc_request * (
|
||||
1.0 - 1e-12
|
||||
):
|
||||
# The battery delivered less than requested (low state of charge or
|
||||
# its power limit). The conversion then runs at a lower AC load than
|
||||
# requested, so the efficiency must be the one at the AC energy that
|
||||
# is actually delivered, not at the request.
|
||||
battery_discharge_ac = solve_ac_for_dc_energy(
|
||||
self.dc_to_ac_efficiency_curve,
|
||||
battery_discharge_dc,
|
||||
self.max_power_wh,
|
||||
requested_ac_wh,
|
||||
)
|
||||
else:
|
||||
battery_discharge_ac = battery_discharge_dc * dc_to_ac_efficiency
|
||||
inverter_discharge_losses = battery_discharge_dc - battery_discharge_ac
|
||||
return battery_discharge_ac, discharge_losses + inverter_discharge_losses
|
||||
|
||||
@@ -258,16 +339,20 @@ class Inverter:
|
||||
|
||||
if allow_battery_grid_export and self.battery and remaining_inverter_ac_capacity > 0.0:
|
||||
export_factor = min(max(float(battery_grid_export_factor), 0.0), 1.0)
|
||||
remaining_battery_ac = (
|
||||
self.battery.remaining_discharge_energy_wh(hour) * self.dc_to_ac_efficiency
|
||||
# Upper bounds of the export. With an efficiency curve, the
|
||||
# efficiency is estimated at the DC energy of each bound; the
|
||||
# conversion itself in _discharge_battery_to_ac() is exact and is
|
||||
# limited by the energy the battery can actually deliver.
|
||||
remaining_battery_dc = self.battery.remaining_discharge_energy_wh(hour)
|
||||
remaining_battery_ac = remaining_battery_dc * self.dc_to_ac_efficiency_at(
|
||||
remaining_battery_dc
|
||||
)
|
||||
# The rate caps the export against the battery's *rated* discharge
|
||||
# power, so it stays a plain power setpoint ("export at 50 %") that
|
||||
# does not silently grow when self-consumption used less of the slot.
|
||||
# At factor 1.0 this bound never binds; behaviour is unchanged.
|
||||
rated_export_ac = (
|
||||
self.battery.rated_discharge_energy_wh() * export_factor * self.dc_to_ac_efficiency
|
||||
)
|
||||
rated_export_dc = self.battery.rated_discharge_energy_wh() * export_factor
|
||||
rated_export_ac = rated_export_dc * self.dc_to_ac_efficiency_at(rated_export_dc)
|
||||
export_capacity = min(
|
||||
remaining_inverter_ac_capacity, remaining_battery_ac, rated_export_ac
|
||||
)
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import Field, computed_field, model_validator
|
||||
from pydantic import Field, computed_field, field_validator, model_validator
|
||||
|
||||
from akkudoktoreos.config.configabc import ConfigScope
|
||||
from akkudoktoreos.devices.devicesabc import validate_efficiency_curve
|
||||
from akkudoktoreos.devices.settings.devicebasesettings import (
|
||||
DevicesBaseSettings,
|
||||
)
|
||||
@@ -62,6 +63,38 @@ class InverterCommonSettings(DevicesBaseSettings):
|
||||
"x-scope": [str(ConfigScope.GENETIC), str(ConfigScope.GENETIC0)],
|
||||
},
|
||||
)
|
||||
dc_to_ac_efficiency_curve: Optional[list[tuple[float, float]]] = Field(
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Load-dependent efficiency of DC→AC conversion for battery discharging as "
|
||||
"a list of [load_fraction, efficiency] points. The load fraction is the AC "
|
||||
"power relative to max_power_w (0–1); points must be strictly increasing in "
|
||||
"load fraction, efficiencies in (0, 1]. Values are interpolated linearly and "
|
||||
"clamped to the first/last point. When set, the curve replaces "
|
||||
"dc_to_ac_efficiency. null keeps the constant dc_to_ac_efficiency. "
|
||||
"Default null."
|
||||
),
|
||||
"examples": [None, [[0.02, 0.8], [0.07, 0.93], [0.3, 0.96], [1.0, 0.95]]],
|
||||
"x-scope": [str(ConfigScope.GENETIC)],
|
||||
},
|
||||
)
|
||||
dc_to_ac_efficiency_reference_load_fraction: float = Field(
|
||||
default=0.06,
|
||||
ge=0,
|
||||
le=1,
|
||||
json_schema_extra={
|
||||
"description": (
|
||||
"Load fraction of max_power_w (0–1) at which dc_to_ac_efficiency_curve is "
|
||||
"evaluated to value stored battery energy without a specific conversion "
|
||||
"(remaining energy at the end of the horizon, AC charge break-even). Should "
|
||||
"represent the typical load served by the battery, e.g. the night-time base "
|
||||
"load. Only used with dc_to_ac_efficiency_curve. Default 0.06."
|
||||
),
|
||||
"examples": [0.06, 0.1],
|
||||
"x-scope": [str(ConfigScope.GENETIC)],
|
||||
},
|
||||
)
|
||||
max_ac_charge_power_w: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
@@ -351,6 +384,13 @@ class InverterCommonSettings(DevicesBaseSettings):
|
||||
# Validators
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@field_validator("dc_to_ac_efficiency_curve")
|
||||
@classmethod
|
||||
def _validate_dc_to_ac_efficiency_curve(
|
||||
cls, value: Optional[list[tuple[float, float]]]
|
||||
) -> Optional[list[tuple[float, float]]]:
|
||||
return validate_efficiency_curve(value)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_soc_factors(self) -> "InverterCommonSettings":
|
||||
if self.battery_min_soc_factor >= self.battery_max_soc_factor:
|
||||
@@ -375,6 +415,10 @@ class InverterCommonSettings(DevicesBaseSettings):
|
||||
battery_id=self.battery_id,
|
||||
ac_to_dc_efficiency=self.ac_to_dc_efficiency,
|
||||
dc_to_ac_efficiency=self.dc_to_ac_efficiency,
|
||||
dc_to_ac_efficiency_curve=self.dc_to_ac_efficiency_curve,
|
||||
dc_to_ac_efficiency_reference_load_fraction=(
|
||||
self.dc_to_ac_efficiency_reference_load_fraction
|
||||
),
|
||||
max_ac_charge_power_w=self.max_ac_charge_power_w,
|
||||
ac_charge_limits_total_charge=self.ac_charge_limits_total_charge,
|
||||
)
|
||||
|
||||
@@ -1002,7 +1002,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
self._terminal_value_reason = "terminal_value_mode is FIXED"
|
||||
return None
|
||||
|
||||
dc_to_ac = inverter.dc_to_ac_efficiency if inverter else 1.0
|
||||
dc_to_ac = inverter.reference_dc_to_ac_efficiency if inverter else 1.0
|
||||
# A full battery, expressed in the same unit as the curve: AC energy
|
||||
# that can actually leave the house.
|
||||
max_energy_wh = (
|
||||
@@ -1125,7 +1125,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
# Usable DC energy, converted to the AC energy that can serve a load.
|
||||
energy_wh = battery.current_energy_content()
|
||||
if self.simulation.inverter:
|
||||
energy_wh *= self.simulation.inverter.dc_to_ac_efficiency
|
||||
energy_wh *= self.simulation.inverter.reference_dc_to_ac_efficiency
|
||||
|
||||
curve = getattr(self, "_terminal_value_curve", None)
|
||||
if curve is not None and curve.energy_wh:
|
||||
@@ -3129,12 +3129,15 @@ class GeneticOptimization(OptimizationBase):
|
||||
inv = self.simulation.inverter
|
||||
bat = self.simulation.battery
|
||||
|
||||
# Full round-trip efficiency: 1 Wh drawn from grid → η Wh delivered to AC load
|
||||
# Full round-trip efficiency: 1 Wh drawn from grid → η Wh delivered to AC load.
|
||||
# The later discharge has no known size here, so the DC-to-AC part
|
||||
# is the efficiency at the inverter's reference load (equal to
|
||||
# dc_to_ac_efficiency without an efficiency curve).
|
||||
round_trip_eff = (
|
||||
inv.ac_to_dc_efficiency
|
||||
* bat.charging_efficiency
|
||||
* bat.discharging_efficiency
|
||||
* inv.dc_to_ac_efficiency
|
||||
* inv.reference_dc_to_ac_efficiency
|
||||
)
|
||||
|
||||
# Configurable penalty multiplier (default 1 = economic loss in €)
|
||||
@@ -3160,7 +3163,7 @@ class GeneticOptimization(OptimizationBase):
|
||||
free_ac_wh = (
|
||||
max(0.0, initial_soc_wh - bat.min_soc_wh)
|
||||
* bat.discharging_efficiency
|
||||
* inv.dc_to_ac_efficiency
|
||||
* inv.reference_dc_to_ac_efficiency
|
||||
)
|
||||
|
||||
# Prices/loads/free energy are constant within one optimization
|
||||
@@ -3186,7 +3189,8 @@ class GeneticOptimization(OptimizationBase):
|
||||
# corresponding cost per useful/exported AC Wh.
|
||||
lcos_per_wh_dc = getattr(bat, "levelized_cost_of_storage_kwh", 0.0) / 1000.0
|
||||
break_even_price = (
|
||||
charge_price / round_trip_eff + lcos_per_wh_dc / inv.dc_to_ac_efficiency
|
||||
charge_price / round_trip_eff
|
||||
+ lcos_per_wh_dc / inv.reference_dc_to_ac_efficiency
|
||||
)
|
||||
|
||||
best_uncovered_price = best_prices[hour]
|
||||
|
||||
@@ -128,7 +128,7 @@ class TailValueCurve(TerminalValueCurve):
|
||||
battery=bat,
|
||||
slot_duration_h=context["slot_duration_h"],
|
||||
)
|
||||
conversion = bat.discharging_efficiency * inv.dc_to_ac_efficiency
|
||||
conversion = bat.discharging_efficiency * inv.reference_dc_to_ac_efficiency
|
||||
state_wh = bat.min_soc_wh + (energy_wh / conversion if conversion > 0 else 0.0)
|
||||
state_wh = float(np.clip(state_wh, bat.min_soc_wh, bat.max_soc_wh))
|
||||
plan: list[TailPlanSlot] = []
|
||||
@@ -231,7 +231,9 @@ def build_tail_value_curve(
|
||||
bat.charge_array = np.zeros(1, dtype=float)
|
||||
inv = Inverter(inverter.parameters, battery=bat, slot_duration_h=battery.slot_duration_h)
|
||||
states = np.linspace(bat.min_soc_wh, bat.max_soc_wh, grid_points)
|
||||
usable = (states - bat.min_soc_wh) * bat.discharging_efficiency * inv.dc_to_ac_efficiency
|
||||
usable = (
|
||||
(states - bat.min_soc_wh) * bat.discharging_efficiency * inv.reference_dc_to_ac_efficiency
|
||||
)
|
||||
continuation_values = np.array([continuation.value(e) for e in usable])
|
||||
operating_values = np.zeros(len(states))
|
||||
values = continuation_values.copy()
|
||||
|
||||
Reference in New Issue
Block a user