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,1348 @@
|
|
|
1
|
+
"""Native cascade driver.
|
|
2
|
+
|
|
3
|
+
This module owns the per-solve cascade loop. :func:`native_run_model`
|
|
4
|
+
is invoked by :func:`._orchestration._drive_cascade`.
|
|
5
|
+
|
|
6
|
+
Design decisions
|
|
7
|
+
----------------
|
|
8
|
+
|
|
9
|
+
* **Cascade loop is native; inner preprocessing is not (yet).**
|
|
10
|
+
Scope: own the cascade walk and inline the solve-tree expansion /
|
|
11
|
+
stochastic branching / per-solve setup; not re-port
|
|
12
|
+
``preprocessing.solve_time`` or the ``solve_writers`` module. Those
|
|
13
|
+
remain authoritative and are still called as functions — the
|
|
14
|
+
``_native_leaf_set_override()`` context (wired by
|
|
15
|
+
:func:`._orchestration._drive_cascade`) intercepts the already-native
|
|
16
|
+
preprocessing helpers.
|
|
17
|
+
* **Native solve-tree expansion** via
|
|
18
|
+
:class:`flextool.engine_polars._recursive_solve.RecursiveSolveBuilder`
|
|
19
|
+
+ :class:`._stochastic.StochasticSolver`.
|
|
20
|
+
* **Direct handoff construction.** The post-solve ``SolveHandoff`` is
|
|
21
|
+
built by ``build_handoff_from_solution`` directly from the polar_high
|
|
22
|
+
``Solution`` object — no CSV round-trip.
|
|
23
|
+
* **Optional state fields tolerated.** Native :class:`RunnerState`
|
|
24
|
+
may lack ``timing_recorder`` / ``auto_scale``. Callers that consume
|
|
25
|
+
those guard with ``getattr(state, name, default)``.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import copy
|
|
30
|
+
import csv
|
|
31
|
+
import os
|
|
32
|
+
import time
|
|
33
|
+
from collections import defaultdict
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Imports. Per Phase 3 scope, we depend on legacy preprocessing /
|
|
37
|
+
# solve_writer modules directly — Phase 2 covers the override hook that
|
|
38
|
+
# intercepts the already-ported helpers. Phases 4+ will retire the
|
|
39
|
+
# remaining writers.
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
from flextool.engine_polars._blocks import emit_block_data_for_solve
|
|
43
|
+
# Step 2.5 — legacy preprocessing package deleted (item 15). The per-
|
|
44
|
+
# solve orchestrator now lives natively at
|
|
45
|
+
# :mod:`flextool.engine_polars._emit_solve_time`.
|
|
46
|
+
from flextool.engine_polars import (
|
|
47
|
+
_emit_solve_time as preprocessing_solve_time,
|
|
48
|
+
)
|
|
49
|
+
from flextool.engine_polars._solve_state import (
|
|
50
|
+
FlexToolConfigError,
|
|
51
|
+
FlexToolSolveError,
|
|
52
|
+
)
|
|
53
|
+
# Step 2.5 — solve_writers calls now resolve to the native polars
|
|
54
|
+
# implementations. The legacy disk-writing module remains in the tree
|
|
55
|
+
# but is no longer called from the cascade.
|
|
56
|
+
from flextool.engine_polars import _emit_solve_writers as solve_writers
|
|
57
|
+
from flextool.engine_polars import _provider_keys
|
|
58
|
+
from flextool.engine_polars import _provider_translators
|
|
59
|
+
|
|
60
|
+
from flextool.engine_polars._flex_data_provider import FlexDataProvider
|
|
61
|
+
from flextool.engine_polars._solve_state import compute_level_key
|
|
62
|
+
|
|
63
|
+
# Native solve-tree expansion + stochastic branching + timeline helpers.
|
|
64
|
+
from flextool.engine_polars._recursive_solve import (
|
|
65
|
+
ParentSolveInfo,
|
|
66
|
+
RecursiveSolveBuilder,
|
|
67
|
+
)
|
|
68
|
+
from flextool.engine_polars._stochastic import StochasticSolver
|
|
69
|
+
from flextool.engine_polars._timeline import (
|
|
70
|
+
get_active_time,
|
|
71
|
+
make_period_block,
|
|
72
|
+
separate_period_and_timeseries_data,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Map from RP-flavoured storage_binding_method to its non-RP equivalent.
|
|
77
|
+
# Used by :func:`_downgrade_rp_methods_for_non_rp_solve` to strip the
|
|
78
|
+
# ``_blended_weights`` suffix when a solve's active timeset carries no
|
|
79
|
+
# ``representative_period_weights`` entry. The same storage entity is
|
|
80
|
+
# allowed to drive an RP investment solve AND a chronological dispatch
|
|
81
|
+
# solve back-to-back; per-solve downgrade keeps both paths alive.
|
|
82
|
+
_RP_METHOD_DOWNGRADE: dict[str, str] = {
|
|
83
|
+
"bind_within_solve_blended_weights": "bind_within_solve",
|
|
84
|
+
"bind_within_period_blended_weights": "bind_within_period",
|
|
85
|
+
"bind_forward_only_blended_weights": "bind_forward_only",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _downgrade_rp_methods_for_non_rp_solve(
|
|
90
|
+
*, solve, complete_solve_name, roll_index,
|
|
91
|
+
active_timeset_names, rp_weights, provider, logger,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Silently downgrade RP storage-binding methods on non-RP solves.
|
|
94
|
+
|
|
95
|
+
Replaces the Phase 5 strict check. When the solve's active timeset
|
|
96
|
+
has NO entry in ``state.timeline.rp_weights``, rewrite the per-solve
|
|
97
|
+
Provider's ``input/node__storage_binding_method`` frame in place,
|
|
98
|
+
mapping every ``*_blended_weights`` row to its non-RP equivalent
|
|
99
|
+
(see :data:`_RP_METHOD_DOWNGRADE`). When at least one active
|
|
100
|
+
timeset DOES have RP weights, do nothing — the RP path is correct.
|
|
101
|
+
|
|
102
|
+
The rewrite touches ONLY the per-solve provider (in-memory); the
|
|
103
|
+
on-disk DB and any upstream input CSVs stay unchanged. Downstream
|
|
104
|
+
in this same solve, ``preprocessing_solve_time.run`` will derive
|
|
105
|
+
``solve_data/node__storage_binding_method`` from the rewritten
|
|
106
|
+
``input/...`` key, so all later consumers see the downgraded values
|
|
107
|
+
for THIS solve only.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
solve: per-iter solve name (used in log line).
|
|
111
|
+
complete_solve_name: fully-qualified solve name (debug aid).
|
|
112
|
+
roll_index: integer roll within ``complete_solve_name`` (debug aid).
|
|
113
|
+
active_timeset_names: list of timeset names active in this solve.
|
|
114
|
+
rp_weights: ``state.timeline.rp_weights`` dict (timeset -> weights).
|
|
115
|
+
provider: the per-solve :class:`FlexDataProvider`.
|
|
116
|
+
logger: solve logger. One info-level line is emitted per
|
|
117
|
+
(old, new) downgrade-mapping that fires.
|
|
118
|
+
"""
|
|
119
|
+
# Any active timeset has RP weights → RP path is correct, no-op.
|
|
120
|
+
for ts_name in active_timeset_names:
|
|
121
|
+
if ts_name in rp_weights:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
key = "input/node__storage_binding_method"
|
|
125
|
+
if not provider.has(key):
|
|
126
|
+
return # no per-solve binding-method frame at all → nothing to do
|
|
127
|
+
import polars as pl
|
|
128
|
+
sbm = provider.get(key)
|
|
129
|
+
if sbm.height == 0:
|
|
130
|
+
return
|
|
131
|
+
if "storage_binding_method" in sbm.columns:
|
|
132
|
+
method_col = "storage_binding_method"
|
|
133
|
+
elif "method" in sbm.columns:
|
|
134
|
+
method_col = "method"
|
|
135
|
+
else:
|
|
136
|
+
return # malformed frame; let downstream loaders surface it
|
|
137
|
+
|
|
138
|
+
# Count downgrades per (old, new) pair so the log line is precise.
|
|
139
|
+
per_pair_counts: dict[tuple[str, str], int] = {}
|
|
140
|
+
for old, new in _RP_METHOD_DOWNGRADE.items():
|
|
141
|
+
n = (sbm.filter(pl.col(method_col) == old)
|
|
142
|
+
.select("node")
|
|
143
|
+
.unique()
|
|
144
|
+
.height)
|
|
145
|
+
if n > 0:
|
|
146
|
+
per_pair_counts[(old, new)] = n
|
|
147
|
+
if not per_pair_counts:
|
|
148
|
+
return # nothing to downgrade
|
|
149
|
+
|
|
150
|
+
# In-place rewrite via a single replace_strict expression.
|
|
151
|
+
sbm_new = sbm.with_columns(
|
|
152
|
+
pl.col(method_col).replace_strict(
|
|
153
|
+
_RP_METHOD_DOWNGRADE, default=pl.col(method_col),
|
|
154
|
+
).alias(method_col),
|
|
155
|
+
)
|
|
156
|
+
provider.put(key, sbm_new)
|
|
157
|
+
|
|
158
|
+
ts_summary = (
|
|
159
|
+
", ".join(active_timeset_names) if active_timeset_names else "(none)"
|
|
160
|
+
)
|
|
161
|
+
for (old, new), n in per_pair_counts.items():
|
|
162
|
+
logger.info(
|
|
163
|
+
f"Solve '{solve}' has no representative_period_weights "
|
|
164
|
+
f"for active timeset {ts_summary}; downgrading {n} node(s) "
|
|
165
|
+
f"from {old} to {new}."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def native_run_model(state, solver) -> int:
|
|
170
|
+
"""Drive the per-solve cascade natively.
|
|
171
|
+
|
|
172
|
+
Walks the solve tree, applies stochastic branching, writes the
|
|
173
|
+
per-solve inputs the solver needs (via ``solve_writers`` /
|
|
174
|
+
``preprocessing.solve_time`` — both intercepted where native ports
|
|
175
|
+
exist by the override hook in ``_orchestration._drive_cascade``),
|
|
176
|
+
and invokes the per-solve callback ``solver.run(complete_solve_name)``.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
state: ``RunnerState`` carrier — paths, solve config, timeline,
|
|
180
|
+
handoffs, logger. Either the flextool ``RunnerState`` or
|
|
181
|
+
the native :class:`flextool.engine_polars._solve_state.RunnerState`
|
|
182
|
+
shape; optional fields (``timing_recorder``, ``auto_scale``)
|
|
183
|
+
are tolerated via ``getattr``.
|
|
184
|
+
solver: A :class:`SolverRunner` subclass. In the native cascade
|
|
185
|
+
this is ``_PolarHighCascadeSolver`` defined inside
|
|
186
|
+
:func:`._orchestration._drive_cascade`.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
0 on success.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
FlexToolConfigError: configuration errors.
|
|
193
|
+
FlexToolSolveError: any per-solve non-zero exit status.
|
|
194
|
+
"""
|
|
195
|
+
active_time_lists: dict = {}
|
|
196
|
+
jump_lists: dict = {}
|
|
197
|
+
solve_period_history: defaultdict[str, list] = defaultdict(list)
|
|
198
|
+
fix_storage_time_lists: dict = {}
|
|
199
|
+
realized_time_lists: dict = {}
|
|
200
|
+
complete_solve: dict = {}
|
|
201
|
+
parent_roll: dict = {}
|
|
202
|
+
period__branch_lists: dict = {}
|
|
203
|
+
branch_start_time_lists: defaultdict = defaultdict()
|
|
204
|
+
all_solves: list = []
|
|
205
|
+
|
|
206
|
+
timer = time.perf_counter()
|
|
207
|
+
|
|
208
|
+
wf = state.paths.work_folder
|
|
209
|
+
for sub in ("solve_data", "output_raw", "output_plots"):
|
|
210
|
+
try:
|
|
211
|
+
os.mkdir(wf / sub)
|
|
212
|
+
except FileExistsError:
|
|
213
|
+
state.logger.debug(f"{sub} folder existed")
|
|
214
|
+
|
|
215
|
+
if not state.solve.model_solve:
|
|
216
|
+
message = (
|
|
217
|
+
"No model. Make sure the 'model' class defines solves [Array]."
|
|
218
|
+
)
|
|
219
|
+
state.logger.error(message)
|
|
220
|
+
raise FlexToolConfigError(message)
|
|
221
|
+
solves = next(iter(state.solve.model_solve.values()))
|
|
222
|
+
if not solves:
|
|
223
|
+
message = "No solves in model."
|
|
224
|
+
state.logger.error(message)
|
|
225
|
+
raise FlexToolConfigError(message)
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
# 1. Expand the solve tree (rolling / nested / stochastic).
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
solve_builder = RecursiveSolveBuilder(state)
|
|
231
|
+
for solve in solves:
|
|
232
|
+
parent_info = ParentSolveInfo(solve=None, roll=None)
|
|
233
|
+
result = solve_builder.define_solve_recursive(
|
|
234
|
+
solve, parent_info, None, None, -1,
|
|
235
|
+
)
|
|
236
|
+
all_solves += result.solves
|
|
237
|
+
complete_solve.update(result.complete_solves)
|
|
238
|
+
parent_roll.update(result.parent_roll_lists)
|
|
239
|
+
active_time_lists.update(result.active_time_lists)
|
|
240
|
+
fix_storage_time_lists.update(result.fix_storage_time_lists)
|
|
241
|
+
realized_time_lists.update(copy.deepcopy(result.realized_time_lists))
|
|
242
|
+
|
|
243
|
+
# NOTE: the realized-dispatch overlap resolution (last-wins) runs
|
|
244
|
+
# AFTER the stochastic pass below (step 3.5), where
|
|
245
|
+
# ``realized_time_lists`` is authoritative — the stochastic pass
|
|
246
|
+
# re-derives it.
|
|
247
|
+
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
# 2. Per-real-solve period history accumulation (O(N) instead of O(N²)).
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
cumulative_contributions: list[tuple[str, float]] = []
|
|
252
|
+
cumulative_period_names: set[str] = set()
|
|
253
|
+
|
|
254
|
+
for solve in state.solve.real_solves:
|
|
255
|
+
timeset_periods = {
|
|
256
|
+
pt[0] for pt in state.solve.timesets_used_by_solves[solve]
|
|
257
|
+
}
|
|
258
|
+
state.solve.solve_period_years_represented[solve] = [
|
|
259
|
+
py for py in state.solve.solve_period_years_represented[solve]
|
|
260
|
+
if py[0] in timeset_periods
|
|
261
|
+
]
|
|
262
|
+
history_period_names: set[str] = set()
|
|
263
|
+
for period_name, years in cumulative_contributions:
|
|
264
|
+
if period_name not in history_period_names:
|
|
265
|
+
solve_period_history[solve].append((period_name, years))
|
|
266
|
+
history_period_names.add(period_name)
|
|
267
|
+
for period__year in state.solve.solve_period_years_represented[solve]:
|
|
268
|
+
if period__year[0] not in history_period_names:
|
|
269
|
+
solve_period_history[solve].append(
|
|
270
|
+
(period__year[0], period__year[1])
|
|
271
|
+
)
|
|
272
|
+
history_period_names.add(period__year[0])
|
|
273
|
+
# Default to one year per period when years_represented is empty.
|
|
274
|
+
if not state.solve.solve_period_years_represented[solve]:
|
|
275
|
+
for period__timeset in state.solve.timesets_used_by_solves[solve]:
|
|
276
|
+
if period__timeset[0] not in history_period_names:
|
|
277
|
+
solve_period_history[solve].append(
|
|
278
|
+
(period__timeset[0], 1)
|
|
279
|
+
)
|
|
280
|
+
history_period_names.add(period__timeset[0])
|
|
281
|
+
period_dict_names = {
|
|
282
|
+
t[0] for t in (
|
|
283
|
+
state.solve.realized_periods.get(solve, [])
|
|
284
|
+
+ state.solve.invest_periods.get(solve, [])
|
|
285
|
+
+ state.solve.fix_storage_periods.get(solve, [])
|
|
286
|
+
+ state.solve.realized_invest_periods.get(solve, [])
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
for period in state.solve.solve_period_years_represented[solve]:
|
|
290
|
+
if (
|
|
291
|
+
period[0] in period_dict_names
|
|
292
|
+
and period[0] not in cumulative_period_names
|
|
293
|
+
):
|
|
294
|
+
cumulative_contributions.append((period[0], period[1]))
|
|
295
|
+
cumulative_period_names.add(period[0])
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------
|
|
298
|
+
# 3. Stochastic branching expansion.
|
|
299
|
+
# ------------------------------------------------------------------
|
|
300
|
+
stochastic_solver = StochasticSolver(state)
|
|
301
|
+
(
|
|
302
|
+
period__branch_lists,
|
|
303
|
+
solve_branch__time_branch_lists,
|
|
304
|
+
active_time_lists,
|
|
305
|
+
jump_lists,
|
|
306
|
+
fix_storage_time_lists,
|
|
307
|
+
realized_time_lists,
|
|
308
|
+
branch_start_time_lists,
|
|
309
|
+
) = stochastic_solver.create_stochastic_periods(
|
|
310
|
+
state.solve.stochastic_branches,
|
|
311
|
+
all_solves,
|
|
312
|
+
complete_solve,
|
|
313
|
+
active_time_lists,
|
|
314
|
+
fix_storage_time_lists,
|
|
315
|
+
realized_time_lists,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
# 3.5 Resolve realized-dispatch overlap across solves (LAST-WINS).
|
|
320
|
+
#
|
|
321
|
+
# ``realized_time_lists`` is authoritative only HERE — the stochastic
|
|
322
|
+
# pass above re-derives it. When two solves realize the same
|
|
323
|
+
# (period, timestep), the LATER solve in cascade order wins: it keeps
|
|
324
|
+
# the cell and the earlier solve's claim is dropped. This is the
|
|
325
|
+
# maintainer-directed resolution ("the lower level / later solve
|
|
326
|
+
# wins") and makes each solve's persisted realized slice disjoint, so
|
|
327
|
+
# the downstream multi-solve output union stays a clean concat.
|
|
328
|
+
#
|
|
329
|
+
# Implementation: iterate solves in REVERSE cascade order so a later
|
|
330
|
+
# solve is seen first and claims its (period, timestep) pairs. When
|
|
331
|
+
# an earlier solve reaches a timestep already claimed by a later
|
|
332
|
+
# solve, truncate it (and the rest of that period's realized window —
|
|
333
|
+
# realized windows are contiguous per period, so once one cell is
|
|
334
|
+
# taken the remainder overlaps too). Drops the resolved cells from
|
|
335
|
+
# ``realized_time_lists`` itself, which then flows into
|
|
336
|
+
# ``solve_data/realized_dispatch.csv`` and thus each roll's
|
|
337
|
+
# ``flex_data.realized_dispatch`` that drives per-roll persistence.
|
|
338
|
+
# ------------------------------------------------------------------
|
|
339
|
+
already_realized_timesteps: dict[str, set[str]] = {}
|
|
340
|
+
for solve, realized_time_list in reversed(realized_time_lists.items()):
|
|
341
|
+
for period, timesteps in list(realized_time_list.items()):
|
|
342
|
+
if period not in already_realized_timesteps:
|
|
343
|
+
already_realized_timesteps[period] = set()
|
|
344
|
+
for i, entry in enumerate(timesteps):
|
|
345
|
+
# Hitting a cell already owned by a later solve means the
|
|
346
|
+
# rest of this period's window overlaps too — truncate.
|
|
347
|
+
if entry.timestep in already_realized_timesteps[period]:
|
|
348
|
+
del realized_time_lists[solve][period][i:]
|
|
349
|
+
break
|
|
350
|
+
already_realized_timesteps[period].add(entry.timestep)
|
|
351
|
+
if not realized_time_lists[solve][period]:
|
|
352
|
+
del realized_time_lists[solve][period]
|
|
353
|
+
|
|
354
|
+
for solve in active_time_lists.keys():
|
|
355
|
+
for period in active_time_lists[solve]:
|
|
356
|
+
if (period, period) in period__branch_lists[solve] and not any(
|
|
357
|
+
period == sublist[0]
|
|
358
|
+
for sublist in solve_period_history[complete_solve[solve]]
|
|
359
|
+
):
|
|
360
|
+
message = (
|
|
361
|
+
f"The years_represented is defined, but not to all "
|
|
362
|
+
f"of the periods ({period}) in the solve"
|
|
363
|
+
)
|
|
364
|
+
state.logger.error(message)
|
|
365
|
+
raise FlexToolConfigError(message)
|
|
366
|
+
|
|
367
|
+
timing = time.perf_counter() - timer
|
|
368
|
+
state.logger.debug(f"--- Pre-processing of data: {timing:.4f} seconds ---")
|
|
369
|
+
timing_recorder = getattr(state, "timing_recorder", None)
|
|
370
|
+
if timing_recorder is not None:
|
|
371
|
+
timing_recorder.record(
|
|
372
|
+
"preprocessing_global",
|
|
373
|
+
seconds=timing,
|
|
374
|
+
t_start=time.perf_counter() - timing,
|
|
375
|
+
)
|
|
376
|
+
timer = timer + timing
|
|
377
|
+
|
|
378
|
+
# Step 2.5-E Phase A — Provider-routed pdt -> {pd, pt} split. The
|
|
379
|
+
# cascade-input Provider on ``state`` already carries the seeded
|
|
380
|
+
# ``input/pdt_commodity`` / ``input/pdt_group`` frames; the two
|
|
381
|
+
# derived shards land back on the Provider under the same parent-
|
|
382
|
+
# qualified keys (``input/pd_*``, ``input/pt_*``).
|
|
383
|
+
cascade_input_provider_seed: "FlexDataProvider | None" = getattr(
|
|
384
|
+
state, "cascade_input_provider", None,
|
|
385
|
+
)
|
|
386
|
+
if cascade_input_provider_seed is None:
|
|
387
|
+
cascade_input_provider_seed = FlexDataProvider()
|
|
388
|
+
state.cascade_input_provider = cascade_input_provider_seed
|
|
389
|
+
separate_period_and_timeseries_data(
|
|
390
|
+
state.timeline.timelines,
|
|
391
|
+
state.solve.timesets_used_by_solves,
|
|
392
|
+
provider=cascade_input_provider_seed,
|
|
393
|
+
work_folder=wf,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Step 1-f — cascade-wide Provider seeded once and re-used for every
|
|
397
|
+
# sub-solve's pre-populated frames. Captures the
|
|
398
|
+
# ``solve_writers.emit_timesets`` output (``input/timesets_in_use.csv``
|
|
399
|
+
# + ``input/timesets__timeline.csv``) so the per-sub-solve preprocessing
|
|
400
|
+
# readers can find them via ``provider.get`` without touching disk.
|
|
401
|
+
cascade_input_provider: "FlexDataProvider | None" = getattr(
|
|
402
|
+
state, "cascade_input_provider", None,
|
|
403
|
+
)
|
|
404
|
+
if cascade_input_provider is None:
|
|
405
|
+
cascade_input_provider = FlexDataProvider()
|
|
406
|
+
state.cascade_input_provider = cascade_input_provider
|
|
407
|
+
|
|
408
|
+
# Solve-loop-invariant timesets — hoisted out of the per-solve loop.
|
|
409
|
+
solve_writers.emit_timesets(
|
|
410
|
+
state.solve.timesets_used_by_solves,
|
|
411
|
+
state.timeline.timesets__timeline,
|
|
412
|
+
provider=cascade_input_provider,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# ------------------------------------------------------------------
|
|
416
|
+
# 5. Per-solve loop.
|
|
417
|
+
# ------------------------------------------------------------------
|
|
418
|
+
first = True
|
|
419
|
+
previous_complete_solve = None
|
|
420
|
+
last_captured_solve: str | None = None
|
|
421
|
+
cached_complete_active_time_lists: dict = {}
|
|
422
|
+
|
|
423
|
+
# Pre-compute per-iter level keys. Same-key consecutive iters are
|
|
424
|
+
# rolling continuations of one base solve; a key change marks a
|
|
425
|
+
# sequential / nested level transition. Used to gate phase-progress
|
|
426
|
+
# output: within-group rolling iters emit only the "Solve start"
|
|
427
|
+
# marker, while the last iter of each group emits the four phase
|
|
428
|
+
# checkpoints whose deltas aggregate across the whole group.
|
|
429
|
+
level_keys = [
|
|
430
|
+
compute_level_key(
|
|
431
|
+
solve_name=s,
|
|
432
|
+
complete_solve_name=complete_solve[s],
|
|
433
|
+
solve_config=state.solve,
|
|
434
|
+
timeline_config=state.timeline,
|
|
435
|
+
)
|
|
436
|
+
for s in all_solves
|
|
437
|
+
]
|
|
438
|
+
# Phase 2 — expose the full pre-planned sequence of level_keys onto
|
|
439
|
+
# ``state`` so the per-solve callback (``_PolarHighCascadeSolver.run``)
|
|
440
|
+
# can answer the "is there another solve of THIS level coming?"
|
|
441
|
+
# question used by the warm-path per-level retention slim. Empty
|
|
442
|
+
# tuple is safe (no upcoming iter exists for the empty case).
|
|
443
|
+
state._all_level_keys = tuple(level_keys)
|
|
444
|
+
|
|
445
|
+
for i, solve in enumerate(all_solves):
|
|
446
|
+
timer_in_solve = time.perf_counter()
|
|
447
|
+
|
|
448
|
+
# Between-solves memory snapshot — fires before every roll/solve
|
|
449
|
+
# (i > 0). The previous iter now always emits the four phase
|
|
450
|
+
# checkpoints (ending in ``Outputs written``), so this shows
|
|
451
|
+
# what's left in memory at each solve boundary and cross-solve
|
|
452
|
+
# retention is visible per roll in the standard log. The Δ
|
|
453
|
+
# columns are taken vs. the previous emitted line (``Outputs
|
|
454
|
+
# written``), so positive Δrss here is memory the previous
|
|
455
|
+
# solve's outputs phase did not release.
|
|
456
|
+
_memrec_iter = getattr(state, "_memory_recorder", None)
|
|
457
|
+
if (
|
|
458
|
+
i > 0
|
|
459
|
+
and _memrec_iter is not None
|
|
460
|
+
):
|
|
461
|
+
try:
|
|
462
|
+
_memrec_iter.checkpoint(
|
|
463
|
+
"solve_cleanup", state.logger,
|
|
464
|
+
user_label="Solve cleanup",
|
|
465
|
+
)
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
# Per-sub-solve marker — plain text line (no timer/memory).
|
|
470
|
+
# Always printed so the user can see roll progression even
|
|
471
|
+
# when the four phase checkpoints are suppressed for within-
|
|
472
|
+
# group rolling iters.
|
|
473
|
+
if _memrec_iter is not None and getattr(_memrec_iter, "verbose", True):
|
|
474
|
+
try:
|
|
475
|
+
print(
|
|
476
|
+
f"Solve start: {complete_solve[solve]}, "
|
|
477
|
+
f"{i + 1}/{len(all_solves)}",
|
|
478
|
+
flush=True,
|
|
479
|
+
)
|
|
480
|
+
except OSError:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
state.logger.debug(
|
|
484
|
+
f"Creating timelines for solve {solve} ({i})"
|
|
485
|
+
)
|
|
486
|
+
cs = complete_solve[solve]
|
|
487
|
+
|
|
488
|
+
# Per-level Provider detection (Design A, step A2). Below we
|
|
489
|
+
# reuse a single FlexDataProvider per distinct level_key so
|
|
490
|
+
# iters at the same level share one Provider instance.
|
|
491
|
+
_level_key = level_keys[i]
|
|
492
|
+
# level_key is debug-only output (verbose mem mode):
|
|
493
|
+
# ``runner.state.logger`` is forced to ERROR level by the
|
|
494
|
+
# orchestration driver, so the regular ERROR-level print would
|
|
495
|
+
# always escape. Gate behind the same env var that surfaces
|
|
496
|
+
# the full mem-checkpoint trace.
|
|
497
|
+
if os.environ.get("FLEXTOOL_MEMORY_VERBOSE") == "1":
|
|
498
|
+
state.logger.error(
|
|
499
|
+
"level_key for solve %r (complete=%r): %r",
|
|
500
|
+
solve, complete_solve[solve], _level_key,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Per-level boundary (Design A, step A3): when the level_key
|
|
504
|
+
# changes from the previous iter, drop any warm-LP carry-over
|
|
505
|
+
# on the cascade solver so we don't accidentally reuse
|
|
506
|
+
# structures keyed on the prior level's matrix shape. The
|
|
507
|
+
# warm-LP fingerprint check inside ``_PolarHighCascadeSolver.run``
|
|
508
|
+
# will ALSO null these on shape change (a level transition
|
|
509
|
+
# always changes the FlexData shape in practice), but doing
|
|
510
|
+
# it explicitly at the level boundary makes the intent
|
|
511
|
+
# explicit and gives subprocess-per-chain work a clean
|
|
512
|
+
# lifecycle hook.
|
|
513
|
+
_last_level_key = getattr(state, "_last_level_key_seen", None)
|
|
514
|
+
if _last_level_key is not None and _last_level_key != _level_key:
|
|
515
|
+
if hasattr(solver, "_warm_problem"):
|
|
516
|
+
solver._warm_problem = None
|
|
517
|
+
if hasattr(solver, "_prior_data"):
|
|
518
|
+
solver._prior_data = None
|
|
519
|
+
state._last_level_key_seen = _level_key
|
|
520
|
+
|
|
521
|
+
if cs not in cached_complete_active_time_lists:
|
|
522
|
+
cached_complete_active_time_lists[cs] = get_active_time(
|
|
523
|
+
cs,
|
|
524
|
+
state.solve.timesets_used_by_solves,
|
|
525
|
+
state.timeline.timeset_durations,
|
|
526
|
+
state.timeline.timelines,
|
|
527
|
+
state.timeline.timesets__timeline,
|
|
528
|
+
)
|
|
529
|
+
complete_active_time_lists = cached_complete_active_time_lists[cs]
|
|
530
|
+
|
|
531
|
+
# Combined period/timeset list including history periods.
|
|
532
|
+
period__timesets_with_history = list(
|
|
533
|
+
state.solve.timesets_used_by_solves[complete_solve[solve]]
|
|
534
|
+
)
|
|
535
|
+
current_periods = {pt[0] for pt in period__timesets_with_history}
|
|
536
|
+
current_timeset = (
|
|
537
|
+
period__timesets_with_history[0][1]
|
|
538
|
+
if period__timesets_with_history else None
|
|
539
|
+
)
|
|
540
|
+
for history_period, _ in solve_period_history[complete_solve[solve]]:
|
|
541
|
+
if history_period not in current_periods:
|
|
542
|
+
if current_timeset:
|
|
543
|
+
period__timesets_with_history.append(
|
|
544
|
+
(history_period, current_timeset)
|
|
545
|
+
)
|
|
546
|
+
current_periods.add(history_period)
|
|
547
|
+
|
|
548
|
+
# Per-level Provider (Design A, step A2). Two consecutive
|
|
549
|
+
# iters with the same ``_level_key`` reuse the same Provider —
|
|
550
|
+
# e.g. the 72 dispatch rolls on the multi-invest fixture all
|
|
551
|
+
# share one Provider, while each of the 4 invest sub-solves
|
|
552
|
+
# (distinct period windows -> distinct keys) gets its own.
|
|
553
|
+
# On level transition, build a fresh Provider seeded from the
|
|
554
|
+
# cascade-input frames; per-iter writers below overwrite their
|
|
555
|
+
# own keys in place across iters at the same level.
|
|
556
|
+
if not hasattr(state, "_level_providers"):
|
|
557
|
+
state._level_providers = {}
|
|
558
|
+
sub_solve_provider = state._level_providers.get(_level_key)
|
|
559
|
+
if sub_solve_provider is None:
|
|
560
|
+
sub_solve_provider = FlexDataProvider()
|
|
561
|
+
# Seed cascade-wide ``input/*.csv`` frames so per-iter
|
|
562
|
+
# readers find them via ``provider.get``
|
|
563
|
+
# (e.g. ``input/timesets_in_use.csv``).
|
|
564
|
+
for _key, _frame in cascade_input_provider.items():
|
|
565
|
+
sub_solve_provider.put(_key, _frame)
|
|
566
|
+
state._level_providers[_level_key] = sub_solve_provider
|
|
567
|
+
# else: existing Provider — already seeded; per-iter writers
|
|
568
|
+
# below will overwrite their own keys in place.
|
|
569
|
+
# Resolve nesting parent's complete-solve name; reused below by
|
|
570
|
+
# the parent-handoff translator call (see Phase 4.1e).
|
|
571
|
+
_parent_solve_for_carriers = parent_roll.get(solve)
|
|
572
|
+
_parent_complete_for_carriers = (
|
|
573
|
+
complete_solve.get(_parent_solve_for_carriers)
|
|
574
|
+
if _parent_solve_for_carriers else None
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# S1-g-3 — expose the per-sub-solve Provider to writer entry
|
|
578
|
+
# points BEFORE preprocessing runs, so native writers threaded
|
|
579
|
+
# with ``provider=`` (via :func:`_emit_solve_time.run`) can
|
|
580
|
+
# fetch it from ``state.current_provider``. Writers not yet
|
|
581
|
+
# threaded still resolve their reads via the Provider-as-seed
|
|
582
|
+
# bridge installed above; Step 2 deletes both paths.
|
|
583
|
+
state.current_provider = sub_solve_provider
|
|
584
|
+
|
|
585
|
+
# Memory checkpoints — fire only on the first sub-solve so the
|
|
586
|
+
# output stays readable on multi-roll scenarios. Used to
|
|
587
|
+
# attribute the pre-load_flextool preprocessing-chain memory
|
|
588
|
+
# spike across writer groups.
|
|
589
|
+
_mem_cp = None
|
|
590
|
+
if i == 0:
|
|
591
|
+
_memrec_native = getattr(state, "_memory_recorder", None)
|
|
592
|
+
if _memrec_native is not None:
|
|
593
|
+
def _mem_cp(label: str, user_label: str,
|
|
594
|
+
_rec=_memrec_native, _log=state.logger) -> None:
|
|
595
|
+
_rec.checkpoint(label, _log, user_label=user_label)
|
|
596
|
+
if _mem_cp is not None:
|
|
597
|
+
_mem_cp("prep_seeded",
|
|
598
|
+
"prep: provider seeded")
|
|
599
|
+
|
|
600
|
+
solve_writers.emit_full_timelines(
|
|
601
|
+
state.timeline.stochastic_timesteps[solve],
|
|
602
|
+
period__timesets_with_history,
|
|
603
|
+
state.timeline.timesets__timeline,
|
|
604
|
+
state.timeline.timelines,
|
|
605
|
+
str(wf / "solve_data/steps_in_timeline.csv"),
|
|
606
|
+
provider=sub_solve_provider,
|
|
607
|
+
)
|
|
608
|
+
solve_writers.emit_active_timelines(
|
|
609
|
+
active_time_lists[solve],
|
|
610
|
+
str(wf / "solve_data/steps_in_use.csv"),
|
|
611
|
+
provider=sub_solve_provider,
|
|
612
|
+
)
|
|
613
|
+
solve_writers.emit_active_timelines(
|
|
614
|
+
complete_active_time_lists,
|
|
615
|
+
str(wf / "solve_data/steps_complete_solve.csv"),
|
|
616
|
+
complete=True,
|
|
617
|
+
provider=sub_solve_provider,
|
|
618
|
+
)
|
|
619
|
+
solve_writers.emit_step_jump(
|
|
620
|
+
jump_lists[solve],
|
|
621
|
+
provider=sub_solve_provider,
|
|
622
|
+
)
|
|
623
|
+
pb_time, pb_succ = make_period_block(active_time_lists[solve])
|
|
624
|
+
solve_writers.emit_period_block(
|
|
625
|
+
pb_time, pb_succ,
|
|
626
|
+
provider=sub_solve_provider,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
if _mem_cp is not None:
|
|
630
|
+
_mem_cp("prep_timeline_writers_done",
|
|
631
|
+
"prep: timeline writers done")
|
|
632
|
+
|
|
633
|
+
state.logger.debug("Creating period data")
|
|
634
|
+
solve_writers.emit_period_years(
|
|
635
|
+
period__branch_lists[solve],
|
|
636
|
+
solve_period_history[complete_solve[solve]],
|
|
637
|
+
str(wf / "solve_data/period_with_history.csv"),
|
|
638
|
+
provider=sub_solve_provider,
|
|
639
|
+
)
|
|
640
|
+
solve_writers.emit_periods(
|
|
641
|
+
complete_solve[solve],
|
|
642
|
+
state.solve.realized_invest_periods,
|
|
643
|
+
str(wf / "solve_data/realized_invest_periods_of_current_solve.csv"),
|
|
644
|
+
provider=sub_solve_provider,
|
|
645
|
+
)
|
|
646
|
+
# If realized_invest_periods is empty but both invest_periods and
|
|
647
|
+
# realized_periods are defined, fall back to realized_periods.
|
|
648
|
+
if (
|
|
649
|
+
not state.solve.realized_invest_periods[complete_solve[solve]]
|
|
650
|
+
and state.solve.invest_periods[complete_solve[solve]]
|
|
651
|
+
and state.solve.realized_periods[complete_solve[solve]]
|
|
652
|
+
):
|
|
653
|
+
solve_writers.emit_periods(
|
|
654
|
+
complete_solve[solve],
|
|
655
|
+
state.solve.realized_periods,
|
|
656
|
+
str(
|
|
657
|
+
wf
|
|
658
|
+
/ "solve_data/realized_invest_periods_of_current_solve.csv"
|
|
659
|
+
),
|
|
660
|
+
provider=sub_solve_provider,
|
|
661
|
+
)
|
|
662
|
+
sub_solve_provider.put(
|
|
663
|
+
_provider_keys.SOLVE_DATA_INVEST_PERIODS_OF_CURRENT_SOLVE,
|
|
664
|
+
solve_writers.derive_periods(
|
|
665
|
+
complete_solve[solve], state.solve.invest_periods,
|
|
666
|
+
),
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
years_rep = state.solve.solve_period_years_represented[
|
|
670
|
+
complete_solve[solve]
|
|
671
|
+
]
|
|
672
|
+
if not years_rep:
|
|
673
|
+
years_rep = [
|
|
674
|
+
(pt[0], 1)
|
|
675
|
+
for pt in state.solve.timesets_used_by_solves[
|
|
676
|
+
complete_solve[solve]
|
|
677
|
+
]
|
|
678
|
+
]
|
|
679
|
+
solve_writers.emit_years_represented(
|
|
680
|
+
period__branch_lists[solve],
|
|
681
|
+
years_rep,
|
|
682
|
+
str(wf / "solve_data/p_years_represented.csv"),
|
|
683
|
+
provider=sub_solve_provider,
|
|
684
|
+
)
|
|
685
|
+
solve_writers.emit_period_years(
|
|
686
|
+
period__branch_lists[solve],
|
|
687
|
+
years_rep,
|
|
688
|
+
str(wf / "solve_data/p_discount_years.csv"),
|
|
689
|
+
provider=sub_solve_provider,
|
|
690
|
+
)
|
|
691
|
+
solve_writers.emit_current_solve(
|
|
692
|
+
solve, str(wf / "solve_data/solve_current.csv"),
|
|
693
|
+
provider=sub_solve_provider,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
if _mem_cp is not None:
|
|
697
|
+
_mem_cp("prep_period_writers_done",
|
|
698
|
+
"prep: period writers done")
|
|
699
|
+
|
|
700
|
+
# ---- LP scaling: emit user-set row-scaling state and the
|
|
701
|
+
# header-only objective-scaling CSV. ``analyze_solve`` runs
|
|
702
|
+
# post-solve from ``_orchestration.py`` for reporting (Γ-scale);
|
|
703
|
+
# auto-row-scaling and objective auto-scaling were removed in
|
|
704
|
+
# Δ.22 along with the ``--auto-scale`` CLI flag.
|
|
705
|
+
solve_writers.emit_p_use_row_scaling(
|
|
706
|
+
solve,
|
|
707
|
+
state.solve.use_row_scaling,
|
|
708
|
+
str(wf / "solve_data/p_use_row_scaling.csv"),
|
|
709
|
+
provider=sub_solve_provider,
|
|
710
|
+
)
|
|
711
|
+
solve_writers.emit_scale_the_objective_header_only(
|
|
712
|
+
wf / "solve_data",
|
|
713
|
+
provider=sub_solve_provider,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
if _mem_cp is not None:
|
|
717
|
+
_mem_cp("prep_scaling_done", "prep: scaling done")
|
|
718
|
+
|
|
719
|
+
solve_writers.emit_first_steps(
|
|
720
|
+
active_time_lists[solve],
|
|
721
|
+
str(wf / "solve_data/first_timesteps.csv"),
|
|
722
|
+
provider=sub_solve_provider,
|
|
723
|
+
)
|
|
724
|
+
solve_writers.emit_last_steps(
|
|
725
|
+
active_time_lists[solve],
|
|
726
|
+
str(wf / "solve_data/last_timesteps.csv"),
|
|
727
|
+
provider=sub_solve_provider,
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
state.logger.debug("Create realized timeline")
|
|
731
|
+
solve_writers.emit_realized_dispatch(
|
|
732
|
+
realized_time_lists[solve],
|
|
733
|
+
complete_solve[solve],
|
|
734
|
+
state.solve.realized_periods.get(complete_solve[solve], []),
|
|
735
|
+
provider=sub_solve_provider,
|
|
736
|
+
)
|
|
737
|
+
solve_writers.emit_fix_storage_timesteps(
|
|
738
|
+
fix_storage_time_lists[solve],
|
|
739
|
+
complete_solve[solve],
|
|
740
|
+
state.solve.fix_storage_periods.get(complete_solve[solve], []),
|
|
741
|
+
provider=sub_solve_provider,
|
|
742
|
+
)
|
|
743
|
+
solve_writers.emit_delayed_durations(
|
|
744
|
+
active_time_lists[solve],
|
|
745
|
+
complete_solve[solve],
|
|
746
|
+
state.solve.delay_durations,
|
|
747
|
+
provider=sub_solve_provider,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
if _mem_cp is not None:
|
|
751
|
+
_mem_cp("prep_step_writers_done",
|
|
752
|
+
"prep: step + realized + fix_storage + delayed writers done")
|
|
753
|
+
|
|
754
|
+
state.logger.debug("Possible stochastics")
|
|
755
|
+
solve_writers.emit_branch__period_relationship(
|
|
756
|
+
period__branch_lists[solve],
|
|
757
|
+
str(wf / "solve_data/period__branch.csv"),
|
|
758
|
+
provider=sub_solve_provider,
|
|
759
|
+
)
|
|
760
|
+
solve_writers.emit_branch_weights_and_map(
|
|
761
|
+
complete_solve[solve],
|
|
762
|
+
active_time_lists[solve],
|
|
763
|
+
solve_branch__time_branch_lists[solve],
|
|
764
|
+
branch_start_time_lists[solve],
|
|
765
|
+
period__branch_lists[solve],
|
|
766
|
+
state.solve.stochastic_branches,
|
|
767
|
+
provider=sub_solve_provider,
|
|
768
|
+
)
|
|
769
|
+
solve_writers.emit_first_and_last_periods(
|
|
770
|
+
active_time_lists[solve],
|
|
771
|
+
state.solve.timesets_used_by_solves[complete_solve[solve]],
|
|
772
|
+
period__branch_lists[solve],
|
|
773
|
+
provider=sub_solve_provider,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if _mem_cp is not None:
|
|
777
|
+
_mem_cp("prep_branch_writers_done",
|
|
778
|
+
"prep: branch + first_and_last_periods writers done")
|
|
779
|
+
|
|
780
|
+
# ---- Storage fixing from upper level ----
|
|
781
|
+
if (
|
|
782
|
+
[complete_solve[solve]] in state.solve.contains_solves.values()
|
|
783
|
+
and complete_solve[parent_roll[solve]] in state.solve.fix_storage_periods
|
|
784
|
+
):
|
|
785
|
+
storage_fix_values_exist = True
|
|
786
|
+
else:
|
|
787
|
+
storage_fix_values_exist = False
|
|
788
|
+
|
|
789
|
+
if storage_fix_values_exist:
|
|
790
|
+
state.logger.info("Nested timeline matching")
|
|
791
|
+
matching_map = stochastic_solver.write_timeline_matching_map(
|
|
792
|
+
active_time_lists[parent_roll[solve]],
|
|
793
|
+
active_time_lists[solve],
|
|
794
|
+
complete_solve[parent_roll[solve]],
|
|
795
|
+
complete_solve[solve],
|
|
796
|
+
period__branch_lists[solve],
|
|
797
|
+
)
|
|
798
|
+
with open(
|
|
799
|
+
wf / "solve_data/timeline_matching_map.csv", "w", newline=""
|
|
800
|
+
) as realfile:
|
|
801
|
+
writer = csv.writer(realfile)
|
|
802
|
+
writer.writerow(["period", "step", "upper_step"])
|
|
803
|
+
for (period, step), upper_step in matching_map.items():
|
|
804
|
+
writer.writerow([period, step, upper_step])
|
|
805
|
+
else:
|
|
806
|
+
with open(wf / "solve_data/timeline_matching_map.csv", "w") as realfile:
|
|
807
|
+
realfile.write("period,step,upper_step\n")
|
|
808
|
+
|
|
809
|
+
# Aggregate timeseries when step size changes between solves.
|
|
810
|
+
if previous_complete_solve != complete_solve[solve]:
|
|
811
|
+
state.logger.debug(
|
|
812
|
+
"Aggregating timeline and parameters for the new step size"
|
|
813
|
+
)
|
|
814
|
+
# Step 2.5-E Phase C — route averaged-timeseries reads /
|
|
815
|
+
# writes through the cascade-input Provider so the
|
|
816
|
+
# ``solve_data/pt_*`` frames persist across every sub-solve
|
|
817
|
+
# belonging to the same ``complete_solve``. Legacy code
|
|
818
|
+
# relied on the on-disk ``solve_data/pt_*.csv`` files
|
|
819
|
+
# remaining valid for all rolls of one ``complete_solve``;
|
|
820
|
+
# in Provider-land the equivalent is keeping the frames on
|
|
821
|
+
# the cascade-input Provider so each fresh sub-solve
|
|
822
|
+
# Provider seeds them at the top of its iter (line 444).
|
|
823
|
+
state.timeline.create_averaged_timeseries(
|
|
824
|
+
complete_solve[solve], state.solve, state.logger,
|
|
825
|
+
provider=cascade_input_provider,
|
|
826
|
+
work_folder=wf,
|
|
827
|
+
)
|
|
828
|
+
# Mirror the freshly aggregated frames onto the current
|
|
829
|
+
# sub-solve Provider too — this iter's readers must find
|
|
830
|
+
# them without waiting for the next seed pass.
|
|
831
|
+
for _key, _frame in cascade_input_provider.items():
|
|
832
|
+
if _key.startswith("solve_data/pt_") or _key.startswith(
|
|
833
|
+
"solve_data/pbt_",
|
|
834
|
+
):
|
|
835
|
+
sub_solve_provider.put(_key, _frame)
|
|
836
|
+
previous_complete_solve = complete_solve[solve]
|
|
837
|
+
|
|
838
|
+
# ---- Block data (Agent 1.1) ----
|
|
839
|
+
# Δ.31: thread the per-sub-solve Provider so the legacy reads
|
|
840
|
+
# find the ``input/*.csv`` frames in memory. Without this the
|
|
841
|
+
# cascade silently produces a single-block layout (the input/
|
|
842
|
+
# CSVs are kept on the Provider rather than flushed to disk),
|
|
843
|
+
# collapsing every coarse-block fixture (e.g. lh2_three_region)
|
|
844
|
+
# to the default block and breaking the daily-block aggregation.
|
|
845
|
+
try:
|
|
846
|
+
emit_block_data_for_solve(
|
|
847
|
+
solve=complete_solve[solve],
|
|
848
|
+
solve_config=state.solve,
|
|
849
|
+
timeline_config=state.timeline,
|
|
850
|
+
work_folder=wf,
|
|
851
|
+
active_time_list=active_time_lists[solve],
|
|
852
|
+
default_jump_list=jump_lists[solve],
|
|
853
|
+
provider=sub_solve_provider,
|
|
854
|
+
emit_provider=sub_solve_provider,
|
|
855
|
+
)
|
|
856
|
+
except FlexToolConfigError:
|
|
857
|
+
raise
|
|
858
|
+
except Exception as exc: # pragma: no cover — defensive only
|
|
859
|
+
state.logger.warning(
|
|
860
|
+
f"blocks: emission failed (non-fatal — not consumed yet): {exc}"
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
first_of_nested_level = solve in state.solve.first_of_complete_solve
|
|
864
|
+
last_of_nested_level = solve in state.solve.last_of_solve
|
|
865
|
+
|
|
866
|
+
# Phase 4.1j — the parent-to-child fix_storage hand-off is now
|
|
867
|
+
# carried exclusively by the typed ``handoff/*`` Provider keys
|
|
868
|
+
# seeded at iteration start by the parent-handoff translator
|
|
869
|
+
# (Phase 4.1e). The legacy shutil.copy archive path and the
|
|
870
|
+
# in-memory carrier fallback that wrote to the now-dead
|
|
871
|
+
# ``solve_data/fix_storage_*`` Provider keys have been deleted;
|
|
872
|
+
# all downstream consumers (input.py, _derived_params.py,
|
|
873
|
+
# _emit_arc_unions.py, _emit_per_solve.py) consult ``handoff/*``.
|
|
874
|
+
|
|
875
|
+
solve_writers.emit_solve_status(
|
|
876
|
+
first_of_nested_level, last_of_nested_level,
|
|
877
|
+
nested=True,
|
|
878
|
+
provider=sub_solve_provider,
|
|
879
|
+
)
|
|
880
|
+
last = i == len(solves) - 1
|
|
881
|
+
solve_writers.emit_solve_status(
|
|
882
|
+
first, last,
|
|
883
|
+
provider=sub_solve_provider,
|
|
884
|
+
)
|
|
885
|
+
if i == 0:
|
|
886
|
+
first = False
|
|
887
|
+
solve_writers.emit_empty_investment_file(
|
|
888
|
+
provider=sub_solve_provider,
|
|
889
|
+
)
|
|
890
|
+
solve_writers.emit_empty_storage_fix_file(
|
|
891
|
+
provider=sub_solve_provider,
|
|
892
|
+
)
|
|
893
|
+
solve_writers.emit_headers_for_empty_output_files(
|
|
894
|
+
str(wf / "solve_data/costs_discounted.csv"),
|
|
895
|
+
"param_costs,costs_discounted",
|
|
896
|
+
provider=sub_solve_provider,
|
|
897
|
+
)
|
|
898
|
+
solve_writers.emit_headers_for_empty_output_files(
|
|
899
|
+
str(wf / "solve_data/co2.csv"),
|
|
900
|
+
"param_co2,model_wide",
|
|
901
|
+
provider=sub_solve_provider,
|
|
902
|
+
)
|
|
903
|
+
solve_writers.emit_headers_for_empty_output_files(
|
|
904
|
+
str(wf / "solve_data/period_capacity.csv"),
|
|
905
|
+
"period",
|
|
906
|
+
provider=sub_solve_provider,
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# ---- Representative-period / timeset weights ----
|
|
910
|
+
timesets_used = state.solve.timesets_used_by_solves.get(
|
|
911
|
+
complete_solve[solve], []
|
|
912
|
+
)
|
|
913
|
+
active_timeset_names = [ts for _, ts in timesets_used]
|
|
914
|
+
# Phase C — silent degrade. Replaces the Phase 5 strict check.
|
|
915
|
+
# When the active timeset has no representative_period_weights,
|
|
916
|
+
# rewrite the per-solve provider's node__storage_binding_method
|
|
917
|
+
# so the three ``*_blended_weights`` variants degrade to their
|
|
918
|
+
# non-RP equivalents for THIS solve only. Same storage entity
|
|
919
|
+
# can now legitimately drive both an RP-active investment solve
|
|
920
|
+
# and a chronological dispatch solve back-to-back.
|
|
921
|
+
_downgrade_rp_methods_for_non_rp_solve(
|
|
922
|
+
solve=solve,
|
|
923
|
+
complete_solve_name=complete_solve[solve],
|
|
924
|
+
roll_index=i,
|
|
925
|
+
active_timeset_names=active_timeset_names,
|
|
926
|
+
rp_weights=state.timeline.rp_weights,
|
|
927
|
+
provider=sub_solve_provider,
|
|
928
|
+
logger=state.logger,
|
|
929
|
+
)
|
|
930
|
+
for ts_name in active_timeset_names:
|
|
931
|
+
if (
|
|
932
|
+
ts_name in state.timeline.rp_weights
|
|
933
|
+
and ts_name in state.timeline.timeset_weights
|
|
934
|
+
):
|
|
935
|
+
message = (
|
|
936
|
+
f"Timeset '{ts_name}' has both "
|
|
937
|
+
"representative_period_weights and timeset_weights set. "
|
|
938
|
+
"Pick one: use representative_period_weights for RP "
|
|
939
|
+
"scenarios and timeset_weights for non-RP per-step "
|
|
940
|
+
"weighting."
|
|
941
|
+
)
|
|
942
|
+
state.logger.error(message)
|
|
943
|
+
raise FlexToolConfigError(message)
|
|
944
|
+
|
|
945
|
+
# Phase E — determine the RP chain TOPOLOGY for this solve based
|
|
946
|
+
# on which blended-weights binding methods appear in the per-solve
|
|
947
|
+
# ``input/node__storage_binding_method`` frame. The frame family
|
|
948
|
+
# emitted to the per-solve provider can carry only ONE chain
|
|
949
|
+
# topology, so mixing ``bind_within_solve_blended_weights`` (or
|
|
950
|
+
# ``bind_forward_only_blended_weights`` — same across-solve
|
|
951
|
+
# topology) with ``bind_within_period_blended_weights`` (per-
|
|
952
|
+
# FlexTool-period topology) in a single solve is rejected here
|
|
953
|
+
# with a ``FlexToolConfigError`` listing the conflict and the
|
|
954
|
+
# canonical fix (separate solves, or pick one variant for the
|
|
955
|
+
# whole solve). The downgrade above has already rewritten
|
|
956
|
+
# blended-weights methods to their non-RP equivalents on non-RP
|
|
957
|
+
# solves, so this branch only sees genuine RP-active solves.
|
|
958
|
+
_rp_variant = "within_solve"
|
|
959
|
+
_wp_nodes: list[str] = []
|
|
960
|
+
_ws_or_fo_nodes: list[str] = []
|
|
961
|
+
if sub_solve_provider.has("input/node__storage_binding_method"):
|
|
962
|
+
_sbm = sub_solve_provider.get(
|
|
963
|
+
"input/node__storage_binding_method"
|
|
964
|
+
)
|
|
965
|
+
if _sbm.height > 0:
|
|
966
|
+
_mcol = ("storage_binding_method"
|
|
967
|
+
if "storage_binding_method" in _sbm.columns
|
|
968
|
+
else "method")
|
|
969
|
+
import polars as _pl
|
|
970
|
+
_wp_df = _sbm.filter(
|
|
971
|
+
_pl.col(_mcol) == "bind_within_period_blended_weights"
|
|
972
|
+
).select("node").unique()
|
|
973
|
+
_ws_df = _sbm.filter(_pl.col(_mcol).is_in([
|
|
974
|
+
"bind_within_solve_blended_weights",
|
|
975
|
+
"bind_forward_only_blended_weights",
|
|
976
|
+
])).select("node").unique()
|
|
977
|
+
_wp_nodes = _wp_df["node"].to_list() if _wp_df.height else []
|
|
978
|
+
_ws_or_fo_nodes = (
|
|
979
|
+
_ws_df["node"].to_list() if _ws_df.height else []
|
|
980
|
+
)
|
|
981
|
+
if _wp_nodes and _ws_or_fo_nodes:
|
|
982
|
+
_shown_wp = _wp_nodes[:5]
|
|
983
|
+
_shown_ws = _ws_or_fo_nodes[:5]
|
|
984
|
+
raise FlexToolConfigError(
|
|
985
|
+
f"Solve '{complete_solve[solve]}' mixes "
|
|
986
|
+
f"bind_within_period_blended_weights "
|
|
987
|
+
f"({len(_wp_nodes)} node(s), e.g. "
|
|
988
|
+
f"{', '.join(_shown_wp)}) with "
|
|
989
|
+
f"bind_within_solve_blended_weights / "
|
|
990
|
+
f"bind_forward_only_blended_weights "
|
|
991
|
+
f"({len(_ws_or_fo_nodes)} node(s), e.g. "
|
|
992
|
+
f"{', '.join(_shown_ws)}). The two RP chain "
|
|
993
|
+
f"topologies (per-FlexTool-period vs. across-"
|
|
994
|
+
f"solve) cannot share the per-solve RP frame "
|
|
995
|
+
f"family — split into separate solves, or pick "
|
|
996
|
+
f"a single blended-weights variant for every "
|
|
997
|
+
f"node in this solve."
|
|
998
|
+
)
|
|
999
|
+
if _wp_nodes:
|
|
1000
|
+
_rp_variant = "within_period"
|
|
1001
|
+
|
|
1002
|
+
rp_written = False
|
|
1003
|
+
if _rp_variant == "within_period":
|
|
1004
|
+
# Iterate ALL RP-bearing active timesets (each owning a
|
|
1005
|
+
# distinct FlexTool period) and accumulate per-period inputs.
|
|
1006
|
+
# Cross-period chain edges are dropped by _compute_rp_frames
|
|
1007
|
+
# so each period closes its own cycle independently.
|
|
1008
|
+
per_period_inputs: list = []
|
|
1009
|
+
for ts_name in active_timeset_names:
|
|
1010
|
+
if ts_name not in state.timeline.rp_weights:
|
|
1011
|
+
continue
|
|
1012
|
+
period_name = None
|
|
1013
|
+
for p, ts in timesets_used:
|
|
1014
|
+
if ts == ts_name:
|
|
1015
|
+
period_name = p
|
|
1016
|
+
break
|
|
1017
|
+
if not period_name:
|
|
1018
|
+
continue
|
|
1019
|
+
timeline_name = state.timeline.timesets__timeline[ts_name]
|
|
1020
|
+
timeline_steps = [
|
|
1021
|
+
step
|
|
1022
|
+
for step, _dur in state.timeline.timelines.get(
|
|
1023
|
+
timeline_name, []
|
|
1024
|
+
)
|
|
1025
|
+
]
|
|
1026
|
+
per_period_inputs.append((
|
|
1027
|
+
period_name,
|
|
1028
|
+
state.timeline.rp_weights[ts_name],
|
|
1029
|
+
state.timeline.timeset_durations[ts_name],
|
|
1030
|
+
timeline_steps,
|
|
1031
|
+
))
|
|
1032
|
+
if per_period_inputs:
|
|
1033
|
+
# The legacy single-period args are ignored when variant
|
|
1034
|
+
# is within_period (asserted inside _compute_rp_frames),
|
|
1035
|
+
# but pass through harmless placeholders for the
|
|
1036
|
+
# signature; the function reads from per_period_inputs.
|
|
1037
|
+
first_p_name, first_rp_w, first_ts_dur, first_tl = (
|
|
1038
|
+
per_period_inputs[0]
|
|
1039
|
+
)
|
|
1040
|
+
solve_writers.emit_rp_data(
|
|
1041
|
+
rp_weights=first_rp_w,
|
|
1042
|
+
timeset_duration_entries=first_ts_dur,
|
|
1043
|
+
period_name=first_p_name,
|
|
1044
|
+
timeline_steps=first_tl,
|
|
1045
|
+
provider=sub_solve_provider,
|
|
1046
|
+
variant="within_period",
|
|
1047
|
+
per_period_inputs=per_period_inputs,
|
|
1048
|
+
)
|
|
1049
|
+
rp_written = True
|
|
1050
|
+
else:
|
|
1051
|
+
for ts_name in active_timeset_names:
|
|
1052
|
+
if ts_name in state.timeline.rp_weights:
|
|
1053
|
+
period_name = None
|
|
1054
|
+
for p, ts in timesets_used:
|
|
1055
|
+
if ts == ts_name:
|
|
1056
|
+
period_name = p
|
|
1057
|
+
break
|
|
1058
|
+
if period_name:
|
|
1059
|
+
timeline_name = state.timeline.timesets__timeline[ts_name]
|
|
1060
|
+
timeline_steps = [
|
|
1061
|
+
step
|
|
1062
|
+
for step, _dur in state.timeline.timelines.get(
|
|
1063
|
+
timeline_name, []
|
|
1064
|
+
)
|
|
1065
|
+
]
|
|
1066
|
+
solve_writers.emit_rp_data(
|
|
1067
|
+
rp_weights=state.timeline.rp_weights[ts_name],
|
|
1068
|
+
timeset_duration_entries=state.timeline.timeset_durations[
|
|
1069
|
+
ts_name
|
|
1070
|
+
],
|
|
1071
|
+
period_name=period_name,
|
|
1072
|
+
timeline_steps=timeline_steps,
|
|
1073
|
+
provider=sub_solve_provider,
|
|
1074
|
+
)
|
|
1075
|
+
rp_written = True
|
|
1076
|
+
break
|
|
1077
|
+
if not rp_written:
|
|
1078
|
+
# Phase C — Phase 5's strict ``_assert_blended_weights_have_rp_weights``
|
|
1079
|
+
# check has been retired. Per-solve downgrade fired earlier
|
|
1080
|
+
# in this iter (see ``_downgrade_rp_methods_for_non_rp_solve``
|
|
1081
|
+
# above), so any blended-weights node has already been
|
|
1082
|
+
# rewritten to its non-RP equivalent in this solve's
|
|
1083
|
+
# provider. Emit the empty RP scaffolding so downstream
|
|
1084
|
+
# consumers don't see missing keys.
|
|
1085
|
+
solve_writers.emit_empty_rp_data(
|
|
1086
|
+
provider=sub_solve_provider,
|
|
1087
|
+
)
|
|
1088
|
+
solve_writers.emit_timeset_cost_weight(
|
|
1089
|
+
active_time_list=active_time_lists[solve],
|
|
1090
|
+
timesets_used_by_solve=timesets_used,
|
|
1091
|
+
timeset_weights=state.timeline.timeset_weights,
|
|
1092
|
+
provider=sub_solve_provider,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
state.logger.debug("Starting model creation")
|
|
1096
|
+
|
|
1097
|
+
if timing_recorder is not None:
|
|
1098
|
+
roll_setup_seconds = time.perf_counter() - timer_in_solve
|
|
1099
|
+
timing_recorder.record(
|
|
1100
|
+
"roll_setup",
|
|
1101
|
+
solve=solve,
|
|
1102
|
+
roll_index=i,
|
|
1103
|
+
seconds=roll_setup_seconds,
|
|
1104
|
+
t_start=timer_in_solve,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
# Per-roll diagnostics handles.
|
|
1108
|
+
state.current_scale_solve_name = solve
|
|
1109
|
+
state.current_roll_index = i
|
|
1110
|
+
|
|
1111
|
+
# Per-solve preprocessing chain (still authoritative — the
|
|
1112
|
+
# override hook intercepts already-ported helpers). The prior
|
|
1113
|
+
# SolveHandoff is fanned into ``handoff/<field>`` Provider keys
|
|
1114
|
+
# via :func:`_provider_translators.translate_handoff_to_provider`
|
|
1115
|
+
# so cascade consumers go through ``provider.get(K.HANDOFF_X)``
|
|
1116
|
+
# — no ``prior_handoff`` parameter is threaded through the
|
|
1117
|
+
# cascade (Phase 2 of specs/provider_consolidation.md).
|
|
1118
|
+
prior_handoff = (
|
|
1119
|
+
state.handoffs.get(last_captured_solve)
|
|
1120
|
+
if state.handoffs is not None and last_captured_solve is not None
|
|
1121
|
+
else None
|
|
1122
|
+
)
|
|
1123
|
+
_provider_translators.translate_handoff_to_provider(
|
|
1124
|
+
prior_handoff, sub_solve_provider,
|
|
1125
|
+
)
|
|
1126
|
+
# Phase 4.1e — when nested, parent solve's handoff shadows the
|
|
1127
|
+
# sequential prior in ``handoff/*`` keys: both translator calls
|
|
1128
|
+
# write the same Provider keys, and parent's call lands after
|
|
1129
|
+
# sequential's so parent's values win where both are populated.
|
|
1130
|
+
# Reuses ``_parent_complete_for_carriers`` resolved earlier in
|
|
1131
|
+
# the same iteration scope. Guarded by ``if parent_handoff is
|
|
1132
|
+
# not None`` so non-nested cascades skip the second call
|
|
1133
|
+
# entirely; calling the translator with ``None`` would write
|
|
1134
|
+
# empty frames via its empty-schema fallback, obliterating the
|
|
1135
|
+
# sequential prior's just-written data.
|
|
1136
|
+
parent_handoff = (
|
|
1137
|
+
state.handoffs.get(_parent_complete_for_carriers)
|
|
1138
|
+
if state.handoffs is not None and _parent_complete_for_carriers is not None
|
|
1139
|
+
else None
|
|
1140
|
+
)
|
|
1141
|
+
if parent_handoff is not None:
|
|
1142
|
+
_provider_translators.translate_handoff_to_provider(
|
|
1143
|
+
parent_handoff, sub_solve_provider,
|
|
1144
|
+
)
|
|
1145
|
+
# Phase 5b — external overrides shadow handoff via the
|
|
1146
|
+
# ``override/*`` Provider layer. The callable is set by
|
|
1147
|
+
# external code wrapping the runner (file-watch / ZeroMQ /
|
|
1148
|
+
# etc.); default is None (no overrides). Lands AFTER the
|
|
1149
|
+
# sequential + parent handoff translators so override values
|
|
1150
|
+
# take precedence in ``read_handoff_frame`` consumers
|
|
1151
|
+
# (Phase 5a infrastructure). If the callable raises, the
|
|
1152
|
+
# exception propagates — external code owns clean error
|
|
1153
|
+
# reporting.
|
|
1154
|
+
_override_provider = getattr(state, "override_provider", None)
|
|
1155
|
+
if _override_provider is not None:
|
|
1156
|
+
overrides = _override_provider()
|
|
1157
|
+
if overrides:
|
|
1158
|
+
_provider_translators.translate_overrides_to_provider(
|
|
1159
|
+
overrides, sub_solve_provider,
|
|
1160
|
+
)
|
|
1161
|
+
state.logger.info(
|
|
1162
|
+
f"[override] applied {len(overrides)} keys at "
|
|
1163
|
+
f"iter={i} solve={complete_solve[solve]}"
|
|
1164
|
+
)
|
|
1165
|
+
state.logger.debug(
|
|
1166
|
+
f"[override] keys: {sorted(overrides.keys())}"
|
|
1167
|
+
)
|
|
1168
|
+
_phase_timing = (
|
|
1169
|
+
os.environ.get("FLEXTOOL_PHASE_TIMING") == "1"
|
|
1170
|
+
and timing_recorder is not None
|
|
1171
|
+
)
|
|
1172
|
+
_t_preproc_start = time.perf_counter() if _phase_timing else 0.0
|
|
1173
|
+
if _mem_cp is not None:
|
|
1174
|
+
_mem_cp("prep_before_solve_time_dispatcher",
|
|
1175
|
+
"prep: block + rp/timeset + status + empty writers done")
|
|
1176
|
+
# Step 1-f — preprocessing emits directly into
|
|
1177
|
+
# ``sub_solve_provider`` via the threaded ``provider=`` keyword;
|
|
1178
|
+
# the Provider is the sole in-memory carrier.
|
|
1179
|
+
preprocessing_solve_time.run(
|
|
1180
|
+
state, complete_solve[solve],
|
|
1181
|
+
provider=sub_solve_provider,
|
|
1182
|
+
)
|
|
1183
|
+
if _mem_cp is not None:
|
|
1184
|
+
_mem_cp("prep_solve_time_dispatcher_done",
|
|
1185
|
+
"prep: _emit_solve_time.run (per-solve sets + params dispatcher) done")
|
|
1186
|
+
# Step 1-f — Provider stash on state. Read by
|
|
1187
|
+
# ``_PolarHighCascadeSolver.run`` to thread through into
|
|
1188
|
+
# ``load_flextool`` / ``write_outputs_for_solve`` /
|
|
1189
|
+
# ``build_handoff_from_solution``. Replaces any prior
|
|
1190
|
+
# sub-solve's Provider (per-sub-solve memory discipline).
|
|
1191
|
+
state.current_provider = sub_solve_provider
|
|
1192
|
+
if _phase_timing:
|
|
1193
|
+
timing_recorder.record(
|
|
1194
|
+
"per_iter",
|
|
1195
|
+
subphase="preprocessing",
|
|
1196
|
+
solve=complete_solve[solve],
|
|
1197
|
+
roll_index=i,
|
|
1198
|
+
seconds=time.perf_counter() - _t_preproc_start,
|
|
1199
|
+
t_start=_t_preproc_start,
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
# Phase 6b — opt-in source-tagging audit dump. When
|
|
1203
|
+
# ``FLEXTOOL_AUDIT_SOURCES=1`` is set in the environment, append
|
|
1204
|
+
# every Provider key carrying a non-None source tag to
|
|
1205
|
+
# ``<work_folder>/audit_sources.log``. The override translator
|
|
1206
|
+
# tags its writes with ``source="external_override"`` (Phase 6a);
|
|
1207
|
+
# other writes leave the source slot empty, so the log captures
|
|
1208
|
+
# exactly the externally-injected entries for the just-completed
|
|
1209
|
+
# preprocessing pass. Append mode accumulates across sub-solves.
|
|
1210
|
+
if os.environ.get("FLEXTOOL_AUDIT_SOURCES") == "1":
|
|
1211
|
+
_provider_translators.dump_provider_sources(
|
|
1212
|
+
sub_solve_provider,
|
|
1213
|
+
wf / "audit_sources.log",
|
|
1214
|
+
complete_solve[solve],
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
# Phase 4 (Gap F) — expose the upper-level (nesting) parent's
|
|
1218
|
+
# complete solve name so ``_PolarHighCascadeSolver.run`` can look the
|
|
1219
|
+
# parent's :class:`SolveHandoff` up out of ``state.handoffs`` and
|
|
1220
|
+
# pass it to ``build_handoff_from_solution`` (which uses it to skip
|
|
1221
|
+
# the workdir's ``fix_storage_{price,usage}.csv`` reads). Resets
|
|
1222
|
+
# to None for top-level solves.
|
|
1223
|
+
_parent_solve = parent_roll.get(solve)
|
|
1224
|
+
state.current_parent_complete = (
|
|
1225
|
+
complete_solve.get(_parent_solve) if _parent_solve else None
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
# Tell the solver to emit the four per-iter phase checkpoints
|
|
1229
|
+
# (FlexData built / Matrix built / Solver / Outputs written) on
|
|
1230
|
+
# EVERY iter, so each roll/solve reports its own deltas rather
|
|
1231
|
+
# than aggregating across the whole level group. This makes the
|
|
1232
|
+
# per-roll time/memory breakdown visible between solves (the
|
|
1233
|
+
# ``Solve cleanup`` snapshot above closes each boundary).
|
|
1234
|
+
state.emit_phase_checkpoints_this_iter = True
|
|
1235
|
+
# Phase 2 — expose the current iter's position in the level_keys
|
|
1236
|
+
# sequence so the per-solve callback can decide whether to retain
|
|
1237
|
+
# or drop this level's ``Solution.highs`` / ``flex_data_provider``
|
|
1238
|
+
# after the per-iter writers run.
|
|
1239
|
+
state._current_iter_index = i
|
|
1240
|
+
state._current_level_key = _level_key
|
|
1241
|
+
exit_status = solver.run(complete_solve[solve])
|
|
1242
|
+
state.emit_phase_checkpoints_this_iter = False
|
|
1243
|
+
state.current_parent_complete = None
|
|
1244
|
+
state.current_scale_solve_name = None
|
|
1245
|
+
state.current_roll_index = None
|
|
1246
|
+
|
|
1247
|
+
if exit_status == 0:
|
|
1248
|
+
state.logger.debug("Success!")
|
|
1249
|
+
state.logger.debug(
|
|
1250
|
+
"------------------------------------------------------------"
|
|
1251
|
+
"-------------------------------"
|
|
1252
|
+
)
|
|
1253
|
+
else:
|
|
1254
|
+
message = f"Error: {exit_status}"
|
|
1255
|
+
state.logger.error(message)
|
|
1256
|
+
raise FlexToolSolveError(message)
|
|
1257
|
+
|
|
1258
|
+
# ---- mod-phase timing ingest (legacy diagnostic) ----
|
|
1259
|
+
if timing_recorder is not None:
|
|
1260
|
+
mod_phases_path = wf / "solve_data" / "mod_phases.csv"
|
|
1261
|
+
if mod_phases_path.exists():
|
|
1262
|
+
try:
|
|
1263
|
+
with open(mod_phases_path) as _mp:
|
|
1264
|
+
_reader = csv.DictReader(_mp)
|
|
1265
|
+
for _row in _reader:
|
|
1266
|
+
try:
|
|
1267
|
+
_seconds = float(
|
|
1268
|
+
_row.get("seconds", "") or 0.0
|
|
1269
|
+
)
|
|
1270
|
+
except ValueError:
|
|
1271
|
+
continue
|
|
1272
|
+
_phase_name = (_row.get("phase") or "").strip()
|
|
1273
|
+
if not _phase_name:
|
|
1274
|
+
continue
|
|
1275
|
+
timing_recorder.record(
|
|
1276
|
+
"solve",
|
|
1277
|
+
subphase=f"mod_{_phase_name}",
|
|
1278
|
+
solve=complete_solve[solve],
|
|
1279
|
+
roll_index=i,
|
|
1280
|
+
seconds=_seconds,
|
|
1281
|
+
)
|
|
1282
|
+
except Exception as _exc: # diagnostic only
|
|
1283
|
+
state.logger.debug(
|
|
1284
|
+
f"mod_phases ingest failed for "
|
|
1285
|
+
f"{complete_solve[solve]}: {_exc}"
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
# In-memory handoff bookkeeping. ``solver.run`` deposits a
|
|
1289
|
+
# polar_high-derived ``SolveHandoff`` into ``state.handoffs`` via
|
|
1290
|
+
# ``build_handoff_from_solution``. Phase 3 of
|
|
1291
|
+
# specs/provider_consolidation.md deleted the legacy
|
|
1292
|
+
# ``capture_post_solve`` constructor — nothing reads from disk
|
|
1293
|
+
# to overwrite that handoff.
|
|
1294
|
+
if state.handoffs is not None:
|
|
1295
|
+
last_captured_solve = complete_solve[solve]
|
|
1296
|
+
state.last_captured_solve = last_captured_solve
|
|
1297
|
+
# Phase 4.1a — refresh ``handoff/*`` Provider keys from the
|
|
1298
|
+
# post-solve ``SolveHandoff`` so the current iteration's
|
|
1299
|
+
# Provider exposes the FINAL cumulative state of this roll
|
|
1300
|
+
# (not just the prior-roll state seeded at iteration start).
|
|
1301
|
+
# ``csv_dump`` snapshots the Provider after the last roll;
|
|
1302
|
+
# without this refresh the on-disk
|
|
1303
|
+
# ``handoff/cumulative_commodity.csv`` would reflect the
|
|
1304
|
+
# second-to-last roll instead of the final one. The next
|
|
1305
|
+
# iteration's iteration-start translator is unaffected: it
|
|
1306
|
+
# reads from ``state.handoffs[last_captured_solve]`` directly.
|
|
1307
|
+
_latest_handoff = state.handoffs.get(last_captured_solve)
|
|
1308
|
+
if _latest_handoff is not None:
|
|
1309
|
+
_provider_translators.translate_handoff_to_provider(
|
|
1310
|
+
_latest_handoff, sub_solve_provider,
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
# ---- Scaling report — handled by the orchestrator.
|
|
1314
|
+
# The diagnostic TXT report is emitted by
|
|
1315
|
+
# ``_orchestration._write_scale_csv_and_report`` (called from
|
|
1316
|
+
# the cascade post-solve path at ``_orchestration.py:1352``).
|
|
1317
|
+
# The duplicate native call site was removed in Tier 4
|
|
1318
|
+
# Commit 4 since the orchestrator's call uses the modern
|
|
1319
|
+
# ``flex_data`` / ``Solution`` API and emits the same
|
|
1320
|
+
# ``solve_data/scaling_report.txt`` artifact with identical
|
|
1321
|
+
# base-name deduplication.
|
|
1322
|
+
|
|
1323
|
+
# Phase 4.1l — the post-solve fix_storage carrier refresh that
|
|
1324
|
+
# used to fan the wide ``SolveHandoff.fix_storage`` frame into
|
|
1325
|
+
# per-metric ``solve_data/fix_storage_*`` Provider keys is
|
|
1326
|
+
# retired. The narrow ``handoff/fix_storage_{quantity,price,
|
|
1327
|
+
# usage}`` keys are seeded directly by the iteration-start
|
|
1328
|
+
# parent-handoff translator from the three narrow SolveHandoff
|
|
1329
|
+
# fields; no post-solve fan-out is required.
|
|
1330
|
+
|
|
1331
|
+
# Phase 4.1a — the ladder rolling accumulators
|
|
1332
|
+
# (``cumulative_commodity`` / ``cum_sim_hours``) cross sub-solves
|
|
1333
|
+
# via the iteration-start handoff translator
|
|
1334
|
+
# (``translate_handoff_to_provider``). No fan-out needed here:
|
|
1335
|
+
# the next iteration reads ``handoff/cumulative_commodity`` /
|
|
1336
|
+
# ``handoff/cum_sim_hours`` directly from the typed SolveHandoff.
|
|
1337
|
+
|
|
1338
|
+
if len(state.solve.model_solve) > 1:
|
|
1339
|
+
message = (
|
|
1340
|
+
"Trying to run more than one model - not supported. The results "
|
|
1341
|
+
"of the first model are retained."
|
|
1342
|
+
)
|
|
1343
|
+
state.logger.error(message)
|
|
1344
|
+
raise FlexToolConfigError(message)
|
|
1345
|
+
return 0
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
__all__ = ["native_run_model"]
|