Skip to content

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: Engine and 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's to_mapping (plain data, its schema valuation/2 shipped in schemas/) and the versions.
  • microgrid.engine.series: the form a client's data takes (Series, values held until the next sample and known up to known_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, PlantConfig and its parts, Battery and GridCharge among them), behind the port, and examples.
  • 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

ENGINE_CONTRACT: Final = 'engine-api/5'

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

MODEL_VERSION: Final = 'pricing/1.3'

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

VALUATION_SCHEMA: Final = 'valuation/2'

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_SCHEMA module-attribute

ENGINE_SCHEMA: Final = 'engine/2'

Engine

Engine(config: EngineConfig)

The model behind its contract.

config instance-attribute

config = config

value

value(inputs: Inputs, /, *, at: datetime) -> Valuation

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

live_session() -> LiveSession

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

plant: PlantModel

How the plant behaves: a Plant built from its configuration.

pricing class-attribute instance-attribute

pricing: Pricing = field(default_factory=Pricing)

slot class-attribute instance-attribute

slot: timedelta = SLOT

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

horizon: timedelta = timedelta(hours=48)

How far a valuation reaches from the slot under way.

pv_max_age class-attribute instance-attribute

pv_max_age: timedelta = timedelta(hours=20)

A PV forecast older than this cannot be used.

load_max_age class-attribute instance-attribute

load_max_age: timedelta = timedelta(minutes=90)

A load forecast issued, or starting, longer ago than this cannot be used (series.is_stale).

price_max_age class-attribute instance-attribute

price_max_age: timedelta | None = PRICE_MAX_AGE

A price forecast older than this cannot be used; None, no limit (PRICE_MAX_AGE by default).

smoothing class-attribute instance-attribute

smoothing: timedelta = timedelta(seconds=300)

The live value's moving-average time constant for measured PV and load (0: off).

observed class-attribute instance-attribute

observed: Noise = OBSERVED

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

soc_tolerance: float = SOC_TOLERANCE

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

load_jump_kw: float = LOAD_JUMP_KW

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

smoothing_window: timedelta

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).

__post_init__

__post_init__() -> None

from_mapping classmethod

from_mapping(
    data: object, *, plant: PlantModel
) -> EngineConfig

The configuration an engine/2 mapping describes, for plant: every field optional, today's value its default. Raises EngineConfigError naming every problem.

to_mapping

to_mapping() -> dict[str, object]

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).

at instance-attribute

at: datetime

load_kw instance-attribute

load_kw: float | None

House load, as the plant meters it.

soc instance-attribute

soc: float | None

0-1; None where unknown, and always for a plant without a battery, as is dispatch_as_modelled.

dispatch_as_modelled instance-attribute

dispatch_as_modelled: bool | None

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

external: tuple[str, ...] = ()

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

meters: Mapping[str, float | None] = field(
    default_factory=dict
)

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

LiveSession(config: EngineConfig)

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).

at instance-attribute

at: datetime

kind instance-attribute

kind: ValuationKind

surplus_kw instance-attribute

surplus_kw: float

regime instance-attribute

regime: Regime

p_import instance-attribute

p_import: float | None

None: unpriced.

p_export instance-attribute

p_export: float | None

marginal_cost instance-attribute

marginal_cost: float | None

certainty_spill instance-attribute

certainty_spill: float

certainty_covered instance-attribute

certainty_covered: float

degraded instance-attribute

degraded: tuple[Cause, ...]

The engine's causes.

external class-attribute instance-attribute

external: tuple[str, ...] = ()

The client's own (LiveReading.external), beside the engine's.

pv_value class-attribute instance-attribute

pv_value: PvValue | None = None

battery_value class-attribute instance-attribute

battery_value: BatteryValue | None = None

to_mapping

to_mapping() -> dict[str, object]

