Workflow Guide#
workflow/main.py runs one scenario: it picks representative days, builds the inputs of each
storage formulation, solves them with GLPK and collects the results. All settings come from
config/config.yaml (Configuration Guide).
python workflow/main.py # from anywhere; or: make run-main
Overview#
graph TD
A[Load config/config.yaml] --> B[Cluster days: k-means on CF + demand]
B --> C[Per formulation: generate time sets and mappings]
C --> D[otoole: CSV to GLPK data file]
D --> E[glpsol: solve]
E --> F[Collect SoC, cost, capacity, runtime]
F --> G[Reconstruct Kotzur and Welsch intra-day SoC]
G --> H[Comparison figures]
C --> C1[Model_Niet]
C --> C2[Model_Welsch]
C --> C3[Model_Kotzur]
C --> C4[Model_Cluster = Novo]
Steps#
1. Representative days#
src.cluster_data() reads the hourly capacity-factor and demand profiles (data_8760.CF,
data_8760.SDP), builds one 48-value vector per day (24 min–max scaled CF values and 24 demand
values) and runs k-means with n_clusters clusters (k-means++, n_init = 1, seed
random_state). The day closest to each cluster centre becomes the representative day. It returns
the representative days and, for each of the 365 days, the representative day that stands in for
it. Both are saved to results/<scenario>_clustering.json.
From hour_grouping = h: b = 24 / h blocks per day, k · b representative time slices and
365 · b chronological blocks.
2. Inputs of each formulation#
For each formulation in cases (or in models, if given) the script:
clears the previous run’s results of that formulation and copies the shared parameters from
data/system/intomodels/Model_<name>/inputs_csv/;copies the otoole template to
otoole_config_<name>.run.yaml, where the run-specific values go;writes the time sets and mappings the formulation needs:
Formulation |
Generated files |
Functions |
|---|---|---|
Novo ( |
|
|
Kotzur |
|
|
Welsch |
|
|
All four |
|
|
CFandSDP() keeps the representative days of the hourly profiles and aggregates them into blocks
(mean for capacity factors, sum for demand). yearsplit() weights each time slice by the number of
days its representative day stands for.
3. Solve#
otoole converts the CSVs to a GLPK data file (models/Model_<name>/<name>.txt) and
src.run_simulation() solves it with glpsol. If GLPK does not report an optimal solution, the
solve is retried with --nopresol, --dual and --exact; if all fail, the run is recorded as
FAILED.
4. Results#
For each formulation, the native SoC series is copied to
results/<scenario>_Storage_Level_Model_<name>.csv, and src.table() appends one row (LP size,
total cost, run time, memory, new VRE and storage capacity, solver status) to
results/Simulation_Results.xlsx.
5. Post-processing#
Kotzur:
intraday_kotzur()rebuilds the SoC at every chronological block by accumulating the net charge of the mapped representative block, starting from the year-start level (..._Model_Kotzur_intraday.csv).Welsch:
intraday_welsch2()rebuilds the SoC in grouped day-type order (..._Model_Welsch_intraday.csv).When all four formulations ran,
graph()writes three comparison figures:<scenario>_Figure.png(daily),_Figure_Hourly.pngand_Figure_Hourly_2Weeks.png.
The output files are described in Outputs.
Runtime and memory#
Median over all unconstrained runs of the study, almost all of them aggregated configurations (archived results; GLPK 5.0, Intel Core i7, 16 GB):
Niet |
Welsch |
Kotzur |
Novo |
|
|---|---|---|---|---|
Model generation + solve |
~0.2 s |
~0.3 s |
~0.3 s |
~1 s |
Memory |
~3 MB |
~6 MB |
~6 MB |
~43 MB |
Novo grows with the full chronological year: at k = 365, h = 1 it needs about 12 GB of memory and several minutes per solve. The full-year benchmarks are therefore off by default in the reproduction notebook.
Troubleshooting#
Symptom |
Check |
|---|---|
|
GLPK is installed in the environment: |
|
The log in |
A formulation has no SoC file |
It was not selected in |
Figures missing |
They are written only when all four formulations run |