microgrid.engine.analysis.scoring¶
The checks that run the plant and price models against what the meters did.
What a kWh realised. The price, regime and certainties are claims about what
a kWh of flexible load would cost. Over metered flows the rule says what one did
(realise, observed_flows, observed_regime), and an hour of them
can be routed through the plant model and priced (route_hour,
price_hour). The scores over those (the forecast's errors by hours ahead,
BUCKETS) are a client's to keep.
Pure: no I/O.
BUCKETS
module-attribute
¶
BUCKETS: Final[tuple[tuple[timedelta, timedelta], ...]] = (
(timedelta(0), timedelta(hours=1)),
(timedelta(hours=1), timedelta(hours=3)),
(timedelta(hours=3), timedelta(hours=12)),
(timedelta(hours=12), timedelta(hours=48)),
)
Horizon buckets, [from, to) ahead of the issue.
RoutedHour
dataclass
¶
RoutedHour(
regime: Regime,
resolved: bool,
price: float,
event: Event,
event_at: datetime | None,
spilled: bool,
)
The plant model run from one metered slot over what happened, read by the rule.
Realised
dataclass
¶
Realised(
price: Mapping[datetime, float],
event: Mapping[datetime, tuple[Event, datetime]],
outcome: Mapping[datetime, tuple[Event, datetime]],
regime: Mapping[datetime, Regime],
spilled: Mapping[datetime, bool],
covered: Mapping[datetime, bool],
cover_outcome: Mapping[datetime, datetime],
)
What a kWh used in each slot turned out to cost, by slot start (None: unknown).
event
instance-attribute
¶
What priced it (microgrid.engine.extend.Event), and the start of the slot it happened in.
outcome
instance-attribute
¶
The outcome it rests on: the event's kind and the first slot of its run
(FlowPrices.outcome_at). Slots that share one are nearly one trial.
cover_outcome
instance-attribute
¶
The expensive run each covered is about, by its first slot.
storage_of
¶
plant's battery, as the rule asks about it. The checks here score a battery's
regimes, so they take a plant with one; the engine also values plants without.
soc_end
¶
flow's SOC at its end: a plant with a battery projects one, and metered flows
carry the meters' (observed_flows).
bucket_label
¶
A BUCKETS entry's name, e.g. "3-12h".
hours_ahead_bucket
¶
The BUCKETS name ahead falls in; None beyond the last.
observed_flows
¶
observed_flows(
starts: Sequence[datetime],
flows: Sequence[Flows | None],
stored_after: Sequence[float | None],
stored_before: float | None,
*,
plant: Plant,
) -> list[SlotFlow | None]
The metered flows (microgrid.engine.ledger.integrate) as plant's slot
flows (Plant.metered_slot), which its battery's answers read.
SOC at a slot's start is the metered stored energy at the previous slot's
end; None where the flows or either SOC are unknown. A slot flow has no
battery export: where the battery exported, its SOC still falls by it, and
battery_exports names those slots for realise to leave out.
Every load the plant metered stays in, the DC-bus load's included: it is a load like any other.
battery_exports
¶
battery_exports(
starts: Sequence[datetime],
flows: Sequence[Flows | None],
noise: Noise = OBSERVED,
) -> frozenset[datetime]
The slots in which the battery exported: the rule cannot say what a kWh then displaced, since the model has no battery export.
Not where the PV left over after the site's consumption could have been the
whole export: the split (split_paths) gives export to a discharging battery
first, and at full, sensor timing can show a discharge while PV exports (1 of the
16 battery exports at full in one site's 30 days to 2026-09-25).
inverter_kw
¶
The inverter's own draw in a metered slot, kW (Plant.own_draw_kw).
Metered consumption holds it; the house meter and the plant model's load do not.
served_load
¶
Per slot, the load the plant served as its model takes it, kW; None unmetered.
The site's consumption less the inverter's own draw (inverter_kw), which
the plant model adds itself. The consumption is derived from the plant's PV, grid
and battery meters, so it includes the DC-bus load: a load like any other. The
routing check and the plant's ground truth both feed the model this.
route_hour
¶
route_hour(
plant: PlantModel,
window: Sequence[SlotFlow],
load_kw: Sequence[float],
p_import: Sequence[float],
p_export: Sequence[float],
*,
day: int,
dc_load_kw: Sequence[float] | None = None,
held_at_start: bool | None = None,
noise: Noise = OBSERVED,
step: timedelta = SLOT,
) -> RoutedHour
The plant model over window's metered PV and load_kw, from its first
slot's metered SOC, priced by the rule with the meters' noise floors (noise: the
engine's EngineConfig.observed, OBSERVED by default): what the model says
would have priced a kWh in the first slot. A plant model with a DC-bus load takes
its DC kW and whether a session under way holds the battery. step: the slots'
length (EngineConfig.slot).
price_hour
¶
price_hour(
storage: Storage,
modelled: Sequence[SlotFlow],
p_import: Sequence[float],
p_export: Sequence[float],
*,
day: int,
noise: Noise = OBSERVED,
) -> RoutedHour
A projection from a metered slot, priced by the rule with the meters' noise
floors (noise): what the model says would have priced a kWh in its first slot.
The caller projects, so any plant model can be priced.
observed_regime
¶
What +1 kWh displaced in a metered slot, as regimes says of the model's.
PV spilling is export: curtailment is not metered. A battery
above its reserve, not topping up and with the house not importing, would
have served the kWh (battery), idle or not: it sits idle where PV just
covers the house. At the reserve it is import.
realise
¶
realise(
storage: Storage,
flows: Sequence[SlotFlow | None],
p_import: Sequence[float],
p_export: Sequence[float],
*,
step: timedelta,
unscored: Collection[datetime] = frozenset(),
noise: Noise = OBSERVED,
stored_cost: Sequence[float | None] | None = None,
) -> Realised
The rule over what was metered: each unbroken run of known slots on its own.
stored_cost: per slot, the blended cost of what the battery held at its start
(the ledger over the meters; Pricing.battery_value = "stored_cost"), which a
battery kWh is priced at; without it, the make-up's.
A slot is priced only when its event lies inside its run (resolved): a
run ends at the data's end or at a slot the meters did not see, and what
came after that is not known. Slots in unscored (battery_exports) are
followed through but get no values of their own.
So the realised set leans away from slots whose event lay across a gap in the meters, and from the hours the battery exported in.