microgrid.engine.extend¶
For a client's own plant model: the port and the price rule over it.
A plant model is any object that keeps PlantModel: it projects a horizon as
SlotFlow s, its dispatch's bookkeeping in each slot's SlotDispatch,
answers the price rule's questions about its battery through Storage, and reads
its meters into Flows. microgrid.engine.testing.assert_plant_model_contract
checks one.
The operations the engine is built from, for a client that brings its own flows, traces
or prices (a scorer, a backtest): the price rule over given flows (price_flows,
with the noise floors it reads them at: MODEL for a plant model's flows,
OBSERVED for metered ones; the house importing and PV spilling in a slot, as it
reads them: importing, spilling), the valuation of given PV scenarios
(value_energy) and the surplus forecast (forecast_surplus).
Pure: no I/O.
MODEL
module-attribute
¶
MODEL: Final = Noise(
spill_kw=0.001,
short_kw=0.001,
charge_kw=0.001,
topup_kw=_TOLERANCE,
need_kw=_TOLERANCE,
soc=1e-06,
)
The plant model's flows: float residue only (plant.surplus's 1 W floor). The house importing too: a battery that runs empty at dawn with 50 W short is empty (20 Wh a slot missed it and priced the night at the next fill).
OBSERVED
module-attribute
¶
OBSERVED: Final = Noise(
spill_kw=0.12,
short_kw=0.12,
charge_kw=0.12,
topup_kw=0.5,
need_kw=0.12,
soc=0.005,
regulation_kw=0.3,
)
Metered flows: the default floors (EngineConfig.observed), measured on the plant
the engine was first validated on (the 30 days to 2026-09-24); a plant whose
meters report differently sets its own. The measurements:
- spill, import, need: above the model's own import floor, 0.12 kW (20 Wh in a ten-minute slot). The grid and battery sensors report at different instants: net PV export far from full (SOC under 95 %) was up to 0.066 kW in 90 % of exporting slots, 0.13 kW in 95 %; the 5 % above the floor are real exports on PV ramps, while the battery charged below its limit.
- PV charging: 0.12 kW, the same floor. Where PV fell 0.5 kW or more short of the site's consumption, so it could not charge the battery, the meters booked PV charging of at most 0.051 kW in 99.9 % of 2455 slots, and over 0.12 kW once (sensor timing).
- top-up: 0.5 kW. Every grid-charging run the plant made peaked at 2.9 kW or more; the sensors' timing made single slots of up to 0.42 kW (0.07 kWh).
- full and empty: half a point of SOC. Saturation read exactly in 269 of 280 exporting slots at full; the reserve exactly in 64 of 65 importing slots near it.
- regulation: 0.3 kW. While the battery served the house with headroom, the grid supplied up to 0.14 kW in 99.5 % of slots. With the battery idle above its reserve, imports over 0.12 kW were either the dusk hand-off (up to 0.2 kW, a slot as PV fades) or the plant holding the battery (0.4 kW and up, the house load): the first is the battery's, the second the grid's.
SurplusForecast
dataclass
¶
SurplusForecast(
starts: tuple[datetime, ...],
surplus_kw: tuple[float | None, ...],
surplus_kw_p10: tuple[float | None, ...],
regime: tuple[Regime | None, ...],
regime_p10: tuple[Regime | None, ...],
surplus_value: tuple[float | None, ...],
surplus_value_p10: tuple[float | None, ...],
flows: tuple[SlotFlow, ...],
flows_p10: tuple[SlotFlow, ...] = (),
)
Surplus and regime per slot, from the p50 and pessimistic (p10) PV cases.
Every per-slot tuple is None from the first slot whose inputs were unknown.
surplus_kw
instance-attribute
¶
PV the plan exports or curtails: additive, the energy there is to share.
surplus_value
instance-attribute
¶
Per kWh of the surplus (Surplus.value).
flows
instance-attribute
¶
The p50 case's run, and the p10 case's: the valuation prices the p10 case on the same run rather than projecting it again.
EnergyValue
dataclass
¶
EnergyValue(
price: tuple[float, ...],
price_p10: tuple[float, ...],
certainty_spill: tuple[float, ...],
certainty_covered: tuple[float, ...],
pv_value: tuple[PvValue | None, ...] = (),
battery_value: tuple[BatteryValue | None, ...] = (),
stored_value: tuple[float, ...] = (),
unserved: tuple[float, ...] = (),
model: Model | None = None,
)
Per slot: the price of one more kWh of flexible load, and how sure it is.
price_p10
instance-attribute
¶
Per kWh in the pessimistic PV case (R-6): priced on Solcast's p10.
certainty_spill
instance-attribute
¶
Scenario weight in which the kWh costs only exported or curtailed PV (0-1).
certainty_covered
instance-attribute
¶
Scenario weight in which the sun charges the battery enough for the expensive hours ahead (0-1).
pv_value
class-attribute
instance-attribute
¶
Values by source (R-2, R-3), expected over the scenarios in which the source has energy to give (None: in none). A PV kWh diverted to a flexible load, and how that PV was routed; a battery kWh drawn for one, and what the battery buys it back with.
battery_value
class-attribute
instance-attribute
¶
stored_value
class-attribute
instance-attribute
¶
Per stored kWh (FlowPrices.stored_value), expected over the scenarios.
unserved
class-attribute
instance-attribute
¶
Scenario weight in which the slot has unserved load (0-1): its price there is the import as a floor, not a reachable source.
model
class-attribute
instance-attribute
¶
The scenarios as run, for running them again (_live.revalue).
Event
¶
Bases: StrEnum
What priced a slot's kWh (module docstring, the rule).
IMPORT
class-attribute
instance-attribute
¶
The house importing in the slot itself.
TOPUP
class-attribute
instance-attribute
¶
Later: the plant tops the battery up from the grid.
EMPTY
class-attribute
instance-attribute
¶
Later: the battery runs empty and the house imports.
EXPORT
class-attribute
instance-attribute
¶
Later: a battery-export run ends at its floor sooner, a kWh less exported.
UNSERVED
class-attribute
instance-attribute
¶
Load no source can serve, now or where the battery empties at the import limit, priced at the slot's import as a floor.
HORIZON
class-attribute
instance-attribute
¶
None of them within the flows: the day's import price.
FlowPrices
dataclass
¶
FlowPrices(
prices: tuple[float, ...],
served: tuple[Served, ...],
make_up: tuple[float, ...],
make_up_event: tuple[Event, ...],
stored_value: tuple[float, ...],
spilled: tuple[bool, ...],
covered: tuple[bool, ...],
events: tuple[Event, ...],
event_at: tuple[int, ...],
outcome_at: tuple[int, ...],
cover_at: tuple[int, ...],
resolved: tuple[bool, ...],
covered_resolved: tuple[bool, ...],
pv_prices: tuple[float, ...] = (),
grid_diverted: tuple[float, ...] = (),
)
The rule over one sequence of slot flows, per slot.
prices
instance-attribute
¶
Per kWh used in the slot: what serves it (served), at its cost.
make_up
instance-attribute
¶
Per kWh the battery gives up in the slot: what it buys it back at (the
next fill's export, top-up's or empty's import, or the day's import), before
losses and wear; with what (make_up_event).
stored_value
instance-attribute
¶
Per stored kWh the battery gives up in the slot (module docstring):
Battery.refill_value of the make-up after a fill or a top-up,
Battery.displaced_value after an empty battery or the horizon.
spilled
instance-attribute
¶
The kWh costs only spilled PV, now or when the battery fills.
covered
instance-attribute
¶
The expensive hours ahead are served without an import.
events
instance-attribute
¶
What priced it, and in which slot (the flows' length for the horizon).
outcome_at
instance-attribute
¶
The first slot of the run of spilling, importing or topping up that the event belongs to: slots that share it rest on one outcome (a fill and the afternoon's spill are one).
cover_at
instance-attribute
¶
The first slot of the expensive run covered is about (the flows' length: none).
resolved
instance-attribute
¶
The price was settled by an event within the flows, not by the horizon's end.
covered_resolved
instance-attribute
¶
covered was settled within the flows: the run ahead ended in them, or
an import already broke it.
pv_prices
class-attribute
instance-attribute
¶
Per kWh of PV diverted from charging the battery, and of the grid's charge
diverted at the import limit (the supply stack's pv and grid steps):
the dead band's charge side on the make-up rule; on the stored cost, PV at the
export, but both at the make-up where the grid buys the kWh back in the slot.
Empty without a battery.
Noise
dataclass
¶
Noise(
spill_kw: float,
short_kw: float,
charge_kw: float,
topup_kw: float,
need_kw: float,
soc: float,
regulation_kw: float = 0.0,
)
Below these, a flow or an SOC difference is not an event for the rule.
The kW floors are for 10-minute slots (OBSERVED was measured on them).
spill_kw
instance-attribute
¶
PV exported or curtailed, net of what the battery and grid supplied in the same slot, kW.
need_kw
instance-attribute
¶
The battery and the grid serving the house (PV no longer covers it), kW.
soc
instance-attribute
¶
Full is within this much SOC of saturation, empty of the reserve (0-1).
regulation_kw
class-attribute
instance-attribute
¶
An import up to this, kW, while the battery was above its reserve and had the headroom to serve it, is the plant's grid regulation, not the house importing (metered flows only; 0: none).
Served
¶
Bases: StrEnum
What a kWh used in a slot takes first.
PV
class-attribute
instance-attribute
¶
PV charging the battery now: a kWh diverted leaves it charged a kWh less.
BATTERY
class-attribute
instance-attribute
¶
The battery, discharging or able to.
GRID
class-attribute
instance-attribute
¶
The grid: the house importing now, the plant topping the battery up, or the battery with nothing left to give and no PV charging it.
Flows
dataclass
¶
Flows(
consumed: float,
pv_to_load: float,
battery_to_load: float,
grid_to_load: float,
pv_to_battery: float,
grid_to_battery: float,
pv_export: float,
battery_export: float,
dc_pv_to_battery: float = 0.0,
battery_to_dc_load: float = 0.0,
)
One slot's metered flows, mean kW, allocated to sources and sinks: AC, except
the DC-bus paths, each in its path's terms as SlotFlow's are.
consumed is what the AC side served: its loads, the inverters' draw, and what
the hybrid took from it for DC-bus loads.
dc_pv_to_battery
class-attribute
instance-attribute
¶
DC PV into a battery on the DC bus, DC kW; the battery serving DC-bus loads.
RunEnd
dataclass
¶
RunEnd(
export_lost: float,
not_delivered: float,
imported: float,
refilled: float,
refilled_dc: float,
carried: float,
)
What a stored kWh less does in the slot where a battery-export run reaches its floor: the run ends sooner. Each per stored kWh: the AC export the site loses (net of PV the run's end frees), the AC the battery no longer delivers (the discharge wear saved), the AC the house imports instead (where the floor is the reserve), the AC of freed PV that charges the battery (at its charge wear), and the stored kWh still short after the slot (signed: below 0, ahead), which the next event prices. The plant model's.
refilled
instance-attribute
¶
Freed PV charged: on the AC bus (AC kWh), and on the battery's DC bus (DC kWh).
SlotDispatch
dataclass
¶
SlotDispatch(
*,
held: bool = False,
topup_gap: float | None = None,
crossing: float = 0.0,
battery_port: float = 0.0,
crossing_ac: float | None = None,
battery_port_in: float = 0.0,
battery_port_out: float = 0.0,
bus_clipped: float = 0.0,
run_end: RunEnd | None = None,
unserved_unreachable: float = 0.0,
hold_session: bool | None = None,
)
One slot's dispatch bookkeeping: what a plant model that decides the battery's
dispatch knows of the slot beyond its flows. A plant model without it
(SlotFlow.dispatch left at its default) gets every field's default: no hold, no
grid-charge window (the price rule reads the grid charging the battery instead), no
battery-export run's end, no PV clipped on a DC bus, all unserved load reachable,
no hold session, and no hybrid's AC (the ground truth then meters a battery on a
DC bus by its own flows, which leaves out DC PV and DC-bus loads crossing).
What reads it. The price rule reads held, topup_gap, run_end,
bus_clipped and unserved_unreachable (through
SlotFlow.unserved_reachable); the live value reads hold_session; the
general dispatch's Storage reads crossing, battery_port and its gross
battery_port_in and battery_port_out; the ground truth reads crossing_ac.
held
class-attribute
instance-attribute
¶
The plant holds the battery (tops it up, keeps it at its target, or a hold rule keeps it through a DC-bus load's session) and the grid serves the loads. False for metered flows.
topup_gap
class-attribute
instance-attribute
¶
In the plant's grid-charge window, how far short of its course to the target the battery is, stored kWh, before the slot's top-up (> 0: it tops up; about 0: on course, after an early top-up; < 0: PV will carry it there, or later in the window for a late top-up). A kWh less where it is not below 0 is bought back from the grid. None outside the window and for metered flows, which show only the grid charging.
crossing
class-attribute
instance-attribute
¶
The net DC kW the hybrid inverter converts from its DC bus to the AC side (< 0:
from the AC side to the DC bus), and the battery's power at its own port, kW
(> 0 charging; DC at a DC bus, AC on the AC bus): what a plant with a DC bus
answers its Storage questions from.
crossing_ac
class-attribute
instance-attribute
¶
The AC kW the hybrid's net crossing gives the AC side (< 0: takes from it): what
its AC meter reads before its own draw, the battery's flows, the DC PV crossing
and the AC it takes for DC-bus loads all in it. Each part's own, so a slot run
in parts holds their mean (the curve at the mean crossing is not it). 0
without a hybrid; None for metered flows and a plant model that does not give
it.
battery_port_in
class-attribute
instance-attribute
¶
What the battery took in at its port and gave out over the slot, kW, each >= 0 in
battery_port's terms: a metered slot can both charge and discharge, which the
net battery_port hides. A plant model that leaves both at 0 gets its storage's
answers from the net: in where battery_port > 0, out where < 0.
bus_clipped
class-attribute
instance-attribute
¶
Of SlotFlow.pv_clipped, what the hybrid clipped at its rating on its DC bus:
what a battery there, with room, would take (the fill's clipped PV). The rest was
capped at an array's own rating, before the bus. 0 for metered flows.
run_end
class-attribute
instance-attribute
¶
Where a battery-export run reaches its floor in this slot, what a stored kWh less
does (RunEnd); None elsewhere, and for metered flows.
unserved_unreachable
class-attribute
instance-attribute
¶
Of SlotFlow.unserved, what no stored kWh could serve: a DC-bus load beyond
the hybrid's rating, the battery not on its bus (on its own inverter, the battery
crosses the same rating). A kWh more or less in the battery leaves it as it is,
so the price rule does not read it as unserved.
hold_session
class-attribute
instance-attribute
¶
In a hold rule's session, whether it holds the battery; None outside one, and for
metered flows. A projection that starts in a session takes it as
held_at_start, as the plant decided it where the session began.
SlotFlow
dataclass
¶
SlotFlow(
*,
start: datetime,
soc_start: float | None,
soc_end: float | None,
pv_to_load: float = 0.0,
pv_to_battery: float = 0.0,
pv_export: float = 0.0,
pv_curtail: float = 0.0,
battery_to_load: float = 0.0,
grid_to_load: float = 0.0,
grid_to_battery: float = 0.0,
pv_clipped: float = 0.0,
dc_pv_to_battery: float = 0.0,
dc_pv_to_ac: float = 0.0,
dc_pv_to_dc_load: float = 0.0,
battery_to_dc_load: float = 0.0,
battery_export: float = 0.0,
ac_to_dc_load: float = 0.0,
unserved: float = 0.0,
conversion_loss: float = 0.0,
inverter_draw: float = 0.0,
dispatch: SlotDispatch = (lambda: _NO_DISPATCH)(),
)
Mean power flows (kW) over one slot, by path, and the SOC (0-1) at its ends.
Paths. The pv_* fields without dc_ are PV
on the AC bus: an AC array's, or a DC array's that the hybrid converted
(dc_pv_to_ac). The dc_pv_* fields are a DC array's PV on the DC bus.
pv_to_load, battery_to_load and grid_to_load serve the AC side: its
loads, the inverters' draw (inverter_draw) and what the hybrid takes from
it for DC-bus loads (ac_to_dc_load).
Units. Energy into the battery is in
its source's terms (pv_to_battery AC, dc_pv_to_battery DC,
grid_to_battery AC at the meter); what serves a load is what the load
receives (AC on the AC side, DC for a DC-bus load); what crosses the site's
meter is AC at the meter; dc_pv_to_ac is what left the DC array.
Balance. Over a slot hours long, in which the stored energy moves by
stored kWh (from the SOC), sources equal sinks: the PV +
grid_to_load + grid_to_battery = the loads - unserved +
inverter_draw + pv_export + battery_export + pv_curtail +
pv_clipped + conversion_loss + stored / hours. The PV (each
array's output, in its own terms) and the loads are the slot's inputs to the
projection, not fields: once a DC path carries energy, neither can be rebuilt
from the fields, since the AC a DC array delivers and the AC a DC-bus load
takes are inside the AC fields.
A path the plant does not have is 0. On this site's plant (AC-coupled PV, the
battery folded on the hybrid's DC bus, one AC load) every dc_ path,
battery_export, ac_to_dc_load and unserved is 0, and the fields up
to pv_clipped carry every flow.
Keyword-only; the dispatch's bookkeeping is one record, dispatch
(SlotDispatch).
pv_clipped
class-attribute
instance-attribute
¶
PV the plant could neither convert to AC nor store, such as DC beyond a hybrid inverter's AC rating with the battery full: no AC load can take it, so it is neither spill nor surplus, and nothing that prices spare PV reads it. The AC-coupled plant reports 0: its clip happens in the micro-inverters, before the forecast.
dc_pv_to_battery
class-attribute
instance-attribute
¶
DC PV into the battery on the DC bus, DC kW.
dc_pv_to_ac
class-attribute
instance-attribute
¶
DC PV the hybrid converts to the AC side, DC kW: what it delivers is in the AC
bus's PV fields, and its loss in conversion_loss.
dc_pv_to_dc_load
class-attribute
instance-attribute
¶
DC PV serving DC-bus loads, DC kW.
battery_to_dc_load
class-attribute
instance-attribute
¶
The battery serving DC-bus loads, DC kW.
battery_export
class-attribute
instance-attribute
¶
The battery exported, AC kW at the meter. The plant model's: a metered slot leaves it out until the price rule has the export event.
ac_to_dc_load
class-attribute
instance-attribute
¶
The AC side serving DC-bus loads through the hybrid, DC kW the loads receive.
The AC it takes is inside the *_to_load fields, and its loss in
conversion_loss.
unserved
class-attribute
instance-attribute
¶
Load no source could serve, kW: what an import limit leaves (AC at the meter), or a DC-bus load beyond what the hybrid's rating lets the AC side send it (DC at the load).
conversion_loss
class-attribute
instance-attribute
¶
Energy lost converting, kW: what the battery's charge took less what it stored, what its discharge drew less what it delivered, and the hybrid's other conversions. The plant model's; 0 for metered flows.
inverter_draw
class-attribute
instance-attribute
¶
The inverters' own draw, kW: standby, and the overhead while the battery works.
It is inside the *_to_load fields, which serve the load and it, and the
balance counts it once. The plant model's; 0 for metered flows.
dispatch
class-attribute
instance-attribute
¶
The plant model's dispatch bookkeeping (SlotDispatch); a plant model
without it leaves it at the default, every field's default.
unserved_reachable
property
¶
Unserved load a stored kWh would serve: what the price rule reads.
battery_out
property
¶
All the battery delivers, kW: to the AC side, to DC-bus loads and exported, each in its path's terms.
moving
property
¶
What the battery charges or discharges, kW, each path in its terms (all AC on this site's plant).
pv_spare
property
¶
PV the loads do not use: charging, exported or curtailed (not clipped, which nothing could use).
ChargeSource
¶
Bases: StrEnum
Where a kWh that charges the battery comes from, and by which path. Not
Source: the ledger names its lot sources that (ledger.Source).
DC_PV
class-attribute
instance-attribute
¶
PV on the hybrid's DC bus (SlotFlow.dc_pv_to_battery). A plant without that
path raises ValueError when asked about it.
PlantModel
¶
Bases: Protocol
How the plant will behave: the price model's only view of it.
has_pv
property
¶
Whether the plant has PV: without it, no PV forecast is asked for (its
pv_forecast and pv_power are not causes) and its PV is 0.
pv_arrays
property
¶
Its PV arrays' names: what PV per array is keyed by.
pv_per_array
property
¶
Whether it needs PV per array: its arrays have both couplings, so no one series can say what each makes.
import_limit_kw
property
¶
The grid's import limit, kW AC at the meter; None where none binds. A slot at it cannot import a kWh more.
has_dc_loads
property
¶
Whether a load connects to the hybrid's DC bus: then its forecast and live
draw are inputs (dc_load_kw).
meters
property
¶
Its meters: each name a MeterReading keys, to what it measures (pv,
grid, battery, consumption or dc_load).
project
¶
project(
starts: Sequence[datetime],
durations: Sequence[timedelta],
pv_kw: PvKw,
load_kw: Sequence[float],
soc: float | None,
*,
dc_load_kw: Sequence[float] | None = None,
held_at_start: bool | None = None,
stop: Callable[[int, SlotFlow], bool] | None = None,
) -> tuple[SlotFlow, ...]
The plant's flows slot by slot from soc (0-1), as it would dispatch
them, given PV (its source's terms) and the loads it serves by connection point:
load_kw on the AC side, and dc_load_kw on the hybrid's DC bus, DC kW at
the loads (None: none draws). A load series that holds a DC-bus load's draw is
all AC, as before. A hold rule's session under way at the first slot holds the
battery as held_at_start says (None: as the SOC there decides;
SlotDispatch.hold_session reports it slot by slot). It is one state: with
several rules, every session open at the first slot takes it, exact where one
rule holds (plant/2 allows several, and a plant whose rules overlap carries the
approximation). It validates only what it receives: every value is known,
because the caller builds durations and ends its inputs at the first unknown
slot, and a projection shorter than the horizon is the plant_model cause
(R-7). A what-if is the same call with a different load. stop
ends the projection early (a what-if, once the battery agrees again): the price
model no longer asks for it, but it is kept for the port's
stability and for the tests' re-simulation oracle (engine_fixtures.rerun_cost).
A plant without a battery takes soc None and projects SOCs of None; one
without PV takes PV of 0. PV is one series for the site, or one per array
named as pv_arrays (PvKw).
pv_from_meters
¶
The PV reading meters for one slot, as project takes it: per
array where per_array, else the site's. None where a PV meter is unknown or
the meters cannot say it (one total for arrays valued apart).
read_meters
¶
The plant's meters split between sources and sinks, all of it, battery export included: what the battery ledger books lots from. Each meter in its configured sign; a name the plant does not meter raises, and a meter unknown (missing, or None) leaves the split unknown: None.
metered_flow
¶
The meters now (start UTC), read as a slot's flows for the price rule
(the live value; built from read_meters); None where a meter is
unknown. The price model then asks importing(storage, slot, noise) with the
configured floors (EngineConfig.observed), so the noise thresholds stay on
its side.
Storage
¶
Bases: Protocol
The battery as the price rule asks about it. Every question names the slot's flows and the energy's path, so a topology answers for its own conversion.
Every answer that reads the battery's discharge counts all of its output
(SlotFlow.battery_out: to the AC side, to DC-bus loads and exported).
reserve_soc
property
¶
The SOC (0-1) the plant keeps in reserve: empty, to the price rule.
wear_discharge_per_kwh
property
¶
Wear per stored kWh delivered, in the prices' currency.
charge_wear_rate
¶
Wear per kWh charged from source, in that source's terms
(pv_to_battery, grid_to_battery or dc_pv_to_battery): the paths
convert differently, so a source kWh can wear the battery differently.
divertible_kw
¶
AC kW a load could take from the PV now charging the battery, in what the load would receive.
can_charge_more
¶
Whether this slot could store more from source, short of its limit by
more than margin_kw (the top-up test). For the grid it includes the plant's
own order of charging (Plant's, PV first: the grid charges more only
where no more than margin_kw of PV is spare, since spare PV would refill
the battery first).
at_discharge_limit
¶
Whether the battery delivers all it can, within margin_kw.
can_serve
¶
Whether the battery could also deliver more_kw in this slot.
grid_refills_diverted_pv
¶
Whether a top-up makes up PV a load diverted from charging (where PV charges
before the grid, as Plant's dispatch has it, yes).
takes_clipped_pv
¶
Whether the battery, with room, would take PV this slot clips: true on the clipping hybrid's DC bus, false on its own inverter beside it, which the clipped PV cannot reach.
headroom
¶
kW more the battery could deliver in this slot, hours long.
refill_value
¶
The cost per stored kWh, refilled from source at price in this slot
(that path's conversion at that power, and wear).
displaced_value
¶
The worth per stored kWh, at price once delivered (conversion, wear); at
flow's power, or at low power where no one knows it.
dead_band
¶
The prices a stored kWh worth stored_value is not worth moving between,
at this slot's powers: charging pays below the first, discharging above the
second.
stored_per_kwh
¶
Stored kWh per kWh charged from source (in that source's terms, as
charge_wear_rate's), by that source's own path: averaged over the slot's
charge where flow is given (the loss grows with power), else for a charge at
the battery's own limit (a kWh bought to fill it). What the stored cost books a
charge at (project_average): a topology answers for its paths, as a
split battery on a hybrid's DC bus converts the grid's AC through the hybrid and
then its own port, and DC PV through its port alone. A plant without the DC PV
path raises ValueError when asked about it.
forecast_surplus
¶
forecast_surplus(
plant: PlantModel,
starts: Sequence[datetime],
pv_p50: PvMaybe,
pv_p10: PvMaybe,
load_kw: Sequence[float | None],
soc: float | None,
*,
step: timedelta = SLOT,
first_remaining: timedelta | None = None,
p_export: Sequence[float | None] | None = None,
dc_load_kw: Sequence[float | None] | None = None,
held_at_start: bool | None = None,
) -> SurplusForecast
Project both PV cases through plant and derive surplus, regime and surplus
value. A projection ends at the first slot whose PV or load is unknown; every
later slot is None (the plant_model cause, R-7). soc is None
for a plant without a battery. dc_load_kw is the DC-bus loads' (None: none
draws), unknown where a slot's is None.
importing
¶
The house imports in the slot: the battery could not, or would not, serve it.
Whether the battery could carry the import is the plant model's answer
(storage.can_serve). The test is on power, so a slot part way through is
judged as a whole one would be. Without a battery (storage
None), any import above the floor is the house's: there is no regulation.
price_flows
¶
price_flows(
storage: Storage | None,
flows: Sequence[SlotFlow],
p_import: Sequence[float],
p_export: Sequence[float],
day: int,
*,
noise: Noise = MODEL,
import_limit_kw: float | None = None,
stored_cost: Sequence[float | None] | None = None,
) -> FlowPrices
The rule (module docstring) over flows: a scenario's, or what was metered.
stored_cost: what a stored kWh is worth in each slot, the running average cost of
what the battery holds then (ledger.project_average), per stored kWh; where given
(and not None) it is the value the battery's dead band is drawn from, in place of
the make-up's. The events ahead still say what displaces the kWh and how sure.
day is the slots in a day. Without a battery (storage None), see
_price_without_storage. At the grid's import_limit_kw a kWh more cannot
be imported: diverted PV serves it, else the grid's charge diverted, else the battery
beyond its policy, else it is unserved (docs/methodology/make-up-rule.md, Limits and
unusual plants).
spilling
¶
PV spills in the slot, net of what the battery and the grid supplied in it.
The model never exports while it discharges or imports, so for its flows the net is the gross; on the meters a slot can hold both (sensor timing).
value_energy
¶
value_energy(
plant: PlantModel,
starts: Sequence[datetime],
scenarios: Sequence[PvKw],
load_kw: Sequence[float],
soc: float | None,
p_import: Sequence[float],
p_export: Sequence[float],
*,
step: timedelta,
first_remaining: timedelta | None = None,
durations: Sequence[timedelta] | None = None,
weights: Sequence[float] | None = None,
pessimistic: PvKw | None = None,
pessimistic_flows: Sequence[SlotFlow] | None = None,
dc_load_kw: Sequence[float] | None = None,
held_at_start: bool | None = None,
lots: Lots | None = None,
battery_value: BatteryValueRule = "stored_cost",
) -> EnergyValue
The price of one more kWh of flexible load in each slot (module docstring).
scenarios are PV forecasts (kW per slot), equally likely unless weights says
otherwise; pessimistic is the PV trace price_p10 is priced on (else it is
the expectation), and pessimistic_flows its run where the caller has projected
it already, on these same inputs (the surplus forecast's p10 case). Every per-slot
sequence covers starts. soc (0-1) is the SOC now, part way through the first
slot when first_remaining is given; durations gives every slot its own
length instead (the live value's), with step still the slot a day is counted in.
soc is None for a plant without a battery, which then has no values by battery.
load_kw is the AC side's, where a flexible load's kWh lands; dc_load_kw the
DC-bus loads', which it passes to the plant as they are (None: none draws), and
held_at_start whether a hold rule's session under way now holds the battery
(PlantModel.project).