microgrid.engine.analysis.ground_truth¶
The plant model against the plant's own meters.
A plant model is only worth pricing with if what it projects is what the plant does.
project_against_meters feeds a plant model what actually happened (the metered
PV, the load the plant served, the metered SOC at the start), so no forecast error is
in the way, and compares what it projects with the meters:
- from each local midnight, for the day: the daily balances, and when the battery
tops up, fills and runs empty (
DayResult); - from each hour, over the price model's 48 h horizon: the SOC error by hours
ahead, and whether the model's first event (fill, top-up, empty) is the meters'
(
HourlyRun).
Primary measures are raw meters (SOC, grid import and export, the inverter's AC
meter), so a fitted conversion is never scored against itself. Two are not: no meter
separates the grid's share that charges the battery, so the metered top-up (when it
starts, how much) comes from the meters' split (split_paths), as each slot's routing does. Both
use the battery's fitted conversion, so both are computed with a baseline's conversion
pinned (pinned), and so are the inputs and the meters' full and empty
thresholds. The served load is consumption less the inverter's draw, both derived
through the pinned plant, so every candidate model gets the same inputs and a change
in conversion shows only in its prediction.
Every projection goes through project (Projector), so pointing it at
another plant model moves every measure.
What is a site's comes in from its caller, as opaque strings: each day's labels
(why the model does not describe it), and the DC-bus load's draw outside the house
meter. The function adds one label of its own, GAP, where the telemetry was
silent. It names no load, and groups nothing: the caller groups the days and runs by
their labels and sets the bars.
A plant model with a DC-bus load is fed the session as the plant met it: the house
load (the load served less the DC-bus load's draw) on the AC side, the DC-bus load's
meter on the DC bus, and, for a run that starts inside a session, whether its hold
rules held the battery from the session's metered start. The meters are read through
the pinned plant without its DC-bus loads (meters_plant), so a candidate's
change shows only in its prediction. Pure: no I/O.
REACH
module-attribute
¶
How far an hourly run reaches: the price model's default horizon.
HORIZON
module-attribute
¶
REACH in slots of the default length (series.SLOT); a recording on another
slot (Recording.step) reaches as far in its own.
Projector
¶
Bases: Protocol
Projects the plant from soc (0-1) over starts with the given PV and load,
and for a plant with a DC-bus load its DC kW and whether a session under way at the
first slot holds the battery: the ground truth's one call into a plant model (a
PlantModel's project).
__call__
¶
__call__(
starts: Sequence[datetime],
pv: Sequence[float],
load: Sequence[float],
soc: float,
/,
*,
dc_load: Sequence[float] | None = None,
held_at_start: bool | None = None,
) -> tuple[SlotFlow, ...]
Recording
dataclass
¶
Recording(
starts: tuple[datetime, ...],
flows: tuple[Flows | None, ...],
stored_after: tuple[float | None, ...],
stored_before: float | None,
p_import: tuple[float, ...],
p_export: tuple[float, ...],
pv_kw: tuple[float | None, ...],
house_kw: tuple[float | None, ...],
grid_import_kw: tuple[float | None, ...],
grid_export_kw: tuple[float | None, ...],
inverter_out_kw: tuple[float | None, ...] | None = None,
inverter_in_kw: tuple[float | None, ...] | None = None,
dc_load_kw: tuple[float | None, ...] | None = None,
step: timedelta = SLOT,
)
What the meters recorded over the window, per slot of step (UTC).
flows
instance-attribute
¶
Split between sources and sinks at sample resolution, through the pinned conversion; None where the telemetry path was silent (the caller's reader).
pv_kw
instance-attribute
¶
Raw meters, slot means, kW. The library does not read house_kw; a caller
derives its DC-bus draws (dc_draws) from it.
inverter_out_kw
class-attribute
instance-attribute
¶
The inverter's AC meter, out to the house and in from it; None unconfigured.
inverter_in_kw
class-attribute
instance-attribute
¶
dc_load_kw
class-attribute
instance-attribute
¶
The DC-bus load's meter, DC kW; None where the site has none.
step
class-attribute
instance-attribute
¶
The slot the recording is read on (EngineConfig.slot).
DayResult
dataclass
¶
DayResult(
day: date,
reasons: tuple[str, ...],
pairs: dict[str, tuple[float, float]] | None = None,
times: dict[str, tuple[float | None, float | None]]
| None = None,
soc_mae_pp: float | None = None,
soc_max_pp: float | None = None,
)
One local day: why it is not clean, and (when it ran) its measures.
pairs
class-attribute
instance-attribute
¶
Measure name -> (model, meters); None where the day did not run.
times
class-attribute
instance-attribute
¶
Event -> (model, meters) minutes elapsed since local midnight (so a difference
is a true one across a DST change; clock gives the wall clock); None: not
that day.
HourlyRun
dataclass
¶
HourlyRun(
start: datetime,
reasons: tuple[str, ...],
touched: frozenset[date],
full: bool,
soc_errors: tuple[tuple[str, float], ...],
event: tuple[Event, Event] | None,
)
A run of the plant model from one hour, over the horizon or to the first unknown input: what the caller groups by its labels.
Projection
dataclass
¶
clock
¶
A DayResult.times value as the local wall clock, HH:MM.
routing_class
¶
What a slot was doing, by the meters' noise floors (noise).
net_inverter_kw
¶
The model's AC power at the battery's converter, kW, > 0 out to the AC side, net
of its own draw: what its AC meter reads. On a hybrid's DC bus the hybrid is the
converter, and it meters all it converts (SlotDispatch.crossing_ac): the
battery's flows, DC PV crossing to the AC side and the AC it takes for DC-bus loads.
On its own inverter, what the battery delivers (an export too) less what it takes,
as for a model that does not give the hybrid's AC.
dc_bus_inputs
¶
dc_bus_inputs(
plant: Plant,
served: Sequence[float | None],
draws: Sequence[float],
dc_load_kw: Sequence[float | None],
soc_start: Sequence[float | None],
) -> tuple[
list[float | None],
list[float | None],
list[bool | None],
]
What a plant model with a DC-bus load is fed of a session as the plant met it,
per slot: the house (the load served less the DC-bus load's draw outside the house
meter, the caller's draws), the DC-bus load (its meter where it drew, else 0;
None where it drew and the meter is unknown), and whether its hold rules held the
battery from the session's metered start (session_holds). The ground truth and
the routing check share it.
meters_plant
¶
plant as the ground truth reads the meters: without its DC-bus loads, their
hold rules and the limits only they need (the battery's DC limits, the hybrid's AC
rating), so a candidate with one is scored against the same meters as one without.
plant itself where it has none.
session_holds
¶
session_holds(
plant: Plant,
dc_kw: Sequence[float],
soc_start: Sequence[float | None],
) -> list[bool | None]
Per slot inside a session of plant's hold rules, whether it holds the battery:
any rule whose session (the DC-bus load over its dc_load_above_kw) started below
its start_below_soc, by the metered SOC at the session's first slot (as the
general dispatch decides; _paths); None outside every session, and where the
SOC at a session's start is unknown (the engine then decides from the run's start).
The engine takes one answer for every rule (held_at_start), so overlapping
rules can disagree with it; one rule is the common case. A session is the DC-bus load
as fed, which is gated on its draw outside the house meter (dc_draws): a
dip under its limit ends a session, and the next starts at that slot's SOC.
project_against_meters
¶
project_against_meters(
recording: Recording,
*,
pinned: Plant,
plant: Plant,
project: Projector,
tz: ZoneInfo,
labels: Mapping[date, tuple[str, ...]],
dc_draws: Sequence[float] | None = None,
noise: Noise = OBSERVED,
) -> Projection
Run plant (through project) against recording; pinned derives the
inputs and the metered routing (module docstring). labels: each local day's
labels, the caller's; GAP is prepended to a day the telemetry was silent
in. dc_draws: the DC-bus load's draw outside the house meter, per slot (0 where
none), for a plant model with a DC-bus load. noise: the meters' floors the
routing, the events and full and empty are read by (EngineConfig.observed).