Configuration reference¶
SmartCer has two configuration levels:
the scenario: a folder with
config.jsonand one CSV file per node, the physical and economic description of the community (sections 1-5 of this page);the run: the arguments of the environment (period, episodes, observation, reward, …), section 6.
Symbols and equations are on the formulation page. Typical use:
import gymnasium as gym
import smartcer # registers SmartCer-v1
env = gym.make('SmartCer-v1', scenario='path/to/scenario', start='2022-01-01 00:00',
end='2022-12-31 23:00', episode_length=24 * 7, random_start=True)
obs, info = env.reset(seed=0)
obs, reward, terminated, truncated, info = env.step(env.action_space.sample())
Without the environment, smartcer.data.load_scenario(path, start, end) returns the
Community (nodes, incentive model, timestamps) and community.step(t, actions) simulates one
step.
1. Scenario format 1.0¶
{
"format_version": "1.0",
"name": "my_cer",
"description": "optional free text",
"delta_t": 1.0,
"units": "kWh",
"incentive": {"model": "uniform", "unit_value": 0.11},
"nodes": {
"pv_1": {
"type": "producer",
"category": "plant",
"csv": "data/pv_1.csv",
"photovoltaic": {"tip_base": 0.08, "tip_threshold": 0.12, "fi_zone": 0.004, "pnrr": 0.0},
"storage": {
"capacity": 100, "power_charge_max": 30, "power_discharge_max": 30,
"efficiency_charge": 0.95, "efficiency_discharge": 0.95, "soc_min": 0.1,
"usage_cost": 0.01, "charge_limit": "production"
}
},
"aggregated_load": {"type": "consumer", "csv": "data/consumers.csv"}
}
}
Top-level keys¶
Key |
Type |
Required |
Default |
Allowed values |
Meaning |
|---|---|---|---|---|---|
|
string |
yes |
– |
|
without it the file is read as 0.6.2 (section 4) |
|
string |
no |
folder name |
any |
scenario name |
|
string |
no |
– |
any |
free text, not used |
|
number |
no |
|
> 0 |
step Δt in hours; every CSV must have exactly this spacing |
|
string |
no |
|
|
|
|
object |
yes for |
|
section 1.1 |
incentive model |
|
object |
yes |
– |
non-empty |
its order is the node order, and the order of the actions is that of the nodes with a battery |
Unknown keys raise ValueError.
1.1 Incentive¶
|
Other keys |
Incentive I_t (formulation, section 9) |
|---|---|---|
|
|
π^inc_t · E^sh_t |
|
none |
E^sh_t / Σ_u e^gri_u · Σ_u c_u,t e^gri_u, with the per-plant coefficient c of each node’s |
A unit_value CSV has a datetime column and the named column; its timestamps in the period must
be those of the nodes. With tip, every producer and prosumer needs the four TIP keys.
1.2 Node keys¶
Key |
Type |
Required |
Allowed values |
Meaning |
|---|---|---|---|---|
|
string |
yes |
|
|
|
string |
no |
path relative to the scenario folder |
default |
|
string |
no |
any |
free text (e.g. |
|
object |
with |
section 1.2.1 |
TIP parameters of the plant |
|
object |
no |
section 1.3 |
battery; without it the node has no action |
Unknown node keys raise ValueError.
1.2.1 Photovoltaic keys¶
Key |
Type |
Required |
Allowed values |
Meaning |
|---|---|---|---|---|
|
number |
with |
any number |
base value of the TIP, €/kWh |
|
number |
with |
any number |
cap of the TIP before the zone correction, €/kWh |
|
number |
with |
any number |
geographic correction, €/kWh |
|
number |
with |
[0, 40] |
capital grant, % |
|
any |
no |
any |
accepted and not used |
1.3 Storage keys¶
Key |
Type |
Required |
Default |
Allowed values |
Unit |
Meaning |
|---|---|---|---|---|---|---|
|
number |
yes |
– |
> 0 |
kWh |
S^max |
|
number |
yes |
– |
> 0 |
kW |
P^cha, node side |
|
number |
yes |
– |
> 0 |
kW |
P^dis, node side |
|
number |
yes |
– |
(0, 1] |
– |
η^cha |
|
number |
yes |
– |
(0, 1] |
– |
η^dis |
|
number |
no |
0 |
[0, 1) |
fraction of capacity |
S^min |
|
number |
no |
|
[ |
fraction of capacity |
S^ini |
|
number |
no |
0 |
[0, 1) |
per hour |
self-discharge; over a step the battery keeps (1 - α)^Δt of its charge |
|
number |
no |
0 |
≥ 0 |
€/kWh of cell throughput |
π^sto |
|
string |
no |
|
|
– |
charge cap c: |
The 0.6.2 keys are also accepted, with a DeprecationWarning: rated_power → both power limits,
efficiency → both efficiencies, alfa → alpha, beta ignored (no degradation). Combining a
0.6.2 key with the 1.0 key it maps to, unknown keys and missing required keys raise ValueError.
2. CSV files¶
One file per node (several nodes may share one through csv or the _N rule; it is read once).
Column |
Producer |
Consumer |
Prosumer |
Unit |
|---|---|---|---|---|
|
required |
required |
required |
ISO 8601, e.g. |
|
required |
ignored |
required |
kWh per step, or kW with |
|
ignored |
required |
required |
kWh per step, or kW with |
|
required |
optional |
required |
€/kWh |
|
ignored |
required |
required |
€/kWh |
Other columns are ignored. A file with both an alias and its 1.0 name raises ValueError. A
consumer’s sell_price is read when the column exists (and then validated like a required column):
it pays the energy the consumer’s battery exports (discharge beyond the load); without the column
that energy earns nothing. A producer’s buy price is 0: it never imports under the charge rule.
3. Validation¶
Errors (ValueError) name the node, the file, the column and the first offending row as a CSV line
number (header = line 1) with its timestamp:
missing
datetimeor required columns; unparsable dates;missing or non-numeric values in a required column, negative
productionorconsumption(checked in the period);unsorted or duplicated timestamps, or a spacing different from
delta_t(checked on the whole file);nodes whose timestamps in the period differ (format 1.0), or whose numbers of rows in the period differ (both formats);
an empty period, invalid keys or values in
config.json.
4. Scenario format 0.6.2¶
A config.json without format_version and with the groups Producers, Consumers,
Prosumers is read with a DeprecationWarning, which also lists the keys that are not used:
0.6.2 |
1.0 |
|---|---|
group |
|
|
|
|
|
|
|
CSV in |
default |
(implicit) |
|
Timestamps. As in 0.6.2, the rows of the nodes are aligned by position and the community uses
the timestamps of the first node. Every file must have the same number of rows (ValueError
otherwise); the period start–end is selected on the first node’s timestamps and every node is
cut at the same rows. Unsorted, duplicated or wrongly spaced timestamps, and nodes whose
timestamps differ from the first node’s, are reported in one TimestampWarning
(smartcer.data.TimestampWarning) instead of raising. This is meant for files whose timestamps
are shifted by one hour in part of the year, where a period may start or end inside the shifted
block. Converting a scenario to format 1.0 requires regular
timestamps.
5. Units inside SmartCer¶
Energies are kWh per step, powers kW, prices €/kWh, Δt hours (formulation, section 2). With
units: kW the loader multiplies production and consumption by Δt.
6. Run configuration¶
The run is configured by the arguments of SmartCerEnv (smartcer.gym), also accepted by
gymnasium.make('SmartCer-v1', ...):
Argument |
Type |
Default |
Meaning |
|---|---|---|---|
|
str, Path or |
– |
scenario folder (sections 1-4) or a |
|
timestamp-like or |
first / last row |
period, both inclusive (with a folder only) |
|
int or |
whole period |
steps per episode |
|
bool |
|
first step of each episode drawn with |
|
bool |
|
information timing, formulation section 12; with |
|
|
|
observation preset: |
|
int ≥ 0 |
|
past steps of generation and load per battery node ( |
|
path, |
|
fitted normaliser of generation and load: a |
|
tuple or |
whole period |
|
|
|
|
reward type: |
|
bool |
|
per-node variant of |
|
float ≥ 0 |
|
σ |
|
str, provider or |
|
forecasts of the observation’s |
|
int ≥ 0 |
|
forecast horizon H in steps, ≥ 1 with |
|
path, |
|
MILP oracle: an oracle folder (below) or a |
|
int or |
|
seed used by the first |
Actions are clipped to [-1, 1] without penalty before the nodes clip them to their dynamic bounds
(penalised, formulation section 7); non-finite actions raise ValueError. Episodes end on their last
step t with truncated=True. The terminal observation is that of step t + 1 with the final battery
states (the same as the first observation of an episode starting at t + 1 with those states), so
that a learner can bootstrap the value of the next state. Only without forecasts, on the last step
of the data, it is the observation of step t with the final battery states; with forecasts the
episodes stop early enough for step t + 1 to be observable (forecast_horizon).
Observation¶
The observation is a flat Box; env.obs_layout maps each feature name to its slice. Sections,
in order:
Section |
Features |
Presets |
|---|---|---|
time |
|
both |
time |
|
|
price |
|
both |
community |
|
|
nodes |
per battery node, in action order: [ |
both (lookback: |
forecast |
with |
both |
Generation and load (also the community totals, the lookback and the forecasts, which use the
bounds of the current quantity: <node>/generation, community/load, …) are min-max scaled
with the bounds of the training window normalizer_window and clipped to [0, 1], so that values
beyond the training range (e.g. in a test period) stay inside the space; info['obs_clipped']
counts them. The forecast provider is reset with env.np_random at every reset(), so that a
stochastic provider is reproducible from the seed.
With observe_current_step=False
they are those of step t - 1; values before the first row of the data are 0. Save the normaliser of
a training environment and pass it to the evaluation one:
train = SmartCerEnv('path/to/scenario', end='2022-09-30 23:00')
train.normalizer.save('normalizer.json') # bounds, window and scenario name
test = SmartCerEnv('path/to/scenario', start='2022-10-01 00:00', normalizer='normalizer.json')
Forecasts¶
A provider returns, for the H steps after the last observed one, the generation and load of every
battery node ('<node>/generation', '<node>/load') and the community totals
('community/generation', 'community/load'), in kWh per step. PerfectForecast returns the true
values. MMFEForecast(community, parameters) simulates forecast errors with the martingale model
of forecast evolution (MMFE):
for every series and target step τ, H innovations ε_{τ,1}, …, ε_{τ,H} are drawn once per episode; the forecast of τ issued h steps before τ is observed (lead time h, the k-th value of the section has h = k + 1) carries the error e_τ(h) = ε_{τ,1} + … + ε_{τ,h}. The error variance is therefore linear in the lead time, and successive issues for the same target are consistent: each step reveals one innovation, and the true value is observed at lead time 0;
lead-time convention: lead time 1 is the first forecast step, the step right after the last observed one (t + 1 with
observe_current_step=True, t withFalse). Its forecast carries one innovation (error variance σ²); only the observation itself, at lead time 0, is exact. An exact lead-1 forecast would reveal g_t withobserve_current_step=False, which the causal timing hides (formulation section 12);each innovation is ε = b + σ (√ρ z + √(1 − ρ) ξ), with ξ standard normal, independent per series, and z a standard normal factor common to all series of the same quantity (generation or load), AR(1) over the target steps: z_τ = φ z_{τ−1} + √(1 − φ²) η_τ. Two nodes have error correlation ρ; adjacent target steps have error correlation ρ φ;
limitation: the generation and load factors are independent, so a common cause of both errors (e.g. the temperature, which moves PV output and air-conditioning load together) is not modelled;
generation (photovoltaic): ŷ = min(max(y + r_τ e, 0), y^max), with r_τ the reference profile of the series (its mean over the steps of the same calendar month at the same time of day) and y^max the largest value of the series in the period. Where r_τ = 0 (night) the forecast is 0;
load: ŷ = max(y (1 + e), 0), a multiplicative error;
the noise is drawn from a generator spawned from
env.np_randomat everyreset(): the same seed gives the same forecasts, every new episode new errors. Innovations are drawn lazily, as the episode moves forward.
Parameter ( |
Meaning |
|---|---|
|
σ of a generation innovation, fraction of the reference profile r_τ |
|
σ of a load innovation, fraction of the load |
|
share ρ of the innovation variance due to the common factor, in [0, 1] |
|
AR(1) coefficient φ of the common factor over the target steps, in (−1, 1) |
|
mean b of an innovation, in the units of σ’s fraction (default 0); the mean error at lead time h is h b |
|
forecast key → σ of that series, replacing the σ of its quantity (default none), e.g. |
An aggregated load, such as community/load summing many consumers (for example about 100
members), should have a smaller relative σ than a single member: the independent parts of the
members’ errors partly cancel (for n members with equal, independent errors the relative σ of the
sum is σ / √n). Set it with sigma_overrides.
Presets (MMFE_PRESETS, environment names 'mmfe_<preset>'). The values are provisional,
pending calibration on forecast data; with H = 12 they give an error standard deviation at lead
time 12 of σ √12:
Preset |
|
|
|
|
Error std at lead 12 (generation / load) |
|---|---|---|---|---|---|
|
0 |
0 |
0 |
0 |
0 / 0 (true values) |
|
0.07 |
0.03 |
0.5 |
0.8 |
24 % of r_τ / 10 % of the load |
|
0.15 |
0.08 |
0.7 |
0.9 |
52 % of r_τ / 28 % of the load |
Custom parameters:
from smartcer.data import load_scenario
from smartcer.forecasts import MMFEForecast, MMFEParameters
community = load_scenario('path/to/scenario')
forecast = MMFEForecast(community, MMFEParameters(sigma_generation=0.1, sigma_load=0.05,
rho=0.6, phi=0.85,
sigma_overrides={'community/load': 0.02}))
env = SmartCerEnv(community, forecast=forecast, forecast_horizon=12)
env.run_config() records the provider name and, for MMFE, its preset and parameters
(forecast_config).
Info¶
info of reset(): t, timestamp, privileged, episode_start, episode_end,
obs_clipped. info of step(): t, timestamp, privileged, obs_clipped, reward (type,
main, penalty = weighted penalty,
total = R_t and, with reward_per_node, nodes: node → main, penalty, total), welfare
(revenue, cost, storage cost, incentive, shared, exported and imported energy, total), penalty
(K_t), action (after the [-1, 1] clip) and nodes (per-node flows, economics, state of charge,
requested and applied action, bounds). privileged holds data for the critic only, about the step
of the observation it comes with (the step of the next action; for the terminal observation the
step after the episode, or its last step at the end of the data). It is empty without oracle and forecast; otherwise it holds t (that
step), with oracle oracle_action (a*_t, one value per battery in action order) and
oracle_soc (the oracle state of charge at the start of step t, kWh, comparable with the observed
state of charge: S^ini at the first step of the oracle file, else the oracle state at the end of
step t - 1), and with forecast true_future ('<node>/<quantity>' and
'community/<quantity>' → the true values of the observed forecast steps, kWh per step). obs_clipped is the number of values of the returned
observation that were clipped to [0, 1] because they lie outside the normaliser’s training range
(the zeros before the first row of the data are not counted); it is 0 when the normaliser is fitted
on the whole period.
0.6.2 run keys¶
SmartCerEnv.from_v062_config(cer_path, cer_config) maps a 0.6.2 cer_config with a
DeprecationWarning that lists the keys it does not use:
0.6.2 key |
1.0 argument |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
not used (no reward scaling in the environment) |
With single_agent: true the per-node rewards are in info['reward']['nodes'] and the scalar
reward is their sum (0.6.2 returned a list). Other reward types raise ValueError.
Output directory¶
Runs are written to an output directory chosen by the caller (e.g. results/, gitignored), never
into the scenario folder or a cer_data folder (ValueError; 0.6.2 wrote into
cer_data/<scenario>/simulations/):
from smartcer.gym import SmartCerEnv
from smartcer.gym.controllers import compute_baselines
env = SmartCerEnv('path/to/scenario', start='2022-01-01', end='2022-12-31 23:00')
kpis = compute_baselines(env, 'results/baselines/my_run', seed=0) # 'naive', 'rbc_community'
File |
Content |
|---|---|
|
one row per step: |
|
one row per step: |
|
KPIs of the episode ( |
|
|
|
KPIs of every controller |
The same tables come from any run through the TrackResults wrapper
(env.get_wrapper_attr('tracker').save(folder, env.unwrapped.run_config())) and, during
Stable-Baselines3 training, from ResultsCallback(output_dir, save_freq), which also logs the KPIs
of each episode under smartcer/.
Oracle files¶
The MILP outputs are read by smartcer.oracle.read_oracle, the only function that knows their
format. The format below is provisional. An oracle folder holds:
File |
Columns |
Content |
|---|---|---|
|
|
one row per step and battery node, in any order. |
|
|
one row per calendar day: the MILP objective of the day as a welfare in € (formulation section 10, larger is better) |
Other columns are ignored. The node ids must be the battery nodes of the scenario, every step of
the period needs a row for every battery node (rows outside the period are ignored), and every day
of the period needs an objective. OracleProvider computes a* = p^cha / P^cha - p^dis / P^dis per
step and rejects |a*| > 1 + 1e-9. Rows of the period with both p_cha_kw > 0 and p_dis_kw > 0
(simultaneous charge and discharge) are accepted, a* being their net value, but the replay cannot
reproduce them: they are counted (OracleProvider.simultaneous_rows) and reported with one
smartcer.oracle.OracleWarning naming the count and the first timestamp.
The file has no initial state of charge. The MILP’s S^ini is inferred from the first row of the
file, s̃ = soc_kwh - η^cha p_cha_kw Δt + p_dis_kw Δt / η^dis (formulation section 5 solved
for the state before the step), and must equal the scenario’s S^ini (soc_initial, after
self-discharge when α > 0) within soc_initial_tolerance_kwh (default 1e-3 kWh, so that a file
that rounds the state of charge passes; independent of the 1e-6 kWh replay tolerance below);
otherwise OracleProvider raises ValueError naming the node and both values. Set another
tolerance with OracleProvider.from_folder(folder, community, soc_initial_tolerance_kwh=...) and
pass the provider as oracle. A period that starts after the first step of the file starts from
the oracle state at the end of the step before it, which must be in the file.
check_replay plays a* over the whole period with the ReplayController (create the environment
without episode_length and forecasts) and checks that K_t ≤ 1e-9 at every step and, with an
objective, that the cumulative welfare lies within rtol (default 1e-3) of the cumulative
objective. An oracle action beyond a dynamic bound by at most 1e-6 kWh (REPLAY_TOLERANCE_KWH,
measured as energy on the node side: (a* - A^upper) P^cha Δt or (-a* - A^lower) P^dis Δt) is
solver round-off: the controller moves it onto the bound, without penalty, and counts it; larger
violations are clipped and penalised by the environment and fail the check. The period must be
made of whole days (first step at 00:00, last step ending at 24:00), so that every replayed day is
compared with a whole MILP day, and must start at the first step of the oracle file, because the
replay starts from soc_initial, the oracle’s state only there; otherwise it raises
ValueError:
from smartcer.gym import SmartCerEnv
from smartcer.gym.controllers import check_replay
env = SmartCerEnv('path/to/scenario', oracle='path/to/scenario/oracle')
result = check_replay(env, 'results/oracle_replay', rtol=1e-3) # ReplayCheck; result.passed
File |
Content |
|---|---|
|
one row per day: |
|
the |
compute_baselines(env, folder, controllers=['naive', 'rbc_community', 'replay']) also writes the
tables of the replay.