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,1249 @@
|
|
|
1
|
+
"""Per-solve preprocessing emitters — timeline / period / branch / RP frames.
|
|
2
|
+
|
|
3
|
+
This module owns the ~36 ``emit_*`` functions that build the
|
|
4
|
+
``solve_data/*.csv`` artefacts each sub-solve consumes:
|
|
5
|
+
|
|
6
|
+
* Timeline & period: ``steps_in_timeline``, ``steps_in_use``,
|
|
7
|
+
``steps_complete_solve``, ``step_previous``, ``period_block_time``,
|
|
8
|
+
``period_block_succ``, ``p_years_represented``,
|
|
9
|
+
``period_with_history``, ``p_discount_years``,
|
|
10
|
+
``realized_invest_periods_of_current_solve``,
|
|
11
|
+
``invest_periods_of_current_solve``, ``period_last``,
|
|
12
|
+
``period_first_of_solve``, ``period_first``, ``p_model``,
|
|
13
|
+
``p_nested_model``, ``solve_current``, ``first_timesteps``,
|
|
14
|
+
``last_timesteps``,
|
|
15
|
+
``realized_dispatch``, ``fix_storage_timesteps``.
|
|
16
|
+
* Branch: ``period__branch``,
|
|
17
|
+
``solve_branch_weight``, ``solve_branch__time_branch``.
|
|
18
|
+
* Empty / seed: ``p_entity_invested``, ``p_entity_divested``,
|
|
19
|
+
``p_entity_period_existing_capacity``, the three
|
|
20
|
+
rolling-accumulator seeds, the four fix-storage seeds.
|
|
21
|
+
* Headers / timesets: header-only seeds, ``timesets_in_use``,
|
|
22
|
+
``timesets__timeline``.
|
|
23
|
+
* Scaling / delay / RP: ``p_use_row_scaling``,
|
|
24
|
+
``scale_the_objective``, ``delay_duration``,
|
|
25
|
+
``dtt__delay_duration``, and the two representative-period CSVs
|
|
26
|
+
(``rp_weights``, ``rp_base_chain``, ``rp_base_first``,
|
|
27
|
+
``rp_base_last``, ``rp_block_first``, ``rp_block_last``,
|
|
28
|
+
``rp_block_start_last``, ``timestep_weight``).
|
|
29
|
+
|
|
30
|
+
Each ``emit_*`` builds an all-``Utf8`` polars frame (inline or via a
|
|
31
|
+
shared ``_compute_*`` helper) and registers it on the Provider with the
|
|
32
|
+
dual ``basename`` + ``parent/basename`` keys downstream readers expect.
|
|
33
|
+
When the cascade later flushes to disk (``--csv-dump``), the CRLF line
|
|
34
|
+
terminator preserved through the snapshot path matches the legacy
|
|
35
|
+
``csv.writer`` + ``newline=""`` byte shape exactly.
|
|
36
|
+
|
|
37
|
+
The scaling CSVs use ``%.17g`` repr formatting for round-trip safety.
|
|
38
|
+
``emit_rp_data`` preserves the canonical ordering and the
|
|
39
|
+
``weight > 1e-10`` epsilon filter. ``emit_timeset_cost_weight`` uses
|
|
40
|
+
``%.10g`` for the normalised float.
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import math
|
|
45
|
+
from collections import defaultdict
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import Any
|
|
48
|
+
|
|
49
|
+
import polars as pl
|
|
50
|
+
|
|
51
|
+
from flextool.engine_polars import _provider_keys as K
|
|
52
|
+
from flextool.engine_polars._emit_provider_io import _emit
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _emit_path(provider, path: "Path | str", df: pl.DataFrame) -> None:
|
|
56
|
+
"""Provider-emit *df* under both ``basename`` and ``parent/basename``.
|
|
57
|
+
|
|
58
|
+
The dual-key registration matches downstream readers' lookup
|
|
59
|
+
conventions; either form resolves to the same frame.
|
|
60
|
+
"""
|
|
61
|
+
p = Path(path)
|
|
62
|
+
parent = p.parent.name
|
|
63
|
+
key = f"{parent}/{p.name}" if parent else p.name
|
|
64
|
+
_emit(provider, key, df)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Frame construction — :func:`_to_utf8_frame` builds an all-``Utf8``
|
|
69
|
+
# polars frame from a header tuple and a row list. Every value is taken
|
|
70
|
+
# verbatim if already a string, otherwise stringified via ``str(v)`` —
|
|
71
|
+
# matching ``csv.writer``'s behaviour for the types this module emits
|
|
72
|
+
# (``int`` 0/1 flags, ``float`` weights, plain strings). Schema is
|
|
73
|
+
# locked to ``Utf8`` per column so polars' ``write_csv`` does no extra
|
|
74
|
+
# numeric formatting or quoting at flush time.
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _to_utf8_frame(
|
|
79
|
+
headers: tuple[str, ...],
|
|
80
|
+
rows: list[tuple[Any, ...]],
|
|
81
|
+
) -> pl.DataFrame:
|
|
82
|
+
"""Build an all-``Utf8`` polars frame from a header tuple + row list.
|
|
83
|
+
|
|
84
|
+
Each row cell is taken as-is when already a string, otherwise
|
|
85
|
+
rendered via ``str(v)`` — matching ``csv.writer``'s exact byte
|
|
86
|
+
output for ``int`` / ``float`` cells.
|
|
87
|
+
"""
|
|
88
|
+
cols: dict[str, list[str]] = {h: [] for h in headers}
|
|
89
|
+
for row in rows:
|
|
90
|
+
for h, v in zip(headers, row):
|
|
91
|
+
cols[h].append(v if isinstance(v, str) else str(v))
|
|
92
|
+
return pl.DataFrame(cols, schema={h: pl.Utf8 for h in headers})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Group A — timeline writers
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def derive_full_timelines(
|
|
101
|
+
stochastic_timesteps: list[tuple[str, str]],
|
|
102
|
+
period__timesets_in_this_solve: list[tuple[str, str]],
|
|
103
|
+
timesets__timeline: dict[str, str],
|
|
104
|
+
timelines: dict[str, list[tuple[str, ...]]],
|
|
105
|
+
) -> pl.DataFrame:
|
|
106
|
+
"""Build the canonical ``steps_in_timeline`` frame (period, step)."""
|
|
107
|
+
rows: list[tuple[Any, ...]] = []
|
|
108
|
+
for period__timeset in period__timesets_in_this_solve:
|
|
109
|
+
for timeline in timelines:
|
|
110
|
+
for timeset_in_timeline, tt in timesets__timeline.items():
|
|
111
|
+
if period__timeset[1] == timeset_in_timeline:
|
|
112
|
+
if timeline == tt:
|
|
113
|
+
for item in timelines[timeline]:
|
|
114
|
+
rows.append((period__timeset[0], item[0]))
|
|
115
|
+
for step in stochastic_timesteps:
|
|
116
|
+
rows.append((step[0], step[1]))
|
|
117
|
+
return _to_utf8_frame(("period", "step"), rows)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def emit_full_timelines(
|
|
121
|
+
stochastic_timesteps: list[tuple[str, str]],
|
|
122
|
+
period__timesets_in_this_solve: list[tuple[str, str]],
|
|
123
|
+
timesets__timeline: dict[str, str],
|
|
124
|
+
timelines: dict[str, list[tuple[str, ...]]],
|
|
125
|
+
filename: str,
|
|
126
|
+
*, provider,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Emit ``full_timelines`` to the Provider."""
|
|
129
|
+
_emit_path(provider, filename,
|
|
130
|
+
derive_full_timelines(
|
|
131
|
+
stochastic_timesteps,
|
|
132
|
+
period__timesets_in_this_solve,
|
|
133
|
+
timesets__timeline,
|
|
134
|
+
timelines,
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def derive_active_timelines(
|
|
139
|
+
timeline: dict[str, list[tuple[str, ...]]],
|
|
140
|
+
complete: bool = False,
|
|
141
|
+
) -> pl.DataFrame:
|
|
142
|
+
"""Build the canonical ``steps_in_use`` / ``steps_complete_solve``
|
|
143
|
+
frame (period, step, [complete_]step_duration).
|
|
144
|
+
"""
|
|
145
|
+
header_dur = "complete_step_duration" if complete else "step_duration"
|
|
146
|
+
rows: list[tuple[Any, ...]] = []
|
|
147
|
+
for period_name, period in timeline.items():
|
|
148
|
+
for item in period:
|
|
149
|
+
rows.append((period_name, item.timestep, str(item.duration)))
|
|
150
|
+
return _to_utf8_frame(("period", "step", header_dur), rows)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def emit_active_timelines(
|
|
154
|
+
timeline: dict[str, list[tuple[str, ...]]],
|
|
155
|
+
filename: str,
|
|
156
|
+
complete: bool = False,
|
|
157
|
+
*, provider,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Emit ``active_timelines`` to the Provider."""
|
|
160
|
+
_emit_path(provider, filename,
|
|
161
|
+
derive_active_timelines(timeline, complete))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_STEP_JUMP_HEADERS = (
|
|
165
|
+
"period", "time", "previous", "previous_within_timeset",
|
|
166
|
+
"previous_period", "previous_within_solve", "jump",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def derive_step_jump(step_lengths: list[tuple[str, ...]]) -> pl.DataFrame:
|
|
171
|
+
"""Build the canonical ``step_previous`` frame (7 Utf8 columns)."""
|
|
172
|
+
return _to_utf8_frame(_STEP_JUMP_HEADERS, list(step_lengths))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def emit_step_jump(
|
|
176
|
+
step_lengths: list[tuple[str, ...]],
|
|
177
|
+
*, provider,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Emit ``step_jump`` to the Provider."""
|
|
180
|
+
_emit(provider, "solve_data/step_previous.csv",
|
|
181
|
+
derive_step_jump(step_lengths))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def derive_period_block_time(
|
|
185
|
+
period_block_time: list[tuple],
|
|
186
|
+
) -> pl.DataFrame:
|
|
187
|
+
"""Build the ``period_block_time`` frame (period, block_first, step)."""
|
|
188
|
+
return _to_utf8_frame(
|
|
189
|
+
("period", "block_first", "step"), list(period_block_time),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def derive_period_block_succ(
|
|
194
|
+
period_block_succ: list[tuple],
|
|
195
|
+
) -> pl.DataFrame:
|
|
196
|
+
"""Build the ``period_block_succ`` frame
|
|
197
|
+
(period, block_first, block_first_next).
|
|
198
|
+
"""
|
|
199
|
+
return _to_utf8_frame(
|
|
200
|
+
("period", "block_first", "block_first_next"),
|
|
201
|
+
list(period_block_succ),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def emit_period_block(
|
|
206
|
+
period_block_time: list[tuple],
|
|
207
|
+
period_block_succ: list[tuple],
|
|
208
|
+
*, provider,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Emit ``period_block`` to the Provider."""
|
|
211
|
+
_emit(provider, "solve_data/period_block_time.csv",
|
|
212
|
+
derive_period_block_time(period_block_time))
|
|
213
|
+
_emit(provider, "solve_data/period_block_succ.csv",
|
|
214
|
+
derive_period_block_succ(period_block_succ))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# Period writers
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def derive_years_represented(
|
|
223
|
+
period__branch: list[tuple[str, str]],
|
|
224
|
+
years_represented: list[tuple[str, str]],
|
|
225
|
+
) -> pl.DataFrame:
|
|
226
|
+
"""Build the canonical ``p_years_represented`` frame
|
|
227
|
+
(period, years_from_solve, p_years_from_solve, p_years_represented).
|
|
228
|
+
"""
|
|
229
|
+
rows: list[tuple[Any, ...]] = []
|
|
230
|
+
year_count: float = 0
|
|
231
|
+
for period__years in years_represented:
|
|
232
|
+
total_represented = float(period__years[1])
|
|
233
|
+
if total_represented <= 0:
|
|
234
|
+
continue
|
|
235
|
+
n_rows = math.ceil(total_represented)
|
|
236
|
+
remaining = total_represented
|
|
237
|
+
for _ in range(n_rows):
|
|
238
|
+
years_to_cover_within_year = min(1.0, remaining)
|
|
239
|
+
rows.append((
|
|
240
|
+
period__years[0], str(year_count), str(year_count),
|
|
241
|
+
str(years_to_cover_within_year),
|
|
242
|
+
))
|
|
243
|
+
for pd_pair in period__branch:
|
|
244
|
+
if pd_pair[0] in period__years[0] and pd_pair[0] != pd_pair[1]:
|
|
245
|
+
rows.append((
|
|
246
|
+
pd_pair[1], str(year_count), str(year_count),
|
|
247
|
+
str(years_to_cover_within_year),
|
|
248
|
+
))
|
|
249
|
+
year_count += years_to_cover_within_year
|
|
250
|
+
remaining -= years_to_cover_within_year
|
|
251
|
+
return _to_utf8_frame(
|
|
252
|
+
("period", "years_from_solve",
|
|
253
|
+
"p_years_from_solve", "p_years_represented"),
|
|
254
|
+
rows,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def emit_years_represented(
|
|
259
|
+
period__branch: list[tuple[str, str]],
|
|
260
|
+
years_represented: list[tuple[str, str]],
|
|
261
|
+
filename: str,
|
|
262
|
+
*, provider,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Emit ``years_represented`` to the Provider."""
|
|
265
|
+
_emit_path(provider, filename,
|
|
266
|
+
derive_years_represented(period__branch, years_represented))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def derive_period_years(
|
|
270
|
+
stochastic_branches: list[tuple[str, str]],
|
|
271
|
+
years_represented: list[tuple[str, str]],
|
|
272
|
+
) -> pl.DataFrame:
|
|
273
|
+
"""Build the canonical ``period_with_history`` / ``p_discount_years``
|
|
274
|
+
frame (period, param).
|
|
275
|
+
"""
|
|
276
|
+
rows: list[tuple[Any, ...]] = []
|
|
277
|
+
year_count: float = 0
|
|
278
|
+
for period__year in years_represented:
|
|
279
|
+
rows.append((period__year[0], str(year_count)))
|
|
280
|
+
for pd_pair in stochastic_branches:
|
|
281
|
+
if pd_pair[0] in period__year[0] and pd_pair[0] != pd_pair[1]:
|
|
282
|
+
rows.append((pd_pair[1], str(year_count)))
|
|
283
|
+
year_count += float(period__year[1])
|
|
284
|
+
return _to_utf8_frame(("period", "param"), rows)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def emit_period_years(
|
|
288
|
+
stochastic_branches: list[tuple[str, str]],
|
|
289
|
+
years_represented: list[tuple[str, str]],
|
|
290
|
+
filename: str,
|
|
291
|
+
*, provider,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Emit ``period_years`` to the Provider."""
|
|
294
|
+
_emit_path(provider, filename,
|
|
295
|
+
derive_period_years(stochastic_branches, years_represented))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def derive_periods(
|
|
299
|
+
solve: str,
|
|
300
|
+
periods_dict: dict[str, list[tuple[str, str]]],
|
|
301
|
+
) -> pl.DataFrame:
|
|
302
|
+
"""Build a single-column ``period`` frame for the given solve key."""
|
|
303
|
+
rows: list[tuple[Any, ...]] = [
|
|
304
|
+
(period_tuple[1],) for period_tuple in periods_dict.get(solve, [])
|
|
305
|
+
]
|
|
306
|
+
return _to_utf8_frame(("period",), rows)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def emit_periods(
|
|
310
|
+
solve: str,
|
|
311
|
+
periods_dict: dict[str, list[tuple[str, str]]],
|
|
312
|
+
filename: str,
|
|
313
|
+
*, provider,
|
|
314
|
+
) -> None:
|
|
315
|
+
"""Emit ``periods`` to the Provider."""
|
|
316
|
+
_emit_path(provider, filename, derive_periods(solve, periods_dict))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _compute_first_and_last_periods(
|
|
320
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
321
|
+
period__timesets_in_this_solve: list[tuple[str, str]],
|
|
322
|
+
period__branch_list: list[tuple[str, str]],
|
|
323
|
+
) -> tuple[list[str], list[str], list[str]]:
|
|
324
|
+
"""Shared compute for the three first/last period CSVs.
|
|
325
|
+
|
|
326
|
+
Returns ``(period_last, period_first_of_solve_list, period_first_list)``
|
|
327
|
+
each as a flat list of period names.
|
|
328
|
+
"""
|
|
329
|
+
period_first_of_solve = list(active_time_list.keys())[0]
|
|
330
|
+
period_last = [list(active_time_list.keys())[-1]]
|
|
331
|
+
time_step_last = active_time_list[period_last[0]][-1].timestep
|
|
332
|
+
for period in active_time_list.keys():
|
|
333
|
+
if (
|
|
334
|
+
active_time_list[period][-1].timestep == time_step_last
|
|
335
|
+
and period != period_last[0]
|
|
336
|
+
):
|
|
337
|
+
period_last.append(period)
|
|
338
|
+
|
|
339
|
+
period_first_of_solve_list = [
|
|
340
|
+
pb[1] for pb in period__branch_list if pb[0] == period_first_of_solve
|
|
341
|
+
]
|
|
342
|
+
period_first = period__timesets_in_this_solve[0][0]
|
|
343
|
+
period_first_list = [
|
|
344
|
+
pb[1] for pb in period__branch_list if pb[0] == period_first
|
|
345
|
+
]
|
|
346
|
+
return period_last, period_first_of_solve_list, period_first_list
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def emit_first_and_last_periods(
|
|
350
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
351
|
+
period__timesets_in_this_solve: list[tuple[str, str]],
|
|
352
|
+
period__branch_list: list[tuple[str, str]],
|
|
353
|
+
*, provider,
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Emit ``first_and_last_periods`` to the Provider."""
|
|
356
|
+
period_last, period_first_of_solve_list, period_first_list = (
|
|
357
|
+
_compute_first_and_last_periods(
|
|
358
|
+
active_time_list,
|
|
359
|
+
period__timesets_in_this_solve,
|
|
360
|
+
period__branch_list,
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
_emit(provider, "solve_data/period_last.csv",
|
|
364
|
+
_to_utf8_frame(("period",), [(p,) for p in period_last]))
|
|
365
|
+
_emit(provider, "solve_data/period_first_of_solve.csv",
|
|
366
|
+
_to_utf8_frame(("period",),
|
|
367
|
+
[(p,) for p in period_first_of_solve_list]))
|
|
368
|
+
_emit(provider, "solve_data/period_first.csv",
|
|
369
|
+
_to_utf8_frame(("period",),
|
|
370
|
+
[(p,) for p in period_first_list]))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
# Solve status
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def derive_solve_status(
|
|
379
|
+
first_state: bool,
|
|
380
|
+
last_state: bool,
|
|
381
|
+
nested: bool = False,
|
|
382
|
+
) -> pl.DataFrame:
|
|
383
|
+
"""Build the ``p_model`` / ``p_nested_model`` frame
|
|
384
|
+
(modelParam, p_model | p_nested_model).
|
|
385
|
+
"""
|
|
386
|
+
param_col = "p_nested_model" if nested else "p_model"
|
|
387
|
+
rows = [
|
|
388
|
+
("solveFirst", "1" if first_state else "0"),
|
|
389
|
+
("solveLast", "1" if last_state else "0"),
|
|
390
|
+
]
|
|
391
|
+
return _to_utf8_frame(("modelParam", param_col), rows)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def emit_solve_status(
|
|
395
|
+
first_state: bool,
|
|
396
|
+
last_state: bool,
|
|
397
|
+
nested: bool = False,
|
|
398
|
+
*, provider,
|
|
399
|
+
) -> None:
|
|
400
|
+
"""Emit ``solve_status`` to the Provider."""
|
|
401
|
+
key = ("solve_data/p_nested_model.csv" if nested
|
|
402
|
+
else "solve_data/p_model.csv")
|
|
403
|
+
_emit(provider, key,
|
|
404
|
+
derive_solve_status(first_state, last_state, nested))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def derive_current_solve(solve: str) -> pl.DataFrame:
|
|
408
|
+
"""Build the ``solve_current`` frame (solve)."""
|
|
409
|
+
return _to_utf8_frame(("solve",), [(solve,)])
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def emit_current_solve(solve: str, filename: str, *, provider) -> None:
|
|
413
|
+
"""Emit ``current_solve`` to the Provider."""
|
|
414
|
+
_emit_path(provider, filename, derive_current_solve(solve))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# ---------------------------------------------------------------------------
|
|
418
|
+
# Timestep boundary (S04 consolidation in legacy)
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def derive_period_boundary_step(
|
|
423
|
+
timeline: dict[str, list[tuple[str, ...]]],
|
|
424
|
+
last: bool = False,
|
|
425
|
+
) -> pl.DataFrame:
|
|
426
|
+
"""Build the (period, step) boundary-step frame — first or last
|
|
427
|
+
timestep of each period.
|
|
428
|
+
"""
|
|
429
|
+
rows: list[tuple[Any, ...]] = []
|
|
430
|
+
for period_name, period in timeline.items():
|
|
431
|
+
boundary = period[-1:] if last else period[:1]
|
|
432
|
+
for item in boundary:
|
|
433
|
+
rows.append((period_name, item.timestep))
|
|
434
|
+
return _to_utf8_frame(("period", "step"), rows)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def emit_period_boundary_step(
|
|
438
|
+
timeline: dict[str, list[tuple[str, ...]]],
|
|
439
|
+
filename: str,
|
|
440
|
+
*,
|
|
441
|
+
last: bool = False,
|
|
442
|
+
provider,
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Emit ``period_boundary_step`` to the Provider."""
|
|
445
|
+
_emit_path(provider, filename,
|
|
446
|
+
derive_period_boundary_step(timeline, last=last))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def emit_first_steps(
|
|
450
|
+
timeline: dict[str, list[tuple[str, ...]]],
|
|
451
|
+
filename: str,
|
|
452
|
+
*, provider,
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Emit ``first_steps`` to the Provider."""
|
|
455
|
+
emit_period_boundary_step(timeline, filename, last=False, provider=provider)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def emit_last_steps(
|
|
459
|
+
timeline: dict[str, list[tuple[str, ...]]],
|
|
460
|
+
filename: str,
|
|
461
|
+
*, provider,
|
|
462
|
+
) -> None:
|
|
463
|
+
"""Emit ``last_steps`` to the Provider."""
|
|
464
|
+
emit_period_boundary_step(timeline, filename, last=True, provider=provider)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def get_first_steps(
|
|
468
|
+
steplists: dict[str, list[Any]],
|
|
469
|
+
) -> dict[str, tuple[Any, ...]]:
|
|
470
|
+
"""Helper: pull the first step of the current solve (and the first
|
|
471
|
+
step of the next solve in execution order) from each solve's
|
|
472
|
+
steplist. Returns ``{solve: (own_first[, next_first])}``.
|
|
473
|
+
|
|
474
|
+
Pure data transform — no CSV side effects.
|
|
475
|
+
"""
|
|
476
|
+
solve_names = list(steplists.keys())
|
|
477
|
+
starts: dict[str, tuple[Any, ...]] = dict()
|
|
478
|
+
for index, name in enumerate(solve_names):
|
|
479
|
+
if index == (len(solve_names) - 1):
|
|
480
|
+
starts[name] = (steplists[name][0],)
|
|
481
|
+
else:
|
|
482
|
+
starts[name] = (
|
|
483
|
+
steplists[solve_names[index]][0],
|
|
484
|
+
steplists[solve_names[index + 1]][0],
|
|
485
|
+
)
|
|
486
|
+
return starts
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# ---------------------------------------------------------------------------
|
|
490
|
+
# Realized / fix storage
|
|
491
|
+
# ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def derive_realized_dispatch(
|
|
495
|
+
realized_time_list: dict[str, list[tuple[str, ...]]],
|
|
496
|
+
solve: str,
|
|
497
|
+
realized_periods: list[tuple[str, str]],
|
|
498
|
+
) -> pl.DataFrame:
|
|
499
|
+
"""Build the ``realized_dispatch`` frame (period, step)."""
|
|
500
|
+
rows: list[tuple[Any, ...]] = []
|
|
501
|
+
for period, realized_time in realized_time_list.items():
|
|
502
|
+
if any(t[1] == period for t in realized_periods):
|
|
503
|
+
for i in realized_time:
|
|
504
|
+
rows.append((period, i.timestep))
|
|
505
|
+
return _to_utf8_frame(("period", "step"), rows)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def emit_realized_dispatch(
|
|
509
|
+
realized_time_list: dict[str, list[tuple[str, ...]]],
|
|
510
|
+
solve: str,
|
|
511
|
+
realized_periods: list[tuple[str, str]],
|
|
512
|
+
*, provider,
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Emit ``realized_dispatch`` to the Provider."""
|
|
515
|
+
_emit(
|
|
516
|
+
provider, "solve_data/realized_dispatch.csv",
|
|
517
|
+
derive_realized_dispatch(realized_time_list, solve, realized_periods),
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def derive_fix_storage_timesteps(
|
|
522
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
523
|
+
solve: str,
|
|
524
|
+
fix_storage_periods: list[tuple[str, str]],
|
|
525
|
+
) -> pl.DataFrame:
|
|
526
|
+
"""Build the ``fix_storage_timesteps`` frame (period, step)."""
|
|
527
|
+
rows: list[tuple[Any, ...]] = []
|
|
528
|
+
for period, active_time in active_time_list.items():
|
|
529
|
+
if any(t[1] == period for t in fix_storage_periods):
|
|
530
|
+
for i in active_time:
|
|
531
|
+
rows.append((period, i.timestep))
|
|
532
|
+
return _to_utf8_frame(("period", "step"), rows)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def emit_fix_storage_timesteps(
|
|
536
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
537
|
+
solve: str,
|
|
538
|
+
fix_storage_periods: list[tuple[str, str]],
|
|
539
|
+
*, provider,
|
|
540
|
+
) -> None:
|
|
541
|
+
"""Emit ``fix_storage_timesteps`` to the Provider."""
|
|
542
|
+
_emit(
|
|
543
|
+
provider, "solve_data/fix_storage_timesteps.csv",
|
|
544
|
+
derive_fix_storage_timesteps(
|
|
545
|
+
active_time_list, solve, fix_storage_periods,
|
|
546
|
+
),
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# ---------------------------------------------------------------------------
|
|
551
|
+
# Group B — branch / empty / header writers
|
|
552
|
+
# ---------------------------------------------------------------------------
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def derive_branch__period_relationship(
|
|
556
|
+
period__branch: list[tuple[str, str]],
|
|
557
|
+
) -> pl.DataFrame:
|
|
558
|
+
"""Build the ``period__branch`` frame (period, branch)."""
|
|
559
|
+
return _to_utf8_frame(
|
|
560
|
+
("period", "branch"),
|
|
561
|
+
[(row[0], row[1]) for row in period__branch],
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def emit_branch__period_relationship(
|
|
566
|
+
period__branch: list[tuple[str, str]],
|
|
567
|
+
filename: str,
|
|
568
|
+
*, provider,
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Emit ``branch__period_relationship`` to the Provider."""
|
|
571
|
+
_emit_path(provider, filename,
|
|
572
|
+
derive_branch__period_relationship(period__branch))
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def derive_solve_branch_weight(
|
|
576
|
+
complete_solve: str,
|
|
577
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
578
|
+
solve_branch__time_branch_list: list[tuple[str, str]],
|
|
579
|
+
branch_start_time: tuple[str, str] | None,
|
|
580
|
+
period__branch_lists: list[tuple[str, str]],
|
|
581
|
+
stochastic_branches: dict[str, list[Any]],
|
|
582
|
+
) -> pl.DataFrame:
|
|
583
|
+
"""Build the ``solve_branch_weight`` frame
|
|
584
|
+
(branch, p_branch_weight_input).
|
|
585
|
+
"""
|
|
586
|
+
time_branch_weight: dict[str, Any] = defaultdict()
|
|
587
|
+
if branch_start_time is not None:
|
|
588
|
+
for row in stochastic_branches[complete_solve]:
|
|
589
|
+
if (
|
|
590
|
+
branch_start_time[0] == row[0]
|
|
591
|
+
and branch_start_time[1] == row[2]
|
|
592
|
+
):
|
|
593
|
+
time_branch_weight[row[1]] = row[4]
|
|
594
|
+
|
|
595
|
+
rows: list[tuple[Any, ...]] = []
|
|
596
|
+
for solve_branch__time_branch in solve_branch__time_branch_list:
|
|
597
|
+
if (
|
|
598
|
+
(solve_branch__time_branch[0], solve_branch__time_branch[0])
|
|
599
|
+
in period__branch_lists
|
|
600
|
+
):
|
|
601
|
+
rows.append((solve_branch__time_branch[0], "1.0"))
|
|
602
|
+
elif (
|
|
603
|
+
solve_branch__time_branch[1] in time_branch_weight.keys()
|
|
604
|
+
and solve_branch__time_branch[0] in active_time_list.keys()
|
|
605
|
+
):
|
|
606
|
+
rows.append((
|
|
607
|
+
solve_branch__time_branch[0],
|
|
608
|
+
str(time_branch_weight[solve_branch__time_branch[1]]),
|
|
609
|
+
))
|
|
610
|
+
return _to_utf8_frame(("branch", "p_branch_weight_input"), rows)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def derive_solve_branch__time_branch(
|
|
614
|
+
solve_branch__time_branch_list: list[tuple[str, str]],
|
|
615
|
+
) -> pl.DataFrame:
|
|
616
|
+
"""Build the ``solve_branch__time_branch`` frame (period, branch)."""
|
|
617
|
+
return _to_utf8_frame(
|
|
618
|
+
("period", "branch"),
|
|
619
|
+
[(s[0], s[1]) for s in solve_branch__time_branch_list],
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def emit_branch_weights_and_map(
|
|
624
|
+
complete_solve: str,
|
|
625
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
626
|
+
solve_branch__time_branch_list: list[tuple[str, str]],
|
|
627
|
+
branch_start_time: tuple[str, str] | None,
|
|
628
|
+
period__branch_lists: list[tuple[str, str]],
|
|
629
|
+
stochastic_branches: dict[str, list[Any]],
|
|
630
|
+
*, provider,
|
|
631
|
+
) -> None:
|
|
632
|
+
"""Emit ``branch_weights_and_map`` to the Provider."""
|
|
633
|
+
_emit(
|
|
634
|
+
provider, "solve_data/solve_branch_weight.csv",
|
|
635
|
+
derive_solve_branch_weight(
|
|
636
|
+
complete_solve, active_time_list, solve_branch__time_branch_list,
|
|
637
|
+
branch_start_time, period__branch_lists, stochastic_branches,
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
_emit(
|
|
641
|
+
provider, "solve_data/solve_branch__time_branch.csv",
|
|
642
|
+
derive_solve_branch__time_branch(solve_branch__time_branch_list),
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
# ---------------------------------------------------------------------------
|
|
647
|
+
# Init / empty files
|
|
648
|
+
# ---------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _empty_frame(headers: tuple[str, ...]) -> pl.DataFrame:
|
|
652
|
+
"""Build a header-only all-Utf8 polars frame."""
|
|
653
|
+
return pl.DataFrame(schema={h: pl.Utf8 for h in headers})
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def emit_empty_investment_file(
|
|
657
|
+
*, provider) -> None:
|
|
658
|
+
"""Emit ``empty_investment_file`` to the Provider."""
|
|
659
|
+
_emit(provider, "solve_data/p_entity_invested.csv",
|
|
660
|
+
_empty_frame(("entity", "p_entity_invested")))
|
|
661
|
+
_emit(provider, "solve_data/p_entity_divested.csv",
|
|
662
|
+
_empty_frame(("entity", "p_entity_divested")))
|
|
663
|
+
_emit(
|
|
664
|
+
provider, "solve_data/p_entity_period_existing_capacity.csv",
|
|
665
|
+
_empty_frame((
|
|
666
|
+
"entity", "period",
|
|
667
|
+
"p_entity_period_existing_capacity",
|
|
668
|
+
"p_entity_period_invested_capacity",
|
|
669
|
+
)),
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def emit_empty_storage_fix_file(
|
|
674
|
+
*, provider) -> None:
|
|
675
|
+
"""Emit ``empty_storage_fix_file`` to the Provider."""
|
|
676
|
+
_emit(provider, "solve_data/fix_storage_price.csv",
|
|
677
|
+
_empty_frame(
|
|
678
|
+
("node", " period", " step", " ndt_fix_storage_price"),
|
|
679
|
+
))
|
|
680
|
+
_emit(provider, "solve_data/fix_storage_quantity.csv",
|
|
681
|
+
_empty_frame(
|
|
682
|
+
("node", " period", " step", " ndt_fix_storage_quantity"),
|
|
683
|
+
))
|
|
684
|
+
_emit(provider, "solve_data/fix_storage_usage.csv",
|
|
685
|
+
_empty_frame(
|
|
686
|
+
("node", " period", " step", " ndt_fix_storage_usage"),
|
|
687
|
+
))
|
|
688
|
+
_emit(provider, "solve_data/p_roll_continue_state.csv",
|
|
689
|
+
_empty_frame(("node", " p_roll_continue_state")))
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def emit_headers_for_empty_output_files(filename: str, header: str,
|
|
693
|
+
*, provider) -> None:
|
|
694
|
+
"""Emit ``headers_for_empty_output_files`` to the Provider."""
|
|
695
|
+
_emit_path(provider, filename, _empty_frame(tuple(header.split(","))))
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
# ---------------------------------------------------------------------------
|
|
699
|
+
# Misc writers
|
|
700
|
+
# ---------------------------------------------------------------------------
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def derive_timesets_in_use(
|
|
704
|
+
timesets_used_by_solves: dict[str, list[tuple[str, str]]],
|
|
705
|
+
) -> pl.DataFrame:
|
|
706
|
+
"""Build the ``timesets_in_use`` frame (solve, period, timesets)."""
|
|
707
|
+
rows: list[tuple[Any, ...]] = []
|
|
708
|
+
for solve, period_timeset_list in timesets_used_by_solves.items():
|
|
709
|
+
for period, timeset in period_timeset_list:
|
|
710
|
+
rows.append((solve, period, timeset))
|
|
711
|
+
return _to_utf8_frame(("solve", "period", "timesets"), rows)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def derive_timesets__timeline(
|
|
715
|
+
timeset__timeline: dict[str, str],
|
|
716
|
+
) -> pl.DataFrame:
|
|
717
|
+
"""Build the ``timesets__timeline`` frame (timesets, timeline)."""
|
|
718
|
+
rows = [(ts, tl) for ts, tl in timeset__timeline.items()]
|
|
719
|
+
return _to_utf8_frame(("timesets", "timeline"), rows)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def emit_timesets(
|
|
723
|
+
timesets_used_by_solves: dict[str, list[tuple[str, str]]],
|
|
724
|
+
timeset__timeline: dict[str, str],
|
|
725
|
+
*, provider,
|
|
726
|
+
) -> None:
|
|
727
|
+
"""Emit ``timesets`` to the Provider."""
|
|
728
|
+
_emit(provider, "input/timesets_in_use.csv",
|
|
729
|
+
derive_timesets_in_use(timesets_used_by_solves))
|
|
730
|
+
_emit(provider, "input/timesets__timeline.csv",
|
|
731
|
+
derive_timesets__timeline(timeset__timeline))
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# ---------------------------------------------------------------------------
|
|
735
|
+
# Sub-dispatch 7 — scaling writers
|
|
736
|
+
# ---------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def derive_p_use_row_scaling(
|
|
740
|
+
solve: str,
|
|
741
|
+
use_row_scaling: dict[str, str],
|
|
742
|
+
) -> pl.DataFrame:
|
|
743
|
+
"""Build the ``p_use_row_scaling`` frame (solve, p_use_row_scaling).
|
|
744
|
+
|
|
745
|
+
Honours the ``FLEXTOOL_FORCE_ROW_SCALING`` env-var test hook (Agent 9
|
|
746
|
+
Mode B un-scaling benchmark harness).
|
|
747
|
+
"""
|
|
748
|
+
import os as _os
|
|
749
|
+
|
|
750
|
+
value_str = (
|
|
751
|
+
use_row_scaling.get(solve, "no")
|
|
752
|
+
if isinstance(use_row_scaling, dict)
|
|
753
|
+
else "no"
|
|
754
|
+
)
|
|
755
|
+
flag = 1 if str(value_str).strip().lower() == "yes" else 0
|
|
756
|
+
if _os.environ.get("FLEXTOOL_FORCE_ROW_SCALING", "").strip().lower() in (
|
|
757
|
+
"1", "yes", "true", "on",
|
|
758
|
+
):
|
|
759
|
+
flag = 1
|
|
760
|
+
return _to_utf8_frame(
|
|
761
|
+
("solve", "p_use_row_scaling"), [(solve, str(flag))],
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def emit_p_use_row_scaling(
|
|
766
|
+
solve: str,
|
|
767
|
+
use_row_scaling: dict[str, str],
|
|
768
|
+
filename: str,
|
|
769
|
+
*, provider,
|
|
770
|
+
) -> None:
|
|
771
|
+
"""Emit ``p_use_row_scaling`` to the Provider."""
|
|
772
|
+
_emit_path(provider, filename,
|
|
773
|
+
derive_p_use_row_scaling(solve, use_row_scaling))
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def derive_scale_the_objective(value: float) -> pl.DataFrame:
|
|
777
|
+
"""Build the ``scale_the_objective`` keyed-value frame
|
|
778
|
+
(key=``v``, value=``%.17g``).
|
|
779
|
+
"""
|
|
780
|
+
return _to_utf8_frame(
|
|
781
|
+
("key", "value"), [("v", f"{float(value):.17g}")],
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def derive_scale_the_objective_header_only() -> pl.DataFrame:
|
|
786
|
+
"""Build the header-only ``scale_the_objective`` frame."""
|
|
787
|
+
return _empty_frame(("key", "value"))
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def emit_scale_the_objective(
|
|
791
|
+
solve_data_dir: Path | str,
|
|
792
|
+
value: float,
|
|
793
|
+
*, provider,
|
|
794
|
+
) -> Path:
|
|
795
|
+
"""Emit ``scale_the_objective`` to the Provider."""
|
|
796
|
+
path = Path(solve_data_dir) / "scale_the_objective.csv"
|
|
797
|
+
_emit(provider, "solve_data/scale_the_objective.csv",
|
|
798
|
+
derive_scale_the_objective(value))
|
|
799
|
+
return path
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def emit_scale_the_objective_header_only(
|
|
803
|
+
solve_data_dir: Path | str,
|
|
804
|
+
*, provider,
|
|
805
|
+
) -> Path:
|
|
806
|
+
"""Emit ``scale_the_objective_header_only`` to the Provider."""
|
|
807
|
+
path = Path(solve_data_dir) / "scale_the_objective.csv"
|
|
808
|
+
_emit(provider, "solve_data/scale_the_objective.csv",
|
|
809
|
+
derive_scale_the_objective_header_only())
|
|
810
|
+
return path
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
# ---------------------------------------------------------------------------
|
|
814
|
+
# Sub-dispatch 7 — delay durations
|
|
815
|
+
# ---------------------------------------------------------------------------
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _compute_delay_duration_set(
|
|
819
|
+
delay_durations: dict[str, Any],
|
|
820
|
+
) -> set[str]:
|
|
821
|
+
"""Shared compute for the two delayed-duration CSVs."""
|
|
822
|
+
delay_duration_set: set[str] = set()
|
|
823
|
+
for entity, dur in delay_durations.items():
|
|
824
|
+
if isinstance(dur, list):
|
|
825
|
+
for delay_duration in dur:
|
|
826
|
+
delay_duration_set.add(str(delay_duration[0]))
|
|
827
|
+
else:
|
|
828
|
+
delay_duration_set.add(str(dur))
|
|
829
|
+
return delay_duration_set
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def derive_delay_duration(
|
|
833
|
+
delay_durations: dict[str, Any],
|
|
834
|
+
) -> pl.DataFrame:
|
|
835
|
+
"""Build the ``delay_duration`` frame (delay_duration)."""
|
|
836
|
+
rows = [(str(d),) for d in _compute_delay_duration_set(delay_durations)]
|
|
837
|
+
return _to_utf8_frame(("delay_duration",), rows)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def derive_dtt__delay_duration(
|
|
841
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
842
|
+
delay_durations: dict[str, Any],
|
|
843
|
+
) -> pl.DataFrame:
|
|
844
|
+
"""Build the ``dtt__delay_duration`` frame
|
|
845
|
+
(period, time_source, time_sink, delay_duration) with cyclic
|
|
846
|
+
end-of-period wrap-around.
|
|
847
|
+
"""
|
|
848
|
+
delay_duration_set = _compute_delay_duration_set(delay_durations)
|
|
849
|
+
rows: list[tuple[Any, ...]] = []
|
|
850
|
+
for period_name, time_steps in active_time_list.items():
|
|
851
|
+
for k, time_step in enumerate(time_steps):
|
|
852
|
+
for delay_duration in delay_duration_set:
|
|
853
|
+
offset = int(float(delay_duration))
|
|
854
|
+
if k + offset < len(time_steps):
|
|
855
|
+
sink_step = time_steps[k + offset].timestep
|
|
856
|
+
else:
|
|
857
|
+
sink_step = time_steps[
|
|
858
|
+
k - len(time_steps) + offset
|
|
859
|
+
].timestep
|
|
860
|
+
rows.append((
|
|
861
|
+
period_name, time_step.timestep,
|
|
862
|
+
sink_step, str(delay_duration),
|
|
863
|
+
))
|
|
864
|
+
return _to_utf8_frame(
|
|
865
|
+
("period", "time_source", "time_sink", "delay_duration"), rows,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def emit_delayed_durations(
|
|
870
|
+
active_time_list: dict[str, list[tuple[str, ...]]],
|
|
871
|
+
solve: str,
|
|
872
|
+
delay_durations: dict[str, Any],
|
|
873
|
+
*, provider,
|
|
874
|
+
) -> None:
|
|
875
|
+
"""Emit ``delayed_durations`` to the Provider."""
|
|
876
|
+
_emit(provider, "solve_data/delay_duration.csv",
|
|
877
|
+
derive_delay_duration(delay_durations))
|
|
878
|
+
_emit(provider, "solve_data/dtt__delay_duration.csv",
|
|
879
|
+
derive_dtt__delay_duration(active_time_list,
|
|
880
|
+
delay_durations))
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
# ---------------------------------------------------------------------------
|
|
884
|
+
# Sub-dispatch 7 — representative period writers
|
|
885
|
+
# ---------------------------------------------------------------------------
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _compute_rp_frames(
|
|
889
|
+
rp_weights: dict[str, dict[str, float]],
|
|
890
|
+
timeset_duration_entries: list[tuple[str, float]],
|
|
891
|
+
period_name: str,
|
|
892
|
+
timeline_steps: list[str],
|
|
893
|
+
*,
|
|
894
|
+
variant: str = "within_solve",
|
|
895
|
+
per_period_inputs: (
|
|
896
|
+
"list[tuple[str, dict[str, dict[str, float]], "
|
|
897
|
+
"list[tuple[str, float]], list[str]]] | None"
|
|
898
|
+
) = None,
|
|
899
|
+
) -> dict[str, pl.DataFrame]:
|
|
900
|
+
"""Shared compute for the eight representative-period CSVs.
|
|
901
|
+
|
|
902
|
+
Returns a dict keyed by output basename — used by both the
|
|
903
|
+
``derive_*`` accessors and the wrapper writer.
|
|
904
|
+
|
|
905
|
+
Output basenames: ``rp_weights.csv``, ``rp_base_chain.csv``,
|
|
906
|
+
``rp_base_first.csv``, ``rp_base_last.csv``, ``rp_block_first.csv``,
|
|
907
|
+
``rp_block_last.csv``, ``rp_block_start_last.csv``,
|
|
908
|
+
``rp_base_period_set.csv``, ``rp_rep_period_set.csv``,
|
|
909
|
+
``timestep_weight.csv``.
|
|
910
|
+
|
|
911
|
+
``timeline_steps`` is the ordered list of step IDs for the timeline
|
|
912
|
+
this timeset belongs to; it anchors ``(start, count)`` block entries
|
|
913
|
+
to real step labels (e.g. ``2050-01-01T00:00:00``) rather than the
|
|
914
|
+
synthetic ``t####`` form.
|
|
915
|
+
|
|
916
|
+
Phase E — ``variant`` toggles the RP chain TOPOLOGY:
|
|
917
|
+
|
|
918
|
+
* ``"within_solve"`` (default, back-compat): one base-period chain
|
|
919
|
+
spanning every base period in the solve. ``rp_base_first`` /
|
|
920
|
+
``rp_base_last`` are singletons (head/tail of the single chain).
|
|
921
|
+
Used by ``bind_within_solve_blended_weights`` (cyclic closure
|
|
922
|
+
across the entire chain) and ``bind_forward_only_blended_weights``
|
|
923
|
+
(no closure).
|
|
924
|
+
* ``"within_period"``: each FlexTool period has its own independent
|
|
925
|
+
chain that closes within itself. ``per_period_inputs`` carries
|
|
926
|
+
one entry per RP-bearing FlexTool period;
|
|
927
|
+
``rp_base_chain`` accumulates only WITHIN-period predecessor
|
|
928
|
+
edges, and ``rp_base_first`` / ``rp_base_last`` carry one row per
|
|
929
|
+
period plus a ``period`` column so the downstream cyclic-closure
|
|
930
|
+
constraint (``model.py``'s ``rp_inter_period_cyclic``) can pair
|
|
931
|
+
each period's first base against its OWN period's last base.
|
|
932
|
+
|
|
933
|
+
``per_period_inputs`` is required when ``variant == "within_period"``
|
|
934
|
+
and ignored otherwise; the legacy single-period args
|
|
935
|
+
(``rp_weights`` / ``timeset_duration_entries`` / ``period_name`` /
|
|
936
|
+
``timeline_steps``) drive the within_solve path.
|
|
937
|
+
"""
|
|
938
|
+
if variant not in ("within_solve", "within_period"):
|
|
939
|
+
raise ValueError(
|
|
940
|
+
f"_compute_rp_frames: variant must be 'within_solve' or "
|
|
941
|
+
f"'within_period', got {variant!r}."
|
|
942
|
+
)
|
|
943
|
+
if variant == "within_period":
|
|
944
|
+
if not per_period_inputs:
|
|
945
|
+
raise ValueError(
|
|
946
|
+
"_compute_rp_frames: variant='within_period' requires a "
|
|
947
|
+
"non-empty per_period_inputs list (one entry per "
|
|
948
|
+
"RP-bearing FlexTool period)."
|
|
949
|
+
)
|
|
950
|
+
# Per-period assembly: each entry yields a chain segment fully
|
|
951
|
+
# contained within its FlexTool period. Cross-period edges are
|
|
952
|
+
# omitted by construction (we never concatenate cross-segment
|
|
953
|
+
# predecessor edges).
|
|
954
|
+
wp_chain_rows: list[tuple[Any, ...]] = []
|
|
955
|
+
wp_first_rows: list[tuple[Any, ...]] = []
|
|
956
|
+
wp_last_rows: list[tuple[Any, ...]] = []
|
|
957
|
+
wp_block_first_rows: list[tuple[Any, ...]] = []
|
|
958
|
+
wp_block_last_rows: list[tuple[Any, ...]] = []
|
|
959
|
+
wp_block_sl_rows: list[tuple[Any, ...]] = []
|
|
960
|
+
wp_base_period_rows: list[tuple[Any, ...]] = []
|
|
961
|
+
wp_rep_period_rows: list[tuple[Any, ...]] = []
|
|
962
|
+
wp_weights_rows: list[tuple[Any, ...]] = []
|
|
963
|
+
wp_cost_rows: list[tuple[Any, ...]] = []
|
|
964
|
+
for (p_name, p_rp_w, p_ts_dur, p_tl_steps) in per_period_inputs:
|
|
965
|
+
p_sub = _compute_rp_frames(
|
|
966
|
+
p_rp_w, p_ts_dur, p_name, p_tl_steps,
|
|
967
|
+
)
|
|
968
|
+
# rp_base_chain: rename to add the owning period column.
|
|
969
|
+
sub_chain = p_sub["rp_base_chain.csv"]
|
|
970
|
+
for row in sub_chain.iter_rows():
|
|
971
|
+
wp_chain_rows.append((row[0], row[1], p_name))
|
|
972
|
+
# rp_base_first / rp_base_last: tag with period.
|
|
973
|
+
sub_first = p_sub["rp_base_first.csv"]
|
|
974
|
+
for row in sub_first.iter_rows():
|
|
975
|
+
wp_first_rows.append((row[0], p_name))
|
|
976
|
+
sub_last = p_sub["rp_base_last.csv"]
|
|
977
|
+
for row in sub_last.iter_rows():
|
|
978
|
+
wp_last_rows.append((row[0], p_name))
|
|
979
|
+
# Pass through unchanged.
|
|
980
|
+
wp_block_first_rows.extend(p_sub["rp_block_first.csv"].iter_rows())
|
|
981
|
+
wp_block_last_rows.extend(p_sub["rp_block_last.csv"].iter_rows())
|
|
982
|
+
wp_block_sl_rows.extend(
|
|
983
|
+
p_sub["rp_block_start_last.csv"].iter_rows())
|
|
984
|
+
wp_base_period_rows.extend(
|
|
985
|
+
p_sub["rp_base_period_set.csv"].iter_rows())
|
|
986
|
+
wp_rep_period_rows.extend(
|
|
987
|
+
p_sub["rp_rep_period_set.csv"].iter_rows())
|
|
988
|
+
wp_weights_rows.extend(p_sub["rp_weights.csv"].iter_rows())
|
|
989
|
+
wp_cost_rows.extend(p_sub["timestep_weight.csv"].iter_rows())
|
|
990
|
+
out: dict[str, pl.DataFrame] = {}
|
|
991
|
+
out["rp_weights.csv"] = _to_utf8_frame(
|
|
992
|
+
("base_start", "rep_start", "weight"), wp_weights_rows,
|
|
993
|
+
)
|
|
994
|
+
# ``period`` column is the period that owns this chain edge /
|
|
995
|
+
# endpoint; consumed by the FlexData loader and propagated as
|
|
996
|
+
# ``d`` so ``model.py``'s ``rp_inter_period_cyclic`` can pair
|
|
997
|
+
# endpoints per period.
|
|
998
|
+
out["rp_base_chain.csv"] = _to_utf8_frame(
|
|
999
|
+
("base_start", "prev_base_start", "period"), wp_chain_rows,
|
|
1000
|
+
)
|
|
1001
|
+
out["rp_base_first.csv"] = _to_utf8_frame(
|
|
1002
|
+
("base_start", "period"), wp_first_rows,
|
|
1003
|
+
)
|
|
1004
|
+
out["rp_base_last.csv"] = _to_utf8_frame(
|
|
1005
|
+
("base_start", "period"), wp_last_rows,
|
|
1006
|
+
)
|
|
1007
|
+
out["rp_block_first.csv"] = _to_utf8_frame(
|
|
1008
|
+
("period", "step"), wp_block_first_rows,
|
|
1009
|
+
)
|
|
1010
|
+
out["rp_block_last.csv"] = _to_utf8_frame(
|
|
1011
|
+
("period", "step"), wp_block_last_rows,
|
|
1012
|
+
)
|
|
1013
|
+
out["rp_block_start_last.csv"] = _to_utf8_frame(
|
|
1014
|
+
("rep_start", "last_step"), wp_block_sl_rows,
|
|
1015
|
+
)
|
|
1016
|
+
out["rp_base_period_set.csv"] = _to_utf8_frame(
|
|
1017
|
+
("period",), wp_base_period_rows,
|
|
1018
|
+
)
|
|
1019
|
+
out["rp_rep_period_set.csv"] = _to_utf8_frame(
|
|
1020
|
+
("period",), wp_rep_period_rows,
|
|
1021
|
+
)
|
|
1022
|
+
out["timestep_weight.csv"] = _to_utf8_frame(
|
|
1023
|
+
("period", "time", "weight"), wp_cost_rows,
|
|
1024
|
+
)
|
|
1025
|
+
return out
|
|
1026
|
+
|
|
1027
|
+
step_to_idx: dict[str, int] = {s: i for i, s in enumerate(timeline_steps)}
|
|
1028
|
+
|
|
1029
|
+
# RP block boundaries from the timeset_duration entries.
|
|
1030
|
+
rp_starts: list[str] = []
|
|
1031
|
+
rp_lasts: list[str] = []
|
|
1032
|
+
rp_ranges: list[tuple[int, int]] = [] # inclusive (start_idx, last_idx)
|
|
1033
|
+
for start_step, count in timeset_duration_entries:
|
|
1034
|
+
start_step = str(start_step)
|
|
1035
|
+
if start_step not in step_to_idx:
|
|
1036
|
+
raise KeyError(
|
|
1037
|
+
f"Representative-period block start {start_step!r} not "
|
|
1038
|
+
f"found in timeline (period={period_name!r}). Check that "
|
|
1039
|
+
"timeset_duration entries reference real timeline steps."
|
|
1040
|
+
)
|
|
1041
|
+
start_idx = step_to_idx[start_step]
|
|
1042
|
+
last_idx = start_idx + int(float(count)) - 1
|
|
1043
|
+
if last_idx >= len(timeline_steps):
|
|
1044
|
+
raise IndexError(
|
|
1045
|
+
f"Representative-period block {start_step!r} + "
|
|
1046
|
+
f"{int(float(count))} steps overruns timeline length "
|
|
1047
|
+
f"({len(timeline_steps)}) for period={period_name!r}."
|
|
1048
|
+
)
|
|
1049
|
+
rp_starts.append(start_step)
|
|
1050
|
+
rp_lasts.append(timeline_steps[last_idx])
|
|
1051
|
+
rp_ranges.append((start_idx, last_idx))
|
|
1052
|
+
|
|
1053
|
+
base_starts = sorted(
|
|
1054
|
+
rp_weights.keys(),
|
|
1055
|
+
key=lambda s: step_to_idx.get(s, len(timeline_steps)),
|
|
1056
|
+
)
|
|
1057
|
+
n_base = len(base_starts)
|
|
1058
|
+
n_rp = len(rp_starts)
|
|
1059
|
+
|
|
1060
|
+
out: dict[str, pl.DataFrame] = {}
|
|
1061
|
+
|
|
1062
|
+
# 1. rp_weights.csv — drop near-zero entries.
|
|
1063
|
+
rp_weights_rows: list[tuple[Any, ...]] = []
|
|
1064
|
+
for base in base_starts:
|
|
1065
|
+
for rep, weight in rp_weights[base].items():
|
|
1066
|
+
if weight > 1e-10:
|
|
1067
|
+
rp_weights_rows.append((base, rep, str(weight)))
|
|
1068
|
+
out["rp_weights.csv"] = _to_utf8_frame(
|
|
1069
|
+
("base_start", "rep_start", "weight"), rp_weights_rows,
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
# 2. rp_base_chain.csv — predecessor chain (excludes first).
|
|
1073
|
+
out["rp_base_chain.csv"] = _to_utf8_frame(
|
|
1074
|
+
("base_start", "prev_base_start"),
|
|
1075
|
+
[(base_starts[i], base_starts[i - 1]) for i in range(1, n_base)],
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
# 3. rp_base_first.csv / rp_base_last.csv
|
|
1079
|
+
out["rp_base_first.csv"] = _to_utf8_frame(
|
|
1080
|
+
("base_start",), [(base_starts[0],)] if n_base > 0 else [],
|
|
1081
|
+
)
|
|
1082
|
+
out["rp_base_last.csv"] = _to_utf8_frame(
|
|
1083
|
+
("base_start",), [(base_starts[-1],)] if n_base > 0 else [],
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
# 4. rp_block_first.csv / rp_block_last.csv
|
|
1087
|
+
out["rp_block_first.csv"] = _to_utf8_frame(
|
|
1088
|
+
("period", "step"), [(period_name, s) for s in rp_starts],
|
|
1089
|
+
)
|
|
1090
|
+
out["rp_block_last.csv"] = _to_utf8_frame(
|
|
1091
|
+
("period", "step"), [(period_name, lst) for lst in rp_lasts],
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
# 5. rp_block_start_last.csv
|
|
1095
|
+
out["rp_block_start_last.csv"] = _to_utf8_frame(
|
|
1096
|
+
("rep_start", "last_step"),
|
|
1097
|
+
[(s, lst) for s, lst in zip(rp_starts, rp_lasts)],
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# 5b. rp_base_period_set.csv / rp_rep_period_set.csv —
|
|
1101
|
+
# ordered-unique projections of the rp_weights base/rep columns.
|
|
1102
|
+
# Owned by _compute_rp_frames because emit_per_solve_sets runs
|
|
1103
|
+
# BEFORE emit_rp_data in the cascade, so the per-solve emit can't
|
|
1104
|
+
# see ``rp_weights`` yet. Column header ``period`` matches the
|
|
1105
|
+
# convention used by ``_emit_singles``-style set frames; the
|
|
1106
|
+
# FlexData loader renames to its axis name on load.
|
|
1107
|
+
out["rp_base_period_set.csv"] = _to_utf8_frame(
|
|
1108
|
+
("period",), [(b,) for b in base_starts],
|
|
1109
|
+
)
|
|
1110
|
+
out["rp_rep_period_set.csv"] = _to_utf8_frame(
|
|
1111
|
+
("period",), [(r,) for r in rp_starts],
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
# 6. timestep_weight.csv — normalised per-timestep weight.
|
|
1115
|
+
w_r: dict[str, float] = {r: 0.0 for r in rp_starts}
|
|
1116
|
+
for base_weights in rp_weights.values():
|
|
1117
|
+
for rep, weight in base_weights.items():
|
|
1118
|
+
if rep in w_r:
|
|
1119
|
+
w_r[rep] += weight
|
|
1120
|
+
for rep in w_r:
|
|
1121
|
+
w_r[rep] = w_r[rep] * n_rp / n_base if n_base > 0 else 1.0
|
|
1122
|
+
cost_rows: list[tuple[Any, ...]] = []
|
|
1123
|
+
for (start_idx, last_idx), start in zip(rp_ranges, rp_starts):
|
|
1124
|
+
weight = w_r[start]
|
|
1125
|
+
for t_idx in range(start_idx, last_idx + 1):
|
|
1126
|
+
cost_rows.append(
|
|
1127
|
+
(period_name, timeline_steps[t_idx], str(weight))
|
|
1128
|
+
)
|
|
1129
|
+
out["timestep_weight.csv"] = _to_utf8_frame(
|
|
1130
|
+
("period", "time", "weight"), cost_rows,
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
return out
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
# Map from the basename keys produced by :func:`_compute_rp_frames`
|
|
1137
|
+
# to the canonical Provider keys (no ``.csv`` suffix, per the Phase 3b
|
|
1138
|
+
# convention in :mod:`flextool.engine_polars._provider_keys`). Used by
|
|
1139
|
+
# both :func:`emit_rp_data` and :func:`emit_empty_rp_data` so the six
|
|
1140
|
+
# restored RP basenames + the two pre-existing ones land under the
|
|
1141
|
+
# named ``K.SOLVE_DATA_*`` constants.
|
|
1142
|
+
_RP_BASENAME_TO_PROVIDER_KEY: dict[str, str] = {
|
|
1143
|
+
"rp_weights.csv": K.SOLVE_DATA_RP_WEIGHTS,
|
|
1144
|
+
"rp_base_chain.csv": K.SOLVE_DATA_RP_BASE_CHAIN,
|
|
1145
|
+
"rp_base_first.csv": K.SOLVE_DATA_RP_BASE_FIRST,
|
|
1146
|
+
"rp_base_last.csv": K.SOLVE_DATA_RP_BASE_LAST,
|
|
1147
|
+
"rp_block_first.csv": K.SOLVE_DATA_RP_BLOCK_FIRST,
|
|
1148
|
+
"rp_block_last.csv": K.SOLVE_DATA_RP_BLOCK_LAST,
|
|
1149
|
+
"rp_block_start_last.csv": K.SOLVE_DATA_RP_BLOCK_START_LAST,
|
|
1150
|
+
"rp_base_period_set.csv": K.SOLVE_DATA_RP_BASE_PERIOD_SET,
|
|
1151
|
+
"rp_rep_period_set.csv": K.SOLVE_DATA_RP_REP_PERIOD_SET,
|
|
1152
|
+
"timestep_weight.csv": "solve_data/timestep_weight",
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def emit_rp_data(
|
|
1157
|
+
rp_weights: dict[str, dict[str, float]],
|
|
1158
|
+
timeset_duration_entries: list[tuple[str, float]],
|
|
1159
|
+
period_name: str,
|
|
1160
|
+
timeline_steps: list[str],
|
|
1161
|
+
*, provider,
|
|
1162
|
+
variant: str = "within_solve",
|
|
1163
|
+
per_period_inputs: (
|
|
1164
|
+
"list[tuple[str, dict[str, dict[str, float]], "
|
|
1165
|
+
"list[tuple[str, float]], list[str]]] | None"
|
|
1166
|
+
) = None,
|
|
1167
|
+
) -> None:
|
|
1168
|
+
"""Emit ``rp_data`` to the Provider.
|
|
1169
|
+
|
|
1170
|
+
Phase E added ``variant`` + ``per_period_inputs`` (see
|
|
1171
|
+
:func:`_compute_rp_frames` for semantics). The caller in
|
|
1172
|
+
:mod:`flextool.engine_polars._native_run_model` selects the variant
|
|
1173
|
+
based on the binding-method mix of nodes in this solve; mixed
|
|
1174
|
+
within_solve/within_period in a single solve is rejected upstream
|
|
1175
|
+
with ``FlexToolConfigError`` because the per-solve RP frame family
|
|
1176
|
+
can only carry one chain topology.
|
|
1177
|
+
"""
|
|
1178
|
+
frames = _compute_rp_frames(
|
|
1179
|
+
rp_weights, timeset_duration_entries, period_name, timeline_steps,
|
|
1180
|
+
variant=variant, per_period_inputs=per_period_inputs,
|
|
1181
|
+
)
|
|
1182
|
+
for basename, df in frames.items():
|
|
1183
|
+
_emit(provider, _RP_BASENAME_TO_PROVIDER_KEY[basename], df)
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _compute_timeset_cost_weight_rows(
|
|
1187
|
+
active_time_list: dict[str, list],
|
|
1188
|
+
timesets_used_by_solve: list[tuple[str, str]],
|
|
1189
|
+
timeset_weights: dict[str, dict[str, float]],
|
|
1190
|
+
) -> tuple[list[tuple[Any, ...]], bool]:
|
|
1191
|
+
"""Shared compute for :func:`emit_timeset_cost_weight`.
|
|
1192
|
+
Returns ``(rows, any_written)``.
|
|
1193
|
+
"""
|
|
1194
|
+
rows: list[tuple[Any, ...]] = []
|
|
1195
|
+
any_written = False
|
|
1196
|
+
for period, timeset in timesets_used_by_solve:
|
|
1197
|
+
weights = timeset_weights.get(timeset)
|
|
1198
|
+
active_steps = active_time_list.get(period, [])
|
|
1199
|
+
if weights is None or not active_steps:
|
|
1200
|
+
continue
|
|
1201
|
+
raw = [float(weights.get(step.timestep, 0.0)) for step in active_steps]
|
|
1202
|
+
total = sum(raw)
|
|
1203
|
+
n = len(raw)
|
|
1204
|
+
if total <= 0 or n == 0:
|
|
1205
|
+
continue
|
|
1206
|
+
scale = n / total
|
|
1207
|
+
for step, w in zip(active_steps, raw):
|
|
1208
|
+
rows.append((period, step.timestep, f"{w * scale:.10g}"))
|
|
1209
|
+
any_written = True
|
|
1210
|
+
return rows, any_written
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def emit_timeset_cost_weight(
|
|
1214
|
+
active_time_list: dict[str, list],
|
|
1215
|
+
timesets_used_by_solve: list[tuple[str, str]],
|
|
1216
|
+
timeset_weights: dict[str, dict[str, float]],
|
|
1217
|
+
*, provider,
|
|
1218
|
+
) -> bool:
|
|
1219
|
+
"""Emit ``timeset_cost_weight`` to the Provider."""
|
|
1220
|
+
rows, any_written = _compute_timeset_cost_weight_rows(
|
|
1221
|
+
active_time_list, timesets_used_by_solve, timeset_weights,
|
|
1222
|
+
)
|
|
1223
|
+
if not any_written:
|
|
1224
|
+
return False
|
|
1225
|
+
_emit(provider, "solve_data/timestep_weight.csv",
|
|
1226
|
+
_to_utf8_frame(("period", "time", "weight"), rows))
|
|
1227
|
+
return True
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
_EMPTY_RP_HEADERS: dict[str, tuple[str, ...]] = {
|
|
1231
|
+
"rp_weights.csv": ("base_start", "rep_start", "weight"),
|
|
1232
|
+
"rp_base_chain.csv": ("base_start", "prev_base_start"),
|
|
1233
|
+
"rp_base_first.csv": ("base_start",),
|
|
1234
|
+
"rp_base_last.csv": ("base_start",),
|
|
1235
|
+
"rp_block_first.csv": ("period", "step"),
|
|
1236
|
+
"rp_block_last.csv": ("period", "step"),
|
|
1237
|
+
"rp_block_start_last.csv": ("rep_start", "last_step"),
|
|
1238
|
+
"rp_base_period_set.csv": ("period",),
|
|
1239
|
+
"rp_rep_period_set.csv": ("period",),
|
|
1240
|
+
"timestep_weight.csv": ("period", "time", "weight"),
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def emit_empty_rp_data(
|
|
1245
|
+
*, provider) -> None:
|
|
1246
|
+
"""Emit ``empty_rp_data`` to the Provider."""
|
|
1247
|
+
for filename, headers in _EMPTY_RP_HEADERS.items():
|
|
1248
|
+
_emit(provider, _RP_BASENAME_TO_PROVIDER_KEY[filename],
|
|
1249
|
+
_empty_frame(headers))
|