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,336 @@
|
|
|
1
|
+
"""YAML audit report for the autoscaler.
|
|
2
|
+
|
|
3
|
+
Layer 1 writes the four ranges + trigger flag. Layer 2 / Layer 3 will
|
|
4
|
+
extend the same file with their decisions (column scalers per quantity,
|
|
5
|
+
``user_bound_scale`` value applied, etc.) — the structure is kept flat
|
|
6
|
+
and self-documenting so the operator can read it without consulting
|
|
7
|
+
schema docs.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Mapping
|
|
14
|
+
|
|
15
|
+
from polar_high.autoscale import Layer3Plan, RangeReport
|
|
16
|
+
|
|
17
|
+
from ._layer2 import Layer2Plan
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _decades(span: tuple[float, float]) -> Any:
|
|
21
|
+
"""Return the spread of a range as decades (log10(hi/lo)).
|
|
22
|
+
|
|
23
|
+
``(nan, nan)`` (empty group) returns ``None`` — the caller renders
|
|
24
|
+
that as ``"empty"`` in the console line.
|
|
25
|
+
"""
|
|
26
|
+
lo, hi = span
|
|
27
|
+
if math.isnan(lo) or math.isnan(hi):
|
|
28
|
+
return None
|
|
29
|
+
if lo <= 0.0:
|
|
30
|
+
return None
|
|
31
|
+
if hi <= 0.0:
|
|
32
|
+
return None
|
|
33
|
+
return math.log10(hi / lo)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _fmt_decades(value: Any) -> str:
|
|
37
|
+
"""Render a decade spread as ``"7.1d"`` or ``"empty"``."""
|
|
38
|
+
if value is None:
|
|
39
|
+
return "empty"
|
|
40
|
+
return f"{value:.1f}d"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _coerce_range(span: tuple[float, float]) -> dict[str, Any]:
|
|
44
|
+
"""Render a ``(lo, hi)`` tuple as a small YAML dict.
|
|
45
|
+
|
|
46
|
+
NaN entries (the "no finite non-zero entries" sentinel) become the
|
|
47
|
+
string ``"empty"`` so the YAML is still valid (YAML disallows bare
|
|
48
|
+
NaN in most loaders) and the meaning is unambiguous in a manual read.
|
|
49
|
+
"""
|
|
50
|
+
lo, hi = span
|
|
51
|
+
if math.isnan(lo) or math.isnan(hi):
|
|
52
|
+
return {"status": "empty"}
|
|
53
|
+
return {
|
|
54
|
+
"min": float(lo),
|
|
55
|
+
"max": float(hi),
|
|
56
|
+
"ratio": float(hi) / float(lo) if lo != 0 else None,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _coerce_cross_ratio(value: float) -> Any:
|
|
61
|
+
"""Render the cross-group ratio, mapping NaN → ``None``."""
|
|
62
|
+
if math.isnan(value):
|
|
63
|
+
return None
|
|
64
|
+
return float(value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _render_layer1(report: RangeReport) -> dict[str, Any]:
|
|
68
|
+
"""Build the ``layer1`` section of the autoscaler audit YAML.
|
|
69
|
+
|
|
70
|
+
Kept as a small pure function so Layer 2 / Layer 3 sections can be
|
|
71
|
+
composed alongside it without :func:`write_report` growing branches.
|
|
72
|
+
"""
|
|
73
|
+
return {
|
|
74
|
+
"ranges": {
|
|
75
|
+
"matrix": _coerce_range(report.matrix),
|
|
76
|
+
"cost": _coerce_range(report.cost),
|
|
77
|
+
"bound": _coerce_range(report.bound),
|
|
78
|
+
"rhs": _coerce_range(report.rhs),
|
|
79
|
+
},
|
|
80
|
+
"cross_group_max_ratio": _coerce_cross_ratio(report.cross_group_max_ratio),
|
|
81
|
+
"trigger": bool(report.trigger),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _dump_simple_yaml(obj: Any, indent: int = 0) -> str:
|
|
86
|
+
"""Tiny dependency-free YAML emitter.
|
|
87
|
+
|
|
88
|
+
Avoids a hard PyYAML dependency for this stub — the autoscaler only
|
|
89
|
+
ever writes scalar / mapping / list trees with no anchors, references
|
|
90
|
+
or tags, so the surface stays trivial. Keys are emitted in insertion
|
|
91
|
+
order (Python 3.7+ dict semantics) so the operator-visible layout is
|
|
92
|
+
stable. Floats are rendered with ``repr`` so round-tripping into a
|
|
93
|
+
real YAML loader recovers the bit-pattern.
|
|
94
|
+
|
|
95
|
+
If a future phase needs richer YAML (e.g. multi-line strings),
|
|
96
|
+
swap to :mod:`yaml`; the API of :func:`write_report` does not change.
|
|
97
|
+
"""
|
|
98
|
+
pad = " " * indent
|
|
99
|
+
if isinstance(obj, Mapping):
|
|
100
|
+
if not obj:
|
|
101
|
+
return "{}"
|
|
102
|
+
lines: list[str] = []
|
|
103
|
+
for k, v in obj.items():
|
|
104
|
+
if isinstance(v, (Mapping, list)):
|
|
105
|
+
rendered = _dump_simple_yaml(v, indent + 2)
|
|
106
|
+
if isinstance(v, Mapping) and v:
|
|
107
|
+
lines.append(f"{pad}{k}:")
|
|
108
|
+
lines.append(rendered)
|
|
109
|
+
elif isinstance(v, list) and v:
|
|
110
|
+
lines.append(f"{pad}{k}:")
|
|
111
|
+
lines.append(rendered)
|
|
112
|
+
else:
|
|
113
|
+
lines.append(f"{pad}{k}: {rendered}")
|
|
114
|
+
else:
|
|
115
|
+
lines.append(f"{pad}{k}: {_scalar(v)}")
|
|
116
|
+
return "\n".join(lines)
|
|
117
|
+
if isinstance(obj, list):
|
|
118
|
+
if not obj:
|
|
119
|
+
return "[]"
|
|
120
|
+
return "\n".join(
|
|
121
|
+
f"{pad}- {_dump_simple_yaml(item, indent + 2).lstrip() if isinstance(item, (Mapping, list)) else _scalar(item)}"
|
|
122
|
+
for item in obj
|
|
123
|
+
)
|
|
124
|
+
return _scalar(obj)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _scalar(v: Any) -> str:
|
|
128
|
+
"""Render a scalar leaf for the YAML emitter."""
|
|
129
|
+
if v is None:
|
|
130
|
+
return "null"
|
|
131
|
+
if isinstance(v, bool):
|
|
132
|
+
return "true" if v else "false"
|
|
133
|
+
if isinstance(v, float):
|
|
134
|
+
if math.isnan(v):
|
|
135
|
+
return ".nan"
|
|
136
|
+
if math.isinf(v):
|
|
137
|
+
return ".inf" if v > 0 else "-.inf"
|
|
138
|
+
return repr(v)
|
|
139
|
+
if isinstance(v, int):
|
|
140
|
+
return str(v)
|
|
141
|
+
s = str(v)
|
|
142
|
+
if any(c in s for c in (":", "#", "\n", "{", "}", "[", "]", "&", "*")) or s.strip() != s:
|
|
143
|
+
# Escape via single-quoted scalar — YAML's safest form.
|
|
144
|
+
return "'" + s.replace("'", "''") + "'"
|
|
145
|
+
return s
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def render_layer2(plan: Layer2Plan) -> dict[str, Any]:
|
|
149
|
+
"""Build the ``layer2`` section of the autoscaler audit YAML.
|
|
150
|
+
|
|
151
|
+
Captures the per-type exponents, the count of skipped rows /
|
|
152
|
+
integer columns, and the per-type before/after magnitude ranges.
|
|
153
|
+
Also surfaces the post-scale RHS range of the
|
|
154
|
+
``ladder_tier_cap_annual_roll`` family for the H2_trade sanity
|
|
155
|
+
check — the family the regression handoff identifies as the
|
|
156
|
+
Layer-2 trigger.
|
|
157
|
+
"""
|
|
158
|
+
exponents = {t.value: int(e) for t, e in plan.type_exponents.items()}
|
|
159
|
+
type_ranges_before = {
|
|
160
|
+
t.value: _coerce_range(r) for t, r in plan.type_buckets_before.items()
|
|
161
|
+
}
|
|
162
|
+
type_ranges_after = {
|
|
163
|
+
t.value: _coerce_range(r) for t, r in plan.type_buckets_after.items()
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
"type_exponents": exponents,
|
|
167
|
+
"skipped_rows_count": len(plan.skipped_rows),
|
|
168
|
+
"skipped_rows": list(plan.skipped_rows),
|
|
169
|
+
"skipped_integer_cols_count": len(plan.skipped_integer_cols),
|
|
170
|
+
"n_cols": int(plan.col_factors.shape[0]),
|
|
171
|
+
"n_rows": int(plan.row_factors.shape[0]),
|
|
172
|
+
"type_ranges_before": type_ranges_before,
|
|
173
|
+
"type_ranges_after": type_ranges_after,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def render_layer3(plan: Layer3Plan) -> dict[str, Any]:
|
|
178
|
+
"""Build the ``layer3`` section of the autoscaler audit YAML.
|
|
179
|
+
|
|
180
|
+
Surfaces the three HiGHS options Layer 3 set and the reasoning
|
|
181
|
+
string so the operator can correlate the YAML entry with the
|
|
182
|
+
one-line log emitted at apply time.
|
|
183
|
+
"""
|
|
184
|
+
return {
|
|
185
|
+
"user_objective_scale": int(plan.user_objective_scale),
|
|
186
|
+
"user_bound_scale": int(plan.user_bound_scale),
|
|
187
|
+
"simplex_scale_strategy": int(plan.simplex_scale_strategy),
|
|
188
|
+
"reasoning": str(plan.reasoning),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def write_report(result: Mapping[str, Any], path: Path | str) -> Path:
|
|
193
|
+
"""Serialise the autoscaler result tree to ``path`` as YAML.
|
|
194
|
+
|
|
195
|
+
``result`` is a mapping whose top-level keys are layer names
|
|
196
|
+
(``"layer1"``, later ``"layer2"`` / ``"layer3"``) and whose values
|
|
197
|
+
are the per-layer payloads. Layer 1 hands a :class:`RangeReport`;
|
|
198
|
+
we render it via :func:`_render_layer1`. Unknown payload types pass
|
|
199
|
+
through to the generic dumper — keeps the door open for later
|
|
200
|
+
phases without coupling the layers via shared schema.
|
|
201
|
+
|
|
202
|
+
Returns the resolved :class:`Path` that was written, for caller
|
|
203
|
+
bookkeeping (the wire-in logs it).
|
|
204
|
+
"""
|
|
205
|
+
rendered: dict[str, Any] = {}
|
|
206
|
+
for key, payload in result.items():
|
|
207
|
+
if isinstance(payload, RangeReport):
|
|
208
|
+
rendered[key] = _render_layer1(payload)
|
|
209
|
+
else:
|
|
210
|
+
rendered[key] = payload
|
|
211
|
+
|
|
212
|
+
path = Path(path)
|
|
213
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
text = _dump_simple_yaml(rendered)
|
|
215
|
+
path.write_text(text + "\n", encoding="utf-8")
|
|
216
|
+
return path
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def format_console_summary(
|
|
220
|
+
*,
|
|
221
|
+
ranges_pre: RangeReport,
|
|
222
|
+
ranges_post: "RangeReport | None",
|
|
223
|
+
layer2_plan: "Layer2Plan | None",
|
|
224
|
+
layer3_plan: "Layer3Plan | None",
|
|
225
|
+
threshold_decades: float,
|
|
226
|
+
) -> str:
|
|
227
|
+
"""Compose the one-line console summary surfaced when autoscale fires.
|
|
228
|
+
|
|
229
|
+
Renders the four pre-Layer-1 spreads in decades, the per-type Layer 2
|
|
230
|
+
exponents, the Layer 3 ``user_*_scale`` + ``simplex_scale_strategy``
|
|
231
|
+
values, and the post-action four-range spreads so the operator sees
|
|
232
|
+
"before → after" at a glance.
|
|
233
|
+
|
|
234
|
+
When ``ranges_pre.trigger`` is False the function returns the quieter
|
|
235
|
+
"within comfort zone" confirmation — callers can branch on that path
|
|
236
|
+
by inspecting the report themselves, but exposing the message here
|
|
237
|
+
keeps the format definition in one place.
|
|
238
|
+
"""
|
|
239
|
+
if not ranges_pre.trigger:
|
|
240
|
+
return (
|
|
241
|
+
"Autoscale by polar-high: LP within HiGHS comfort zone "
|
|
242
|
+
f"(spreads <= {threshold_decades:g} decades) — no scaling applied"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
pre_matrix = _fmt_decades(_decades(ranges_pre.matrix))
|
|
246
|
+
pre_cost = _fmt_decades(_decades(ranges_pre.cost))
|
|
247
|
+
pre_bound = _fmt_decades(_decades(ranges_pre.bound))
|
|
248
|
+
pre_rhs = _fmt_decades(_decades(ranges_pre.rhs))
|
|
249
|
+
|
|
250
|
+
parts: list[str] = [
|
|
251
|
+
f"Autoscale by polar-high: ranges pre Matrix={pre_matrix} Cost={pre_cost} "
|
|
252
|
+
f"Bound={pre_bound} RHS={pre_rhs}"
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
if layer2_plan is not None:
|
|
256
|
+
exps = ", ".join(
|
|
257
|
+
f"{t.value}:{e:+d}" for t, e in layer2_plan.type_exponents.items()
|
|
258
|
+
)
|
|
259
|
+
skipped = len(layer2_plan.skipped_rows)
|
|
260
|
+
parts.append(
|
|
261
|
+
f"L2 exponents {{{exps}}}"
|
|
262
|
+
+ (f" skipped_rows={skipped}" if skipped else "")
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
parts.append("L2 skipped")
|
|
266
|
+
|
|
267
|
+
if layer3_plan is not None:
|
|
268
|
+
parts.append(
|
|
269
|
+
f"L3 user_obj={layer3_plan.user_objective_scale} "
|
|
270
|
+
f"user_bnd={layer3_plan.user_bound_scale} "
|
|
271
|
+
f"simplex={layer3_plan.simplex_scale_strategy}"
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
parts.append("L3 skipped")
|
|
275
|
+
|
|
276
|
+
if ranges_post is not None:
|
|
277
|
+
post_matrix = _fmt_decades(_decades(ranges_post.matrix))
|
|
278
|
+
post_cost = _fmt_decades(_decades(ranges_post.cost))
|
|
279
|
+
post_bound = _fmt_decades(_decades(ranges_post.bound))
|
|
280
|
+
post_rhs = _fmt_decades(_decades(ranges_post.rhs))
|
|
281
|
+
parts.append(
|
|
282
|
+
f"ranges post Matrix={post_matrix} Cost={post_cost} "
|
|
283
|
+
f"Bound={post_bound} RHS={post_rhs}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
flat = " -> ".join(parts)
|
|
287
|
+
if len(flat) <= 100:
|
|
288
|
+
return flat
|
|
289
|
+
return parts[0] + "".join(f"\n -> {p}" for p in parts[1:])
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def format_nonoptimal_hint(ranges: RangeReport) -> str:
|
|
293
|
+
"""Compose the scaling-related *possibility* hint for a rejected solve.
|
|
294
|
+
|
|
295
|
+
Returns the empty string unless ``ranges.trigger`` is ``True``. The
|
|
296
|
+
caller must pass the ranges of the *actually solved* (post-autoscale)
|
|
297
|
+
LP — a still-tripped trigger there means the autoscaler could NOT tame
|
|
298
|
+
the range spread, so scaling is a plausible (not proven) contributor to
|
|
299
|
+
the failure. Framed as a possibility with remediations, never as the
|
|
300
|
+
certified cause: a wide range does not by itself explain a non-optimal
|
|
301
|
+
status, and passing the raw pre-autoscale ranges here (which are always
|
|
302
|
+
wide for FlexTool's commodity ladders) would make the hint fire on
|
|
303
|
+
essentially every failure regardless of cause.
|
|
304
|
+
"""
|
|
305
|
+
if not ranges.trigger:
|
|
306
|
+
return ""
|
|
307
|
+
|
|
308
|
+
def _span_decades(span: tuple[float, float]) -> str:
|
|
309
|
+
d = _decades(span)
|
|
310
|
+
if d is None:
|
|
311
|
+
return "empty"
|
|
312
|
+
return f"{d:.1f} decades"
|
|
313
|
+
|
|
314
|
+
rhs_span = _span_decades(ranges.rhs)
|
|
315
|
+
cost_span = _span_decades(ranges.cost)
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
"The solved (post-autoscale) LP is still wide-ranged "
|
|
319
|
+
f"(RHS spans {rhs_span}, Cost spans {cost_span}), so scaling MAY be "
|
|
320
|
+
"a contributing factor. Possible remediations:\n"
|
|
321
|
+
" - Check unit conventions on commodity-ladder entities "
|
|
322
|
+
"(set unitsize so quantity/unitsize <= 1e+6)\n"
|
|
323
|
+
" - Re-run with --highs-threads 1 to bypass parallel-mode "
|
|
324
|
+
"brittleness\n"
|
|
325
|
+
" - Re-run with --scaling=solver_only if you suspect the "
|
|
326
|
+
"autoscaler interferes (HiGHS' internal scaling still runs)"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
__all__ = [
|
|
331
|
+
"format_console_summary",
|
|
332
|
+
"format_nonoptimal_hint",
|
|
333
|
+
"render_layer2",
|
|
334
|
+
"render_layer3",
|
|
335
|
+
"write_report",
|
|
336
|
+
]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Thin compat shim around the native cascade — Δ.12e.
|
|
2
|
+
|
|
3
|
+
The legacy file-symlink driver (loading per-sub-solve
|
|
4
|
+
``solve_data_<sub>/`` snapshots directly from a work folder) retired
|
|
5
|
+
in Δ.12e once the native cascade in
|
|
6
|
+
:mod:`flextool.engine_polars._orchestration` reached feature parity:
|
|
7
|
+
warm-LP across structurally-compatible iterations (Δ.12d), in-memory
|
|
8
|
+
handoff carriers between sub-solves (Δ.11/Δ.12), full output writer
|
|
9
|
+
coverage (Δ.1, Δ.12c-fix), and override-chain authority for Direct /
|
|
10
|
+
Derived / Projection params (Δ.12-drop, Δ.12c, Δ.12c-fix2).
|
|
11
|
+
|
|
12
|
+
What's left here is a thin convenience wrapper that converts a
|
|
13
|
+
work-folder path (containing a ``tests.sqlite`` / ``input.sqlite``)
|
|
14
|
+
into the canonical native call
|
|
15
|
+
:func:`flextool.engine_polars._orchestration.run_chain_from_db` and
|
|
16
|
+
adapts the result to :class:`ChainStep` shape for backwards
|
|
17
|
+
compatibility with the few external callers that still import
|
|
18
|
+
``run_chain`` directly.
|
|
19
|
+
|
|
20
|
+
The warm-update primitives (structural fingerprint, Param classification,
|
|
21
|
+
:func:`_apply_warm_updates`) live in :mod:`flextool.engine_polars._warm`
|
|
22
|
+
and are re-exported here as module-level names for backwards
|
|
23
|
+
compatibility (e.g. ``from flextool.engine_polars.chain import
|
|
24
|
+
_MUTABLE_PARAMS`` in ``test_warm_param_autoupdate``).
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from polar_high import Solution
|
|
31
|
+
|
|
32
|
+
# Backwards-compat re-exports of warm-update primitives that now live in
|
|
33
|
+
# :mod:`flextool.engine_polars._warm`. Documented in the module
|
|
34
|
+
# docstring above; consumed by ``tests/engine_polars/test_warm_param_autoupdate.py``
|
|
35
|
+
# (and any external caller that still does ``from
|
|
36
|
+
# flextool.engine_polars.chain import _MUTABLE_PARAMS``). Listed in
|
|
37
|
+
# ``__all__`` so ruff F401 does not strip them as unused.
|
|
38
|
+
from flextool.engine_polars._warm import ( # noqa: F401
|
|
39
|
+
_STRUCTURAL_FIELDS,
|
|
40
|
+
_WARM_PARAMS,
|
|
41
|
+
_MUTABLE_PARAMS,
|
|
42
|
+
_WARM_PARAMS_DEFERRED,
|
|
43
|
+
_WARM_PARAMS_NO_OP,
|
|
44
|
+
_WARM_PARAM_GATES,
|
|
45
|
+
_IncompatibleUpdate,
|
|
46
|
+
_fingerprint,
|
|
47
|
+
_param_frame_equal,
|
|
48
|
+
_param_values_position_equal,
|
|
49
|
+
_gate_active,
|
|
50
|
+
_apply_warm_updates,
|
|
51
|
+
_build_warm_problem,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"run_chain",
|
|
57
|
+
"ChainStep",
|
|
58
|
+
# Re-exported warm-update primitives (see docstring).
|
|
59
|
+
"_STRUCTURAL_FIELDS",
|
|
60
|
+
"_WARM_PARAMS",
|
|
61
|
+
"_MUTABLE_PARAMS",
|
|
62
|
+
"_WARM_PARAMS_DEFERRED",
|
|
63
|
+
"_WARM_PARAMS_NO_OP",
|
|
64
|
+
"_WARM_PARAM_GATES",
|
|
65
|
+
"_IncompatibleUpdate",
|
|
66
|
+
"_fingerprint",
|
|
67
|
+
"_param_frame_equal",
|
|
68
|
+
"_param_values_position_equal",
|
|
69
|
+
"_gate_active",
|
|
70
|
+
"_apply_warm_updates",
|
|
71
|
+
"_build_warm_problem",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ChainStep:
|
|
76
|
+
"""Per-sub-solve result of :func:`run_chain`.
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
solve_name : str
|
|
81
|
+
The sub-solve identifier (e.g. ``"y2025_5week"``).
|
|
82
|
+
solution : polar_high.Solution
|
|
83
|
+
The HiGHS solution for this sub-solve.
|
|
84
|
+
handoff : flextool.SolveHandoff
|
|
85
|
+
The handoff carriers derived from the polar_high solution
|
|
86
|
+
(see :func:`flextool.input.build_handoff_from_solution`).
|
|
87
|
+
warm_used : bool
|
|
88
|
+
True if this sub-solve was solved by warm-updating the prior
|
|
89
|
+
sub-solve's :class:`WarmProblem` instance; False if it was a
|
|
90
|
+
cold rebuild. Always False for the first sub-solve and for
|
|
91
|
+
``warm=False`` runs.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
__slots__ = ("solve_name", "solution", "handoff", "warm_used")
|
|
95
|
+
|
|
96
|
+
def __init__(self, solve_name: str, solution: Solution, handoff,
|
|
97
|
+
warm_used: bool = False):
|
|
98
|
+
self.solve_name = solve_name
|
|
99
|
+
self.solution = solution
|
|
100
|
+
self.handoff = handoff
|
|
101
|
+
self.warm_used = warm_used
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str: # pragma: no cover — debug-only
|
|
104
|
+
return (f"ChainStep(solve_name={self.solve_name!r}, "
|
|
105
|
+
f"obj={self.solution.obj!r}, "
|
|
106
|
+
f"warm_used={self.warm_used}, "
|
|
107
|
+
f"handoff_empty={self.handoff.is_empty()})")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Same overrides used by the parity-sweep fixtures — see
|
|
111
|
+
# tests/engine_polars/test_solve_config_parity._discover_fixtures. The
|
|
112
|
+
# keys are work_folder dirnames; values are the scenario names that
|
|
113
|
+
# produced those snapshots.
|
|
114
|
+
_NATIVE_SCENARIO_OVERRIDES: dict[str, str] = {
|
|
115
|
+
"work_2day_stochastic_dispatch_full_storage": "2_day_stochastic_dispatch",
|
|
116
|
+
"work_commodity_ladder_annual": "coal_ladder_annual",
|
|
117
|
+
"work_commodity_ladder_cumulative": "coal_ladder_cumulative",
|
|
118
|
+
"work_delay_source_coef": "water_pump_delayed",
|
|
119
|
+
"work_inflation_check": "wind_battery_invest_lifetime_renew",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _resolve_native_scenario(db_path: Path, work: Path) -> str | None:
|
|
124
|
+
"""Map ``work/`` dirname → scenario name using the same convention
|
|
125
|
+
as the parity tests. Returns ``None`` when no rule matches.
|
|
126
|
+
"""
|
|
127
|
+
import re
|
|
128
|
+
import spinedb_api as api
|
|
129
|
+
|
|
130
|
+
if work.name in _NATIVE_SCENARIO_OVERRIDES:
|
|
131
|
+
return _NATIVE_SCENARIO_OVERRIDES[work.name]
|
|
132
|
+
|
|
133
|
+
scen_target = work.name.removeprefix("work_") if work.name.startswith("work_") else None
|
|
134
|
+
if scen_target is None:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
with api.DatabaseMapping("sqlite:///" + str(db_path)) as db:
|
|
139
|
+
scenarios = sorted(s.name for s in db.query(db.scenario_sq).all())
|
|
140
|
+
except Exception:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
candidates = [scen_target]
|
|
144
|
+
candidates.append(re.sub(r"(^|_)(\d+)([a-z])", r"\1\2_\3", scen_target))
|
|
145
|
+
candidates.append(re.sub(r"(\d+)_([a-z])", r"\1\2", scen_target))
|
|
146
|
+
if scen_target.endswith("_full_storage"):
|
|
147
|
+
base = scen_target[: -len("_full_storage")]
|
|
148
|
+
candidates.append(re.sub(r"(^|_)(\d+)([a-z])", r"\1\2_\3", base))
|
|
149
|
+
candidates.append(base)
|
|
150
|
+
for cand in candidates:
|
|
151
|
+
if cand in scenarios:
|
|
152
|
+
return cand
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_chain(
|
|
157
|
+
work_folder: Path | str,
|
|
158
|
+
*,
|
|
159
|
+
warm: bool = True,
|
|
160
|
+
scenario: str | None = None,
|
|
161
|
+
) -> dict[str, ChainStep]:
|
|
162
|
+
"""Run a flextool multi-solve scenario end-to-end via the native
|
|
163
|
+
cascade — convenience adapter that takes a work-folder path.
|
|
164
|
+
|
|
165
|
+
Δ.12e — this function is now a thin shim that:
|
|
166
|
+
|
|
167
|
+
1. Looks up ``tests.sqlite`` / ``input.sqlite`` under
|
|
168
|
+
*work_folder*.
|
|
169
|
+
2. Resolves the scenario name from the dir-name convention (see
|
|
170
|
+
:data:`_NATIVE_SCENARIO_OVERRIDES`) or the explicit
|
|
171
|
+
``scenario`` kwarg / ``FLEXTOOL_NATIVE_SCENARIO`` env var.
|
|
172
|
+
3. Delegates to
|
|
173
|
+
:func:`flextool.engine_polars._orchestration.run_chain_from_db`.
|
|
174
|
+
4. Adapts the result from
|
|
175
|
+
:class:`flextool.engine_polars.OrchestrationStep` to
|
|
176
|
+
:class:`ChainStep` shape (the legacy return type a few
|
|
177
|
+
external callers still import).
|
|
178
|
+
|
|
179
|
+
The previous file-symlink driver (loading
|
|
180
|
+
``solve_data_<sub>/`` snapshots directly from the work folder)
|
|
181
|
+
and the ``use_handoff_overlay`` / ``native`` / ``chain`` kwargs
|
|
182
|
+
retired in Δ.12e once the native cascade reached feature parity.
|
|
183
|
+
Callers driving DB scenarios should prefer
|
|
184
|
+
:func:`run_chain_from_db` directly.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
work_folder : Path | str
|
|
189
|
+
Directory containing a Spine SQLite (``tests.sqlite`` or
|
|
190
|
+
``input.sqlite``). The native cascade re-runs flextool's
|
|
191
|
+
preprocessing into a private tempdir; the work folder is
|
|
192
|
+
consulted only for the DB.
|
|
193
|
+
warm : bool, default False
|
|
194
|
+
When True, attempt warm LP updates between consecutive
|
|
195
|
+
structurally-compatible sub-solves using
|
|
196
|
+
:class:`polar_high.WarmProblem`. See
|
|
197
|
+
:func:`flextool.engine_polars._orchestration.run_orchestration`
|
|
198
|
+
for the per-iteration semantics.
|
|
199
|
+
scenario : str | None, default None
|
|
200
|
+
Explicit scenario name override. ``None`` consults the
|
|
201
|
+
``FLEXTOOL_NATIVE_SCENARIO`` env var, then falls back to the
|
|
202
|
+
``work_<scenario>`` dir-name convention.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
dict[str, ChainStep]
|
|
207
|
+
Mapping ``solve_name → ChainStep(solve_name, solution, handoff,
|
|
208
|
+
warm_used)``.
|
|
209
|
+
|
|
210
|
+
Raises
|
|
211
|
+
------
|
|
212
|
+
ValueError
|
|
213
|
+
If no DB is found under *work_folder* or the scenario can't
|
|
214
|
+
be determined.
|
|
215
|
+
"""
|
|
216
|
+
import os
|
|
217
|
+
work = Path(work_folder)
|
|
218
|
+
db_path = None
|
|
219
|
+
for cand in ("tests.sqlite", "input.sqlite"):
|
|
220
|
+
p = work / cand
|
|
221
|
+
if p.exists():
|
|
222
|
+
db_path = p
|
|
223
|
+
break
|
|
224
|
+
if db_path is None:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"run_chain: no DB found under {work} "
|
|
227
|
+
f"(looked for tests.sqlite, input.sqlite). The native "
|
|
228
|
+
f"cascade requires a DB scenario."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Late import to avoid a build-time cycle between chain and _orchestration.
|
|
232
|
+
from flextool.engine_polars._orchestration import run_chain_from_db
|
|
233
|
+
|
|
234
|
+
if scenario is None:
|
|
235
|
+
scenario = os.environ.get("FLEXTOOL_NATIVE_SCENARIO") or None
|
|
236
|
+
if scenario is None:
|
|
237
|
+
scenario = _resolve_native_scenario(db_path, work)
|
|
238
|
+
if scenario is None:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"run_chain: cannot determine which scenario to run for "
|
|
241
|
+
f"{work}. Pass scenario= explicitly, set "
|
|
242
|
+
f"FLEXTOOL_NATIVE_SCENARIO, or rename the work directory to "
|
|
243
|
+
f"match the scenario name (work_<scenario>). "
|
|
244
|
+
f"work.name={work.name!r}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Phase C.5 — ``ChainStep`` keeps the per-step ``solution``
|
|
248
|
+
# contract for legacy callers; opt into the full per-step state on
|
|
249
|
+
# the underlying cascade.
|
|
250
|
+
steps = run_chain_from_db(db_path, scenario, warm=warm, keep_solutions=True)
|
|
251
|
+
out: dict[str, ChainStep] = {}
|
|
252
|
+
for name, step in steps.items():
|
|
253
|
+
out[name] = ChainStep(
|
|
254
|
+
solve_name=name,
|
|
255
|
+
solution=step.solution,
|
|
256
|
+
handoff=step.handoff,
|
|
257
|
+
warm_used=getattr(step, "warm_used", False),
|
|
258
|
+
)
|
|
259
|
+
return out
|