src.reproduce#

Helpers for reproducing Bruno, Islam & Niet, “Storage in long-term energy planning models with temporal aggregation” (Environ. Res. Commun., ERC-105849) with this repository.

Used by notebooks/reproduce_study.ipynb and workflow/automation.py. Every model run goes through workflow/main.py unchanged, so the notebook exercises exactly the code path that produced the published numbers.

Naming: the formulation called Novo in the paper is implemented in Model_Cluster.

Attributes#

Functions#

check_environment(→ pandas.DataFrame)

Installed versions against the environment stated in Section 3.4.

scenario_name(→ str)

run_case(→ pandas.DataFrame)

Run one scenario (all four formulations unless models is given) via main.py.

run_grid(→ pandas.DataFrame)

Run a grid of scenarios; already-completed scenarios are skipped.

load_results(→ pandas.DataFrame)

Tidy Simulation_Results.xlsx (one row per scenario x formulation, last run kept).

add_errors(→ pandas.DataFrame)

Cost and storage-capacity error (%) against the k365h1 Novo benchmark of the same resource.

table4(→ pandas.DataFrame)

Rebuild manuscript Table 4 (h = 1, unconstrained).

compare_to_archive() → pandas.DataFrame)

Relative difference between fresh runs and the archived results (same Scenario x Model).

clustering(resource, k[, random_state])

Representative days and chronological day->representative mapping, as used by main.py.

seed_robustness([ks, seeds])

Compare clusterings for alternative seeds against seed 0.

phantom_swing(→ dict)

Storage swing that Niet's chaining of representative days adds over the actual day order.

run_soc_validation(→ pandas.DataFrame)

read_storage(→ pandas.Series)

Native or reconstructed storage level written by main.py.

soc_violations(→ dict)

Count bound violations of a storage trajectory.

hourly_trajectory(→ numpy.ndarray)

8760-h storage trajectory used for the fidelity metrics.

rainflow_cycles(→ float)

Equivalent full cycles by rainflow counting (ASTM E1049-85 three-point method).

fidelity_metrics(→ dict)

Module Contents#

src.reproduce.REPO#
src.reproduce.TIMESERIES#
src.reproduce.MAIN#
src.reproduce.SOC_VALIDATION#
src.reproduce.RESOURCES#
src.reproduce.DEMAND_PROFILE = 'SpecifiedDemandProfile.csv'#
src.reproduce.K_VALUES = [2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]#
src.reproduce.H_VALUES = [1, 2, 4, 6, 8, 12]#
src.reproduce.MODELS = ['Model_Niet', 'Model_Welsch', 'Model_Kotzur', 'Model_Cluster']#
src.reproduce.FORMULATION#
src.reproduce.UNCONSTRAINED_CAP = 1000.0#
src.reproduce.STRESS_CAP = 0.2#
src.reproduce.RESIDUAL_STORAGE = 0.05#
src.reproduce.MANUSCRIPT_ENV#
src.reproduce.check_environment() → pandas.DataFrame[source]#

Installed versions against the environment stated in Section 3.4.

src.reproduce.scenario_name(resource: str, k: int, h: int, limited: bool = False, suffix: str = '') → str[source]#
src.reproduce.run_case(resource: str, k: int, h: int, *, cap: float = UNCONSTRAINED_CAP, random_state: int = 0, results_dir: str | pathlib.Path = 'results', models: list | None = None, limited: bool | None = None, suffix: str = '', timeout: float | None = None, verbose: bool = False) → pandas.DataFrame[source]#

Run one scenario (all four formulations unless models is given) via main.py.

The config file is restored afterwards. Returns the rows appended to Simulation_Results.xlsx.

src.reproduce.run_grid(resources, ks, hs, *, skip_existing: bool = True, **kw) → pandas.DataFrame[source]#

Run a grid of scenarios; already-completed scenarios are skipped.

