flextool 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flextool/__init__.py +41 -0
- flextool/_mem_sampler.py +193 -0
- flextool/_resources.py +43 -0
- flextool/calibrate/__init__.py +51 -0
- flextool/calibrate/__main__.py +11 -0
- flextool/calibrate/_cli.py +316 -0
- flextool/calibrate/_db_alt.py +166 -0
- flextool/calibrate/_final_outputs.py +110 -0
- flextool/calibrate/_guard.py +151 -0
- flextool/calibrate/_loop.py +558 -0
- flextool/calibrate/_readers.py +223 -0
- flextool/calibrate/_report.py +263 -0
- flextool/calibrate/_sizing.py +699 -0
- flextool/calibrate/_solve.py +134 -0
- flextool/calibrate/_solve_status.py +495 -0
- flextool/cli/__init__.py +9 -0
- flextool/cli/_console.py +51 -0
- flextool/cli/_timing.py +147 -0
- flextool/cli/cmd_execute_flextool_workflow.py +187 -0
- flextool/cli/cmd_export_to_tabular.py +56 -0
- flextool/cli/cmd_import_sensitivities.py +75 -0
- flextool/cli/cmd_migrate_database.py +13 -0
- flextool/cli/cmd_open_results_db.py +269 -0
- flextool/cli/cmd_read_matpower.py +66 -0
- flextool/cli/cmd_read_old_flextool.py +63 -0
- flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
- flextool/cli/cmd_read_tabular_input.py +81 -0
- flextool/cli/cmd_run_flextool.py +1095 -0
- flextool/cli/cmd_scenario_results.py +284 -0
- flextool/cli/cmd_solve_mps.py +169 -0
- flextool/cli/cmd_update_flextool.py +17 -0
- flextool/cli/cmd_write_outputs.py +125 -0
- flextool/common_utils/__init__.py +1 -0
- flextool/common_utils/plot_mem_shape.py +77 -0
- flextool/common_utils/precision.py +451 -0
- flextool/decomposition/__init__.py +0 -0
- flextool/decomposition/region_decomposition.py +128 -0
- flextool/decomposition/region_filter.py +1261 -0
- flextool/engine_polars/__init__.py +110 -0
- flextool/engine_polars/_axis_enums.py +742 -0
- flextool/engine_polars/_benders.py +3462 -0
- flextool/engine_polars/_block_layout.py +1479 -0
- flextool/engine_polars/_blocks.py +1515 -0
- flextool/engine_polars/_commodity_ladder.py +660 -0
- flextool/engine_polars/_cumulative_invest.py +1165 -0
- flextool/engine_polars/_db_loader.py +153 -0
- flextool/engine_polars/_db_reader.py +127 -0
- flextool/engine_polars/_dc_power_flow.py +445 -0
- flextool/engine_polars/_delay.py +442 -0
- flextool/engine_polars/_derived_arithmetic.py +432 -0
- flextool/engine_polars/_derived_block.py +990 -0
- flextool/engine_polars/_derived_branch.py +769 -0
- flextool/engine_polars/_derived_existing.py +1353 -0
- flextool/engine_polars/_derived_npv.py +1297 -0
- flextool/engine_polars/_derived_params.py +9850 -0
- flextool/engine_polars/_derived_profile.py +881 -0
- flextool/engine_polars/_derived_walks.py +276 -0
- flextool/engine_polars/_determinism.py +70 -0
- flextool/engine_polars/_direct_params.py +2186 -0
- flextool/engine_polars/_dump_csvs.py +1009 -0
- flextool/engine_polars/_emit_arc_unions.py +1631 -0
- flextool/engine_polars/_emit_calc_params.py +729 -0
- flextool/engine_polars/_emit_chain_params.py +709 -0
- flextool/engine_polars/_emit_co2_accumulators.py +400 -0
- flextool/engine_polars/_emit_dispatchers.py +690 -0
- flextool/engine_polars/_emit_energy_margin.py +125 -0
- flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
- flextool/engine_polars/_emit_entity_annual.py +428 -0
- flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
- flextool/engine_polars/_emit_leaf_sets.py +550 -0
- flextool/engine_polars/_emit_lp_scaling.py +665 -0
- flextool/engine_polars/_emit_mid_sets.py +859 -0
- flextool/engine_polars/_emit_pdt_params.py +759 -0
- flextool/engine_polars/_emit_per_solve.py +774 -0
- flextool/engine_polars/_emit_period_calc.py +504 -0
- flextool/engine_polars/_emit_period_params.py +2398 -0
- flextool/engine_polars/_emit_provider_io.py +141 -0
- flextool/engine_polars/_emit_reserve.py +574 -0
- flextool/engine_polars/_emit_solve_time.py +311 -0
- flextool/engine_polars/_emit_solve_writers.py +1249 -0
- flextool/engine_polars/_flex_data_accumulator.py +388 -0
- flextool/engine_polars/_flex_data_provider.py +478 -0
- flextool/engine_polars/_group_slack.py +1253 -0
- flextool/engine_polars/_inmemory_reader.py +140 -0
- flextool/engine_polars/_input_source.py +336 -0
- flextool/engine_polars/_invest_seeds.py +191 -0
- flextool/engine_polars/_native_input_writer.py +100 -0
- flextool/engine_polars/_native_run_model.py +1348 -0
- flextool/engine_polars/_orchestration.py +4314 -0
- flextool/engine_polars/_output_writer.py +439 -0
- flextool/engine_polars/_param_shapes.py +1595 -0
- flextool/engine_polars/_parquet_bundle.py +723 -0
- flextool/engine_polars/_pdt_join.py +167 -0
- flextool/engine_polars/_pdt_lookup.py +547 -0
- flextool/engine_polars/_per_solve_sets.py +335 -0
- flextool/engine_polars/_projection_params.py +2056 -0
- flextool/engine_polars/_provider_keys.py +173 -0
- flextool/engine_polars/_provider_translators.py +225 -0
- flextool/engine_polars/_recursive_solve.py +703 -0
- flextool/engine_polars/_region_filter.py +2508 -0
- flextool/engine_polars/_reserve.py +649 -0
- flextool/engine_polars/_solve_acceptance.py +331 -0
- flextool/engine_polars/_solve_config.py +1001 -0
- flextool/engine_polars/_solve_context.py +885 -0
- flextool/engine_polars/_solve_handoff.py +164 -0
- flextool/engine_polars/_solve_state.py +232 -0
- flextool/engine_polars/_solver_base.py +36 -0
- flextool/engine_polars/_solver_dispatch.py +511 -0
- flextool/engine_polars/_spinedb_reader.py +1165 -0
- flextool/engine_polars/_stochastic.py +593 -0
- flextool/engine_polars/_subprocess_solve.py +1838 -0
- flextool/engine_polars/_timeline.py +1416 -0
- flextool/engine_polars/_vectorize.py +438 -0
- flextool/engine_polars/_warm.py +858 -0
- flextool/engine_polars/autoscale/__init__.py +107 -0
- flextool/engine_polars/autoscale/_config.py +218 -0
- flextool/engine_polars/autoscale/_layer2.py +1253 -0
- flextool/engine_polars/autoscale/_layer2_types.py +584 -0
- flextool/engine_polars/autoscale/_quantity_types.py +621 -0
- flextool/engine_polars/autoscale/_report.py +336 -0
- flextool/engine_polars/chain.py +259 -0
- flextool/engine_polars/input.py +6638 -0
- flextool/engine_polars/model.py +4754 -0
- flextool/env_check.py +388 -0
- flextool/export_to_tabular/__init__.py +5 -0
- flextool/export_to_tabular/db_reader.py +224 -0
- flextool/export_to_tabular/excel_writer.py +3559 -0
- flextool/export_to_tabular/export_settings.yaml +377 -0
- flextool/export_to_tabular/export_to_excel.py +227 -0
- flextool/export_to_tabular/formatting.py +543 -0
- flextool/export_to_tabular/sheet_config.py +876 -0
- flextool/gui/__init__.py +0 -0
- flextool/gui/__main__.py +118 -0
- flextool/gui/calibrate_commands.py +184 -0
- flextool/gui/calibrate_jobs.py +424 -0
- flextool/gui/check_tree.py +142 -0
- flextool/gui/cli_format.py +83 -0
- flextool/gui/config_parser.py +68 -0
- flextool/gui/data_models.py +362 -0
- flextool/gui/db_editor_integration.py +202 -0
- flextool/gui/db_version_check.py +269 -0
- flextool/gui/dialogs/__init__.py +0 -0
- flextool/gui/dialogs/add_dialog.py +1098 -0
- flextool/gui/dialogs/calibrate_dialog.py +1259 -0
- flextool/gui/dialogs/file_picker.py +473 -0
- flextool/gui/dialogs/group_picker.py +299 -0
- flextool/gui/dialogs/migration_consent_dialog.py +106 -0
- flextool/gui/dialogs/migration_progress_dialog.py +237 -0
- flextool/gui/dialogs/plot_dialog.py +459 -0
- flextool/gui/dialogs/plot_settings_picker.py +2184 -0
- flextool/gui/dialogs/project_dialog.py +426 -0
- flextool/gui/dialogs/update_dialog.py +212 -0
- flextool/gui/downsampling.py +88 -0
- flextool/gui/error_handling.py +50 -0
- flextool/gui/execution_manager.py +1715 -0
- flextool/gui/execution_window.py +1377 -0
- flextool/gui/hover_tooltip.py +111 -0
- flextool/gui/input_sources.py +730 -0
- flextool/gui/main_window.py +6181 -0
- flextool/gui/network_graph.py +215 -0
- flextool/gui/output_actions.py +393 -0
- flextool/gui/output_log_window.py +159 -0
- flextool/gui/platform_utils.py +421 -0
- flextool/gui/plot_cache.py +88 -0
- flextool/gui/plot_canvas.py +543 -0
- flextool/gui/plot_config_reader.py +272 -0
- flextool/gui/project_utils.py +100 -0
- flextool/gui/result_viewer.py +4394 -0
- flextool/gui/scenario_key.py +162 -0
- flextool/gui/scenario_lists.py +516 -0
- flextool/gui/settings_io.py +360 -0
- flextool/gui/solve_reader.py +103 -0
- flextool/gui/tree_reorder.py +88 -0
- flextool/gui/ui_metrics.py +420 -0
- flextool/input_derivation/__init__.py +281 -0
- flextool/input_derivation/_commodity_ladder.py +375 -0
- flextool/input_derivation/_commodity_ladder_sets.py +70 -0
- flextool/input_derivation/_dc_power_flow.py +377 -0
- flextool/input_derivation/_method_constants.py +77 -0
- flextool/input_derivation/_process_method.py +258 -0
- flextool/input_derivation/_specs.py +1026 -0
- flextool/input_derivation/_validators.py +321 -0
- flextool/lean_parquet.py +159 -0
- flextool/model_builder/__init__.py +5 -0
- flextool/model_builder/build_model.py +589 -0
- flextool/model_builder/encoding.py +67 -0
- flextool/model_builder/names.py +34 -0
- flextool/model_builder/profiles.py +129 -0
- flextool/plot_outputs/__init__.py +14 -0
- flextool/plot_outputs/axis_helpers.py +355 -0
- flextool/plot_outputs/color_template.py +888 -0
- flextool/plot_outputs/config.py +171 -0
- flextool/plot_outputs/format_helpers.py +345 -0
- flextool/plot_outputs/legend_helpers.py +143 -0
- flextool/plot_outputs/orchestrator.py +1141 -0
- flextool/plot_outputs/perf.py +37 -0
- flextool/plot_outputs/plan.py +1787 -0
- flextool/plot_outputs/plot_bars.py +1510 -0
- flextool/plot_outputs/plot_bars_detail.py +753 -0
- flextool/plot_outputs/plot_lines.py +951 -0
- flextool/plot_outputs/shared_manifest.py +564 -0
- flextool/plot_outputs/subplot_helpers.py +137 -0
- flextool/process_inputs/__init__.py +188 -0
- flextool/process_inputs/import_old_excel_input.json +4159 -0
- flextool/process_inputs/read_matpower.py +451 -0
- flextool/process_inputs/read_old_flextool.py +1288 -0
- flextool/process_inputs/read_self_describing_excel.py +1423 -0
- flextool/process_inputs/read_tabular_with_specification.py +1114 -0
- flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
- flextool/process_inputs/write_self_describing_to_db.py +977 -0
- flextool/process_inputs/write_to_input_db.py +269 -0
- flextool/process_outputs/__init__.py +7 -0
- flextool/process_outputs/_annualize.py +55 -0
- flextool/process_outputs/_inmemory_helpers.py +292 -0
- flextool/process_outputs/_output_meta.py +672 -0
- flextool/process_outputs/calc_capacity_flows.py +107 -0
- flextool/process_outputs/calc_connections.py +136 -0
- flextool/process_outputs/calc_costs.py +260 -0
- flextool/process_outputs/calc_group_flows.py +192 -0
- flextool/process_outputs/calc_slacks.py +103 -0
- flextool/process_outputs/calc_storage_vre.py +160 -0
- flextool/process_outputs/drop_levels.py +208 -0
- flextool/process_outputs/handoff_writers.py +1315 -0
- flextool/process_outputs/out_ancillary.py +544 -0
- flextool/process_outputs/out_capacity.py +179 -0
- flextool/process_outputs/out_costs.py +334 -0
- flextool/process_outputs/out_flowgroup.py +189 -0
- flextool/process_outputs/out_flows.py +301 -0
- flextool/process_outputs/out_group.py +475 -0
- flextool/process_outputs/out_node.py +190 -0
- flextool/process_outputs/persist_realized_slice.py +601 -0
- flextool/process_outputs/process_results.py +24 -0
- flextool/process_outputs/read_highs_solution.py +2256 -0
- flextool/process_outputs/read_parameters.py +1799 -0
- flextool/process_outputs/read_sets.py +1095 -0
- flextool/process_outputs/read_variables.py +553 -0
- flextool/process_outputs/solve_order.py +81 -0
- flextool/process_outputs/spinedb_replay.py +412 -0
- flextool/process_outputs/union_realized_slice.py +224 -0
- flextool/process_outputs/write_outputs.py +1286 -0
- flextool/process_outputs/write_spinedb.py +1267 -0
- flextool/representative_periods/__init__.py +5 -0
- flextool/representative_periods/clustering.py +165 -0
- flextool/representative_periods/force_include.py +563 -0
- flextool/representative_periods/netload.py +365 -0
- flextool/representative_periods/netload_inputs.py +345 -0
- flextool/representative_periods/netload_iterate.py +722 -0
- flextool/representative_periods/preprocess.py +948 -0
- flextool/representative_periods/scenario_stack.py +195 -0
- flextool/representative_periods/weights.py +124 -0
- flextool/scenario_comparison/__init__.py +13 -0
- flextool/scenario_comparison/config_builder.py +158 -0
- flextool/scenario_comparison/constants.py +20 -0
- flextool/scenario_comparison/data_models.py +222 -0
- flextool/scenario_comparison/db_reader.py +399 -0
- flextool/scenario_comparison/dispatch_data.py +1002 -0
- flextool/scenario_comparison/dispatch_mappings.py +205 -0
- flextool/scenario_comparison/dispatch_plots.py +691 -0
- flextool/scenario_comparison/input_entity_colors.py +319 -0
- flextool/scenario_comparison/orchestrator.py +453 -0
- flextool/scenario_comparison/plan_union.py +244 -0
- flextool/scenario_comparison/plot_settings_seed.py +205 -0
- flextool/schemas/AXIS_CONTRACT.md +71 -0
- flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
- flextool/schemas/canonical_databases/howto_connections.json +5606 -0
- flextool/schemas/canonical_databases/howto_demand.json +5518 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
- flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
- flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
- flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
- flextool/schemas/canonical_databases/templates_examples.json +13532 -0
- flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
- flextool/schemas/comparison_settings_template.json +197 -0
- flextool/schemas/default_plot_settings.yaml +260 -0
- flextool/schemas/default_plots.yaml +2293 -0
- flextool/schemas/flextool_axis_contract.json +303 -0
- flextool/schemas/flextool_axis_contract.schema.json +247 -0
- flextool/schemas/old_flextool_import_template.json +4443 -0
- flextool/schemas/output_info_template.json +48 -0
- flextool/schemas/output_settings_template.json +256 -0
- flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
- flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
- flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
- flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
- flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
- flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
- flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
- flextool/schemas/spinedb_results_schema.json +581 -0
- flextool/schemas/spinedb_schema.json +4636 -0
- flextool/solver_config/copt.opt.template +18 -0
- flextool/solver_config/cplex.opt.template +25 -0
- flextool/solver_config/gurobi.opt.template +18 -0
- flextool/solver_config/highs.opt.template +18 -0
- flextool/solver_config/xpress.opt.template +26 -0
- flextool/spinedb_backend/__init__.py +26 -0
- flextool/spinedb_backend/_axis_enums.py +1119 -0
- flextool/spinedb_backend/_backend.py +1139 -0
- flextool/update_flextool/__init__.py +12 -0
- flextool/update_flextool/canonical_databases.py +251 -0
- flextool/update_flextool/db_migration.py +7108 -0
- flextool/update_flextool/ensure_settings_db.py +138 -0
- flextool/update_flextool/export_database.py +103 -0
- flextool/update_flextool/extend_tests_fixture.py +772 -0
- flextool/update_flextool/generate_canonical.py +274 -0
- flextool/update_flextool/initialize_database.py +42 -0
- flextool/update_flextool/install_info.py +225 -0
- flextool/update_flextool/self_update.py +464 -0
- flextool/update_flextool/sync_master_json_template.py +125 -0
- flextool/update_flextool/test_fixtures.py +187 -0
- flextool-4.0.0.dist-info/METADATA +217 -0
- flextool-4.0.0.dist-info/RECORD +322 -0
- flextool-4.0.0.dist-info/WHEEL +5 -0
- flextool-4.0.0.dist-info/entry_points.txt +17 -0
- flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
- flextool-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""Derive per-column output metadata (unit + semantics) from transforms.
|
|
2
|
+
|
|
3
|
+
The output stage turns LP variables and input parameters into ~95 processed
|
|
4
|
+
tables. Hand-authoring metadata per column would be a slog and rot on the
|
|
5
|
+
next schema change. Instead we *derive* it: every annual/extensive output is
|
|
6
|
+
produced by one of a small, named set of transforms (see :class:`Transform`),
|
|
7
|
+
and the displayed unit + temporal semantics fall out of the transform applied
|
|
8
|
+
to a base quantity.
|
|
9
|
+
|
|
10
|
+
This is the "derive from transforms" layer. ``annualize_dt_to_d`` (the single
|
|
11
|
+
dt→d aggregation helper all annual outputs now route through, see
|
|
12
|
+
``_annualize.py``) is the runtime embodiment of the ``*_ANNUAL`` transforms;
|
|
13
|
+
this module is the static counterpart that labels the resulting columns.
|
|
14
|
+
|
|
15
|
+
A per-output declaration (:data:`OUTPUT_TRANSFORM`) maps an output key to the
|
|
16
|
+
transform its (non-dimension) columns underwent. :func:`derive_column_meta`
|
|
17
|
+
expands that to one :class:`ColumnMeta` per column, generating a templated
|
|
18
|
+
tooltip and attaching any per-column ``formula`` override.
|
|
19
|
+
|
|
20
|
+
Coverage is enforced by ``tests/test_output_column_metadata.py`` via a ratchet:
|
|
21
|
+
new processed outputs may not land undocumented.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from enum import Enum
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Semantics(str, Enum):
|
|
31
|
+
"""The kind of quantity a column holds — the field that disambiguates
|
|
32
|
+
a per-timestep rate from a full-year-annualized total."""
|
|
33
|
+
|
|
34
|
+
INSTANTANEOUS = "instantaneous" # a rate at the timestep (MW). Integrate for energy.
|
|
35
|
+
PER_STEP = "per_step" # already integrated over the step's duration (cost_dt).
|
|
36
|
+
PER_PERIOD = "per_period" # summed over the period, as-sampled (NOT scaled to a year).
|
|
37
|
+
ANNUALIZED = "annualized" # scaled to a full-year equivalent (÷ period_share_of_year).
|
|
38
|
+
HORIZON = "horizon" # undiscounted total over the represented horizon (×years_represented).
|
|
39
|
+
DISCOUNTED = "discounted" # NPV over the horizon (inflation + years_represented).
|
|
40
|
+
AVERAGE = "average" # time-average over the period (÷ period_hours).
|
|
41
|
+
RATIO = "ratio" # dimensionless (share, capacity factor).
|
|
42
|
+
COUNT = "count" # event count (e.g. start-ups), annualized.
|
|
43
|
+
LEVEL = "level" # a stock/snapshot (capacity, storage state) — not summed.
|
|
44
|
+
PRICE = "price" # marginal value / shadow price (dual).
|
|
45
|
+
DIMENSION = "dimension" # index/key column, not a measure.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Column names that are always index/key dimensions, never measures.
|
|
49
|
+
DIMENSION_COLUMNS: frozenset[str] = frozenset(
|
|
50
|
+
{"solve", "period", "time", "scenario", "step", "year", "group", "node",
|
|
51
|
+
"unit", "connection", "process", "source", "sink", "commodity", "reserve",
|
|
52
|
+
"upDown", "category"}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Transform:
|
|
58
|
+
"""A named output transform → the (unit, semantics) it yields.
|
|
59
|
+
|
|
60
|
+
``unit`` is the *displayed* unit string (already including ``/a`` where the
|
|
61
|
+
quantity is annualized), derived by the simple algebra noted in ``algebra``
|
|
62
|
+
(e.g. MW · h / period_share → MWh/a). Keeping the algebra in a comment
|
|
63
|
+
rather than a full dimensional-analysis engine is deliberate: the transform
|
|
64
|
+
set is tiny and stable, so an explicit table is clearer and just as safe.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
unit: str
|
|
68
|
+
semantics: Semantics
|
|
69
|
+
tooltip: str # short, one-line (plot/CSV/hover).
|
|
70
|
+
description: str = "" # optional longer form (docs/datapackage); falls back to tooltip.
|
|
71
|
+
algebra: str = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# The transform catalog. Each entry is the result of applying one output-stage
|
|
75
|
+
# transform to its base quantity. Names mirror the code paths in out_*/calc_*.
|
|
76
|
+
RATE = Transform(
|
|
77
|
+
"MW", Semantics.INSTANTANEOUS,
|
|
78
|
+
"Power at the timestep (a rate). Multiply by step_duration for energy.",
|
|
79
|
+
algebra="v_flow",
|
|
80
|
+
)
|
|
81
|
+
ENERGY_ANNUAL = Transform(
|
|
82
|
+
"MWh/a", Semantics.ANNUALIZED,
|
|
83
|
+
"Energy, scaled from the sampled timeline to a full-year equivalent.",
|
|
84
|
+
algebra="MW · step_duration · timestep_weight / period_share",
|
|
85
|
+
)
|
|
86
|
+
ENERGY_PERIOD = Transform(
|
|
87
|
+
"MWh", Semantics.PER_PERIOD,
|
|
88
|
+
"Energy summed over the sampled timeline (NOT scaled to a full year).",
|
|
89
|
+
algebra="MW · step_duration · timestep_weight (no /period_share)",
|
|
90
|
+
)
|
|
91
|
+
PREENERGY_ANNUAL = Transform(
|
|
92
|
+
"MWh/a", Semantics.ANNUALIZED,
|
|
93
|
+
"Energy (already per-step MWh), scaled to a full-year equivalent.",
|
|
94
|
+
algebra="MWh_step · timestep_weight / period_share",
|
|
95
|
+
)
|
|
96
|
+
MONEY_PERSTEP = Transform(
|
|
97
|
+
"CUR", Semantics.PER_STEP,
|
|
98
|
+
"Cost incurred during the timestep (step_duration and weight already applied).",
|
|
99
|
+
algebra="price · flow · step_duration · timestep_weight",
|
|
100
|
+
)
|
|
101
|
+
MONEY_ANNUAL = Transform(
|
|
102
|
+
"M CUR/a", Semantics.ANNUALIZED,
|
|
103
|
+
"Cost, scaled from the sampled timeline to a full-year equivalent.",
|
|
104
|
+
algebra="Σ cost_dt / period_share / 1e6",
|
|
105
|
+
)
|
|
106
|
+
MONEY_DISCOUNTED = Transform(
|
|
107
|
+
"M CUR", Semantics.DISCOUNTED,
|
|
108
|
+
"Net present cost over the horizon (inflation × years_represented applied).",
|
|
109
|
+
algebra="annual_cost · inflation_factor · years_represented",
|
|
110
|
+
)
|
|
111
|
+
COST_ENTITY_ANNUAL = Transform(
|
|
112
|
+
"M CUR/a", Semantics.ANNUALIZED,
|
|
113
|
+
"Per-entity annualized cost for this category (full-year equivalent).",
|
|
114
|
+
description="Annualized cost of one (entity, period) cell for this "
|
|
115
|
+
"category — the system annualized cost collapsed per entity "
|
|
116
|
+
"rather than summed system-wide. Fuel/commodity cost is "
|
|
117
|
+
"attributed to the consuming process (the unit/connection that "
|
|
118
|
+
"draws the commodity, not the source node); CO2 cost is summed "
|
|
119
|
+
"over every priced group the process touches. Summing a "
|
|
120
|
+
"category over all entities reproduces the system summary "
|
|
121
|
+
"(annualized_costs_d_p).",
|
|
122
|
+
algebra="Σ entity_cost_dt / period_share / 1e6",
|
|
123
|
+
)
|
|
124
|
+
COST_ENTITY_DISCOUNTED = Transform(
|
|
125
|
+
"M CUR", Semantics.DISCOUNTED,
|
|
126
|
+
"Per-entity net present cost for this category over the horizon.",
|
|
127
|
+
description="Net present (discounted) cost of one (entity, period) cell "
|
|
128
|
+
"for this category — the system discounted cost collapsed per "
|
|
129
|
+
"entity rather than summed system-wide. Fuel/commodity cost is "
|
|
130
|
+
"attributed to the consuming process (the unit/connection that "
|
|
131
|
+
"draws the commodity, not the source node); CO2 cost is summed "
|
|
132
|
+
"over every priced group the process touches. Summing a "
|
|
133
|
+
"category over all entities reproduces the system summary "
|
|
134
|
+
"(costs_discounted_d_p).",
|
|
135
|
+
algebra="annual_entity_cost · inflation_factor · years_represented",
|
|
136
|
+
)
|
|
137
|
+
EMISSION_ANNUAL = Transform(
|
|
138
|
+
"t/a", Semantics.ANNUALIZED,
|
|
139
|
+
"Emissions, scaled from the sampled timeline to a full-year equivalent.",
|
|
140
|
+
algebra="t_step · timestep_weight / period_share",
|
|
141
|
+
)
|
|
142
|
+
COUNT_ANNUAL = Transform(
|
|
143
|
+
"units/a", Semantics.COUNT,
|
|
144
|
+
"Event count (e.g. start-ups), scaled to a full-year equivalent.",
|
|
145
|
+
algebra="count_step · timestep_weight / period_share",
|
|
146
|
+
)
|
|
147
|
+
AVERAGE_MW = Transform(
|
|
148
|
+
"MW", Semantics.AVERAGE,
|
|
149
|
+
"Time-average power held over the period.",
|
|
150
|
+
algebra="Σ(MW · step_duration) / period_hours",
|
|
151
|
+
)
|
|
152
|
+
RATIO = Transform(
|
|
153
|
+
"", Semantics.RATIO,
|
|
154
|
+
"Dimensionless ratio (share / capacity factor).",
|
|
155
|
+
)
|
|
156
|
+
ENERGY_PERSTEP = Transform(
|
|
157
|
+
"MWh", Semantics.PER_STEP,
|
|
158
|
+
"Energy within the timestep (already per-step MWh, not a rate).",
|
|
159
|
+
)
|
|
160
|
+
RAMP = Transform(
|
|
161
|
+
"MW", Semantics.INSTANTANEOUS,
|
|
162
|
+
"Change in flow between consecutive timesteps (a ramp).",
|
|
163
|
+
algebra="flow[t] − flow[t-1]",
|
|
164
|
+
)
|
|
165
|
+
CAPACITY_MW = Transform(
|
|
166
|
+
"MW", Semantics.LEVEL,
|
|
167
|
+
"Installed/usable capacity (a stock that holds over the period, not a flow).",
|
|
168
|
+
algebra="existing + invested − divested",
|
|
169
|
+
)
|
|
170
|
+
CAPACITY_MWH = Transform(
|
|
171
|
+
"MWh", Semantics.LEVEL,
|
|
172
|
+
"Storage energy capacity (a stock).",
|
|
173
|
+
algebra="existing + invested − divested",
|
|
174
|
+
)
|
|
175
|
+
STORAGE_STATE = Transform(
|
|
176
|
+
"MWh", Semantics.LEVEL,
|
|
177
|
+
"Stored energy at the timestep (state of charge).",
|
|
178
|
+
algebra="v_state · unitsize",
|
|
179
|
+
)
|
|
180
|
+
PRICE_ENERGY = Transform(
|
|
181
|
+
"CUR/MWh", Semantics.PRICE,
|
|
182
|
+
"Nodal energy price (shadow price of the node balance constraint).",
|
|
183
|
+
description="Nodal energy price — the shadow price (dual) of the node "
|
|
184
|
+
"balance constraint: the marginal system cost of one more unit "
|
|
185
|
+
"of the commodity at that node and timestep. For a storage "
|
|
186
|
+
"node this is the marginal value of stored energy (the "
|
|
187
|
+
"\"water value\"): a well-defined intertemporal price, but "
|
|
188
|
+
"treat it with care — it can be non-unique when storage sits "
|
|
189
|
+
"at the margin (indifferent between charging and discharging), "
|
|
190
|
+
"spike at empty/full state of charge, differ between the "
|
|
191
|
+
"charge and discharge sides by the round-trip efficiency, and "
|
|
192
|
+
"be shifted by end-of-horizon, rolling and inter-period "
|
|
193
|
+
"(blended-weight) storage linking.",
|
|
194
|
+
algebra="dual(node_balance)",
|
|
195
|
+
)
|
|
196
|
+
EMISSION_MT = Transform(
|
|
197
|
+
"Mt", Semantics.HORIZON,
|
|
198
|
+
"System CO2 emissions, total over the represented horizon (NOT per year).",
|
|
199
|
+
description="System CO2 emissions summed across all periods, each scaled "
|
|
200
|
+
"by its years_represented and converted tonnes→megatonnes — an "
|
|
201
|
+
"undiscounted horizon total, not an annual rate.",
|
|
202
|
+
algebra="Σ_periods(t/a · years_represented) / 1e6",
|
|
203
|
+
)
|
|
204
|
+
FACTOR = Transform(
|
|
205
|
+
"", Semantics.RATIO,
|
|
206
|
+
"Dimensionless discount / inflation factor.",
|
|
207
|
+
)
|
|
208
|
+
ANNUITY = Transform(
|
|
209
|
+
"CUR/MW/a", Semantics.ANNUALIZED,
|
|
210
|
+
"Annualised investment cost per MW of capacity — the cost charged for "
|
|
211
|
+
"each year of the entity's economic lifetime.",
|
|
212
|
+
)
|
|
213
|
+
YEARS = Transform(
|
|
214
|
+
"a", Semantics.COUNT,
|
|
215
|
+
"Calendar years the period represents (annualisation weight).",
|
|
216
|
+
)
|
|
217
|
+
PRICE_RESERVE = Transform(
|
|
218
|
+
"CUR/MW", Semantics.PRICE,
|
|
219
|
+
"Reserve price (dual of the reserve balance) — CUR per MW of reserve held "
|
|
220
|
+
"(= CUR/MWh held for one hour).",
|
|
221
|
+
algebra="dual(reserve_balance)",
|
|
222
|
+
)
|
|
223
|
+
PRICE_CO2 = Transform(
|
|
224
|
+
"CUR/t", Semantics.PRICE,
|
|
225
|
+
"CO2 price (shadow price of the CO2 cap).",
|
|
226
|
+
algebra="dual(co2_cap) / 1000",
|
|
227
|
+
)
|
|
228
|
+
# Investment duals carry the same convention as invest_cost: CUR/kW for
|
|
229
|
+
# units & connections (CUR/kWh for storage nodes).
|
|
230
|
+
DUAL_INVEST_KW = Transform(
|
|
231
|
+
"CUR/kW", Semantics.PRICE,
|
|
232
|
+
"Effective marginal system value of +1 unit of capacity "
|
|
233
|
+
"(same convention as invest_cost).",
|
|
234
|
+
algebra="dual(invest constraint) / entity_unitsize",
|
|
235
|
+
)
|
|
236
|
+
DUAL_INVEST_KWH = Transform(
|
|
237
|
+
"CUR/kWh", Semantics.PRICE,
|
|
238
|
+
"Effective marginal system value of +1 unit of storage capacity "
|
|
239
|
+
"(same convention as invest_cost).",
|
|
240
|
+
)
|
|
241
|
+
INERTIA = Transform(
|
|
242
|
+
"MW·s", Semantics.LEVEL,
|
|
243
|
+
"Inertia provided (rotational energy) at the timestep.",
|
|
244
|
+
algebra="online · unitsize · inertia_constant",
|
|
245
|
+
)
|
|
246
|
+
INERTIA_ANNUAL = Transform(
|
|
247
|
+
"MW·s/a", Semantics.ANNUALIZED,
|
|
248
|
+
"Inertia-constraint slack, scaled to a full-year equivalent.",
|
|
249
|
+
)
|
|
250
|
+
ONLINE_COUNT = Transform(
|
|
251
|
+
"units", Semantics.LEVEL,
|
|
252
|
+
"Number of online units at the timestep.",
|
|
253
|
+
)
|
|
254
|
+
ONLINE_AVG = Transform(
|
|
255
|
+
"units", Semantics.AVERAGE,
|
|
256
|
+
"Average number of online units over the period.",
|
|
257
|
+
)
|
|
258
|
+
RESERVE_AVG = Transform(
|
|
259
|
+
"MW", Semantics.AVERAGE,
|
|
260
|
+
"Average reserve held over the period.",
|
|
261
|
+
)
|
|
262
|
+
ANGLE = Transform(
|
|
263
|
+
"rad", Semantics.LEVEL,
|
|
264
|
+
"DC voltage angle (Bθ formulation; reference node pinned to 0). Radians — "
|
|
265
|
+
"flow = susceptance · Δangle with susceptance = base_MVA / reactance.",
|
|
266
|
+
)
|
|
267
|
+
SLACK_CAP_MARGIN = Transform(
|
|
268
|
+
"MW", Semantics.LEVEL,
|
|
269
|
+
"Capacity-margin slack — per-period MW shortfall (not annualized).",
|
|
270
|
+
)
|
|
271
|
+
# Sentinel: an output that is a pure membership/index set (no measures).
|
|
272
|
+
DIMENSION_TABLE = Transform(
|
|
273
|
+
"", Semantics.DIMENSION,
|
|
274
|
+
"Membership / index set — no measure columns.",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass(frozen=True)
|
|
279
|
+
class ColumnMeta:
|
|
280
|
+
"""Derived metadata for one output column."""
|
|
281
|
+
|
|
282
|
+
name: str
|
|
283
|
+
unit: str
|
|
284
|
+
semantics: Semantics
|
|
285
|
+
tooltip: str # short, one-line.
|
|
286
|
+
long: str = "" # optional longer description (falls back to tooltip).
|
|
287
|
+
formula: str = ""
|
|
288
|
+
docs: str = ""
|
|
289
|
+
|
|
290
|
+
def as_dict(self) -> dict[str, str]:
|
|
291
|
+
return {
|
|
292
|
+
"unit": self.unit,
|
|
293
|
+
"semantics": self.semantics.value,
|
|
294
|
+
"tooltip": self.tooltip,
|
|
295
|
+
"long": self.long,
|
|
296
|
+
"formula": self.formula,
|
|
297
|
+
"docs": self.docs,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ── Per-column transform maps (for mixed-unit tables) ───────────────────────
|
|
302
|
+
# Some outputs hold columns of different units in one frame. Their
|
|
303
|
+
# OUTPUT_TRANSFORM value is a {column_label: Transform} map instead of a single
|
|
304
|
+
# Transform; derive_column_meta applies the per-column entry (columns absent
|
|
305
|
+
# from the map fall back to a '*' default if present, else dimension).
|
|
306
|
+
_NODEGROUP_GDT_P = {
|
|
307
|
+
'1. Loss of load': ENERGY_PERSTEP,
|
|
308
|
+
'2. VRE generation': ENERGY_PERSTEP,
|
|
309
|
+
'3. Excess load': ENERGY_PERSTEP,
|
|
310
|
+
'4. Curtailed VRE': ENERGY_PERSTEP,
|
|
311
|
+
# Raw inflow level at the timestep — no step_duration applied (an MW
|
|
312
|
+
# level, like nodeGroup/node dt flows), unlike the slack/VRE columns
|
|
313
|
+
# above which are integrated to MWh/step.
|
|
314
|
+
'5. Timestep inflow': RATE,
|
|
315
|
+
'6. Curtailed VRE of potential VRE': RATIO,
|
|
316
|
+
'7. Annualized inflow': PREENERGY_ANNUAL,
|
|
317
|
+
'8. VRE share of demand': RATIO,
|
|
318
|
+
}
|
|
319
|
+
_NODEGROUP_GD_P = {
|
|
320
|
+
'1. Loss of load share': RATIO,
|
|
321
|
+
'2. VRE share of demand': RATIO,
|
|
322
|
+
'3. Excess load share': RATIO,
|
|
323
|
+
'4. Curtailed VRE of demand': RATIO,
|
|
324
|
+
'5. Annualized inflow': PREENERGY_ANNUAL,
|
|
325
|
+
'6. Curtailed VRE of potential VRE': RATIO,
|
|
326
|
+
}
|
|
327
|
+
_FLOWGROUP_GD_P = {
|
|
328
|
+
'cumulative_flow': ENERGY_PERIOD, # MWh summed over the sample (not annualized)
|
|
329
|
+
'average_flow': AVERAGE_MW, # MW time-average
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ── Per-output declaration ──────────────────────────────────────────────────
|
|
334
|
+
# Map each output key to the transform its measure columns underwent. One line
|
|
335
|
+
# per output; the per-column expansion + tooltips are generated. Seeded with
|
|
336
|
+
# the high-traffic outputs; extend as coverage grows (the CI ratchet enforces
|
|
337
|
+
# that no NEW output lands here undocumented).
|
|
338
|
+
OUTPUT_TRANSFORM: "dict[str, Transform | dict[str, Transform]]" = {
|
|
339
|
+
# Costs
|
|
340
|
+
"costs_dt_p": MONEY_PERSTEP,
|
|
341
|
+
"annualized_costs_d_p": MONEY_ANNUAL,
|
|
342
|
+
"costs_discounted_d_p": MONEY_DISCOUNTED,
|
|
343
|
+
# Per-entity cost break-down (period level). Category column VALUES are
|
|
344
|
+
# the measures; the entity/period index levels auto-resolve to dimension.
|
|
345
|
+
"cost_unit_annualized_d_ec": COST_ENTITY_ANNUAL,
|
|
346
|
+
"cost_connection_annualized_d_ec": COST_ENTITY_ANNUAL,
|
|
347
|
+
"cost_node_annualized_d_ec": COST_ENTITY_ANNUAL,
|
|
348
|
+
"cost_unit_discounted_d_ec": COST_ENTITY_DISCOUNTED,
|
|
349
|
+
"cost_connection_discounted_d_ec": COST_ENTITY_DISCOUNTED,
|
|
350
|
+
"cost_node_discounted_d_ec": COST_ENTITY_DISCOUNTED,
|
|
351
|
+
# Unit / connection flows
|
|
352
|
+
"unit_outputNode_dt_ee": RATE,
|
|
353
|
+
"unit_inputNode_dt_ee": RATE,
|
|
354
|
+
"unit_outputNode_d_ee": ENERGY_ANNUAL,
|
|
355
|
+
"unit_inputNode_d_ee": ENERGY_ANNUAL,
|
|
356
|
+
"connection_dt_eee": RATE,
|
|
357
|
+
"connection_d_eee": ENERGY_ANNUAL,
|
|
358
|
+
"connection_leftward_dt_eee": RATE,
|
|
359
|
+
"connection_rightward_dt_eee": RATE,
|
|
360
|
+
"connection_losses_dt_eee": RATE,
|
|
361
|
+
"connection_leftward_d_eee": ENERGY_ANNUAL,
|
|
362
|
+
"connection_rightward_d_eee": ENERGY_ANNUAL,
|
|
363
|
+
"connection_losses_d_eee": ENERGY_ANNUAL,
|
|
364
|
+
# CO2
|
|
365
|
+
"CO2_d_g": EMISSION_ANNUAL,
|
|
366
|
+
"process_co2_d_eee": EMISSION_ANNUAL,
|
|
367
|
+
"CO2__": EMISSION_MT,
|
|
368
|
+
# Node / group flows
|
|
369
|
+
"nodeGroup_flows_d_g": PREENERGY_ANNUAL,
|
|
370
|
+
"nodeGroup_flows_d_gpe": PREENERGY_ANNUAL,
|
|
371
|
+
# dt group/node flows show the un-integrated MW level (no step_duration —
|
|
372
|
+
# relatable to installed capacity), per maintainer convention. Only
|
|
373
|
+
# sum_flow_t (gpe) integrates the flow columns to MWh/step.
|
|
374
|
+
"nodeGroup_flows_dt_g": RATE,
|
|
375
|
+
"nodeGroup_flows_dt_gpe": ENERGY_PERSTEP,
|
|
376
|
+
"node_inflow__dt": RATE,
|
|
377
|
+
"node_state_dt_e": STORAGE_STATE,
|
|
378
|
+
# VRE shares + curtailment/potential. NOTE: the *_d_ee curtailment and
|
|
379
|
+
# potential are SUM-ONLY over the sample (per_period), NOT annualized —
|
|
380
|
+
# unlike unit_outputNode_d_ee (see Task #4 / Stage-1 finding).
|
|
381
|
+
"nodeGroup_VRE_share_d_g": RATIO,
|
|
382
|
+
"nodeGroup_VRE_share_dt_g": RATIO,
|
|
383
|
+
"unit_VRE_potential_outputNode_dt_ee": RATE,
|
|
384
|
+
"unit_curtailment_outputNode_dt_ee": RATE,
|
|
385
|
+
"unit_VRE_potential_outputNode_d_ee": ENERGY_PERIOD,
|
|
386
|
+
"unit_curtailment_outputNode_d_ee": ENERGY_PERIOD,
|
|
387
|
+
"unit_curtailment_share_outputNode_dt_ee": RATIO,
|
|
388
|
+
"unit_curtailment_share_outputNode_d_ee": RATIO,
|
|
389
|
+
# Ramps
|
|
390
|
+
"unit_ramp_inputs_dt_ee": RAMP,
|
|
391
|
+
"unit_ramp_outputs_dt_ee": RAMP,
|
|
392
|
+
# Node slacks (dt = MW at the timestep; d = annualized MWh/a — out_node:120-126)
|
|
393
|
+
"node_slack_up_dt_e": RATE,
|
|
394
|
+
"node_slack_down_dt_e": RATE,
|
|
395
|
+
"node_slack_up_d_e": ENERGY_ANNUAL,
|
|
396
|
+
"node_slack_down_d_e": ENERGY_ANNUAL,
|
|
397
|
+
"nodeGroup_slack_nonsync_dt_g": RATE,
|
|
398
|
+
"nodeGroup_slack_nonsync_d_g": ENERGY_ANNUAL,
|
|
399
|
+
# Capacity (a stock, not a flow)
|
|
400
|
+
"unit_capacity_ed_p": CAPACITY_MW,
|
|
401
|
+
"connection_capacity_ed_p": CAPACITY_MW,
|
|
402
|
+
"node_capacity_ed_p": CAPACITY_MWH,
|
|
403
|
+
# Costs / factors
|
|
404
|
+
"costs_discounted_p_": MONEY_DISCOUNTED,
|
|
405
|
+
"discountFactors_d_p": FACTOR,
|
|
406
|
+
"entity_annuity_d_p": ANNUITY,
|
|
407
|
+
"years_represented__d": YEARS,
|
|
408
|
+
# Start-ups
|
|
409
|
+
"unit_startup_d_e": COUNT_ANNUAL,
|
|
410
|
+
# Prices / duals
|
|
411
|
+
"node_prices_dt_e": PRICE_ENERGY,
|
|
412
|
+
"reserve_prices_dt_ppg": PRICE_RESERVE,
|
|
413
|
+
"co2_price_period_d_g": PRICE_CO2,
|
|
414
|
+
"co2_price_total_d_g": PRICE_CO2,
|
|
415
|
+
"dual_invest_effective_unit_d_e": DUAL_INVEST_KW,
|
|
416
|
+
"dual_invest_effective_connection_d_e": DUAL_INVEST_KW,
|
|
417
|
+
"dual_invest_effective_node_d_e": DUAL_INVEST_KWH,
|
|
418
|
+
# Inertia (MW·s) + reserve provision + online counts
|
|
419
|
+
"nodeGroup_inertia_dt_g": INERTIA,
|
|
420
|
+
"nodeGroup_unit_node_inertia_dt_gee": INERTIA,
|
|
421
|
+
"nodeGroup_inertia_largest_flow_dt_g": RATE,
|
|
422
|
+
"nodeGroup_slack_inertia_dt_g": INERTIA,
|
|
423
|
+
"nodeGroup_slack_inertia_d_g": INERTIA_ANNUAL,
|
|
424
|
+
"process_reserve_upDown_node_dt_eppe": RATE,
|
|
425
|
+
"process_reserve_average_d_eppe": RESERVE_AVG,
|
|
426
|
+
"dc_angle_dt_e": ANGLE,
|
|
427
|
+
"dc_angle_diff_dt_e": ANGLE,
|
|
428
|
+
"unit_online_dt_e": ONLINE_COUNT,
|
|
429
|
+
"unit_online_average_d_e": ONLINE_AVG,
|
|
430
|
+
"flowGroup_gd_t": RATE,
|
|
431
|
+
"nodeGroup_slack_reserve_dt_eeg": RATE,
|
|
432
|
+
"nodeGroup_slack_reserve_d_eeg": ENERGY_ANNUAL,
|
|
433
|
+
"nodeGroup_slack_capacity_margin_d_g": SLACK_CAP_MARGIN,
|
|
434
|
+
"nodeGroup_total_inflow": PREENERGY_ANNUAL,
|
|
435
|
+
# Node balance tables. _dt_ep is the un-integrated MW level (assembled
|
|
436
|
+
# from raw flow_dt / inflow / slacks with no step_duration, like the dt
|
|
437
|
+
# group flows); _d_ep integrates + annualizes to MWh/a.
|
|
438
|
+
"node_dt_ep": RATE,
|
|
439
|
+
"node_d_ep": ENERGY_ANNUAL,
|
|
440
|
+
# Mixed-unit tables — per-column transform maps.
|
|
441
|
+
"nodeGroup_gdt_p": _NODEGROUP_GDT_P,
|
|
442
|
+
"nodeGroup_gd_p": _NODEGROUP_GD_P,
|
|
443
|
+
"flowGroup_gd_p": _FLOWGROUP_GD_P,
|
|
444
|
+
# Pure membership / index sets (no measure columns)
|
|
445
|
+
"group_node": DIMENSION_TABLE,
|
|
446
|
+
"group_process": DIMENSION_TABLE,
|
|
447
|
+
"group_process_node": DIMENSION_TABLE,
|
|
448
|
+
"nodeGroupDispatch": DIMENSION_TABLE,
|
|
449
|
+
"nodeGroupIndicators": DIMENSION_TABLE,
|
|
450
|
+
"flowGroupIndicators": DIMENSION_TABLE,
|
|
451
|
+
"connection_dc_power_flow": DIMENSION_TABLE,
|
|
452
|
+
"node_dc_power_flow": DIMENSION_TABLE,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Per-(output, column) formula overrides — only where a bespoke derivation
|
|
456
|
+
# string helps more than the transform's generic tooltip.
|
|
457
|
+
FORMULA_OVERRIDE: dict[tuple[str, str], str] = {
|
|
458
|
+
("annualized_costs_d_p", "commodity_cost"):
|
|
459
|
+
"Σₜ(costs__dt.commodity_cost) / complete_period_share_of_year / 1e6",
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
DOCS_ANCHOR: dict[str, str] = {
|
|
463
|
+
"annualized_costs_d_p": "results.html#costs",
|
|
464
|
+
"costs_dt_p": "results.html#costs",
|
|
465
|
+
"cost_unit_annualized_d_ec": "results.html#cost-by-entity",
|
|
466
|
+
"cost_connection_annualized_d_ec": "results.html#cost-by-entity",
|
|
467
|
+
"cost_node_annualized_d_ec": "results.html#cost-by-entity",
|
|
468
|
+
"cost_unit_discounted_d_ec": "results.html#cost-by-entity",
|
|
469
|
+
"cost_connection_discounted_d_ec": "results.html#cost-by-entity",
|
|
470
|
+
"cost_node_discounted_d_ec": "results.html#cost-by-entity",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def is_dimension(column: str) -> bool:
|
|
475
|
+
"""True if ``column`` is an index/key dimension rather than a measure."""
|
|
476
|
+
return column in DIMENSION_COLUMNS
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def result_key_summary(result_key: str) -> "tuple[str, str, str] | None":
|
|
480
|
+
"""Output-level ``(unit, semantics, tooltip)`` for a result key.
|
|
481
|
+
|
|
482
|
+
For a uniform output this is its transform's fields; for a mixed-unit
|
|
483
|
+
table the unit lists the distinct measure units and semantics is
|
|
484
|
+
``"mixed"``. Returns ``None`` for an undeclared output or a pure
|
|
485
|
+
membership/index set (nothing to annotate). Intended for UI hovers
|
|
486
|
+
(e.g. the result viewer's plot-tree tooltip), where ``result_key`` is the
|
|
487
|
+
plot variant's key.
|
|
488
|
+
"""
|
|
489
|
+
spec = OUTPUT_TRANSFORM.get(result_key)
|
|
490
|
+
if spec is None:
|
|
491
|
+
return None
|
|
492
|
+
if isinstance(spec, dict):
|
|
493
|
+
units = sorted({t.unit for t in spec.values() if t.unit})
|
|
494
|
+
sems = sorted({t.semantics.value for t in spec.values()
|
|
495
|
+
if t.semantics is not Semantics.DIMENSION})
|
|
496
|
+
if not sems:
|
|
497
|
+
return None
|
|
498
|
+
return ("; ".join(units) if units else "ratio",
|
|
499
|
+
"mixed" if len(sems) > 1 else sems[0],
|
|
500
|
+
"Mixed-unit table — units vary by column.")
|
|
501
|
+
if spec.semantics is Semantics.DIMENSION:
|
|
502
|
+
return None
|
|
503
|
+
return (spec.unit or "ratio", spec.semantics.value, spec.tooltip)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def result_variant_summary(
|
|
507
|
+
result_key: str, variant_letter: str
|
|
508
|
+
) -> "tuple[str, str, str] | None":
|
|
509
|
+
"""Variant-adjusted ``(unit, semantics, tooltip)`` for a plot variant.
|
|
510
|
+
|
|
511
|
+
The ``a`` (sum-over-periods → total) and ``w`` (weekly chunk
|
|
512
|
+
aggregation) plot variants are *on-the-fly* aggregations layered on the
|
|
513
|
+
same base ``result_key`` as the ``p``/``h`` variants — they have no
|
|
514
|
+
output of their own in :data:`OUTPUT_TRANSFORM`. So their unit and
|
|
515
|
+
semantics differ from the base summary and must be derived here:
|
|
516
|
+
|
|
517
|
+
* ``'a'`` — the variant totals the base over the whole horizon. An
|
|
518
|
+
annual-rate base unit (suffix ``/a``, e.g. ``M CUR/a``, ``MWh/a``)
|
|
519
|
+
becomes the absolute total (``M CUR``, ``MWh``); a base unit without
|
|
520
|
+
``/a`` (e.g. a discounted ``M CUR``) is already absolute and stays as
|
|
521
|
+
is. Semantics become ``"total"`` (a horizon total, *not* annual).
|
|
522
|
+
* ``'w'`` — weekly chunk aggregation keeps the base/``h`` unit unchanged;
|
|
523
|
+
semantics become ``"weekly"``.
|
|
524
|
+
* any other letter (``'p'``, ``'h'``, …) — the base summary unchanged.
|
|
525
|
+
|
|
526
|
+
Returns ``None`` when the base output is undeclared or a pure
|
|
527
|
+
membership/index set (i.e. when :func:`result_key_summary` returns
|
|
528
|
+
``None``), so callers can skip it exactly as they do for the base.
|
|
529
|
+
Pure and GUI-free; the single source for variant-aware UI hovers and
|
|
530
|
+
plot value-axis units.
|
|
531
|
+
"""
|
|
532
|
+
base = result_key_summary(result_key)
|
|
533
|
+
if base is None:
|
|
534
|
+
return None
|
|
535
|
+
unit, semantics, desc = base
|
|
536
|
+
if variant_letter == 'a':
|
|
537
|
+
if unit and unit.endswith('/a'):
|
|
538
|
+
unit = unit[:-2]
|
|
539
|
+
return (unit, "total", desc)
|
|
540
|
+
if variant_letter == 'w':
|
|
541
|
+
return (unit, "weekly", desc)
|
|
542
|
+
return base
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def output_metadata_rows(output_key: str, columns) -> list[dict[str, str]]:
|
|
546
|
+
"""Flat ``[{output, column, unit, semantics, tooltip, formula}, …]`` rows
|
|
547
|
+
for the *measure* columns of one output (dimension columns dropped).
|
|
548
|
+
|
|
549
|
+
Single source for the tabular renderers that list metadata in a side
|
|
550
|
+
table (the Excel ``_output_metadata`` sheet). Returns ``[]`` for an
|
|
551
|
+
undeclared output or a pure membership/index set.
|
|
552
|
+
"""
|
|
553
|
+
meta = derive_column_meta(output_key, columns)
|
|
554
|
+
if not meta:
|
|
555
|
+
return []
|
|
556
|
+
rows: list[dict[str, str]] = []
|
|
557
|
+
for col in columns:
|
|
558
|
+
cm = meta.get(str(col))
|
|
559
|
+
if cm is None or cm.semantics is Semantics.DIMENSION:
|
|
560
|
+
continue
|
|
561
|
+
rows.append({
|
|
562
|
+
"output": output_key,
|
|
563
|
+
"column": cm.name,
|
|
564
|
+
"unit": cm.unit,
|
|
565
|
+
"semantics": cm.semantics.value,
|
|
566
|
+
"tooltip": cm.tooltip,
|
|
567
|
+
"long": cm.long,
|
|
568
|
+
"formula": cm.formula,
|
|
569
|
+
})
|
|
570
|
+
return rows
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def datapackage_resource(
|
|
574
|
+
output_key: str, path: str, index_names, measure_columns,
|
|
575
|
+
) -> dict:
|
|
576
|
+
"""A Frictionless ``tabular-data-resource`` describing one written CSV.
|
|
577
|
+
|
|
578
|
+
``index_names`` are the (reset-to-column) dimension levels and
|
|
579
|
+
``measure_columns`` the value columns — together the CSV's header. Each
|
|
580
|
+
field carries its ``type`` and, for measures, the derived ``unit`` /
|
|
581
|
+
semantics / description / formula (under ``flextool:`` keys so the
|
|
582
|
+
descriptor stays valid Frictionless). Always returns a resource (an
|
|
583
|
+
undeclared output simply yields string/dimension fields), so the sidecar
|
|
584
|
+
documents the full ``output_csv/`` tree, declared or not.
|
|
585
|
+
"""
|
|
586
|
+
meta = derive_column_meta(output_key, measure_columns) or {}
|
|
587
|
+
fields: list[dict[str, str]] = []
|
|
588
|
+
seen: set[str] = set()
|
|
589
|
+
|
|
590
|
+
def _dimension_field(name: str) -> dict[str, str]:
|
|
591
|
+
return {"name": name, "type": "string", "flextool:semantics": "dimension"}
|
|
592
|
+
|
|
593
|
+
for name in index_names:
|
|
594
|
+
if name is None:
|
|
595
|
+
continue
|
|
596
|
+
key = str(name)
|
|
597
|
+
if key in seen:
|
|
598
|
+
continue
|
|
599
|
+
seen.add(key)
|
|
600
|
+
fields.append(_dimension_field(key))
|
|
601
|
+
|
|
602
|
+
for col in measure_columns:
|
|
603
|
+
name = str(col)
|
|
604
|
+
if name in seen:
|
|
605
|
+
continue
|
|
606
|
+
seen.add(name)
|
|
607
|
+
cm = meta.get(name)
|
|
608
|
+
if cm is None or cm.semantics is Semantics.DIMENSION:
|
|
609
|
+
fields.append(_dimension_field(name))
|
|
610
|
+
continue
|
|
611
|
+
spec: dict[str, str] = {
|
|
612
|
+
"name": name,
|
|
613
|
+
"type": "number",
|
|
614
|
+
"flextool:semantics": cm.semantics.value,
|
|
615
|
+
}
|
|
616
|
+
if cm.unit:
|
|
617
|
+
spec["unit"] = cm.unit
|
|
618
|
+
# Frictionless `description` carries the longer form when present.
|
|
619
|
+
if cm.long or cm.tooltip:
|
|
620
|
+
spec["description"] = cm.long or cm.tooltip
|
|
621
|
+
if cm.formula:
|
|
622
|
+
spec["flextool:formula"] = cm.formula
|
|
623
|
+
if cm.docs:
|
|
624
|
+
spec["flextool:docs"] = cm.docs
|
|
625
|
+
fields.append(spec)
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
"name": output_key.replace("_", "-").strip("-").lower() or "resource",
|
|
629
|
+
"path": path,
|
|
630
|
+
"profile": "tabular-data-resource",
|
|
631
|
+
"schema": {"fields": fields},
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def derive_column_meta(
|
|
636
|
+
output_key: str, columns,
|
|
637
|
+
) -> dict[str, ColumnMeta] | None:
|
|
638
|
+
"""Derive ``{column_name: ColumnMeta}`` for one output.
|
|
639
|
+
|
|
640
|
+
``columns`` is the iterable of *leaf* column names (e.g. cost categories),
|
|
641
|
+
typically ``df.columns`` before any scenario-level wrapping. Dimension
|
|
642
|
+
columns get ``Semantics.DIMENSION``; measure columns get the output's
|
|
643
|
+
declared transform. The declared value may be a single ``Transform``
|
|
644
|
+
(uniform) or a ``{column_label: Transform}`` map (mixed-unit tables, with
|
|
645
|
+
an optional ``'*'`` default). Returns ``None`` when the output is not (yet)
|
|
646
|
+
declared in :data:`OUTPUT_TRANSFORM`, so callers can treat absence as
|
|
647
|
+
"no metadata".
|
|
648
|
+
"""
|
|
649
|
+
spec = OUTPUT_TRANSFORM.get(output_key)
|
|
650
|
+
if spec is None:
|
|
651
|
+
return None
|
|
652
|
+
docs = DOCS_ANCHOR.get(output_key, "")
|
|
653
|
+
out: dict[str, ColumnMeta] = {}
|
|
654
|
+
for col in columns:
|
|
655
|
+
name = str(col)
|
|
656
|
+
tf = spec.get(name, spec.get('*')) if isinstance(spec, dict) else spec
|
|
657
|
+
# Index/key dimension, a pure membership set, or a column absent from a
|
|
658
|
+
# mixed-table map → dimension.
|
|
659
|
+
if tf is None or tf.semantics is Semantics.DIMENSION or is_dimension(name):
|
|
660
|
+
out[name] = ColumnMeta(name, "", Semantics.DIMENSION,
|
|
661
|
+
"Index / key dimension.")
|
|
662
|
+
continue
|
|
663
|
+
out[name] = ColumnMeta(
|
|
664
|
+
name=name,
|
|
665
|
+
unit=tf.unit,
|
|
666
|
+
semantics=tf.semantics,
|
|
667
|
+
tooltip=tf.tooltip,
|
|
668
|
+
long=tf.description,
|
|
669
|
+
formula=FORMULA_OVERRIDE.get((output_key, name), ""),
|
|
670
|
+
docs=docs,
|
|
671
|
+
)
|
|
672
|
+
return out
|