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,1009 @@
|
|
|
1
|
+
"""``FlexData.dump_csvs(workdir)`` — debug oracle that writes a
|
|
2
|
+
materialised ``FlexData`` back to flextool's ``input/`` + ``solve_data/``
|
|
3
|
+
CSV layout.
|
|
4
|
+
|
|
5
|
+
**Use case.** When the DB-direct loader's output for some Param
|
|
6
|
+
diverges from what flextool's preprocessing would have produced, this
|
|
7
|
+
helper lets a developer dump both views to disk and ``diff`` them.
|
|
8
|
+
The DB-loaded ``FlexData`` is the in-memory canonical;
|
|
9
|
+
``dump_csvs(tmpdir)`` materialises that canonical to CSV so a
|
|
10
|
+
side-by-side ``diff -r tmpdir tests/data/work_<fixture>`` surfaces the
|
|
11
|
+
divergence at file granularity.
|
|
12
|
+
|
|
13
|
+
**Round-trip contract.** After::
|
|
14
|
+
|
|
15
|
+
data = load_flextool(workdir, db_reader=reader)
|
|
16
|
+
data.dump_csvs(tmp)
|
|
17
|
+
redo = load_flextool(tmp)
|
|
18
|
+
|
|
19
|
+
every ``Param`` / ``DataFrame`` field on ``data`` and ``redo`` should
|
|
20
|
+
compare frame-equal up to row order. This is the basis of
|
|
21
|
+
:func:`tests.test_flex_dump_csvs_roundtrip.test_dump_csvs_roundtrip`.
|
|
22
|
+
|
|
23
|
+
**Scope.** The mapping covers every FlexData field that
|
|
24
|
+
``flextool/input.py`` reads from a single canonical CSV via a
|
|
25
|
+
direct-rename (the bulk of the surface). A handful of Params today
|
|
26
|
+
read from sliced wide-by-param files (``pdtNode.csv``,
|
|
27
|
+
``pdtCommodity.csv``, ``pdtGroup.csv``, ``pdtProcess.csv``,
|
|
28
|
+
``pd_group.csv``, ``p_commodity.csv``, ``p_node.csv``, ``p_process.csv``,
|
|
29
|
+
``p_group.csv``) — those slice files we reconstruct by writing the
|
|
30
|
+
sliced rows back into a wide-by-param file (one row per (entity,
|
|
31
|
+
param, …)). Where flextool emits both an ``input/`` and a
|
|
32
|
+
``solve_data/`` copy of the same logical CSV, ``dump_csvs`` writes
|
|
33
|
+
to ``solve_data/`` (the canonical post-preprocessing location) and
|
|
34
|
+
also to ``input/`` when ``input.py`` reads from there for that field.
|
|
35
|
+
|
|
36
|
+
**Not in scope.** Per-solve metadata (``solve_current.csv``,
|
|
37
|
+
``period_first_of_solve.csv``, ``solve_mode.csv``, etc.) — these are
|
|
38
|
+
flextool-runner state, not FlexData fields, and the round-trip path
|
|
39
|
+
doesn't need them when the source ``workdir`` is also handed to
|
|
40
|
+
``load_flextool`` (we can copy them in advance). ``dump_csvs`` will
|
|
41
|
+
copy them through if a ``copy_meta_from`` argument is given (used by
|
|
42
|
+
the round-trip test).
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import os
|
|
47
|
+
import shutil
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import TYPE_CHECKING, Any
|
|
50
|
+
|
|
51
|
+
import polars as pl
|
|
52
|
+
|
|
53
|
+
from polar_high import Param
|
|
54
|
+
|
|
55
|
+
from ._axis_enums import rename_to_axis, schema_dtype
|
|
56
|
+
from ._param_shapes import promote_param_to_dt
|
|
57
|
+
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
from flextool.engine_polars.input import FlexData
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Heavy CSVs — gigabyte-scale on large fixtures (e.g. y2050 H2 supply
|
|
64
|
+
# curves: ~3.8 GB total per cascade iteration). These files are NOT
|
|
65
|
+
# consumed by ``flextool/process_outputs/`` or ``flextool/plot_outputs/``
|
|
66
|
+
# post-processing; they exist as a debug oracle for diff-roundtripping
|
|
67
|
+
# the in-memory FlexData against the on-disk CSV layout. The cascade's
|
|
68
|
+
# downstream consumers all read from ``flex_data.<field>.frame``
|
|
69
|
+
# in-memory.
|
|
70
|
+
#
|
|
71
|
+
# Set ``FLEXTOOL_DUMP_CSVS=1`` to restore the debug-oracle behaviour
|
|
72
|
+
# (write the full ~50 CSVs). When the env var is unset (default),
|
|
73
|
+
# these seven files are skipped — every other file in ``DIRECT_WRITES``
|
|
74
|
+
# and every sliced / metadata file continues to be emitted.
|
|
75
|
+
_HEAVY_CSV_FILES: frozenset[str] = frozenset({
|
|
76
|
+
"p_flow_max.csv",
|
|
77
|
+
"pdtProcess__source__sink__dt_varCost.csv",
|
|
78
|
+
"pdtProcess_slope.csv",
|
|
79
|
+
"pdtProcess.csv",
|
|
80
|
+
"pdtNode.csv",
|
|
81
|
+
"pdtNodeInflow.csv",
|
|
82
|
+
"ptNode_inflow.csv",
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _heavy_dumps_enabled() -> bool:
|
|
87
|
+
"""Return True iff the heavy CSV writes are gated on (debug oracle).
|
|
88
|
+
|
|
89
|
+
The default is OFF: the seven gigabyte-scale CSVs in
|
|
90
|
+
:data:`_HEAVY_CSV_FILES` are not written by ``dump_csvs`` unless
|
|
91
|
+
``FLEXTOOL_DUMP_CSVS=1`` is set in the environment.
|
|
92
|
+
"""
|
|
93
|
+
return os.environ.get("FLEXTOOL_DUMP_CSVS") == "1"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Field → CSV mapping.
|
|
98
|
+
#
|
|
99
|
+
# One entry per FlexData field we know how to write. Each entry is one of:
|
|
100
|
+
#
|
|
101
|
+
# * ``("solve_data", "<file>.csv", {flex_col: csv_col, ...})`` — direct
|
|
102
|
+
# write of the field's frame, applying the rename (renaming the
|
|
103
|
+
# short names back to flextool's long names).
|
|
104
|
+
#
|
|
105
|
+
# * ``("input", "<file>.csv", {...})`` — same, but to ``input/``.
|
|
106
|
+
#
|
|
107
|
+
# * ``("slice", kind, file, entity_col, param_value)`` — write into a
|
|
108
|
+
# wide-by-param file (e.g. ``pdtNode.csv`` rows with
|
|
109
|
+
# ``param=penalty_up``). The dump aggregates all slice rows for a
|
|
110
|
+
# file across multiple FlexData fields in a single pass.
|
|
111
|
+
#
|
|
112
|
+
# Unknown fields (None on the FlexData) are skipped silently — the
|
|
113
|
+
# round-trip semantics are "every populated field round-trips"; we
|
|
114
|
+
# never fabricate empty CSVs.
|
|
115
|
+
|
|
116
|
+
# Direct frame → CSV writes (set & param fields). Format:
|
|
117
|
+
# field_name -> (kind, csv_filename, rename_to_csv)
|
|
118
|
+
DIRECT_WRITES: dict[str, tuple[str, str, dict[str, str]]] = {
|
|
119
|
+
# ─── Time / weighting ─────────────────────────────────────────────
|
|
120
|
+
"p_step_duration": ("solve_data", "steps_in_use.csv",
|
|
121
|
+
{"d": "period", "t": "step", "value": "step_duration"}),
|
|
122
|
+
"p_timestep_weight": ("solve_data", "timestep_weight.csv",
|
|
123
|
+
{"d": "period", "t": "time", "value": "weight"}),
|
|
124
|
+
"p_inflation_op": ("solve_data", "p_inflation_factor_operations_yearly.csv",
|
|
125
|
+
{"d": "period", "value": "value"}),
|
|
126
|
+
"p_period_share": ("solve_data", "complete_period_share_of_year_calc.csv",
|
|
127
|
+
{"d": "period", "value": "value"}),
|
|
128
|
+
# ─── Nodes ────────────────────────────────────────────────────────
|
|
129
|
+
"nodeBalance": ("solve_data", "nodeBalance.csv", {"n": "node"}),
|
|
130
|
+
"p_inflow": ("solve_data", "pdtNodeInflow.csv",
|
|
131
|
+
{"n": "node", "d": "period", "t": "time", "value": "value"}),
|
|
132
|
+
# ─── Process topology ────────────────────────────────────────────
|
|
133
|
+
"process_source_sink": ("solve_data", "process_source_sink.csv",
|
|
134
|
+
{"p": "process"}),
|
|
135
|
+
"process_source_sink_eff": ("solve_data", "process_source_sink_eff.csv",
|
|
136
|
+
{"p": "process"}),
|
|
137
|
+
"process_source_sink_noEff": ("solve_data", "process_source_sink_noEff.csv",
|
|
138
|
+
{"p": "process"}),
|
|
139
|
+
"p_unitsize": ("solve_data", "p_entity_unitsize.csv",
|
|
140
|
+
{"p": "entity", "value": "value"}),
|
|
141
|
+
# p_flow_upper -> p_flow_max (long form: process, source, sink, period, time, value)
|
|
142
|
+
"p_flow_upper": ("solve_data", "p_flow_max.csv",
|
|
143
|
+
{"p": "process", "d": "period", "t": "time"}),
|
|
144
|
+
"p_slope": ("solve_data", "pdtProcess_slope.csv",
|
|
145
|
+
{"p": "process", "d": "period", "t": "time"}),
|
|
146
|
+
# ─── CO2 cap / price ─────────────────────────────────────────────
|
|
147
|
+
"group_co2_max_period": ("solve_data", "group_co2_max_period.csv",
|
|
148
|
+
{"g": "group"}),
|
|
149
|
+
"group_co2_max_total": ("solve_data", "group_co2_max_total.csv",
|
|
150
|
+
{"g": "group"}),
|
|
151
|
+
# ─── User-defined flow constraints ───────────────────────────────
|
|
152
|
+
# ─── Profiles ────────────────────────────────────────────────────
|
|
153
|
+
# ─── Invest / divest sets ────────────────────────────────────────
|
|
154
|
+
"ed_invest_set": ("solve_data", "ed_invest.csv",
|
|
155
|
+
{"e": "entity", "d": "period"}),
|
|
156
|
+
"ed_divest_set": ("solve_data", "ed_divest.csv",
|
|
157
|
+
{"e": "entity", "d": "period"}),
|
|
158
|
+
"pd_invest_set": ("solve_data", "pd_invest.csv",
|
|
159
|
+
{"p": "process", "d": "period"}),
|
|
160
|
+
"pd_divest_set": ("solve_data", "pd_divest.csv",
|
|
161
|
+
{"p": "process", "d": "period"}),
|
|
162
|
+
"nd_invest_set": ("solve_data", "nd_invest.csv",
|
|
163
|
+
{"n": "node", "d": "period"}),
|
|
164
|
+
"nd_divest_set": ("solve_data", "nd_divest.csv",
|
|
165
|
+
{"n": "node", "d": "period"}),
|
|
166
|
+
"edd_invest_set": ("solve_data", "edd_invest.csv",
|
|
167
|
+
{"e": "entity", "d_invest": "d_invest", "d": "period"}),
|
|
168
|
+
"e_invest_total": ("solve_data", "e_invest_total.csv",
|
|
169
|
+
{"e": "entity"}),
|
|
170
|
+
"e_divest_total": ("solve_data", "e_divest_total.csv",
|
|
171
|
+
{"e": "entity"}),
|
|
172
|
+
# ─── Online (UC) sets ────────────────────────────────────────────
|
|
173
|
+
"process_online": ("solve_data", "process_online.csv", {"p": "process"}),
|
|
174
|
+
"process_online_linear": ("solve_data", "process_online_linear.csv",
|
|
175
|
+
{"p": "process"}),
|
|
176
|
+
"process_online_integer": ("solve_data", "process_online_integer.csv",
|
|
177
|
+
{"p": "process"}),
|
|
178
|
+
"process_minload": ("solve_data", "process_minload.csv", {"p": "process"}),
|
|
179
|
+
# ─── Storage ──────────────────────────────────────────────────────
|
|
180
|
+
"nodeState": ("solve_data", "nodeState.csv", {"n": "node"}),
|
|
181
|
+
"nodeStateBlock": ("solve_data", "nodeStateBlock.csv", {"n": "node"}),
|
|
182
|
+
# ─── Variable cost ────────────────────────────────────────────────
|
|
183
|
+
"p_pssdt_varCost": ("solve_data", "pdtProcess__source__sink__dt_varCost.csv",
|
|
184
|
+
{"p": "process", "d": "period", "t": "time",
|
|
185
|
+
"value": "value"}),
|
|
186
|
+
# ─── Fixed cost / scaling ─────────────────────────────────────────
|
|
187
|
+
"p_ed_fixed_cost": ("solve_data", "ed_fixed_cost.csv",
|
|
188
|
+
{"e": "entity", "d": "period"}),
|
|
189
|
+
"p_entity_all_existing": ("solve_data", "p_entity_all_existing.csv",
|
|
190
|
+
{"e": "entity", "d": "period"}),
|
|
191
|
+
"p_node_capacity_for_scaling": ("solve_data", "node_capacity_for_scaling.csv",
|
|
192
|
+
{"n": "node", "d": "period"}),
|
|
193
|
+
# ─── DC power flow ────────────────────────────────────────────────
|
|
194
|
+
"node_dc_power_flow": ("input", "node_dc_power_flow.csv", {"n": "node"}),
|
|
195
|
+
"connection_dc_power_flow": ("input", "connection_dc_power_flow.csv",
|
|
196
|
+
{"p": "connection"}),
|
|
197
|
+
"node_reference_angle": ("input", "node_reference_angle.csv", {"n": "node"}),
|
|
198
|
+
# ─── Stochastics ──────────────────────────────────────────────────
|
|
199
|
+
"pdt_branch_weight": ("solve_data", "pdt_branch_weight.csv",
|
|
200
|
+
{"d": "period", "t": "time", "value": "value"}),
|
|
201
|
+
"pd_branch_weight": ("solve_data", "pd_branch_weight.csv",
|
|
202
|
+
{"d": "period", "value": "value"}),
|
|
203
|
+
"period_in_use_set": ("solve_data", "period_in_use_set.csv",
|
|
204
|
+
{"d": "period"}),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _frame_of(value: Any) -> pl.DataFrame | None:
|
|
209
|
+
"""Return the eager polars frame for a Param or DataFrame value.
|
|
210
|
+
Returns None when the value is None or empty.
|
|
211
|
+
"""
|
|
212
|
+
if value is None:
|
|
213
|
+
return None
|
|
214
|
+
if isinstance(value, Param):
|
|
215
|
+
f = value.frame
|
|
216
|
+
elif isinstance(value, pl.DataFrame):
|
|
217
|
+
f = value
|
|
218
|
+
elif isinstance(value, pl.LazyFrame):
|
|
219
|
+
f = value.collect()
|
|
220
|
+
else:
|
|
221
|
+
return None
|
|
222
|
+
if f.height == 0:
|
|
223
|
+
return None
|
|
224
|
+
return f
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _frame_of_pdt(value: Any, dt: pl.DataFrame | None) -> pl.DataFrame | None:
|
|
228
|
+
"""Return an eager (entity, d, t, value)-shaped frame for a Param.
|
|
229
|
+
|
|
230
|
+
Phase E.1: flex_data Params keep their authored dims (``(entity,)``
|
|
231
|
+
/ ``(entity, d)`` / ``(entity, t)`` / ``(entity, d, t)``). The CSV
|
|
232
|
+
dump path needs the fully-expanded (entity, d, t) frame so the
|
|
233
|
+
output CSVs match the pre-Phase-E layout that downstream consumers
|
|
234
|
+
(GUI inspection, parity tests) expect. Promote via lazy joins on
|
|
235
|
+
``dt`` when the Param is narrower than (entity, d, t).
|
|
236
|
+
"""
|
|
237
|
+
if value is None or dt is None or dt.height == 0:
|
|
238
|
+
return _frame_of(value)
|
|
239
|
+
if isinstance(value, Param):
|
|
240
|
+
f = promote_param_to_dt(value, dt).collect()
|
|
241
|
+
elif isinstance(value, pl.DataFrame):
|
|
242
|
+
f = value
|
|
243
|
+
elif isinstance(value, pl.LazyFrame):
|
|
244
|
+
f = value.collect()
|
|
245
|
+
else:
|
|
246
|
+
return None
|
|
247
|
+
if f.height == 0:
|
|
248
|
+
return None
|
|
249
|
+
return f
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _write_frame(frame: pl.DataFrame, path: Path,
|
|
253
|
+
rename: dict[str, str]) -> None:
|
|
254
|
+
"""Apply ``rename`` (flex columns → CSV columns), reorder columns
|
|
255
|
+
so the rename-target order matches the original ``rename`` dict
|
|
256
|
+
(insertion order = canonical column order), and write CSV.
|
|
257
|
+
Columns NOT in the rename map stay in their relative position
|
|
258
|
+
after the renamed ones.
|
|
259
|
+
"""
|
|
260
|
+
df = frame
|
|
261
|
+
# Apply the rename only for present columns.
|
|
262
|
+
eff_rename = {k: v for k, v in rename.items() if k in df.columns}
|
|
263
|
+
if eff_rename:
|
|
264
|
+
df = df.pipe(rename_to_axis, eff_rename)
|
|
265
|
+
# Order: rename targets first (in dict order), then any remaining cols.
|
|
266
|
+
target_order = [v for v in rename.values() if v in df.columns]
|
|
267
|
+
rest = [c for c in df.columns if c not in target_order]
|
|
268
|
+
final_cols = target_order + rest
|
|
269
|
+
df = df.select(final_cols)
|
|
270
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
df.write_csv(path)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def dump_csvs(data: "FlexData", workdir: Path | str,
|
|
275
|
+
*, copy_meta_from: Path | str | None = None,
|
|
276
|
+
include_heavy: bool | None = None) -> Path:
|
|
277
|
+
"""Materialise ``data`` to ``workdir/input/`` + ``workdir/solve_data/``
|
|
278
|
+
in flextool's CSV layout.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
data : FlexData
|
|
283
|
+
The in-memory FlexData to dump.
|
|
284
|
+
workdir : Path | str
|
|
285
|
+
Target directory. Created if missing.
|
|
286
|
+
copy_meta_from : Path | str, optional
|
|
287
|
+
When supplied, copies per-solve metadata files
|
|
288
|
+
(``solve_current.csv``, ``period_first_of_solve.csv``,
|
|
289
|
+
``solve_mode.csv``, etc. — see :data:`_META_FILES_TO_COPY`)
|
|
290
|
+
from this source workdir into the target. This is the
|
|
291
|
+
round-trip use case where the CSV reader needs metadata
|
|
292
|
+
(timeline, solve list) that isn't on FlexData but is in the
|
|
293
|
+
original workdir.
|
|
294
|
+
include_heavy : bool, optional
|
|
295
|
+
When ``True``, force-write the seven gigabyte-scale CSVs in
|
|
296
|
+
:data:`_HEAVY_CSV_FILES` regardless of the
|
|
297
|
+
``FLEXTOOL_DUMP_CSVS`` env var. When ``False``, force-skip
|
|
298
|
+
them. When ``None`` (default), consult the env var — set to
|
|
299
|
+
``"1"`` to enable. The round-trip regression test
|
|
300
|
+
(``tests/engine_polars/test_dump_csvs_roundtrip.py``) passes
|
|
301
|
+
``include_heavy=True`` to exercise the full debug-oracle
|
|
302
|
+
round-trip contract.
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
Path
|
|
307
|
+
The ``workdir`` (as a :class:`pathlib.Path`).
|
|
308
|
+
"""
|
|
309
|
+
work = Path(workdir)
|
|
310
|
+
inp_dir = work / "input"
|
|
311
|
+
sd_dir = work / "solve_data"
|
|
312
|
+
inp_dir.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
sd_dir.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
|
|
315
|
+
# ─── Direct field → CSV writes ───────────────────────────────────
|
|
316
|
+
heavy_on = _heavy_dumps_enabled() if include_heavy is None else include_heavy
|
|
317
|
+
for field, (kind, csv_name, rename) in DIRECT_WRITES.items():
|
|
318
|
+
if not hasattr(data, field):
|
|
319
|
+
continue
|
|
320
|
+
if not heavy_on and csv_name in _HEAVY_CSV_FILES:
|
|
321
|
+
# Skip gigabyte-scale CSVs that no post-processing reads
|
|
322
|
+
# (see :data:`_HEAVY_CSV_FILES` for the list).
|
|
323
|
+
continue
|
|
324
|
+
value = getattr(data, field)
|
|
325
|
+
f = _frame_of(value)
|
|
326
|
+
if f is None:
|
|
327
|
+
continue
|
|
328
|
+
target_dir = sd_dir if kind == "solve_data" else inp_dir
|
|
329
|
+
_write_frame(f, target_dir / csv_name, rename)
|
|
330
|
+
|
|
331
|
+
# ─── dt set (special: drop value column from p_step_duration) ─
|
|
332
|
+
# ``data.dt`` is the (d, t) set; flextool writes it implicitly via
|
|
333
|
+
# steps_in_use.csv (handled by the p_step_duration write above).
|
|
334
|
+
# If steps_in_use.csv was NOT written (no p_step_duration), but dt
|
|
335
|
+
# exists, fall back to writing a header-only steps_in_use.csv.
|
|
336
|
+
if (data.p_step_duration is None
|
|
337
|
+
and getattr(data, "dt", None) is not None):
|
|
338
|
+
f = data.dt.with_columns(value=pl.lit(1.0)) if data.dt.height > 0 else None
|
|
339
|
+
if f is not None:
|
|
340
|
+
_write_frame(f, sd_dir / "steps_in_use.csv",
|
|
341
|
+
{"d": "period", "t": "step", "value": "step_duration"})
|
|
342
|
+
|
|
343
|
+
# ─── flow_to_n / flow_from_n: derived sets used by the loader ──
|
|
344
|
+
# ``flow_to_n`` is just pss with sink renamed to n; we don't need to
|
|
345
|
+
# write a separate CSV — the loader reconstructs from
|
|
346
|
+
# process_source_sink.csv.
|
|
347
|
+
|
|
348
|
+
# ─── Sliced (wide-by-param) files ─────────────────────────────────
|
|
349
|
+
# pdtNode.csv carries (node, param, period, time, value) rows for
|
|
350
|
+
# multiple FlexData Params: penalty_up, penalty_down, availability,
|
|
351
|
+
# storage_state_reference_value. Same pattern for pdtProcess.csv,
|
|
352
|
+
# pdtCommodity.csv, pdtGroup.csv (period+time keys), and
|
|
353
|
+
# pdProcess.csv / pdGroup.csv (period only).
|
|
354
|
+
_write_pdt_sliced(data, sd_dir, heavy_on=heavy_on)
|
|
355
|
+
_write_pd_sliced(data, sd_dir)
|
|
356
|
+
_write_p_input_sliced(data, inp_dir)
|
|
357
|
+
|
|
358
|
+
# ─── Capacity / unitsize composites ───────────────────────────────
|
|
359
|
+
# p_entity_period_existing_capacity.csv has TWO value columns
|
|
360
|
+
# (existing + invested). Our p_entity_all_existing carries
|
|
361
|
+
# the 'all_existing' column; for round-trip we recompute existing =
|
|
362
|
+
# all_existing - 0 (no prior invest) and invested = 0. The reader
|
|
363
|
+
# prefers p_entity_all_existing.csv when it exists, so this is a
|
|
364
|
+
# stub for compatibility.
|
|
365
|
+
if data.p_entity_all_existing is not None:
|
|
366
|
+
f = _frame_of(data.p_entity_all_existing)
|
|
367
|
+
if f is not None:
|
|
368
|
+
stub = (f.pipe(rename_to_axis, {"e": "entity", "d": "period"})
|
|
369
|
+
.with_columns(
|
|
370
|
+
p_entity_period_existing_capacity=pl.col("value"),
|
|
371
|
+
p_entity_period_invested_capacity=pl.lit(0.0),
|
|
372
|
+
)
|
|
373
|
+
.select("entity", "period",
|
|
374
|
+
"p_entity_period_existing_capacity",
|
|
375
|
+
"p_entity_period_invested_capacity"))
|
|
376
|
+
stub.write_csv(sd_dir / "p_entity_period_existing_capacity.csv")
|
|
377
|
+
|
|
378
|
+
# ─── input/ entity-class set + wide-format unitsize ────────
|
|
379
|
+
# flextool's handoff_writers (process_outputs/handoff_writers.py) read
|
|
380
|
+
# ``input/p_entity_unitsize.csv`` (wide-transposed: ``entity,<e1>,<e2>,…``
|
|
381
|
+
# / ``value,<v1>,<v2>,…``) plus the entity-class set CSVs
|
|
382
|
+
# ``input/{entity,process_unit,process_connection,node}.csv``
|
|
383
|
+
# (single-column lists). These are produced by flextool's
|
|
384
|
+
# preprocessing pipeline on the slow path; the fast path skips
|
|
385
|
+
# preprocessing, so without these the handoff writers fail with
|
|
386
|
+
# ``[Errno 2]`` and the wide-format output writers downstream see
|
|
387
|
+
# an incomplete workdir. We synthesise them from FlexData here.
|
|
388
|
+
_write_input_entity_set_csvs(data, inp_dir)
|
|
389
|
+
_write_input_p_entity_unitsize_wide(data, inp_dir)
|
|
390
|
+
|
|
391
|
+
# ─── solve_data/ handoff stub CSVs ─────────────────────────
|
|
392
|
+
# ``write_p_entity_period_existing_capacity`` reads
|
|
393
|
+
# ``solve_data/solve__p_entity_pre_existing.csv`` (wide-by-entity,
|
|
394
|
+
# indexed by (solve, period)). In single-solve mode there's no
|
|
395
|
+
# prior solve so no pre-existing capacity has been carried over;
|
|
396
|
+
# write a header-only stub so ``pd.read_csv(... index_col=[0, 1])``
|
|
397
|
+
# returns an empty frame and the writer's ``if df.empty: return {}``
|
|
398
|
+
# branch fires. Idempotent — overwrites only when missing.
|
|
399
|
+
pre_existing_path = sd_dir / "solve__p_entity_pre_existing.csv"
|
|
400
|
+
if not pre_existing_path.exists():
|
|
401
|
+
pre_existing_path.write_text("solve,period\n")
|
|
402
|
+
|
|
403
|
+
# ─── Optional metadata copy-through ───────────────────────────────
|
|
404
|
+
if copy_meta_from is not None:
|
|
405
|
+
_copy_meta(Path(copy_meta_from), work)
|
|
406
|
+
|
|
407
|
+
return work
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _write_input_entity_set_csvs(data: "FlexData", inp_dir: Path) -> None:
|
|
411
|
+
"""write the four single-column entity-class set CSVs that
|
|
412
|
+
handoff_writers' ``_load_entity_class_set`` reads.
|
|
413
|
+
|
|
414
|
+
``input/node.csv`` — every row in ``data.nodeBalance`` (column ``n``).
|
|
415
|
+
``input/process_unit.csv`` — every row in ``data.process_unit``
|
|
416
|
+
(column ``p``). Header-only when the field is None / empty.
|
|
417
|
+
``input/process_connection.csv`` — every process in
|
|
418
|
+
``data.process_source_sink`` whose ``p`` is NOT in
|
|
419
|
+
``data.process_unit``. Header-only when no connection processes
|
|
420
|
+
exist (the common case for unit-only fixtures).
|
|
421
|
+
``input/entity.csv`` — union of nodes + processes.
|
|
422
|
+
|
|
423
|
+
All four files MUST exist for handoff_writers to walk them; an
|
|
424
|
+
empty (header-only) file is fine.
|
|
425
|
+
"""
|
|
426
|
+
# node.csv
|
|
427
|
+
nb = _frame_of(data.nodeBalance) if data.nodeBalance is not None else None
|
|
428
|
+
if nb is not None:
|
|
429
|
+
node_lf = nb.pipe(rename_to_axis, {"n": "node"}).select("node")
|
|
430
|
+
else:
|
|
431
|
+
_enums = getattr(data, "_axis_enums", None)
|
|
432
|
+
node_lf = pl.DataFrame({"node": []},
|
|
433
|
+
schema={"node": schema_dtype(_enums, "node")})
|
|
434
|
+
node_lf.write_csv(inp_dir / "node.csv")
|
|
435
|
+
|
|
436
|
+
# process_unit.csv
|
|
437
|
+
pu = _frame_of(data.process_unit) if data.process_unit is not None else None
|
|
438
|
+
if pu is not None:
|
|
439
|
+
pu_out = pu.rename({"p": "process_unit"}).select("process_unit")
|
|
440
|
+
else:
|
|
441
|
+
_enums = getattr(data, "_axis_enums", None)
|
|
442
|
+
pu_out = pl.DataFrame({"process_unit": []},
|
|
443
|
+
schema={"process_unit": schema_dtype(_enums, "process_unit")})
|
|
444
|
+
pu_out.write_csv(inp_dir / "process_unit.csv")
|
|
445
|
+
|
|
446
|
+
# process_connection.csv = (every process in pss) - process_unit
|
|
447
|
+
pss = (
|
|
448
|
+
_frame_of(data.process_source_sink)
|
|
449
|
+
if data.process_source_sink is not None
|
|
450
|
+
else None
|
|
451
|
+
)
|
|
452
|
+
pu_set: set[str] = (
|
|
453
|
+
set(pu["p"].cast(pl.Utf8).to_list()) if pu is not None else set()
|
|
454
|
+
)
|
|
455
|
+
if pss is not None and "p" in pss.columns:
|
|
456
|
+
all_p = set(pss["p"].cast(pl.Utf8).to_list())
|
|
457
|
+
conn_processes = sorted(all_p - pu_set)
|
|
458
|
+
else:
|
|
459
|
+
conn_processes = []
|
|
460
|
+
pl.DataFrame({"process_connection": conn_processes}).write_csv(
|
|
461
|
+
inp_dir / "process_connection.csv"
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# entity.csv = nodes + processes (sorted, unique).
|
|
465
|
+
nodes = (
|
|
466
|
+
nb["n"].cast(pl.Utf8).to_list() if nb is not None else []
|
|
467
|
+
)
|
|
468
|
+
processes = (
|
|
469
|
+
list(set(pss["p"].cast(pl.Utf8).to_list())) if pss is not None and "p" in pss.columns else []
|
|
470
|
+
)
|
|
471
|
+
entities = sorted(set(nodes) | set(processes))
|
|
472
|
+
pl.DataFrame(
|
|
473
|
+
{"entity": entities},
|
|
474
|
+
schema={"entity": schema_dtype(None, "entity")},
|
|
475
|
+
).write_csv(inp_dir / "entity.csv")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _write_input_p_entity_unitsize_wide(data: "FlexData",
|
|
479
|
+
inp_dir: Path) -> None:
|
|
480
|
+
"""write the wide-transposed ``input/p_entity_unitsize.csv``
|
|
481
|
+
that ``handoff_writers._load_unitsize`` consumes.
|
|
482
|
+
|
|
483
|
+
Layout (read with ``pd.read_csv(path, index_col=0)``):
|
|
484
|
+
|
|
485
|
+
entity,e1,e2,e3,...
|
|
486
|
+
value,v1,v2,v3,...
|
|
487
|
+
|
|
488
|
+
Source: union of ``data.p_unitsize`` (process unitsize, column ``p``)
|
|
489
|
+
and ``data.p_state_unitsize`` (node-state unitsize, column ``n``).
|
|
490
|
+
Entities not present in either Param fall back to 1000.0.
|
|
491
|
+
"""
|
|
492
|
+
# Build the (entity → value) map.
|
|
493
|
+
entity_value: dict[str, float] = {}
|
|
494
|
+
pu_param = data.p_unitsize
|
|
495
|
+
pu_frame = _frame_of(pu_param) if pu_param is not None else None
|
|
496
|
+
if pu_frame is not None and "p" in pu_frame.columns and "value" in pu_frame.columns:
|
|
497
|
+
for row in pu_frame.iter_rows(named=True):
|
|
498
|
+
entity_value[str(row["p"])] = float(row["value"])
|
|
499
|
+
psu_param = getattr(data, "p_state_unitsize", None)
|
|
500
|
+
psu_frame = _frame_of(psu_param) if psu_param is not None else None
|
|
501
|
+
if psu_frame is not None and "n" in psu_frame.columns and "value" in psu_frame.columns:
|
|
502
|
+
for row in psu_frame.iter_rows(named=True):
|
|
503
|
+
entity_value[str(row["n"])] = float(row["value"])
|
|
504
|
+
|
|
505
|
+
# Discover the full entity universe so every node / process appears
|
|
506
|
+
# in the wide CSV — even when the Params didn't carry it. Default
|
|
507
|
+
# 1000.0 mirrors flextool's preprocessing default.
|
|
508
|
+
nb = _frame_of(data.nodeBalance) if data.nodeBalance is not None else None
|
|
509
|
+
pss = (
|
|
510
|
+
_frame_of(data.process_source_sink)
|
|
511
|
+
if data.process_source_sink is not None
|
|
512
|
+
else None
|
|
513
|
+
)
|
|
514
|
+
nodes = (
|
|
515
|
+
nb["n"].cast(pl.Utf8).to_list() if nb is not None else []
|
|
516
|
+
)
|
|
517
|
+
processes = (
|
|
518
|
+
list(set(pss["p"].cast(pl.Utf8).to_list())) if pss is not None and "p" in pss.columns else []
|
|
519
|
+
)
|
|
520
|
+
universe = sorted(set(nodes) | set(processes))
|
|
521
|
+
rows: list[tuple[str, float]] = [
|
|
522
|
+
(e, entity_value.get(e, 1000.0)) for e in universe
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
# Wide-transposed: header row "entity,e1,e2,...", value row "value,v1,v2,...".
|
|
526
|
+
if not rows:
|
|
527
|
+
# Header-only file — handoff_writers' _load_unitsize would fail,
|
|
528
|
+
# but _load_unitsize_map (the forgiving variant) tolerates a
|
|
529
|
+
# missing 'value' index. Emit a single-column header so
|
|
530
|
+
# ``pd.read_csv(... index_col=0)`` returns an empty frame.
|
|
531
|
+
(inp_dir / "p_entity_unitsize.csv").write_text("entity\nvalue\n")
|
|
532
|
+
return
|
|
533
|
+
header = "entity," + ",".join(e for e, _ in rows)
|
|
534
|
+
values = "value," + ",".join(repr(v) for _, v in rows)
|
|
535
|
+
(inp_dir / "p_entity_unitsize.csv").write_text(header + "\n" + values + "\n")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# ---------------------------------------------------------------------------
|
|
539
|
+
# Sliced-by-param writers
|
|
540
|
+
|
|
541
|
+
# Each entry: {param_value: (FlexData_field, dim_for_value)}. The
|
|
542
|
+
# key is the literal `param=` value in the wide-by-param CSV.
|
|
543
|
+
_PDT_NODE_SLICES = {
|
|
544
|
+
"penalty_up": "p_penalty_up",
|
|
545
|
+
"penalty_down": "p_penalty_down",
|
|
546
|
+
"availability": "p_node_availability",
|
|
547
|
+
"storage_state_reference_value": "p_storage_state_reference_value",
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
_PDT_PROCESS_SLICES = {
|
|
551
|
+
"availability": "p_process_availability",
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
_PDT_COMMODITY_SLICES = {
|
|
555
|
+
"price": "p_commodity_price",
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_PDT_GROUP_SLICES = {
|
|
559
|
+
"co2_price": "p_co2_price",
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _write_pdt_sliced(data: "FlexData", sd_dir: Path,
|
|
564
|
+
*, heavy_on: bool | None = None) -> None:
|
|
565
|
+
"""Write the wide-by-param ``pdt*.csv`` files (period + time keyed)."""
|
|
566
|
+
if heavy_on is None:
|
|
567
|
+
heavy_on = _heavy_dumps_enabled()
|
|
568
|
+
# pdtNode.csv columns: node, param, period, time, value
|
|
569
|
+
if heavy_on or "pdtNode.csv" not in _HEAVY_CSV_FILES:
|
|
570
|
+
rows: list[pl.DataFrame] = []
|
|
571
|
+
for param_value, field in _PDT_NODE_SLICES.items():
|
|
572
|
+
f = _frame_of_pdt(getattr(data, field, None), getattr(data, "dt", None))
|
|
573
|
+
if f is None:
|
|
574
|
+
continue
|
|
575
|
+
# Expected schema: (n, d, t, value) — rename to canonical and add
|
|
576
|
+
# the literal ``param=`` column.
|
|
577
|
+
ren = {"n": "node", "d": "period", "t": "time"}
|
|
578
|
+
rows.append((f.pipe(rename_to_axis,
|
|
579
|
+
{k: v for k, v in ren.items() if k in f.columns})
|
|
580
|
+
.with_columns(param=pl.lit(param_value))
|
|
581
|
+
.select("node", "param", "period", "time", "value")))
|
|
582
|
+
if rows:
|
|
583
|
+
out = pl.concat(rows, how="vertical_relaxed")
|
|
584
|
+
out.write_csv(sd_dir / "pdtNode.csv")
|
|
585
|
+
|
|
586
|
+
# pdtProcess.csv columns: process, param, period, time, value
|
|
587
|
+
if heavy_on or "pdtProcess.csv" not in _HEAVY_CSV_FILES:
|
|
588
|
+
rows = []
|
|
589
|
+
for param_value, field in _PDT_PROCESS_SLICES.items():
|
|
590
|
+
f = _frame_of_pdt(getattr(data, field, None), getattr(data, "dt", None))
|
|
591
|
+
if f is None:
|
|
592
|
+
continue
|
|
593
|
+
ren = {"p": "process", "d": "period", "t": "time"}
|
|
594
|
+
rows.append((f.pipe(rename_to_axis,
|
|
595
|
+
{k: v for k, v in ren.items() if k in f.columns})
|
|
596
|
+
.with_columns(param=pl.lit(param_value))
|
|
597
|
+
.select("process", "param", "period", "time", "value")))
|
|
598
|
+
if rows:
|
|
599
|
+
out = pl.concat(rows, how="vertical_relaxed")
|
|
600
|
+
out.write_csv(sd_dir / "pdtProcess.csv")
|
|
601
|
+
|
|
602
|
+
# pdtCommodity.csv columns: commodity, param, period, time, value
|
|
603
|
+
rows = []
|
|
604
|
+
for param_value, field in _PDT_COMMODITY_SLICES.items():
|
|
605
|
+
f = _frame_of_pdt(getattr(data, field, None), getattr(data, "dt", None))
|
|
606
|
+
if f is None:
|
|
607
|
+
continue
|
|
608
|
+
ren = {"c": "commodity", "d": "period", "t": "time"}
|
|
609
|
+
rows.append((f.pipe(rename_to_axis,
|
|
610
|
+
{k: v for k, v in ren.items() if k in f.columns})
|
|
611
|
+
.with_columns(param=pl.lit(param_value))
|
|
612
|
+
.select("commodity", "param", "period", "time", "value")))
|
|
613
|
+
if rows:
|
|
614
|
+
out = pl.concat(rows, how="vertical_relaxed")
|
|
615
|
+
out.write_csv(sd_dir / "pdtCommodity.csv")
|
|
616
|
+
|
|
617
|
+
# pdtGroup.csv columns: group, param, period, time, value
|
|
618
|
+
rows = []
|
|
619
|
+
for param_value, field in _PDT_GROUP_SLICES.items():
|
|
620
|
+
f = _frame_of_pdt(getattr(data, field, None), getattr(data, "dt", None))
|
|
621
|
+
if f is None:
|
|
622
|
+
continue
|
|
623
|
+
ren = {"g": "group", "d": "period", "t": "time"}
|
|
624
|
+
rows.append((f.pipe(rename_to_axis,
|
|
625
|
+
{k: v for k, v in ren.items() if k in f.columns})
|
|
626
|
+
.with_columns(param=pl.lit(param_value))
|
|
627
|
+
.select("group", "param", "period", "time", "value")))
|
|
628
|
+
if rows:
|
|
629
|
+
out = pl.concat(rows, how="vertical_relaxed")
|
|
630
|
+
out.write_csv(sd_dir / "pdtGroup.csv")
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
# pdProcess.csv columns: process, param, period, value
|
|
634
|
+
_PD_PROCESS_SLICES = {
|
|
635
|
+
"startup_cost": "p_startup_cost",
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
# pdGroup.csv columns: group, param, period, value (1d_map period)
|
|
639
|
+
_PD_GROUP_SLICES: dict[str, str] = {}
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _write_pd_sliced(data: "FlexData", sd_dir: Path) -> None:
|
|
643
|
+
"""Write wide-by-param ``pd*.csv`` files (period only, no time)."""
|
|
644
|
+
rows: list[pl.DataFrame] = []
|
|
645
|
+
for param_value, field in _PD_PROCESS_SLICES.items():
|
|
646
|
+
f = _frame_of(getattr(data, field, None))
|
|
647
|
+
if f is None:
|
|
648
|
+
continue
|
|
649
|
+
ren = {"p": "process", "d": "period"}
|
|
650
|
+
rows.append((f.pipe(rename_to_axis,
|
|
651
|
+
{k: v for k, v in ren.items() if k in f.columns})
|
|
652
|
+
.with_columns(param=pl.lit(param_value))
|
|
653
|
+
.select("process", "param", "period", "value")))
|
|
654
|
+
if rows:
|
|
655
|
+
out = pl.concat(rows, how="vertical_relaxed")
|
|
656
|
+
out.write_csv(sd_dir / "pdProcess.csv")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# p_commodity.csv columns: commodity, commodityParam, p_commodity (long-by-param)
|
|
660
|
+
_P_COMMODITY_SLICES = {
|
|
661
|
+
"co2_content": "p_co2_content",
|
|
662
|
+
"unitsize": "p_commodity_unitsize",
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
# p_node.csv columns: node, nodeParam, p_node
|
|
666
|
+
_P_NODE_SLICES = {
|
|
667
|
+
"self_discharge_loss": "p_state_self_discharge",
|
|
668
|
+
"storage_state_start": "p_state_start",
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# p_process.csv columns: process, processParam, p_process
|
|
672
|
+
_P_PROCESS_SLICES = {
|
|
673
|
+
"min_load": "p_min_load",
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _merge_param_slice(path: Path, new_rows: pl.DataFrame,
|
|
678
|
+
entity_col: str, param_col: str) -> pl.DataFrame:
|
|
679
|
+
"""Merge a freshly-built wide-by-param slice with the existing file.
|
|
680
|
+
|
|
681
|
+
The legacy ``input_writer.write_parameter`` populates
|
|
682
|
+
``input/p_<class>.csv`` with the full entity-level scalar surface
|
|
683
|
+
(e.g. for nodes: ``penalty_up``, ``existing``, ``lifetime``,
|
|
684
|
+
``self_discharge_loss``, …). The override-chain-driven dump in
|
|
685
|
+
:func:`_write_p_input_sliced` knows the canonical value for only a
|
|
686
|
+
handful of slices (``_P_NODE_SLICES`` etc.), so a plain
|
|
687
|
+
``write_csv`` would wipe the rest of the entity-level surface.
|
|
688
|
+
|
|
689
|
+
Read existing rows, drop those whose ``param_col`` is replaced by
|
|
690
|
+
*new_rows*, and concatenate. Both halves are normalised to Utf8
|
|
691
|
+
so ``vertical_relaxed`` does not have to promote across numeric /
|
|
692
|
+
string mismatches; ``write_csv`` produces text anyway.
|
|
693
|
+
"""
|
|
694
|
+
if not path.exists():
|
|
695
|
+
return new_rows
|
|
696
|
+
existing = _read_csv_keep_columns(path)
|
|
697
|
+
if existing is None or existing.height == 0:
|
|
698
|
+
return new_rows
|
|
699
|
+
cols = new_rows.columns
|
|
700
|
+
keep = [c for c in cols if c in existing.columns]
|
|
701
|
+
if len(keep) != len(cols):
|
|
702
|
+
return new_rows
|
|
703
|
+
existing = existing.select(keep)
|
|
704
|
+
if param_col not in existing.columns:
|
|
705
|
+
return new_rows
|
|
706
|
+
replaced_params = new_rows.select(param_col).unique()
|
|
707
|
+
survivors = (existing.lazy()
|
|
708
|
+
.join(replaced_params.lazy().with_columns(_drop=pl.lit(True)),
|
|
709
|
+
on=param_col, how="left")
|
|
710
|
+
.filter(pl.col("_drop").is_null())
|
|
711
|
+
.drop("_drop")
|
|
712
|
+
.collect())
|
|
713
|
+
if survivors.height == 0:
|
|
714
|
+
return new_rows
|
|
715
|
+
new_rows_str = new_rows.with_columns(
|
|
716
|
+
[pl.col(c).cast(pl.Utf8) for c in new_rows.columns]
|
|
717
|
+
)
|
|
718
|
+
return pl.concat([survivors, new_rows_str], how="vertical_relaxed")
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _read_csv_keep_columns(path: Path) -> pl.DataFrame | None:
|
|
722
|
+
"""Read a CSV with all columns as Utf8 to preserve original formatting.
|
|
723
|
+
|
|
724
|
+
Used by :func:`_merge_param_slice` to avoid coercing existing
|
|
725
|
+
numeric / string entity-level rows to a different dtype during the
|
|
726
|
+
merge. Returns ``None`` if the file is empty / unreadable.
|
|
727
|
+
"""
|
|
728
|
+
try:
|
|
729
|
+
return pl.read_csv(path, infer_schema_length=0)
|
|
730
|
+
except Exception: # noqa: BLE001 — defensive
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _write_p_input_sliced(data: "FlexData", inp_dir: Path) -> None:
|
|
735
|
+
"""Write wide-by-param ``input/p_*.csv`` files (scalar slices).
|
|
736
|
+
|
|
737
|
+
These files carry the full entity-level scalar surface (e.g. for
|
|
738
|
+
nodes: ``penalty_up``, ``existing``, ``lifetime``, …). The
|
|
739
|
+
override chain only feeds FlexData for a subset (``_P_*_SLICES``),
|
|
740
|
+
so when a legacy ``input/p_<class>.csv`` already exists on disk
|
|
741
|
+
we merge our slices INTO it rather than overwriting — otherwise
|
|
742
|
+
legacy entity-level params would be silently dropped.
|
|
743
|
+
"""
|
|
744
|
+
# p_commodity.csv
|
|
745
|
+
rows: list[pl.DataFrame] = []
|
|
746
|
+
for param_value, field in _P_COMMODITY_SLICES.items():
|
|
747
|
+
f = _frame_of(getattr(data, field, None))
|
|
748
|
+
if f is None:
|
|
749
|
+
continue
|
|
750
|
+
rec = (f.pipe(rename_to_axis, {"c": "commodity", "value": "p_commodity"})
|
|
751
|
+
.with_columns(commodityParam=pl.lit(param_value))
|
|
752
|
+
.select("commodity", "commodityParam", "p_commodity"))
|
|
753
|
+
rows.append(rec)
|
|
754
|
+
if rows:
|
|
755
|
+
new_rows = pl.concat(rows, how="vertical_relaxed")
|
|
756
|
+
merged = _merge_param_slice(inp_dir / "p_commodity.csv", new_rows,
|
|
757
|
+
entity_col="commodity",
|
|
758
|
+
param_col="commodityParam")
|
|
759
|
+
merged.write_csv(inp_dir / "p_commodity.csv")
|
|
760
|
+
|
|
761
|
+
rows = []
|
|
762
|
+
for param_value, field in _P_NODE_SLICES.items():
|
|
763
|
+
f = _frame_of(getattr(data, field, None))
|
|
764
|
+
if f is None:
|
|
765
|
+
continue
|
|
766
|
+
rec = (f.pipe(rename_to_axis, {"n": "node", "value": "p_node"})
|
|
767
|
+
.with_columns(nodeParam=pl.lit(param_value))
|
|
768
|
+
.select("node", "nodeParam", "p_node"))
|
|
769
|
+
rows.append(rec)
|
|
770
|
+
if rows:
|
|
771
|
+
new_rows = pl.concat(rows, how="vertical_relaxed")
|
|
772
|
+
merged = _merge_param_slice(inp_dir / "p_node.csv", new_rows,
|
|
773
|
+
entity_col="node",
|
|
774
|
+
param_col="nodeParam")
|
|
775
|
+
merged.write_csv(inp_dir / "p_node.csv")
|
|
776
|
+
|
|
777
|
+
rows = []
|
|
778
|
+
for param_value, field in _P_PROCESS_SLICES.items():
|
|
779
|
+
f = _frame_of(getattr(data, field, None))
|
|
780
|
+
if f is None:
|
|
781
|
+
continue
|
|
782
|
+
rec = (f.pipe(rename_to_axis, {"p": "process", "value": "p_process"})
|
|
783
|
+
.with_columns(processParam=pl.lit(param_value))
|
|
784
|
+
.select("process", "processParam", "p_process"))
|
|
785
|
+
rows.append(rec)
|
|
786
|
+
if rows:
|
|
787
|
+
new_rows = pl.concat(rows, how="vertical_relaxed")
|
|
788
|
+
merged = _merge_param_slice(inp_dir / "p_process.csv", new_rows,
|
|
789
|
+
entity_col="process",
|
|
790
|
+
param_col="processParam")
|
|
791
|
+
merged.write_csv(inp_dir / "p_process.csv")
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# ---------------------------------------------------------------------------
|
|
795
|
+
# Metadata copy-through
|
|
796
|
+
|
|
797
|
+
# These files are read by ``load_flextool`` but are NOT FlexData fields.
|
|
798
|
+
# When a round-trip test wants the dumped workdir to reload via the CSV
|
|
799
|
+
# path, it copies these from the original workdir verbatim.
|
|
800
|
+
_META_FILES_TO_COPY: tuple[tuple[str, str], ...] = (
|
|
801
|
+
# (kind, filename)
|
|
802
|
+
("solve_data", "solve_current.csv"),
|
|
803
|
+
("solve_data", "period_first_of_solve.csv"),
|
|
804
|
+
("solve_data", "period_first.csv"),
|
|
805
|
+
("solve_data", "period_last.csv"),
|
|
806
|
+
("solve_data", "period__time_last.csv"),
|
|
807
|
+
("solve_data", "block_period_time_last.csv"),
|
|
808
|
+
("solve_data", "step_previous.csv"),
|
|
809
|
+
("solve_data", "p_years_d.csv"),
|
|
810
|
+
("solve_data", "p_nested_model.csv"),
|
|
811
|
+
("solve_data", "fix_storage_timesteps.csv"),
|
|
812
|
+
("solve_data", "realized_invest_periods_of_current_solve.csv"),
|
|
813
|
+
("solve_data", "realized_dispatch.csv"),
|
|
814
|
+
("solve_data", "p_entity_pre_existing.csv"),
|
|
815
|
+
("solve_data", "entity.csv"),
|
|
816
|
+
("solve_data", "entityDivest.csv"),
|
|
817
|
+
("solve_data", "process_side_block.csv"),
|
|
818
|
+
("solve_data", "entity_block.csv"),
|
|
819
|
+
("solve_data", "overlap_set.csv"),
|
|
820
|
+
("solve_data", "block_step_duration.csv"),
|
|
821
|
+
("solve_data", "period_block_set.csv"),
|
|
822
|
+
("solve_data", "period_block_succ.csv"),
|
|
823
|
+
("solve_data", "period_block_time.csv"),
|
|
824
|
+
("solve_data", "period__branch.csv"),
|
|
825
|
+
("solve_data", "first_timesteps.csv"),
|
|
826
|
+
("solve_data", "solve_branch_weight.csv"),
|
|
827
|
+
("input", "solve_mode.csv"),
|
|
828
|
+
("input", "p_model.csv"),
|
|
829
|
+
("input", "p_group.csv"),
|
|
830
|
+
("input", "pd_group.csv"),
|
|
831
|
+
("input", "p_flowgroup.csv"),
|
|
832
|
+
("input", "pdt_flowgroup.csv"),
|
|
833
|
+
("input", "process__ct_method.csv"),
|
|
834
|
+
("input", "groupIncludeStochastics.csv"),
|
|
835
|
+
("input", "commodity__node.csv"),
|
|
836
|
+
("input", "group__node.csv"),
|
|
837
|
+
("input", "group__entity.csv"),
|
|
838
|
+
("input", "flowGroup__process__node.csv"),
|
|
839
|
+
("input", "constraint__sense.csv"),
|
|
840
|
+
("input", "p_constraint_constant.csv"),
|
|
841
|
+
("input", "p_node_constraint_invested_capacity_coeff.csv"),
|
|
842
|
+
("input", "p_process_constraint_invested_capacity_coeff.csv"),
|
|
843
|
+
("input", "p_node_constraint_state_coeff.csv"),
|
|
844
|
+
("input", "p_node_constraint_cumulative_pre_built_capacity_coeff.csv"),
|
|
845
|
+
("input", "p_process_constraint_cumulative_pre_built_capacity_coeff.csv"),
|
|
846
|
+
("input", "p_process_node_constraint_flow_coeff.csv"),
|
|
847
|
+
("input", "p_process_source_conversion_flow_coeff.csv"),
|
|
848
|
+
("input", "p_process_sink_conversion_flow_coeff.csv"),
|
|
849
|
+
("input", "p_entity_unitsize.csv"),
|
|
850
|
+
("input", "process__source__sink__profile__profile_method.csv"),
|
|
851
|
+
("input", "node__profile__profile_method.csv"),
|
|
852
|
+
("input", "node__storage_start_end_method.csv"),
|
|
853
|
+
("input", "node__storage_solve_horizon_method.csv"),
|
|
854
|
+
("input", "node__storage_binding_method.csv"),
|
|
855
|
+
("input", "node__storage_nested_fix_method.csv"),
|
|
856
|
+
("input", "p_reserve__upDown__group.csv"),
|
|
857
|
+
("input", "p_process__reserve__upDown__node.csv"),
|
|
858
|
+
("input", "groupNonSync.csv"),
|
|
859
|
+
("input", "p_process_sink.csv"),
|
|
860
|
+
("input", "p_process_source.csv"),
|
|
861
|
+
("input", "node_dc_power_flow.csv"),
|
|
862
|
+
("input", "connection_dc_power_flow.csv"),
|
|
863
|
+
("input", "node_reference_angle.csv"),
|
|
864
|
+
("input", "p_connection_susceptance.csv"),
|
|
865
|
+
("input", "commodity_ladder_annual.csv"),
|
|
866
|
+
("input", "commodity_ladder_cumulative.csv"),
|
|
867
|
+
("input", "p_commodity_unitsize.csv"),
|
|
868
|
+
# solve_data — additional preprocessed files
|
|
869
|
+
("solve_data", "process__method_indirect.csv"),
|
|
870
|
+
("solve_data", "commodity_node_co2.csv"),
|
|
871
|
+
("solve_data", "group_co2_price.csv"),
|
|
872
|
+
("solve_data", "p_online_dt_set.csv"),
|
|
873
|
+
("solve_data", "process_source_sink_ramp_limit_sink_up.csv"),
|
|
874
|
+
("solve_data", "process_source_sink_ramp_limit_sink_down.csv"),
|
|
875
|
+
("solve_data", "process_source_sink_ramp_limit_source_up.csv"),
|
|
876
|
+
("solve_data", "process_source_sink_ramp_limit_source_down.csv"),
|
|
877
|
+
("solve_data", "process_minload.csv"),
|
|
878
|
+
("solve_data", "process_online_linear.csv"),
|
|
879
|
+
("solve_data", "process_online_integer.csv"),
|
|
880
|
+
("solve_data", "process_online.csv"),
|
|
881
|
+
("solve_data", "ed_invest.csv"),
|
|
882
|
+
("solve_data", "ed_divest.csv"),
|
|
883
|
+
("solve_data", "edd_invest.csv"),
|
|
884
|
+
("solve_data", "ed_invest_period.csv"),
|
|
885
|
+
("solve_data", "ed_divest_period.csv"),
|
|
886
|
+
("solve_data", "ed_invest_max_period.csv"),
|
|
887
|
+
("solve_data", "ed_divest_max_period.csv"),
|
|
888
|
+
("solve_data", "ed_invest_min_period.csv"),
|
|
889
|
+
("solve_data", "ed_divest_min_period.csv"),
|
|
890
|
+
("solve_data", "e_invest_total.csv"),
|
|
891
|
+
("solve_data", "e_divest_total.csv"),
|
|
892
|
+
("solve_data", "e_invest_max_total.csv"),
|
|
893
|
+
("solve_data", "e_divest_max_total.csv"),
|
|
894
|
+
("solve_data", "e_invest_min_total.csv"),
|
|
895
|
+
("solve_data", "e_divest_min_total.csv"),
|
|
896
|
+
("solve_data", "ed_lifetime_fixed_cost.csv"),
|
|
897
|
+
("solve_data", "ed_lifetime_fixed_cost_divest.csv"),
|
|
898
|
+
("solve_data", "ed_entity_annual_discounted.csv"),
|
|
899
|
+
("solve_data", "ed_entity_annual_divest_discounted.csv"),
|
|
900
|
+
("solve_data", "p_entity_max_units.csv"),
|
|
901
|
+
("solve_data", "p_entity_period_existing_capacity.csv"),
|
|
902
|
+
("solve_data", "p_entity_previously_invested_capacity.csv"),
|
|
903
|
+
("solve_data", "p_entity_invested.csv"),
|
|
904
|
+
("solve_data", "p_entity_divested.csv"),
|
|
905
|
+
("solve_data", "ed_invest_forbidden_no_investment.csv"),
|
|
906
|
+
("solve_data", "ed_invest_cumulative.csv"),
|
|
907
|
+
("solve_data", "ed_cumulative_max_capacity.csv"),
|
|
908
|
+
("solve_data", "ed_cumulative_min_capacity.csv"),
|
|
909
|
+
("solve_data", "g_invest_total.csv"),
|
|
910
|
+
("solve_data", "g_divest_total.csv"),
|
|
911
|
+
("solve_data", "g_invest_cumulative.csv"),
|
|
912
|
+
("solve_data", "gd_invest_period.csv"),
|
|
913
|
+
("solve_data", "gd_divest_period.csv"),
|
|
914
|
+
("solve_data", "n_fix_storage_quantity_set.csv"),
|
|
915
|
+
("solve_data", "fix_storage_quantity.csv"),
|
|
916
|
+
("solve_data", "p_roll_continue_state.csv"),
|
|
917
|
+
("solve_data", "process_indirect.csv"),
|
|
918
|
+
("solve_data", "process_input_flows.csv"),
|
|
919
|
+
("solve_data", "process_output_flows.csv"),
|
|
920
|
+
("solve_data", "ed_fixed_cost.csv"),
|
|
921
|
+
("solve_data", "p_node.csv"),
|
|
922
|
+
("solve_data", "p_process.csv"),
|
|
923
|
+
("solve_data", "process_source_delayed.csv"),
|
|
924
|
+
("solve_data", "process_source_undelayed.csv"),
|
|
925
|
+
("solve_data", "process_source_sink_delayed.csv"),
|
|
926
|
+
("solve_data", "process_source_sink_undelayed.csv"),
|
|
927
|
+
("solve_data", "process_delayed.csv"),
|
|
928
|
+
("solve_data", "process_delayed__duration.csv"),
|
|
929
|
+
("solve_data", "p_process_delay_weight.csv"),
|
|
930
|
+
("solve_data", "dtt__delay_duration.csv"),
|
|
931
|
+
("solve_data", "commodity_with_ladder.csv"),
|
|
932
|
+
("solve_data", "commodity_with_ladder_annual.csv"),
|
|
933
|
+
("solve_data", "commodity_with_ladder_cumulative.csv"),
|
|
934
|
+
("solve_data", "cnd_ladder_set.csv"),
|
|
935
|
+
("solve_data", "cndi_ladder_set.csv"),
|
|
936
|
+
("solve_data", "cndi_ladder_ann_set.csv"),
|
|
937
|
+
("solve_data", "cndi_ladder_cum_set.csv"),
|
|
938
|
+
("solve_data", "ci_ladder_cumulative.csv"),
|
|
939
|
+
("solve_data", "commodity__tier_ann.csv"),
|
|
940
|
+
("solve_data", "f_d_k.csv"),
|
|
941
|
+
("solve_data", "ladder_cum_realized_mwh.csv"),
|
|
942
|
+
("solve_data", "node_capacity_for_scaling.csv"),
|
|
943
|
+
("solve_data", "group_capacity_for_scaling.csv"),
|
|
944
|
+
("solve_data", "inv_group_cap.csv"),
|
|
945
|
+
("solve_data", "group_node.csv"),
|
|
946
|
+
("solve_data", "group_entity.csv"),
|
|
947
|
+
("solve_data", "group_process_node.csv"),
|
|
948
|
+
("solve_data", "process_unit.csv"),
|
|
949
|
+
("solve_data", "process_sink_inertia.csv"),
|
|
950
|
+
("solve_data", "process_source_inertia.csv"),
|
|
951
|
+
("solve_data", "process__sink_nonSync.csv"),
|
|
952
|
+
("solve_data", "process__group_inside_group_nonSync.csv"),
|
|
953
|
+
("solve_data", "p_positive_inflow.csv"),
|
|
954
|
+
("solve_data", "p_negative_inflow.csv"),
|
|
955
|
+
("solve_data", "pdGroup.csv"),
|
|
956
|
+
("solve_data", "pdGroup_capacity_margin.csv"),
|
|
957
|
+
("solve_data", "pdGroup_inertia_limit.csv"),
|
|
958
|
+
("solve_data", "pdGroup_penalty_capacity_margin.csv"),
|
|
959
|
+
("solve_data", "pdGroup_penalty_inertia.csv"),
|
|
960
|
+
("solve_data", "pdGroup_penalty_non_synchronous.csv"),
|
|
961
|
+
("solve_data", "reserve__upDown__group.csv"),
|
|
962
|
+
("solve_data", "reserve__upDown__group__method_timeseries.csv"),
|
|
963
|
+
("solve_data", "reserve__upDown__group__method_dynamic.csv"),
|
|
964
|
+
("solve_data", "reserve__upDown__group__method_n_1.csv"),
|
|
965
|
+
("solve_data", "prundt.csv"),
|
|
966
|
+
("solve_data", "process_reserve_upDown_node_active.csv"),
|
|
967
|
+
("solve_data", "process_reserve_upDown_node_increase_reserve_ratio.csv"),
|
|
968
|
+
("solve_data", "process_reserve_upDown_node_large_failure_ratio.csv"),
|
|
969
|
+
("solve_data", "p_process_reserve_upDown_node_reliability.csv"),
|
|
970
|
+
("solve_data", "pdtReserve_upDown_group.csv"),
|
|
971
|
+
("solve_data", "dt_non_anticipativity_set.csv"),
|
|
972
|
+
("solve_data", "timeline_matching_map.csv"),
|
|
973
|
+
("solve_data", "fix_storage_quantity.csv"),
|
|
974
|
+
("solve_data", "edd_history.csv"),
|
|
975
|
+
("solve_data", "edd_history_invest.csv"),
|
|
976
|
+
("solve_data", "pssdt_varCost_noEff.csv"),
|
|
977
|
+
("solve_data", "pssdt_varCost_eff_unit_source.csv"),
|
|
978
|
+
("solve_data", "pssdt_varCost_eff_unit_sink.csv"),
|
|
979
|
+
("solve_data", "pssdt_varCost_eff_connection.csv"),
|
|
980
|
+
("solve_data", "pdtProcess_source.csv"),
|
|
981
|
+
("solve_data", "pdtProcess_sink.csv"),
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _copy_meta(src_workdir: Path, dst_workdir: Path) -> None:
|
|
986
|
+
"""Copy metadata files from ``src`` to ``dst``, preserving directory.
|
|
987
|
+
|
|
988
|
+
Strategy: copy every ``.csv`` file from ``src/{input,solve_data}/``
|
|
989
|
+
that does NOT already exist in ``dst/{input,solve_data}/``. The
|
|
990
|
+
direct-write pass has already populated the FlexData-derived files;
|
|
991
|
+
this pass fills in metadata + structural files (timeline, period
|
|
992
|
+
markers, method discriminators, header-only filter sets, etc.) that
|
|
993
|
+
the CSV reader needs but that aren't FlexData fields.
|
|
994
|
+
|
|
995
|
+
The intent is that the dumped tree is a *complete* workdir that
|
|
996
|
+
``load_flextool`` can consume — the CSV reader's many file-existence
|
|
997
|
+
probes don't crash because every CSV in the source is mirrored.
|
|
998
|
+
"""
|
|
999
|
+
for kind in ("input", "solve_data"):
|
|
1000
|
+
src_dir = src_workdir / kind
|
|
1001
|
+
dst_dir = dst_workdir / kind
|
|
1002
|
+
if not src_dir.is_dir():
|
|
1003
|
+
continue
|
|
1004
|
+
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
1005
|
+
for src in src_dir.glob("*.csv"):
|
|
1006
|
+
dst = dst_dir / src.name
|
|
1007
|
+
if dst.exists():
|
|
1008
|
+
continue # direct-write or earlier copy already populated
|
|
1009
|
+
shutil.copy(src, dst)
|