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,859 @@
1
+ """Mid-level set / param projections.
2
+
3
+ Each family reads a small handful of ``input/*.csv`` (and,
4
+ occasionally, a leaf-level ``solve_data/*.csv`` written earlier by
5
+ :mod:`._emit_leaf_sets`) and emits one or more ``solve_data/`` CSVs.
6
+
7
+ Families:
8
+
9
+ * ``node_type_sets.py`` — 77 LOC — 4 partitions of node by p_node_type
10
+ * ``union_sets.py`` — 69 LOC — 2 ordered-union 2-tuples
11
+ * ``dc_angle_bounds.py`` — 52 LOC — per-DC-node angle bounds
12
+ * ``reserve_method_partitions.py`` — 73 LOC — 3 method partitions
13
+ * ``nonsync_sets.py`` — 153 LOC — process__sink_nonSync + group_inside_group_nonSync
14
+ * ``method_with_fallback_sets.py`` — 194 LOC — 5 per-entity fallback method tables
15
+ * ``invest_total_sets.py`` — 113 LOC — 5 invest/divest-total filters + ci_ladder_cumulative
16
+ * ``structural_filters.py`` — 182 LOC — 6 single-condition filters
17
+
18
+ Total ~913 LOC of legacy code ported. Each ``derive_*`` returns a
19
+ fresh ``pl.DataFrame`` (the in-memory contract); ``write_*`` wrappers
20
+ materialise the frame to the legacy ``solve_data/*.csv`` path so
21
+ downstream consumers continue to read identical bytes.
22
+
23
+ Style mirrors :mod:`._emit_leaf_sets`: eager polars reads of tiny
24
+ CSVs, expression chains, ``unique(maintain_order=True)`` for ordered
25
+ dedup. Constants mirror the legacy module's literals one-for-one.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from pathlib import Path
30
+
31
+ import polars as pl
32
+
33
+ from flextool.engine_polars._axis_enums import rename_to_axis
34
+ from flextool.engine_polars._emit_provider_io import _emit
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # CSV I/O — same conventions as _emit_leaf_sets:
39
+ # * eager read, missing file → empty frame with requested schema
40
+ # * positional column rename (handle legacy headers that differ in label)
41
+ # * empty frame still writes header line
42
+ # ---------------------------------------------------------------------------
43
+
44
+ def _read_csv(path: Path, columns: list[str],
45
+ *, provider: "object | None" = None) -> pl.DataFrame:
46
+ """Read a tiny flextool CSV with positional column rename.
47
+
48
+ Forces every column to ``Utf8`` (via ``infer_schema_length=0``)
49
+ regardless of the CSV's header names or data shape. This matters
50
+ because the ``columns=`` arg is the *post-rename* target — it
51
+ cannot be used as ``schema_overrides=`` keys, since those match
52
+ CSV header names (e.g. ``p_commodity.csv`` carries the header
53
+ ``commodity,commodityParam,p_commodity`` and gets renamed to
54
+ ``commodity,param,value`` here). Without forcing Utf8, polars
55
+ type-inference picks Float64 for all-numeric value columns and
56
+ downstream ``pl.col(...) != ""`` filters raise
57
+ ``cannot compare string with numeric type``.
58
+ """
59
+ # Step 2.5 Phase C — Provider-only. Returns an empty all-Utf8
60
+ # frame on Provider miss (matches legacy missing-CSV behaviour).
61
+ from flextool.engine_polars._emit_provider_io import (
62
+ _provider_key,
63
+ _provider_lookup_positional,
64
+ )
65
+ seeded = _provider_lookup_positional(
66
+ provider, _provider_key(path), path, columns,
67
+ )
68
+ if seeded is not None:
69
+ return seeded
70
+ return pl.DataFrame(
71
+ {c: [] for c in columns}, schema={c: pl.Utf8 for c in columns},
72
+ )
73
+
74
+
75
+ def _drop_blank_rows(df: pl.DataFrame, required_cols: list[str]) -> pl.DataFrame:
76
+ expr = pl.col(required_cols[0]) != ""
77
+ for c in required_cols[1:]:
78
+ expr = expr & (pl.col(c) != "")
79
+ return df.filter(expr)
80
+
81
+
82
+ # ===========================================================================
83
+ # Family 5 — node_type_sets (legacy: preprocessing/node_type_sets.py)
84
+ # ===========================================================================
85
+
86
+ # Mirror flextool.mod:192 ``default 'balance'`` clause on p_node_type.
87
+ _DEFAULT_NODE_TYPE = "balance"
88
+
89
+ # (output filename, set of effective types that match the partition)
90
+ _NODE_TYPE_PARTITIONS: list[tuple[str, frozenset[str]]] = [
91
+ ("nodeCommodity.csv", frozenset(("commodity",))),
92
+ ("nodeBalance.csv", frozenset(("balance", "storage"))),
93
+ ("nodeState.csv", frozenset(("storage",))),
94
+ ("nodeBalancePeriod.csv", frozenset(("balance_within_period",))),
95
+ ]
96
+
97
+
98
+ def derive_node_effective_type(input_dir: Path,
99
+ *, provider: "object | None" = None,
100
+ ) -> pl.DataFrame:
101
+ """Materialize every node with its effective ``p_node_type``.
102
+
103
+ Nodes without an explicit row in ``p_node_type.csv`` get the
104
+ flextool.mod default ``'balance'``. Order = ``node.csv`` order
105
+ (mod's would-be iteration order).
106
+ """
107
+ nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
108
+ nodes = _drop_blank_rows(nodes, ["node"])
109
+ explicit = _read_csv(input_dir / "p_node_type.csv", ["node", "type"],
110
+ provider=provider)
111
+ explicit = _drop_blank_rows(explicit, ["node", "type"])
112
+ return (
113
+ nodes.join(explicit, on="node", how="left")
114
+ .with_columns(
115
+ pl.col("type").fill_null(_DEFAULT_NODE_TYPE),
116
+ )
117
+ )
118
+
119
+
120
+ def emit_node_type_sets(input_dir: Path, solve_data_dir: Path,
121
+ *, provider) -> None:
122
+ """Emit ``node_type_sets`` to the Provider."""
123
+ del solve_data_dir
124
+ effective = derive_node_effective_type(input_dir, provider=provider)
125
+ for fname, types in _NODE_TYPE_PARTITIONS:
126
+ out = (
127
+ effective.filter(pl.col("type").is_in(list(types)))
128
+ .select("node")
129
+ )
130
+ _emit(provider, f"solve_data/{fname}", out)
131
+
132
+
133
+ # ===========================================================================
134
+ # Family 6 — union_sets (legacy: preprocessing/union_sets.py)
135
+ # ===========================================================================
136
+
137
+ def _ordered_union_pairs(
138
+ sources: list[pl.DataFrame], columns: list[str],
139
+ ) -> pl.DataFrame:
140
+ """Vertical-concat then dedupe preserving first-occurrence order."""
141
+ aligned = [df.select(columns) for df in sources]
142
+ return (
143
+ pl.concat(aligned, how="vertical")
144
+ .pipe(_drop_blank_rows, columns)
145
+ .unique(maintain_order=True)
146
+ )
147
+
148
+
149
+ def derive_group_entity(input_dir: Path,
150
+ *, provider: "object | None" = None,
151
+ ) -> pl.DataFrame:
152
+ """flextool.mod:287 — ``group_process ∪ group_node``."""
153
+ gp = _read_csv(input_dir / "group__process.csv", ["group", "entity"],
154
+ provider=provider)
155
+ gn = _read_csv(input_dir / "group__node.csv", ["group", "entity"],
156
+ provider=provider)
157
+ return _ordered_union_pairs([gp, gn], ["group", "entity"])
158
+
159
+
160
+ def derive_process_delayed__duration(input_dir: Path,
161
+ *, provider: "object | None" = None,
162
+ ) -> pl.DataFrame:
163
+ """flextool.mod:950 — ``process_delay_weighted ∪ process_delay_single``."""
164
+ w = _read_csv(input_dir / "p_process_delay_weighted.csv",
165
+ ["process", "delay_duration"], provider=provider)
166
+ s = _read_csv(input_dir / "process_delay_single.csv",
167
+ ["process", "delay_duration"], provider=provider)
168
+ return _ordered_union_pairs([w, s], ["process", "delay_duration"])
169
+
170
+
171
+ def emit_group_entity(input_dir: Path, solve_data_dir: Path,
172
+ *, provider) -> None:
173
+ """Emit ``group_entity`` to the Provider."""
174
+ del solve_data_dir
175
+ _emit(provider, "solve_data/group_entity.csv",
176
+ derive_group_entity(input_dir, provider=provider))
177
+
178
+
179
+ def emit_process_delayed__duration(input_dir: Path, solve_data_dir: Path,
180
+ *, provider) -> None:
181
+ """Emit ``process_delayed__duration`` to the Provider."""
182
+ del solve_data_dir
183
+ _emit(provider, "solve_data/process_delayed__duration.csv",
184
+ derive_process_delayed__duration(input_dir, provider=provider))
185
+
186
+
187
+ # ===========================================================================
188
+ # Family 8 — reserve_method_partitions
189
+ # (legacy: preprocessing/reserve_method_partitions.py)
190
+ # ===========================================================================
191
+
192
+ _RESERVE_TIMESERIES_METHODS: frozenset[str] = frozenset((
193
+ "timeseries_only", "timeseries_and_dynamic",
194
+ "timeseries_and_large_failure", "all",
195
+ ))
196
+ _RESERVE_DYNAMIC_METHODS: frozenset[str] = frozenset((
197
+ "dynamic_only", "timeseries_and_dynamic",
198
+ "dynamic_and_large_failure", "all",
199
+ ))
200
+ _RESERVE_N_1_METHODS: frozenset[str] = frozenset((
201
+ "large_failure_only", "timeseries_and_large_failure",
202
+ "dynamic_and_large_failure", "all",
203
+ ))
204
+
205
+ _RESERVE_QUAD_COLS = ["reserve", "upDown", "group", "method"]
206
+
207
+
208
+ def derive_reserve_method_partition(
209
+ input_dir: Path, allowed: frozenset[str],
210
+ *, provider: "object | None" = None,
211
+ ) -> pl.DataFrame:
212
+ """4-tuple rows whose method ∈ allowed. Order preserved, deduped."""
213
+ quad = _read_csv(input_dir / "reserve__upDown__group__method.csv",
214
+ _RESERVE_QUAD_COLS, provider=provider)
215
+ quad = _drop_blank_rows(quad, _RESERVE_QUAD_COLS)
216
+ return (
217
+ quad.filter(pl.col("method").is_in(list(allowed)))
218
+ .unique(maintain_order=True)
219
+ )
220
+
221
+
222
+ def emit_reserve_partitions(input_dir: Path, solve_data_dir: Path,
223
+ *, provider) -> None:
224
+ """Emit ``reserve_partitions`` to the Provider."""
225
+ del solve_data_dir
226
+ for fname, allowed in (
227
+ ("reserve__upDown__group__method_timeseries.csv", _RESERVE_TIMESERIES_METHODS),
228
+ ("reserve__upDown__group__method_dynamic.csv", _RESERVE_DYNAMIC_METHODS),
229
+ ("reserve__upDown__group__method_n_1.csv", _RESERVE_N_1_METHODS),
230
+ ):
231
+ _emit(provider, f"solve_data/{fname}",
232
+ derive_reserve_method_partition(input_dir, allowed,
233
+ provider=provider))
234
+
235
+
236
+ # ===========================================================================
237
+ # Family 9 — nonsync_sets (legacy: preprocessing/nonsync_sets.py)
238
+ # ===========================================================================
239
+
240
+ def derive_process__sink_nonSync(input_dir: Path,
241
+ *, provider: "object | None" = None,
242
+ ) -> pl.DataFrame:
243
+ """flextool.mod:1980-1985 — 3-branch OR over sink/source membership.
244
+
245
+ Output: (process, sink) 2-tuples. Branch order (deduped):
246
+ 1. ``(p, sink) in process_sink`` AND ``(p, sink) in process__sink_nonSync_unit``
247
+ 2. ``(p, sink) in process_sink`` AND ``p in process_nonSync_connection``
248
+ 3. ``(p, source) in process_source`` AND ``p in process_nonSync_connection``
249
+ """
250
+ sinks = _read_csv(input_dir / "process__sink.csv", ["process", "sink"],
251
+ provider=provider)
252
+ sinks = _drop_blank_rows(sinks, ["process", "sink"])
253
+ sources = _read_csv(input_dir / "process__source.csv", ["process", "sink"],
254
+ provider=provider)
255
+ sources = _drop_blank_rows(sources, ["process", "sink"])
256
+ nonsync_units = _read_csv(
257
+ input_dir / "process__sink_nonSync_unit.csv", ["process", "sink"],
258
+ provider=provider,
259
+ )
260
+ nonsync_conn = _read_csv(
261
+ input_dir / "process_nonSync_connection.csv", ["process"],
262
+ provider=provider,
263
+ )
264
+ nonsync_conn_set = (
265
+ nonsync_conn.filter(pl.col("process") != "")
266
+ .get_column("process").to_list()
267
+ )
268
+
269
+ # Branch 1+2: walk sinks once, admit if either condition matches.
270
+ sink_matches = sinks.join(
271
+ nonsync_units, on=["process", "sink"], how="inner",
272
+ ).select("process", "sink")
273
+ sink_conn_matches = sinks.filter(
274
+ pl.col("process").is_in(nonsync_conn_set)
275
+ ).select("process", "sink")
276
+ # Branch 3: source rows admitted whenever process ∈ nonSync_connection.
277
+ source_conn_matches = sources.filter(
278
+ pl.col("process").is_in(nonsync_conn_set)
279
+ ).select("process", "sink")
280
+
281
+ # Legacy walks sinks first (in order), THEN sources. Within the sink
282
+ # walk, branch 1 takes precedence over branch 2 but neither emits
283
+ # duplicates because the dict deduplicates. Reproducing that with
284
+ # vertical-concat-then-unique preserves the same first-seen order.
285
+ combined = pl.concat(
286
+ [sink_matches, sink_conn_matches, source_conn_matches],
287
+ how="vertical",
288
+ )
289
+ return combined.unique(maintain_order=True)
290
+
291
+
292
+ def derive_process__group_inside_group_nonSync(input_dir: Path,
293
+ *, provider: "object | None" = None,
294
+ ) -> pl.DataFrame:
295
+ """flextool.mod:2017-2023 — exists (source, sink) ∈ g, source ≠ sink, for p.
296
+
297
+ Iterate process × groupNonSync in CSV order to match the order the
298
+ mod's nested loops would produce.
299
+ """
300
+ nonsync_groups = _read_csv(input_dir / "groupNonSync.csv", ["group"],
301
+ provider=provider)
302
+ nonsync_groups = _drop_blank_rows(nonsync_groups, ["group"])
303
+ if nonsync_groups.height == 0:
304
+ return pl.DataFrame({"process": [], "group": []},
305
+ schema={"process": pl.Utf8, "group": pl.Utf8})
306
+
307
+ processes = _read_csv(input_dir / "process.csv", ["process"],
308
+ provider=provider)
309
+ processes = _drop_blank_rows(processes, ["process"])
310
+ group_nodes = _read_csv(input_dir / "group__node.csv", ["group", "node"],
311
+ provider=provider)
312
+ group_nodes = _drop_blank_rows(group_nodes, ["group", "node"])
313
+ sources = _read_csv(input_dir / "process__source.csv", ["process", "node"],
314
+ provider=provider)
315
+ sources = _drop_blank_rows(sources, ["process", "node"])
316
+ sinks = _read_csv(input_dir / "process__sink.csv", ["process", "node"],
317
+ provider=provider)
318
+ sinks = _drop_blank_rows(sinks, ["process", "node"])
319
+
320
+ # Lookup tables. We materialise to python dicts because the per-row
321
+ # set-intersection logic ("∃ source ≠ sink both in g") is awkward to
322
+ # express as a pure polars expression and the input sizes are tiny.
323
+ group_node_lookup: dict[str, set[str]] = {}
324
+ for g, n in group_nodes.iter_rows():
325
+ group_node_lookup.setdefault(g, set()).add(n)
326
+ process_sources: dict[str, set[str]] = {}
327
+ for p, n in sources.iter_rows():
328
+ process_sources.setdefault(p, set()).add(n)
329
+ process_sinks: dict[str, set[str]] = {}
330
+ for p, n in sinks.iter_rows():
331
+ process_sinks.setdefault(p, set()).add(n)
332
+
333
+ nonsync_group_list = nonsync_groups.get_column("group").to_list()
334
+ rows: list[tuple[str, str]] = []
335
+ for p in processes.get_column("process").to_list():
336
+ psrc = process_sources.get(p)
337
+ psnk = process_sinks.get(p)
338
+ if not psrc or not psnk:
339
+ continue
340
+ for g in nonsync_group_list:
341
+ gnodes = group_node_lookup.get(g)
342
+ if not gnodes:
343
+ continue
344
+ srcs_in = psrc & gnodes
345
+ sinks_in = psnk & gnodes
346
+ if not srcs_in or not sinks_in:
347
+ continue
348
+ # ∃ s ≠ t with s ∈ srcs_in, t ∈ sinks_in iff NOT
349
+ # (|srcs|=1 ∧ |sinks|=1 ∧ srcs==sinks).
350
+ if (len(srcs_in) == 1 and len(sinks_in) == 1
351
+ and srcs_in == sinks_in):
352
+ continue
353
+ rows.append((p, g))
354
+ return pl.DataFrame(
355
+ {"process": [r[0] for r in rows], "group": [r[1] for r in rows]},
356
+ schema={"process": pl.Utf8, "group": pl.Utf8},
357
+ )
358
+
359
+
360
+ def emit_process__sink_nonSync(input_dir: Path, solve_data_dir: Path,
361
+ *, provider) -> None:
362
+ """Emit ``process__sink_nonSync`` to the Provider."""
363
+ del solve_data_dir
364
+ _emit(provider, "solve_data/process__sink_nonSync.csv",
365
+ derive_process__sink_nonSync(input_dir, provider=provider))
366
+
367
+
368
+ def emit_process_group_inside_group_nonsync(
369
+ input_dir: Path, solve_data_dir: Path,
370
+ *, provider,
371
+ ) -> None:
372
+ """Emit ``process_group_inside_group_nonsync`` to the Provider."""
373
+ del solve_data_dir
374
+ _emit(provider, "solve_data/process__group_inside_group_nonSync.csv",
375
+ derive_process__group_inside_group_nonSync(input_dir,
376
+ provider=provider))
377
+
378
+
379
+ # ===========================================================================
380
+ # Family 10 — method_with_fallback_sets
381
+ # (legacy: preprocessing/method_with_fallback_sets.py)
382
+ # ===========================================================================
383
+
384
+ # Single-element defaults from flextool/flextool_base.dat — mirror exactly.
385
+ _LIFETIME_METHOD_DEFAULT = "reinvest_automatic"
386
+ _CT_METHOD_REGULAR = "regular"
387
+ _CT_METHOD_CONSTANT = "constant_efficiency"
388
+ _STARTUP_METHOD_NO = "no_startup"
389
+ _INFLOW_METHOD_DEFAULT = "use_original"
390
+ _PENALTY_METHOD_DEFAULT = "regular"
391
+ _STORAGE_BINDING_METHOD_DEFAULT = "bind_forward_only"
392
+
393
+
394
+ def _per_entity_fallback(
395
+ explicit: pl.DataFrame,
396
+ entities: pl.DataFrame,
397
+ default_for: dict[str, str | None],
398
+ out_columns: tuple[str, str],
399
+ ) -> pl.DataFrame:
400
+ """Emit explicit rows + a default row for each entity without explicit.
401
+
402
+ ``default_for`` maps entity → method-or-None. ``None`` means
403
+ "skip this entity" (mirrors the ``return ()`` branch in the legacy
404
+ ``process__ct_method`` fallback for non-connection/non-unit
405
+ processes).
406
+
407
+ Order is ``entities.csv`` order; within an entity, explicit rows
408
+ preserve their CSV order.
409
+ """
410
+ entity_col, method_col = out_columns
411
+ explicit = explicit.pipe(rename_to_axis,
412
+ {explicit.columns[0]: entity_col,
413
+ explicit.columns[1]: method_col})
414
+
415
+ explicit_by_entity: dict[str, list[str]] = {}
416
+ for e, m in explicit.iter_rows():
417
+ explicit_by_entity.setdefault(e, []).append(m)
418
+
419
+ rows: list[tuple[str, str]] = []
420
+ for e in entities.get_column(entities.columns[0]).to_list():
421
+ if e in explicit_by_entity:
422
+ for m in explicit_by_entity[e]:
423
+ rows.append((e, m))
424
+ else:
425
+ default = default_for.get(e)
426
+ if default is not None:
427
+ rows.append((e, default))
428
+ return pl.DataFrame(
429
+ {entity_col: [r[0] for r in rows], method_col: [r[1] for r in rows]},
430
+ schema={entity_col: pl.Utf8, method_col: pl.Utf8},
431
+ )
432
+
433
+
434
+ def derive_entity_lifetime_method(input_dir: Path,
435
+ *, provider: "object | None" = None,
436
+ ) -> pl.DataFrame:
437
+ explicit = _read_csv(
438
+ input_dir / "entity__lifetime_method.csv",
439
+ ["entity", "lifetime_method"], provider=provider,
440
+ )
441
+ explicit = _drop_blank_rows(explicit, ["entity", "lifetime_method"])
442
+ entities = _read_csv(input_dir / "entity.csv", ["entity"],
443
+ provider=provider)
444
+ entities = _drop_blank_rows(entities, ["entity"])
445
+ defaults = {e: _LIFETIME_METHOD_DEFAULT
446
+ for e in entities.get_column("entity").to_list()}
447
+ return _per_entity_fallback(
448
+ explicit, entities, defaults, ("entity", "lifetime_method"),
449
+ )
450
+
451
+
452
+ def derive_process_ct_method(input_dir: Path,
453
+ *, provider: "object | None" = None,
454
+ ) -> pl.DataFrame:
455
+ explicit = _read_csv(
456
+ input_dir / "process__ct_method.csv", ["process", "ct_method"],
457
+ provider=provider,
458
+ )
459
+ explicit = _drop_blank_rows(explicit, ["process", "ct_method"])
460
+ processes = _read_csv(input_dir / "process.csv", ["process"],
461
+ provider=provider)
462
+ processes = _drop_blank_rows(processes, ["process"])
463
+ connections = set(
464
+ _drop_blank_rows(
465
+ _read_csv(input_dir / "process_connection.csv", ["process"],
466
+ provider=provider), ["process"],
467
+ ).get_column("process").to_list()
468
+ )
469
+ units = set(
470
+ _drop_blank_rows(
471
+ _read_csv(input_dir / "process_unit.csv", ["process"],
472
+ provider=provider), ["process"],
473
+ ).get_column("process").to_list()
474
+ )
475
+
476
+ defaults: dict[str, str | None] = {}
477
+ for p in processes.get_column("process").to_list():
478
+ if p in connections:
479
+ defaults[p] = _CT_METHOD_REGULAR
480
+ elif p in units:
481
+ defaults[p] = _CT_METHOD_CONSTANT
482
+ else:
483
+ defaults[p] = None
484
+ return _per_entity_fallback(
485
+ explicit, processes, defaults, ("process", "ct_method"),
486
+ )
487
+
488
+
489
+ def derive_node_inflow_method(input_dir: Path,
490
+ *, provider: "object | None" = None,
491
+ ) -> pl.DataFrame:
492
+ explicit = _read_csv(
493
+ input_dir / "node__inflow_method.csv", ["node", "inflow_method"],
494
+ provider=provider,
495
+ )
496
+ explicit = _drop_blank_rows(explicit, ["node", "inflow_method"])
497
+ nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
498
+ nodes = _drop_blank_rows(nodes, ["node"])
499
+ defaults = {n: _INFLOW_METHOD_DEFAULT
500
+ for n in nodes.get_column("node").to_list()}
501
+ return _per_entity_fallback(
502
+ explicit, nodes, defaults, ("node", "inflow_method"),
503
+ )
504
+
505
+
506
+ def derive_node_penalty_method(input_dir: Path,
507
+ *, provider: "object | None" = None,
508
+ ) -> pl.DataFrame:
509
+ explicit = _read_csv(
510
+ input_dir / "node__penalty_method.csv", ["node", "penalty_method"],
511
+ provider=provider,
512
+ )
513
+ explicit = _drop_blank_rows(explicit, ["node", "penalty_method"])
514
+ nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
515
+ nodes = _drop_blank_rows(nodes, ["node"])
516
+ defaults = {n: _PENALTY_METHOD_DEFAULT
517
+ for n in nodes.get_column("node").to_list()}
518
+ return _per_entity_fallback(
519
+ explicit, nodes, defaults, ("node", "penalty_method"),
520
+ )
521
+
522
+
523
+ def derive_node_storage_binding_method(input_dir: Path,
524
+ *, provider: "object | None" = None,
525
+ ) -> pl.DataFrame:
526
+ explicit = _read_csv(
527
+ input_dir / "node__storage_binding_method.csv",
528
+ ["node", "storage_binding_method"], provider=provider,
529
+ )
530
+ explicit = _drop_blank_rows(explicit, ["node", "storage_binding_method"])
531
+ nodes = _read_csv(input_dir / "node.csv", ["node"], provider=provider)
532
+ nodes = _drop_blank_rows(nodes, ["node"])
533
+ defaults = {n: _STORAGE_BINDING_METHOD_DEFAULT
534
+ for n in nodes.get_column("node").to_list()}
535
+ frame = _per_entity_fallback(
536
+ explicit, nodes, defaults, ("node", "storage_binding_method"),
537
+ )
538
+ # Single-valued contract (v54 migration): each node must appear at most
539
+ # once. A duplicate here means either a v52-era DB slipped past the
540
+ # v54 migration in update_flextool/db_migration.py, or a multi-row
541
+ # record was manually inserted into node__storage_binding_method.csv.
542
+ # Compare heights rather than silently dedup so the contract violation
543
+ # surfaces as an explicit error.
544
+ unique_node_height = frame.select("node").unique().height
545
+ if frame.height != unique_node_height:
546
+ from flextool.engine_polars._solve_state import FlexToolConfigError
547
+ dup_nodes = (frame
548
+ .group_by("node")
549
+ .agg(pl.len().alias("_count"))
550
+ .filter(pl.col("_count") > 1)
551
+ .get_column("node")
552
+ .to_list())
553
+ raise FlexToolConfigError(
554
+ "node__storage_binding_method must have a single method per "
555
+ "node (v54 single-valued contract); duplicate node entries "
556
+ f"detected: {sorted(dup_nodes)}. Re-run the v54 DB migration "
557
+ "or remove the extra rows from "
558
+ "input/node__storage_binding_method.csv."
559
+ )
560
+ return frame
561
+
562
+
563
+ def derive_process_startup_method(input_dir: Path,
564
+ *, provider: "object | None" = None,
565
+ ) -> pl.DataFrame:
566
+ explicit = _read_csv(
567
+ input_dir / "process__startup_method.csv",
568
+ ["process", "startup_method"], provider=provider,
569
+ )
570
+ explicit = _drop_blank_rows(explicit, ["process", "startup_method"])
571
+ processes = _read_csv(input_dir / "process.csv", ["process"],
572
+ provider=provider)
573
+ processes = _drop_blank_rows(processes, ["process"])
574
+ defaults = {p: _STARTUP_METHOD_NO
575
+ for p in processes.get_column("process").to_list()}
576
+ return _per_entity_fallback(
577
+ explicit, processes, defaults, ("process", "startup_method"),
578
+ )
579
+
580
+
581
+ def emit_entity_lifetime_method(input_dir: Path, solve_data_dir: Path,
582
+ *, provider) -> None:
583
+ """Emit ``entity_lifetime_method`` to the Provider."""
584
+ del solve_data_dir
585
+ _emit(provider, "solve_data/entity__lifetime_method.csv",
586
+ derive_entity_lifetime_method(input_dir, provider=provider))
587
+
588
+
589
+ def emit_process_ct_method(input_dir: Path, solve_data_dir: Path,
590
+ *, provider) -> None:
591
+ """Emit ``process_ct_method`` to the Provider."""
592
+ del solve_data_dir
593
+ _emit(provider, "solve_data/process__ct_method.csv",
594
+ derive_process_ct_method(input_dir, provider=provider))
595
+
596
+
597
+ def emit_process_startup_method(input_dir: Path, solve_data_dir: Path,
598
+ *, provider) -> None:
599
+ """Emit ``process_startup_method`` to the Provider."""
600
+ del solve_data_dir
601
+ _emit(provider, "solve_data/process__startup_method.csv",
602
+ derive_process_startup_method(input_dir, provider=provider))
603
+
604
+
605
+ def emit_node_inflow_method(input_dir: Path, solve_data_dir: Path,
606
+ *, provider) -> None:
607
+ """Emit ``node_inflow_method`` to the Provider."""
608
+ del solve_data_dir
609
+ _emit(provider, "solve_data/node__inflow_method.csv",
610
+ derive_node_inflow_method(input_dir, provider=provider))
611
+
612
+
613
+ def emit_node_penalty_method(input_dir: Path, solve_data_dir: Path,
614
+ *, provider) -> None:
615
+ """Emit ``node_penalty_method`` to the Provider."""
616
+ del solve_data_dir
617
+ _emit(provider, "solve_data/node__penalty_method.csv",
618
+ derive_node_penalty_method(input_dir, provider=provider))
619
+
620
+
621
+ def emit_node_storage_binding_method(input_dir: Path, solve_data_dir: Path,
622
+ *, provider) -> None:
623
+ """Emit ``node_storage_binding_method`` to the Provider."""
624
+ del solve_data_dir
625
+ _emit(provider, "solve_data/node__storage_binding_method.csv",
626
+ derive_node_storage_binding_method(input_dir, provider=provider))
627
+
628
+
629
+ def derive_node_storage_nested_fix_method(
630
+ input_dir: Path, *, provider: "object | None" = None,
631
+ ) -> pl.DataFrame:
632
+ """Explicit node→method rows for the nested-solve storage handoff.
633
+
634
+ Reads ``input/node__storage_nested_fix_method.csv`` (cols
635
+ ``node, storage_nested_fix_method`` from the generic
636
+ ``_PARAMETER_SPECS`` emit) and renames the value column to the
637
+ ``method`` contract every solve-time consumer expects
638
+ (``_load_handoff_aux_pair`` in ``input.py`` and the
639
+ ``build_handoff_from_solution`` producer guards, which all check for
640
+ a column literally named ``method``). This is the crux deviation
641
+ from the sibling ``derive_node_storage_binding_method``, whose
642
+ consumer reads positionally and so tolerates the value column's name.
643
+
644
+ Unlike the sibling this emits ONLY the explicit rows (no per-node
645
+ ``fix_nothing`` fallback): absence == ``fix_nothing`` == inert, and
646
+ every downstream consumer filters ``method == 'fix_<x>'``, so
647
+ unlisted nodes need no row. The frame may be empty (header only) for
648
+ models with no nested-fix node; ``_emit`` still materialises it so
649
+ the coverage manifest is satisfied and the loader's
650
+ ``height == 0 -> None`` short-circuit keeps such models unchanged.
651
+ """
652
+ explicit = _read_csv(
653
+ input_dir / "node__storage_nested_fix_method.csv",
654
+ ["node", "storage_nested_fix_method"], provider=provider,
655
+ )
656
+ explicit = _drop_blank_rows(explicit, ["node", "storage_nested_fix_method"])
657
+ return explicit.rename({"storage_nested_fix_method": "method"})
658
+
659
+
660
+ def emit_node_storage_nested_fix_method(input_dir: Path, solve_data_dir: Path,
661
+ *, provider) -> None:
662
+ """Emit ``node_storage_nested_fix_method`` to the Provider."""
663
+ del solve_data_dir
664
+ _emit(provider, "solve_data/node__storage_nested_fix_method.csv",
665
+ derive_node_storage_nested_fix_method(input_dir, provider=provider))
666
+
667
+
668
+ # ===========================================================================
669
+ # Family 11 — invest_total_sets (legacy: preprocessing/invest_total_sets.py)
670
+ # ===========================================================================
671
+
672
+ _INVEST_TOTAL_METHODS: frozenset[str] = frozenset((
673
+ "invest_total", "invest_period_total",
674
+ "invest_retire_total", "invest_retire_period_total",
675
+ ))
676
+ _RETIRE_TOTAL_METHODS: frozenset[str] = frozenset((
677
+ "retire_total", "retire_period_total",
678
+ "invest_retire_total", "invest_retire_period_total",
679
+ ))
680
+ _CUMULATIVE_METHODS: frozenset[str] = frozenset(("cumulative_limits",))
681
+
682
+
683
+ def _entities_with_method_in(
684
+ method_csv: Path, allowed: frozenset[str], col1: str,
685
+ *, provider: "object | None" = None,
686
+ ) -> set[str]:
687
+ df = _read_csv(method_csv, [col1, "method"], provider=provider)
688
+ df = _drop_blank_rows(df, [col1, "method"])
689
+ return set(
690
+ df.filter(pl.col("method").is_in(list(allowed)))
691
+ .get_column(col1).to_list()
692
+ )
693
+
694
+
695
+ def _filter_singles(
696
+ universe_csv: Path, with_method: set[str], col: str,
697
+ *, provider: "object | None" = None,
698
+ ) -> pl.DataFrame:
699
+ universe = _read_csv(universe_csv, [col], provider=provider)
700
+ universe = _drop_blank_rows(universe, [col])
701
+ return universe.filter(pl.col(col).is_in(list(with_method)))
702
+
703
+
704
+ def emit_invest_total_sets(input_dir: Path, solve_data_dir: Path,
705
+ *, provider) -> None:
706
+ """Emit ``invest_total_sets`` to the Provider."""
707
+ entity_methods_csv = input_dir / "entity__invest_method.csv"
708
+ group_methods_csv = input_dir / "group__invest_method.csv"
709
+
710
+ e_with_invest = _entities_with_method_in(
711
+ entity_methods_csv, _INVEST_TOTAL_METHODS, "entity", provider=provider)
712
+ e_with_retire = _entities_with_method_in(
713
+ entity_methods_csv, _RETIRE_TOTAL_METHODS, "entity", provider=provider)
714
+ g_with_invest = _entities_with_method_in(
715
+ group_methods_csv, _INVEST_TOTAL_METHODS, "group", provider=provider)
716
+ g_with_retire = _entities_with_method_in(
717
+ group_methods_csv, _RETIRE_TOTAL_METHODS, "group", provider=provider)
718
+ g_with_cum = _entities_with_method_in(
719
+ group_methods_csv, _CUMULATIVE_METHODS, "group", provider=provider)
720
+
721
+ _emit(provider, "solve_data/e_invest_total.csv",
722
+ _filter_singles(solve_data_dir / "entityInvest.csv", e_with_invest,
723
+ "entity", provider=provider))
724
+ _emit(provider, "solve_data/e_divest_total.csv",
725
+ _filter_singles(solve_data_dir / "entityDivest.csv", e_with_retire,
726
+ "entity", provider=provider))
727
+ _emit(provider, "solve_data/g_invest_total.csv",
728
+ _filter_singles(solve_data_dir / "group_invest.csv", g_with_invest,
729
+ "group", provider=provider))
730
+ _emit(provider, "solve_data/g_divest_total.csv",
731
+ _filter_singles(solve_data_dir / "group_divest.csv", g_with_retire,
732
+ "group", provider=provider))
733
+ _emit(provider, "solve_data/g_invest_cumulative.csv",
734
+ _filter_singles(solve_data_dir / "group_invest.csv", g_with_cum,
735
+ "group", provider=provider))
736
+
737
+
738
+ def emit_ci_ladder_cumulative(input_dir: Path, solve_data_dir: Path,
739
+ *, provider) -> None:
740
+ """Emit ``ci_ladder_cumulative`` to the Provider."""
741
+ cum = _read_csv(
742
+ input_dir / "commodity_ladder_cumulative.csv", ["commodity", "tier"],
743
+ provider=provider,
744
+ )
745
+ cum = _drop_blank_rows(cum, ["commodity", "tier"])
746
+ with_cum = _read_csv(
747
+ solve_data_dir / "commodity_with_ladder_cumulative.csv", ["commodity"],
748
+ provider=provider,
749
+ )
750
+ with_cum_set = (
751
+ with_cum.filter(pl.col("commodity") != "")
752
+ .get_column("commodity").to_list()
753
+ )
754
+ filtered = (
755
+ cum.filter(pl.col("commodity").is_in(with_cum_set))
756
+ .unique(maintain_order=True)
757
+ )
758
+ _emit(provider, "solve_data/ci_ladder_cumulative.csv", filtered)
759
+
760
+
761
+ # ===========================================================================
762
+ # Family 12 — structural_filters (legacy: preprocessing/structural_filters.py)
763
+ # ===========================================================================
764
+
765
+ def derive_commodity_node_co2(input_dir: Path,
766
+ *, provider: "object | None" = None,
767
+ ) -> pl.DataFrame:
768
+ """commodity_node filtered by ``p_commodity[c, 'co2_content'] != 0``.
769
+
770
+ flextool.mod:2011. ``default 0`` on p_commodity means commodities
771
+ without an explicit ``co2_content`` row are excluded (0 is falsy).
772
+ """
773
+ cn = _read_csv(input_dir / "commodity__node.csv", ["commodity", "node"],
774
+ provider=provider)
775
+ cn = _drop_blank_rows(cn, ["commodity", "node"])
776
+ p_commodity = _read_csv(
777
+ input_dir / "p_commodity.csv", ["commodity", "param", "value"],
778
+ provider=provider,
779
+ )
780
+ co2 = (
781
+ p_commodity.filter(
782
+ (pl.col("param") == "co2_content")
783
+ & (pl.col("value") != "")
784
+ & (pl.col("value").cast(pl.Float64, strict=False) != 0.0)
785
+ )
786
+ .select("commodity")
787
+ .unique()
788
+ )
789
+ return (
790
+ cn.join(co2, on="commodity", how="semi")
791
+ .unique(maintain_order=True)
792
+ )
793
+
794
+
795
+ def _derive_coeff_zero(
796
+ arc_csv: Path, coef_csv: Path, second_col: str,
797
+ *, provider: "object | None" = None,
798
+ ) -> pl.DataFrame:
799
+ """(process, source/sink) rows whose max-capacity-coefficient is 0.
800
+
801
+ Default of 1 on missing coefficients means only EXPLICITLY-zero rows
802
+ appear in the output (matches the mod's truthy-check behaviour).
803
+ """
804
+ arcs = _read_csv(arc_csv, ["process", second_col], provider=provider)
805
+ arcs = _drop_blank_rows(arcs, ["process", second_col])
806
+ coef = _read_csv(coef_csv, ["process", second_col, "value"],
807
+ provider=provider)
808
+ zeros = (
809
+ coef.filter(
810
+ (pl.col("process") != "")
811
+ & (pl.col(second_col) != "")
812
+ & (pl.col("value") != "")
813
+ & (pl.col("value").cast(pl.Float64, strict=False) == 0.0)
814
+ )
815
+ .select("process", second_col)
816
+ .unique()
817
+ )
818
+ return (
819
+ arcs.join(zeros, on=["process", second_col], how="semi")
820
+ .unique(maintain_order=True)
821
+ )
822
+
823
+
824
+ def derive_process_source_coeff_zero(input_dir: Path,
825
+ *, provider: "object | None" = None,
826
+ ) -> pl.DataFrame:
827
+ return _derive_coeff_zero(
828
+ input_dir / "process__source.csv",
829
+ input_dir / "p_process_source_capacity_max_coeff.csv",
830
+ "source", provider=provider,
831
+ )
832
+
833
+
834
+ def derive_process_sink_coeff_zero(input_dir: Path,
835
+ *, provider: "object | None" = None,
836
+ ) -> pl.DataFrame:
837
+ return _derive_coeff_zero(
838
+ input_dir / "process__sink.csv",
839
+ input_dir / "p_process_sink_capacity_max_coeff.csv",
840
+ "sink", provider=provider,
841
+ )
842
+
843
+
844
+ def emit_commodity_node_co2(input_dir: Path, solve_data_dir: Path,
845
+ *, provider) -> None:
846
+ """Emit ``commodity_node_co2`` to the Provider."""
847
+ del solve_data_dir
848
+ _emit(provider, "solve_data/commodity_node_co2.csv",
849
+ derive_commodity_node_co2(input_dir, provider=provider))
850
+
851
+
852
+ def emit_process_coeff_zero_sets(input_dir: Path, solve_data_dir: Path,
853
+ *, provider) -> None:
854
+ """Emit ``process_coeff_zero_sets`` to the Provider."""
855
+ del solve_data_dir
856
+ _emit(provider, "solve_data/process_source_coeff_zero.csv",
857
+ derive_process_source_coeff_zero(input_dir, provider=provider))
858
+ _emit(provider, "solve_data/process_sink_coeff_zero.csv",
859
+ derive_process_sink_coeff_zero(input_dir, provider=provider))