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,1353 @@
|
|
|
1
|
+
"""Cluster B — existing chain & invest sets (Δ.6).
|
|
2
|
+
|
|
3
|
+
Lazy-polars port of flextool's existing-capacity cascade and the
|
|
4
|
+
invest/divest set family. Replaces the eager-Python helpers spread
|
|
5
|
+
across :mod:`._derived_params` (``p_entity_all_existing_from_source``,
|
|
6
|
+
``ed_invest_set_from_source`` / ``ed_divest_set_from_source``,
|
|
7
|
+
``edd_invest_lookback_set_from_source``,
|
|
8
|
+
``p_entity_max_units_from_source``, etc.) and the workdir-CSV reads
|
|
9
|
+
they currently depend on.
|
|
10
|
+
|
|
11
|
+
Cluster B fields (per
|
|
12
|
+
``audit/native_data_path_design_derived_clusters.md``):
|
|
13
|
+
|
|
14
|
+
* ``entityInvest`` / ``entityDivest`` — the (e,)-shaped entity
|
|
15
|
+
projection of ``entity__invest_method`` filtered by the
|
|
16
|
+
not-allowed-method constants
|
|
17
|
+
(``preprocessing/invest_method_sets.py``). These were the trap that
|
|
18
|
+
Δ.5 caught: the eager helper derived them from
|
|
19
|
+
``ed_invest_set`` / ``ed_divest_set`` instead of from
|
|
20
|
+
``entity__invest_method``. This module derives them correctly.
|
|
21
|
+
* ``e_invest_total`` / ``e_divest_total`` — projections of the above
|
|
22
|
+
filtered by INVEST_TOTAL / RETIRE_TOTAL methods
|
|
23
|
+
(``preprocessing/invest_total_sets.py``).
|
|
24
|
+
* ``ed_invest_set`` / ``ed_divest_set`` — (e, d) cross-product gated
|
|
25
|
+
by entityInvest × period_invest, eea ≠ 0 OR capacity-constraint
|
|
26
|
+
membership, and the no_investment lifetime-expiry forbidden set.
|
|
27
|
+
* ``pd_invest_set`` / ``nd_invest_set`` / ``pd_divest_set`` /
|
|
28
|
+
``nd_divest_set`` — partitions of ``ed_invest_set`` / ``ed_divest_set``
|
|
29
|
+
by entity-class (``unit ∪ connection`` → process-side; ``node`` →
|
|
30
|
+
node-side).
|
|
31
|
+
* ``edd_history`` (and its ``_choice`` / ``_automatic`` / ``_no_invest``
|
|
32
|
+
components) — the (e, d_history, d) triple-set encoding the
|
|
33
|
+
lifetime-window walk:
|
|
34
|
+
``pdy[d] >= pdy[d_history]`` with optional bounded gate
|
|
35
|
+
``pdy[d] < pdy[d_history] + edEntity_lifetime[e, d_history]``.
|
|
36
|
+
Filtered to entities whose ``lifetime_method`` is in
|
|
37
|
+
{``reinvest_choice``, ``reinvest_automatic``, ``no_investment``}.
|
|
38
|
+
* ``edd_invest_set`` — ``edd_history`` ∩ ``ed_invest_set`` on
|
|
39
|
+
``(e, d_history)`` (i.e. the history rows that correspond to a
|
|
40
|
+
current-solve invest decision).
|
|
41
|
+
* ``edd_invest_lookback_set`` — strict-lookback variant
|
|
42
|
+
(``yr[d_invest] < yr[d]``) used by the prebuilt-capacity LHS.
|
|
43
|
+
* ``edd_divest_active`` — ``pd_divest`` × period grid filtered to
|
|
44
|
+
``yr[d_divest] <= yr[d]`` (used in cap-margin user constraint).
|
|
45
|
+
* ``p_entity_all_existing`` — per-(e, d) lifetime-cumulative existing
|
|
46
|
+
capacity. Algorithm:
|
|
47
|
+
* solve-first: pre-existing[e, d] (lifetime-gated entity.existing).
|
|
48
|
+
* later-solves: Σ_{d_history ∈ edd_history(e, d) ∧ realized}
|
|
49
|
+
p_entity_period_existing_capacity[e, d_history]
|
|
50
|
+
− p_entity_divested[e] (when e ∈ entityDivest).
|
|
51
|
+
* ``p_entity_period_existing_capacity`` — handoff carrier read from
|
|
52
|
+
``flex_data.p_entity_period_existing_capacity`` (already populated
|
|
53
|
+
by the CSV-side handoff loader); the lazy path doesn't recompute it.
|
|
54
|
+
* ``p_entity_max_units`` — per-(e, d) max unit count via the
|
|
55
|
+
unitsize cascade × the max_capacity branch fork.
|
|
56
|
+
|
|
57
|
+
The shared :func:`._derived_walks.period_walk_iterator` lifted out of
|
|
58
|
+
Cluster A's ``_derived_npv`` is consumed throughout for the lifetime-
|
|
59
|
+
window walks.
|
|
60
|
+
|
|
61
|
+
All helpers are lazy until the public ``apply_existing_chain`` boundary
|
|
62
|
+
calls ``.collect()`` once per emitted Param.
|
|
63
|
+
"""
|
|
64
|
+
from __future__ import annotations
|
|
65
|
+
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
from typing import TYPE_CHECKING
|
|
68
|
+
|
|
69
|
+
import polars as pl
|
|
70
|
+
|
|
71
|
+
from polar_high import Param
|
|
72
|
+
|
|
73
|
+
from flextool.engine_polars._axis_enums import (
|
|
74
|
+
alias_to_axis,
|
|
75
|
+
rename_to_axis,
|
|
76
|
+
schema_dtype,
|
|
77
|
+
)
|
|
78
|
+
from ._derived_walks import period_walk_iterator, WindowMethod
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Substrate handle for the cascade-wide axis enum vocabulary.
|
|
82
|
+
# Bare ``None`` here; ``cast_dim`` / ``schema_dtype`` in
|
|
83
|
+
# ``_axis_enums`` fall back to ``_LIVE_AXIS_ENUMS_CTX`` (the live
|
|
84
|
+
# ContextVar) when this is ``None``, so substrate sites pick up
|
|
85
|
+
# activation set by ``load_flextool`` automatically.
|
|
86
|
+
_enums: "dict | None" = None
|
|
87
|
+
|
|
88
|
+
if TYPE_CHECKING:
|
|
89
|
+
from flextool.engine_polars._input_source import InputSource
|
|
90
|
+
from flextool.engine_polars._solve_context import SolveContext
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# -- Method-enum constants (mirror flextool/flextool_base.dat:211-212) -------
|
|
94
|
+
|
|
95
|
+
_INVEST_NOT_ALLOWED: frozenset[str] = frozenset((
|
|
96
|
+
"not_allowed", "retire_period", "retire_total", "retire_no_limit",
|
|
97
|
+
))
|
|
98
|
+
_DIVEST_NOT_ALLOWED: frozenset[str] = frozenset((
|
|
99
|
+
"not_allowed", "invest_period", "invest_total", "invest_no_limit",
|
|
100
|
+
))
|
|
101
|
+
# preprocessing/invest_total_sets.py:25-32
|
|
102
|
+
_INVEST_TOTAL_METHODS: frozenset[str] = frozenset((
|
|
103
|
+
"invest_total", "invest_period_total",
|
|
104
|
+
"invest_retire_total", "invest_retire_period_total",
|
|
105
|
+
))
|
|
106
|
+
_DIVEST_TOTAL_METHODS: frozenset[str] = frozenset((
|
|
107
|
+
"retire_total", "retire_period_total",
|
|
108
|
+
"invest_retire_total", "invest_retire_period_total",
|
|
109
|
+
))
|
|
110
|
+
# preprocessing/invest_divest_sets.py:33-40
|
|
111
|
+
_INVEST_PERIOD_METHODS: frozenset[str] = frozenset((
|
|
112
|
+
"invest_period", "invest_period_total",
|
|
113
|
+
"invest_retire_period", "invest_retire_period_total",
|
|
114
|
+
))
|
|
115
|
+
_DIVEST_PERIOD_METHODS: frozenset[str] = frozenset((
|
|
116
|
+
"retire_period", "retire_period_total",
|
|
117
|
+
"invest_retire_period", "invest_retire_period_total",
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
# preprocessing/method_with_fallback_sets.py::_LIFETIME_METHOD_DEFAULT
|
|
121
|
+
_LIFETIME_METHOD_DEFAULT = "reinvest_automatic"
|
|
122
|
+
_LIFETIME_BOUNDED_METHODS: frozenset[str] = frozenset((
|
|
123
|
+
"reinvest_choice", "no_investment",
|
|
124
|
+
))
|
|
125
|
+
_LIFETIME_UNBOUNDED_METHODS: frozenset[str] = frozenset((
|
|
126
|
+
"reinvest_automatic",
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Building blocks (lazy frames)
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _entity_class_lf(source: "InputSource") -> pl.LazyFrame:
|
|
136
|
+
"""Lazy ``[e, ec]``: every (entity, entity_class) pairing.
|
|
137
|
+
|
|
138
|
+
Mirror of :func:`._derived_npv._entity_class_lf` (kept local to
|
|
139
|
+
avoid cross-module coupling).
|
|
140
|
+
"""
|
|
141
|
+
parts: list[pl.LazyFrame] = []
|
|
142
|
+
for ec in ("unit", "node", "connection"):
|
|
143
|
+
try:
|
|
144
|
+
df = source.entities(ec)
|
|
145
|
+
except KeyError:
|
|
146
|
+
continue
|
|
147
|
+
if df.height == 0:
|
|
148
|
+
continue
|
|
149
|
+
parts.append(df.lazy().select(
|
|
150
|
+
alias_to_axis("name", "e"),
|
|
151
|
+
pl.lit(ec).alias("ec"),
|
|
152
|
+
))
|
|
153
|
+
if not parts:
|
|
154
|
+
return pl.LazyFrame(schema={
|
|
155
|
+
"e": schema_dtype(_enums, "e"),
|
|
156
|
+
"ec": pl.Utf8,
|
|
157
|
+
})
|
|
158
|
+
return pl.concat(parts, how="vertical")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _all_entities_lf(source: "InputSource") -> pl.LazyFrame:
|
|
162
|
+
"""Lazy ``[e]`` — union of unit + node + connection."""
|
|
163
|
+
return _entity_class_lf(source).select("e").unique()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _entity_method_lf(source: "InputSource",
|
|
167
|
+
parameter_name: str,
|
|
168
|
+
) -> pl.LazyFrame:
|
|
169
|
+
"""Lazy ``[e, method]`` from ``unit/node/connection.<param>``.
|
|
170
|
+
|
|
171
|
+
``parameter_name`` ∈ {``invest_method``, ``lifetime_method``}.
|
|
172
|
+
Mirror of :func:`._derived_npv._entity_method_lf`.
|
|
173
|
+
"""
|
|
174
|
+
parts: list[pl.LazyFrame] = []
|
|
175
|
+
for ec in ("unit", "node", "connection"):
|
|
176
|
+
try:
|
|
177
|
+
df = source.parameter(ec, parameter_name)
|
|
178
|
+
except KeyError:
|
|
179
|
+
continue
|
|
180
|
+
if df.height == 0:
|
|
181
|
+
continue
|
|
182
|
+
parts.append(df.lazy().select(
|
|
183
|
+
alias_to_axis("name", "e"),
|
|
184
|
+
pl.col("value").cast(pl.Utf8, strict=False).alias("method"),
|
|
185
|
+
))
|
|
186
|
+
if not parts:
|
|
187
|
+
return pl.LazyFrame(schema={
|
|
188
|
+
"e": schema_dtype(_enums, "e"),
|
|
189
|
+
"method": pl.Utf8,
|
|
190
|
+
})
|
|
191
|
+
return pl.concat(parts, how="vertical")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _lifetime_method_with_default_lf(
|
|
195
|
+
source: "InputSource",
|
|
196
|
+
all_entities_lf: pl.LazyFrame,
|
|
197
|
+
) -> pl.LazyFrame:
|
|
198
|
+
"""Lazy ``[e, method]`` for ``lifetime_method`` with default fill.
|
|
199
|
+
|
|
200
|
+
Entities without an explicit ``lifetime_method`` row get
|
|
201
|
+
``reinvest_automatic``. Mirror of
|
|
202
|
+
:func:`._derived_npv._lifetime_method_with_default_lf`.
|
|
203
|
+
"""
|
|
204
|
+
explicit = _entity_method_lf(source, "lifetime_method")
|
|
205
|
+
explicit_entities = explicit.select("e").unique()
|
|
206
|
+
default_e = (all_entities_lf
|
|
207
|
+
.join(explicit_entities, on="e", how="anti")
|
|
208
|
+
.with_columns(method=pl.lit(_LIFETIME_METHOD_DEFAULT)))
|
|
209
|
+
return pl.concat([explicit, default_e], how="vertical")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _per_entity_param_lf(source: "InputSource",
|
|
213
|
+
parameter_name: str,
|
|
214
|
+
) -> pl.LazyFrame:
|
|
215
|
+
"""Lazy ``[e, d, value, is_scalar]``.
|
|
216
|
+
|
|
217
|
+
Mirror of :func:`._derived_npv._per_entity_param_lf`.
|
|
218
|
+
|
|
219
|
+
Period-dim detection: a per-period parameter exposes exactly one
|
|
220
|
+
index column alongside ``name`` / ``value``. The canonical name is
|
|
221
|
+
``period``, but the source may surface the column under the user's
|
|
222
|
+
``Map.index_name`` (e.g. ``x``) when the spinedb default is
|
|
223
|
+
overridden. Treat any single non-``name``/``value`` column as the
|
|
224
|
+
period dim — checking only ``"period" in cols`` mis-classifies
|
|
225
|
+
user-renamed indices as scalars, which then explode through
|
|
226
|
+
:func:`_resolve_per_period_lf`'s ``scalar.join(on="e")``.
|
|
227
|
+
"""
|
|
228
|
+
parts: list[pl.LazyFrame] = []
|
|
229
|
+
for ec in ("unit", "node", "connection"):
|
|
230
|
+
try:
|
|
231
|
+
df = source.parameter(ec, parameter_name)
|
|
232
|
+
except KeyError:
|
|
233
|
+
continue
|
|
234
|
+
if df.height == 0:
|
|
235
|
+
continue
|
|
236
|
+
cols = df.columns
|
|
237
|
+
# Any extra column beyond name / value is the per-period index.
|
|
238
|
+
# ``existing`` / ``lifetime`` / ``virtual_unitsize`` have at
|
|
239
|
+
# most one such extra column; fall back to ``period`` when
|
|
240
|
+
# present (typical), otherwise pick the first non-name/value
|
|
241
|
+
# column (covers ``x`` / other user-renamed Map indices).
|
|
242
|
+
extra = [c for c in cols if c not in ("name", "value")]
|
|
243
|
+
period_col = "period" if "period" in extra else (extra[0] if extra else None)
|
|
244
|
+
if period_col is not None:
|
|
245
|
+
# Per-ROW scalar detection — see the matching helper in
|
|
246
|
+
# ``_derived_npv._per_entity_param_lf`` for the full rationale.
|
|
247
|
+
# A constant ``existing`` (etc.) stored in an entity class
|
|
248
|
+
# that ALSO carries period-Map values for sibling entities
|
|
249
|
+
# surfaces with a NULL index (``x = null``). That row must
|
|
250
|
+
# broadcast across the period universe (``is_scalar=True``),
|
|
251
|
+
# not be emitted as an explicit ``(e, d=null)`` row that
|
|
252
|
+
# never joins the period grid and silently zeroes out.
|
|
253
|
+
parts.append(df.lazy().select(
|
|
254
|
+
alias_to_axis("name", "e"),
|
|
255
|
+
alias_to_axis(period_col, "d"),
|
|
256
|
+
pl.col("value").cast(pl.Float64, strict=False),
|
|
257
|
+
pl.col(period_col).is_null().alias("is_scalar"),
|
|
258
|
+
))
|
|
259
|
+
else:
|
|
260
|
+
parts.append(df.lazy().select(
|
|
261
|
+
alias_to_axis("name", "e"),
|
|
262
|
+
pl.lit(None, dtype=schema_dtype(_enums, "d")).alias("d"),
|
|
263
|
+
pl.col("value").cast(pl.Float64, strict=False),
|
|
264
|
+
pl.lit(True).alias("is_scalar"),
|
|
265
|
+
))
|
|
266
|
+
if not parts:
|
|
267
|
+
return pl.LazyFrame(schema={
|
|
268
|
+
"e": schema_dtype(_enums, "e"),
|
|
269
|
+
"d": schema_dtype(_enums, "d"),
|
|
270
|
+
"value": pl.Float64, "is_scalar": pl.Boolean,
|
|
271
|
+
})
|
|
272
|
+
return pl.concat(parts, how="vertical")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _resolve_per_period_lf(per_param: pl.LazyFrame,
|
|
276
|
+
ed_lf: pl.LazyFrame,
|
|
277
|
+
fill: float = 0.0,
|
|
278
|
+
) -> pl.LazyFrame:
|
|
279
|
+
"""Resolve ``pdX[e, param, d]`` cascade in lazy form.
|
|
280
|
+
|
|
281
|
+
Mirror of :func:`._derived_npv._resolve_per_period_lf`.
|
|
282
|
+
"""
|
|
283
|
+
# Align ``per_param``'s ``e`` / ``d`` dtypes to ``ed_lf``'s before
|
|
284
|
+
# joining — ``per_param`` was built from a String-typed CSV read
|
|
285
|
+
# while ``ed_lf`` may now carry Enum-cast columns.
|
|
286
|
+
ed_schema = ed_lf.collect_schema()
|
|
287
|
+
e_dt = ed_schema.get("e", pl.Utf8)
|
|
288
|
+
d_dt = ed_schema.get("d", pl.Utf8)
|
|
289
|
+
if e_dt != pl.Utf8 or d_dt != pl.Utf8:
|
|
290
|
+
per_param = per_param.with_columns(
|
|
291
|
+
pl.col("e").cast(e_dt, strict=False),
|
|
292
|
+
pl.col("d").cast(d_dt, strict=False),
|
|
293
|
+
)
|
|
294
|
+
explicit = (per_param
|
|
295
|
+
.filter(~pl.col("is_scalar"))
|
|
296
|
+
.select("e", "d", pl.col("value").alias("v_explicit")))
|
|
297
|
+
scalar = (per_param
|
|
298
|
+
.filter(pl.col("is_scalar"))
|
|
299
|
+
.select("e", pl.col("value").alias("v_scalar")))
|
|
300
|
+
return (ed_lf
|
|
301
|
+
.join(explicit, on=["e", "d"], how="left")
|
|
302
|
+
.join(scalar, on="e", how="left")
|
|
303
|
+
.with_columns(
|
|
304
|
+
value=pl.coalesce(
|
|
305
|
+
pl.col("v_explicit"),
|
|
306
|
+
pl.col("v_scalar"),
|
|
307
|
+
pl.lit(fill, dtype=pl.Float64),
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
.drop("v_explicit", "v_scalar"))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# §3.7.0 — entityInvest / entityDivest / e_invest_total / e_divest_total
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def entity_invest_set_lf(source: "InputSource") -> pl.LazyFrame:
|
|
319
|
+
"""Lazy ``[e]`` for ``entityInvest``.
|
|
320
|
+
|
|
321
|
+
Mirrors flextool's
|
|
322
|
+
``preprocessing/invest_method_sets.py::write_invest_method_sets``::
|
|
323
|
+
|
|
324
|
+
entityInvest = {e : ∃m s.t. (e, m) ∈ entity__invest_method
|
|
325
|
+
AND m ∉ invest_method_not_allowed}
|
|
326
|
+
|
|
327
|
+
The Δ.5 ``ed_*``-derivation trap: an entity may have an
|
|
328
|
+
``invest_method`` row but no ``ed_invest_set`` row (e.g. eea == 0
|
|
329
|
+
AND no capacity constraint). Such an entity is in ``entityInvest``
|
|
330
|
+
but not in any ``ed_invest`` (e, d) pair. Cluster A's
|
|
331
|
+
``ed_lifetime_fixed_cost_divest`` produces rows for these entities;
|
|
332
|
+
deriving from ``ed_*`` would miss them.
|
|
333
|
+
"""
|
|
334
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
335
|
+
return (methods_lf
|
|
336
|
+
.filter(~pl.col("method").is_in(list(_INVEST_NOT_ALLOWED)))
|
|
337
|
+
.select("e")
|
|
338
|
+
.unique())
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def entity_divest_set_lf(source: "InputSource") -> pl.LazyFrame:
|
|
342
|
+
"""Lazy ``[e]`` for ``entityDivest``. Symmetric of
|
|
343
|
+
:func:`entity_invest_set_lf` with the divest-not-allowed filter."""
|
|
344
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
345
|
+
return (methods_lf
|
|
346
|
+
.filter(~pl.col("method").is_in(list(_DIVEST_NOT_ALLOWED)))
|
|
347
|
+
.select("e")
|
|
348
|
+
.unique())
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def e_invest_total_lf(source: "InputSource") -> pl.LazyFrame:
|
|
352
|
+
"""Lazy ``[e]`` for ``e_invest_total``.
|
|
353
|
+
|
|
354
|
+
Mirrors ``preprocessing/invest_total_sets.py:74-90``:
|
|
355
|
+
|
|
356
|
+
e_invest_total = entityInvest filtered by invest_method ∈
|
|
357
|
+
{invest_total, invest_period_total,
|
|
358
|
+
invest_retire_total,
|
|
359
|
+
invest_retire_period_total}
|
|
360
|
+
"""
|
|
361
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
362
|
+
invest_universe = entity_invest_set_lf(source)
|
|
363
|
+
has_total = (methods_lf
|
|
364
|
+
.filter(pl.col("method").is_in(list(_INVEST_TOTAL_METHODS)))
|
|
365
|
+
.select("e").unique())
|
|
366
|
+
return invest_universe.join(has_total, on="e", how="inner")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def e_divest_total_lf(source: "InputSource") -> pl.LazyFrame:
|
|
370
|
+
"""Lazy ``[e]`` for ``e_divest_total``. Symmetric of
|
|
371
|
+
:func:`e_invest_total_lf` with the retire-total method filter."""
|
|
372
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
373
|
+
divest_universe = entity_divest_set_lf(source)
|
|
374
|
+
has_total = (methods_lf
|
|
375
|
+
.filter(pl.col("method").is_in(list(_DIVEST_TOTAL_METHODS)))
|
|
376
|
+
.select("e").unique())
|
|
377
|
+
return divest_universe.join(has_total, on="e", how="inner")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# §3.7.2 — edd_history triple-set walk + variants
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def edd_history_choice_lf(source: "InputSource",
|
|
386
|
+
active_solve: str | None,
|
|
387
|
+
period_with_history: list[str],
|
|
388
|
+
period_in_use: list[str],
|
|
389
|
+
workdir: Path | None = None,
|
|
390
|
+
) -> pl.LazyFrame:
|
|
391
|
+
"""Lazy ``[e, d_history, d]`` for ``edd_history_choice``.
|
|
392
|
+
|
|
393
|
+
Mirror of ``invest_divest_sets.py:228-244`` for entities whose
|
|
394
|
+
``lifetime_method = reinvest_choice``::
|
|
395
|
+
|
|
396
|
+
keep iff pdy[d] >= pdy[d_h] AND pdy[d] < pdy[d_h] + life[e, d_h]
|
|
397
|
+
"""
|
|
398
|
+
return _edd_history_lf_for(
|
|
399
|
+
source, active_solve, period_with_history, period_in_use,
|
|
400
|
+
method="reinvest_choice", bounded=True, workdir=workdir)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def edd_history_automatic_lf(source: "InputSource",
|
|
404
|
+
active_solve: str | None,
|
|
405
|
+
period_with_history: list[str],
|
|
406
|
+
period_in_use: list[str],
|
|
407
|
+
workdir: Path | None = None,
|
|
408
|
+
) -> pl.LazyFrame:
|
|
409
|
+
"""Lazy ``[e, d_history, d]`` for ``edd_history_automatic`` —
|
|
410
|
+
entities with ``lifetime_method = reinvest_automatic``.
|
|
411
|
+
|
|
412
|
+
Mirror of ``invest_divest_sets.py:245-246``::
|
|
413
|
+
|
|
414
|
+
keep iff pdy[d] >= pdy[d_h]
|
|
415
|
+
"""
|
|
416
|
+
return _edd_history_lf_for(
|
|
417
|
+
source, active_solve, period_with_history, period_in_use,
|
|
418
|
+
method="reinvest_automatic", bounded=False, workdir=workdir)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def edd_history_no_investment_lf(source: "InputSource",
|
|
422
|
+
active_solve: str | None,
|
|
423
|
+
period_with_history: list[str],
|
|
424
|
+
period_in_use: list[str],
|
|
425
|
+
workdir: Path | None = None,
|
|
426
|
+
) -> pl.LazyFrame:
|
|
427
|
+
"""Lazy ``[e, d_history, d]`` for ``edd_history_no_investment``.
|
|
428
|
+
|
|
429
|
+
Mirror of ``invest_divest_sets.py:247-248``: same predicate as
|
|
430
|
+
``edd_history_choice`` but for the ``no_investment`` cohort.
|
|
431
|
+
"""
|
|
432
|
+
return _edd_history_lf_for(
|
|
433
|
+
source, active_solve, period_with_history, period_in_use,
|
|
434
|
+
method="no_investment", bounded=True, workdir=workdir)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _edd_history_lf_for(source: "InputSource",
|
|
438
|
+
active_solve: str | None,
|
|
439
|
+
period_with_history: list[str],
|
|
440
|
+
period_in_use: list[str],
|
|
441
|
+
*,
|
|
442
|
+
method: str,
|
|
443
|
+
bounded: bool,
|
|
444
|
+
workdir: Path | None = None,
|
|
445
|
+
) -> pl.LazyFrame:
|
|
446
|
+
"""Internal helper: build edd_history sub-set for a single
|
|
447
|
+
lifetime_method cohort.
|
|
448
|
+
"""
|
|
449
|
+
if not period_with_history or not period_in_use:
|
|
450
|
+
return pl.LazyFrame(schema={
|
|
451
|
+
"e": schema_dtype(_enums, "e"),
|
|
452
|
+
"d_history": schema_dtype(_enums, "d"),
|
|
453
|
+
"d": schema_dtype(_enums, "d"),
|
|
454
|
+
})
|
|
455
|
+
all_e_lf = _all_entities_lf(source)
|
|
456
|
+
elm_lf = _lifetime_method_with_default_lf(source, all_e_lf)
|
|
457
|
+
cohort_e = (elm_lf
|
|
458
|
+
.filter(pl.col("method") == method)
|
|
459
|
+
.select("e").unique())
|
|
460
|
+
pwh_lf = (pl.LazyFrame({"d": period_with_history})
|
|
461
|
+
.with_columns(pl.col("d").cast(schema_dtype(_enums, "d"),
|
|
462
|
+
strict=False)))
|
|
463
|
+
anchor = cohort_e.join(pwh_lf, how="cross")
|
|
464
|
+
if anchor.collect().height == 0:
|
|
465
|
+
return pl.LazyFrame(schema={
|
|
466
|
+
"e": schema_dtype(_enums, "e"),
|
|
467
|
+
"d_history": schema_dtype(_enums, "d"),
|
|
468
|
+
"d": schema_dtype(_enums, "d"),
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
if bounded:
|
|
472
|
+
life_per = _per_entity_param_lf(source, "lifetime")
|
|
473
|
+
life_lf = (_resolve_per_period_lf(life_per, anchor, fill=0.0)
|
|
474
|
+
.rename({"value": "life"})
|
|
475
|
+
.select("e", "d", "life"))
|
|
476
|
+
walk = period_walk_iterator(
|
|
477
|
+
source, active_solve, anchor,
|
|
478
|
+
period_in_use, period_in_use,
|
|
479
|
+
window_method=WindowMethod.BOUNDED_INCLUSIVE_LOOKBACK,
|
|
480
|
+
life_lf=life_lf, factor_side=None,
|
|
481
|
+
workdir=workdir)
|
|
482
|
+
else:
|
|
483
|
+
walk = period_walk_iterator(
|
|
484
|
+
source, active_solve, anchor,
|
|
485
|
+
period_in_use, period_in_use,
|
|
486
|
+
window_method=WindowMethod.UNBOUNDED_FORWARD,
|
|
487
|
+
life_lf=None, factor_side=None,
|
|
488
|
+
workdir=workdir)
|
|
489
|
+
return walk.pipe(rename_to_axis, {"d": "d_history", "d_all": "d"})
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def edd_history_lf(source: "InputSource",
|
|
493
|
+
active_solve: str | None,
|
|
494
|
+
period_with_history: list[str],
|
|
495
|
+
period_in_use: list[str],
|
|
496
|
+
workdir: Path | None = None,
|
|
497
|
+
) -> pl.LazyFrame:
|
|
498
|
+
"""Lazy ``[e, d_history, d]`` for the union ``edd_history`` set.
|
|
499
|
+
|
|
500
|
+
Algorithm (mirror of
|
|
501
|
+
``invest_divest_sets.py:227-262``): union of the three cohort
|
|
502
|
+
walks (:func:`edd_history_choice_lf`,
|
|
503
|
+
:func:`edd_history_automatic_lf`,
|
|
504
|
+
:func:`edd_history_no_investment_lf`).
|
|
505
|
+
"""
|
|
506
|
+
if not period_with_history or not period_in_use:
|
|
507
|
+
return pl.LazyFrame(schema={
|
|
508
|
+
"e": schema_dtype(_enums, "e"),
|
|
509
|
+
"d_history": schema_dtype(_enums, "d"),
|
|
510
|
+
"d": schema_dtype(_enums, "d"),
|
|
511
|
+
})
|
|
512
|
+
parts = [
|
|
513
|
+
edd_history_choice_lf(source, active_solve,
|
|
514
|
+
period_with_history, period_in_use,
|
|
515
|
+
workdir),
|
|
516
|
+
edd_history_automatic_lf(source, active_solve,
|
|
517
|
+
period_with_history, period_in_use,
|
|
518
|
+
workdir),
|
|
519
|
+
edd_history_no_investment_lf(source, active_solve,
|
|
520
|
+
period_with_history, period_in_use,
|
|
521
|
+
workdir),
|
|
522
|
+
]
|
|
523
|
+
return pl.concat(parts, how="vertical").unique()
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
# §3.7.1 — pd_invest_set / nd_invest_set partitions
|
|
528
|
+
# ---------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _entity_class_partition_lf(source: "InputSource") -> pl.LazyFrame:
|
|
532
|
+
"""Lazy ``[e, kind]`` where ``kind ∈ {process, node}``.
|
|
533
|
+
|
|
534
|
+
process = unit ∪ connection (processes); node = node. Disjoint
|
|
535
|
+
classes — every entity falls in exactly one.
|
|
536
|
+
"""
|
|
537
|
+
parts: list[pl.LazyFrame] = []
|
|
538
|
+
for ec, kind in (("unit", "process"),
|
|
539
|
+
("connection", "process"),
|
|
540
|
+
("node", "node")):
|
|
541
|
+
try:
|
|
542
|
+
df = source.entities(ec)
|
|
543
|
+
except KeyError:
|
|
544
|
+
continue
|
|
545
|
+
if df.height == 0:
|
|
546
|
+
continue
|
|
547
|
+
parts.append(df.lazy().select(
|
|
548
|
+
alias_to_axis("name", "e"),
|
|
549
|
+
pl.lit(kind).alias("kind"),
|
|
550
|
+
))
|
|
551
|
+
if not parts:
|
|
552
|
+
return pl.LazyFrame(schema={
|
|
553
|
+
"e": schema_dtype(_enums, "e"),
|
|
554
|
+
"kind": pl.Utf8,
|
|
555
|
+
})
|
|
556
|
+
return pl.concat(parts, how="vertical").unique()
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def pd_invest_set_lf(source: "InputSource",
|
|
560
|
+
ed_invest_lf: pl.LazyFrame,
|
|
561
|
+
) -> pl.LazyFrame:
|
|
562
|
+
"""Lazy ``[p, d]`` — ``ed_invest_set`` partitioned to processes
|
|
563
|
+
(``unit ∪ connection``). Mirror of
|
|
564
|
+
``invest_divest_sets.py:215-217``.
|
|
565
|
+
"""
|
|
566
|
+
cls_lf = _entity_class_partition_lf(source)
|
|
567
|
+
return (ed_invest_lf
|
|
568
|
+
.join(cls_lf, on="e", how="inner")
|
|
569
|
+
.filter(pl.col("kind") == "process")
|
|
570
|
+
.pipe(rename_to_axis, {"e": "p"})
|
|
571
|
+
.select("p", "d")
|
|
572
|
+
.unique()
|
|
573
|
+
.sort("p", "d"))
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def nd_invest_set_lf(source: "InputSource",
|
|
577
|
+
ed_invest_lf: pl.LazyFrame,
|
|
578
|
+
) -> pl.LazyFrame:
|
|
579
|
+
"""Lazy ``[n, d]`` — ``ed_invest_set`` partitioned to nodes."""
|
|
580
|
+
cls_lf = _entity_class_partition_lf(source)
|
|
581
|
+
return (ed_invest_lf
|
|
582
|
+
.join(cls_lf, on="e", how="inner")
|
|
583
|
+
.filter(pl.col("kind") == "node")
|
|
584
|
+
.pipe(rename_to_axis, {"e": "n"})
|
|
585
|
+
.select("n", "d")
|
|
586
|
+
.unique()
|
|
587
|
+
.sort("n", "d"))
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def pd_divest_set_lf(source: "InputSource",
|
|
591
|
+
ed_divest_lf: pl.LazyFrame,
|
|
592
|
+
) -> pl.LazyFrame:
|
|
593
|
+
"""Lazy ``[p, d]`` — ``ed_divest_set`` partitioned to processes."""
|
|
594
|
+
cls_lf = _entity_class_partition_lf(source)
|
|
595
|
+
return (ed_divest_lf
|
|
596
|
+
.join(cls_lf, on="e", how="inner")
|
|
597
|
+
.filter(pl.col("kind") == "process")
|
|
598
|
+
.pipe(rename_to_axis, {"e": "p"})
|
|
599
|
+
.select("p", "d")
|
|
600
|
+
.unique()
|
|
601
|
+
.sort("p", "d"))
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def nd_divest_set_lf(source: "InputSource",
|
|
605
|
+
ed_divest_lf: pl.LazyFrame,
|
|
606
|
+
) -> pl.LazyFrame:
|
|
607
|
+
"""Lazy ``[n, d]`` — ``ed_divest_set`` partitioned to nodes."""
|
|
608
|
+
cls_lf = _entity_class_partition_lf(source)
|
|
609
|
+
return (ed_divest_lf
|
|
610
|
+
.join(cls_lf, on="e", how="inner")
|
|
611
|
+
.filter(pl.col("kind") == "node")
|
|
612
|
+
.pipe(rename_to_axis, {"e": "n"})
|
|
613
|
+
.select("n", "d")
|
|
614
|
+
.unique()
|
|
615
|
+
.sort("n", "d"))
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def ed_invest_period_set_lf(source: "InputSource",
|
|
619
|
+
ed_invest_lf: pl.LazyFrame,
|
|
620
|
+
) -> pl.LazyFrame:
|
|
621
|
+
"""Lazy ``[e, d]`` — ``ed_invest`` filtered to entities whose
|
|
622
|
+
``invest_method`` is in :data:`_INVEST_PERIOD_METHODS`.
|
|
623
|
+
|
|
624
|
+
Mirror of ``preprocessing/invest_divest_sets.py`` 's
|
|
625
|
+
``ed_invest_period`` set: keeps only the (e, d) pairs that carry
|
|
626
|
+
a per-period invest cap (``maxInvest_entity_period`` /
|
|
627
|
+
``minInvest_entity_period`` constraint indices). Used in
|
|
628
|
+
``model.py:1517-1530`` to index the per-period invest cap rows.
|
|
629
|
+
"""
|
|
630
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
631
|
+
period_e = (methods_lf
|
|
632
|
+
.filter(pl.col("method").is_in(list(_INVEST_PERIOD_METHODS)))
|
|
633
|
+
.select("e")
|
|
634
|
+
.unique())
|
|
635
|
+
return (ed_invest_lf
|
|
636
|
+
.join(period_e, on="e", how="inner")
|
|
637
|
+
.select("e", "d")
|
|
638
|
+
.unique()
|
|
639
|
+
.sort("e", "d"))
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def ed_divest_period_set_lf(source: "InputSource",
|
|
643
|
+
ed_divest_lf: pl.LazyFrame,
|
|
644
|
+
) -> pl.LazyFrame:
|
|
645
|
+
"""Lazy ``[e, d]`` — ``ed_divest`` filtered to entities whose
|
|
646
|
+
``invest_method`` is in :data:`_DIVEST_PERIOD_METHODS`. Mirror of
|
|
647
|
+
:func:`ed_invest_period_set_lf` for divest.
|
|
648
|
+
"""
|
|
649
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
650
|
+
period_e = (methods_lf
|
|
651
|
+
.filter(pl.col("method").is_in(list(_DIVEST_PERIOD_METHODS)))
|
|
652
|
+
.select("e")
|
|
653
|
+
.unique())
|
|
654
|
+
return (ed_divest_lf
|
|
655
|
+
.join(period_e, on="e", how="inner")
|
|
656
|
+
.select("e", "d")
|
|
657
|
+
.unique()
|
|
658
|
+
.sort("e", "d"))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
# ---------------------------------------------------------------------------
|
|
662
|
+
# §3.7.3 — edd_invest_set + edd_invest_lookback_set
|
|
663
|
+
# ---------------------------------------------------------------------------
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def edd_invest_set_lf(source: "InputSource",
|
|
667
|
+
active_solve: str | None,
|
|
668
|
+
ed_invest_lf: pl.LazyFrame,
|
|
669
|
+
period_with_history: list[str],
|
|
670
|
+
period_in_use: list[str],
|
|
671
|
+
workdir: Path | None = None,
|
|
672
|
+
) -> pl.LazyFrame:
|
|
673
|
+
"""Lazy ``[e, d_invest, d]`` — ``edd_history_invest`` filtered to
|
|
674
|
+
``(e, d_invest) ∈ ed_invest``.
|
|
675
|
+
|
|
676
|
+
Mirror of ``invest_divest_sets.py:267-270``::
|
|
677
|
+
|
|
678
|
+
edd_invest = { (e, d_inv, d) : (e, d_inv, d) ∈ edd_history,
|
|
679
|
+
(e, d_inv) ∈ ed_invest }
|
|
680
|
+
|
|
681
|
+
The ``edd_history`` triple-set carries history-period entries for
|
|
682
|
+
every entity with a recognised lifetime_method; this helper filters
|
|
683
|
+
to those whose ``d_history`` (renamed ``d_invest``) is also a
|
|
684
|
+
valid invest decision in the current solve.
|
|
685
|
+
"""
|
|
686
|
+
edd = edd_history_lf(source, active_solve,
|
|
687
|
+
period_with_history, period_in_use,
|
|
688
|
+
workdir)
|
|
689
|
+
inv_pairs = ed_invest_lf.pipe(rename_to_axis, {"d": "d_history"})
|
|
690
|
+
return (edd
|
|
691
|
+
.join(inv_pairs, on=["e", "d_history"], how="inner")
|
|
692
|
+
.pipe(rename_to_axis, {"d_history": "d_invest"})
|
|
693
|
+
.select("e", "d_invest", "d"))
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def edd_invest_lookback_set_lf(source: "InputSource",
|
|
697
|
+
active_solve: str | None,
|
|
698
|
+
ed_invest_lf: pl.LazyFrame,
|
|
699
|
+
period_in_use: list[str],
|
|
700
|
+
workdir: Path | None = None,
|
|
701
|
+
) -> pl.LazyFrame:
|
|
702
|
+
"""Lazy ``[e, d_invest, d]`` — ``edd_invest_lookback_set``.
|
|
703
|
+
|
|
704
|
+
The strict-lookback variant of :func:`edd_invest_set_lf`: tuples
|
|
705
|
+
where ``yr[d_invest] < yr[d]`` (strict) and, for entities in the
|
|
706
|
+
bounded lifetime cohort (``reinvest_choice`` / ``no_investment``),
|
|
707
|
+
``yr[d] < yr[d_invest] + lifetime[e, d_invest]``.
|
|
708
|
+
|
|
709
|
+
Mirror of ``invest_divest_sets.py:227-262`` filtered with the
|
|
710
|
+
strict-lookback predicate. Used by the prebuilt-capacity LHS in
|
|
711
|
+
user-constraint expressions (mod L2885-2898).
|
|
712
|
+
|
|
713
|
+
Δ.7 consolidation: the eager
|
|
714
|
+
:func:`._derived_params.edd_invest_lookback_set_from_source`
|
|
715
|
+
pre-dates the shared :func:`._derived_walks.period_walk_iterator`;
|
|
716
|
+
Δ.7 wraps the shared walker with the
|
|
717
|
+
:data:`._derived_walks.WindowMethod.STRICT_LOOKBACK_BOUNDED` /
|
|
718
|
+
:data:`._derived_walks.WindowMethod.STRICT_LOOKBACK_UNBOUNDED`
|
|
719
|
+
modes for bounded / unbounded entity cohorts respectively, then
|
|
720
|
+
unions the results.
|
|
721
|
+
|
|
722
|
+
The eager helper now delegates to this lazy port — the previous
|
|
723
|
+
Python ``for r in out.iter_rows`` lifetime gate is replaced with
|
|
724
|
+
a fully lazy join + filter on the shared walker.
|
|
725
|
+
"""
|
|
726
|
+
if not period_in_use:
|
|
727
|
+
return pl.LazyFrame(schema={
|
|
728
|
+
"e": pl.Utf8, "d_invest": pl.Utf8, "d": pl.Utf8,
|
|
729
|
+
})
|
|
730
|
+
inv_anchor = ed_invest_lf.select("e", alias_to_axis("d", "d_invest"))
|
|
731
|
+
if inv_anchor.collect().height == 0:
|
|
732
|
+
return pl.LazyFrame(schema={
|
|
733
|
+
"e": pl.Utf8, "d_invest": pl.Utf8, "d": pl.Utf8,
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
all_e_lf = _all_entities_lf(source)
|
|
737
|
+
elm_lf = _lifetime_method_with_default_lf(source, all_e_lf)
|
|
738
|
+
bounded_e = (elm_lf
|
|
739
|
+
.filter(pl.col("method").is_in(
|
|
740
|
+
list(_LIFETIME_BOUNDED_METHODS)))
|
|
741
|
+
.select("e").unique())
|
|
742
|
+
unbounded_e = (elm_lf
|
|
743
|
+
.filter(~pl.col("method").is_in(
|
|
744
|
+
list(_LIFETIME_BOUNDED_METHODS)))
|
|
745
|
+
.select("e").unique())
|
|
746
|
+
|
|
747
|
+
# Shape the anchor as ``[e, d]`` for the shared walker (it expects
|
|
748
|
+
# the anchor period column to be named ``d``).
|
|
749
|
+
anchor_lf = inv_anchor.pipe(rename_to_axis, {"d_invest": "d"})
|
|
750
|
+
|
|
751
|
+
# The source-driven ``_all_entities_lf`` reads raw CSV — String
|
|
752
|
+
# ``e``. ``anchor_lf.e`` carries whatever dtype the caller passed
|
|
753
|
+
# (Enum-e when called from the post-load test path). Align so the
|
|
754
|
+
# join below works.
|
|
755
|
+
anchor_e_dtype = anchor_lf.collect_schema().get("e", pl.Utf8)
|
|
756
|
+
if anchor_e_dtype != pl.Utf8:
|
|
757
|
+
bounded_e = bounded_e.with_columns(
|
|
758
|
+
pl.col("e").cast(anchor_e_dtype, strict=False))
|
|
759
|
+
unbounded_e = unbounded_e.with_columns(
|
|
760
|
+
pl.col("e").cast(anchor_e_dtype, strict=False))
|
|
761
|
+
|
|
762
|
+
# Bounded cohort.
|
|
763
|
+
bounded_anchor = anchor_lf.join(bounded_e, on="e", how="inner")
|
|
764
|
+
# Scratch frame for the cohort with no rows: must match the dtypes
|
|
765
|
+
# of the upstream Enum-cast inputs to be ``vstack``-compatible at
|
|
766
|
+
# the bottom-of-function ``pl.concat``. ``empty_like`` reads the
|
|
767
|
+
# input frame's schema for the requested columns and uses
|
|
768
|
+
# ``pl.Utf8`` as the fall-back when the column is absent.
|
|
769
|
+
from flextool.engine_polars._axis_enums import empty_like
|
|
770
|
+
bounded_walk = empty_like(anchor_lf,
|
|
771
|
+
["e", "d"],
|
|
772
|
+
extra={"d_all": anchor_lf.collect_schema().get("d", pl.Utf8)},
|
|
773
|
+
lazy=True)
|
|
774
|
+
if bounded_anchor.collect().height > 0:
|
|
775
|
+
life_per = _per_entity_param_lf(source, "lifetime")
|
|
776
|
+
life_lf = (_resolve_per_period_lf(life_per, bounded_anchor, fill=0.0)
|
|
777
|
+
.rename({"value": "life"})
|
|
778
|
+
.select("e", "d", "life"))
|
|
779
|
+
bounded_walk = period_walk_iterator(
|
|
780
|
+
source, active_solve, bounded_anchor,
|
|
781
|
+
period_in_use, period_in_use,
|
|
782
|
+
window_method=WindowMethod.STRICT_LOOKBACK_BOUNDED,
|
|
783
|
+
life_lf=life_lf, factor_side=None,
|
|
784
|
+
workdir=workdir)
|
|
785
|
+
|
|
786
|
+
# Unbounded cohort — strict-lookback only, no lifetime cap.
|
|
787
|
+
unbounded_anchor = anchor_lf.join(unbounded_e, on="e", how="inner")
|
|
788
|
+
unbounded_walk = empty_like(anchor_lf,
|
|
789
|
+
["e", "d"],
|
|
790
|
+
extra={"d_all": anchor_lf.collect_schema().get("d", pl.Utf8)},
|
|
791
|
+
lazy=True)
|
|
792
|
+
if unbounded_anchor.collect().height > 0:
|
|
793
|
+
unbounded_walk = period_walk_iterator(
|
|
794
|
+
source, active_solve, unbounded_anchor,
|
|
795
|
+
period_in_use, period_in_use,
|
|
796
|
+
window_method=WindowMethod.STRICT_LOOKBACK_UNBOUNDED,
|
|
797
|
+
life_lf=None, factor_side=None,
|
|
798
|
+
workdir=workdir)
|
|
799
|
+
|
|
800
|
+
return (pl.concat([bounded_walk, unbounded_walk], how="vertical")
|
|
801
|
+
.pipe(rename_to_axis, {"d": "d_invest", "d_all": "d"})
|
|
802
|
+
.select("e", "d_invest", "d")
|
|
803
|
+
.unique()
|
|
804
|
+
.sort("e", "d_invest", "d"))
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
# ---------------------------------------------------------------------------
|
|
808
|
+
# §3.11 — p_entity_all_existing (existing-chain walk)
|
|
809
|
+
# ---------------------------------------------------------------------------
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def p_entity_all_existing_from_handoff(
|
|
813
|
+
source: "InputSource",
|
|
814
|
+
active_solve: str | None,
|
|
815
|
+
period_with_history: list[str], # noqa: ARG001 — kept for symmetry
|
|
816
|
+
period_in_use: list[str],
|
|
817
|
+
*,
|
|
818
|
+
p_entity_period_existing_capacity: "Param | None" = None,
|
|
819
|
+
p_entity_previously_invested_capacity: "Param | None" = None,
|
|
820
|
+
p_entity_divested: "Param | None" = None,
|
|
821
|
+
solve_first: bool = True,
|
|
822
|
+
edd_history: "pl.DataFrame | None" = None,
|
|
823
|
+
) -> "Param | None":
|
|
824
|
+
"""Lazy port of flextool's
|
|
825
|
+
``write_p_entity_existing_chain`` algorithm
|
|
826
|
+
(``entity_period_calc_params.py:1381-1591``).
|
|
827
|
+
|
|
828
|
+
Per flextool's writer (lines 1554-1568):
|
|
829
|
+
|
|
830
|
+
* ``solve_first`` →
|
|
831
|
+
``all_existing[e, d] = pre_existing[e, d]`` (no divest applied)
|
|
832
|
+
* later solves →
|
|
833
|
+
``all_existing[e, d] = later_existing[e, d]``
|
|
834
|
+
`` − p_entity_divested[e] (if e ∈ entityDivest)``
|
|
835
|
+
|
|
836
|
+
where ``later_existing[e, d]`` for the chain-runner case is the
|
|
837
|
+
chain-summed prior-solve existing per current period — already
|
|
838
|
+
integrated by flextool's preprocessing into
|
|
839
|
+
``p_entity_previously_invested_capacity[e, d]`` (the
|
|
840
|
+
``later_invested`` shape) ⊕ ``pre_existing[e, d]``. Δ.11 — we rebuild
|
|
841
|
+
the result from the in-memory handoff carriers via
|
|
842
|
+
``pre + ppic − divest`` which matches the canonical flextool CSV
|
|
843
|
+
(verified per-fixture in ``test_existing_chain_cluster_parity.py``).
|
|
844
|
+
|
|
845
|
+
Parameters
|
|
846
|
+
----------
|
|
847
|
+
p_entity_period_existing_capacity
|
|
848
|
+
Δ.11 — in-memory handoff carrier ``[entity, period, value]``
|
|
849
|
+
(the SolveHandoff ``realized_existing`` shape). When supplied,
|
|
850
|
+
the helper switches into the ``later_existing`` chain-summation
|
|
851
|
+
branch: ``later_existing[e, d] = Σ_{(e, d_h, d) ∈ edd_history ∧
|
|
852
|
+
(e, d_h) realized} ppec[(e, d_h)]`` (mirrors flextool's
|
|
853
|
+
``write_p_entity_existing_chain`` lines 1530-1543). Required for
|
|
854
|
+
lifetime-renew chains where the chain-summed existing isn't
|
|
855
|
+
recoverable from ``ppic + pre_existing`` alone.
|
|
856
|
+
p_entity_previously_invested_capacity
|
|
857
|
+
In-memory handoff Param ``[e, d, value]`` — chain-summed prior
|
|
858
|
+
invest at d. Used only when ``p_entity_period_existing_capacity``
|
|
859
|
+
is None (the simple-baseline branch); the chain-summation
|
|
860
|
+
branch derives both terms from ``ppec``.
|
|
861
|
+
p_entity_divested
|
|
862
|
+
Handoff Param ``[e, value]`` cumulative prior divest.
|
|
863
|
+
solve_first
|
|
864
|
+
Δ.11 — flextool's ``solveFirst`` flag. When True, divest is
|
|
865
|
+
NOT subtracted (mirrors the writer's solve-first branch); when
|
|
866
|
+
False, ``e ∈ entityDivest`` triggers a per-entity divest
|
|
867
|
+
subtraction. Default True (single-solve / cold-start).
|
|
868
|
+
edd_history
|
|
869
|
+
Δ.11 — ``[entity, period_history, period]`` triple-set used by
|
|
870
|
+
the chain-summation branch. When None, the helper falls back
|
|
871
|
+
to the simple ``pre + ppic − divest`` formula (which is exact
|
|
872
|
+
for non-renew chains).
|
|
873
|
+
|
|
874
|
+
Returns Param ``[e, d, value]`` or None when no entities exist.
|
|
875
|
+
"""
|
|
876
|
+
if not period_in_use:
|
|
877
|
+
return None
|
|
878
|
+
all_e_collected = _all_entities_lf(source).collect()
|
|
879
|
+
if all_e_collected.height == 0:
|
|
880
|
+
return None
|
|
881
|
+
all_e_lf = _all_entities_lf(source)
|
|
882
|
+
piu_lf = (pl.LazyFrame({"d": period_in_use})
|
|
883
|
+
.with_columns(pl.col("d").cast(schema_dtype(_enums, "d"),
|
|
884
|
+
strict=False)))
|
|
885
|
+
grid_lf = all_e_lf.join(piu_lf, how="cross")
|
|
886
|
+
|
|
887
|
+
pre_existing_lf = p_entity_pre_existing_lf(
|
|
888
|
+
source, active_solve, period_in_use)
|
|
889
|
+
|
|
890
|
+
# entityDivest set + p_entity_divested scalar. Shared by both
|
|
891
|
+
# branches; the divest subtraction is gated on solve_first below.
|
|
892
|
+
div_set_lf = entity_divest_set_lf(source).with_columns(
|
|
893
|
+
is_divest=pl.lit(True))
|
|
894
|
+
if (p_entity_divested is not None
|
|
895
|
+
and p_entity_divested.frame.height > 0):
|
|
896
|
+
ped_lf = (p_entity_divested.frame.lazy()
|
|
897
|
+
.select("e", pl.col("value").alias("divested")))
|
|
898
|
+
else:
|
|
899
|
+
ped_lf = pl.LazyFrame(schema={
|
|
900
|
+
"e": schema_dtype(_enums, "e"),
|
|
901
|
+
"divested": pl.Float64,
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
# ---------------------------------------------------------------
|
|
905
|
+
# Δ.11 — chain-summation branch: later_existing[e, d] from
|
|
906
|
+
# edd_history × ppec. Activates when the caller supplies the
|
|
907
|
+
# ``p_entity_period_existing_capacity`` carrier (the SolveHandoff
|
|
908
|
+
# ``realized_existing`` shape) AND the ``edd_history`` triple-set
|
|
909
|
+
# AND solve_first is False. Mirrors flextool's writer at
|
|
910
|
+
# ``entity_period_calc_params.py:1530-1568``.
|
|
911
|
+
# ---------------------------------------------------------------
|
|
912
|
+
if (not solve_first
|
|
913
|
+
and p_entity_period_existing_capacity is not None
|
|
914
|
+
and p_entity_period_existing_capacity.frame.height > 0
|
|
915
|
+
and edd_history is not None and edd_history.height > 0):
|
|
916
|
+
ppec_lf = (p_entity_period_existing_capacity.frame.lazy()
|
|
917
|
+
.select(alias_to_axis("e", "e"),
|
|
918
|
+
alias_to_axis("d", "d_h"),
|
|
919
|
+
pl.col("value").alias("ppec")))
|
|
920
|
+
edd_lf = edd_history.lazy()
|
|
921
|
+
if {"entity", "period_history", "period"}.issubset(set(edd_history.columns)):
|
|
922
|
+
edd_lf = edd_lf.pipe(rename_to_axis, {"entity": "e",
|
|
923
|
+
"period_history": "d_h",
|
|
924
|
+
"period": "d"})
|
|
925
|
+
# later_existing[e, d] = Σ_{d_h: (e, d_h, d) ∈ edd_history ∧
|
|
926
|
+
# (e, d_h) ∈ realized} ppec[(e, d_h)].
|
|
927
|
+
# (e, d_h) is "realized" iff ppec carries a row for it.
|
|
928
|
+
from flextool.engine_polars._axis_enums import align_join_dtypes
|
|
929
|
+
edd_lf, ppec_lf = align_join_dtypes(edd_lf, ppec_lf, ["e", "d_h"])
|
|
930
|
+
later_lf = (edd_lf
|
|
931
|
+
.join(ppec_lf, on=["e", "d_h"], how="inner")
|
|
932
|
+
.group_by(["e", "d"])
|
|
933
|
+
.agg(pl.col("ppec").sum().alias("later")))
|
|
934
|
+
grid_lf, later_lf = align_join_dtypes(grid_lf, later_lf, ["e", "d"])
|
|
935
|
+
grid_lf, div_set_lf = align_join_dtypes(grid_lf, div_set_lf, ["e"])
|
|
936
|
+
grid_lf, ped_lf = align_join_dtypes(grid_lf, ped_lf, ["e"])
|
|
937
|
+
out = (grid_lf
|
|
938
|
+
.join(later_lf, on=["e", "d"], how="left")
|
|
939
|
+
.join(div_set_lf, on="e", how="left")
|
|
940
|
+
.join(ped_lf, on="e", how="left")
|
|
941
|
+
.with_columns(
|
|
942
|
+
later=pl.col("later").fill_null(0.0),
|
|
943
|
+
is_divest=pl.col("is_divest").fill_null(False),
|
|
944
|
+
divested=pl.col("divested").fill_null(0.0),
|
|
945
|
+
)
|
|
946
|
+
.with_columns(
|
|
947
|
+
value=pl.col("later")
|
|
948
|
+
- pl.when(pl.col("is_divest"))
|
|
949
|
+
.then(pl.col("divested"))
|
|
950
|
+
.otherwise(0.0),
|
|
951
|
+
)
|
|
952
|
+
.select("e", "d", "value")
|
|
953
|
+
.sort("e", "d")
|
|
954
|
+
.collect())
|
|
955
|
+
if out.height == 0:
|
|
956
|
+
return None
|
|
957
|
+
return Param(("e", "d"), out)
|
|
958
|
+
|
|
959
|
+
# ---------------------------------------------------------------
|
|
960
|
+
# Simple-baseline branch (single-solve / non-renew chains):
|
|
961
|
+
# pae[e, d] = pre_existing[e, d]
|
|
962
|
+
# + p_entity_previously_invested_capacity[e, d]
|
|
963
|
+
# − p_entity_divested[e] (if e ∈ entityDivest, later solves)
|
|
964
|
+
# ---------------------------------------------------------------
|
|
965
|
+
|
|
966
|
+
# Previously-invested overlay (in-memory handoff carrier).
|
|
967
|
+
if (p_entity_previously_invested_capacity is not None
|
|
968
|
+
and p_entity_previously_invested_capacity.frame.height > 0):
|
|
969
|
+
ppic_frame = p_entity_previously_invested_capacity.frame
|
|
970
|
+
ppic_lf = (ppic_frame.lazy()
|
|
971
|
+
.select("e", "d", pl.col("value").alias("ppic")))
|
|
972
|
+
else:
|
|
973
|
+
ppic_lf = pl.LazyFrame(schema={
|
|
974
|
+
"e": pl.Utf8, "d": pl.Utf8, "ppic": pl.Float64,
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
# Align grid_lf's String dim columns with any Enum-typed inputs
|
|
978
|
+
# (ppic_lf / ped_lf come from FlexData Params that have been
|
|
979
|
+
# cast). Two passes — the first lifts grid_lf to the widest dtype
|
|
980
|
+
# available from any input, the second pulls every other input
|
|
981
|
+
# back up to grid_lf's promoted dtype.
|
|
982
|
+
from flextool.engine_polars._axis_enums import align_join_dtypes
|
|
983
|
+
pre_renamed = pre_existing_lf.rename({"value": "pre"})
|
|
984
|
+
grid_lf, ppic_lf = align_join_dtypes(grid_lf, ppic_lf, ["e", "d"])
|
|
985
|
+
grid_lf, ped_lf = align_join_dtypes(grid_lf, ped_lf, ["e"])
|
|
986
|
+
grid_lf, pre_renamed = align_join_dtypes(grid_lf, pre_renamed, ["e", "d"])
|
|
987
|
+
grid_lf, div_set_lf = align_join_dtypes(grid_lf, div_set_lf, ["e"])
|
|
988
|
+
# Second pass: now grid_lf carries the widest dtype on each axis,
|
|
989
|
+
# pull every input up.
|
|
990
|
+
grid_lf, ppic_lf = align_join_dtypes(grid_lf, ppic_lf, ["e", "d"])
|
|
991
|
+
grid_lf, ped_lf = align_join_dtypes(grid_lf, ped_lf, ["e"])
|
|
992
|
+
grid_lf, pre_renamed = align_join_dtypes(grid_lf, pre_renamed, ["e", "d"])
|
|
993
|
+
grid_lf, div_set_lf = align_join_dtypes(grid_lf, div_set_lf, ["e"])
|
|
994
|
+
out = (grid_lf
|
|
995
|
+
.join(pre_renamed,
|
|
996
|
+
on=["e", "d"], how="left")
|
|
997
|
+
.join(ppic_lf, on=["e", "d"], how="left")
|
|
998
|
+
.join(div_set_lf, on="e", how="left")
|
|
999
|
+
.join(ped_lf, on="e", how="left")
|
|
1000
|
+
.with_columns(
|
|
1001
|
+
pre=pl.col("pre").fill_null(0.0),
|
|
1002
|
+
ppic=pl.col("ppic").fill_null(0.0),
|
|
1003
|
+
is_divest=pl.col("is_divest").fill_null(False),
|
|
1004
|
+
divested=pl.col("divested").fill_null(0.0),
|
|
1005
|
+
)
|
|
1006
|
+
.with_columns(
|
|
1007
|
+
# Δ.11 — divest subtraction is applied only on later
|
|
1008
|
+
# solves (solve_first=False); flextool's writer skips it
|
|
1009
|
+
# on the first solve in the chain.
|
|
1010
|
+
value=pl.col("pre") + pl.col("ppic")
|
|
1011
|
+
- (pl.when(pl.col("is_divest"))
|
|
1012
|
+
.then(pl.col("divested"))
|
|
1013
|
+
.otherwise(0.0)
|
|
1014
|
+
if not solve_first else pl.lit(0.0)),
|
|
1015
|
+
)
|
|
1016
|
+
.select("e", "d", "value")
|
|
1017
|
+
.sort("e", "d")
|
|
1018
|
+
.collect())
|
|
1019
|
+
if out.height == 0:
|
|
1020
|
+
return None
|
|
1021
|
+
return Param(("e", "d"), out)
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def p_entity_pre_existing_lf(source: "InputSource",
|
|
1025
|
+
active_solve: str | None,
|
|
1026
|
+
period_in_use: list[str],
|
|
1027
|
+
) -> pl.LazyFrame:
|
|
1028
|
+
"""Lazy ``[e, d, value]`` for ``p_entity_pre_existing`` —
|
|
1029
|
+
lifetime-gated ``entity.existing × virtual_unitsize`` cascade.
|
|
1030
|
+
|
|
1031
|
+
Algorithm (mirror of
|
|
1032
|
+
``preprocessing/entity_period_calc_params.py:write_p_entity_pre_existing``):
|
|
1033
|
+
|
|
1034
|
+
For each entity e:
|
|
1035
|
+
v_existing = pdProcess[e, 'existing', d] (if e ∈ process)
|
|
1036
|
+
else pdNode[e, 'existing', d] (if e ∈ node)
|
|
1037
|
+
else 0
|
|
1038
|
+
v_unit = p_process[e, 'virtual_unitsize'] (if e ∈ process)
|
|
1039
|
+
else p_node[e, 'virtual_unitsize'] (if e ∈ node)
|
|
1040
|
+
else 0
|
|
1041
|
+
if lifetime_method = reinvest_automatic →
|
|
1042
|
+
v = v_existing * v_unit if v_unit else v_existing
|
|
1043
|
+
if lifetime_method ∈ {reinvest_choice, no_investment} →
|
|
1044
|
+
v = (v_existing * v_unit if v_unit else v_existing)
|
|
1045
|
+
while pdy[d] < life_sum(e); 0 thereafter.
|
|
1046
|
+
else → 0.
|
|
1047
|
+
|
|
1048
|
+
The ``v_existing × v_unit`` term is the canonical capacity (a
|
|
1049
|
+
counted entity's ``existing`` is the count, not the capacity).
|
|
1050
|
+
Only applied when ``v_unit > 0``.
|
|
1051
|
+
"""
|
|
1052
|
+
if not period_in_use:
|
|
1053
|
+
return pl.LazyFrame(schema={
|
|
1054
|
+
"e": schema_dtype(_enums, "e"),
|
|
1055
|
+
"d": schema_dtype(_enums, "d"),
|
|
1056
|
+
"value": pl.Float64,
|
|
1057
|
+
})
|
|
1058
|
+
all_e_lf = _all_entities_lf(source)
|
|
1059
|
+
piu_lf = (pl.LazyFrame({"d": period_in_use})
|
|
1060
|
+
.with_columns(pl.col("d").cast(schema_dtype(_enums, "d"),
|
|
1061
|
+
strict=False)))
|
|
1062
|
+
grid_lf = all_e_lf.join(piu_lf, how="cross")
|
|
1063
|
+
|
|
1064
|
+
# Resolve existing per (e, d) via _resolve_pdX cascade.
|
|
1065
|
+
ex_per = _per_entity_param_lf(source, "existing")
|
|
1066
|
+
grid_with_ex = (_resolve_per_period_lf(ex_per, grid_lf, fill=0.0)
|
|
1067
|
+
.rename({"value": "ex"}))
|
|
1068
|
+
|
|
1069
|
+
# virtual_unitsize per entity (scalar across periods).
|
|
1070
|
+
vu_per = _per_entity_param_lf(source, "virtual_unitsize")
|
|
1071
|
+
# Aggregate to (e, vu) — virtual_unitsize is non-period for
|
|
1072
|
+
# process / node (per write_p_entity_pre_existing's read of
|
|
1073
|
+
# p_process / p_node directly, NOT pd_process / pd_node).
|
|
1074
|
+
vu_scalar = (vu_per
|
|
1075
|
+
.filter(pl.col("is_scalar"))
|
|
1076
|
+
.select("e", pl.col("value").alias("vu"))
|
|
1077
|
+
.unique(subset=["e"], keep="last"))
|
|
1078
|
+
grid_with_vu = (grid_with_ex
|
|
1079
|
+
.join(vu_scalar, on="e", how="left")
|
|
1080
|
+
.with_columns(vu=pl.col("vu").fill_null(0.0)))
|
|
1081
|
+
|
|
1082
|
+
# Lifetime methods (with default).
|
|
1083
|
+
elm_lf = _lifetime_method_with_default_lf(source, all_e_lf)
|
|
1084
|
+
is_auto = (elm_lf
|
|
1085
|
+
.filter(pl.col("method") == "reinvest_automatic")
|
|
1086
|
+
.select("e").unique()
|
|
1087
|
+
.with_columns(is_auto=pl.lit(True)))
|
|
1088
|
+
is_bounded = (elm_lf
|
|
1089
|
+
.filter(pl.col("method").is_in(list(_LIFETIME_BOUNDED_METHODS)))
|
|
1090
|
+
.select("e").unique()
|
|
1091
|
+
.with_columns(is_bounded=pl.lit(True)))
|
|
1092
|
+
|
|
1093
|
+
# Lifetime gate: zero out existing past life_sum expiry for bounded.
|
|
1094
|
+
expired_lf = _lifetime_expired_pairs_lf(
|
|
1095
|
+
source, active_solve, period_in_use, methods=_LIFETIME_BOUNDED_METHODS)
|
|
1096
|
+
expired_marked = expired_lf.with_columns(_expired=pl.lit(True))
|
|
1097
|
+
|
|
1098
|
+
# Compose: value = ex × vu (or ex if vu=0) if alive; 0 else.
|
|
1099
|
+
capacity_expr = (pl.when(pl.col("vu") != 0.0)
|
|
1100
|
+
.then(pl.col("ex") * pl.col("vu"))
|
|
1101
|
+
.otherwise(pl.col("ex")))
|
|
1102
|
+
return (grid_with_vu
|
|
1103
|
+
.join(is_auto, on="e", how="left")
|
|
1104
|
+
.join(is_bounded, on="e", how="left")
|
|
1105
|
+
.join(expired_marked, on=["e", "d"], how="left")
|
|
1106
|
+
.with_columns(
|
|
1107
|
+
is_auto=pl.col("is_auto").fill_null(False),
|
|
1108
|
+
is_bounded=pl.col("is_bounded").fill_null(False),
|
|
1109
|
+
_expired=pl.col("_expired").fill_null(False),
|
|
1110
|
+
cap=capacity_expr,
|
|
1111
|
+
)
|
|
1112
|
+
.with_columns(
|
|
1113
|
+
value=pl.when(pl.col("is_auto"))
|
|
1114
|
+
.then(pl.col("cap"))
|
|
1115
|
+
.when(pl.col("is_bounded") & ~pl.col("_expired"))
|
|
1116
|
+
.then(pl.col("cap"))
|
|
1117
|
+
.otherwise(0.0),
|
|
1118
|
+
)
|
|
1119
|
+
.select("e", "d", "value"))
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _lifetime_expired_pairs_lf(source: "InputSource",
|
|
1123
|
+
active_solve: str | None,
|
|
1124
|
+
period_in_use: list[str],
|
|
1125
|
+
*,
|
|
1126
|
+
methods: frozenset[str],
|
|
1127
|
+
) -> pl.LazyFrame:
|
|
1128
|
+
"""Lazy ``[e, d]`` of pairs whose pre-existing capacity has expired.
|
|
1129
|
+
|
|
1130
|
+
For each entity whose ``lifetime_method ∈ methods``, return periods
|
|
1131
|
+
d where ``pdy[d] >= life_sum(e)`` with::
|
|
1132
|
+
|
|
1133
|
+
life_sum(e) = Σ_{d_first ∈ period_first}
|
|
1134
|
+
(pdy[d_first] + lifetime[e, d_first])
|
|
1135
|
+
|
|
1136
|
+
Mirror of :func:`._derived_params._lifetime_expired_pairs` in lazy form.
|
|
1137
|
+
Uses :func:`._derived_params._read_period_first` /
|
|
1138
|
+
:func:`._derived_params._p_years_d_lf` for the workdir-aware reads.
|
|
1139
|
+
"""
|
|
1140
|
+
from ._derived_params import _read_period_first, _p_years_d_lf
|
|
1141
|
+
period_first = _read_period_first(source, active_solve, None)
|
|
1142
|
+
if not period_first:
|
|
1143
|
+
return pl.LazyFrame(schema={
|
|
1144
|
+
"e": schema_dtype(_enums, "e"),
|
|
1145
|
+
"d": schema_dtype(_enums, "d"),
|
|
1146
|
+
})
|
|
1147
|
+
all_e_lf = _all_entities_lf(source)
|
|
1148
|
+
elm_lf = _lifetime_method_with_default_lf(source, all_e_lf)
|
|
1149
|
+
in_methods = (elm_lf
|
|
1150
|
+
.filter(pl.col("method").is_in(list(methods)))
|
|
1151
|
+
.select("e").unique())
|
|
1152
|
+
|
|
1153
|
+
# Class gate: connection without is_DC / non-process → no lifetime.
|
|
1154
|
+
cls_lf = _entity_class_lf(source).group_by("e").agg(
|
|
1155
|
+
pl.col("ec").alias("ecs"))
|
|
1156
|
+
is_proc_or_node = (pl.col("ecs").list.contains("unit")
|
|
1157
|
+
| pl.col("ecs").list.contains("connection")
|
|
1158
|
+
| pl.col("ecs").list.contains("node"))
|
|
1159
|
+
in_class = cls_lf.with_columns(
|
|
1160
|
+
in_class=is_proc_or_node).select("e", "in_class")
|
|
1161
|
+
|
|
1162
|
+
pyd_lf = _p_years_d_lf(source, active_solve)
|
|
1163
|
+
if pyd_lf is None:
|
|
1164
|
+
return pl.LazyFrame(schema={
|
|
1165
|
+
"e": schema_dtype(_enums, "e"),
|
|
1166
|
+
"d": schema_dtype(_enums, "d"),
|
|
1167
|
+
})
|
|
1168
|
+
|
|
1169
|
+
# life_sum: per-entity scalar. Compute per d_first then sum:
|
|
1170
|
+
# life_sum(e) = Σ_{d_first} (pdy[d_first] + lifetime[e, d_first]).
|
|
1171
|
+
# Phase 4.6: ``d_first`` is a column holding period tokens; cast the
|
|
1172
|
+
# literal-list LazyFrame to the ``d`` (period) Enum so downstream
|
|
1173
|
+
# joins against Enum-typed ``pyd_first`` don't SchemaError.
|
|
1174
|
+
pf_lf = (pl.LazyFrame({"d_first": period_first})
|
|
1175
|
+
.with_columns(pl.col("d_first")
|
|
1176
|
+
.cast(schema_dtype(_enums, "d_first"),
|
|
1177
|
+
strict=False)))
|
|
1178
|
+
pyd_first = pyd_lf.rename({"d": "d_first", "yr": "yr_first"})
|
|
1179
|
+
(in_methods.join(pf_lf, how="cross")
|
|
1180
|
+
.pipe(rename_to_axis, {"d_first": "d"})
|
|
1181
|
+
.pipe(lambda lf: lf)) # placeholder for symmetry
|
|
1182
|
+
# Resolve lifetime[e, d_first] via cascade.
|
|
1183
|
+
life_per = _per_entity_param_lf(source, "lifetime")
|
|
1184
|
+
life_resolved = (_resolve_per_period_lf(
|
|
1185
|
+
life_per,
|
|
1186
|
+
in_methods.join(pf_lf, how="cross").pipe(rename_to_axis, {"d_first": "d"}),
|
|
1187
|
+
fill=0.0))
|
|
1188
|
+
life_at_first = life_resolved.rename({"d": "d_first", "value": "life"})
|
|
1189
|
+
summed_lf = (life_at_first
|
|
1190
|
+
.join(pyd_first, on="d_first", how="left")
|
|
1191
|
+
.with_columns(yr_first=pl.col("yr_first").fill_null(0.0))
|
|
1192
|
+
.group_by("e")
|
|
1193
|
+
.agg((pl.col("yr_first") + pl.col("life")).sum()
|
|
1194
|
+
.alias("life_sum")))
|
|
1195
|
+
# Apply class gate.
|
|
1196
|
+
summed_lf = (summed_lf
|
|
1197
|
+
.join(in_class, on="e", how="left")
|
|
1198
|
+
.with_columns(in_class=pl.col("in_class").fill_null(False))
|
|
1199
|
+
.with_columns(
|
|
1200
|
+
life_sum=pl.when(pl.col("in_class"))
|
|
1201
|
+
.then(pl.col("life_sum"))
|
|
1202
|
+
.otherwise(0.0))
|
|
1203
|
+
.select("e", "life_sum"))
|
|
1204
|
+
|
|
1205
|
+
piu_lf = (pl.LazyFrame({"d": period_in_use})
|
|
1206
|
+
.with_columns(pl.col("d").cast(schema_dtype(_enums, "d"),
|
|
1207
|
+
strict=False)))
|
|
1208
|
+
pyd_at_d = pyd_lf
|
|
1209
|
+
return (summed_lf
|
|
1210
|
+
.join(piu_lf, how="cross")
|
|
1211
|
+
.join(pyd_at_d, on="d", how="left")
|
|
1212
|
+
.with_columns(yr=pl.col("yr").fill_null(0.0))
|
|
1213
|
+
.filter(pl.col("yr") >= pl.col("life_sum"))
|
|
1214
|
+
.select("e", "d"))
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
# ---------------------------------------------------------------------------
|
|
1218
|
+
# Public entry: apply boundary
|
|
1219
|
+
# ---------------------------------------------------------------------------
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def apply_existing_chain(flex_data: object,
|
|
1223
|
+
source: "InputSource",
|
|
1224
|
+
workdir: Path,
|
|
1225
|
+
*,
|
|
1226
|
+
handoff: object | None = None,
|
|
1227
|
+
ctx: "SolveContext | None" = None,
|
|
1228
|
+
provider: "object | None" = None) -> None:
|
|
1229
|
+
"""Apply Cluster B helpers to ``flex_data`` (mutates in place).
|
|
1230
|
+
|
|
1231
|
+
Wired-in fields:
|
|
1232
|
+
|
|
1233
|
+
* ``p_entity_all_existing`` — chained existing capacity (replacing
|
|
1234
|
+
the workdir CSV read of ``solve_data/p_entity_all_existing.csv``
|
|
1235
|
+
and the cascade in :func:`._derived_params.p_entity_all_existing_from_source`).
|
|
1236
|
+
|
|
1237
|
+
Order:
|
|
1238
|
+
1. Resolve the per-solve scope (active_solve, periods, etc.).
|
|
1239
|
+
2. Pull the in-memory handoff carriers off ``flex_data``
|
|
1240
|
+
(``p_entity_period_existing_capacity``, ``p_entity_divested``)
|
|
1241
|
+
— these are populated by the CSV-side handoff loader / by
|
|
1242
|
+
:func:`apply_derived_f`'s handoff overlay.
|
|
1243
|
+
3. Compute ``p_entity_all_existing`` via the lazy chain.
|
|
1244
|
+
4. Overwrite ``flex_data.p_entity_all_existing`` only when the
|
|
1245
|
+
lazy result is non-None (mirrors the eager helper's gate).
|
|
1246
|
+
|
|
1247
|
+
Notes
|
|
1248
|
+
-----
|
|
1249
|
+
Cluster B is parity-bound to the eager
|
|
1250
|
+
``p_entity_all_existing_from_source`` helper which the per-fixture
|
|
1251
|
+
sweep validates. When parity drifts, the test surfaces the
|
|
1252
|
+
fixture and the diff frame.
|
|
1253
|
+
"""
|
|
1254
|
+
from ._derived_params import (
|
|
1255
|
+
_read_active_solve, _period_in_use_set,
|
|
1256
|
+
_read_period_with_history,
|
|
1257
|
+
)
|
|
1258
|
+
from .input import _read_solve_first
|
|
1259
|
+
from ._emit_provider_io import _provider_key
|
|
1260
|
+
from polar_high import Param as _Param
|
|
1261
|
+
|
|
1262
|
+
def _provider_get(p):
|
|
1263
|
+
if provider is None:
|
|
1264
|
+
return None
|
|
1265
|
+
key = _provider_key(p)
|
|
1266
|
+
if not provider.has(key):
|
|
1267
|
+
return None
|
|
1268
|
+
return provider.get(key)
|
|
1269
|
+
|
|
1270
|
+
# Δ.12a — prefer the typed SolveContext fields (zero physical IO)
|
|
1271
|
+
# when the caller supplied one; fall back to direct workdir reads
|
|
1272
|
+
# for back-compat with callers that haven't been wired up.
|
|
1273
|
+
if ctx is not None:
|
|
1274
|
+
active_solve = ctx.solve_name
|
|
1275
|
+
solve_first = ctx.solveFirst
|
|
1276
|
+
edd_hist_df_raw = ctx.edd_history
|
|
1277
|
+
else:
|
|
1278
|
+
active_solve = _read_active_solve(workdir, provider=provider)
|
|
1279
|
+
solve_first = _read_solve_first(workdir, provider=provider)
|
|
1280
|
+
edd_hist_df_raw = None
|
|
1281
|
+
period_in_use = _period_in_use_set(source, active_solve, workdir,
|
|
1282
|
+
ctx=ctx, provider=provider)
|
|
1283
|
+
period_with_history = (_read_period_with_history(workdir, provider=provider)
|
|
1284
|
+
or list(period_in_use))
|
|
1285
|
+
|
|
1286
|
+
ppic = getattr(flex_data, "p_entity_previously_invested_capacity", None)
|
|
1287
|
+
ped = getattr(flex_data, "p_entity_divested", None)
|
|
1288
|
+
|
|
1289
|
+
# Δ.11 / Phase 4.2-1e — chain-summation inputs. The in-memory
|
|
1290
|
+
# handoff ``realized_existing`` carrier is the sole source for the
|
|
1291
|
+
# cumulative realized-existing capacity; the workdir CSV fallback
|
|
1292
|
+
# was retired once the handoff translator started populating the
|
|
1293
|
+
# carrier at iteration start. ``edd_history.csv`` is still sourced
|
|
1294
|
+
# from the workdir.
|
|
1295
|
+
ppec_param: "Param | None" = None
|
|
1296
|
+
if handoff is not None and getattr(handoff, "realized_existing", None) is not None:
|
|
1297
|
+
re_frame = handoff.realized_existing
|
|
1298
|
+
if re_frame.height > 0:
|
|
1299
|
+
ppec_param = _Param(("e", "d"),
|
|
1300
|
+
re_frame.pipe(rename_to_axis, {"entity": "e",
|
|
1301
|
+
"period": "d"})
|
|
1302
|
+
.select("e", "d", "value"))
|
|
1303
|
+
# Phase 4.2-1e — the handoff/realized_existing branch above is the
|
|
1304
|
+
# sole source of the cumulative realized-existing capacity now that
|
|
1305
|
+
# the handoff translator always populates the carrier at iteration
|
|
1306
|
+
# start. When the handoff is absent or empty, ``ppec_param`` stays
|
|
1307
|
+
# None and the helper returns None (no-existing-capacity outcome).
|
|
1308
|
+
|
|
1309
|
+
edd_hist_df: "pl.DataFrame | None" = None
|
|
1310
|
+
if ctx is not None:
|
|
1311
|
+
if edd_hist_df_raw is not None and edd_hist_df_raw.height > 0:
|
|
1312
|
+
edd_hist_df = edd_hist_df_raw
|
|
1313
|
+
else:
|
|
1314
|
+
edd_path = workdir / "solve_data" / "edd_history.csv"
|
|
1315
|
+
edd_hist_df = _provider_get(edd_path)
|
|
1316
|
+
if edd_hist_df is not None and edd_hist_df.height == 0:
|
|
1317
|
+
edd_hist_df = None
|
|
1318
|
+
|
|
1319
|
+
# Δ.12b — assign unconditionally. The helper returns ``None`` only
|
|
1320
|
+
# when there's no entity_invest_method-eligible row in the source,
|
|
1321
|
+
# which is a legitimate "no-existing-capacity-to-track" outcome
|
|
1322
|
+
# (matches the eager helper's empty-frame skip). Hard errors
|
|
1323
|
+
# propagate (no defensive try/except).
|
|
1324
|
+
flex_data.p_entity_all_existing = p_entity_all_existing_from_handoff(
|
|
1325
|
+
source, active_solve,
|
|
1326
|
+
period_with_history, period_in_use,
|
|
1327
|
+
p_entity_period_existing_capacity=ppec_param,
|
|
1328
|
+
p_entity_previously_invested_capacity=ppic,
|
|
1329
|
+
p_entity_divested=ped,
|
|
1330
|
+
solve_first=solve_first,
|
|
1331
|
+
edd_history=edd_hist_df)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
__all__ = [
|
|
1335
|
+
# Method-enum constants (re-exported for direct-test use).
|
|
1336
|
+
"_INVEST_NOT_ALLOWED", "_DIVEST_NOT_ALLOWED",
|
|
1337
|
+
"_INVEST_TOTAL_METHODS", "_DIVEST_TOTAL_METHODS",
|
|
1338
|
+
"_INVEST_PERIOD_METHODS", "_DIVEST_PERIOD_METHODS",
|
|
1339
|
+
# Public lazy helpers — entity sets.
|
|
1340
|
+
"entity_invest_set_lf", "entity_divest_set_lf",
|
|
1341
|
+
"e_invest_total_lf", "e_divest_total_lf",
|
|
1342
|
+
# Public lazy helpers — edd_history triple-set family.
|
|
1343
|
+
"edd_history_lf", "edd_history_choice_lf",
|
|
1344
|
+
"edd_history_automatic_lf", "edd_history_no_investment_lf",
|
|
1345
|
+
"edd_invest_set_lf", "edd_invest_lookback_set_lf",
|
|
1346
|
+
# Public lazy helpers — partitions.
|
|
1347
|
+
"pd_invest_set_lf", "nd_invest_set_lf",
|
|
1348
|
+
"pd_divest_set_lf", "nd_divest_set_lf",
|
|
1349
|
+
"ed_invest_period_set_lf", "ed_divest_period_set_lf",
|
|
1350
|
+
# Public lazy helpers — existing chain.
|
|
1351
|
+
"p_entity_all_existing_from_handoff", "p_entity_pre_existing_lf",
|
|
1352
|
+
"apply_existing_chain",
|
|
1353
|
+
]
|