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.
Files changed (322) hide show
  1. flextool/__init__.py +41 -0
  2. flextool/_mem_sampler.py +193 -0
  3. flextool/_resources.py +43 -0
  4. flextool/calibrate/__init__.py +51 -0
  5. flextool/calibrate/__main__.py +11 -0
  6. flextool/calibrate/_cli.py +316 -0
  7. flextool/calibrate/_db_alt.py +166 -0
  8. flextool/calibrate/_final_outputs.py +110 -0
  9. flextool/calibrate/_guard.py +151 -0
  10. flextool/calibrate/_loop.py +558 -0
  11. flextool/calibrate/_readers.py +223 -0
  12. flextool/calibrate/_report.py +263 -0
  13. flextool/calibrate/_sizing.py +699 -0
  14. flextool/calibrate/_solve.py +134 -0
  15. flextool/calibrate/_solve_status.py +495 -0
  16. flextool/cli/__init__.py +9 -0
  17. flextool/cli/_console.py +51 -0
  18. flextool/cli/_timing.py +147 -0
  19. flextool/cli/cmd_execute_flextool_workflow.py +187 -0
  20. flextool/cli/cmd_export_to_tabular.py +56 -0
  21. flextool/cli/cmd_import_sensitivities.py +75 -0
  22. flextool/cli/cmd_migrate_database.py +13 -0
  23. flextool/cli/cmd_open_results_db.py +269 -0
  24. flextool/cli/cmd_read_matpower.py +66 -0
  25. flextool/cli/cmd_read_old_flextool.py +63 -0
  26. flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
  27. flextool/cli/cmd_read_tabular_input.py +81 -0
  28. flextool/cli/cmd_run_flextool.py +1095 -0
  29. flextool/cli/cmd_scenario_results.py +284 -0
  30. flextool/cli/cmd_solve_mps.py +169 -0
  31. flextool/cli/cmd_update_flextool.py +17 -0
  32. flextool/cli/cmd_write_outputs.py +125 -0
  33. flextool/common_utils/__init__.py +1 -0
  34. flextool/common_utils/plot_mem_shape.py +77 -0
  35. flextool/common_utils/precision.py +451 -0
  36. flextool/decomposition/__init__.py +0 -0
  37. flextool/decomposition/region_decomposition.py +128 -0
  38. flextool/decomposition/region_filter.py +1261 -0
  39. flextool/engine_polars/__init__.py +110 -0
  40. flextool/engine_polars/_axis_enums.py +742 -0
  41. flextool/engine_polars/_benders.py +3462 -0
  42. flextool/engine_polars/_block_layout.py +1479 -0
  43. flextool/engine_polars/_blocks.py +1515 -0
  44. flextool/engine_polars/_commodity_ladder.py +660 -0
  45. flextool/engine_polars/_cumulative_invest.py +1165 -0
  46. flextool/engine_polars/_db_loader.py +153 -0
  47. flextool/engine_polars/_db_reader.py +127 -0
  48. flextool/engine_polars/_dc_power_flow.py +445 -0
  49. flextool/engine_polars/_delay.py +442 -0
  50. flextool/engine_polars/_derived_arithmetic.py +432 -0
  51. flextool/engine_polars/_derived_block.py +990 -0
  52. flextool/engine_polars/_derived_branch.py +769 -0
  53. flextool/engine_polars/_derived_existing.py +1353 -0
  54. flextool/engine_polars/_derived_npv.py +1297 -0
  55. flextool/engine_polars/_derived_params.py +9850 -0
  56. flextool/engine_polars/_derived_profile.py +881 -0
  57. flextool/engine_polars/_derived_walks.py +276 -0
  58. flextool/engine_polars/_determinism.py +70 -0
  59. flextool/engine_polars/_direct_params.py +2186 -0
  60. flextool/engine_polars/_dump_csvs.py +1009 -0
  61. flextool/engine_polars/_emit_arc_unions.py +1631 -0
  62. flextool/engine_polars/_emit_calc_params.py +729 -0
  63. flextool/engine_polars/_emit_chain_params.py +709 -0
  64. flextool/engine_polars/_emit_co2_accumulators.py +400 -0
  65. flextool/engine_polars/_emit_dispatchers.py +690 -0
  66. flextool/engine_polars/_emit_energy_margin.py +125 -0
  67. flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
  68. flextool/engine_polars/_emit_entity_annual.py +428 -0
  69. flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
  70. flextool/engine_polars/_emit_leaf_sets.py +550 -0
  71. flextool/engine_polars/_emit_lp_scaling.py +665 -0
  72. flextool/engine_polars/_emit_mid_sets.py +859 -0
  73. flextool/engine_polars/_emit_pdt_params.py +759 -0
  74. flextool/engine_polars/_emit_per_solve.py +774 -0
  75. flextool/engine_polars/_emit_period_calc.py +504 -0
  76. flextool/engine_polars/_emit_period_params.py +2398 -0
  77. flextool/engine_polars/_emit_provider_io.py +141 -0
  78. flextool/engine_polars/_emit_reserve.py +574 -0
  79. flextool/engine_polars/_emit_solve_time.py +311 -0
  80. flextool/engine_polars/_emit_solve_writers.py +1249 -0
  81. flextool/engine_polars/_flex_data_accumulator.py +388 -0
  82. flextool/engine_polars/_flex_data_provider.py +478 -0
  83. flextool/engine_polars/_group_slack.py +1253 -0
  84. flextool/engine_polars/_inmemory_reader.py +140 -0
  85. flextool/engine_polars/_input_source.py +336 -0
  86. flextool/engine_polars/_invest_seeds.py +191 -0
  87. flextool/engine_polars/_native_input_writer.py +100 -0
  88. flextool/engine_polars/_native_run_model.py +1348 -0
  89. flextool/engine_polars/_orchestration.py +4314 -0
  90. flextool/engine_polars/_output_writer.py +439 -0
  91. flextool/engine_polars/_param_shapes.py +1595 -0
  92. flextool/engine_polars/_parquet_bundle.py +723 -0
  93. flextool/engine_polars/_pdt_join.py +167 -0
  94. flextool/engine_polars/_pdt_lookup.py +547 -0
  95. flextool/engine_polars/_per_solve_sets.py +335 -0
  96. flextool/engine_polars/_projection_params.py +2056 -0
  97. flextool/engine_polars/_provider_keys.py +173 -0
  98. flextool/engine_polars/_provider_translators.py +225 -0
  99. flextool/engine_polars/_recursive_solve.py +703 -0
  100. flextool/engine_polars/_region_filter.py +2508 -0
  101. flextool/engine_polars/_reserve.py +649 -0
  102. flextool/engine_polars/_solve_acceptance.py +331 -0
  103. flextool/engine_polars/_solve_config.py +1001 -0
  104. flextool/engine_polars/_solve_context.py +885 -0
  105. flextool/engine_polars/_solve_handoff.py +164 -0
  106. flextool/engine_polars/_solve_state.py +232 -0
  107. flextool/engine_polars/_solver_base.py +36 -0
  108. flextool/engine_polars/_solver_dispatch.py +511 -0
  109. flextool/engine_polars/_spinedb_reader.py +1165 -0
  110. flextool/engine_polars/_stochastic.py +593 -0
  111. flextool/engine_polars/_subprocess_solve.py +1838 -0
  112. flextool/engine_polars/_timeline.py +1416 -0
  113. flextool/engine_polars/_vectorize.py +438 -0
  114. flextool/engine_polars/_warm.py +858 -0
  115. flextool/engine_polars/autoscale/__init__.py +107 -0
  116. flextool/engine_polars/autoscale/_config.py +218 -0
  117. flextool/engine_polars/autoscale/_layer2.py +1253 -0
  118. flextool/engine_polars/autoscale/_layer2_types.py +584 -0
  119. flextool/engine_polars/autoscale/_quantity_types.py +621 -0
  120. flextool/engine_polars/autoscale/_report.py +336 -0
  121. flextool/engine_polars/chain.py +259 -0
  122. flextool/engine_polars/input.py +6638 -0
  123. flextool/engine_polars/model.py +4754 -0
  124. flextool/env_check.py +388 -0
  125. flextool/export_to_tabular/__init__.py +5 -0
  126. flextool/export_to_tabular/db_reader.py +224 -0
  127. flextool/export_to_tabular/excel_writer.py +3559 -0
  128. flextool/export_to_tabular/export_settings.yaml +377 -0
  129. flextool/export_to_tabular/export_to_excel.py +227 -0
  130. flextool/export_to_tabular/formatting.py +543 -0
  131. flextool/export_to_tabular/sheet_config.py +876 -0
  132. flextool/gui/__init__.py +0 -0
  133. flextool/gui/__main__.py +118 -0
  134. flextool/gui/calibrate_commands.py +184 -0
  135. flextool/gui/calibrate_jobs.py +424 -0
  136. flextool/gui/check_tree.py +142 -0
  137. flextool/gui/cli_format.py +83 -0
  138. flextool/gui/config_parser.py +68 -0
  139. flextool/gui/data_models.py +362 -0
  140. flextool/gui/db_editor_integration.py +202 -0
  141. flextool/gui/db_version_check.py +269 -0
  142. flextool/gui/dialogs/__init__.py +0 -0
  143. flextool/gui/dialogs/add_dialog.py +1098 -0
  144. flextool/gui/dialogs/calibrate_dialog.py +1259 -0
  145. flextool/gui/dialogs/file_picker.py +473 -0
  146. flextool/gui/dialogs/group_picker.py +299 -0
  147. flextool/gui/dialogs/migration_consent_dialog.py +106 -0
  148. flextool/gui/dialogs/migration_progress_dialog.py +237 -0
  149. flextool/gui/dialogs/plot_dialog.py +459 -0
  150. flextool/gui/dialogs/plot_settings_picker.py +2184 -0
  151. flextool/gui/dialogs/project_dialog.py +426 -0
  152. flextool/gui/dialogs/update_dialog.py +212 -0
  153. flextool/gui/downsampling.py +88 -0
  154. flextool/gui/error_handling.py +50 -0
  155. flextool/gui/execution_manager.py +1715 -0
  156. flextool/gui/execution_window.py +1377 -0
  157. flextool/gui/hover_tooltip.py +111 -0
  158. flextool/gui/input_sources.py +730 -0
  159. flextool/gui/main_window.py +6181 -0
  160. flextool/gui/network_graph.py +215 -0
  161. flextool/gui/output_actions.py +393 -0
  162. flextool/gui/output_log_window.py +159 -0
  163. flextool/gui/platform_utils.py +421 -0
  164. flextool/gui/plot_cache.py +88 -0
  165. flextool/gui/plot_canvas.py +543 -0
  166. flextool/gui/plot_config_reader.py +272 -0
  167. flextool/gui/project_utils.py +100 -0
  168. flextool/gui/result_viewer.py +4394 -0
  169. flextool/gui/scenario_key.py +162 -0
  170. flextool/gui/scenario_lists.py +516 -0
  171. flextool/gui/settings_io.py +360 -0
  172. flextool/gui/solve_reader.py +103 -0
  173. flextool/gui/tree_reorder.py +88 -0
  174. flextool/gui/ui_metrics.py +420 -0
  175. flextool/input_derivation/__init__.py +281 -0
  176. flextool/input_derivation/_commodity_ladder.py +375 -0
  177. flextool/input_derivation/_commodity_ladder_sets.py +70 -0
  178. flextool/input_derivation/_dc_power_flow.py +377 -0
  179. flextool/input_derivation/_method_constants.py +77 -0
  180. flextool/input_derivation/_process_method.py +258 -0
  181. flextool/input_derivation/_specs.py +1026 -0
  182. flextool/input_derivation/_validators.py +321 -0
  183. flextool/lean_parquet.py +159 -0
  184. flextool/model_builder/__init__.py +5 -0
  185. flextool/model_builder/build_model.py +589 -0
  186. flextool/model_builder/encoding.py +67 -0
  187. flextool/model_builder/names.py +34 -0
  188. flextool/model_builder/profiles.py +129 -0
  189. flextool/plot_outputs/__init__.py +14 -0
  190. flextool/plot_outputs/axis_helpers.py +355 -0
  191. flextool/plot_outputs/color_template.py +888 -0
  192. flextool/plot_outputs/config.py +171 -0
  193. flextool/plot_outputs/format_helpers.py +345 -0
  194. flextool/plot_outputs/legend_helpers.py +143 -0
  195. flextool/plot_outputs/orchestrator.py +1141 -0
  196. flextool/plot_outputs/perf.py +37 -0
  197. flextool/plot_outputs/plan.py +1787 -0
  198. flextool/plot_outputs/plot_bars.py +1510 -0
  199. flextool/plot_outputs/plot_bars_detail.py +753 -0
  200. flextool/plot_outputs/plot_lines.py +951 -0
  201. flextool/plot_outputs/shared_manifest.py +564 -0
  202. flextool/plot_outputs/subplot_helpers.py +137 -0
  203. flextool/process_inputs/__init__.py +188 -0
  204. flextool/process_inputs/import_old_excel_input.json +4159 -0
  205. flextool/process_inputs/read_matpower.py +451 -0
  206. flextool/process_inputs/read_old_flextool.py +1288 -0
  207. flextool/process_inputs/read_self_describing_excel.py +1423 -0
  208. flextool/process_inputs/read_tabular_with_specification.py +1114 -0
  209. flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
  210. flextool/process_inputs/write_self_describing_to_db.py +977 -0
  211. flextool/process_inputs/write_to_input_db.py +269 -0
  212. flextool/process_outputs/__init__.py +7 -0
  213. flextool/process_outputs/_annualize.py +55 -0
  214. flextool/process_outputs/_inmemory_helpers.py +292 -0
  215. flextool/process_outputs/_output_meta.py +672 -0
  216. flextool/process_outputs/calc_capacity_flows.py +107 -0
  217. flextool/process_outputs/calc_connections.py +136 -0
  218. flextool/process_outputs/calc_costs.py +260 -0
  219. flextool/process_outputs/calc_group_flows.py +192 -0
  220. flextool/process_outputs/calc_slacks.py +103 -0
  221. flextool/process_outputs/calc_storage_vre.py +160 -0
  222. flextool/process_outputs/drop_levels.py +208 -0
  223. flextool/process_outputs/handoff_writers.py +1315 -0
  224. flextool/process_outputs/out_ancillary.py +544 -0
  225. flextool/process_outputs/out_capacity.py +179 -0
  226. flextool/process_outputs/out_costs.py +334 -0
  227. flextool/process_outputs/out_flowgroup.py +189 -0
  228. flextool/process_outputs/out_flows.py +301 -0
  229. flextool/process_outputs/out_group.py +475 -0
  230. flextool/process_outputs/out_node.py +190 -0
  231. flextool/process_outputs/persist_realized_slice.py +601 -0
  232. flextool/process_outputs/process_results.py +24 -0
  233. flextool/process_outputs/read_highs_solution.py +2256 -0
  234. flextool/process_outputs/read_parameters.py +1799 -0
  235. flextool/process_outputs/read_sets.py +1095 -0
  236. flextool/process_outputs/read_variables.py +553 -0
  237. flextool/process_outputs/solve_order.py +81 -0
  238. flextool/process_outputs/spinedb_replay.py +412 -0
  239. flextool/process_outputs/union_realized_slice.py +224 -0
  240. flextool/process_outputs/write_outputs.py +1286 -0
  241. flextool/process_outputs/write_spinedb.py +1267 -0
  242. flextool/representative_periods/__init__.py +5 -0
  243. flextool/representative_periods/clustering.py +165 -0
  244. flextool/representative_periods/force_include.py +563 -0
  245. flextool/representative_periods/netload.py +365 -0
  246. flextool/representative_periods/netload_inputs.py +345 -0
  247. flextool/representative_periods/netload_iterate.py +722 -0
  248. flextool/representative_periods/preprocess.py +948 -0
  249. flextool/representative_periods/scenario_stack.py +195 -0
  250. flextool/representative_periods/weights.py +124 -0
  251. flextool/scenario_comparison/__init__.py +13 -0
  252. flextool/scenario_comparison/config_builder.py +158 -0
  253. flextool/scenario_comparison/constants.py +20 -0
  254. flextool/scenario_comparison/data_models.py +222 -0
  255. flextool/scenario_comparison/db_reader.py +399 -0
  256. flextool/scenario_comparison/dispatch_data.py +1002 -0
  257. flextool/scenario_comparison/dispatch_mappings.py +205 -0
  258. flextool/scenario_comparison/dispatch_plots.py +691 -0
  259. flextool/scenario_comparison/input_entity_colors.py +319 -0
  260. flextool/scenario_comparison/orchestrator.py +453 -0
  261. flextool/scenario_comparison/plan_union.py +244 -0
  262. flextool/scenario_comparison/plot_settings_seed.py +205 -0
  263. flextool/schemas/AXIS_CONTRACT.md +71 -0
  264. flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
  265. flextool/schemas/canonical_databases/howto_connections.json +5606 -0
  266. flextool/schemas/canonical_databases/howto_demand.json +5518 -0
  267. flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
  268. flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
  269. flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
  270. flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
  271. flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
  272. flextool/schemas/canonical_databases/templates_examples.json +13532 -0
  273. flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
  274. flextool/schemas/comparison_settings_template.json +197 -0
  275. flextool/schemas/default_plot_settings.yaml +260 -0
  276. flextool/schemas/default_plots.yaml +2293 -0
  277. flextool/schemas/flextool_axis_contract.json +303 -0
  278. flextool/schemas/flextool_axis_contract.schema.json +247 -0
  279. flextool/schemas/old_flextool_import_template.json +4443 -0
  280. flextool/schemas/output_info_template.json +48 -0
  281. flextool/schemas/output_settings_template.json +256 -0
  282. flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
  283. flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
  284. flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
  285. flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
  286. flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
  287. flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
  288. flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
  289. flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
  290. flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
  291. flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
  292. flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
  293. flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
  294. flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
  295. flextool/schemas/spinedb_results_schema.json +581 -0
  296. flextool/schemas/spinedb_schema.json +4636 -0
  297. flextool/solver_config/copt.opt.template +18 -0
  298. flextool/solver_config/cplex.opt.template +25 -0
  299. flextool/solver_config/gurobi.opt.template +18 -0
  300. flextool/solver_config/highs.opt.template +18 -0
  301. flextool/solver_config/xpress.opt.template +26 -0
  302. flextool/spinedb_backend/__init__.py +26 -0
  303. flextool/spinedb_backend/_axis_enums.py +1119 -0
  304. flextool/spinedb_backend/_backend.py +1139 -0
  305. flextool/update_flextool/__init__.py +12 -0
  306. flextool/update_flextool/canonical_databases.py +251 -0
  307. flextool/update_flextool/db_migration.py +7108 -0
  308. flextool/update_flextool/ensure_settings_db.py +138 -0
  309. flextool/update_flextool/export_database.py +103 -0
  310. flextool/update_flextool/extend_tests_fixture.py +772 -0
  311. flextool/update_flextool/generate_canonical.py +274 -0
  312. flextool/update_flextool/initialize_database.py +42 -0
  313. flextool/update_flextool/install_info.py +225 -0
  314. flextool/update_flextool/self_update.py +464 -0
  315. flextool/update_flextool/sync_master_json_template.py +125 -0
  316. flextool/update_flextool/test_fixtures.py +187 -0
  317. flextool-4.0.0.dist-info/METADATA +217 -0
  318. flextool-4.0.0.dist-info/RECORD +322 -0
  319. flextool-4.0.0.dist-info/WHEEL +5 -0
  320. flextool-4.0.0.dist-info/entry_points.txt +17 -0
  321. flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
  322. 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"]