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,859 @@
|
|
|
1
|
+
"""Mid-level set / param projections.
|
|
2
|
+
|
|
3
|
+
Each family reads a small handful of ``input/*.csv`` (and,
|
|
4
|
+
occasionally, a leaf-level ``solve_data/*.csv`` written earlier by
|
|
5
|
+
:mod:`._emit_leaf_sets`) and emits one or more ``solve_data/`` CSVs.
|
|
6
|
+
|
|
7
|
+
Families:
|
|
8
|
+
|
|
9
|
+
* ``node_type_sets.py`` — 77 LOC — 4 partitions of node by p_node_type
|
|
10
|
+
* ``union_sets.py`` — 69 LOC — 2 ordered-union 2-tuples
|
|
11
|
+
* ``dc_angle_bounds.py`` — 52 LOC — per-DC-node angle bounds
|
|
12
|
+
* ``reserve_method_partitions.py`` — 73 LOC — 3 method partitions
|
|
13
|
+
* ``nonsync_sets.py`` — 153 LOC — process__sink_nonSync + group_inside_group_nonSync
|
|
14
|
+
* ``method_with_fallback_sets.py`` — 194 LOC — 5 per-entity fallback method tables
|
|
15
|
+
* ``invest_total_sets.py`` — 113 LOC — 5 invest/divest-total filters + ci_ladder_cumulative
|
|
16
|
+
* ``structural_filters.py`` — 182 LOC — 6 single-condition filters
|
|
17
|
+
|
|
18
|
+
Total ~913 LOC of legacy code ported. Each ``derive_*`` returns a
|
|
19
|
+
fresh ``pl.DataFrame`` (the in-memory contract); ``write_*`` wrappers
|
|
20
|
+
materialise the frame to the legacy ``solve_data/*.csv`` path so
|
|
21
|
+
downstream consumers continue to read identical bytes.
|
|
22
|
+
|
|
23
|
+
Style mirrors :mod:`._emit_leaf_sets`: eager polars reads of tiny
|
|
24
|
+
CSVs, expression chains, ``unique(maintain_order=True)`` for ordered
|
|
25
|
+
dedup. Constants mirror the legacy module's literals one-for-one.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import polars as pl
|
|
32
|
+
|
|
33
|
+
from flextool.engine_polars._axis_enums import rename_to_axis
|
|
34
|
+
from flextool.engine_polars._emit_provider_io import _emit
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# CSV I/O — same conventions as _emit_leaf_sets:
|
|
39
|
+
# * eager read, missing file → empty frame with requested schema
|
|
40
|
+
# * positional column rename (handle legacy headers that differ in label)
|
|
41
|
+
# * empty frame still writes header line
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def _read_csv(path: Path, columns: list[str],
|
|
45
|
+
*, provider: "object | None" = None) -> pl.DataFrame:
|
|
46
|
+
"""Read a tiny flextool CSV with positional column rename.
|
|
47
|
+
|
|
48
|
+
Forces every column to ``Utf8`` (via ``infer_schema_length=0``)
|
|
49
|
+
regardless of the CSV's header names or data shape. This matters
|
|
50
|
+
because the ``columns=`` arg is the *post-rename* target — it
|
|
51
|
+
cannot be used as ``schema_overrides=`` keys, since those match
|
|
52
|
+
CSV header names (e.g. ``p_commodity.csv`` carries the header
|
|
53
|
+
``commodity,commodityParam,p_commodity`` and gets renamed to
|
|
54
|
+
``commodity,param,value`` here). Without forcing Utf8, polars
|
|
55
|
+
type-inference picks Float64 for all-numeric value columns and
|
|
56
|
+
downstream ``pl.col(...) != ""`` filters raise
|
|
57
|
+
``cannot compare string with numeric type``.
|
|
58
|
+
"""
|
|
59
|
+
# Step 2.5 Phase C — Provider-only. Returns an empty all-Utf8
|
|
60
|
+
# frame on Provider miss (matches legacy missing-CSV behaviour).
|
|
61
|
+
from flextool.engine_polars._emit_provider_io import (
|
|
62
|
+
_provider_key,
|
|
63
|
+
_provider_lookup_positional,
|
|
64
|
+
)
|
|
65
|
+
seeded = _provider_lookup_positional(
|
|
66
|
+
provider, _provider_key(path), path, columns,
|
|
67
|
+
)
|
|
68
|
+
if seeded is not None:
|
|
69
|
+
return seeded
|
|
70
|
+
return pl.DataFrame(
|
|
71
|
+
{c: [] for c in columns}, schema={c: pl.Utf8 for c in columns},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _drop_blank_rows(df: pl.DataFrame, required_cols: list[str]) -> pl.DataFrame:
|
|
76
|
+
expr = pl.col(required_cols[0]) != ""
|
|
77
|
+
for c in required_cols[1:]:
|
|
78
|
+
expr = expr & (pl.col(c) != "")
|
|
79
|
+
return df.filter(expr)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ===========================================================================
|
|
83
|
+
# Family 5 — node_type_sets (legacy: preprocessing/node_type_sets.py)
|
|
84
|
+
# ===========================================================================
|
|
85
|
+
|
|
86
|
+
# Mirror flextool.mod:192 ``default 'balance'`` clause on p_node_type.
|
|
87
|
+
_DEFAULT_NODE_TYPE = "balance"
|
|
88
|
+
|
|
89
|
+
# (output filename, set of effective types that match the partition)
|
|
90
|
+
_NODE_TYPE_PARTITIONS: list[tuple[str, frozenset[str]]] = [
|
|
91
|
+
("nodeCommodity.csv", frozenset(("commodity",))),
|
|
92
|
+
("nodeBalance.csv", frozenset(("balance", "storage"))),
|
|
93
|
+
("nodeState.csv", frozenset(("storage",))),
|
|
94
|
+
("nodeBalancePeriod.csv", frozenset(("balance_within_period",))),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def derive_node_effective_type(input_dir: Path,
|
|
99
|
+
*, provider: "object | None" = None,
|
|
100
|
+
) -> pl.DataFrame:
|
|
101
|
+
"""Materialize every node with its effective ``p_node_type``.
|
|
102
|
+
|
|
103
|
+
Nodes without an explicit row in ``p_node_type.csv`` get the
|
|
104
|
+
flextool.mod default ``'balance'``. Order = ``node.csv`` order
|
|
105
|
+
(mod's would-be iteration order).
|
|
106
|
+
"""
|
|
107
|
+
nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
|
|
108
|
+
nodes = _drop_blank_rows(nodes, ["node"])
|
|
109
|
+
explicit = _read_csv(input_dir / "p_node_type.csv", ["node", "type"],
|
|
110
|
+
provider=provider)
|
|
111
|
+
explicit = _drop_blank_rows(explicit, ["node", "type"])
|
|
112
|
+
return (
|
|
113
|
+
nodes.join(explicit, on="node", how="left")
|
|
114
|
+
.with_columns(
|
|
115
|
+
pl.col("type").fill_null(_DEFAULT_NODE_TYPE),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def emit_node_type_sets(input_dir: Path, solve_data_dir: Path,
|
|
121
|
+
*, provider) -> None:
|
|
122
|
+
"""Emit ``node_type_sets`` to the Provider."""
|
|
123
|
+
del solve_data_dir
|
|
124
|
+
effective = derive_node_effective_type(input_dir, provider=provider)
|
|
125
|
+
for fname, types in _NODE_TYPE_PARTITIONS:
|
|
126
|
+
out = (
|
|
127
|
+
effective.filter(pl.col("type").is_in(list(types)))
|
|
128
|
+
.select("node")
|
|
129
|
+
)
|
|
130
|
+
_emit(provider, f"solve_data/{fname}", out)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ===========================================================================
|
|
134
|
+
# Family 6 — union_sets (legacy: preprocessing/union_sets.py)
|
|
135
|
+
# ===========================================================================
|
|
136
|
+
|
|
137
|
+
def _ordered_union_pairs(
|
|
138
|
+
sources: list[pl.DataFrame], columns: list[str],
|
|
139
|
+
) -> pl.DataFrame:
|
|
140
|
+
"""Vertical-concat then dedupe preserving first-occurrence order."""
|
|
141
|
+
aligned = [df.select(columns) for df in sources]
|
|
142
|
+
return (
|
|
143
|
+
pl.concat(aligned, how="vertical")
|
|
144
|
+
.pipe(_drop_blank_rows, columns)
|
|
145
|
+
.unique(maintain_order=True)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def derive_group_entity(input_dir: Path,
|
|
150
|
+
*, provider: "object | None" = None,
|
|
151
|
+
) -> pl.DataFrame:
|
|
152
|
+
"""flextool.mod:287 — ``group_process ∪ group_node``."""
|
|
153
|
+
gp = _read_csv(input_dir / "group__process.csv", ["group", "entity"],
|
|
154
|
+
provider=provider)
|
|
155
|
+
gn = _read_csv(input_dir / "group__node.csv", ["group", "entity"],
|
|
156
|
+
provider=provider)
|
|
157
|
+
return _ordered_union_pairs([gp, gn], ["group", "entity"])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def derive_process_delayed__duration(input_dir: Path,
|
|
161
|
+
*, provider: "object | None" = None,
|
|
162
|
+
) -> pl.DataFrame:
|
|
163
|
+
"""flextool.mod:950 — ``process_delay_weighted ∪ process_delay_single``."""
|
|
164
|
+
w = _read_csv(input_dir / "p_process_delay_weighted.csv",
|
|
165
|
+
["process", "delay_duration"], provider=provider)
|
|
166
|
+
s = _read_csv(input_dir / "process_delay_single.csv",
|
|
167
|
+
["process", "delay_duration"], provider=provider)
|
|
168
|
+
return _ordered_union_pairs([w, s], ["process", "delay_duration"])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def emit_group_entity(input_dir: Path, solve_data_dir: Path,
|
|
172
|
+
*, provider) -> None:
|
|
173
|
+
"""Emit ``group_entity`` to the Provider."""
|
|
174
|
+
del solve_data_dir
|
|
175
|
+
_emit(provider, "solve_data/group_entity.csv",
|
|
176
|
+
derive_group_entity(input_dir, provider=provider))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def emit_process_delayed__duration(input_dir: Path, solve_data_dir: Path,
|
|
180
|
+
*, provider) -> None:
|
|
181
|
+
"""Emit ``process_delayed__duration`` to the Provider."""
|
|
182
|
+
del solve_data_dir
|
|
183
|
+
_emit(provider, "solve_data/process_delayed__duration.csv",
|
|
184
|
+
derive_process_delayed__duration(input_dir, provider=provider))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ===========================================================================
|
|
188
|
+
# Family 8 — reserve_method_partitions
|
|
189
|
+
# (legacy: preprocessing/reserve_method_partitions.py)
|
|
190
|
+
# ===========================================================================
|
|
191
|
+
|
|
192
|
+
_RESERVE_TIMESERIES_METHODS: frozenset[str] = frozenset((
|
|
193
|
+
"timeseries_only", "timeseries_and_dynamic",
|
|
194
|
+
"timeseries_and_large_failure", "all",
|
|
195
|
+
))
|
|
196
|
+
_RESERVE_DYNAMIC_METHODS: frozenset[str] = frozenset((
|
|
197
|
+
"dynamic_only", "timeseries_and_dynamic",
|
|
198
|
+
"dynamic_and_large_failure", "all",
|
|
199
|
+
))
|
|
200
|
+
_RESERVE_N_1_METHODS: frozenset[str] = frozenset((
|
|
201
|
+
"large_failure_only", "timeseries_and_large_failure",
|
|
202
|
+
"dynamic_and_large_failure", "all",
|
|
203
|
+
))
|
|
204
|
+
|
|
205
|
+
_RESERVE_QUAD_COLS = ["reserve", "upDown", "group", "method"]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def derive_reserve_method_partition(
|
|
209
|
+
input_dir: Path, allowed: frozenset[str],
|
|
210
|
+
*, provider: "object | None" = None,
|
|
211
|
+
) -> pl.DataFrame:
|
|
212
|
+
"""4-tuple rows whose method ∈ allowed. Order preserved, deduped."""
|
|
213
|
+
quad = _read_csv(input_dir / "reserve__upDown__group__method.csv",
|
|
214
|
+
_RESERVE_QUAD_COLS, provider=provider)
|
|
215
|
+
quad = _drop_blank_rows(quad, _RESERVE_QUAD_COLS)
|
|
216
|
+
return (
|
|
217
|
+
quad.filter(pl.col("method").is_in(list(allowed)))
|
|
218
|
+
.unique(maintain_order=True)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def emit_reserve_partitions(input_dir: Path, solve_data_dir: Path,
|
|
223
|
+
*, provider) -> None:
|
|
224
|
+
"""Emit ``reserve_partitions`` to the Provider."""
|
|
225
|
+
del solve_data_dir
|
|
226
|
+
for fname, allowed in (
|
|
227
|
+
("reserve__upDown__group__method_timeseries.csv", _RESERVE_TIMESERIES_METHODS),
|
|
228
|
+
("reserve__upDown__group__method_dynamic.csv", _RESERVE_DYNAMIC_METHODS),
|
|
229
|
+
("reserve__upDown__group__method_n_1.csv", _RESERVE_N_1_METHODS),
|
|
230
|
+
):
|
|
231
|
+
_emit(provider, f"solve_data/{fname}",
|
|
232
|
+
derive_reserve_method_partition(input_dir, allowed,
|
|
233
|
+
provider=provider))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ===========================================================================
|
|
237
|
+
# Family 9 — nonsync_sets (legacy: preprocessing/nonsync_sets.py)
|
|
238
|
+
# ===========================================================================
|
|
239
|
+
|
|
240
|
+
def derive_process__sink_nonSync(input_dir: Path,
|
|
241
|
+
*, provider: "object | None" = None,
|
|
242
|
+
) -> pl.DataFrame:
|
|
243
|
+
"""flextool.mod:1980-1985 — 3-branch OR over sink/source membership.
|
|
244
|
+
|
|
245
|
+
Output: (process, sink) 2-tuples. Branch order (deduped):
|
|
246
|
+
1. ``(p, sink) in process_sink`` AND ``(p, sink) in process__sink_nonSync_unit``
|
|
247
|
+
2. ``(p, sink) in process_sink`` AND ``p in process_nonSync_connection``
|
|
248
|
+
3. ``(p, source) in process_source`` AND ``p in process_nonSync_connection``
|
|
249
|
+
"""
|
|
250
|
+
sinks = _read_csv(input_dir / "process__sink.csv", ["process", "sink"],
|
|
251
|
+
provider=provider)
|
|
252
|
+
sinks = _drop_blank_rows(sinks, ["process", "sink"])
|
|
253
|
+
sources = _read_csv(input_dir / "process__source.csv", ["process", "sink"],
|
|
254
|
+
provider=provider)
|
|
255
|
+
sources = _drop_blank_rows(sources, ["process", "sink"])
|
|
256
|
+
nonsync_units = _read_csv(
|
|
257
|
+
input_dir / "process__sink_nonSync_unit.csv", ["process", "sink"],
|
|
258
|
+
provider=provider,
|
|
259
|
+
)
|
|
260
|
+
nonsync_conn = _read_csv(
|
|
261
|
+
input_dir / "process_nonSync_connection.csv", ["process"],
|
|
262
|
+
provider=provider,
|
|
263
|
+
)
|
|
264
|
+
nonsync_conn_set = (
|
|
265
|
+
nonsync_conn.filter(pl.col("process") != "")
|
|
266
|
+
.get_column("process").to_list()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Branch 1+2: walk sinks once, admit if either condition matches.
|
|
270
|
+
sink_matches = sinks.join(
|
|
271
|
+
nonsync_units, on=["process", "sink"], how="inner",
|
|
272
|
+
).select("process", "sink")
|
|
273
|
+
sink_conn_matches = sinks.filter(
|
|
274
|
+
pl.col("process").is_in(nonsync_conn_set)
|
|
275
|
+
).select("process", "sink")
|
|
276
|
+
# Branch 3: source rows admitted whenever process ∈ nonSync_connection.
|
|
277
|
+
source_conn_matches = sources.filter(
|
|
278
|
+
pl.col("process").is_in(nonsync_conn_set)
|
|
279
|
+
).select("process", "sink")
|
|
280
|
+
|
|
281
|
+
# Legacy walks sinks first (in order), THEN sources. Within the sink
|
|
282
|
+
# walk, branch 1 takes precedence over branch 2 but neither emits
|
|
283
|
+
# duplicates because the dict deduplicates. Reproducing that with
|
|
284
|
+
# vertical-concat-then-unique preserves the same first-seen order.
|
|
285
|
+
combined = pl.concat(
|
|
286
|
+
[sink_matches, sink_conn_matches, source_conn_matches],
|
|
287
|
+
how="vertical",
|
|
288
|
+
)
|
|
289
|
+
return combined.unique(maintain_order=True)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def derive_process__group_inside_group_nonSync(input_dir: Path,
|
|
293
|
+
*, provider: "object | None" = None,
|
|
294
|
+
) -> pl.DataFrame:
|
|
295
|
+
"""flextool.mod:2017-2023 — exists (source, sink) ∈ g, source ≠ sink, for p.
|
|
296
|
+
|
|
297
|
+
Iterate process × groupNonSync in CSV order to match the order the
|
|
298
|
+
mod's nested loops would produce.
|
|
299
|
+
"""
|
|
300
|
+
nonsync_groups = _read_csv(input_dir / "groupNonSync.csv", ["group"],
|
|
301
|
+
provider=provider)
|
|
302
|
+
nonsync_groups = _drop_blank_rows(nonsync_groups, ["group"])
|
|
303
|
+
if nonsync_groups.height == 0:
|
|
304
|
+
return pl.DataFrame({"process": [], "group": []},
|
|
305
|
+
schema={"process": pl.Utf8, "group": pl.Utf8})
|
|
306
|
+
|
|
307
|
+
processes = _read_csv(input_dir / "process.csv", ["process"],
|
|
308
|
+
provider=provider)
|
|
309
|
+
processes = _drop_blank_rows(processes, ["process"])
|
|
310
|
+
group_nodes = _read_csv(input_dir / "group__node.csv", ["group", "node"],
|
|
311
|
+
provider=provider)
|
|
312
|
+
group_nodes = _drop_blank_rows(group_nodes, ["group", "node"])
|
|
313
|
+
sources = _read_csv(input_dir / "process__source.csv", ["process", "node"],
|
|
314
|
+
provider=provider)
|
|
315
|
+
sources = _drop_blank_rows(sources, ["process", "node"])
|
|
316
|
+
sinks = _read_csv(input_dir / "process__sink.csv", ["process", "node"],
|
|
317
|
+
provider=provider)
|
|
318
|
+
sinks = _drop_blank_rows(sinks, ["process", "node"])
|
|
319
|
+
|
|
320
|
+
# Lookup tables. We materialise to python dicts because the per-row
|
|
321
|
+
# set-intersection logic ("∃ source ≠ sink both in g") is awkward to
|
|
322
|
+
# express as a pure polars expression and the input sizes are tiny.
|
|
323
|
+
group_node_lookup: dict[str, set[str]] = {}
|
|
324
|
+
for g, n in group_nodes.iter_rows():
|
|
325
|
+
group_node_lookup.setdefault(g, set()).add(n)
|
|
326
|
+
process_sources: dict[str, set[str]] = {}
|
|
327
|
+
for p, n in sources.iter_rows():
|
|
328
|
+
process_sources.setdefault(p, set()).add(n)
|
|
329
|
+
process_sinks: dict[str, set[str]] = {}
|
|
330
|
+
for p, n in sinks.iter_rows():
|
|
331
|
+
process_sinks.setdefault(p, set()).add(n)
|
|
332
|
+
|
|
333
|
+
nonsync_group_list = nonsync_groups.get_column("group").to_list()
|
|
334
|
+
rows: list[tuple[str, str]] = []
|
|
335
|
+
for p in processes.get_column("process").to_list():
|
|
336
|
+
psrc = process_sources.get(p)
|
|
337
|
+
psnk = process_sinks.get(p)
|
|
338
|
+
if not psrc or not psnk:
|
|
339
|
+
continue
|
|
340
|
+
for g in nonsync_group_list:
|
|
341
|
+
gnodes = group_node_lookup.get(g)
|
|
342
|
+
if not gnodes:
|
|
343
|
+
continue
|
|
344
|
+
srcs_in = psrc & gnodes
|
|
345
|
+
sinks_in = psnk & gnodes
|
|
346
|
+
if not srcs_in or not sinks_in:
|
|
347
|
+
continue
|
|
348
|
+
# ∃ s ≠ t with s ∈ srcs_in, t ∈ sinks_in iff NOT
|
|
349
|
+
# (|srcs|=1 ∧ |sinks|=1 ∧ srcs==sinks).
|
|
350
|
+
if (len(srcs_in) == 1 and len(sinks_in) == 1
|
|
351
|
+
and srcs_in == sinks_in):
|
|
352
|
+
continue
|
|
353
|
+
rows.append((p, g))
|
|
354
|
+
return pl.DataFrame(
|
|
355
|
+
{"process": [r[0] for r in rows], "group": [r[1] for r in rows]},
|
|
356
|
+
schema={"process": pl.Utf8, "group": pl.Utf8},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def emit_process__sink_nonSync(input_dir: Path, solve_data_dir: Path,
|
|
361
|
+
*, provider) -> None:
|
|
362
|
+
"""Emit ``process__sink_nonSync`` to the Provider."""
|
|
363
|
+
del solve_data_dir
|
|
364
|
+
_emit(provider, "solve_data/process__sink_nonSync.csv",
|
|
365
|
+
derive_process__sink_nonSync(input_dir, provider=provider))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def emit_process_group_inside_group_nonsync(
|
|
369
|
+
input_dir: Path, solve_data_dir: Path,
|
|
370
|
+
*, provider,
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Emit ``process_group_inside_group_nonsync`` to the Provider."""
|
|
373
|
+
del solve_data_dir
|
|
374
|
+
_emit(provider, "solve_data/process__group_inside_group_nonSync.csv",
|
|
375
|
+
derive_process__group_inside_group_nonSync(input_dir,
|
|
376
|
+
provider=provider))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ===========================================================================
|
|
380
|
+
# Family 10 — method_with_fallback_sets
|
|
381
|
+
# (legacy: preprocessing/method_with_fallback_sets.py)
|
|
382
|
+
# ===========================================================================
|
|
383
|
+
|
|
384
|
+
# Single-element defaults from flextool/flextool_base.dat — mirror exactly.
|
|
385
|
+
_LIFETIME_METHOD_DEFAULT = "reinvest_automatic"
|
|
386
|
+
_CT_METHOD_REGULAR = "regular"
|
|
387
|
+
_CT_METHOD_CONSTANT = "constant_efficiency"
|
|
388
|
+
_STARTUP_METHOD_NO = "no_startup"
|
|
389
|
+
_INFLOW_METHOD_DEFAULT = "use_original"
|
|
390
|
+
_PENALTY_METHOD_DEFAULT = "regular"
|
|
391
|
+
_STORAGE_BINDING_METHOD_DEFAULT = "bind_forward_only"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _per_entity_fallback(
|
|
395
|
+
explicit: pl.DataFrame,
|
|
396
|
+
entities: pl.DataFrame,
|
|
397
|
+
default_for: dict[str, str | None],
|
|
398
|
+
out_columns: tuple[str, str],
|
|
399
|
+
) -> pl.DataFrame:
|
|
400
|
+
"""Emit explicit rows + a default row for each entity without explicit.
|
|
401
|
+
|
|
402
|
+
``default_for`` maps entity → method-or-None. ``None`` means
|
|
403
|
+
"skip this entity" (mirrors the ``return ()`` branch in the legacy
|
|
404
|
+
``process__ct_method`` fallback for non-connection/non-unit
|
|
405
|
+
processes).
|
|
406
|
+
|
|
407
|
+
Order is ``entities.csv`` order; within an entity, explicit rows
|
|
408
|
+
preserve their CSV order.
|
|
409
|
+
"""
|
|
410
|
+
entity_col, method_col = out_columns
|
|
411
|
+
explicit = explicit.pipe(rename_to_axis,
|
|
412
|
+
{explicit.columns[0]: entity_col,
|
|
413
|
+
explicit.columns[1]: method_col})
|
|
414
|
+
|
|
415
|
+
explicit_by_entity: dict[str, list[str]] = {}
|
|
416
|
+
for e, m in explicit.iter_rows():
|
|
417
|
+
explicit_by_entity.setdefault(e, []).append(m)
|
|
418
|
+
|
|
419
|
+
rows: list[tuple[str, str]] = []
|
|
420
|
+
for e in entities.get_column(entities.columns[0]).to_list():
|
|
421
|
+
if e in explicit_by_entity:
|
|
422
|
+
for m in explicit_by_entity[e]:
|
|
423
|
+
rows.append((e, m))
|
|
424
|
+
else:
|
|
425
|
+
default = default_for.get(e)
|
|
426
|
+
if default is not None:
|
|
427
|
+
rows.append((e, default))
|
|
428
|
+
return pl.DataFrame(
|
|
429
|
+
{entity_col: [r[0] for r in rows], method_col: [r[1] for r in rows]},
|
|
430
|
+
schema={entity_col: pl.Utf8, method_col: pl.Utf8},
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def derive_entity_lifetime_method(input_dir: Path,
|
|
435
|
+
*, provider: "object | None" = None,
|
|
436
|
+
) -> pl.DataFrame:
|
|
437
|
+
explicit = _read_csv(
|
|
438
|
+
input_dir / "entity__lifetime_method.csv",
|
|
439
|
+
["entity", "lifetime_method"], provider=provider,
|
|
440
|
+
)
|
|
441
|
+
explicit = _drop_blank_rows(explicit, ["entity", "lifetime_method"])
|
|
442
|
+
entities = _read_csv(input_dir / "entity.csv", ["entity"],
|
|
443
|
+
provider=provider)
|
|
444
|
+
entities = _drop_blank_rows(entities, ["entity"])
|
|
445
|
+
defaults = {e: _LIFETIME_METHOD_DEFAULT
|
|
446
|
+
for e in entities.get_column("entity").to_list()}
|
|
447
|
+
return _per_entity_fallback(
|
|
448
|
+
explicit, entities, defaults, ("entity", "lifetime_method"),
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def derive_process_ct_method(input_dir: Path,
|
|
453
|
+
*, provider: "object | None" = None,
|
|
454
|
+
) -> pl.DataFrame:
|
|
455
|
+
explicit = _read_csv(
|
|
456
|
+
input_dir / "process__ct_method.csv", ["process", "ct_method"],
|
|
457
|
+
provider=provider,
|
|
458
|
+
)
|
|
459
|
+
explicit = _drop_blank_rows(explicit, ["process", "ct_method"])
|
|
460
|
+
processes = _read_csv(input_dir / "process.csv", ["process"],
|
|
461
|
+
provider=provider)
|
|
462
|
+
processes = _drop_blank_rows(processes, ["process"])
|
|
463
|
+
connections = set(
|
|
464
|
+
_drop_blank_rows(
|
|
465
|
+
_read_csv(input_dir / "process_connection.csv", ["process"],
|
|
466
|
+
provider=provider), ["process"],
|
|
467
|
+
).get_column("process").to_list()
|
|
468
|
+
)
|
|
469
|
+
units = set(
|
|
470
|
+
_drop_blank_rows(
|
|
471
|
+
_read_csv(input_dir / "process_unit.csv", ["process"],
|
|
472
|
+
provider=provider), ["process"],
|
|
473
|
+
).get_column("process").to_list()
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
defaults: dict[str, str | None] = {}
|
|
477
|
+
for p in processes.get_column("process").to_list():
|
|
478
|
+
if p in connections:
|
|
479
|
+
defaults[p] = _CT_METHOD_REGULAR
|
|
480
|
+
elif p in units:
|
|
481
|
+
defaults[p] = _CT_METHOD_CONSTANT
|
|
482
|
+
else:
|
|
483
|
+
defaults[p] = None
|
|
484
|
+
return _per_entity_fallback(
|
|
485
|
+
explicit, processes, defaults, ("process", "ct_method"),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def derive_node_inflow_method(input_dir: Path,
|
|
490
|
+
*, provider: "object | None" = None,
|
|
491
|
+
) -> pl.DataFrame:
|
|
492
|
+
explicit = _read_csv(
|
|
493
|
+
input_dir / "node__inflow_method.csv", ["node", "inflow_method"],
|
|
494
|
+
provider=provider,
|
|
495
|
+
)
|
|
496
|
+
explicit = _drop_blank_rows(explicit, ["node", "inflow_method"])
|
|
497
|
+
nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
|
|
498
|
+
nodes = _drop_blank_rows(nodes, ["node"])
|
|
499
|
+
defaults = {n: _INFLOW_METHOD_DEFAULT
|
|
500
|
+
for n in nodes.get_column("node").to_list()}
|
|
501
|
+
return _per_entity_fallback(
|
|
502
|
+
explicit, nodes, defaults, ("node", "inflow_method"),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def derive_node_penalty_method(input_dir: Path,
|
|
507
|
+
*, provider: "object | None" = None,
|
|
508
|
+
) -> pl.DataFrame:
|
|
509
|
+
explicit = _read_csv(
|
|
510
|
+
input_dir / "node__penalty_method.csv", ["node", "penalty_method"],
|
|
511
|
+
provider=provider,
|
|
512
|
+
)
|
|
513
|
+
explicit = _drop_blank_rows(explicit, ["node", "penalty_method"])
|
|
514
|
+
nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
|
|
515
|
+
nodes = _drop_blank_rows(nodes, ["node"])
|
|
516
|
+
defaults = {n: _PENALTY_METHOD_DEFAULT
|
|
517
|
+
for n in nodes.get_column("node").to_list()}
|
|
518
|
+
return _per_entity_fallback(
|
|
519
|
+
explicit, nodes, defaults, ("node", "penalty_method"),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def derive_node_storage_binding_method(input_dir: Path,
|
|
524
|
+
*, provider: "object | None" = None,
|
|
525
|
+
) -> pl.DataFrame:
|
|
526
|
+
explicit = _read_csv(
|
|
527
|
+
input_dir / "node__storage_binding_method.csv",
|
|
528
|
+
["node", "storage_binding_method"], provider=provider,
|
|
529
|
+
)
|
|
530
|
+
explicit = _drop_blank_rows(explicit, ["node", "storage_binding_method"])
|
|
531
|
+
nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
|
|
532
|
+
nodes = _drop_blank_rows(nodes, ["node"])
|
|
533
|
+
defaults = {n: _STORAGE_BINDING_METHOD_DEFAULT
|
|
534
|
+
for n in nodes.get_column("node").to_list()}
|
|
535
|
+
frame = _per_entity_fallback(
|
|
536
|
+
explicit, nodes, defaults, ("node", "storage_binding_method"),
|
|
537
|
+
)
|
|
538
|
+
# Single-valued contract (v54 migration): each node must appear at most
|
|
539
|
+
# once. A duplicate here means either a v52-era DB slipped past the
|
|
540
|
+
# v54 migration in update_flextool/db_migration.py, or a multi-row
|
|
541
|
+
# record was manually inserted into node__storage_binding_method.csv.
|
|
542
|
+
# Compare heights rather than silently dedup so the contract violation
|
|
543
|
+
# surfaces as an explicit error.
|
|
544
|
+
unique_node_height = frame.select("node").unique().height
|
|
545
|
+
if frame.height != unique_node_height:
|
|
546
|
+
from flextool.engine_polars._solve_state import FlexToolConfigError
|
|
547
|
+
dup_nodes = (frame
|
|
548
|
+
.group_by("node")
|
|
549
|
+
.agg(pl.len().alias("_count"))
|
|
550
|
+
.filter(pl.col("_count") > 1)
|
|
551
|
+
.get_column("node")
|
|
552
|
+
.to_list())
|
|
553
|
+
raise FlexToolConfigError(
|
|
554
|
+
"node__storage_binding_method must have a single method per "
|
|
555
|
+
"node (v54 single-valued contract); duplicate node entries "
|
|
556
|
+
f"detected: {sorted(dup_nodes)}. Re-run the v54 DB migration "
|
|
557
|
+
"or remove the extra rows from "
|
|
558
|
+
"input/node__storage_binding_method.csv."
|
|
559
|
+
)
|
|
560
|
+
return frame
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def derive_process_startup_method(input_dir: Path,
|
|
564
|
+
*, provider: "object | None" = None,
|
|
565
|
+
) -> pl.DataFrame:
|
|
566
|
+
explicit = _read_csv(
|
|
567
|
+
input_dir / "process__startup_method.csv",
|
|
568
|
+
["process", "startup_method"], provider=provider,
|
|
569
|
+
)
|
|
570
|
+
explicit = _drop_blank_rows(explicit, ["process", "startup_method"])
|
|
571
|
+
processes = _read_csv(input_dir / "process.csv", ["process"],
|
|
572
|
+
provider=provider)
|
|
573
|
+
processes = _drop_blank_rows(processes, ["process"])
|
|
574
|
+
defaults = {p: _STARTUP_METHOD_NO
|
|
575
|
+
for p in processes.get_column("process").to_list()}
|
|
576
|
+
return _per_entity_fallback(
|
|
577
|
+
explicit, processes, defaults, ("process", "startup_method"),
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def emit_entity_lifetime_method(input_dir: Path, solve_data_dir: Path,
|
|
582
|
+
*, provider) -> None:
|
|
583
|
+
"""Emit ``entity_lifetime_method`` to the Provider."""
|
|
584
|
+
del solve_data_dir
|
|
585
|
+
_emit(provider, "solve_data/entity__lifetime_method.csv",
|
|
586
|
+
derive_entity_lifetime_method(input_dir, provider=provider))
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def emit_process_ct_method(input_dir: Path, solve_data_dir: Path,
|
|
590
|
+
*, provider) -> None:
|
|
591
|
+
"""Emit ``process_ct_method`` to the Provider."""
|
|
592
|
+
del solve_data_dir
|
|
593
|
+
_emit(provider, "solve_data/process__ct_method.csv",
|
|
594
|
+
derive_process_ct_method(input_dir, provider=provider))
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def emit_process_startup_method(input_dir: Path, solve_data_dir: Path,
|
|
598
|
+
*, provider) -> None:
|
|
599
|
+
"""Emit ``process_startup_method`` to the Provider."""
|
|
600
|
+
del solve_data_dir
|
|
601
|
+
_emit(provider, "solve_data/process__startup_method.csv",
|
|
602
|
+
derive_process_startup_method(input_dir, provider=provider))
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def emit_node_inflow_method(input_dir: Path, solve_data_dir: Path,
|
|
606
|
+
*, provider) -> None:
|
|
607
|
+
"""Emit ``node_inflow_method`` to the Provider."""
|
|
608
|
+
del solve_data_dir
|
|
609
|
+
_emit(provider, "solve_data/node__inflow_method.csv",
|
|
610
|
+
derive_node_inflow_method(input_dir, provider=provider))
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def emit_node_penalty_method(input_dir: Path, solve_data_dir: Path,
|
|
614
|
+
*, provider) -> None:
|
|
615
|
+
"""Emit ``node_penalty_method`` to the Provider."""
|
|
616
|
+
del solve_data_dir
|
|
617
|
+
_emit(provider, "solve_data/node__penalty_method.csv",
|
|
618
|
+
derive_node_penalty_method(input_dir, provider=provider))
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def emit_node_storage_binding_method(input_dir: Path, solve_data_dir: Path,
|
|
622
|
+
*, provider) -> None:
|
|
623
|
+
"""Emit ``node_storage_binding_method`` to the Provider."""
|
|
624
|
+
del solve_data_dir
|
|
625
|
+
_emit(provider, "solve_data/node__storage_binding_method.csv",
|
|
626
|
+
derive_node_storage_binding_method(input_dir, provider=provider))
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def derive_node_storage_nested_fix_method(
|
|
630
|
+
input_dir: Path, *, provider: "object | None" = None,
|
|
631
|
+
) -> pl.DataFrame:
|
|
632
|
+
"""Explicit node→method rows for the nested-solve storage handoff.
|
|
633
|
+
|
|
634
|
+
Reads ``input/node__storage_nested_fix_method.csv`` (cols
|
|
635
|
+
``node, storage_nested_fix_method`` from the generic
|
|
636
|
+
``_PARAMETER_SPECS`` emit) and renames the value column to the
|
|
637
|
+
``method`` contract every solve-time consumer expects
|
|
638
|
+
(``_load_handoff_aux_pair`` in ``input.py`` and the
|
|
639
|
+
``build_handoff_from_solution`` producer guards, which all check for
|
|
640
|
+
a column literally named ``method``). This is the crux deviation
|
|
641
|
+
from the sibling ``derive_node_storage_binding_method``, whose
|
|
642
|
+
consumer reads positionally and so tolerates the value column's name.
|
|
643
|
+
|
|
644
|
+
Unlike the sibling this emits ONLY the explicit rows (no per-node
|
|
645
|
+
``fix_nothing`` fallback): absence == ``fix_nothing`` == inert, and
|
|
646
|
+
every downstream consumer filters ``method == 'fix_<x>'``, so
|
|
647
|
+
unlisted nodes need no row. The frame may be empty (header only) for
|
|
648
|
+
models with no nested-fix node; ``_emit`` still materialises it so
|
|
649
|
+
the coverage manifest is satisfied and the loader's
|
|
650
|
+
``height == 0 -> None`` short-circuit keeps such models unchanged.
|
|
651
|
+
"""
|
|
652
|
+
explicit = _read_csv(
|
|
653
|
+
input_dir / "node__storage_nested_fix_method.csv",
|
|
654
|
+
["node", "storage_nested_fix_method"], provider=provider,
|
|
655
|
+
)
|
|
656
|
+
explicit = _drop_blank_rows(explicit, ["node", "storage_nested_fix_method"])
|
|
657
|
+
return explicit.rename({"storage_nested_fix_method": "method"})
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def emit_node_storage_nested_fix_method(input_dir: Path, solve_data_dir: Path,
|
|
661
|
+
*, provider) -> None:
|
|
662
|
+
"""Emit ``node_storage_nested_fix_method`` to the Provider."""
|
|
663
|
+
del solve_data_dir
|
|
664
|
+
_emit(provider, "solve_data/node__storage_nested_fix_method.csv",
|
|
665
|
+
derive_node_storage_nested_fix_method(input_dir, provider=provider))
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# ===========================================================================
|
|
669
|
+
# Family 11 — invest_total_sets (legacy: preprocessing/invest_total_sets.py)
|
|
670
|
+
# ===========================================================================
|
|
671
|
+
|
|
672
|
+
_INVEST_TOTAL_METHODS: frozenset[str] = frozenset((
|
|
673
|
+
"invest_total", "invest_period_total",
|
|
674
|
+
"invest_retire_total", "invest_retire_period_total",
|
|
675
|
+
))
|
|
676
|
+
_RETIRE_TOTAL_METHODS: frozenset[str] = frozenset((
|
|
677
|
+
"retire_total", "retire_period_total",
|
|
678
|
+
"invest_retire_total", "invest_retire_period_total",
|
|
679
|
+
))
|
|
680
|
+
_CUMULATIVE_METHODS: frozenset[str] = frozenset(("cumulative_limits",))
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _entities_with_method_in(
|
|
684
|
+
method_csv: Path, allowed: frozenset[str], col1: str,
|
|
685
|
+
*, provider: "object | None" = None,
|
|
686
|
+
) -> set[str]:
|
|
687
|
+
df = _read_csv(method_csv, [col1, "method"], provider=provider)
|
|
688
|
+
df = _drop_blank_rows(df, [col1, "method"])
|
|
689
|
+
return set(
|
|
690
|
+
df.filter(pl.col("method").is_in(list(allowed)))
|
|
691
|
+
.get_column(col1).to_list()
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _filter_singles(
|
|
696
|
+
universe_csv: Path, with_method: set[str], col: str,
|
|
697
|
+
*, provider: "object | None" = None,
|
|
698
|
+
) -> pl.DataFrame:
|
|
699
|
+
universe = _read_csv(universe_csv, [col], provider=provider)
|
|
700
|
+
universe = _drop_blank_rows(universe, [col])
|
|
701
|
+
return universe.filter(pl.col(col).is_in(list(with_method)))
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def emit_invest_total_sets(input_dir: Path, solve_data_dir: Path,
|
|
705
|
+
*, provider) -> None:
|
|
706
|
+
"""Emit ``invest_total_sets`` to the Provider."""
|
|
707
|
+
entity_methods_csv = input_dir / "entity__invest_method.csv"
|
|
708
|
+
group_methods_csv = input_dir / "group__invest_method.csv"
|
|
709
|
+
|
|
710
|
+
e_with_invest = _entities_with_method_in(
|
|
711
|
+
entity_methods_csv, _INVEST_TOTAL_METHODS, "entity", provider=provider)
|
|
712
|
+
e_with_retire = _entities_with_method_in(
|
|
713
|
+
entity_methods_csv, _RETIRE_TOTAL_METHODS, "entity", provider=provider)
|
|
714
|
+
g_with_invest = _entities_with_method_in(
|
|
715
|
+
group_methods_csv, _INVEST_TOTAL_METHODS, "group", provider=provider)
|
|
716
|
+
g_with_retire = _entities_with_method_in(
|
|
717
|
+
group_methods_csv, _RETIRE_TOTAL_METHODS, "group", provider=provider)
|
|
718
|
+
g_with_cum = _entities_with_method_in(
|
|
719
|
+
group_methods_csv, _CUMULATIVE_METHODS, "group", provider=provider)
|
|
720
|
+
|
|
721
|
+
_emit(provider, "solve_data/e_invest_total.csv",
|
|
722
|
+
_filter_singles(solve_data_dir / "entityInvest.csv", e_with_invest,
|
|
723
|
+
"entity", provider=provider))
|
|
724
|
+
_emit(provider, "solve_data/e_divest_total.csv",
|
|
725
|
+
_filter_singles(solve_data_dir / "entityDivest.csv", e_with_retire,
|
|
726
|
+
"entity", provider=provider))
|
|
727
|
+
_emit(provider, "solve_data/g_invest_total.csv",
|
|
728
|
+
_filter_singles(solve_data_dir / "group_invest.csv", g_with_invest,
|
|
729
|
+
"group", provider=provider))
|
|
730
|
+
_emit(provider, "solve_data/g_divest_total.csv",
|
|
731
|
+
_filter_singles(solve_data_dir / "group_divest.csv", g_with_retire,
|
|
732
|
+
"group", provider=provider))
|
|
733
|
+
_emit(provider, "solve_data/g_invest_cumulative.csv",
|
|
734
|
+
_filter_singles(solve_data_dir / "group_invest.csv", g_with_cum,
|
|
735
|
+
"group", provider=provider))
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def emit_ci_ladder_cumulative(input_dir: Path, solve_data_dir: Path,
|
|
739
|
+
*, provider) -> None:
|
|
740
|
+
"""Emit ``ci_ladder_cumulative`` to the Provider."""
|
|
741
|
+
cum = _read_csv(
|
|
742
|
+
input_dir / "commodity_ladder_cumulative.csv", ["commodity", "tier"],
|
|
743
|
+
provider=provider,
|
|
744
|
+
)
|
|
745
|
+
cum = _drop_blank_rows(cum, ["commodity", "tier"])
|
|
746
|
+
with_cum = _read_csv(
|
|
747
|
+
solve_data_dir / "commodity_with_ladder_cumulative.csv", ["commodity"],
|
|
748
|
+
provider=provider,
|
|
749
|
+
)
|
|
750
|
+
with_cum_set = (
|
|
751
|
+
with_cum.filter(pl.col("commodity") != "")
|
|
752
|
+
.get_column("commodity").to_list()
|
|
753
|
+
)
|
|
754
|
+
filtered = (
|
|
755
|
+
cum.filter(pl.col("commodity").is_in(with_cum_set))
|
|
756
|
+
.unique(maintain_order=True)
|
|
757
|
+
)
|
|
758
|
+
_emit(provider, "solve_data/ci_ladder_cumulative.csv", filtered)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
# ===========================================================================
|
|
762
|
+
# Family 12 — structural_filters (legacy: preprocessing/structural_filters.py)
|
|
763
|
+
# ===========================================================================
|
|
764
|
+
|
|
765
|
+
def derive_commodity_node_co2(input_dir: Path,
|
|
766
|
+
*, provider: "object | None" = None,
|
|
767
|
+
) -> pl.DataFrame:
|
|
768
|
+
"""commodity_node filtered by ``p_commodity[c, 'co2_content'] != 0``.
|
|
769
|
+
|
|
770
|
+
flextool.mod:2011. ``default 0`` on p_commodity means commodities
|
|
771
|
+
without an explicit ``co2_content`` row are excluded (0 is falsy).
|
|
772
|
+
"""
|
|
773
|
+
cn = _read_csv(input_dir / "commodity__node.csv", ["commodity", "node"],
|
|
774
|
+
provider=provider)
|
|
775
|
+
cn = _drop_blank_rows(cn, ["commodity", "node"])
|
|
776
|
+
p_commodity = _read_csv(
|
|
777
|
+
input_dir / "p_commodity.csv", ["commodity", "param", "value"],
|
|
778
|
+
provider=provider,
|
|
779
|
+
)
|
|
780
|
+
co2 = (
|
|
781
|
+
p_commodity.filter(
|
|
782
|
+
(pl.col("param") == "co2_content")
|
|
783
|
+
& (pl.col("value") != "")
|
|
784
|
+
& (pl.col("value").cast(pl.Float64, strict=False) != 0.0)
|
|
785
|
+
)
|
|
786
|
+
.select("commodity")
|
|
787
|
+
.unique()
|
|
788
|
+
)
|
|
789
|
+
return (
|
|
790
|
+
cn.join(co2, on="commodity", how="semi")
|
|
791
|
+
.unique(maintain_order=True)
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _derive_coeff_zero(
|
|
796
|
+
arc_csv: Path, coef_csv: Path, second_col: str,
|
|
797
|
+
*, provider: "object | None" = None,
|
|
798
|
+
) -> pl.DataFrame:
|
|
799
|
+
"""(process, source/sink) rows whose max-capacity-coefficient is 0.
|
|
800
|
+
|
|
801
|
+
Default of 1 on missing coefficients means only EXPLICITLY-zero rows
|
|
802
|
+
appear in the output (matches the mod's truthy-check behaviour).
|
|
803
|
+
"""
|
|
804
|
+
arcs = _read_csv(arc_csv, ["process", second_col], provider=provider)
|
|
805
|
+
arcs = _drop_blank_rows(arcs, ["process", second_col])
|
|
806
|
+
coef = _read_csv(coef_csv, ["process", second_col, "value"],
|
|
807
|
+
provider=provider)
|
|
808
|
+
zeros = (
|
|
809
|
+
coef.filter(
|
|
810
|
+
(pl.col("process") != "")
|
|
811
|
+
& (pl.col(second_col) != "")
|
|
812
|
+
& (pl.col("value") != "")
|
|
813
|
+
& (pl.col("value").cast(pl.Float64, strict=False) == 0.0)
|
|
814
|
+
)
|
|
815
|
+
.select("process", second_col)
|
|
816
|
+
.unique()
|
|
817
|
+
)
|
|
818
|
+
return (
|
|
819
|
+
arcs.join(zeros, on=["process", second_col], how="semi")
|
|
820
|
+
.unique(maintain_order=True)
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def derive_process_source_coeff_zero(input_dir: Path,
|
|
825
|
+
*, provider: "object | None" = None,
|
|
826
|
+
) -> pl.DataFrame:
|
|
827
|
+
return _derive_coeff_zero(
|
|
828
|
+
input_dir / "process__source.csv",
|
|
829
|
+
input_dir / "p_process_source_capacity_max_coeff.csv",
|
|
830
|
+
"source", provider=provider,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def derive_process_sink_coeff_zero(input_dir: Path,
|
|
835
|
+
*, provider: "object | None" = None,
|
|
836
|
+
) -> pl.DataFrame:
|
|
837
|
+
return _derive_coeff_zero(
|
|
838
|
+
input_dir / "process__sink.csv",
|
|
839
|
+
input_dir / "p_process_sink_capacity_max_coeff.csv",
|
|
840
|
+
"sink", provider=provider,
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def emit_commodity_node_co2(input_dir: Path, solve_data_dir: Path,
|
|
845
|
+
*, provider) -> None:
|
|
846
|
+
"""Emit ``commodity_node_co2`` to the Provider."""
|
|
847
|
+
del solve_data_dir
|
|
848
|
+
_emit(provider, "solve_data/commodity_node_co2.csv",
|
|
849
|
+
derive_commodity_node_co2(input_dir, provider=provider))
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def emit_process_coeff_zero_sets(input_dir: Path, solve_data_dir: Path,
|
|
853
|
+
*, provider) -> None:
|
|
854
|
+
"""Emit ``process_coeff_zero_sets`` to the Provider."""
|
|
855
|
+
del solve_data_dir
|
|
856
|
+
_emit(provider, "solve_data/process_source_coeff_zero.csv",
|
|
857
|
+
derive_process_source_coeff_zero(input_dir, provider=provider))
|
|
858
|
+
_emit(provider, "solve_data/process_sink_coeff_zero.csv",
|
|
859
|
+
derive_process_sink_coeff_zero(input_dir, provider=provider))
|