src.reproduce.load_results(xlsx: str | pathlib.Path) → pandas.DataFrame[source]#

Tidy Simulation_Results.xlsx (one row per scenario x formulation, last run kept).

src.reproduce.add_errors(df: pandas.DataFrame) → pandas.DataFrame[source]#

Cost and storage-capacity error (%) against the k365h1 Novo benchmark of the same resource.

Errors use unrounded values. (Table 4 in the paper divides by the benchmark rounded to two decimals, which shifts some capacity errors by up to ~3 percentage points.)

src.reproduce.TABLE4_CASES#
src.reproduce.table4(df: pandas.DataFrame) → pandas.DataFrame[source]#

Rebuild manuscript Table 4 (h = 1, unconstrained).

src.reproduce.compare_to_archive(new: pandas.DataFrame, archive: pandas.DataFrame, cols=('Total cost', 'Power Capacity', 'Storage Capacity')) → pandas.DataFrame[source]#

Relative difference between fresh runs and the archived results (same Scenario x Model).

src.reproduce.clustering(resource: str, k: int, random_state: int = 0)[source]#

Representative days and chronological day->representative mapping, as used by main.py.

src.reproduce.seed_robustness(resources=('WND', 'SOL', 'HYD'), ks=K_VALUES, seeds=range(10)) → pandas.DataFrame[source]#

Compare clusterings for alternative seeds against seed 0.

day_agreement: share of the 365 days mapped to the same representative day as with seed 0. ARI: adjusted Rand index of the day partition (label-permutation invariant). same_rep_days: whether the set of representative days is identical.

src.reproduce.phantom_swing(resource: str, k: int, random_state: int = 0) → dict[source]#

Storage swing that Niet’s chaining of representative days adds over the actual day order.

Solver-free diagnostic. In Niet (constraint S2) the representative days are chained in date order of the medoid days, and each representative hour’s net flow is multiplied by YearSplit, so one hour lasts N_c hours, N_c being the number of days in its cluster. Kotzur and Novo follow the actual sequence of the 365 days. phantom_swing_day_blocks repeats each representative day N_c times instead, which is how Welsch (one season) orders day types; for wind both readings give almost the same swing, for solar they do not (stretching hours breaks the day/night cycle). The VRE profile is scaled so that the represented year is energy-neutral; curtailment and optimised capacities are ignored.

src.reproduce.run_soc_validation() → pandas.DataFrame[source]#
src.reproduce.read_storage(results_dir: str | pathlib.Path, scenario: str, model: str) → pandas.Series[source]#

Native or reconstructed storage level written by main.py.

model: ‘Niet’, ‘Cluster’ (Novo), ‘Kotzur’ (day starts), ‘Kotzur_intraday’, ‘Welsch’ (day-type starts) or ‘Welsch_intraday’.

src.reproduce.soc_violations(soc, upper: float, lower: float = 0.0, tol: float = 1e-09) → dict[source]#

Count bound violations of a storage trajectory.

strict: SoC > upper + tol or SoC < lower - tol (physically infeasible states). at_or_beyond: SoC >= upper - tol or SoC <= lower + tol (includes states exactly on a bound, which are feasible). The paper’s ‘183 h above’ for wind equals the at_or_beyond count.

src.reproduce.hourly_trajectory(results_dir, scenario: str, formulation: str, h: int) → numpy.ndarray[source]#

8760-h storage trajectory used for the fidelity metrics.

Novo and Kotzur: chronological block series (Kotzur reconstructed), held over each block. Welsch: reconstructed series in its native grouped day-type order, as plotted by graph_generator.py. Niet: representative blocks held for their year-split duration.

src.reproduce.rainflow_cycles(series) → float[source]#

Equivalent full cycles by rainflow counting (ASTM E1049-85 three-point method).

Half cycles count 0.5. Returns the total number of (equivalent full) cycles.

src.reproduce.fidelity_metrics(benchmark, test) → dict[source]#