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,439 @@
1
+ """Output writer adapter.
2
+
3
+ Bridges the polars-build :class:`polar_high.Solution` to the post-solve
4
+ writers in ``flextool.process_outputs``. Feeds them the live
5
+ ``highspy.Highs`` instance the polars LP just produced + the work
6
+ folder so they read the support CSVs (``p_step_duration.csv``,
7
+ ``process_block.csv``, …) emitted by ``FlexToolRunner.write_input`` /
8
+ ``preprocessing_solve_time``.
9
+
10
+ Variable-name reconciliation: the polars LP carries
11
+ ``v_invest_p[<entity>,<period>]`` / ``v_invest_n[…]`` /
12
+ ``v_divest_p[…]`` / ``v_divest_n[…]`` whereas the extractor expects
13
+ unified ``v_invest[…]`` / ``v_divest[…]``. We resolve in-place by
14
+ renaming the live HiGHS column names BEFORE delegating;
15
+ ``passColName`` accepts a post-solve update. The rename is a no-op
16
+ for variables already named correctly.
17
+
18
+ ``v_ramp`` is absent from the polars LP entirely — the extractor
19
+ handles "no matching columns" gracefully by emitting an empty-but-
20
+ well-shaped parquet (see ``read_highs_solution.extract_variable``'s
21
+ ``if not seen_cols`` fallback).
22
+
23
+ The carry-forward dependency is the support CSV cluster
24
+ (``p_step_duration.csv``, ``process_block.csv``, ``entity_block.csv``,
25
+ …) read by ``read_highs_solution._apply_block_expand`` /
26
+ ``handoff_writers._load_*``. Δ.2-Δ.10 retire those one cluster at a
27
+ time; Δ.1's job is to consume them via the writers, not to replace.
28
+
29
+ ``periods_already_emitted`` carrier
30
+ -----------------------------------
31
+ ``handoff_writers._bump_period_capacity`` accumulates this set on disk
32
+ into ``solve_data/period_capacity.csv``. Δ.1 removed the in-memory
33
+ mirror that previously lived on :class:`SolveHandoff` and put it
34
+ where it belongs — :class:`OutputWriterState`, this module's
35
+ per-cascade scratch carrier.
36
+ """
37
+ from __future__ import annotations
38
+
39
+ import logging
40
+ from dataclasses import dataclass, field
41
+ from pathlib import Path
42
+ from typing import TYPE_CHECKING
43
+
44
+ if TYPE_CHECKING:
45
+ from polar_high import Solution
46
+
47
+ from flextool.engine_polars._solve_handoff import SolveHandoff
48
+ from flextool.engine_polars.input import FlexData
49
+
50
+ _logger = logging.getLogger(__name__)
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Per-cascade writer state
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ @dataclass
59
+ class OutputWriterState:
60
+ """Cross-solve carrier for the output writer.
61
+
62
+ Δ.1 placement: ``periods_already_emitted`` was previously a field on
63
+ :class:`SolveHandoff` (and is still populated there for backward
64
+ compatibility — Δ.2-Δ.10 will retire that mirror). The canonical
65
+ in-memory location for new consumers is this writer-owned state.
66
+
67
+ The set is bumped after each successful adapter call by reading
68
+ back the file ``handoff_writers._bump_period_capacity`` just
69
+ overwrote; that's the same source of truth flextool uses.
70
+
71
+ Other writer-level scratch state (e.g. solve_progress.csv append
72
+ pointers if ever ported) belongs here too.
73
+ """
74
+
75
+ # Per-period bare-set; periods accumulated by ``_bump_period_capacity``
76
+ # across the cascade. Empty initially; grows monotonically.
77
+ periods_already_emitted: set[str] = field(default_factory=set)
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Variable-name reconciliation
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ # Map from polars-LP variable name (in ``Solution._vars``) to the
86
+ # flextool-writer variable name (the prefix the extractor's regex
87
+ # expects). Entries with identical source/target are no-ops; we list
88
+ # them only for clarity.
89
+ _VAR_RENAME: dict[str, str] = {
90
+ "v_invest_p": "v_invest",
91
+ "v_invest_n": "v_invest",
92
+ "v_divest_p": "v_divest",
93
+ "v_divest_n": "v_divest",
94
+ }
95
+
96
+
97
+ def _rename_invest_columns(sol: "Solution") -> None:
98
+ """Rename ``v_invest_p`` / ``v_invest_n`` / ``v_divest_*`` columns
99
+ in-place on the live HiGHS instance to the unified flextool names.
100
+
101
+ The polars LP splits each invest/divest decision into two
102
+ non-negative columns (process-side and node-side); flextool's
103
+ writers expect a single ``v_invest[<entity>,<period>]`` /
104
+ ``v_divest[<entity>,<period>]`` family. Since the entity sets for
105
+ ``_p`` and ``_n`` are disjoint (one is process-only, the other
106
+ node-only, by construction in
107
+ ``flextool/engine_polars/model.py``), the union of the renamed
108
+ columns is itself a well-formed ``v_invest`` family.
109
+
110
+ ``passColName`` is invoked per column-id derived from the polars
111
+ Var's ``frame["col_id"]``. This is O(n_cols_to_rename) — typically
112
+ < 1000 for realistic cases. No-op if ``sol.highs is None``.
113
+ """
114
+ h = getattr(sol, "highs", None)
115
+ if h is None:
116
+ return # Solution synthesized outside a real solve — adapter
117
+ # caller is responsible for ensuring this doesn't happen
118
+ # on the cascade path.
119
+
120
+ for src_name, dst_name in _VAR_RENAME.items():
121
+ if src_name not in sol._vars:
122
+ continue
123
+ v = sol._vars[src_name]
124
+ if not v.dims:
125
+ continue
126
+ # Build the destination col-name strings — same bracket payload,
127
+ # just a different prefix. We reuse the existing col_names list
128
+ # we already computed at solve time (sol.col_names) to avoid
129
+ # re-rendering them; the only diff is the prefix.
130
+ ids = v.frame["col_id"].to_numpy().tolist()
131
+ for cid in ids:
132
+ old_name = sol.col_names[cid]
133
+ # old_name is "v_invest_p[entity,period]" — replace the
134
+ # prefix only, keep the bracketed payload verbatim.
135
+ assert old_name is not None and old_name.startswith(src_name + "[")
136
+ new_name = dst_name + old_name[len(src_name):]
137
+ h.passColName(cid, new_name)
138
+ # Mirror onto the Solution's name array so downstream
139
+ # consumers that look at ``sol.col_names`` (e.g. tests) see
140
+ # the unified name too.
141
+ sol.col_names[cid] = new_name
142
+
143
+
144
+ def _restore_space_mangled_names(sol: "Solution") -> None:
145
+ """Restore original entity names that HiGHS' solution-file writer
146
+ mangled (spaces → underscores) on the live in-process LP.
147
+
148
+ ``writeSolution`` (triggered by ``write_solution_to_file``) rewrites
149
+ every space in the LP's column/row names to an underscore, in place,
150
+ and the output extractors read those mutated names back via
151
+ ``allVariableNames()`` / ``getLp().row_names_``. :class:`Solution`
152
+ keeps the un-mangled originals in ``col_names`` / ``row_names``
153
+ (rendered from the polars Var frames, position-aligned with the LP),
154
+ so we re-pass the ones that contain a space.
155
+
156
+ HiGHS mangles *only* the space character — tabs, slashes, commas,
157
+ quotes and brackets survive verbatim — so a ``' ' in name`` test
158
+ covers exactly the affected names. Models with no spaces do zero
159
+ ``passColName`` / ``passRowName`` calls, keeping byte-parity for
160
+ every existing fixture. Gated on ``passRowName`` so it runs only for
161
+ a real ``highspy.Highs`` (the cold-path ``_SolHighsShim`` never
162
+ mangles names and exposes no such method).
163
+ """
164
+ h = getattr(sol, "highs", None)
165
+ if h is None or not hasattr(h, "passRowName"):
166
+ return # cold/subprocess shim (immune) or no live solver
167
+
168
+ col_names = getattr(sol, "col_names", None)
169
+ if col_names is not None:
170
+ for cid, name in enumerate(col_names):
171
+ if name is not None and " " in name:
172
+ h.passColName(cid, name)
173
+
174
+ row_names = getattr(sol, "row_names", None)
175
+ if row_names is not None:
176
+ for rid, name in enumerate(row_names):
177
+ if name is not None and " " in name:
178
+ h.passRowName(rid, name)
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Main entry point
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ def write_outputs_for_solve(
187
+ sol: "Solution",
188
+ *,
189
+ work_folder: Path | str,
190
+ solve_name: str,
191
+ prior_handoff: "SolveHandoff | None" = None,
192
+ writer_state: "OutputWriterState | None" = None,
193
+ flex_data: "FlexData | None" = None,
194
+ is_first_solve: bool | None = None,
195
+ scale_the_objective: float | None = None,
196
+ provider: "object | None" = None,
197
+ csv_dump: bool = False,
198
+ ) -> None:
199
+ """Adapter — emit TIER A artefacts for one cascade sub-solve.
200
+
201
+ Calls flextool's ``process_outputs.read_highs_solution.write_all_variables``
202
+ (~30 variable parquets to ``output_raw/``) and
203
+ ``process_outputs.handoff_writers.write_all_handoffs`` (handoff
204
+ CSVs in ``output_raw/``) using the live HiGHS
205
+ instance carried on :class:`Solution`. The writers consume the
206
+ support CSVs in ``solve_data/`` that flextool's preprocessing has
207
+ already produced — Δ.1's carry-forward dependency.
208
+
209
+ ``writer_state`` (optional) accumulates cross-solve scratch
210
+ (``periods_already_emitted``). When present, the set is updated
211
+ from the freshly-bumped ``solve_data/period_capacity.csv``.
212
+
213
+ No-ops gracefully when ``sol.highs is None`` (no live solver
214
+ instance available — typically a synthesized Solution in a unit
215
+ test); callers should pass solutions from a real
216
+ :func:`polar_high.Problem.solve` call. Post task #15 every cold
217
+ solve (HiGHS and commercial) goes through
218
+ :func:`flextool.engine_polars._subprocess_solve.solve_via_subprocess`,
219
+ which always returns a Solution with a populated ``highs`` instance
220
+ read back from the MPS — so the None branch is only hit by unit
221
+ tests that synthesize Solutions by hand.
222
+ """
223
+ h = getattr(sol, "highs", None)
224
+ if h is None:
225
+ _logger.warning(
226
+ "write_outputs_for_solve: Solution carries no live HiGHS "
227
+ "instance (sol.highs is None); skipping output emission for "
228
+ "solve '%s'", solve_name,
229
+ )
230
+ return
231
+
232
+ work_folder = Path(work_folder)
233
+ output_dir = work_folder / "output_raw"
234
+ output_dir.mkdir(parents=True, exist_ok=True)
235
+
236
+ # Variable-name shim — flextool's writers see ``v_invest[…]`` /
237
+ # ``v_divest[…]`` after this call. In-place mutation on the live
238
+ # HiGHS; safe because the Solution is read-only post-solve and the
239
+ # adapter consumes it once.
240
+ _rename_invest_columns(sol)
241
+
242
+ # Undo HiGHS' space→underscore name mangling. When the in-process
243
+ # solve writes a solution file (``write_solution_to_file=true``,
244
+ # highs.opt.template), HiGHS' ``writeSolution`` permanently rewrites
245
+ # spaces to underscores in the live LP's column/row names (it happens
246
+ # for every solution style — the GLPSOL/pretty writers are whitespace-
247
+ # delimited). The output extractors below read ``h.allVariableNames()``
248
+ # / ``h.getLp().row_names_`` from that same mutated LP, so any entity
249
+ # whose name contains a space (e.g. a node "AY. ATHANASIOS") comes back
250
+ # with an underscore and no longer joins against ``flex_data``'s
251
+ # original names — a KeyError in calc_capacity_flows / silently-empty
252
+ # dual columns. ``Solution.col_names`` / ``.row_names`` are rendered
253
+ # from the polars Var frames (never round-tripped through the mangling
254
+ # writer), so they are the authoritative, position-aligned originals;
255
+ # restore them onto the live HiGHS. The cold/subprocess path is immune
256
+ # by construction (generic MPS names, index-mapped back — see
257
+ # _subprocess_solve) and its shim exposes no ``passRowName``, so gate
258
+ # on that to run only for the real in-process ``highspy.Highs``.
259
+ _restore_space_mangled_names(sol)
260
+
261
+ # ``scale_the_objective`` — the polars LP now applies the resolved
262
+ # per-solve ``scale_the_objective`` at LP construction (engine_polars/
263
+ # scaling.py auto-apply, commits 19aca81b / 2682cea1 / 4c3b49ca /
264
+ # 8bac7d70). ``_orchestration._write_scale_csv_and_report`` already
265
+ # writes ``solve_data/scale_the_objective.csv`` with the effective
266
+ # value before this adapter runs; the downstream writers' multiplier
267
+ # (``_resolve_inv_scale_the_objective``) then un-scales objective /
268
+ # dual values back to user-facing units. An earlier shim here
269
+ # forced the CSV to ``value=1.0`` from the era when the polars LP
270
+ # did NOT scale — that override is now destructive (it canceled
271
+ # the un-scale, leaving ``v_obj`` at the LP-internal magnitude
272
+ # ~1e6× too small). Removed: trust the upstream CSV.
273
+
274
+ # Late imports — keep the adapter's import surface narrow for the
275
+ # 99% of callers that never instantiate it.
276
+ from flextool.process_outputs.read_highs_solution import (
277
+ _actual_solve_name,
278
+ write_all_variables,
279
+ )
280
+ from flextool.process_outputs.handoff_writers import write_all_handoffs
281
+
282
+ # Some scenarios use a "complete-solve" name distinct from the
283
+ # roll/solve-current name written into solve_data/ CSVs. Mirror
284
+ # solver_runner._run_highs_or_cplex's resolution.
285
+ roll_name = _actual_solve_name(work_folder, solve_name, provider=provider)
286
+
287
+ # Multi-solve output union — record this sub-solve in the creation-
288
+ # order manifest (``output_raw/_solve_order.txt``). The per-roll
289
+ # variable / param / set parquet readers concat in this order so the
290
+ # ``drop_levels`` ``keep='last'`` dedup picks the same realizing roll
291
+ # the in-memory oracle does. Authoritative on the default
292
+ # ``keep_solutions=False`` flow, where ``solve__p_entity_pre_existing
293
+ # .csv`` is absent (so ``load_solve_order`` would otherwise fall back
294
+ # to filesystem glob order and mis-pick the realizing solve label).
295
+ try:
296
+ from flextool.process_outputs.solve_order import append_solve_order
297
+
298
+ append_solve_order(output_dir, roll_name)
299
+ except Exception as exc: # noqa: BLE001
300
+ _logger.warning(
301
+ "append_solve_order failed for solve '%s': %s", solve_name, exc,
302
+ )
303
+
304
+ sd = work_folder / "solve_data"
305
+ realized_dispatch_csv = sd / "realized_dispatch.csv"
306
+ realized_periods_csv = sd / "realized_invest_periods_of_current_solve.csv"
307
+
308
+ # Provider-only existence check. Post-Step-2 the cascade has one
309
+ # data pathway; the on-disk CSVs are debug dumps, never an input.
310
+ def _provider_has_frame(name: str) -> bool:
311
+ return provider is not None and provider.has(name)
312
+ try:
313
+ write_all_variables(
314
+ h,
315
+ solve_name=roll_name,
316
+ output_dir=output_dir,
317
+ realized_dispatch_csv=(
318
+ realized_dispatch_csv
319
+ if _provider_has_frame(
320
+ "solve_data/realized_dispatch",
321
+ ) else None
322
+ ),
323
+ realized_periods_csv=(
324
+ realized_periods_csv
325
+ if _provider_has_frame(
326
+ "solve_data/realized_invest_periods_of_current_solve",
327
+ ) else None
328
+ ),
329
+ # Phase G — route in-memory carriers through to the
330
+ # extractor + custom writers so per-iter file reads
331
+ # (_load_canonical_*, _load_inflation_*, _load_row_scaler,
332
+ # scale_the_objective.csv) can short-circuit. CSV fallback
333
+ # preserved.
334
+ flex_data=flex_data,
335
+ scale_the_objective=scale_the_objective,
336
+ provider=provider,
337
+ )
338
+ except Exception as exc: # noqa: BLE001
339
+ _logger.warning(
340
+ "write_all_variables failed for solve '%s': %s", solve_name, exc,
341
+ )
342
+
343
+ try:
344
+ write_all_handoffs(
345
+ h, solve_name=roll_name, work_folder=work_folder,
346
+ prior_handoff=prior_handoff,
347
+ flex_data=flex_data,
348
+ writer_state=writer_state,
349
+ is_first_solve=is_first_solve,
350
+ scale_the_objective=scale_the_objective,
351
+ provider=provider,
352
+ csv_dump=csv_dump,
353
+ )
354
+ except Exception as exc: # noqa: BLE001
355
+ _logger.warning(
356
+ "write_all_handoffs failed for solve '%s': %s", solve_name, exc,
357
+ )
358
+
359
+ # Multi-solve output union (stage 2a) — persist this roll's realized
360
+ # slice of every per-roll-VARYING parameter to ``output_raw/`` as
361
+ # parquet, mirroring the variable convention
362
+ # (``{attr}__{solve}.parquet``). Runs HERE, while the Solution is
363
+ # live, because ``entity_all_capacity`` needs ``solution.value(
364
+ # "v_invest")`` (nulled on prior steps by memory slimming). The
365
+ # stage-3 reader unions these per-roll parquets into the full
366
+ # output, fixing the last-roll-only collapse. ``flex_data`` carries
367
+ # the realized (period, time) dispatch slice; the realized-invest
368
+ # periods come from the per-solve CSV / provider frame (same source
369
+ # the variable writer's ``realized_periods_csv`` uses).
370
+ if flex_data is not None:
371
+ from flextool.process_outputs.persist_realized_slice import (
372
+ write_all_params_realized_slice,
373
+ write_all_sets_realized_slice,
374
+ )
375
+ from flextool.process_outputs.read_highs_solution import (
376
+ _load_realized_periods,
377
+ )
378
+ try:
379
+ invest_periods = _load_realized_periods(
380
+ realized_periods_csv
381
+ if _provider_has_frame(
382
+ "solve_data/realized_invest_periods_of_current_solve",
383
+ ) else None,
384
+ provider=provider,
385
+ )
386
+ except Exception as exc: # noqa: BLE001
387
+ _logger.warning(
388
+ "loading realized invest periods failed for solve '%s': %s",
389
+ solve_name, exc,
390
+ )
391
+ invest_periods = None
392
+ try:
393
+ write_all_params_realized_slice(
394
+ flex_data,
395
+ sol,
396
+ solve_name=roll_name,
397
+ output_dir=output_dir,
398
+ realized_invest_periods=invest_periods,
399
+ )
400
+ except Exception as exc: # noqa: BLE001
401
+ _logger.warning(
402
+ "write_all_params_realized_slice failed for solve '%s': %s",
403
+ solve_name, exc,
404
+ )
405
+
406
+ # Multi-solve output union (stage 2b) — persist this roll's
407
+ # realized slice of every per-roll-VARYING set (incl. the
408
+ # storage-adjacency sets ``dtt`` / ``dtttdt``) to ``output_raw/``
409
+ # as parquet, mirroring the variable / param convention. Runs
410
+ # HERE, while the Solution is live, alongside the param writer so
411
+ # the per-roll realized window (``flex_data.realized_dispatch``)
412
+ # is available. The stage-3 reader unions these per-roll set
413
+ # parquets, replacing the dead ``read_sets_multi`` source.
414
+ try:
415
+ write_all_sets_realized_slice(
416
+ flex_data,
417
+ sol,
418
+ solve_name=roll_name,
419
+ output_dir=output_dir,
420
+ realized_invest_periods=invest_periods,
421
+ )
422
+ except Exception as exc: # noqa: BLE001
423
+ _logger.warning(
424
+ "write_all_sets_realized_slice failed for solve '%s': %s",
425
+ solve_name, exc,
426
+ )
427
+
428
+ # Phase G — ``writer_state.periods_already_emitted`` is updated
429
+ # in-place by ``handoff_writers._bump_period_capacity`` when the
430
+ # writer_state is threaded through (above). The previous paranoia
431
+ # re-read of ``solve_data/period_capacity.csv`` was redundant because
432
+ # ``_bump_period_capacity`` is the sole producer and now updates
433
+ # both sinks atomically. No file re-read here.
434
+
435
+
436
+ __all__ = [
437
+ "OutputWriterState",
438
+ "write_outputs_for_solve",
439
+ ]