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,858 @@
|
|
|
1
|
+
"""Warm-LP primitives — structural fingerprint, Param classification, and
|
|
2
|
+
WarmProblem update routine for the native cascade in ``_orchestration``.
|
|
3
|
+
|
|
4
|
+
Two consecutive sub-solves of a chain are "warm-compatible" iff they emit
|
|
5
|
+
an LP of identical shape (same set of vars and cstrs by row count and
|
|
6
|
+
dim signature). When that holds AND every changed Param either belongs
|
|
7
|
+
to the clean-mapping set (:data:`_WARM_PARAMS`) or is declared mutable
|
|
8
|
+
on the :class:`polar_high.WarmProblem` (:data:`_MUTABLE_PARAMS`), the
|
|
9
|
+
warm-update routine pushes the deltas into the live HiGHS instance via
|
|
10
|
+
``changeRowsBounds`` / ``changeColsCost`` / per-cell coefficient writes
|
|
11
|
+
instead of cold-rebuilding the model.
|
|
12
|
+
|
|
13
|
+
Consumed by ``_orchestration.py::_drive_cascade`` (native polar_high
|
|
14
|
+
cascade — Δ.12d). Re-exported as module attributes on ``chain.py`` for
|
|
15
|
+
backward-compat with callers like ``test_warm_param_autoupdate``.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
import polars as pl
|
|
22
|
+
|
|
23
|
+
from polar_high import Problem, WarmProblem
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from flextool.engine_polars.input import FlexData
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"_STRUCTURAL_FIELDS",
|
|
31
|
+
"_WARM_PARAMS",
|
|
32
|
+
"_MUTABLE_PARAMS",
|
|
33
|
+
"_WARM_PARAMS_DEFERRED",
|
|
34
|
+
"_WARM_PARAMS_NO_OP",
|
|
35
|
+
"_WARM_PARAM_GATES",
|
|
36
|
+
"_IncompatibleUpdate",
|
|
37
|
+
"_fingerprint",
|
|
38
|
+
"_param_frame_equal",
|
|
39
|
+
"_param_values_position_equal",
|
|
40
|
+
"_gate_active",
|
|
41
|
+
"_apply_warm_updates",
|
|
42
|
+
"_build_warm_problem",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Structural-fingerprint fields.
|
|
48
|
+
#
|
|
49
|
+
# Two consecutive sub-solves are "warm-compatible" iff they emit an LP of
|
|
50
|
+
# identical shape — same set of variables (same dims, same row counts) and
|
|
51
|
+
# same set of constraints (same row counts). In flextool the LP shape is
|
|
52
|
+
# determined by which "set"-typed FlexData fields are populated and how
|
|
53
|
+
# many rows they hold. We capture that with a tuple of (field_name,
|
|
54
|
+
# height) pairs.
|
|
55
|
+
#
|
|
56
|
+
# The list is intentionally NOT exhaustive — only fields that we have
|
|
57
|
+
# evidence affect the LP structure in tested scenarios are listed. When
|
|
58
|
+
# warm=True misclassifies a transition (i.e. the obj diverges from the
|
|
59
|
+
# cold rebuild path), add the offending field here.
|
|
60
|
+
|
|
61
|
+
_STRUCTURAL_FIELDS: tuple[str, ...] = (
|
|
62
|
+
# Time + node sets (the foundation of every LP).
|
|
63
|
+
"dt", "nodeBalance", "nodeBalance_dt",
|
|
64
|
+
# Process topology.
|
|
65
|
+
"process_source_sink", "process_source_sink_eff",
|
|
66
|
+
"process_source_sink_noEff", "pss_dt",
|
|
67
|
+
"flow_to_n", "flow_from_n",
|
|
68
|
+
"flow_from_commodity_eff", "flow_from_commodity_noEff",
|
|
69
|
+
"flow_to_commodity",
|
|
70
|
+
"pd_neg_cap",
|
|
71
|
+
# CO2.
|
|
72
|
+
"flow_from_co2_priced", "flow_from_co2_priced_noEff",
|
|
73
|
+
"group_co2_max_period", "flow_from_co2_capped",
|
|
74
|
+
"flow_from_co2_capped_noEff", "group_d_co2_capped",
|
|
75
|
+
"group_co2_max_total", "flow_from_co2_capped_total",
|
|
76
|
+
"flow_from_co2_capped_total_noEff",
|
|
77
|
+
# Indirect (CHP).
|
|
78
|
+
"process_indirect", "process_input_flows",
|
|
79
|
+
"process_output_flows", "process_indirect_dt",
|
|
80
|
+
# User constraints.
|
|
81
|
+
"flow_constraint_idx", "cdt_eq", "cdt_le", "cdt_ge",
|
|
82
|
+
# Profiles.
|
|
83
|
+
"process_profile_upper", "process_profile_lower",
|
|
84
|
+
"process_profile_fixed",
|
|
85
|
+
# Invest / divest.
|
|
86
|
+
"ed_invest_set", "ed_divest_set",
|
|
87
|
+
"pd_invest_set", "pd_divest_set",
|
|
88
|
+
"nd_invest_set", "nd_divest_set",
|
|
89
|
+
"edd_invest_set", "edd_invest_lookback_set", "edd_divest_active",
|
|
90
|
+
"e_invest_total", "e_divest_total",
|
|
91
|
+
"ed_invest_period_set", "ed_divest_period_set",
|
|
92
|
+
# Ramp.
|
|
93
|
+
"process_source_sink_ramp_limit_sink_up",
|
|
94
|
+
"process_source_sink_ramp_limit_sink_down",
|
|
95
|
+
"process_source_sink_ramp_limit_source_up",
|
|
96
|
+
"process_source_sink_ramp_limit_source_down",
|
|
97
|
+
# Online / UC.
|
|
98
|
+
"process_online", "process_online_linear", "process_online_integer",
|
|
99
|
+
"process_minload", "process_min_load_eff",
|
|
100
|
+
"p_online_dt", "pdt_online_linear", "pdt_online_integer",
|
|
101
|
+
"pdt_uptime_set", "pdt_downtime_set",
|
|
102
|
+
"uptime_lookback", "downtime_lookback",
|
|
103
|
+
# Storage.
|
|
104
|
+
"nodeState", "nodeState_dt", "nodeState_first_dt",
|
|
105
|
+
"storage_bind_within_timeblock", "storage_bind_forward_only",
|
|
106
|
+
"storage_bind_within_solve",
|
|
107
|
+
"storage_bind_within_solve_blended_weights",
|
|
108
|
+
# Phase D — landed with the new variant's constraint wiring.
|
|
109
|
+
"storage_bind_forward_only_blended_weights",
|
|
110
|
+
# Phase E — landed with the per-period cyclic-closure variant.
|
|
111
|
+
"storage_bind_within_period_blended_weights",
|
|
112
|
+
"storage_fix_start",
|
|
113
|
+
"dtttdt", "dtttdt_forward_only",
|
|
114
|
+
"n_fix_storage_quantity", "ndt_fix_storage_quantity",
|
|
115
|
+
"dtt_timeline_matching", "period_branch", "period_last",
|
|
116
|
+
"nodeState_last_dt",
|
|
117
|
+
"nodeStateBlock", "period_block", "period_block_succ",
|
|
118
|
+
"period_block_time", "dtttdt_block_interior",
|
|
119
|
+
# RP-blended-weights (bind_within_solve_blended_weights) sets / params —
|
|
120
|
+
# per-solve constants that don't change across iterations within a
|
|
121
|
+
# level; same carry-over kind as the intraperiod-block family above.
|
|
122
|
+
"nodeState_rp", "rp_base_period_set",
|
|
123
|
+
"rp_base_chain", "rp_base_first", "rp_base_last",
|
|
124
|
+
"rp_block_first", "p_rp_last_step", "rp_base__rep",
|
|
125
|
+
"arc_sink_block_dt", "arc_source_block_dt",
|
|
126
|
+
"flow_from_nodeBalance_eff", "flow_from_nodeBalance_noEff",
|
|
127
|
+
"node_profile_upper", "node_profile_lower", "node_profile_fixed",
|
|
128
|
+
# Variable cost partitions.
|
|
129
|
+
"pssdt_varCost_noEff", "pssdt_varCost_eff_unit_source",
|
|
130
|
+
"pssdt_varCost_eff_unit_sink", "pssdt_varCost_eff_connection",
|
|
131
|
+
# Group slack.
|
|
132
|
+
"groupCapacityMargin", "groupInertia", "groupNonSync",
|
|
133
|
+
"group_node", "process_unit",
|
|
134
|
+
"process_sink_inertia", "process_source_inertia",
|
|
135
|
+
"process_sink_nonSync", "process_group_inside_nonSync",
|
|
136
|
+
# Reserves.
|
|
137
|
+
"reserve_upDown_group",
|
|
138
|
+
"reserve_upDown_group_method_timeseries",
|
|
139
|
+
"reserve_upDown_group_method_dynamic",
|
|
140
|
+
"reserve_upDown_group_method_n_1",
|
|
141
|
+
"prundt", "process_reserve_upDown_node_active",
|
|
142
|
+
"process_reserve_upDown_node_increase_reserve_ratio",
|
|
143
|
+
"process_reserve_upDown_node_large_failure_ratio",
|
|
144
|
+
# Cumulative invest / group invest.
|
|
145
|
+
"ed_invest_forbidden_no_investment", "ed_invest_cumulative",
|
|
146
|
+
"group_entity", "g_invest_total", "g_divest_total",
|
|
147
|
+
"g_invest_cumulative", "gd_invest_period", "gd_divest_period",
|
|
148
|
+
"group_process_node",
|
|
149
|
+
# Delays.
|
|
150
|
+
"process_delayed", "process_delayed__duration",
|
|
151
|
+
"process_source_delayed", "process_source_undelayed",
|
|
152
|
+
"process_source_sink_delayed", "process_source_sink_undelayed",
|
|
153
|
+
"dtt__delay_duration",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Clean-mapping Params for warm updates.
|
|
159
|
+
#
|
|
160
|
+
# Only Params whose contribution to the LP is exactly "RHS of constraint X"
|
|
161
|
+
# OR "objective coefficient of variable Y" via a single-Param algebraic
|
|
162
|
+
# pathway can be warm-updated cleanly. Multi-Param composite expressions
|
|
163
|
+
# (e.g. ``vq_up * p_penalty_up * p_node_capacity_for_scaling * op_factor``)
|
|
164
|
+
# would require the engine to track which Params feed which LP cells —
|
|
165
|
+
# that's WarmProblem's deferred Phase 2.
|
|
166
|
+
#
|
|
167
|
+
# Each entry is (flexdata_field, kind, target, transform, over_field).
|
|
168
|
+
# ``kind`` is either "rhs" (constraint RHS) or "obj" (variable objective
|
|
169
|
+
# coefficient). ``target`` is the constraint or variable name in the
|
|
170
|
+
# built Problem. ``transform`` is None (push the Param as-is) or "neg"
|
|
171
|
+
# (push -Param). ``over_field`` names the FlexData index frame that the
|
|
172
|
+
# constraint was built ``over=`` (used to position-align values when the
|
|
173
|
+
# new sub-solve has different dim labels but same row counts — the
|
|
174
|
+
# rolling-horizon case). None means push the Param's value column
|
|
175
|
+
# directly (used when the constraint axis dim values are stable across
|
|
176
|
+
# rolls, e.g. (n,) for storage-anchor handoff).
|
|
177
|
+
#
|
|
178
|
+
# Adding entries here only widens the set of transitions for which warm
|
|
179
|
+
# update is attempted; if a transition's diff falls entirely inside this
|
|
180
|
+
# set it stays warm, otherwise it falls back to cold rebuild.
|
|
181
|
+
|
|
182
|
+
_WARM_PARAMS: tuple[tuple[str, str, str, str | None, str | None], ...] = (
|
|
183
|
+
("p_inflow", "rhs", "nodeBalance_eq", "neg", "nodeBalance_dt"),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Params that participate in composite LP cells (multi-Param products).
|
|
187
|
+
# When ``run_chain_from_db(..., warm=True)`` is invoked, the WarmProblem is told to
|
|
188
|
+
# track these via :meth:`polar_high.WarmProblem.declare_mutable` so per-cell
|
|
189
|
+
# auto-update can fire on transitions where any of them differs between
|
|
190
|
+
# sub-solves. This widens the warm-compatible regime BEYOND the clean-RHS
|
|
191
|
+
# subset above to cover:
|
|
192
|
+
# * slack penalties scaled by op-factor and capacity_for_scaling,
|
|
193
|
+
# * commodity-price terms multiplied by step_duration / inflation /
|
|
194
|
+
# period_share / cost_weight,
|
|
195
|
+
# * storage-anchor RHS terms ``p_state_start * p_state_existing_capacity``
|
|
196
|
+
# and ``p_roll_continue_state``,
|
|
197
|
+
# * per-(d,t) fix-storage RHS / inflow time-series.
|
|
198
|
+
# The list is kept narrow; additions are cheap (~tens of MB of side-table
|
|
199
|
+
# storage in the worst case) but each new entry has to be vetted against
|
|
200
|
+
# the auto-update math (numerator vs denominator direction recovery).
|
|
201
|
+
_MUTABLE_PARAMS: tuple[str, ...] = (
|
|
202
|
+
"p_inflow",
|
|
203
|
+
"p_penalty_up", "p_penalty_down",
|
|
204
|
+
"p_state_start", "p_roll_continue_state",
|
|
205
|
+
"p_fix_storage_quantity",
|
|
206
|
+
"p_commodity_price",
|
|
207
|
+
"p_step_duration", "p_timestep_weight",
|
|
208
|
+
"p_inflation_op", "p_period_share",
|
|
209
|
+
"p_node_capacity_for_scaling",
|
|
210
|
+
"p_state_existing_capacity",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Param fields that we know change between sub-solves but cannot warm-update
|
|
214
|
+
# (touched by composite expressions, multi-constraint patterns, or matrix
|
|
215
|
+
# coefficients that would require :meth:`WarmProblem.update_coef`). When
|
|
216
|
+
# any of these differ between consecutive sub-solves and the structural
|
|
217
|
+
# fingerprint matches, we still cold-rebuild — they're listed here purely
|
|
218
|
+
# for clarity / future Phase-2 work.
|
|
219
|
+
#
|
|
220
|
+
# D1 audit (2026-05-03; see ``audit/handoff_full_parity_gaps.md`` §D1)
|
|
221
|
+
# categorised every entry on the
|
|
222
|
+
# ``work_multi_fullYear_battery_nested_multi_invest`` 80-roll chain:
|
|
223
|
+
#
|
|
224
|
+
# * **No-op-on-tested-fixtures** entries (``presence_count == 0`` AND
|
|
225
|
+
# ``diff_count == 0`` across every observed transition) are split
|
|
226
|
+
# into :data:`_WARM_PARAMS_NO_OP` for documentation — they remain
|
|
227
|
+
# here too so a regression on a fixture that DOES populate them is
|
|
228
|
+
# still caught.
|
|
229
|
+
# * **Gated-by-dormant-feature** entries are listed in
|
|
230
|
+
# :data:`_WARM_PARAM_GATES`; on transitions where every gate field
|
|
231
|
+
# is None the diff is "phantom" (the consuming constraint family
|
|
232
|
+
# was never emitted) and we short-circuit the cold-rebuild fallback.
|
|
233
|
+
# * **Sum-collapse RHS-side** entries (Params reaching the LP only via
|
|
234
|
+
# constraint RHS as composite anonymous Params — e.g.
|
|
235
|
+
# ``p_profile_value`` going through ``p_profile_value
|
|
236
|
+
# · p_process_existing_count [· p_process_availability]`` into the
|
|
237
|
+
# RHS of ``profile_flow_*``, or ``p_roll_continue_state`` rebuilt
|
|
238
|
+
# into a sparse ``(n, d, t)`` Param dropped into nodeBalance LHS
|
|
239
|
+
# constants) remain genuine cold-rebuild triggers. Hand-coded
|
|
240
|
+
# warm-update exception paths for these are blocked on engine-side
|
|
241
|
+
# RHS source-tracking AND on rolling-horizon t-label-shift handling
|
|
242
|
+
# for ``nodeState_first_dt`` / ``dtt_timeline_matching`` — both
|
|
243
|
+
# explicitly out of scope for D1 (engine refactor; see follow-ups
|
|
244
|
+
# in ``audit/handoff_param_tracked_autoupdate.md``).
|
|
245
|
+
_WARM_PARAMS_DEFERRED: tuple[str, ...] = (
|
|
246
|
+
# Slack-penalty composites: vq * p_penalty_* * op_factor * scaling.
|
|
247
|
+
"p_penalty_up", "p_penalty_down",
|
|
248
|
+
# Time-weight composites that touch every (d,t)-keyed obj/lhs term.
|
|
249
|
+
"p_step_duration", "p_inflation_op",
|
|
250
|
+
"p_period_share", "p_timestep_weight",
|
|
251
|
+
# Storage handoff / anchor — multi-cstr.
|
|
252
|
+
"p_state_start", "p_roll_continue_state",
|
|
253
|
+
"p_fix_storage_quantity", "p_state_existing_capacity",
|
|
254
|
+
"p_state_unitsize", "p_state_self_discharge", "p_state_upper",
|
|
255
|
+
"p_storage_state_reference_price",
|
|
256
|
+
# Invest handoff — RHS of multiple invest/divest cstrs.
|
|
257
|
+
"p_entity_previously_invested_capacity",
|
|
258
|
+
"p_entity_invested", "p_entity_divested",
|
|
259
|
+
"p_entity_max_units", "p_entity_all_existing",
|
|
260
|
+
"ed_lifetime_fixed_cost", "ed_lifetime_fixed_cost_divest",
|
|
261
|
+
"ed_entity_annual_discounted", "ed_entity_annual_divest_discounted",
|
|
262
|
+
"e_invest_max_total", "e_divest_max_total",
|
|
263
|
+
"ed_invest_max_period", "ed_divest_max_period",
|
|
264
|
+
# Commodity / CO2 — composite obj.
|
|
265
|
+
"p_commodity_price", "p_co2_price", "p_co2_max_period",
|
|
266
|
+
"p_co2_max_total", "p_co2_content",
|
|
267
|
+
# Commodity ladder (cumulative + annual) — RHS / cap terms of
|
|
268
|
+
# ``ladder_tier_cap_cumulative_roll`` / ``ladder_tier_cap_annual_roll``.
|
|
269
|
+
# The rolling cumulative accumulator (``p_ladder_cum_realized_mwh``)
|
|
270
|
+
# AND the per-roll period-fill fraction (``p_f_d_k``) BOTH change
|
|
271
|
+
# between rolls of a rolling-window solve — they encode the cross-
|
|
272
|
+
# solve carry of realised MWh and the current roll's share of each
|
|
273
|
+
# period, respectively. Without listing them here, ``_apply_warm_updates``
|
|
274
|
+
# silently kept roll N+1's LP at roll N's RHS, so the tier-cap
|
|
275
|
+
# constraint never tightened and roll N+1's v_trade collapsed to
|
|
276
|
+
# roll N's solution. Symptom: tests/test_commodity_ladder_rolling.py
|
|
277
|
+
# ``TestWithinPeriodCumulativeRolling::test_within_period_cumulative_completes``
|
|
278
|
+
# (only p2020 in the final accumulator) and
|
|
279
|
+
# ``TestCumulativeLadderBindingCap::test_roll2_uses_tier2_only_after_roll1_saturates_cap``
|
|
280
|
+
# (roll-2 v_trade zero across all tiers). Listing them as deferred
|
|
281
|
+
# forces a cold rebuild on any per-roll diff — the ladder constraints
|
|
282
|
+
# touch composite LP cells with no warm-update side-table entry, so
|
|
283
|
+
# cold rebuild is the only correct option here.
|
|
284
|
+
"p_ladder_cum_realized_mwh", "p_f_d_k",
|
|
285
|
+
"p_ladder_cum_price", "p_ladder_cum_quantity",
|
|
286
|
+
"p_ladder_ann_price", "p_ladder_ann_quantity",
|
|
287
|
+
# Process topology Params used in many cstrs / objs.
|
|
288
|
+
"p_unitsize", "p_flow_upper", "p_flow_upper_existing",
|
|
289
|
+
"p_slope", "p_process_existing_count", "p_process_availability",
|
|
290
|
+
"p_node_availability",
|
|
291
|
+
# Profile Params — drive process_profile_* cstrs.
|
|
292
|
+
"p_profile_value",
|
|
293
|
+
# User constraints.
|
|
294
|
+
"p_flow_constraint_coef", "p_constraint_constant",
|
|
295
|
+
"p_node_constraint_invested_capacity_coeff",
|
|
296
|
+
"p_process_constraint_invested_capacity_coeff",
|
|
297
|
+
"p_node_constraint_state_coeff",
|
|
298
|
+
"p_node_constraint_prebuilt_capacity_coeff",
|
|
299
|
+
"p_process_constraint_prebuilt_capacity_coeff",
|
|
300
|
+
# Variable cost partitions.
|
|
301
|
+
"p_pssdt_varCost", "p_pdt_varCost_source",
|
|
302
|
+
"p_pdt_varCost_sink", "p_pdt_varCost_process",
|
|
303
|
+
# Online / UC.
|
|
304
|
+
"p_startup_cost", "p_section", "p_min_load",
|
|
305
|
+
# Ramp speeds.
|
|
306
|
+
"p_ramp_speed_up_sink", "p_ramp_speed_down_sink",
|
|
307
|
+
"p_ramp_speed_up_source", "p_ramp_speed_down_source",
|
|
308
|
+
# Capacity scaling.
|
|
309
|
+
"p_node_capacity_for_scaling", "p_group_capacity_for_scaling",
|
|
310
|
+
# Inflow (split into positive / negative for slack scaling — feeds
|
|
311
|
+
# nodeBalance terms beyond just RHS).
|
|
312
|
+
"p_positive_inflow", "p_negative_inflow", "pdtNodeInflow_per_step",
|
|
313
|
+
# Existing-fixed cost on entities.
|
|
314
|
+
"p_ed_fixed_cost",
|
|
315
|
+
# Group reserves / capacity-margin / inertia.
|
|
316
|
+
"pdGroup_capacity_margin", "pdGroup_penalty_capacity_margin",
|
|
317
|
+
"pdGroup_inertia_limit", "pdGroup_penalty_inertia",
|
|
318
|
+
"pdGroup_non_synchronous_limit", "pdGroup_penalty_non_synchronous",
|
|
319
|
+
"p_inv_group_cap",
|
|
320
|
+
"p_process_sink_inertia_constant", "p_process_source_inertia_constant",
|
|
321
|
+
# Reserves.
|
|
322
|
+
"pdtReserve_upDown_group_reservation",
|
|
323
|
+
"p_reserve_upDown_group_penalty_reserve",
|
|
324
|
+
"p_process_reserve_upDown_node_reliability",
|
|
325
|
+
"p_process_reserve_upDown_node_max_share",
|
|
326
|
+
"p_process_reserve_upDown_node_large_failure_ratio_value",
|
|
327
|
+
"p_process_reserve_upDown_node_increase_reserve_ratio_value",
|
|
328
|
+
# Cumulative invest / group invest Params.
|
|
329
|
+
"ed_invest_min_period", "ed_divest_min_period",
|
|
330
|
+
"e_invest_min_total", "e_divest_min_total",
|
|
331
|
+
"ed_cumulative_max_capacity", "ed_cumulative_min_capacity",
|
|
332
|
+
"p_group_invest_max_period", "p_group_invest_min_period",
|
|
333
|
+
"p_group_retire_max_period", "p_group_retire_min_period",
|
|
334
|
+
"p_group_invest_max_total", "p_group_invest_min_total",
|
|
335
|
+
"p_group_retire_max_total", "p_group_retire_min_total",
|
|
336
|
+
"p_group_invest_max_cumulative", "p_group_invest_min_cumulative",
|
|
337
|
+
"p_group_max_cumulative_flow", "p_group_min_cumulative_flow",
|
|
338
|
+
"pd_max_cumulative_flow", "pd_min_cumulative_flow",
|
|
339
|
+
"pdt_max_instant_flow", "pdt_min_instant_flow",
|
|
340
|
+
# Block / per-arc step durations.
|
|
341
|
+
"p_arc_step_duration_sink", "p_arc_step_duration_source",
|
|
342
|
+
"p_arc_sink_weight", "p_arc_source_weight",
|
|
343
|
+
# Delays.
|
|
344
|
+
"p_process_delay_weight",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# Subset of :data:`_WARM_PARAMS_DEFERRED` that the D1 audit observed to
|
|
349
|
+
# never differ in any tested chain transition (presence_count == 0 OR
|
|
350
|
+
# diff_count == 0 across every observed transition). Kept as a tuple
|
|
351
|
+
# rather than removed so a future fixture that DOES populate one of
|
|
352
|
+
# these still gets caught by :data:`_WARM_PARAMS_DEFERRED`'s diff scan.
|
|
353
|
+
# This list is documentation only — :func:`_apply_warm_updates` does
|
|
354
|
+
# not consult it.
|
|
355
|
+
_WARM_PARAMS_NO_OP: tuple[str, ...] = (
|
|
356
|
+
# Reserves — none of the in-tree fixtures exercise reserve scenarios.
|
|
357
|
+
"pdtReserve_upDown_group_reservation",
|
|
358
|
+
"p_reserve_upDown_group_penalty_reserve",
|
|
359
|
+
"p_process_reserve_upDown_node_reliability",
|
|
360
|
+
"p_process_reserve_upDown_node_max_share",
|
|
361
|
+
"p_process_reserve_upDown_node_large_failure_ratio_value",
|
|
362
|
+
"p_process_reserve_upDown_node_increase_reserve_ratio_value",
|
|
363
|
+
# Cumulative invest / group invest — not active on the multi-invest
|
|
364
|
+
# nested fixture.
|
|
365
|
+
"ed_invest_min_period", "ed_divest_min_period",
|
|
366
|
+
"e_invest_min_total", "e_divest_min_total",
|
|
367
|
+
"ed_cumulative_max_capacity", "ed_cumulative_min_capacity",
|
|
368
|
+
"p_group_invest_max_period", "p_group_invest_min_period",
|
|
369
|
+
"p_group_retire_max_period", "p_group_retire_min_period",
|
|
370
|
+
"p_group_invest_max_total", "p_group_invest_min_total",
|
|
371
|
+
"p_group_retire_max_total", "p_group_retire_min_total",
|
|
372
|
+
"p_group_invest_max_cumulative", "p_group_invest_min_cumulative",
|
|
373
|
+
"p_group_max_cumulative_flow", "p_group_min_cumulative_flow",
|
|
374
|
+
"pd_max_cumulative_flow", "pd_min_cumulative_flow",
|
|
375
|
+
"pdt_max_instant_flow", "pdt_min_instant_flow",
|
|
376
|
+
# Block / per-arc step durations — only used in multi-block fixtures.
|
|
377
|
+
"p_arc_step_duration_sink", "p_arc_step_duration_source",
|
|
378
|
+
"p_arc_sink_weight", "p_arc_source_weight",
|
|
379
|
+
# Delays.
|
|
380
|
+
"p_process_delay_weight",
|
|
381
|
+
# Variable-cost partitions — none of the tested fixtures exercise
|
|
382
|
+
# priced flows yet.
|
|
383
|
+
"p_pssdt_varCost", "p_pdt_varCost_source",
|
|
384
|
+
"p_pdt_varCost_sink", "p_pdt_varCost_process",
|
|
385
|
+
# CO2 (not active on this fixture).
|
|
386
|
+
"p_co2_price", "p_co2_max_period", "p_co2_max_total", "p_co2_content",
|
|
387
|
+
# Online / UC.
|
|
388
|
+
"p_startup_cost", "p_section", "p_min_load",
|
|
389
|
+
# Ramp speeds.
|
|
390
|
+
"p_ramp_speed_up_sink", "p_ramp_speed_down_sink",
|
|
391
|
+
"p_ramp_speed_up_source", "p_ramp_speed_down_source",
|
|
392
|
+
# Per-process inertia constants.
|
|
393
|
+
"p_process_sink_inertia_constant", "p_process_source_inertia_constant",
|
|
394
|
+
# Divest siblings (only present when divest is active).
|
|
395
|
+
"p_entity_invested", "p_entity_divested",
|
|
396
|
+
"ed_lifetime_fixed_cost_divest", "ed_entity_annual_divest_discounted",
|
|
397
|
+
"e_divest_max_total", "ed_divest_max_period",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Per-Param "structural gates": tuples of FlexData field names whose
|
|
402
|
+
# non-None state determines whether the Param can possibly contribute to
|
|
403
|
+
# any LP cell on the new sub-solve. When ALL gates are None on
|
|
404
|
+
# ``nxt`` (and, by fingerprint match, also on ``prior``), the consuming
|
|
405
|
+
# constraint family was never emitted and the Param's diff is a phantom
|
|
406
|
+
# — :func:`_apply_warm_updates` skips the cold-rebuild check.
|
|
407
|
+
#
|
|
408
|
+
# Conservative by design: only Params whose consuming pathways are
|
|
409
|
+
# fully gated by tracked structural fields appear here. Params with
|
|
410
|
+
# unconditional consumers (e.g. ``p_inflow`` always reaches
|
|
411
|
+
# ``nodeBalance_eq``) are absent and treated as always-active.
|
|
412
|
+
_WARM_PARAM_GATES: dict[str, tuple[str, ...]] = {
|
|
413
|
+
# Group-slack inflow consumers (capacityMargin / inertia /
|
|
414
|
+
# non_sync_constraint).
|
|
415
|
+
"p_positive_inflow": ("groupNonSync",),
|
|
416
|
+
"p_negative_inflow": ("groupNonSync",),
|
|
417
|
+
"pdtNodeInflow_per_step": ("groupCapacityMargin",),
|
|
418
|
+
"pdGroup_capacity_margin": ("groupCapacityMargin",),
|
|
419
|
+
"pdGroup_penalty_capacity_margin": ("groupCapacityMargin",),
|
|
420
|
+
"pdGroup_inertia_limit": ("groupInertia",),
|
|
421
|
+
"pdGroup_penalty_inertia": ("groupInertia",),
|
|
422
|
+
"pdGroup_non_synchronous_limit": ("groupNonSync",),
|
|
423
|
+
"pdGroup_penalty_non_synchronous": ("groupNonSync",),
|
|
424
|
+
"p_inv_group_cap": ("groupCapacityMargin", "groupInertia",
|
|
425
|
+
"groupNonSync"),
|
|
426
|
+
"p_process_sink_inertia_constant": ("groupInertia",),
|
|
427
|
+
"p_process_source_inertia_constant": ("groupInertia",),
|
|
428
|
+
"p_group_capacity_for_scaling": ("groupCapacityMargin", "groupInertia",
|
|
429
|
+
"groupNonSync"),
|
|
430
|
+
# Reserves.
|
|
431
|
+
"pdtReserve_upDown_group_reservation": ("reserve_upDown_group",),
|
|
432
|
+
"p_reserve_upDown_group_penalty_reserve": ("reserve_upDown_group",),
|
|
433
|
+
"p_process_reserve_upDown_node_reliability": ("prundt",),
|
|
434
|
+
"p_process_reserve_upDown_node_max_share": ("prundt",),
|
|
435
|
+
"p_process_reserve_upDown_node_large_failure_ratio_value":
|
|
436
|
+
("process_reserve_upDown_node_large_failure_ratio",),
|
|
437
|
+
"p_process_reserve_upDown_node_increase_reserve_ratio_value":
|
|
438
|
+
("process_reserve_upDown_node_increase_reserve_ratio",),
|
|
439
|
+
# CO2.
|
|
440
|
+
"p_co2_price": ("flow_from_co2_priced",),
|
|
441
|
+
"p_co2_max_period": ("group_co2_max_period",),
|
|
442
|
+
"p_co2_max_total": ("group_co2_max_total",),
|
|
443
|
+
"p_co2_content": ("flow_from_co2_priced", "flow_from_co2_capped",
|
|
444
|
+
"group_co2_max_period",
|
|
445
|
+
"flow_from_co2_capped_total",
|
|
446
|
+
"group_co2_max_total"),
|
|
447
|
+
# Online / UC — only emitted when online sets are populated.
|
|
448
|
+
"p_startup_cost": ("process_online",),
|
|
449
|
+
"p_section": ("process_min_load_eff",),
|
|
450
|
+
"p_min_load": ("process_minload",),
|
|
451
|
+
# Ramps.
|
|
452
|
+
"p_ramp_speed_up_sink": ("process_source_sink_ramp_limit_sink_up",),
|
|
453
|
+
"p_ramp_speed_down_sink": ("process_source_sink_ramp_limit_sink_down",),
|
|
454
|
+
"p_ramp_speed_up_source": ("process_source_sink_ramp_limit_source_up",),
|
|
455
|
+
"p_ramp_speed_down_source":
|
|
456
|
+
("process_source_sink_ramp_limit_source_down",),
|
|
457
|
+
# Cumulative invest / group invest — gated by their respective sets.
|
|
458
|
+
"ed_invest_min_period": ("ed_invest_period_set",),
|
|
459
|
+
"ed_divest_min_period": ("ed_divest_period_set",),
|
|
460
|
+
"e_invest_min_total": ("e_invest_total",),
|
|
461
|
+
"e_divest_min_total": ("e_divest_total",),
|
|
462
|
+
"ed_cumulative_max_capacity": ("ed_invest_cumulative",),
|
|
463
|
+
"ed_cumulative_min_capacity": ("ed_invest_cumulative",),
|
|
464
|
+
"p_group_invest_max_period": ("gd_invest_period",),
|
|
465
|
+
"p_group_invest_min_period": ("gd_invest_period",),
|
|
466
|
+
"p_group_retire_max_period": ("gd_divest_period",),
|
|
467
|
+
"p_group_retire_min_period": ("gd_divest_period",),
|
|
468
|
+
"p_group_invest_max_total": ("g_invest_total",),
|
|
469
|
+
"p_group_invest_min_total": ("g_invest_total",),
|
|
470
|
+
"p_group_retire_max_total": ("g_divest_total",),
|
|
471
|
+
"p_group_retire_min_total": ("g_divest_total",),
|
|
472
|
+
"p_group_invest_max_cumulative": ("g_invest_cumulative",),
|
|
473
|
+
"p_group_invest_min_cumulative": ("g_invest_cumulative",),
|
|
474
|
+
"p_group_max_cumulative_flow": ("group_process_node",),
|
|
475
|
+
"p_group_min_cumulative_flow": ("group_process_node",),
|
|
476
|
+
"pd_max_cumulative_flow": ("group_process_node",),
|
|
477
|
+
"pd_min_cumulative_flow": ("group_process_node",),
|
|
478
|
+
"pdt_max_instant_flow": ("group_process_node",),
|
|
479
|
+
"pdt_min_instant_flow": ("group_process_node",),
|
|
480
|
+
# Variable-cost partitions.
|
|
481
|
+
"p_pssdt_varCost": ("pssdt_varCost_noEff",
|
|
482
|
+
"pssdt_varCost_eff_unit_source",
|
|
483
|
+
"pssdt_varCost_eff_unit_sink",
|
|
484
|
+
"pssdt_varCost_eff_connection"),
|
|
485
|
+
"p_pdt_varCost_source": ("pssdt_varCost_eff_unit_source",
|
|
486
|
+
"pssdt_varCost_eff_connection"),
|
|
487
|
+
"p_pdt_varCost_sink": ("pssdt_varCost_eff_unit_sink",
|
|
488
|
+
"pssdt_varCost_eff_connection"),
|
|
489
|
+
"p_pdt_varCost_process": ("pssdt_varCost_noEff",),
|
|
490
|
+
# Block / per-arc step durations.
|
|
491
|
+
"p_arc_step_duration_sink": ("arc_sink_block_dt",),
|
|
492
|
+
"p_arc_step_duration_source": ("arc_source_block_dt",),
|
|
493
|
+
"p_arc_sink_weight": ("arc_sink_block_dt",),
|
|
494
|
+
"p_arc_source_weight": ("arc_source_block_dt",),
|
|
495
|
+
# Delays.
|
|
496
|
+
"p_process_delay_weight": ("process_delayed",),
|
|
497
|
+
# User constraints.
|
|
498
|
+
"p_flow_constraint_coef": ("flow_constraint_idx",),
|
|
499
|
+
"p_constraint_constant": ("cdt_eq", "cdt_le", "cdt_ge"),
|
|
500
|
+
"p_node_constraint_invested_capacity_coeff":
|
|
501
|
+
("flow_constraint_idx", "cdt_eq", "cdt_le", "cdt_ge"),
|
|
502
|
+
"p_process_constraint_invested_capacity_coeff":
|
|
503
|
+
("flow_constraint_idx", "cdt_eq", "cdt_le", "cdt_ge"),
|
|
504
|
+
"p_node_constraint_state_coeff":
|
|
505
|
+
("flow_constraint_idx", "cdt_eq", "cdt_le", "cdt_ge"),
|
|
506
|
+
"p_node_constraint_prebuilt_capacity_coeff":
|
|
507
|
+
("flow_constraint_idx", "cdt_eq", "cdt_le", "cdt_ge"),
|
|
508
|
+
"p_process_constraint_prebuilt_capacity_coeff":
|
|
509
|
+
("flow_constraint_idx", "cdt_eq", "cdt_le", "cdt_ge"),
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class _IncompatibleUpdate(Exception):
|
|
514
|
+
"""Raised by :func:`_apply_warm_updates` when the difference between
|
|
515
|
+
two consecutive sub-solves' FlexData includes Params outside the
|
|
516
|
+
clean-mapping set, forcing a cold rebuild for the next sub-solve."""
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _fingerprint(data: "FlexData") -> tuple:
|
|
520
|
+
"""Compute a structural fingerprint of a FlexData snapshot.
|
|
521
|
+
|
|
522
|
+
Returns a tuple of ``(field_name, height_or_None)`` pairs covering
|
|
523
|
+
every field listed in :data:`_STRUCTURAL_FIELDS`. Two FlexData
|
|
524
|
+
snapshots with equal fingerprints emit identically-shaped LPs (set
|
|
525
|
+
of vars and cstrs match by row count and dim signature) under the
|
|
526
|
+
current ``build_flextool`` rules.
|
|
527
|
+
|
|
528
|
+
``height_or_None`` is ``None`` when the field is unset and the
|
|
529
|
+
integer ``height`` when it's a polars DataFrame. Boolean / scalar
|
|
530
|
+
fields contribute their value directly.
|
|
531
|
+
"""
|
|
532
|
+
out = []
|
|
533
|
+
for name in _STRUCTURAL_FIELDS:
|
|
534
|
+
v = getattr(data, name, None)
|
|
535
|
+
if v is None:
|
|
536
|
+
out.append((name, None))
|
|
537
|
+
elif isinstance(v, pl.DataFrame):
|
|
538
|
+
out.append((name, int(v.height)))
|
|
539
|
+
elif isinstance(v, bool):
|
|
540
|
+
out.append((name, bool(v)))
|
|
541
|
+
else:
|
|
542
|
+
# Unexpected — be conservative and force-mismatch.
|
|
543
|
+
out.append((name, repr(type(v).__name__)))
|
|
544
|
+
# ``p_nested_solve_first`` is a tri-state flag that swaps a whole
|
|
545
|
+
# constraint family in/out — track it explicitly.
|
|
546
|
+
out.append(("p_nested_solve_first",
|
|
547
|
+
getattr(data, "p_nested_solve_first", None)))
|
|
548
|
+
return tuple(out)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _param_frame_equal(a, b) -> bool:
|
|
552
|
+
"""Return True if two polar_high Params have identical frames.
|
|
553
|
+
|
|
554
|
+
Compares dim signature and value column row-by-row. Robust to
|
|
555
|
+
polars row-order differences via a sort on the dim columns.
|
|
556
|
+
"""
|
|
557
|
+
if a is None and b is None:
|
|
558
|
+
return True
|
|
559
|
+
if (a is None) != (b is None):
|
|
560
|
+
return False
|
|
561
|
+
if a.dims != b.dims:
|
|
562
|
+
return False
|
|
563
|
+
af = a.frame
|
|
564
|
+
bf = b.frame
|
|
565
|
+
if af.height != bf.height:
|
|
566
|
+
return False
|
|
567
|
+
if af.height == 0:
|
|
568
|
+
return True
|
|
569
|
+
if a.dims:
|
|
570
|
+
cols = list(a.dims)
|
|
571
|
+
af = af.sort(cols)
|
|
572
|
+
bf = bf.sort(cols)
|
|
573
|
+
return af.equals(bf)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _param_values_position_equal(a, b) -> bool:
|
|
577
|
+
"""Return True if two Params have value columns that are equal at
|
|
578
|
+
matching positions (after sorting each by its full set of dim
|
|
579
|
+
columns).
|
|
580
|
+
|
|
581
|
+
Captures the rolling-horizon case where dim labels (e.g. ``t``)
|
|
582
|
+
shift between sub-solves but the per-position values (e.g. constant
|
|
583
|
+
``3000.0`` slack penalties for every (n,d,t)) stay identical.
|
|
584
|
+
|
|
585
|
+
Returns ``False`` if dim signatures differ or row counts differ.
|
|
586
|
+
Returns ``True`` if both Params are ``None``. Otherwise compares
|
|
587
|
+
the sorted-by-dims value column element-wise within float64
|
|
588
|
+
precision.
|
|
589
|
+
"""
|
|
590
|
+
if a is None and b is None:
|
|
591
|
+
return True
|
|
592
|
+
if (a is None) != (b is None):
|
|
593
|
+
return False
|
|
594
|
+
if a.dims != b.dims:
|
|
595
|
+
return False
|
|
596
|
+
af = a.frame
|
|
597
|
+
bf = b.frame
|
|
598
|
+
if af.height != bf.height:
|
|
599
|
+
return False
|
|
600
|
+
if af.height == 0:
|
|
601
|
+
return True
|
|
602
|
+
if a.dims:
|
|
603
|
+
# Sort each frame by its dim columns positionally — the t-labels
|
|
604
|
+
# in `a` and `b` differ for rolling-horizon snapshots, so we
|
|
605
|
+
# can't just compare frames as-is. Sorting brings the value
|
|
606
|
+
# columns into 1-to-1 positional correspondence assuming the
|
|
607
|
+
# sort orders agree (which they do when both sub-solves have
|
|
608
|
+
# the same number of dim-tuples). This is a fast O(n log n)
|
|
609
|
+
# numeric check rather than a full frame equality.
|
|
610
|
+
cols = list(a.dims)
|
|
611
|
+
av = af.sort(cols)["value"].to_numpy()
|
|
612
|
+
bv = bf.sort(cols)["value"].to_numpy()
|
|
613
|
+
else:
|
|
614
|
+
av = af["value"].to_numpy()
|
|
615
|
+
bv = bf["value"].to_numpy()
|
|
616
|
+
import numpy as np
|
|
617
|
+
return bool(np.array_equal(av, bv))
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _gate_active(d: "FlexData", fld: str) -> bool:
|
|
621
|
+
"""Return True if Param ``fld`` can possibly contribute to an LP
|
|
622
|
+
cell on FlexData ``d`` based on its consuming-feature gates.
|
|
623
|
+
|
|
624
|
+
Defaults to True (assume active) for Params absent from
|
|
625
|
+
:data:`_WARM_PARAM_GATES` — gating is opt-in and conservative.
|
|
626
|
+
Returns False ONLY when every gate field is None or an empty
|
|
627
|
+
polars frame on ``d``; that means the consuming constraint family
|
|
628
|
+
was never emitted, so the Param is dormant on this LP and a diff
|
|
629
|
+
in its values can be safely ignored.
|
|
630
|
+
"""
|
|
631
|
+
gates = _WARM_PARAM_GATES.get(fld)
|
|
632
|
+
if not gates:
|
|
633
|
+
return True
|
|
634
|
+
for g in gates:
|
|
635
|
+
v = getattr(d, g, None)
|
|
636
|
+
if v is None:
|
|
637
|
+
continue
|
|
638
|
+
# Empty polars frame counts as "gate inactive" — the constraint
|
|
639
|
+
# iterator yields zero rows.
|
|
640
|
+
try:
|
|
641
|
+
if hasattr(v, "height") and v.height == 0:
|
|
642
|
+
continue
|
|
643
|
+
except Exception:
|
|
644
|
+
pass
|
|
645
|
+
return True
|
|
646
|
+
return False
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _apply_warm_updates(warm: WarmProblem,
|
|
650
|
+
prior: "FlexData", nxt: "FlexData") -> int:
|
|
651
|
+
"""Push every changed clean-mapping Param from ``prior`` → ``nxt``
|
|
652
|
+
into ``warm``.
|
|
653
|
+
|
|
654
|
+
Returns the count of warm-update calls executed.
|
|
655
|
+
|
|
656
|
+
Raises :class:`_IncompatibleUpdate` if any Param in
|
|
657
|
+
:data:`_WARM_PARAMS_DEFERRED` differs between ``prior`` and ``nxt``
|
|
658
|
+
AND that Param is NOT in :data:`_MUTABLE_PARAMS`. Mutable Params
|
|
659
|
+
are auto-updated via :meth:`polar_high.WarmProblem.update_param`.
|
|
660
|
+
Phantom diffs (Params whose consuming feature is dormant per
|
|
661
|
+
:data:`_WARM_PARAM_GATES`) are skipped — those are the audit-proven
|
|
662
|
+
no-effect cases on this LP. Mutable Params with zero tracked cells
|
|
663
|
+
(Sum-collapse on the build-side composite Param construction) also
|
|
664
|
+
raise :class:`_IncompatibleUpdate` rather than silently no-op'ing
|
|
665
|
+
via ``update_param`` — the silent-corruption guard.
|
|
666
|
+
"""
|
|
667
|
+
# First, scan the deferred (force-cold) list — any difference there
|
|
668
|
+
# is a hard "cold rebuild" signal UNLESS the field is in
|
|
669
|
+
# _MUTABLE_PARAMS, in which case auto-update will handle it below.
|
|
670
|
+
mutable_set = set(_MUTABLE_PARAMS)
|
|
671
|
+
deferred_diffs: dict[str, "object"] = {}
|
|
672
|
+
for fld in _WARM_PARAMS_DEFERRED:
|
|
673
|
+
prior_p = getattr(prior, fld, None)
|
|
674
|
+
next_p = getattr(nxt, fld, None)
|
|
675
|
+
if not _param_values_position_equal(prior_p, next_p):
|
|
676
|
+
if fld in mutable_set:
|
|
677
|
+
deferred_diffs[fld] = next_p
|
|
678
|
+
continue
|
|
679
|
+
if not _gate_active(nxt, fld):
|
|
680
|
+
# Phantom diff — the consuming feature is dormant on
|
|
681
|
+
# this sub-solve (e.g. p_negative_inflow when
|
|
682
|
+
# groupNonSync is None), so the Param can't reach any
|
|
683
|
+
# LP cell. Safe to skip; ignoring this diff is
|
|
684
|
+
# equivalent to cold-rebuilding and re-evaluating an
|
|
685
|
+
# unused Param.
|
|
686
|
+
continue
|
|
687
|
+
raise _IncompatibleUpdate(
|
|
688
|
+
f"Param {fld!r} differs between sub-solves and is not in "
|
|
689
|
+
f"the clean-mapping set — falling back to cold rebuild.")
|
|
690
|
+
|
|
691
|
+
import numpy as np
|
|
692
|
+
|
|
693
|
+
n_updates = 0
|
|
694
|
+
for fld, kind, target, transform, over_field in _WARM_PARAMS:
|
|
695
|
+
prior_p = getattr(prior, fld, None)
|
|
696
|
+
next_p = getattr(nxt, fld, None)
|
|
697
|
+
if _param_frame_equal(prior_p, next_p):
|
|
698
|
+
continue
|
|
699
|
+
if next_p is None:
|
|
700
|
+
# Going from "param present" to "param absent" effectively
|
|
701
|
+
# changes the LP shape — treat as cold.
|
|
702
|
+
raise _IncompatibleUpdate(
|
|
703
|
+
f"Param {fld!r} disappeared between sub-solves; "
|
|
704
|
+
f"falling back to cold rebuild.")
|
|
705
|
+
if kind == "rhs":
|
|
706
|
+
# Resolve the new RHS values positionally aligned to the
|
|
707
|
+
# ORIGINAL over frame's row order. The original over
|
|
708
|
+
# frame's dim labels (e.g. ``t``) differ from ``next_p``'s
|
|
709
|
+
# labels in rolling-horizon scenarios, so we can't rely on
|
|
710
|
+
# WarmProblem.update_rhs's label-based join — it would
|
|
711
|
+
# produce zeros for every row. Instead we resolve the new
|
|
712
|
+
# value vector against the NEW over frame (which has the
|
|
713
|
+
# new t-labels) and push as a positional ndarray of length
|
|
714
|
+
# row_count.
|
|
715
|
+
if over_field is None:
|
|
716
|
+
push = next_p
|
|
717
|
+
if transform == "neg":
|
|
718
|
+
push = -next_p
|
|
719
|
+
warm.update_rhs(target, push)
|
|
720
|
+
else:
|
|
721
|
+
new_over = getattr(nxt, over_field, None)
|
|
722
|
+
if new_over is None:
|
|
723
|
+
# Phase E.3: ``pss_dt`` / ``nodeBalance_dt`` /
|
|
724
|
+
# ``nodeState_dt`` / ``process_indirect_dt`` are no
|
|
725
|
+
# longer materialised on FlexData; build the cross-
|
|
726
|
+
# join on demand from the constituents so warm RHS
|
|
727
|
+
# alignment still works.
|
|
728
|
+
from flextool.engine_polars._pdt_join import (
|
|
729
|
+
compute_pss_dt,
|
|
730
|
+
compute_nodeBalance_dt,
|
|
731
|
+
compute_nodeState_dt,
|
|
732
|
+
compute_process_indirect_dt,
|
|
733
|
+
)
|
|
734
|
+
_compute = {
|
|
735
|
+
"pss_dt": compute_pss_dt,
|
|
736
|
+
"nodeBalance_dt": compute_nodeBalance_dt,
|
|
737
|
+
"nodeState_dt": compute_nodeState_dt,
|
|
738
|
+
"process_indirect_dt": compute_process_indirect_dt,
|
|
739
|
+
}.get(over_field)
|
|
740
|
+
if _compute is not None:
|
|
741
|
+
new_over = _compute(nxt)
|
|
742
|
+
if new_over is None:
|
|
743
|
+
raise _IncompatibleUpdate(
|
|
744
|
+
f"warm-update needs FlexData.{over_field}, but it "
|
|
745
|
+
f"is None on the new sub-solve")
|
|
746
|
+
# Left-join new_over with next_p on shared dims; values
|
|
747
|
+
# come out aligned to new_over's row order, which by
|
|
748
|
+
# fingerprint match has the same row count as the
|
|
749
|
+
# original LP cstr over.
|
|
750
|
+
shared = [c for c in next_p.dims if c in new_over.columns]
|
|
751
|
+
if not shared:
|
|
752
|
+
rhs_vec = np.full(new_over.height,
|
|
753
|
+
float(next_p.frame["value"][0]),
|
|
754
|
+
dtype=np.float64)
|
|
755
|
+
else:
|
|
756
|
+
j = new_over.join(next_p.frame, on=shared, how="left")
|
|
757
|
+
rhs_vec = (j["value"].fill_null(0.0)
|
|
758
|
+
.to_numpy()
|
|
759
|
+
.astype(np.float64, copy=False))
|
|
760
|
+
if transform == "neg":
|
|
761
|
+
rhs_vec = -rhs_vec
|
|
762
|
+
warm.update_rhs(target, rhs_vec)
|
|
763
|
+
elif kind == "obj":
|
|
764
|
+
push = next_p
|
|
765
|
+
if transform == "neg":
|
|
766
|
+
push = -next_p
|
|
767
|
+
warm.update_obj_coef(target, push)
|
|
768
|
+
else:
|
|
769
|
+
raise _IncompatibleUpdate(
|
|
770
|
+
f"unknown warm-update kind {kind!r} for {fld!r}")
|
|
771
|
+
n_updates += 1
|
|
772
|
+
|
|
773
|
+
# Auto-update for declared-mutable Params via the Param-tracked
|
|
774
|
+
# cell map. This handles every composite-Param diff that the
|
|
775
|
+
# clean-RHS / clean-obj path can't represent.
|
|
776
|
+
for fld, next_p in deferred_diffs.items():
|
|
777
|
+
if next_p is None:
|
|
778
|
+
# Param disappeared — can't auto-update (no values to push).
|
|
779
|
+
raise _IncompatibleUpdate(
|
|
780
|
+
f"mutable Param {fld!r} went None between sub-solves; "
|
|
781
|
+
f"falling back to cold rebuild.")
|
|
782
|
+
if fld not in warm._mutable_params:
|
|
783
|
+
# Param was tracked-mutable but the LP didn't actually
|
|
784
|
+
# carry it (build skipped its branch). Different LP — treat
|
|
785
|
+
# as cold.
|
|
786
|
+
raise _IncompatibleUpdate(
|
|
787
|
+
f"mutable Param {fld!r} differs but isn't tracked on "
|
|
788
|
+
f"the warm problem; falling back to cold rebuild.")
|
|
789
|
+
# CRITICAL silent-corruption guard (D1 audit, 2026-05-03).
|
|
790
|
+
# ``WarmProblem.update_param`` silently returns when the Param
|
|
791
|
+
# has no tracked cells — that's correct behaviour for Params
|
|
792
|
+
# whose only effect is on a code path that the engine's
|
|
793
|
+
# source-tracker walks (e.g. p_step_duration on dispatch
|
|
794
|
+
# rolls). But many "mutable" Params reach the LP through
|
|
795
|
+
# composite-anonymous-Param construction in flextool/model.py
|
|
796
|
+
# (Sum-collapse: the new Param is built fresh without a
|
|
797
|
+
# ``name=`` or ``_sources=`` link to the origin Param), and
|
|
798
|
+
# for those the side-table is empty even though the LP DOES
|
|
799
|
+
# depend on the values. Pushing a no-op there leaves stale
|
|
800
|
+
# coefficients in the live LP and silently corrupts the
|
|
801
|
+
# objective.
|
|
802
|
+
#
|
|
803
|
+
# Fall back to cold rebuild whenever a mutable Param differs
|
|
804
|
+
# but has zero tracked cells. Loses warm-mode benefit on
|
|
805
|
+
# those transitions but preserves correctness — the only
|
|
806
|
+
# acceptable trade-off given task constraints.
|
|
807
|
+
cells = warm._param_cells.get(fld)
|
|
808
|
+
has_cells = cells is not None and int(cells["rows"].size) > 0
|
|
809
|
+
if not has_cells and not _gate_active(nxt, fld):
|
|
810
|
+
# Param's gates are dormant — diff is phantom, no-op is
|
|
811
|
+
# genuinely safe (the consuming feature was never built).
|
|
812
|
+
continue
|
|
813
|
+
if not has_cells:
|
|
814
|
+
raise _IncompatibleUpdate(
|
|
815
|
+
f"mutable Param {fld!r} differs but the WarmProblem "
|
|
816
|
+
f"recorded zero tracked cells for it (Sum-collapse on "
|
|
817
|
+
f"the build-side composite-Param construction); "
|
|
818
|
+
f"falling back to cold rebuild to avoid silent stale "
|
|
819
|
+
f"LP coefficients.")
|
|
820
|
+
warm.update_param(fld, next_p)
|
|
821
|
+
n_updates += 1
|
|
822
|
+
|
|
823
|
+
return n_updates
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _build_warm_problem(
|
|
827
|
+
data: "FlexData",
|
|
828
|
+
*,
|
|
829
|
+
scale_the_objective: float = 1.0,
|
|
830
|
+
solver_options: dict | None = None,
|
|
831
|
+
) -> WarmProblem:
|
|
832
|
+
"""Build a fresh :class:`polar_high.WarmProblem` from a FlexData
|
|
833
|
+
snapshot, with all :data:`_MUTABLE_PARAMS` declared mutable so the
|
|
834
|
+
per-cell tracking side-table is populated during the build.
|
|
835
|
+
|
|
836
|
+
Parameters
|
|
837
|
+
----------
|
|
838
|
+
data
|
|
839
|
+
The :class:`FlexData` bundle for this iteration.
|
|
840
|
+
scale_the_objective
|
|
841
|
+
Multiplier applied to the objective inside ``build_flextool``.
|
|
842
|
+
solver_options
|
|
843
|
+
HiGHS options to apply via ``pb.set_solver_options`` BEFORE
|
|
844
|
+
wrapping in :class:`WarmProblem`. Typically the dict returned
|
|
845
|
+
by :func:`flextool.engine_polars.scaling.recommended_highs_options`.
|
|
846
|
+
|
|
847
|
+
Late-imports ``build_flextool`` to avoid a build-time cycle between
|
|
848
|
+
this module and ``model.py``.
|
|
849
|
+
"""
|
|
850
|
+
from flextool.engine_polars.model import build_flextool
|
|
851
|
+
|
|
852
|
+
pb = Problem()
|
|
853
|
+
build_flextool(pb, data, scale_the_objective=scale_the_objective)
|
|
854
|
+
if solver_options:
|
|
855
|
+
pb.set_solver_options(solver_options)
|
|
856
|
+
warm = WarmProblem(pb)
|
|
857
|
+
warm.declare_mutable(*_MUTABLE_PARAMS)
|
|
858
|
+
return warm
|