Plain data (valuation/2's LiveValue): the time in ISO 8601 with its offset, enums as their values.

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.

start instance-attribute

start: datetime

p_import instance-attribute

p_import: float

p_export instance-attribute

p_export: float

surplus_kw instance-attribute

surplus_kw: float

surplus_kw_p10 instance-attribute

surplus_kw_p10: float

regime instance-attribute

regime: Regime

marginal_cost instance-attribute

marginal_cost: float

marginal_cost_p10 instance-attribute

marginal_cost_p10: float

certainty_spill instance-attribute

certainty_spill: float

certainty_covered instance-attribute

certainty_covered: float

pv_value instance-attribute

pv_value: PvValue | None

Values by source (R-2, R-3); None where the source has none.

battery_value instance-attribute

battery_value: BatteryValue | None

supply instance-attribute

supply: tuple[Step, ...]

The supply stack in the median PV case.

unserved class-attribute instance-attribute

unserved: float = 0.0

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

to_mapping() -> dict[str, object]

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

model_version: str

The price rule and plant model that made it (MODEL_VERSION).

now instance-attribute

now: datetime

kind instance-attribute

kind: ValuationKind

degraded instance-attribute

degraded: tuple[Cause, ...]

The inputs that could not be used: then kind is the fallback (or unpriced, for the prices).

inputs instance-attribute

inputs: Mapping[Cause, InputState]

Per input: when it was known, and whether it could be used.

load_forecast instance-attribute

load_forecast: str | None

The load forecast's source label, None without one.

slots instance-attribute

slots: tuple[SlotValue, ...]

horizon_cut class-attribute instance-attribute

horizon_cut: bool = False

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

model: Model | None = field(
    default=None, repr=False, compare=False
)

The scenarios valued: what LiveSession runs again. Opaque, and not part of the contract.

starts property

starts: tuple[datetime, ...]

Each slot's start.

assessment property

assessment: Assessment

The kind and its causes.

to_mapping

to_mapping() -> dict[str, object]

Plain data (valuation/2, VALUATION_SCHEMA): times in ISO 8601 with their offset, enums as their values; model, opaque, left out.

Assessment dataclass

Assessment(
    valuation: ValuationKind, degraded: tuple[str, ...]
)

How an issue would be valued, and which inputs were not usable.

valuation instance-attribute

valuation: ValuationKind

degraded instance-attribute

degraded: tuple[str, ...]

The engine's causes (Cause), or a client's own names beside them.

Cause

Bases: StrEnum

Why the engine could not use an input, named in an output's degraded: each value names an input.

PRICES class-attribute instance-attribute

PRICES = 'prices'

The valuation's inputs (its inputs).

PV_FORECAST class-attribute instance-attribute

PV_FORECAST = 'pv_forecast'

BATTERY_SOC class-attribute instance-attribute

BATTERY_SOC = 'battery_soc'

WORK_MODE class-attribute instance-attribute

WORK_MODE = 'work_mode'

LOAD_FORECAST class-attribute instance-attribute

LOAD_FORECAST = 'load_forecast'

PLANT_MODEL class-attribute instance-attribute

PLANT_MODEL = 'plant_model'

PV_POWER class-attribute instance-attribute

PV_POWER = 'pv_power'

A live value's own, besides the valuation's.

LOAD_POWER class-attribute instance-attribute

LOAD_POWER = 'load_power'

SMOOTHING class-attribute instance-attribute

SMOOTHING = 'smoothing'

Warming up after a start or a gap.

ISSUE class-attribute instance-attribute

ISSUE = 'issue'

The valuation is past its valid_until.

BATTERY_LOTS class-attribute instance-attribute

BATTERY_LOTS = 'battery_lots'

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

PV_SPREAD = 'pv_spread'

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

InputState(as_of: datetime | None, usable: bool)

One input's freshness, as published in the issue's inputs.

as_of instance-attribute

as_of: datetime | None

usable instance-attribute

usable: bool

ValuationKind

Bases: StrEnum

How a valuation's costs were made (a valuation's kind).

STORED_VALUE_V1 class-attribute instance-attribute

STORED_VALUE_V1 = 'stored_value_v1'

FALLBACK_IMPORT class-attribute instance-attribute

FALLBACK_IMPORT = 'fallback_import'

UNPRICED class-attribute instance-attribute

UNPRICED = 'unpriced'

No price for the slot under way: nothing valued, no slots.

EngineConfigError

EngineConfigError(errors: Sequence[str])

