microgrid.engine¶
The engine, its configuration, its inputs and its outputs. The other API pages hold the rest of the public names; every other module is private.
The microgrid engine: the model that values energy by time and source.
It has no dependencies. The contract is what this module
exports; every other module (_value, plant._paths, ...) is private.
engine = Engine(EngineConfig(plant=plant.Plant(plant.PlantConfig(tz=..., pv=..., ...))))
# what the battery holds and what it cost: the ledger over the meters' recent record
# (``ledger.integrate`` the meters, ``ledger.Ledger.step`` each slot; a ``Lots`` after
# each), as a series; without it the battery's energy is priced as bought and the
# valuation names ``battery_lots``
lots = series.Series.of([(slot_end, ledger.Lots(slot_end, held, pool)), ...],
known_until=...)
# time series, each read as it stood at the moment:
valuation = engine.value(Inputs(prices=..., pv=..., load=..., soc=...,
dispatch_as_modelled=..., lots=lots), at=...)
# the live value: the valuation run again from the measured series at a moment
value = engine.live(Inputs(prices=None, soc=..., dispatch_as_modelled=...,
meters={"pv": ..., "grid": ..., "battery": ...},
measured_load=..., lots=lots),
at=..., valuation=valuation, valid_until=...)
# or, for readings that arrive one at a time
session = engine.live_session()
value = session.update(LiveReading(at=..., load_kw=..., soc=..., dispatch_as_modelled=...,
meters={"pv": ..., "grid": ..., "battery": ...}),
valuation, valid_until)
The microgrid command does all of this on a dataset, or on a site's sources now
(microgrid.cli; microgrid now runs the ledger over the recorded history).
Every valuation and live value has a kind (ValuationKind: unpriced, with no
price for the slot under way, among them) and names the engine's causes (Cause) in
degraded; a live value keeps the client's own beside them (external). A gap in
the load forecast that cut the horizon is Valuation.horizon_cut. The versions:
ENGINE_CONTRACT (engine-api/5), this API's; ENGINE_SCHEMA (engine/2),
the configuration's; VALUATION_SCHEMA (valuation/2), the outputs'; and
MODEL_VERSION (pricing/1.3), the price rule and plant model each valuation
names.
The public names, in tiers; every other module is private:
- this module:
Engineand its configuration (EngineConfig,Pricing,EngineConfigError,ENGINE_SCHEMA),Inputs, the live session (LiveSession,LiveReading), the outputs (Valuation,SlotValue,LiveValue,PvValue,BatteryValue,Step), their enums and parts (ValuationKind,Regime,Cause,Served,PvRouting,Displaces,InputState,Assessment), each output'sto_mapping(plain data, its schemavaluation/2shipped inschemas/) and the versions. microgrid.engine.series: the form a client's data takes (Series, values held until the next sample and known up toknown_until;Forecast), the operations on it, and the slot grid (SLOT,slot_starts, ...).microgrid.engine.units: a series in the engine's units.microgrid.plant: the plant a configuration describes (Plant,PlantConfigand its parts,BatteryandGridChargeamong them), behind the port, andexamples.microgrid.engine.ledger: the battery's lots, where the energy it holds came from and what it cost (Ledger,Lots).microgrid.engine.analysis: the models checked against what the meters did (scoring: what a kWh realised;ground_truth: the projection over metered inputs).microgrid.engine.extend: for a client's own plant model, the port (PlantModel,Storage,ChargeSource,SlotFlow) and the operations the engine is built from (price_flows,value_energy,forecast_surplus).microgrid.engine.testing:assert_plant_model_contract, for such a model.
A plant may have a DC bus: DC-coupled PV, a battery folded or split on a hybrid's
DC bus, the hybrid's AC rating, a hybrid beside a battery on its own inverter. A plant
may have no battery or no PV. Without a battery its storage is None and its SOCs are
None, and its inputs take no SOC or dispatch.
Without PV it takes no PV forecast. Without either, every kWh is the import price.
ENGINE_CONTRACT
module-attribute
¶
The API contract's version: the public names of every tier and what they mean. Its
own name, apart from the configuration's schema (engine/2, ENGINE_SCHEMA):
its number goes up with every change a client must follow (a name removed or renamed,
a signature or a meaning changed), and an addition leaves it. engine-api/2: names
removed, and the live value, the lots and the port changed; the API and the
configuration had shared one name.
engine-api/3: the plant model left the engine for its own package,
microgrid.plant (microgrid.engine.plant and the port's and flows' modules moved).
engine-api/4: no name says a currency (per_kwh for usd_per_kwh, the wear's
and the ledger's floor's names), as money is in the prices' own.
engine-api/5: analysis.ground_truth.net_inverter_kw takes no house load (a
hybrid's meter is the AC its crossing gives, SlotDispatch.crossing_ac).
MODEL_VERSION
module-attribute
¶
The engine's model: the price rule and the plant model it runs, as
every Valuation names them. A client that publishes its own version keeps
it beside this one.
VALUATION_SCHEMA
module-attribute
¶
The outputs' schema: what Valuation.to_mapping and
LiveValue.to_mapping give, shipped as schemas/valuation-2.schema.json.
valuation/2: per_kwh for valuation/1's usd_per_kwh.
Engine
¶
The model behind its contract.
value
¶
The horizon from at; unpriced, with no slots, without a price for the
slot under way.
Each series is read as it stood at at: what was received by at itself,
the load forecasts' ages judged at at itself. The valuation's moment is
at to whole seconds: a client may tag what it publishes with it, and it is
the plant_model input's as_of.
live
¶
live(
inputs: Inputs,
/,
*,
at: datetime,
valuation: Valuation | None,
valid_until: datetime | None,
external: Sequence[str] = (),
) -> LiveValue
The energy context at at: valuation (the latest the client published,
valid until valid_until: past it, issue is a cause and its prices still
serve) run again from the meters, the house load and the SOC as measured
(inputs.meters, inputs.measured_load, inputs.soc,
inputs.dispatch_as_modelled); unpriced without a price for the slot under
way (no valuation, or none that prices it).
A pure function of the series up to at: one slot from at at measured PV
and house load, each averaged over the series (_live.smoothed:
EngineConfig.smoothing, its load_jump_kw), from the measured SOC, then
the valuation's forecast. Surplus is what that slot would export or curtail.
When the grid is serving the house now, beyond the plant's regulation, a kWh
used now is imported too. The averages restart at a gap (R-7); the
warm-up after the client began reading publishes no surplus, one after a gap is
the fallback. A series need reach back only EngineConfig.smoothing_window
from at, and the sample in force then.
external are the client's own causes (a silent telemetry path, a meter it
cannot read): any makes the value the fallback. The engine names its own whatever
the client names, and keeps the client's beside them (external).
valid_until is required with a valuation: without it a valuation of any age
would be run again as current (R-7).
live_session
¶
A live session, for a client that has readings one at a time rather than
series: it keeps them as series for live.
EngineConfig
dataclass
¶
EngineConfig(
plant: PlantModel,
pricing: Pricing = Pricing(),
slot: timedelta = SLOT,
horizon: timedelta = timedelta(hours=48),
pv_max_age: timedelta = timedelta(hours=20),
load_max_age: timedelta = timedelta(minutes=90),
price_max_age: timedelta | None = PRICE_MAX_AGE,
smoothing: timedelta = timedelta(seconds=300),
observed: Noise = OBSERVED,
soc_tolerance: float = SOC_TOLERANCE,
load_jump_kw: float = LOAD_JUMP_KW,
)
What the plant is and how the price is computed, not where data comes from.
plant
instance-attribute
¶
How the plant behaves: a Plant built from its configuration.
slot
class-attribute
instance-attribute
¶
The slot a valuation prices, counted from the epoch in UTC: it divides an hour.
The noise floors (observed) are for 10-minute slots.
horizon
class-attribute
instance-attribute
¶
How far a valuation reaches from the slot under way.
pv_max_age
class-attribute
instance-attribute
¶
A PV forecast older than this cannot be used.
load_max_age
class-attribute
instance-attribute
¶
A load forecast issued, or starting, longer ago than this cannot be used
(series.is_stale).
price_max_age
class-attribute
instance-attribute
¶
A price forecast older than this cannot be used; None, no limit
(PRICE_MAX_AGE by default).
smoothing
class-attribute
instance-attribute
¶
The live value's moving-average time constant for measured PV and load (0: off).
observed
class-attribute
instance-attribute
¶
The meters' noise floors, for 10-minute slots: below them a metered flow or SOC
difference is no event. OBSERVED by default, measured on one plant's
meters; a plant whose meters report differently sets its own.
soc_tolerance
class-attribute
instance-attribute
¶
An SOC this far outside 0-1 is clamped to it; beyond, it cannot be used
(SOC_TOLERANCE).
load_jump_kw
class-attribute
instance-attribute
¶
A house-load change larger than this, kW, must be seen on two readings in a row
before the live value takes it (LOAD_JUMP_KW).
smoothing_window
property
¶
How far back from its moment Engine.live reads the measured series: a
client keeps its readings back this far, and the one in force then
(WINDOW_TAUS time constants).
from_mapping
classmethod
¶
The configuration an engine/2 mapping describes, for plant: every
field optional, today's value its default. Raises
EngineConfigError naming every problem.
to_mapping
¶
This configuration as engine/2 (the plant is its own, plant/2).
LiveReading
dataclass
¶
LiveReading(
at: datetime,
load_kw: float | None,
soc: float | None,
dispatch_as_modelled: bool | None,
external: tuple[str, ...] = (),
meters: Mapping[str, float | None] = dict(),
)
Measured now. The engine smooths the meters and the load (LiveSession).
soc
instance-attribute
¶
0-1; None where unknown, and always for a plant without a battery, as is
dispatch_as_modelled.
dispatch_as_modelled
instance-attribute
¶
The plant runs the dispatch its model describes (Inputs.dispatch_as_modelled
now). None or False: the work_mode cause.
external
class-attribute
instance-attribute
¶
The client's own causes (a silent telemetry path, a meter it cannot read): any
makes the value the fallback. Kept beside the engine's, in
LiveValue.external, never in place of them.
meters
class-attribute
instance-attribute
¶
The plant's meters, kW keyed by name (PlantModel.meters), each in
its configured sign: in place of engine/0's pv_kw, grid_kw and
battery_kw. A meter missing or None is unknown. Its PV meters give the
measured PV (missing: pv_power); without the grid (or consumption) and battery meters the
house is not known to be importing (not causes). A plant with DC-bus loads reads
their draw from its dc_load meter (missing: load_power), the one input
for it. A name the plant does not meter raises.
LiveSession
¶
Readings one at a time, kept as series for Engine.live: each reading is a
sample held until the next, a missing value a gap. A reading earlier than the last
(the wall clock stepped back) starts the series again.
update
¶
update(
reading: LiveReading,
valuation: Valuation | None,
valid_until: datetime | None,
) -> LiveValue
The value at reading.at (Engine.live), with this reading added to
the series.
LiveValue
dataclass
¶
LiveValue(
at: datetime,
kind: ValuationKind,
surplus_kw: float,
regime: Regime,
p_import: float | None,
p_export: float | None,
marginal_cost: float | None,
certainty_spill: float,
certainty_covered: float,
degraded: tuple[Cause, ...],
external: tuple[str, ...] = (),
pv_value: PvValue | None = None,
battery_value: BatteryValue | None = None,
)
The energy context now, before rounding.
kind is fallback_import where a cause, the engine's or the client's, made
it the import price with no surplus; unpriced without a price for the slot
under way (the prices None, no surplus); stored_value_v1 otherwise, the
warm-up after a start among them (degraded is smoothing).
SlotValue
dataclass
¶
SlotValue(
start: datetime,
p_import: float,
p_export: float,
surplus_kw: float,
surplus_kw_p10: float,
regime: Regime,
marginal_cost: float,
marginal_cost_p10: float,
certainty_spill: float,
certainty_covered: float,
pv_value: PvValue | None,
battery_value: BatteryValue | None,
supply: tuple[Step, ...],
unserved: float = 0.0,
)
One slot of a valuation, before rounding.
pv_value
instance-attribute
¶
Values by source (R-2, R-3); None where the source has none.
unserved
class-attribute
instance-attribute
¶
Scenario weight in which the slot has unserved load a stored kWh would serve
(0-1): at an import limit, or a DC-bus load beyond the hybrid's rating with the
battery on its bus at its reserve or limit. Its price is then the import as a
floor. Load beyond any stored kWh's
reach (SlotDispatch.unserved_unreachable) is not counted.
to_mapping
¶
Plain data (valuation/2's SlotValue): times in ISO 8601 with their
offset, enums as their values.
Valuation
dataclass
¶
Valuation(
model_version: str,
now: datetime,
kind: ValuationKind,
degraded: tuple[Cause, ...],
inputs: Mapping[Cause, InputState],
load_forecast: str | None,
slots: tuple[SlotValue, ...],
horizon_cut: bool = False,
model: Model | None = None,
)
A horizon valued from now: the price curve and surplus by slot.
unpriced (no price for the slot under way) has no slots, and names
prices in degraded.
model_version
instance-attribute
¶
The price rule and plant model that made it (MODEL_VERSION).
degraded
instance-attribute
¶
The inputs that could not be used: then kind is the fallback (or
unpriced, for the prices).
inputs
instance-attribute
¶
Per input: when it was known, and whether it could be used.
load_forecast
instance-attribute
¶
The load forecast's source label, None without one.
horizon_cut
class-attribute
instance-attribute
¶
A gap in the load forecast (a slot unknown with known ones after it) cut the horizon short of its end: the slots before the gap are valued. Not a cause: the load forecast was used.
model
class-attribute
instance-attribute
¶
The scenarios valued: what LiveSession runs again. Opaque, and not
part of the contract.
to_mapping
¶
Plain data (valuation/2, VALUATION_SCHEMA): times in ISO 8601
with their offset, enums as their values; model, opaque, left out.
Assessment
dataclass
¶
Cause
¶
Bases: StrEnum
Why the engine could not use an input, named in an output's degraded: each value
names an input.
PV_POWER
class-attribute
instance-attribute
¶
A live value's own, besides the valuation's.
SMOOTHING
class-attribute
instance-attribute
¶
Warming up after a start or a gap.
BATTERY_LOTS
class-attribute
instance-attribute
¶
The battery's lots were not given (Inputs.lots) on the stored-cost rule: all
it holds is of unknown origin, at the import price. Named; the values stand.
PV_SPREAD
class-attribute
instance-attribute
¶
The PV forecast gives no quantile below its median (a point forecast, say): the
pessimistic case (marginal_cost_p10, surplus_kw_p10; R-6) is the
median, as nothing is known of the downside. Named; the values stand.
InputState
dataclass
¶
ValuationKind
¶
Bases: StrEnum
How a valuation's costs were made (a valuation's kind).
UNPRICED
class-attribute
instance-attribute
¶
No price for the slot under way: nothing valued, no slots.
EngineConfigError
¶
Bases: ValueError
An engine/1 mapping the engine cannot take; errors names each problem
with its path.
Inputs
dataclass
¶
Inputs(
prices: Series[Forecast[Price]] | None,
pv: Series[Forecast[Quantiles]]
| Mapping[str, Series[Forecast[Quantiles]]]
| None = None,
load: Series[Forecast[float]] | None = None,
dc_load: Series[Forecast[float]] | None = None,
soc: Series[float] | None = None,
dispatch_as_modelled: Series[bool] | None = None,
meters: Mapping[str, Series[float]] = dict(),
measured_load: Series[float] | None = None,
lots: Series[Lots] | None = None,
)
Everything one valuation rests on, as series; the moment is given beside it.
Each forecast input is a series of forecasts: a sample is a forecast the client
received at the sample's time. The engine reads what was received by the moment,
whatever the source's own stamp says. Prices and PV come from the issue received
last, which replaces those before it; the loads, slot by slot, from the newest fresh
issue that covers the slot (microgrid.engine.series.compose), as a source
that publishes from the next half hour needs. A sample with no value (the source
answered nothing) leaves the forecast read before it in force until it is too old.
A measured input (the SOC, the dispatch) is a series of values: the engine reads the
value in force at the moment, if the client vouches for the series up to it
(known_until), and when that value took effect.
The live value (microgrid.engine.Engine.live) reads the measured series as
well: the meters and the house load, smoothed over time, as they stood at its moment.
An input the plant does not have is left None: PV for a plant without arrays; the
SOC and the dispatch for one without a battery; dc_load for one with no load on
a DC bus. Passing one is a mistake (ValueError).
prices
instance-attribute
¶
Import and export prices per kWh; None: no prices, so no valuation. Their age is
their issue's (EngineConfig.price_max_age): a client whose source restamps
the same rates at each fetch (TariffKit does) records each fetch as a sample, or
the rates age from the first.
pv
class-attribute
instance-attribute
¶
PV, kW in its source's terms, at quantiles: for the site, or per array by name (a plant with AC and DC arrays takes it per array).
load
class-attribute
instance-attribute
¶
The AC side's load, kW, as the plant meters it.
dc_load
class-attribute
instance-attribute
¶
The DC-bus loads' draw, DC kW at the loads, for a plant with any; load then
excludes them. Two keywords, as the plant model's projection takes them.
soc
class-attribute
instance-attribute
¶
The battery's state of charge, 0-1 (units.normalise(series, "fraction")).
dispatch_as_modelled
class-attribute
instance-attribute
¶
Whether the plant runs the dispatch its model describes (a work mode the model
covers); not so, or unknown, is the work_mode cause.
meters
class-attribute
instance-attribute
¶
The plant's meters, kW by name (PlantModel.meters), each in its configured
sign, as measured: what the live value smooths (Engine.live). A meter
missing is unknown; a name the plant does not meter raises. A valuation does not
read them.
measured_load
class-attribute
instance-attribute
¶
The house load as the plant meters it, kW, as measured: the live value's.
lots
class-attribute
instance-attribute
¶
The battery's lots: what it holds by source and what that cost
(Lots, a client's ledger). A battery kWh is priced
at the running average cost of all it holds (Lots.pool; Pricing.battery_value);
without them, all it holds is of unknown origin, at the import price, and the output
names battery_lots (R-7), the valuation standing.
Step
dataclass
¶
Power available at one price through a slot.
BatteryValue
dataclass
¶
A battery kWh drawn for a flexible load: per kWh, expected over the
scenarios in which the battery had it to give (weight, 0-1), and what buys
it back in most of them.
Displaces
¶
Bases: StrEnum
What a battery kWh drawn now is bought back with (R-3).
In declaration order: a tie in scenario weight goes to the first, the import.
Pricing
dataclass
¶
How the price is computed.
scenarios
class-attribute
instance-attribute
¶
Equal-probability PV scenarios cut from Solcast's p10/p50/p90.
battery_value
class-attribute
instance-attribute
¶
What a kWh the battery holds is worth, which prices a kWh it delivers:
stored_cost, what the energy it holds cost: its running
average, solar at the export it gave up and grid energy at what it was bought for,
at most the solar that refills the room where a fill comes first; or
replacement, what makes the kWh up later (the make-up rule).
quantiles
property
¶
The scenarios' PV percentiles: the midpoints of equal-probability bins.
PvRouting
¶
Bases: StrEnum
Where a PV kWh would have gone had no flexible load taken it (R-3).
In declaration order: a tie in scenario weight goes to the first.
PvValue
dataclass
¶
A PV kWh diverted to a flexible load: per kWh, expected over the scenarios
in which PV had spare (weight, 0-1), and where it went in most of them.
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.