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,276 @@
|
|
|
1
|
+
"""Shared period-walk utilities for derived-helper clusters (Δ.6).
|
|
2
|
+
|
|
3
|
+
This module hosts the lazy-polars period-walk + lifetime-window join
|
|
4
|
+
utility consumed by both Cluster A (annual integration / NPV; Δ.5) and
|
|
5
|
+
Cluster B (existing chain & invest sets; Δ.6) of the derived-helper
|
|
6
|
+
port.
|
|
7
|
+
|
|
8
|
+
The Δ.5 close stanza in ``progress.md`` flagged that the
|
|
9
|
+
``period_walk_iterator`` helper in ``_derived_npv.py`` was a candidate
|
|
10
|
+
for extraction so Cluster B can re-use it without circular import.
|
|
11
|
+
This file is the realised refactor.
|
|
12
|
+
|
|
13
|
+
User-locked decisions (per the Δ.6 dispatch):
|
|
14
|
+
|
|
15
|
+
* **Lazy polars throughout.** Helpers return ``pl.LazyFrame`` chains;
|
|
16
|
+
the ``Param`` constructor calls ``.collect()`` at the boundary.
|
|
17
|
+
* **``window_method`` enum dispatches bounded vs. unbounded** lifetime
|
|
18
|
+
windows in a single helper signature (replacing the Δ.5 ``bounded:
|
|
19
|
+
bool`` + conditional kwarg).
|
|
20
|
+
* **No defensive gating** — invalid inputs raise.
|
|
21
|
+
|
|
22
|
+
Algorithm
|
|
23
|
+
---------
|
|
24
|
+
|
|
25
|
+
The walk implements the per-(e, d) "integrate over lifetime window"
|
|
26
|
+
pattern shared by Cluster A (NPV / lifetime fixed cost) and Cluster B
|
|
27
|
+
(``edd_history`` / ``edd_invest_set``) derivations.
|
|
28
|
+
|
|
29
|
+
For each (e, d) anchor pair, walk every ``d_all`` ∈ ``period_in_use``
|
|
30
|
+
and collect those that match the window predicate:
|
|
31
|
+
|
|
32
|
+
* :data:`WindowMethod.BOUNDED` —
|
|
33
|
+
``pdy[d_all] ∈ [pdy[d], pdy[d] + life[e, d])``.
|
|
34
|
+
* :data:`WindowMethod.UNBOUNDED_FORWARD` —
|
|
35
|
+
``pdy[d_all] ≥ pdy[d]``.
|
|
36
|
+
* :data:`WindowMethod.STRICT_LOOKBACK_BOUNDED` —
|
|
37
|
+
``pdy[d_all] > pdy[d]`` AND ``pdy[d_all] < pdy[d] + life[e, d]``
|
|
38
|
+
(used by Cluster B's ``edd_invest_lookback_set`` for bounded
|
|
39
|
+
``reinvest_choice`` / ``no_investment`` entities).
|
|
40
|
+
* :data:`WindowMethod.STRICT_LOOKBACK_UNBOUNDED` —
|
|
41
|
+
``pdy[d_all] > pdy[d]`` (no lifetime cap; for unbounded
|
|
42
|
+
``reinvest_automatic`` entities in Cluster B's lookback walk).
|
|
43
|
+
|
|
44
|
+
The aggregation is configurable: callers can either pass a per-d_all
|
|
45
|
+
weight column to sum (``factor_side``: ``"inv"`` / ``"ops"`` →
|
|
46
|
+
inflation factor), or skip aggregation entirely and return the
|
|
47
|
+
``(e, d, d_all)`` triples (``factor_side=None``) for set-shape outputs.
|
|
48
|
+
"""
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import enum
|
|
52
|
+
from typing import TYPE_CHECKING
|
|
53
|
+
|
|
54
|
+
import polars as pl
|
|
55
|
+
|
|
56
|
+
from flextool.engine_polars._axis_enums import rename_to_axis
|
|
57
|
+
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
from flextool.engine_polars._input_source import InputSource
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class WindowMethod(enum.Enum):
|
|
63
|
+
"""Lifetime-window selector for :func:`period_walk_iterator`.
|
|
64
|
+
|
|
65
|
+
Members:
|
|
66
|
+
|
|
67
|
+
* ``BOUNDED`` — ``pdy[d_all] ∈ [pdy[d], pdy[d] + life[e, d])``.
|
|
68
|
+
Used by Cluster A's ``reinvest_choice`` / ``no_investment`` cohort
|
|
69
|
+
and by Cluster A's divest-side NPV (always bounded, raw lifetime).
|
|
70
|
+
* ``UNBOUNDED_FORWARD`` — ``pdy[d_all] ≥ pdy[d]``. Used by
|
|
71
|
+
Cluster A's ``reinvest_automatic`` cohort.
|
|
72
|
+
* ``STRICT_LOOKBACK_BOUNDED`` — ``pdy[d] < pdy[d_all] < pdy[d] +
|
|
73
|
+
life[e, d]``. Used by Cluster B's
|
|
74
|
+
``edd_invest_lookback_set`` walk where the invest is treated as
|
|
75
|
+
having taken effect strictly *before* the dispatch period.
|
|
76
|
+
* ``BOUNDED_INCLUSIVE_LOOKBACK`` — ``pdy[d_all] >= pdy[d]`` AND
|
|
77
|
+
``pdy[d_all] < pdy[d] + life[e, d]``. Used by Cluster B's
|
|
78
|
+
``edd_history`` triple set: the (e, d_history, d) triple is
|
|
79
|
+
kept when ``d_history`` ≤ ``d`` and within the lifetime window
|
|
80
|
+
(the "history → current" cascade in
|
|
81
|
+
``invest_divest_sets.py:241-248``). Note: this is the same as
|
|
82
|
+
:data:`BOUNDED` semantically; we expose it as a separate name so
|
|
83
|
+
that the call site is self-documenting.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
BOUNDED = "bounded"
|
|
87
|
+
UNBOUNDED_FORWARD = "unbounded_forward"
|
|
88
|
+
STRICT_LOOKBACK_BOUNDED = "strict_lookback_bounded"
|
|
89
|
+
STRICT_LOOKBACK_UNBOUNDED = "strict_lookback_unbounded"
|
|
90
|
+
BOUNDED_INCLUSIVE_LOOKBACK = "bounded_inclusive_lookback"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def period_walk_iterator(
|
|
94
|
+
source: "InputSource",
|
|
95
|
+
active_solve: str | None,
|
|
96
|
+
ed_lf: pl.LazyFrame,
|
|
97
|
+
period_in_use: list[str],
|
|
98
|
+
period_universe: list[str],
|
|
99
|
+
*,
|
|
100
|
+
window_method: WindowMethod,
|
|
101
|
+
life_lf: pl.LazyFrame | None,
|
|
102
|
+
factor_side: str | None,
|
|
103
|
+
workdir = None,
|
|
104
|
+
) -> pl.LazyFrame:
|
|
105
|
+
"""Lazy per-(e, d) walk over ``period_in_use``, gated by lifetime.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
source
|
|
110
|
+
``InputSource`` for the cluster's data (used to read
|
|
111
|
+
``p_years_d`` / yearly inflation factors via lazy helpers
|
|
112
|
+
in ``_derived_npv``).
|
|
113
|
+
active_solve
|
|
114
|
+
Active solve name for ``p_years_d`` resolution.
|
|
115
|
+
ed_lf
|
|
116
|
+
Lazy frame ``[e, d, ...]`` enumerating entity-period anchor
|
|
117
|
+
pairs. Extra columns are dropped — only ``e``, ``d`` survive.
|
|
118
|
+
period_in_use
|
|
119
|
+
Active-solve dispatch periods over which ``d_all`` ranges.
|
|
120
|
+
period_universe
|
|
121
|
+
Period universe for the inflation factor computation
|
|
122
|
+
(typically ``periodAll``). Only consulted when
|
|
123
|
+
``factor_side`` is set.
|
|
124
|
+
window_method
|
|
125
|
+
See :class:`WindowMethod`.
|
|
126
|
+
life_lf
|
|
127
|
+
Lazy ``[e, d, life]`` providing the lifetime per (e, d).
|
|
128
|
+
Required for any bounded variant; ignored for
|
|
129
|
+
:data:`WindowMethod.UNBOUNDED_FORWARD`.
|
|
130
|
+
factor_side
|
|
131
|
+
``"inv"`` / ``"ops"`` selects the per-d_all inflation factor
|
|
132
|
+
and aggregates ``Σ_{d_all matching} factor[d_all]`` per (e, d)
|
|
133
|
+
(returns ``[e, d, factor]``). ``None`` returns the unaggregated
|
|
134
|
+
``[e, d, d_all]`` triple frame (set-shape, used by Cluster B).
|
|
135
|
+
workdir
|
|
136
|
+
Optional ``Path`` for resolving ``p_years_d`` from
|
|
137
|
+
``solve_data/p_years_d.csv`` (preferred when present — it's
|
|
138
|
+
the canonical post-preprocessing CSV that already encodes the
|
|
139
|
+
cumulative year offset for the active solve). Cluster A's
|
|
140
|
+
callers don't pass this (they're called from the apply_npv
|
|
141
|
+
boundary which has consumed the workdir already); Cluster B's
|
|
142
|
+
invest-history callers do.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
Lazy frame. Schema depends on ``factor_side``:
|
|
147
|
+
|
|
148
|
+
* ``factor_side="inv" | "ops"`` → ``[e, d, factor]``.
|
|
149
|
+
* ``factor_side=None`` → ``[e, d, d_all]`` (one row per matching
|
|
150
|
+
triple).
|
|
151
|
+
"""
|
|
152
|
+
from flextool.engine_polars._axis_enums import empty_like
|
|
153
|
+
if not period_in_use:
|
|
154
|
+
if factor_side is None:
|
|
155
|
+
return empty_like(ed_lf, ["e", "d"],
|
|
156
|
+
extra={"d_all": ed_lf.collect_schema().get(
|
|
157
|
+
"d", pl.Utf8)},
|
|
158
|
+
lazy=True)
|
|
159
|
+
return ed_lf.select("e", "d").with_columns(
|
|
160
|
+
factor=pl.lit(0.0, dtype=pl.Float64))
|
|
161
|
+
# Lazy import to avoid circular dependency at module-load time.
|
|
162
|
+
from ._derived_params import _p_years_d_lf
|
|
163
|
+
pyd_lf = _p_years_d_lf(source, active_solve, workdir)
|
|
164
|
+
if pyd_lf is None:
|
|
165
|
+
# Without years offsets, the integral collapses to 0 / no rows.
|
|
166
|
+
if factor_side is None:
|
|
167
|
+
return empty_like(ed_lf, ["e", "d"],
|
|
168
|
+
extra={"d_all": ed_lf.collect_schema().get(
|
|
169
|
+
"d", pl.Utf8)},
|
|
170
|
+
lazy=True)
|
|
171
|
+
return ed_lf.select("e", "d").with_columns(
|
|
172
|
+
factor=pl.lit(0.0, dtype=pl.Float64))
|
|
173
|
+
|
|
174
|
+
bounded_methods = {
|
|
175
|
+
WindowMethod.BOUNDED,
|
|
176
|
+
WindowMethod.STRICT_LOOKBACK_BOUNDED,
|
|
177
|
+
WindowMethod.BOUNDED_INCLUSIVE_LOOKBACK,
|
|
178
|
+
}
|
|
179
|
+
if window_method in bounded_methods and life_lf is None:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"life_lf is required for window_method={window_method.value}"
|
|
182
|
+
)
|
|
183
|
+
# STRICT_LOOKBACK_UNBOUNDED is permitted with life_lf=None
|
|
184
|
+
# (the strict-lookback predicate doesn't reference lifetime).
|
|
185
|
+
|
|
186
|
+
# Anchor years.
|
|
187
|
+
pyd_anchor = pyd_lf.pipe(rename_to_axis, {"d": "d", "yr": "yr_d"})
|
|
188
|
+
# d_all years.
|
|
189
|
+
pyd_all = pyd_lf.rename({"d": "d_all", "yr": "yr_dall"})
|
|
190
|
+
# ``period_in_use`` is a plain ``list[str]``; the cross join with
|
|
191
|
+
# ``ed_lf`` (which may carry Enum-typed ``d``) and the subsequent
|
|
192
|
+
# joins against ``pyd_*`` (CSV-read String) need a single dtype.
|
|
193
|
+
# Cast ``d_all`` to match ``ed_lf.d``'s dtype, then push the same
|
|
194
|
+
# dtype onto ``pyd_anchor.d`` / ``pyd_all.d_all`` so every join key
|
|
195
|
+
# is consistent.
|
|
196
|
+
ed_d_dtype = ed_lf.collect_schema().get("d", pl.Utf8)
|
|
197
|
+
piu_lf = pl.LazyFrame({"d_all": period_in_use}).with_columns(
|
|
198
|
+
pl.col("d_all").cast(ed_d_dtype, strict=False))
|
|
199
|
+
if ed_d_dtype != pl.Utf8:
|
|
200
|
+
pyd_anchor = pyd_anchor.with_columns(
|
|
201
|
+
pl.col("d").cast(ed_d_dtype, strict=False))
|
|
202
|
+
pyd_all = pyd_all.with_columns(
|
|
203
|
+
pl.col("d_all").cast(ed_d_dtype, strict=False))
|
|
204
|
+
|
|
205
|
+
walk = (ed_lf
|
|
206
|
+
.select("e", "d")
|
|
207
|
+
.join(piu_lf, how="cross")
|
|
208
|
+
.join(pyd_anchor, on="d", how="left")
|
|
209
|
+
.join(pyd_all, on="d_all", how="left")
|
|
210
|
+
.with_columns(
|
|
211
|
+
yr_d=pl.col("yr_d").fill_null(0.0),
|
|
212
|
+
yr_dall=pl.col("yr_dall").fill_null(0.0),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if window_method == WindowMethod.UNBOUNDED_FORWARD:
|
|
217
|
+
walk = walk.filter(pl.col("yr_dall") >= pl.col("yr_d"))
|
|
218
|
+
elif window_method == WindowMethod.STRICT_LOOKBACK_UNBOUNDED:
|
|
219
|
+
walk = walk.filter(pl.col("yr_dall") > pl.col("yr_d"))
|
|
220
|
+
elif window_method == WindowMethod.STRICT_LOOKBACK_BOUNDED:
|
|
221
|
+
# Align life_lf's dim-column dtypes to walk's before joining.
|
|
222
|
+
walk_schema = walk.collect_schema()
|
|
223
|
+
life_lf = life_lf.with_columns(
|
|
224
|
+
pl.col("e").cast(walk_schema.get("e", pl.Utf8), strict=False),
|
|
225
|
+
pl.col("d").cast(walk_schema.get("d", pl.Utf8), strict=False))
|
|
226
|
+
walk = (walk
|
|
227
|
+
.join(life_lf, on=["e", "d"], how="left")
|
|
228
|
+
.with_columns(life=pl.col("life").fill_null(0.0))
|
|
229
|
+
.filter(pl.col("yr_dall") > pl.col("yr_d"))
|
|
230
|
+
.filter(pl.col("yr_dall") < pl.col("yr_d") + pl.col("life"))
|
|
231
|
+
)
|
|
232
|
+
else: # BOUNDED / BOUNDED_INCLUSIVE_LOOKBACK
|
|
233
|
+
walk_schema = walk.collect_schema()
|
|
234
|
+
life_lf = life_lf.with_columns(
|
|
235
|
+
pl.col("e").cast(walk_schema.get("e", pl.Utf8), strict=False),
|
|
236
|
+
pl.col("d").cast(walk_schema.get("d", pl.Utf8), strict=False))
|
|
237
|
+
walk = (walk
|
|
238
|
+
.join(life_lf, on=["e", "d"], how="left")
|
|
239
|
+
.with_columns(life=pl.col("life").fill_null(0.0))
|
|
240
|
+
.filter(pl.col("yr_dall") >= pl.col("yr_d"))
|
|
241
|
+
.filter(pl.col("yr_dall") < pl.col("yr_d") + pl.col("life"))
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if factor_side is None:
|
|
245
|
+
return (walk
|
|
246
|
+
.select("e", "d", "d_all")
|
|
247
|
+
.unique())
|
|
248
|
+
|
|
249
|
+
# Inflation factor sum.
|
|
250
|
+
from ._derived_npv import _inflation_factors_lf
|
|
251
|
+
factors_lf = _inflation_factors_lf(source, active_solve, period_universe)
|
|
252
|
+
if factor_side == "inv":
|
|
253
|
+
factors_lf = factors_lf.select(
|
|
254
|
+
"d", pl.col("inv_factor").alias("factor"))
|
|
255
|
+
elif factor_side == "ops":
|
|
256
|
+
factors_lf = factors_lf.select(
|
|
257
|
+
"d", pl.col("ops_factor").alias("factor"))
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"factor_side must be 'inv', 'ops', or None; got {factor_side!r}")
|
|
261
|
+
factor_dall = factors_lf.rename({"d": "d_all"})
|
|
262
|
+
if ed_d_dtype != pl.Utf8:
|
|
263
|
+
factor_dall = factor_dall.with_columns(
|
|
264
|
+
pl.col("d_all").cast(ed_d_dtype, strict=False))
|
|
265
|
+
|
|
266
|
+
return (walk
|
|
267
|
+
.join(factor_dall, on="d_all", how="left")
|
|
268
|
+
.with_columns(factor=pl.col("factor").fill_null(1.0))
|
|
269
|
+
.group_by(["e", "d"])
|
|
270
|
+
.agg(pl.col("factor").sum().alias("factor")))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
__all__ = [
|
|
274
|
+
"WindowMethod",
|
|
275
|
+
"period_walk_iterator",
|
|
276
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""HiGHS determinism pin + advanced simplex-scaling strategy constant.
|
|
2
|
+
|
|
3
|
+
Extracted from the legacy :mod:`flextool.engine_polars.scaling` module so the
|
|
4
|
+
orchestrator can keep importing these knobs after ``scaling.py`` is retired
|
|
5
|
+
in favour of the :mod:`flextool.engine_polars.autoscale` package.
|
|
6
|
+
|
|
7
|
+
HiGHS' default-of-``choose`` for ``parallel`` / ``presolve`` / ``solver`` plus
|
|
8
|
+
a time-varying ``random_seed`` lets HiGHS pick different code paths between
|
|
9
|
+
runs. For LPs with multiple optimal vertices that flips which optimum is
|
|
10
|
+
returned, which in turn flips golden-file comparisons (e.g. the
|
|
11
|
+
``coal_wind_ev`` / ``network_coal_wind_reserve_co2_capacity_margin``
|
|
12
|
+
scenarios in ``test_scenarios.py``).
|
|
13
|
+
|
|
14
|
+
Pinned:
|
|
15
|
+
|
|
16
|
+
* ``random_seed=0`` — fully deterministic seed.
|
|
17
|
+
* ``parallel="off"`` — no multi-threading parallelism inside HiGHS.
|
|
18
|
+
This alone forces the serial dual simplex (HiGHS' default solver), so we
|
|
19
|
+
deliberately do NOT also set ``threads=1`` here. The user can override via
|
|
20
|
+
``--highs-threads N`` (N > 1), which flips ``parallel`` to ``on`` and sets
|
|
21
|
+
``threads=N`` in ``_finalise_highs_options`` — N == 1 leaves this pin
|
|
22
|
+
intact and keeps default behaviour byte-identical. HiGHS still initialises
|
|
23
|
+
a single, process-global thread scheduler on the FIRST ``Highs::run()`` of
|
|
24
|
+
the process; subsequent ``Highs`` instances that try to set ``threads`` to
|
|
25
|
+
a different value are rejected with
|
|
26
|
+
``"global scheduler has already been initialized"``. We avoid that error
|
|
27
|
+
path because ``_finalise_highs_options`` resolves ``FLEXTOOL_HIGHS_THREADS``
|
|
28
|
+
from a process-level env var set by the CLI before any solve runs — every
|
|
29
|
+
sub-solve in the cascade sees the same value, so the scheduler is
|
|
30
|
+
initialised once with the final thread count and never reconfigured.
|
|
31
|
+
``parallel=off`` (the default) is sufficient for determinism (no
|
|
32
|
+
concurrent simplex trajectories); users who opt in to N > 1 explicitly
|
|
33
|
+
accept the determinism / wall-clock trade-off.
|
|
34
|
+
* ``solver="simplex"`` — pick simplex unconditionally (avoids HiGHS' internal
|
|
35
|
+
``choose`` heuristic flipping between simplex / IPM).
|
|
36
|
+
* ``presolve="on"`` — force presolve on (vs the non-deterministic
|
|
37
|
+
``choose``). We do NOT disable presolve — turning it off makes the gate
|
|
38
|
+
~3x slower and changes a great many LP solutions; ``on`` is deterministic
|
|
39
|
+
and matches HiGHS' usual recommendation.
|
|
40
|
+
|
|
41
|
+
These propagate through the orchestrator's HiGHS-options builders to the
|
|
42
|
+
``set_solver_options(highs_options)`` call sites in
|
|
43
|
+
:mod:`flextool.engine_polars._orchestration` (both the warm path and the
|
|
44
|
+
cold path). This is the actual control point — ``tests/highs.opt`` is
|
|
45
|
+
copied into the bin-dir fixture for CLI-style runs but is NOT read by
|
|
46
|
+
``polar_high.Problem`` (which is what ``test_scenarios.py`` drives via
|
|
47
|
+
``run_chain_from_db``).
|
|
48
|
+
"""
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
DETERMINISM_OPTIONS: dict[str, object] = {
|
|
53
|
+
"random_seed": 0,
|
|
54
|
+
"parallel": "off",
|
|
55
|
+
"solver": "simplex",
|
|
56
|
+
"presolve": "on",
|
|
57
|
+
}
|
|
58
|
+
"""HiGHS solver-option keys that pin byte-deterministic LP solutions."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
SIMPLEX_SCALE_STRATEGY_ADVANCED: int = 2
|
|
62
|
+
"""HiGHS ``simplex_scale_strategy`` value for Curtis-Reid row/col scaling.
|
|
63
|
+
|
|
64
|
+
HiGHS' default (1) is basic equilibration; (2) adds Curtis-Reid which costs
|
|
65
|
+
negligibly more but handles wide coefficient spreads much better. Centralised
|
|
66
|
+
here so every call site uses the same value.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["DETERMINISM_OPTIONS", "SIMPLEX_SCALE_STRATEGY_ADVANCED"]
|