Simulation table — end-to-end coverage¶
Antares currently produces two output systems in parallel:
- the historical
mc-all/mc-indtext tree, built by thesrc/solver/variable/template machinery — this is the production output; - the flat simulation table
(
simulation-table-<year>-optim-nb-<n>.csv/.parquet), written only when the solver runs with--output=allor--output=simulation-tables.
The simulation table is not used in production yet. This page tracks the e2e coverage that has to be in place before it can be.
Test assets¶
| Layer | Location |
|---|---|
| C++ unit tests | src/tests/src/io/outputs/testSimulationTable.cpp, test-parquet-simulation-table-writer.cpp |
| Worked-example e2e (hand-picked hours, inline expected values) | src/tests/cucumber/features/solver-features/legacy_simulation_table.feature |
| Systematic legacy ⇔ table equivalence | src/tests/cucumber/features/solver-features/legacy_simulation_table_equivalence.feature + features/steps/common_steps/simulation_table_equivalence.py |
| Parquet round-trip (pure modeler) | src/tests/cucumber/features/modeler-features/test_launcher_parquet.feature |
| Folder diff vs inline reference | hybrid_studies.feature (simulation tables match the references) |
The equivalence check¶
the simulation table matches the legacy mc-ind output for year N walks a
declarative mapping over
every area / thermal cluster / short-term-storage cluster / link of the study
and asserts, for each timestep the table covers, that the table value equals its
mc-ind counterpart within a per-quantity tolerance.
Design points:
- reads the final optimisation pass —
optim-nb-2when present, elseoptim-nb-1.optim-nb-1is the LP relaxation and diverges frommc-indon unit-commitment studies; - on a study where a later resolution stage (
peak-shaving,adq-patch) changes the numbers mc-ind actually prints, theoptim-nb-*default is wrong —the simulation table for stage "<stage>" matches the legacy mc-ind output for year Nreads a named stage's table instead. Two scenarios exercise this: "Hydro remix (peak-shaving)" (RemixHydroPostProcessCmdreshuffling hydro generation movesunsupplied_energy/spilled_energy/price) and "Adequacy patch (CSR)" (see below); mc-indfolders are 1-based, the table'sscenario_indexis 0-based (year N⇒mc-ind/0000N⇔simulation-table-(N-1)-...);- no frozen reference file — it compares two live outputs, so it keeps working while the table layout/naming still changes;
basis_statusis never compared (see CHANGELOG#3233);- tolerances are generous because
mc-indhourly columns are printed rounded; the table carries full precision.
Quantity coverage¶
| Quantity | Table output |
mc-ind source | Status |
|---|---|---|---|
| Unsupplied energy | unsupplied_energy |
area values-hourly UNSP. ENRG |
✅ equivalence-checked |
| Spilled energy | spilled_energy |
area values-hourly SPIL. ENRG |
✅ equivalence-checked |
| Area marginal price | price |
area values-hourly MRG. PRICE |
✅ equivalence-checked — dual-derived, see gaps |
| Actual load | actual_load |
area values-hourly LOAD |
✅ equivalence-checked |
| Thermal generation | generation_power |
area details-hourly <cluster> / MWh |
✅ equivalence-checked (optim-nb-2) |
| Units on (ceil) | actual_num_units_on |
area details-hourly <cluster> / NODU |
✅ equivalence-checked — skipped when [other preferences] unit-commitment-mode is fast (Mapping.requires_accurate_uc); the ST value is ceil(x(NumberOfDispatchableUnits)) while mc-ind's NODU is the fast-UC heuristic's own unit count, two independently-computed integers that can differ by 1 on borderline hours |
| STS injection / withdrawal / level | injection_power / withdrawal_power / level |
area details-STstorage-hourly |
✅ equivalence-checked, including studies where the cluster's id and display name differ |
| Link flow / abs / minus / loop | flow / abs_flow / minus_flow / actual_loop_flow |
link values-hourly FLOW LIN. / LOOP FLOW |
✅ equivalence-checked |
Raw num_units_on (LP value) |
num_units_on |
— | ⛔ not equivalence-checked — limitation: fractional LP value, no integer mc-ind column to compare to (actual_num_units_on is the checked, rounded counterpart) |
Derived costs (prop_cost, non_prop_cost, imbalance_cost, …) |
— | — | ⛔ not equivalence-checked — limitation: no mc-ind column at all (mc-ind never printed these); values also carry anti-degeneracy noise. Tested instead as hand-derived worked examples in legacy_simulation_table.feature |
Emissions (co2_emissions, …) |
*_emissions |
area values-hourly CO2 EMIS. (area total only) |
⛔ not equivalence-checked — limitation: mc-ind only prints an area-level total, not per-cluster, so per-cluster ST rows have nothing to diff against. Tested instead as a worked example in legacy_simulation_table.feature |
Hydro level_percentage, actual_inflows, hydro_shadow_price, bellman_value |
— | H. LEV is absolute, not a direct match |
⛔ not equivalence-checked — limitation: H. LEV is a different (absolute, not percentage) quantity. Tested instead as a worked example in legacy_simulation_table.feature |
Congestion fees (abs_congestion_fee, alg_congestion_fee) |
abs_congestion_fee / alg_congestion_fee |
link values-hourly CONG. FEE (ABS./ALG.) |
✅ equivalence-checked — dual-derived, see gaps |
| Reserve area spilled/unsupplied energy | spilled_energy_reserve_<id> / unsupplied_energy_reserve_<id> |
area values-hourly <reserve>_SPIL. / <reserve>_UNSP. |
✅ equivalence-checked |
| Reserve thermal participation (on/off units) | units_on_reserve_power_<id> / units_off_reserve_power_<id> |
area details-hourly <reserve>_<cluster> / <reserve>_<cluster>_off |
✅ equivalence-checked |
Reserve costs (reserve_imbalance_cost_<id>, reserve_participation_cost_<id>) |
— | — | ⛔ not equivalence-checked — limitation: no mc-ind column (same reason as the derived costs above) |
Thermal reserve total participation (reserve_power_<id> on a thermal component) |
— | — | ⛔ not equivalence-checked — redundant, not a limitation: always equals units_on_reserve_power + units_off_reserve_power, both of which are already checked individually |
| STS reserve participation (net) | reserve_power_<id> on ..._short_term_storage_<cluster> |
area details-hourly <reserve>_<cluster> |
✅ equivalence-checked — unlike thermal, mc-ind only exposes the net (signed release-minus-store) value for STS, so there's nothing to check reserve_released_power_<id> / reserve_stored_power_<id> against individually (see below) |
| Hydro reserve participation (net) | reserve_power_<id> on ..._hydro_storage |
area details-hourly <reserve>_Hydro |
✅ equivalence-checked — same net-only caveat as STS above |
STS/hydro reserve released/stored power (reserve_released_power_<id>, reserve_stored_power_<id>) |
— | — | ⛔ not equivalence-checked — limitation: no mc-ind column; mc-ind only ever exposes the net participation (see reserve_power_<id> rows above) |
Known gaps¶
- MIP / MILP weeks — dual-derived rows (
price, STSprofit,hydro_shadow_price) are zero on weeks solved as MIP because duals are not extracted there. Seedocs/architecture/legacy-extra-outputs-spec-checklist.md. - Adequacy patch —
domestic_unsupplied_energy(DENS),is_local_matching_rule_violated(LMR.VIOL) blocked on ANT-5240; CSR outputs out of scope. The rest ofLEGACY_TO_ST(unsupplied_energy,price,flow, …) is equivalence-checked on a CSR study, scoped to theadq-patchstage table — see "Adequacy patch (CSR)" inlegacy_simulation_table_equivalence.feature, which reads that stage because mc-ind prints the post-CSR numbers, notoptim-nb-2's. - ~~
abs_flow/minus_flow/abs_congestion_fee/alg_congestion_feeon theadq-patchstage~~ — fixed. These were wrong on the congested link inside a CSR patch, found by the "Adequacy patch (CSR)" scenario.LegacyExtraOutputEmitter::linkOutputs()computes them fromx(variableManager_.DirectFlow(interco, pdt)). The adequacy patch's CSR post-process (HourlyCSRProblem::constructVariableFlows,adequacy_patch_csr/construct_problem_variables.cpp) repoints that hour'sCorrespondanceVarNativesVarOptimentry at an index in its own short-lived per-hour LP while building it, and nothing put it back once CSR moved on, so by dump time most CSR-touched hours resolved that index against the original weekly problem's variable table — unrelated to CSR's flow. (ReadingValeursDeNTC[pdt].ValeurDuFlux[interco]instead, mirroring howactual_loop_flowis read two lines below, looked like a tempting one-line fix and is what both the ordinary weekly solve and CSR publish theirDirectFlowresult to — but it broke the ordinary, non-post-process dump path: that dump runs before this week's own publish-to-address step catches up, soValeurDuFluxthere is one dump cycle stale whilex(DirectFlow)is fresh. Do not reintroduce that swap.) Fixed instead at the source, inHourlyCSRProblem::run()(adequacy_patch_csr/adq_patch_curtailment_sharing.cpp): it now saves the hour's originalCorrespondanceVarNativesVarOptimindices (still valid, still correctly published) before building its own per-hour problem and restores them once that problem is solved and its result published, so nothing outside CSR's own solve ever resolves against its numbering. - Coverage is hybrid-only. Pure-classic studies produce a sparse table
(
SimulationTableWriterthrows on an empty table); pure-modeler has only the Parquet round-trip test. - No frozen-reference regression in
src/tests/run-study-tests/— deferred until the table file layout (feature/st_stage_selection: per-MC-year files, per-stage tables) stabilises.
Adding coverage¶
- New quantity, direct mc-ind counterpart — add a
Mapping(...)row toLEGACY_TO_STinsimulation_table_equivalence.py. Pick a study that exercises it, confirm the sign and themc-indprint precision, setatol/rtolaccordingly. - New study — add a
Scenariotolegacy_simulation_table_equivalence.featurepointing at a study already in theAntares_Simulator_Tests_NRsubmodule;--output=allplus the existingyear-by-yearforcing ininit_simulationis enough. - Quantity with no mc-ind counterpart (derived costs, hydro %) — keep it as
a worked example in
legacy_simulation_table.featurewith an inline expected value and a comment deriving it.