flexmeasures.data.models.planning.scheduling_problem
Solver-agnostic preparation of the device scheduler’s inputs.
flexmeasures.data.models.planning.linear_optimization.device_scheduler() (Pyomo) and
flexmeasures.data.models.planning.highspy_optimization.device_scheduler_highspy() (direct HiGHS)
build the same mathematical model in two very different representations,
so the model construction itself is necessarily written twice.
Everything around it is not:
normalising arguments, resolving stock groups, converting legacy commitments, deriving Big-Ms,
and turning solver output back into schedules and costs is plain pandas/numpy work with no solver in it.
Keeping that work here means the two backends cannot drift apart on input handling —
only on the model, which is what the equivalence tests in tests/test_highspy_equivalence.py compare.
It also gives both backends a single place to grow support for a new scheduling feature’s inputs.
Functions
- flexmeasures.data.models.planning.scheduling_problem.aggregate_commodity_costs(commitments: list[DataFrame], subcommitment_costs: dict) dict
Sum sub-commitment costs per commodity, skipping commitments without one.
- flexmeasures.data.models.planning.scheduling_problem.aggregate_subcommitment_costs(subcommitment_costs: dict, commitment_mapping: dict) dict
Sum sub-commitment costs back onto the commitments they were split from.
- flexmeasures.data.models.planning.scheduling_problem.convert_commitments_to_subcommitments(dfs: list[DataFrame]) tuple[list[DataFrame], dict[int, int]]
Transform commitments, each specifying a group for each time step, to sub-commitments, one per group.
‘Groups’ are a commitment concept (grouping time slots of a commitment), making it possible that deviations/breaches can be accounted for properly within this group (e.g. highest breach per calendar month defines the penalty). Here, we define sub-commitments, by separating commitments by group and by direction of deviation (up, down).
We also enumerate the time steps in a new column “j”.
For example, given contracts A and B (represented by 2 DataFrames), each with 3 groups, we return (sub)commitments A1, A2, A3, B1, B2 and B3, where A,B,C is the enumerated contract and 1,2,3 is the enumerated group.
- flexmeasures.data.models.planning.scheduling_problem.loss_coefficients(efficiency: float) tuple[float, float]
Coefficients (a, b) of one step of the stock recursion, for how=”linear”.
stock[j] = a * stock[j-1] + b * change[j]
Mirrors
apply_stock_changes_and_losses(), which we cannot call here because it expects numbers, while change[j] may be a Pyomo expression.
- flexmeasures.data.models.planning.scheduling_problem.planned_power_per_device(power_per_device, start, end, resolution) list[Series]
Turn each device’s planned power values into a time series.
- flexmeasures.data.models.planning.scheduling_problem.prepare_scheduling_problem(device_constraints: list[DataFrame], ems_constraints: DataFrame | list[DataFrame], commitment_quantities: list[Series] | None = None, commitment_downwards_deviation_price: list[Series] | list[float] | None = None, commitment_upwards_deviation_price: list[Series] | list[float] | None = None, commitments: list[DataFrame] | list[Commitment] | None = None, initial_stock: float | list[float] = 0, stock_groups: dict[int, list[int]] | None = None, ems_constraint_groups: list[list[int]] | None = None, device_power_bands: list[list[tuple[float, float]] | None] | None = None, coupling_groups: dict[str, list[tuple[int, float]]] | None = None, balance_groups: dict[str, list[int]] | None = None) SchedulingProblem
Normalise and validate
device_scheduler’s arguments into a SchedulingProblem.Note
This adds a “stock delta” column to the passed
device_constraintsDataFrames in place, as the schedulers have always done.
- flexmeasures.data.models.planning.scheduling_problem.solver_options(solver_name: str) dict
The solver options to apply, for the given solver.
HiGHS (whether reached through Pyomo as
appsi_highsor directly ashighspy– both match on “highs”) gets a tight-tolerance profile, so the two backends cannot disagree on tolerances and silently produce different schedules. Operator-configured options are applied last, so they win.
- flexmeasures.data.models.planning.scheduling_problem.validate_highs_options(options: dict) None
Raise if HiGHS would refuse any of these options.
Pyomo’s appsi_highs interface applies solver options without checking HiGHS’ return status, so an unknown name, an invalid value, or a feature missing from the installed HiGHS build is otherwise ignored without a word. That silently turns a mis-typed option into a no-op, and a benchmark of it into a false negative. Probing a throwaway Highs instance surfaces the rejection instead.
Classes
- class flexmeasures.data.models.planning.scheduling_problem.SchedulingProblem(start: object, end: object, resolution: object, device_constraints: list[~pandas.core.frame.DataFrame], ems_constraints_list: list[~pandas.core.frame.DataFrame], ems_constraint_device_groups: list[list[int]], device_to_group: dict[int, str], group_to_devices: dict[str, list[int]], commitments: list[~pandas.core.frame.DataFrame], commitment_mapping: dict[int, int], device_group_lookup: dict[int, dict], convex_cost_curve: bool, Md: float, Mc: float, band_lookup: dict[int, list[tuple[float, float]]], coupling_device_specs: list[tuple[int, int, float]], balance_group_specs: list[list[int]], initial_stock: float | list[float], original_commitments: list[~pandas.core.frame.DataFrame] = <factory>)
Everything both scheduler backends need before building their model.
Produced by
prepare_scheduling_problem(); seedevice_scheduler’s docstring for what the underlying arguments mean.- __init__(start: object, end: object, resolution: object, device_constraints: list[~pandas.core.frame.DataFrame], ems_constraints_list: list[~pandas.core.frame.DataFrame], ems_constraint_device_groups: list[list[int]], device_to_group: dict[int, str], group_to_devices: dict[str, list[int]], commitments: list[~pandas.core.frame.DataFrame], commitment_mapping: dict[int, int], device_group_lookup: dict[int, dict], convex_cost_curve: bool, Md: float, Mc: float, band_lookup: dict[int, list[tuple[float, float]]], coupling_device_specs: list[tuple[int, int, float]], balance_group_specs: list[list[int]], initial_stock: float | list[float], original_commitments: list[~pandas.core.frame.DataFrame] = <factory>) None
- balance_group_specs: list[list[int]]
device lists of the balance groups (internal commodity nodes), empty groups dropped
- band_lookup: dict[int, list[tuple[float, float]]]
device index -> its signed power bands (S2 operation modes)
- commitments: list[DataFrame]
Sub-commitments (one per commitment group and deviation direction), and the mapping from each sub-commitment index back to its original commitment index
- property commodity_devices: dict
commodity -> set(device indices).
Computed on demand: only the EMS-level flow commitment constraints need it, and the per-row scan is not cheap enough to pay for unconditionally.
- convex_cost_curve: bool
Whether the summed deviation prices describe a convex cost curve (a non-convex curve needs binary commitment-sign variables).
- coupling_device_specs: list[tuple[int, int, float]]
(group index, device index, coefficient) triples for hard flow-coupling constraints
- device_constraints: list[DataFrame]
Device constraints, with a “stock delta” column guaranteed to be present
- device_group_lookup: dict[int, dict]
sub-commitment index -> {device group label -> member device indices}
- device_to_group: dict[int, str]
device -> its primary stock group key, and stock group key -> member devices
- ems_constraints_list: list[DataFrame]
EMS constraints, normalised to a list, plus the device indices each applies to
- initial_stock_of(d) float
The initial stock of device
d, defaulting to 0.Device indices reaching this from a commitment’s “device” column may be numpy floats, hence the cast.
- original_commitments: list[DataFrame]
The commitments as passed in, before the sub-commitment split. Only kept to derive
commodity_deviceslazily.