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,774 @@
1
+ """Per-solve set / invest-divest writers.
2
+
3
+ Two preprocessing families called per-solve (not per-write-input):
4
+
5
+ * ``per_solve_sets.write_per_solve_sets`` — ~24 set-projection / union
6
+ outputs covering ``branch_set``, ``year_set``, ``period_*_set``,
7
+ ``rp_*_set``, ``dt*``, ``period__timeline_set``, ``cnd_ladder*`` and
8
+ the small storage-fix / online sets.
9
+ * ``invest_divest_sets.write_invest_divest_sets`` +
10
+ ``write_ed_invest_forbidden_no_investment`` — the
11
+ ed_invest / ed_divest / edd_history family + the gd_* group
12
+ projections + the no-investment-lifetime gate.
13
+
14
+ Style mirrors :mod:`._emit_leaf_sets` / :mod:`._emit_mid_sets`:
15
+ eager polars CSV reads, expression chains, ``unique(maintain_order=True)``
16
+ for legacy-bit-identical row order, missing files treated as empty
17
+ frames.
18
+
19
+ Output bytes are CSV-identical to the legacy emitter — verified by
20
+ ``tests/engine_polars/test_writer_port_phase1.py`` parity tests.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+
26
+ import polars as pl
27
+
28
+ from flextool.engine_polars import _provider_keys as K
29
+ from flextool.engine_polars._emit_provider_io import _emit
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Method-enum constants — mirror preprocessing/invest_divest_sets.py and
34
+ # flextool/flextool_base.dat. Frozen sets so identity-equality checks at
35
+ # join time are stable.
36
+ # ---------------------------------------------------------------------------
37
+
38
+ _INVEST_PERIOD_METHODS: frozenset[str] = frozenset((
39
+ "invest_period", "invest_period_total",
40
+ "invest_retire_period", "invest_retire_period_total",
41
+ ))
42
+ _DIVEST_PERIOD_METHODS: frozenset[str] = frozenset((
43
+ "retire_period", "retire_period_total",
44
+ "invest_retire_period", "invest_retire_period_total",
45
+ ))
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # CSV I/O helpers — same conventions as _emit_leaf_sets / _emit_mid_sets.
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ def _read_csv(path: Path, columns: list[str],
54
+ *, provider: "object | None" = None) -> pl.DataFrame:
55
+ """Provider-only — returns an empty all-Utf8 frame on Provider miss.
56
+ Step 2.5 Phase C dropped the disk-fallback arm.
57
+ """
58
+ from flextool.engine_polars._emit_provider_io import (
59
+ _provider_key,
60
+ _provider_lookup_positional,
61
+ )
62
+ seeded = _provider_lookup_positional(
63
+ provider, _provider_key(path), path, columns,
64
+ )
65
+ if seeded is not None:
66
+ return seeded
67
+ return pl.DataFrame(
68
+ {c: [] for c in columns},
69
+ schema={c: pl.Utf8 for c in columns},
70
+ )
71
+
72
+
73
+ def _to_utf8_frame(
74
+ headers: tuple[str, ...],
75
+ rows: list[tuple],
76
+ ) -> pl.DataFrame:
77
+ """Build an all-``Utf8`` polars frame from a header tuple + row list."""
78
+ cols: dict[str, list[str]] = {h: [] for h in headers}
79
+ for row in rows:
80
+ for h, v in zip(headers, row):
81
+ cols[h].append(v if isinstance(v, str) else str(v))
82
+ return pl.DataFrame(cols, schema={h: pl.Utf8 for h in headers})
83
+
84
+
85
+ def _emit_singles(provider, key: str, header: str, values: list[str]) -> None:
86
+ """Provider-emit a one-column header + rows frame under *key*."""
87
+ _emit(provider, key, _to_utf8_frame((header,), [(v,) for v in values]))
88
+
89
+
90
+ def _emit_tuples(provider, key: str, header: tuple[str, ...],
91
+ rows: list[tuple[str, ...]]) -> None:
92
+ """Provider-emit a multi-column header + tuple rows frame under *key*."""
93
+ _emit(provider, key, _to_utf8_frame(header, rows))
94
+
95
+
96
+ def _read_singles(path: Path,
97
+ *, provider: "object | None" = None) -> list[str]:
98
+ """List the first column of a header+rows CSV, dropping blanks."""
99
+ df = _read_csv(path, ["v"], provider=provider)
100
+ return [v for v in df["v"].to_list() if v]
101
+
102
+
103
+ def _read_pairs(path: Path,
104
+ *, provider: "object | None" = None) -> list[tuple[str, str]]:
105
+ """List 2-tuples from a 2+-column CSV, dropping rows with any blank."""
106
+ df = _read_csv(path, ["a", "b"], provider=provider)
107
+ return [(a, b) for a, b in zip(df["a"].to_list(), df["b"].to_list())
108
+ if a and b]
109
+
110
+
111
+ def _project_column(df: pl.DataFrame, col_idx: int) -> list[str]:
112
+ """Ordered-unique projection of the *col_idx* column, dropping blanks.
113
+
114
+ Mirrors the legacy ``_project_column`` helper's dict-of-None semantics
115
+ by using polars' ``unique(maintain_order=True)`` over a single-column
116
+ frame.
117
+ """
118
+ if col_idx >= len(df.columns):
119
+ return []
120
+ col_name = df.columns[col_idx]
121
+ s = df[col_name].cast(pl.Utf8).fill_null("")
122
+ return [v for v in s.unique(maintain_order=True).to_list() if v]
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Family A — per_solve_sets.write_per_solve_sets
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ def emit_per_solve_sets(solve_data_dir: Path, *, provider) -> None:
131
+ """Emit ``per_solve_sets`` to the Provider.
132
+ Emits the same ~24 frames under ``solve_data/<basename>`` keys via
133
+ :func:`_emit` (dual-key registration). *solve_data_dir* is retained
134
+ because the function still reads sister CSVs from disk via the
135
+ Provider-only read helpers (which derive keys from the path).
136
+ """
137
+ input_dir = solve_data_dir.parent / "input"
138
+
139
+ df = _read_csv(solve_data_dir / "period__branch.csv", ["period", "branch"], provider=provider)
140
+ _emit_singles(provider, "solve_data/branch_set.csv", "branch",
141
+ _project_column(df, 1))
142
+
143
+ df = _read_csv(solve_data_dir / "p_years_represented.csv",
144
+ ["period", "year"], provider=provider)
145
+ _emit_singles(provider, "solve_data/year_set.csv", "year",
146
+ _project_column(df, 1))
147
+
148
+ df = _read_csv(solve_data_dir / "steps_in_timeline.csv",
149
+ ["period", "step"], provider=provider)
150
+ _emit_singles(provider, "solve_data/period_from_period_time_set.csv",
151
+ "period", _project_column(df, 0))
152
+
153
+ df = _read_csv(solve_data_dir / "steps_in_use.csv",
154
+ ["period", "time"], provider=provider)
155
+ _emit_singles(provider, "solve_data/period_in_use_set.csv", "period",
156
+ _project_column(df, 0))
157
+ _emit_singles(provider, "solve_data/time_in_use_set.csv", "time",
158
+ _project_column(df, 1))
159
+
160
+ df = _read_csv(solve_data_dir / "steps_complete_solve.csv",
161
+ ["period", "time"], provider=provider)
162
+ _emit_singles(provider, "solve_data/complete_time_in_use_set.csv", "time",
163
+ _project_column(df, 1))
164
+
165
+ # rp_base_period_set / rp_rep_period_set used to be derived here from
166
+ # rp_weights.csv, but emit_per_solve_sets runs BEFORE emit_rp_data in
167
+ # the cascade — the Provider doesn't yet hold ``rp_weights`` at this
168
+ # point, so the derivation always wrote empty frames. Both sets are
169
+ # now emitted by ``_emit_solve_writers._compute_rp_frames`` (which
170
+ # owns the rp_weights data) so they land populated for the RP path
171
+ # and empty under ``emit_empty_rp_data`` for the non-RP path.
172
+
173
+ df = _read_csv(solve_data_dir / "period_block_time.csv",
174
+ ["period", "block_first", "step"], provider=provider)
175
+ pb_pairs = [
176
+ (p, b) for p, b in zip(df["period"].to_list(),
177
+ df["block_first"].to_list())
178
+ if p and b
179
+ ]
180
+ pb_dedup: list[tuple[str, str]] = []
181
+ seen: dict[tuple[str, str], None] = {}
182
+ for r in pb_pairs:
183
+ if r not in seen:
184
+ seen[r] = None
185
+ pb_dedup.append(r)
186
+ _emit_tuples(provider, "solve_data/period_block_set.csv",
187
+ ("period", "block_first"), pb_dedup)
188
+
189
+ df = _read_csv(
190
+ solve_data_dir / "step_previous.csv",
191
+ ["period", "time", "time_previous", "tprev_within_ts",
192
+ "period_previous", "tprev_within_solve"], provider=provider
193
+ )
194
+ triples = list(zip(
195
+ df["period"].to_list(),
196
+ df["time"].to_list(),
197
+ df["time_previous"].to_list(),
198
+ ))
199
+ seen3: dict[tuple[str, str, str], None] = {}
200
+ dtt_rows: list[tuple[str, str, str]] = []
201
+ for r in triples:
202
+ if all(r) and r not in seen3:
203
+ seen3[r] = None
204
+ dtt_rows.append(r)
205
+ _emit_tuples(provider, "solve_data/dtt_set.csv",
206
+ ("period", "time", "time_previous"), dtt_rows)
207
+
208
+ df = _read_csv(solve_data_dir / "fix_storage_timesteps.csv",
209
+ ["period", "step"], provider=provider)
210
+ _emit_singles(provider, "solve_data/d_fix_storage_period_set.csv",
211
+ "period", _project_column(df, 0))
212
+
213
+ pa = _read_csv(input_dir / "periods_available.csv", ["period"], provider=provider)
214
+ pfpt = _read_csv(
215
+ solve_data_dir / "period_from_period_time_set.csv", ["period"], provider=provider
216
+ )
217
+ seen_p: dict[str, None] = {}
218
+ for v in pa["period"].to_list() + pfpt["period"].to_list():
219
+ if v and v not in seen_p:
220
+ seen_p[v] = None
221
+ _emit_singles(provider, "solve_data/period_set.csv", "period",
222
+ list(seen_p.keys()))
223
+
224
+ seen_pa: dict[str, None] = {}
225
+ for fname in (
226
+ "period_group.csv", "period_node.csv", "period_commodity.csv",
227
+ "period_process.csv", "period_solve.csv",
228
+ ):
229
+ for v in _read_csv(solve_data_dir / fname, ["period"], provider=provider)["period"].to_list():
230
+ if v and v not in seen_pa:
231
+ seen_pa[v] = None
232
+ for v in _read_csv(solve_data_dir / "branch_set.csv", ["period"], provider=provider)["period"].to_list():
233
+ if v and v not in seen_pa:
234
+ seen_pa[v] = None
235
+ _emit_singles(provider, "solve_data/periodAll_set.csv", "period",
236
+ list(seen_pa.keys()))
237
+
238
+ seen_b: dict[str, None] = {}
239
+
240
+ df = _read_csv(solve_data_dir / "entity_block.csv", ["entity", "block"], provider=provider)
241
+ for v in df["block"].to_list():
242
+ if v and v not in seen_b:
243
+ seen_b[v] = None
244
+
245
+ df = _read_csv(solve_data_dir / "process_side_block.csv",
246
+ ["process", "side", "block"], provider=provider)
247
+ for v in df["block"].to_list():
248
+ if v and v not in seen_b:
249
+ seen_b[v] = None
250
+
251
+ df = _read_csv(solve_data_dir / "process_block.csv",
252
+ ["process", "block"], provider=provider)
253
+ for v in df["block"].to_list():
254
+ if v and v not in seen_b:
255
+ seen_b[v] = None
256
+
257
+ df = _read_csv(solve_data_dir / "block_step_duration.csv",
258
+ ["block", "period", "step", "duration"], provider=provider)
259
+ for v in df["block"].to_list():
260
+ if v and v not in seen_b:
261
+ seen_b[v] = None
262
+
263
+ df = _read_csv(solve_data_dir / "overlap_set.csv",
264
+ ["period", "b_coarse", "t_coarse",
265
+ "b_fine", "t_fine", "fraction"], provider=provider)
266
+ for v in df["b_coarse"].to_list():
267
+ if v and v not in seen_b:
268
+ seen_b[v] = None
269
+ for v in df["b_fine"].to_list():
270
+ if v and v not in seen_b:
271
+ seen_b[v] = None
272
+
273
+ _emit_singles(provider, "solve_data/block_set.csv", "block",
274
+ list(seen_b.keys()))
275
+
276
+ spt = _read_csv(input_dir / "timesets_in_use.csv",
277
+ ["solve", "period", "timeset"], provider=provider)
278
+ cur_solve = _read_csv(solve_data_dir / "solve_current.csv", ["solve"], provider=provider)
279
+ cur_solve_set = frozenset(v for v in cur_solve["solve"].to_list() if v)
280
+ tt = _read_csv(input_dir / "timesets__timeline.csv",
281
+ ["timeset", "timeline"], provider=provider)
282
+ tb_to_tl: dict[str, list[str]] = {}
283
+ for ts, tl in zip(tt["timeset"].to_list(), tt["timeline"].to_list()):
284
+ if ts and tl:
285
+ tb_to_tl.setdefault(ts, []).append(tl)
286
+ pt_seen: dict[tuple[str, str], None] = {}
287
+ for s, d, tb in zip(spt["solve"].to_list(),
288
+ spt["period"].to_list(),
289
+ spt["timeset"].to_list()):
290
+ if s in cur_solve_set:
291
+ for tl in tb_to_tl.get(tb, ()):
292
+ key = (d, tl)
293
+ if key not in pt_seen:
294
+ pt_seen[key] = None
295
+ _emit_tuples(provider, "solve_data/period__timeline_set.csv",
296
+ ("period", "timeline"), list(pt_seen.keys()))
297
+
298
+ enable = _read_csv(solve_data_dir / "enable_optional_outputs.csv",
299
+ ["flag"], provider=provider)
300
+ enable_set = frozenset(v for v in enable["flag"].to_list() if v)
301
+ if "output_horizon" in enable_set:
302
+ rows_df = _read_csv(solve_data_dir / "steps_in_use.csv",
303
+ ["period", "time"], provider=provider)
304
+ else:
305
+ rows_df = _read_csv(solve_data_dir / "realized_dispatch.csv",
306
+ ["period", "time"], provider=provider)
307
+ drd_pairs_raw = list(zip(rows_df["period"].to_list(),
308
+ rows_df["time"].to_list()))
309
+ seen_drd: dict[tuple[str, str], None] = {}
310
+ drd_pairs: list[tuple[str, str]] = []
311
+ for r in drd_pairs_raw:
312
+ if all(r) and r not in seen_drd:
313
+ seen_drd[r] = None
314
+ drd_pairs.append(r)
315
+ _emit_tuples(provider, "solve_data/dt_realize_dispatch_set.csv",
316
+ ("period", "time"), drd_pairs)
317
+
318
+ drp_seen: dict[str, None] = {}
319
+ for d, _t in drd_pairs:
320
+ if d not in drp_seen:
321
+ drp_seen[d] = None
322
+ _emit_singles(provider, "solve_data/d_realized_period_set.csv", "period",
323
+ list(drp_seen.keys()))
324
+
325
+ drealize = _read_csv(
326
+ solve_data_dir / "realized_invest_periods_of_current_solve.csv",
327
+ ["period"], provider=provider
328
+ )
329
+ union_seen: dict[str, None] = dict(drp_seen)
330
+ for v in drealize["period"].to_list():
331
+ if v and v not in union_seen:
332
+ union_seen[v] = None
333
+ _emit_singles(provider,
334
+ "solve_data/d_realize_dispatch_or_invest_set.csv", "period",
335
+ list(union_seen.keys()))
336
+
337
+ a_df = _read_csv(solve_data_dir / "realized_dispatch.csv",
338
+ ["period", "time"], provider=provider)
339
+ b_df = _read_csv(solve_data_dir / "fix_storage_timesteps.csv",
340
+ ["period", "time"], provider=provider)
341
+ dtna_seen: dict[tuple[str, str], None] = {}
342
+ for src in (a_df, b_df):
343
+ for d, t in zip(src["period"].to_list(), src["time"].to_list()):
344
+ if d and t and (d, t) not in dtna_seen:
345
+ dtna_seen[(d, t)] = None
346
+ _emit_tuples(provider, "solve_data/dt_non_anticipativity_set.csv",
347
+ ("period", "time"), list(dtna_seen.keys()))
348
+
349
+ cn_df = _read_csv(input_dir / "commodity__node.csv",
350
+ ["commodity", "node"], provider=provider)
351
+ cn_pairs = [
352
+ (c, n) for c, n in zip(cn_df["commodity"].to_list(),
353
+ cn_df["node"].to_list())
354
+ if c and n
355
+ ]
356
+ pin_use_df = _read_csv(solve_data_dir / "period_in_use_set.csv",
357
+ ["period"], provider=provider)
358
+ pin_use = [v for v in pin_use_df["period"].to_list() if v]
359
+ with_ladder = frozenset(
360
+ v for v in _read_csv(solve_data_dir / "commodity_with_ladder.csv",
361
+ ["commodity"], provider=provider)["commodity"].to_list() if v
362
+ )
363
+ with_ladder_cum = frozenset(
364
+ v for v in _read_csv(
365
+ solve_data_dir / "commodity_with_ladder_cumulative.csv",
366
+ ["commodity"], provider=provider
367
+ )["commodity"].to_list() if v
368
+ )
369
+ with_ladder_ann = frozenset(
370
+ v for v in _read_csv(
371
+ solve_data_dir / "commodity_with_ladder_annual.csv",
372
+ ["commodity"], provider=provider
373
+ )["commodity"].to_list() if v
374
+ )
375
+ cum_df = _read_csv(input_dir / "commodity_ladder_cumulative.csv",
376
+ ["commodity", "tier"], provider=provider)
377
+ ann_df = _read_csv(solve_data_dir / "commodity__tier_ann.csv",
378
+ ["commodity", "tier"], provider=provider)
379
+ tiers_for_cum: dict[str, list[str]] = {}
380
+ for c, i in zip(cum_df["commodity"].to_list(), cum_df["tier"].to_list()):
381
+ if c and i:
382
+ tiers_for_cum.setdefault(c, []).append(i)
383
+ tiers_for_ann: dict[str, list[str]] = {}
384
+ for c, i in zip(ann_df["commodity"].to_list(), ann_df["tier"].to_list()):
385
+ if c and i:
386
+ tiers_for_ann.setdefault(c, []).append(i)
387
+
388
+ cnd_rows = [
389
+ (c, n, d) for (c, n) in cn_pairs for d in pin_use
390
+ if c in with_ladder
391
+ ]
392
+ _emit_tuples(provider, "solve_data/cnd_ladder_set.csv",
393
+ ("commodity", "node", "period"),
394
+ list(dict.fromkeys(cnd_rows)))
395
+
396
+ cum_rows_out: list[tuple[str, str, str, str]] = []
397
+ for (c, n) in cn_pairs:
398
+ if c not in with_ladder_cum:
399
+ continue
400
+ tiers = tiers_for_cum.get(c, ())
401
+ for d in pin_use:
402
+ for i in tiers:
403
+ cum_rows_out.append((c, n, d, i))
404
+ _emit_tuples(provider, "solve_data/cndi_ladder_cum_set.csv",
405
+ ("commodity", "node", "period", "tier"),
406
+ list(dict.fromkeys(cum_rows_out)))
407
+
408
+ ann_rows_out: list[tuple[str, str, str, str]] = []
409
+ for (c, n) in cn_pairs:
410
+ if c not in with_ladder_ann:
411
+ continue
412
+ tiers = tiers_for_ann.get(c, ())
413
+ for d in pin_use:
414
+ for i in tiers:
415
+ ann_rows_out.append((c, n, d, i))
416
+ _emit_tuples(provider, "solve_data/cndi_ladder_ann_set.csv",
417
+ ("commodity", "node", "period", "tier"),
418
+ list(dict.fromkeys(ann_rows_out)))
419
+
420
+ union4: dict[tuple[str, str, str, str], None] = {}
421
+ for r in cum_rows_out + ann_rows_out:
422
+ if r not in union4:
423
+ union4[r] = None
424
+ _emit_tuples(provider, "solve_data/cndi_ladder_set.csv",
425
+ ("commodity", "node", "period", "tier"),
426
+ list(union4.keys()))
427
+
428
+ spv_df = _read_csv(
429
+ solve_data_dir / "step_previous.csv",
430
+ ["period", "time", "time_previous", "tprev_within_ts",
431
+ "period_previous", "tprev_within_solve"], provider=provider
432
+ )
433
+ quads_raw = list(zip(
434
+ spv_df["period_previous"].to_list(),
435
+ spv_df["tprev_within_solve"].to_list(),
436
+ spv_df["period"].to_list(),
437
+ spv_df["time"].to_list(),
438
+ ))
439
+ seen_q: dict[tuple[str, str, str, str], None] = {}
440
+ quads: list[tuple[str, str, str, str]] = []
441
+ for r in quads_raw:
442
+ if all(r) and r not in seen_q:
443
+ seen_q[r] = None
444
+ quads.append(r)
445
+ _emit_tuples(provider, "solve_data/dtdt_next_set.csv",
446
+ ("period_prev", "time_prev_solve", "period", "time"), quads)
447
+
448
+ # Phase 4.1g — derive ``n_fix_storage_{quantity,price,usage}_set`` from
449
+ # the canonical handoff Provider keys (``handoff/fix_storage_<metric>``,
450
+ # schema ``[node, period, step, p_fix_storage_<metric>]``). Only the
451
+ # ``node`` column is consumed; the translator seeds these keys at
452
+ # iteration start, replacing the legacy ``solve_data/fix_storage_*``
453
+ # Provider read path ahead of the wide-field deletion in 4.1k.
454
+ from flextool.engine_polars._provider_translators import read_handoff_frame
455
+ for handoff_key, dst in (
456
+ (K.HANDOFF_FIX_STORAGE_QUANTITY, "n_fix_storage_quantity_set.csv"),
457
+ (K.HANDOFF_FIX_STORAGE_PRICE, "n_fix_storage_price_set.csv"),
458
+ (K.HANDOFF_FIX_STORAGE_USAGE, "n_fix_storage_usage_set.csv"),
459
+ ):
460
+ df = read_handoff_frame(provider, handoff_key)
461
+ if df is None:
462
+ nodes: list[str] = []
463
+ else:
464
+ nodes = _project_column(df, 0)
465
+ _emit_singles(provider, f"solve_data/{dst}", "node", nodes)
466
+
467
+ proc_online = frozenset(
468
+ v for v in _read_csv(solve_data_dir / "process_online.csv",
469
+ ["process"], provider=provider)["process"].to_list() if v
470
+ )
471
+ proc_blocks: dict[str, list[str]] = {}
472
+ pb_df = _read_csv(solve_data_dir / "process_block.csv",
473
+ ["process", "block"], provider=provider)
474
+ for p, b in zip(pb_df["process"].to_list(), pb_df["block"].to_list()):
475
+ if p and b:
476
+ proc_blocks.setdefault(p, []).append(b)
477
+ block_dt: dict[str, set[tuple[str, str]]] = {}
478
+ bsd_df = _read_csv(solve_data_dir / "block_step_duration.csv",
479
+ ["block", "period", "step", "duration"], provider=provider)
480
+ for b, d, t in zip(bsd_df["block"].to_list(),
481
+ bsd_df["period"].to_list(),
482
+ bsd_df["step"].to_list()):
483
+ if b and d and t:
484
+ block_dt.setdefault(b, set()).add((d, t))
485
+ su_df = _read_csv(solve_data_dir / "steps_in_use.csv",
486
+ ["period", "time"], provider=provider)
487
+ dt_set = frozenset(
488
+ (d, t) for d, t in zip(su_df["period"].to_list(),
489
+ su_df["time"].to_list())
490
+ if d and t
491
+ )
492
+ online_rows: list[tuple[str, str, str]] = []
493
+ for p in proc_online:
494
+ seen_pdt: set[tuple[str, str]] = set()
495
+ blocks_for_p = proc_blocks.get(p)
496
+ if blocks_for_p:
497
+ for b in blocks_for_p:
498
+ for d, t in block_dt.get(b, ()):
499
+ if (d, t) in dt_set and (d, t) not in seen_pdt:
500
+ seen_pdt.add((d, t))
501
+ online_rows.append((p, d, t))
502
+ else:
503
+ for d, t in zip(su_df["period"].to_list(),
504
+ su_df["time"].to_list()):
505
+ if d and t and (d, t) not in seen_pdt:
506
+ seen_pdt.add((d, t))
507
+ online_rows.append((p, d, t))
508
+ _emit_tuples(provider, "solve_data/p_online_dt_set.csv",
509
+ ("process", "period", "step"), online_rows)
510
+
511
+
512
+ # ---------------------------------------------------------------------------
513
+ # Family B — invest_divest_sets.write_invest_divest_sets
514
+ # + write_ed_invest_forbidden_no_investment
515
+ # ---------------------------------------------------------------------------
516
+
517
+
518
+ def _read_keyed_value_kv(path: Path,
519
+ *, provider: "object | None" = None) -> dict[str, float]:
520
+ """Read a (key, value) CSV into a dict (float values, blanks skipped)."""
521
+ df = _read_csv(path, ["key", "value"], provider=provider)
522
+ out: dict[str, float] = {}
523
+ for k, v in zip(df["key"].to_list(), df["value"].to_list()):
524
+ if not k or v is None or v == "":
525
+ continue
526
+ try:
527
+ out[k] = float(v)
528
+ except ValueError:
529
+ continue
530
+ return out
531
+
532
+
533
+ def _read_pdv(path: Path,
534
+ *, provider: "object | None" = None) -> dict[tuple[str, str], float]:
535
+ """Read a 3-col (entity, period, value) CSV into a dict."""
536
+ df = _read_csv(path, ["entity", "period", "value"], provider=provider)
537
+ out: dict[tuple[str, str], float] = {}
538
+ for e, d, v in zip(df["entity"].to_list(),
539
+ df["period"].to_list(),
540
+ df["value"].to_list()):
541
+ if not e or not d:
542
+ continue
543
+ try:
544
+ out[(e, d)] = float(v)
545
+ except (ValueError, TypeError):
546
+ continue
547
+ return out
548
+
549
+
550
+ def emit_invest_divest_sets(input_dir: Path, solve_data_dir: Path,
551
+ *, provider) -> None:
552
+ """Emit ``invest_divest_sets`` to the Provider.
553
+ Emits the same 15 frames under ``solve_data/<basename>`` keys via
554
+ :func:`_emit` (dual-key registration).
555
+ """
556
+ entityInvest = _read_singles(solve_data_dir / "entityInvest.csv", provider=provider)
557
+ entityDivest = _read_singles(solve_data_dir / "entityDivest.csv", provider=provider)
558
+ period_invest = _read_singles(
559
+ solve_data_dir / "invest_periods_of_current_solve.csv", provider=provider
560
+ )
561
+ period_in_use = _read_singles(solve_data_dir / "period_in_use_set.csv", provider=provider)
562
+ period_with_history = _read_singles(
563
+ solve_data_dir / "period_with_history.csv", provider=provider
564
+ )
565
+ process_set = frozenset(_read_singles(input_dir / "process.csv", provider=provider))
566
+ node_set = frozenset(_read_singles(input_dir / "node.csv", provider=provider))
567
+ entity_set = _read_singles(input_dir / "entity.csv", provider=provider)
568
+
569
+ pcc_inv = _read_pairs(
570
+ input_dir / "p_process_constraint_invested_capacity_coeff.csv", provider=provider
571
+ )
572
+ pcc_pre = _read_pairs(
573
+ input_dir / "p_process_constraint_pre_built_capacity_coeff.csv", provider=provider
574
+ )
575
+ ncc_inv = _read_pairs(
576
+ input_dir / "p_node_constraint_invested_capacity_coeff.csv", provider=provider
577
+ )
578
+ ncc_pre = _read_pairs(
579
+ input_dir / "p_node_constraint_pre_built_capacity_coeff.csv", provider=provider
580
+ )
581
+ has_capacity_constraint = (
582
+ frozenset(p for p, _ in pcc_inv)
583
+ | frozenset(p for p, _ in pcc_pre)
584
+ | frozenset(n for n, _ in ncc_inv)
585
+ | frozenset(n for n, _ in ncc_pre)
586
+ )
587
+
588
+ eea = _read_pdv(solve_data_dir / "ed_entity_annual.csv", provider=provider)
589
+ eead = _read_pdv(solve_data_dir / "ed_entity_annual_divest.csv", provider=provider)
590
+
591
+ eim = _read_pairs(input_dir / "entity__invest_method.csv", provider=provider)
592
+ methods_for_e: dict[str, set[str]] = {}
593
+ for e, m in eim:
594
+ methods_for_e.setdefault(e, set()).add(m)
595
+
596
+ elm = _read_pairs(solve_data_dir / "entity__lifetime_method.csv", provider=provider)
597
+ lm_for_e: dict[str, set[str]] = {}
598
+ for e, m in elm:
599
+ lm_for_e.setdefault(e, set()).add(m)
600
+
601
+ p_years_d = _read_keyed_value_kv(solve_data_dir / "p_years_d.csv", provider=provider)
602
+ edEntity_lifetime = _read_pdv(solve_data_dir / "edEntity_lifetime.csv", provider=provider)
603
+
604
+ group_invest = _read_singles(solve_data_dir / "group_invest.csv", provider=provider)
605
+ group_divest = _read_singles(solve_data_dir / "group_divest.csv", provider=provider)
606
+ del group_divest
607
+ group_entity = _read_pairs(solve_data_dir / "group_entity.csv", provider=provider)
608
+ entities_for_g: dict[str, list[str]] = {}
609
+ for g, e in group_entity:
610
+ entities_for_g.setdefault(g, []).append(e)
611
+ gim = _read_pairs(input_dir / "group__invest_method.csv", provider=provider)
612
+ methods_for_g: dict[str, set[str]] = {}
613
+ for g, m in gim:
614
+ methods_for_g.setdefault(g, set()).add(m)
615
+
616
+ ed_invest_pairs: list[tuple[str, str]] = []
617
+ for e in entityInvest:
618
+ has_cap = e in has_capacity_constraint
619
+ for d in period_invest:
620
+ if eea.get((e, d), 0.0) != 0.0 or has_cap:
621
+ ed_invest_pairs.append((e, d))
622
+ _emit_tuples(provider, "solve_data/ed_invest.csv",
623
+ ("entity", "period"), ed_invest_pairs)
624
+ ed_invest_set = frozenset(ed_invest_pairs)
625
+
626
+ ed_divest_pairs: list[tuple[str, str]] = []
627
+ for e in entityDivest:
628
+ has_cap = e in has_capacity_constraint
629
+ for d in period_invest:
630
+ if eead.get((e, d), 0.0) != 0.0 or has_cap:
631
+ ed_divest_pairs.append((e, d))
632
+ _emit_tuples(provider, "solve_data/ed_divest.csv",
633
+ ("entity", "period"), ed_divest_pairs)
634
+ ed_divest_set = frozenset(ed_divest_pairs)
635
+
636
+ rows = [(e, d) for e, d in ed_invest_pairs
637
+ if methods_for_e.get(e, set()) & _INVEST_PERIOD_METHODS]
638
+ _emit_tuples(provider, "solve_data/ed_invest_period.csv",
639
+ ("entity", "period"), rows)
640
+ rows = [(e, d) for e, d in ed_invest_pairs
641
+ if methods_for_e.get(e, set()) & _DIVEST_PERIOD_METHODS]
642
+ _emit_tuples(provider, "solve_data/ed_divest_period.csv",
643
+ ("entity", "period"), rows)
644
+ rows = [(e, d) for e, d in ed_invest_pairs
645
+ if "cumulative_limits" in methods_for_e.get(e, set())]
646
+ _emit_tuples(provider, "solve_data/ed_invest_cumulative.csv",
647
+ ("entity", "period"), rows)
648
+
649
+ _emit_tuples(provider, "solve_data/pd_invest.csv", ("process", "period"),
650
+ [(e, d) for e, d in ed_invest_pairs if e in process_set])
651
+ _emit_tuples(provider, "solve_data/nd_invest.csv", ("node", "period"),
652
+ [(e, d) for e, d in ed_invest_pairs if e in node_set])
653
+ _emit_tuples(provider, "solve_data/pd_divest.csv", ("process", "period"),
654
+ [(e, d) for e, d in ed_divest_pairs if e in process_set])
655
+ _emit_tuples(provider, "solve_data/nd_divest.csv", ("node", "period"),
656
+ [(e, d) for e, d in ed_divest_pairs if e in node_set])
657
+
658
+ edd_choice: list[tuple[str, str, str]] = []
659
+ edd_auto: list[tuple[str, str, str]] = []
660
+ edd_noinv: list[tuple[str, str, str]] = []
661
+ for e in entity_set:
662
+ e_lm = lm_for_e.get(e, set())
663
+ is_choice = "reinvest_choice" in e_lm
664
+ is_auto = "reinvest_automatic" in e_lm
665
+ is_noinv = "no_investment" in e_lm
666
+ if not (is_choice or is_auto or is_noinv):
667
+ continue
668
+ for d_h in period_with_history:
669
+ life = edEntity_lifetime.get((e, d_h), 0.0)
670
+ pdy_dh = p_years_d.get(d_h, 0.0)
671
+ for d in period_in_use:
672
+ pdy_d = p_years_d.get(d, 0.0)
673
+ if is_choice and pdy_d >= pdy_dh and pdy_d < pdy_dh + life:
674
+ edd_choice.append((e, d_h, d))
675
+ if is_auto and pdy_d >= pdy_dh:
676
+ edd_auto.append((e, d_h, d))
677
+ if is_noinv and pdy_d >= pdy_dh and pdy_d < pdy_dh + life:
678
+ edd_noinv.append((e, d_h, d))
679
+ _emit_tuples(provider, "solve_data/edd_history_choice.csv",
680
+ ("entity", "period_history", "period"), edd_choice)
681
+ _emit_tuples(provider, "solve_data/edd_history_automatic.csv",
682
+ ("entity", "period_history", "period"), edd_auto)
683
+ _emit_tuples(provider, "solve_data/edd_history_no_investment.csv",
684
+ ("entity", "period_history", "period"), edd_noinv)
685
+
686
+ edd_seen: dict[tuple[str, str, str], None] = {}
687
+ for r in edd_choice + edd_auto + edd_noinv:
688
+ if r not in edd_seen:
689
+ edd_seen[r] = None
690
+ edd_history = list(edd_seen.keys())
691
+ _emit_tuples(provider, "solve_data/edd_history.csv",
692
+ ("entity", "period_history", "period"), edd_history)
693
+ invest_set = frozenset(entityInvest)
694
+ edd_history_invest = [r for r in edd_history if r[0] in invest_set]
695
+ _emit_tuples(provider, "solve_data/edd_history_invest.csv",
696
+ ("entity", "period_history", "period"), edd_history_invest)
697
+ edd_invest = [
698
+ (e, d_inv, d) for (e, d_inv, d) in edd_history_invest
699
+ if (e, d_inv) in ed_invest_set
700
+ ]
701
+ _emit_tuples(provider, "solve_data/edd_invest.csv",
702
+ ("entity", "period_history", "period"), edd_invest)
703
+ _ = ed_divest_set
704
+
705
+ gd_invest_pairs: list[tuple[str, str]] = []
706
+ for g in group_invest:
707
+ ents = entities_for_g.get(g, ())
708
+ for d in period_invest:
709
+ if any((e, d) in ed_invest_set for e in ents):
710
+ gd_invest_pairs.append((g, d))
711
+ _emit_tuples(provider, "solve_data/gd_invest.csv",
712
+ ("group", "period"), gd_invest_pairs)
713
+ _emit_tuples(provider, "solve_data/gd_divest.csv",
714
+ ("group", "period"), gd_invest_pairs[:])
715
+
716
+ rows = [
717
+ (g, d) for g, d in gd_invest_pairs
718
+ if methods_for_g.get(g, set()) & _INVEST_PERIOD_METHODS
719
+ ]
720
+ _emit_tuples(provider, "solve_data/gd_invest_period.csv",
721
+ ("group", "period"), rows)
722
+ rows = [
723
+ (g, d) for g, d in gd_invest_pairs
724
+ if methods_for_g.get(g, set()) & _DIVEST_PERIOD_METHODS
725
+ ]
726
+ _emit_tuples(provider, "solve_data/gd_divest_period.csv",
727
+ ("group", "period"), rows)
728
+
729
+
730
+ def emit_ed_invest_forbidden_no_investment(
731
+ input_dir: Path, solve_data_dir: Path,
732
+ *, provider,
733
+ ) -> None:
734
+ """Emit ``ed_invest_forbidden_no_investment`` to the Provider."""
735
+ del input_dir # legacy signature parity; no input/ reads here.
736
+ ed_invest_pairs = _read_pairs(solve_data_dir / "ed_invest.csv", provider=provider)
737
+ elm = _read_pairs(solve_data_dir / "entity__lifetime_method.csv", provider=provider)
738
+ no_invest_set = frozenset(e for e, m in elm if m == "no_investment")
739
+
740
+ p_years_d: dict[str, float] = {}
741
+ pyd_df = _read_csv(solve_data_dir / "p_years_d.csv",
742
+ ["period", "value"], provider=provider)
743
+ for d, v in zip(pyd_df["period"].to_list(), pyd_df["value"].to_list()):
744
+ if not d:
745
+ continue
746
+ try:
747
+ p_years_d[d] = float(v)
748
+ except (ValueError, TypeError):
749
+ continue
750
+
751
+ ed_lifetime = _read_pdv(solve_data_dir / "edEntity_lifetime.csv", provider=provider)
752
+ period_first = _read_singles(solve_data_dir / "period_first.csv", provider=provider)
753
+
754
+ def _life_sum(e: str) -> float:
755
+ return sum(
756
+ p_years_d.get(d_first, 0.0)
757
+ + ed_lifetime.get((e, d_first), 0.0)
758
+ for d_first in period_first
759
+ )
760
+
761
+ cached_sum: dict[str, float] = {}
762
+ rows: list[tuple[str, str]] = []
763
+ for e, d in ed_invest_pairs:
764
+ if e not in no_invest_set:
765
+ continue
766
+ s = cached_sum.get(e)
767
+ if s is None:
768
+ s = _life_sum(e)
769
+ cached_sum[e] = s
770
+ if p_years_d.get(d, 0.0) >= s:
771
+ rows.append((e, d))
772
+ _emit_tuples(provider,
773
+ "solve_data/ed_invest_forbidden_no_investment.csv",
774
+ ("entity", "period"), rows)