Bases: ValueError

An engine/1 mapping the engine cannot take; errors names each problem with its path.

errors instance-attribute

errors = tuple(errors)

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

prices: Series[Forecast[Price]] | None

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: (
    Series[Forecast[Quantiles]]
    | Mapping[str, Series[Forecast[Quantiles]]]
    | None
) = None

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

load: Series[Forecast[float]] | None = None

The AC side's load, kW, as the plant meters it.

dc_load class-attribute instance-attribute

dc_load: Series[Forecast[float]] | None = None

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

soc: Series[float] | None = None

The battery's state of charge, 0-1 (units.normalise(series, "fraction")).

dispatch_as_modelled class-attribute instance-attribute

dispatch_as_modelled: Series[bool] | None = None

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

meters: Mapping[str, Series[float]] = field(
    default_factory=dict
)

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

measured_load: Series[float] | None = None

The house load as the plant meters it, kW, as measured: the live value's.

lots class-attribute instance-attribute

lots: Series[Lots] | None = None

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

Step(source: Served, kw: float | None, per_kwh: float)

Power available at one price through a slot.

source instance-attribute

source: Served

kw instance-attribute

kw: float | None

kW; None for the grid, which has no limit here.

per_kwh instance-attribute

per_kwh: float

to_mapping

to_mapping() -> dict[str, object]

Plain data (valuation/2's Step).

BatteryValue dataclass

BatteryValue(
    per_kwh: float, displaces: Displaces, weight: float
)

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.

per_kwh instance-attribute

per_kwh: float

displaces instance-attribute

displaces: Displaces

weight instance-attribute

weight: float

to_mapping

to_mapping() -> dict[str, object]

Plain data (valuation/2's BatteryValue).

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.

LATER_IMPORT class-attribute instance-attribute

LATER_IMPORT = 'later_import'

A top-up, an import once it runs empty, or the horizon's import: a kWh more bought.

EXPORT class-attribute instance-attribute

EXPORT = 'export'

The battery fills and spills later: a kWh less exported then.

Pricing dataclass

Pricing(
    scenarios: int = 10,
    battery_value: BatteryValueRule = "stored_cost",
)

How the price is computed.

scenarios class-attribute instance-attribute

scenarios: int = 10

Equal-probability PV scenarios cut from Solcast's p10/p50/p90.

battery_value class-attribute instance-attribute

battery_value: BatteryValueRule = 'stored_cost'

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

quantiles: tuple[float, ...]

The scenarios' PV percentiles: the midpoints of equal-probability bins.

__post_init__

__post_init__() -> None

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.

EXPORT class-attribute instance-attribute

EXPORT = 'export'

CURTAIL class-attribute instance-attribute

CURTAIL = 'curtail'

BATTERY class-attribute instance-attribute

BATTERY = 'battery'

PvValue dataclass

PvValue(per_kwh: float, routing: PvRouting, weight: float)

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.

per_kwh instance-attribute

per_kwh: float

routing instance-attribute

routing: PvRouting

weight instance-attribute

weight: float

to_mapping

to_mapping() -> dict[str, object]

Plain data (valuation/2's PvValue).

Served

Bases: StrEnum

What a kWh used in a slot takes first.

SPILL class-attribute instance-attribute

SPILL = 'spill'

PV spilling now: exported or curtailed.

PV class-attribute instance-attribute

PV = 'pv'

PV charging the battery now: a kWh diverted leaves it charged a kWh less.

BATTERY class-attribute instance-attribute

BATTERY = 'battery'

The battery, discharging or able to.

GRID class-attribute instance-attribute

GRID = 'grid'

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.

Regime

Bases: StrEnum

What +1 kWh of flexible load in a slot would displace.

EXPORT class-attribute instance-attribute

EXPORT = 'export'

PV that would be exported.

CURTAIL class-attribute instance-attribute

CURTAIL = 'curtail'

PV that would be curtailed.

BATTERY class-attribute instance-attribute

BATTERY = 'battery'

Energy the battery would otherwise hold or deliver.

IMPORT class-attribute instance-attribute

IMPORT = 'import'

Grid import.