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,759 @@
1
+ """pdtProcess / pdtNode / pdtProcess_{source,sink} writers.
2
+
3
+ Four ``write_pdt*`` helpers that consume the ``PdtLookup`` /
4
+ ``PdtLookupPerSide`` class hierarchy.
5
+
6
+ The lookup classes live in :mod:`._pdt_lookup`; this module
7
+ orchestrates the per-row resolution against fixture inputs and emits
8
+ the canonical CSV format.
9
+
10
+ Writers:
11
+
12
+ * ``write_pdtProcess`` — 7-branch.
13
+ * ``write_pdtNode`` — 9-branch (time-first + class-default
14
+ fallback).
15
+ * ``write_pdtProcess_source`` — 6-branch, per-side.
16
+ * ``write_pdtProcess_sink`` — 6-branch, per-side.
17
+
18
+ Phase E-b lift — derive_X / _write split
19
+ ----------------------------------------
20
+
21
+ Each writer is now a thin ``_write(derive_X(...), path)`` wrapper around
22
+ its ``derive_*`` companion that materialises a canonical polars frame.
23
+ The accumulator in :mod:`._flex_data_accumulator` captures these frames
24
+ via its patched ``_write`` helper, so downstream Phase D / E-a
25
+ consumers can read them straight out of memory instead of round-tripping
26
+ through ``solve_data/*.csv`` on disk.
27
+
28
+ These four writers are the high-memory hotspots flagged in
29
+ ``specs/sparse_writer_lessons_for_engine_polars.md`` §1 — a real
30
+ ``write_pdtProcess`` can emit a 441 MB / 280k-row dense CSV. The
31
+ sparse-emit optimisation (``filter(value != 0.0)`` + consumer-side
32
+ ``fill_null(0.0)``) is intentionally NOT applied here: byte-parity with
33
+ the legacy dense emit must hold for ``test_writer_port_phase1.py``.
34
+ The sparse-emit rework is a separate future dispatch that needs
35
+ goldens regenerated and consumer-side overlays.
36
+
37
+ Value-column semantics: the legacy emitters wrote ``f"...,{repr(v)}\\n"``
38
+ which preserves the Python-type-as-emitted distinction
39
+ between ``0`` (int) and ``0.0`` (float). We mirror that by building
40
+ ``value`` as a ``Utf8`` column with ``repr(v)`` applied per-row (the
41
+ same pattern :mod:`._emit_chain_params` uses for its
42
+ ``_ed_value_frame`` helper).
43
+ """
44
+ from __future__ import annotations
45
+
46
+ from pathlib import Path
47
+
48
+ import polars as pl
49
+
50
+ from flextool.engine_polars._emit_provider_io import (
51
+ _emit,
52
+ _provider_key,
53
+ )
54
+ from flextool.engine_polars._pdt_lookup import (
55
+ NODE_PARAM_DEF1,
56
+ PROCESS_PARAM_DEF1,
57
+ PdtLookup,
58
+ PdtLookupPerSide,
59
+ read_class_defaults,
60
+ )
61
+ from flextool.engine_polars._vectorize import (
62
+ build_entity_dt_grid,
63
+ build_fold_frame,
64
+ coalesce_value,
65
+ collect_value_frame,
66
+ lift_dict_to_lookup,
67
+ )
68
+
69
+
70
+ def _cell_str(value: "object | None") -> str:
71
+ """Reproduce a ``csv.reader`` cell string for a native frame value.
72
+
73
+ ``DataFrame.write_csv`` renders ``null`` as the empty string and every
74
+ other scalar as its textual form; ``csv.reader`` then reads those
75
+ strings back. Mirror that so structural string keys stay
76
+ byte-identical to the legacy CSV round-trip.
77
+ """
78
+ return "" if value is None else str(value)
79
+
80
+
81
+ def _read_pairs(path: Path,
82
+ *,
83
+ provider: "object | None" = None) -> list[tuple[str, str]]:
84
+ df = provider.get(_provider_key(path))
85
+ if df is None:
86
+ return []
87
+ out: list[tuple[str, str]] = []
88
+ for row in df.iter_rows():
89
+ if len(row) < 2:
90
+ continue
91
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
92
+ if c0 and c1:
93
+ out.append((c0, c1))
94
+ return out
95
+
96
+
97
+ def _read_triples(path: Path,
98
+ *,
99
+ provider: "object | None" = None) -> list[tuple[str, str, str]]:
100
+ df = provider.get(_provider_key(path))
101
+ if df is None:
102
+ return []
103
+ out: list[tuple[str, str, str]] = []
104
+ for row in df.iter_rows():
105
+ if len(row) < 3:
106
+ continue
107
+ c0, c1, c2 = _cell_str(row[0]), _cell_str(row[1]), _cell_str(row[2])
108
+ if c0 and c1 and c2:
109
+ out.append((c0, c1, c2))
110
+ return out
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Family — pdtProcess (mod L1227).
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def derive_pdtProcess(input_dir: Path, solve_data_dir: Path,
118
+ *,
119
+ provider: "object | None" = None) -> pl.DataFrame:
120
+ """Materialise the ``pdtProcess`` frame.
121
+
122
+ Columns: ``process, param, period, time, value`` — all ``Utf8``
123
+ (value cells are ``repr(v)`` so int/float distinction round-trips
124
+ byte-identically to the legacy ``f",{repr(v)}\\n"`` emit).
125
+
126
+ Domain: ``process_TimeParam_in_use × steps_in_use`` (dense; the
127
+ sparse-emit optimisation from
128
+ ``specs/sparse_writer_lessons_for_engine_polars.md`` §1 is NOT
129
+ applied — preserving byte-parity with the legacy writer is the
130
+ Phase E-b gate).
131
+ """
132
+ lookup = PdtLookup(
133
+ pbt_csv=input_dir / "pbt_process.csv",
134
+ pd_csv=input_dir / "pd_process.csv",
135
+ pt_csv=input_dir / "pt_process.csv",
136
+ p_csv=input_dir / "p_process.csv",
137
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
138
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
139
+ period_branch_csv=solve_data_dir / "period__branch.csv",
140
+ group_entity_csv=input_dir / "group__process.csv",
141
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
142
+ param_def1=PROCESS_PARAM_DEF1,
143
+ provider=provider,
144
+ )
145
+ domain = _read_pairs(
146
+ solve_data_dir / "process_TimeParam_in_use.csv", provider=provider,
147
+ )
148
+ dt = _read_pairs(solve_data_dir / "steps_in_use.csv", provider=provider)
149
+
150
+ n = len(domain) * len(dt)
151
+ processes: list[str] = [""] * n
152
+ params: list[str] = [""] * n
153
+ periods: list[str] = [""] * n
154
+ times: list[str] = [""] * n
155
+ values: list[str] = [""] * n
156
+ i = 0
157
+ for (p, param) in domain:
158
+ for (d, t) in dt:
159
+ v = lookup.get(p, param, d, t)
160
+ processes[i] = p
161
+ params[i] = param
162
+ periods[i] = d
163
+ times[i] = t
164
+ values[i] = repr(v)
165
+ i += 1
166
+ return pl.DataFrame(
167
+ {
168
+ "process": processes,
169
+ "param": params,
170
+ "period": periods,
171
+ "time": times,
172
+ "value": values,
173
+ },
174
+ schema={
175
+ "process": pl.Utf8,
176
+ "param": pl.Utf8,
177
+ "period": pl.Utf8,
178
+ "time": pl.Utf8,
179
+ "value": pl.Utf8,
180
+ },
181
+ )
182
+
183
+
184
+ def derive_pdtProcess_vectorized(input_dir: Path, solve_data_dir: Path,
185
+ *,
186
+ provider: "object | None" = None,
187
+ engine: str = "eager") -> pl.DataFrame:
188
+ """Vectorized ``pdtProcess`` derive — byte-parity with the legacy.
189
+
190
+ Replaces the per-cell ``PdtLookup.get`` loop in
191
+ :func:`derive_pdtProcess` with vectorized polars (left-joins +
192
+ ``coalesce`` in cascade-priority order + the group-by-sum folds),
193
+ still per roll over the roll's own window. Output is byte-identical
194
+ to :func:`derive_pdtProcess`: columns ``process, param, period,
195
+ time, value`` all ``Utf8``, entity-major row order, ``repr(v)``
196
+ value cells.
197
+
198
+ Cascade (``PdtLookup`` 7-branch, period-first):
199
+ fold(branch1 stoch + branch2 parent) → ``pd`` → ``pt`` → ``p`` →
200
+ ``param ∈ PROCESS_PARAM_DEF1`` → ``1.0`` → ``0.0``.
201
+ """
202
+ lookup = PdtLookup(
203
+ pbt_csv=input_dir / "pbt_process.csv",
204
+ pd_csv=input_dir / "pd_process.csv",
205
+ pt_csv=input_dir / "pt_process.csv",
206
+ p_csv=input_dir / "p_process.csv",
207
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
208
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
209
+ period_branch_csv=solve_data_dir / "period__branch.csv",
210
+ group_entity_csv=input_dir / "group__process.csv",
211
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
212
+ param_def1=PROCESS_PARAM_DEF1,
213
+ provider=provider,
214
+ )
215
+ domain = _read_pairs(
216
+ solve_data_dir / "process_TimeParam_in_use.csv", provider=provider,
217
+ )
218
+ dt = _read_pairs(solve_data_dir / "steps_in_use.csv", provider=provider)
219
+
220
+ key_cols = ["process", "param"]
221
+ out_cols = [*key_cols, "period", "time"]
222
+
223
+ grid = build_entity_dt_grid(domain, dt, key_cols=key_cols)
224
+
225
+ pd_df = lift_dict_to_lookup(
226
+ lookup._pd, ["process", "param", "period"], "v_pd")
227
+ pt_df = lift_dict_to_lookup(
228
+ lookup._pt, ["process", "param", "time"], "v_pt")
229
+ p_df = lift_dict_to_lookup(
230
+ lookup._p, ["process", "param"], "v_p")
231
+
232
+ periods = [d for (d, _t) in dt]
233
+ fold = build_fold_frame(
234
+ pbt=lookup._pbt,
235
+ pbt_key_cols=key_cols,
236
+ out_key_cols=out_cols,
237
+ ts_for_d=lookup._ts_for_d,
238
+ tb_for_d=lookup._tb_for_d,
239
+ pe_for_d=lookup._pe_for_d,
240
+ stoch_entities=lookup._stoch_entity,
241
+ stoch_filter_cols=["process"],
242
+ periods=periods,
243
+ )
244
+
245
+ out = (
246
+ grid
247
+ .join(pd_df, on=["process", "param", "period"], how="left")
248
+ .join(pt_df, on=["process", "param", "time"], how="left")
249
+ .join(p_df, on=["process", "param"], how="left")
250
+ )
251
+ if fold is not None:
252
+ out = out.join(fold, on=out_cols, how="left")
253
+ else:
254
+ out = out.with_columns(
255
+ pl.lit(None, dtype=pl.Float64).alias("v_fold"))
256
+
257
+ def1 = list(PROCESS_PARAM_DEF1)
258
+ out = out.with_columns(
259
+ coalesce_value([
260
+ pl.col("v_fold"), # branches 1-2 (fold)
261
+ pl.col("v_pd"), # branch 3 (period-first)
262
+ pl.col("v_pt"), # branch 4
263
+ pl.col("v_p"), # branch 5
264
+ pl.when(pl.col("param").is_in(def1))
265
+ .then(pl.lit(1.0)).otherwise(pl.lit(0.0)), # branch 6 / 8
266
+ ])
267
+ )
268
+ return collect_value_frame(
269
+ out, key_cols=out_cols, engine=engine)
270
+
271
+
272
+ def emit_pdtProcess(input_dir: Path, solve_data_dir: Path,
273
+ *, provider) -> None:
274
+ """Emit ``pdtProcess`` to the Provider."""
275
+ _emit(provider, "solve_data/pdtProcess.csv",
276
+ derive_pdtProcess_vectorized(
277
+ input_dir, solve_data_dir, provider=provider))
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Family — pdtNode (mod L1176; time-first + class default).
282
+ # ---------------------------------------------------------------------------
283
+
284
+ def derive_pdtNode(input_dir: Path, solve_data_dir: Path,
285
+ *,
286
+ provider: "object | None" = None) -> pl.DataFrame:
287
+ """Materialise the ``pdtNode`` frame.
288
+
289
+ Columns: ``node, param, period, time, value`` — all ``Utf8``.
290
+ Domain: ``node__TimeParam_in_use × steps_in_use``.
291
+ """
292
+ lookup = PdtLookup(
293
+ pbt_csv=input_dir / "pbt_node.csv",
294
+ pd_csv=input_dir / "pd_node.csv",
295
+ pt_csv=input_dir / "pt_node.csv",
296
+ p_csv=input_dir / "p_node.csv",
297
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
298
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
299
+ period_branch_csv=solve_data_dir / "period__branch.csv",
300
+ group_entity_csv=input_dir / "group__node.csv",
301
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
302
+ param_def1=NODE_PARAM_DEF1,
303
+ time_first_priority=True,
304
+ class_default_values=read_class_defaults(
305
+ input_dir / "default_values.csv", "node", provider=provider,
306
+ ),
307
+ provider=provider,
308
+ )
309
+ domain = _read_pairs(
310
+ solve_data_dir / "node__TimeParam_in_use.csv", provider=provider,
311
+ )
312
+ dt = _read_pairs(solve_data_dir / "steps_in_use.csv", provider=provider)
313
+
314
+ n = len(domain) * len(dt)
315
+ nodes: list[str] = [""] * n
316
+ params: list[str] = [""] * n
317
+ periods: list[str] = [""] * n
318
+ times: list[str] = [""] * n
319
+ values: list[str] = [""] * n
320
+ i = 0
321
+ for (nd, param) in domain:
322
+ for (d, t) in dt:
323
+ v = lookup.get(nd, param, d, t)
324
+ nodes[i] = nd
325
+ params[i] = param
326
+ periods[i] = d
327
+ times[i] = t
328
+ values[i] = repr(v)
329
+ i += 1
330
+ return pl.DataFrame(
331
+ {
332
+ "node": nodes,
333
+ "param": params,
334
+ "period": periods,
335
+ "time": times,
336
+ "value": values,
337
+ },
338
+ schema={
339
+ "node": pl.Utf8,
340
+ "param": pl.Utf8,
341
+ "period": pl.Utf8,
342
+ "time": pl.Utf8,
343
+ "value": pl.Utf8,
344
+ },
345
+ )
346
+
347
+
348
+ def derive_pdtNode_vectorized(input_dir: Path, solve_data_dir: Path,
349
+ *,
350
+ provider: "object | None" = None,
351
+ engine: str = "eager") -> pl.DataFrame:
352
+ """Vectorized ``pdtNode`` derive — byte-parity with the legacy.
353
+
354
+ Replaces the per-cell ``PdtLookup.get`` loop in
355
+ :func:`derive_pdtNode` with vectorized polars (left-joins +
356
+ ``coalesce`` in cascade-priority order + the group-by-sum folds),
357
+ still per roll over the roll's own window. Output is byte-identical
358
+ to :func:`derive_pdtNode`: columns ``node, param, period, time,
359
+ value`` all ``Utf8``, entity-major row order, ``repr(v)`` value
360
+ cells.
361
+
362
+ Cascade (``PdtLookup`` 9-branch, **time-first** —
363
+ ``time_first_priority=True`` ⇒ ``pt`` BEFORE ``pd``):
364
+ fold(branch1 stoch + branch2 parent) → ``pt`` → ``pd`` → ``p`` →
365
+ ``param ∈ NODE_PARAM_DEF1 (= {"availability"})`` → ``1.0`` →
366
+ **class-default** (``read_class_defaults(default_values.csv,
367
+ "node")`` keyed on ``param``) → ``0.0``.
368
+
369
+ The class-default term is a ``param → value`` dict lifted to a
370
+ ``[param, value]`` frame and left-joined on ``param`` alone (it is
371
+ node-independent); it is placed AFTER the def1 term and BEFORE the
372
+ literal ``0.0`` to mirror branches 6 → 7 → 8 of the scalar cascade.
373
+ """
374
+ class_defaults = read_class_defaults(
375
+ input_dir / "default_values.csv", "node", provider=provider,
376
+ )
377
+ lookup = PdtLookup(
378
+ pbt_csv=input_dir / "pbt_node.csv",
379
+ pd_csv=input_dir / "pd_node.csv",
380
+ pt_csv=input_dir / "pt_node.csv",
381
+ p_csv=input_dir / "p_node.csv",
382
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
383
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
384
+ period_branch_csv=solve_data_dir / "period__branch.csv",
385
+ group_entity_csv=input_dir / "group__node.csv",
386
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
387
+ param_def1=NODE_PARAM_DEF1,
388
+ time_first_priority=True,
389
+ class_default_values=class_defaults,
390
+ provider=provider,
391
+ )
392
+ domain = _read_pairs(
393
+ solve_data_dir / "node__TimeParam_in_use.csv", provider=provider,
394
+ )
395
+ dt = _read_pairs(solve_data_dir / "steps_in_use.csv", provider=provider)
396
+
397
+ key_cols = ["node", "param"]
398
+ out_cols = [*key_cols, "period", "time"]
399
+
400
+ grid = build_entity_dt_grid(domain, dt, key_cols=key_cols)
401
+
402
+ pd_df = lift_dict_to_lookup(
403
+ lookup._pd, ["node", "param", "period"], "v_pd")
404
+ pt_df = lift_dict_to_lookup(
405
+ lookup._pt, ["node", "param", "time"], "v_pt")
406
+ p_df = lift_dict_to_lookup(
407
+ lookup._p, ["node", "param"], "v_p")
408
+
409
+ # Class-default: a node-independent ``param -> value`` dict lifted to
410
+ # a ``[param, value]`` frame, left-joined on ``param`` alone.
411
+ if class_defaults:
412
+ cd_df = pl.DataFrame(
413
+ {
414
+ "param": list(class_defaults.keys()),
415
+ "v_cd": list(class_defaults.values()),
416
+ },
417
+ schema={"param": pl.Utf8, "v_cd": pl.Float64},
418
+ )
419
+ else:
420
+ cd_df = pl.DataFrame(
421
+ {"param": [], "v_cd": []},
422
+ schema={"param": pl.Utf8, "v_cd": pl.Float64},
423
+ )
424
+
425
+ periods = [d for (d, _t) in dt]
426
+ fold = build_fold_frame(
427
+ pbt=lookup._pbt,
428
+ pbt_key_cols=key_cols,
429
+ out_key_cols=out_cols,
430
+ ts_for_d=lookup._ts_for_d,
431
+ tb_for_d=lookup._tb_for_d,
432
+ pe_for_d=lookup._pe_for_d,
433
+ stoch_entities=lookup._stoch_entity,
434
+ stoch_filter_cols=["node"],
435
+ periods=periods,
436
+ )
437
+
438
+ out = (
439
+ grid
440
+ .join(pt_df, on=["node", "param", "time"], how="left")
441
+ .join(pd_df, on=["node", "param", "period"], how="left")
442
+ .join(p_df, on=["node", "param"], how="left")
443
+ .join(cd_df, on="param", how="left")
444
+ )
445
+ if fold is not None:
446
+ out = out.join(fold, on=out_cols, how="left")
447
+ else:
448
+ out = out.with_columns(
449
+ pl.lit(None, dtype=pl.Float64).alias("v_fold"))
450
+
451
+ def1 = list(NODE_PARAM_DEF1)
452
+ out = out.with_columns(
453
+ coalesce_value([
454
+ pl.col("v_fold"), # branches 1-2 (fold)
455
+ pl.col("v_pt"), # branch 3 (time-first ⇒ pt before pd)
456
+ pl.col("v_pd"), # branch 4
457
+ pl.col("v_p"), # branch 5
458
+ # branch 6: def1 → 1.0 (only when param ∈ NODE_PARAM_DEF1).
459
+ pl.when(pl.col("param").is_in(def1))
460
+ .then(pl.lit(1.0)).otherwise(pl.lit(None, dtype=pl.Float64)),
461
+ pl.col("v_cd"), # branch 7: class-default (join on param)
462
+ pl.lit(0.0), # branch 8: literal 0.0
463
+ ])
464
+ )
465
+ return collect_value_frame(
466
+ out, key_cols=out_cols, engine=engine)
467
+
468
+
469
+ def emit_pdtNode(input_dir: Path, solve_data_dir: Path,
470
+ *, provider) -> None:
471
+ """Emit ``pdtNode`` to the Provider."""
472
+ _emit(provider, "solve_data/pdtNode.csv",
473
+ derive_pdtNode_vectorized(
474
+ input_dir, solve_data_dir, provider=provider))
475
+
476
+
477
+ # ---------------------------------------------------------------------------
478
+ # Family — pdtProcess_source / pdtProcess_sink (mod L1265 / L1279).
479
+ # ---------------------------------------------------------------------------
480
+
481
+ def _derive_pdtProcess_side(
482
+ *,
483
+ pbt_csv: Path,
484
+ pd_csv: Path,
485
+ pt_csv: Path,
486
+ p_csv: Path,
487
+ period_time_first_csv: Path,
488
+ solve_branch_csv: Path,
489
+ period_branch_csv: Path,
490
+ group_process_csv: Path,
491
+ group_stochastic_csv: Path,
492
+ domain_csv: Path,
493
+ dt_csv: Path,
494
+ side_col: str,
495
+ provider: "object | None" = None,
496
+ ) -> pl.DataFrame:
497
+ lookup = PdtLookupPerSide(
498
+ pbt_csv=pbt_csv,
499
+ pd_csv=pd_csv,
500
+ pt_csv=pt_csv,
501
+ p_csv=p_csv,
502
+ period_time_first_csv=period_time_first_csv,
503
+ solve_branch_csv=solve_branch_csv,
504
+ period_branch_csv=period_branch_csv,
505
+ group_process_csv=group_process_csv,
506
+ group_stochastic_csv=group_stochastic_csv,
507
+ provider=provider,
508
+ )
509
+ domain = _read_triples(domain_csv, provider=provider)
510
+ dt = _read_pairs(dt_csv, provider=provider)
511
+
512
+ n = len(domain) * len(dt)
513
+ processes: list[str] = [""] * n
514
+ sides: list[str] = [""] * n
515
+ params: list[str] = [""] * n
516
+ periods: list[str] = [""] * n
517
+ times: list[str] = [""] * n
518
+ values: list[str] = [""] * n
519
+ i = 0
520
+ for (p, side, param) in domain:
521
+ for (d, t) in dt:
522
+ v = lookup.get(p, side, param, d, t)
523
+ processes[i] = p
524
+ sides[i] = side
525
+ params[i] = param
526
+ periods[i] = d
527
+ times[i] = t
528
+ values[i] = repr(v)
529
+ i += 1
530
+ return pl.DataFrame(
531
+ {
532
+ "process": processes,
533
+ side_col: sides,
534
+ "param": params,
535
+ "period": periods,
536
+ "time": times,
537
+ "value": values,
538
+ },
539
+ schema={
540
+ "process": pl.Utf8,
541
+ side_col: pl.Utf8,
542
+ "param": pl.Utf8,
543
+ "period": pl.Utf8,
544
+ "time": pl.Utf8,
545
+ "value": pl.Utf8,
546
+ },
547
+ )
548
+
549
+
550
+ def _derive_pdtProcess_side_vectorized(
551
+ *,
552
+ pbt_csv: Path,
553
+ pd_csv: Path,
554
+ pt_csv: Path,
555
+ p_csv: Path,
556
+ period_time_first_csv: Path,
557
+ solve_branch_csv: Path,
558
+ period_branch_csv: Path,
559
+ group_process_csv: Path,
560
+ group_stochastic_csv: Path,
561
+ domain_csv: Path,
562
+ dt_csv: Path,
563
+ side_col: str,
564
+ provider: "object | None" = None,
565
+ engine: str = "eager",
566
+ ) -> pl.DataFrame:
567
+ """Vectorized per-side derive — byte-parity with ``_derive_pdtProcess_side``.
568
+
569
+ Replaces the per-cell ``PdtLookupPerSide.get`` loop with vectorized
570
+ polars (left-joins + ``coalesce`` in cascade-priority order + the
571
+ group-by-sum folds), still per roll over the roll's own window.
572
+ Output is byte-identical to :func:`_derive_pdtProcess_side`: columns
573
+ ``process, <side_col>, param, period, time, value`` all ``Utf8``,
574
+ entity-major row order, ``repr(v)`` value cells.
575
+
576
+ Cascade (``PdtLookupPerSide`` 6-branch, period-first; NO def1 / NO
577
+ class-default):
578
+ fold(branch1 stoch + branch2 parent) → ``pd`` → ``pt`` → ``p`` → ``0.0``.
579
+
580
+ The 3-col entity key is ``(process, <side_col>, param)``. Per the
581
+ M1 quirk (design §4) the stochastic-fold membership test keys on the
582
+ ``process`` column ALONE (``p ∈ _stoch_process``), even though the
583
+ pbt join key is the full ``(process, side, param, tb, ts, t)``. The
584
+ physical ``side`` column carries the lookup-internal name ``"side"``
585
+ throughout the join graph and is renamed to *side_col* only at the
586
+ very end, so the join keys stay aligned regardless of the public
587
+ column name.
588
+ """
589
+ lookup = PdtLookupPerSide(
590
+ pbt_csv=pbt_csv,
591
+ pd_csv=pd_csv,
592
+ pt_csv=pt_csv,
593
+ p_csv=p_csv,
594
+ period_time_first_csv=period_time_first_csv,
595
+ solve_branch_csv=solve_branch_csv,
596
+ period_branch_csv=period_branch_csv,
597
+ group_process_csv=group_process_csv,
598
+ group_stochastic_csv=group_stochastic_csv,
599
+ provider=provider,
600
+ )
601
+ domain = _read_triples(domain_csv, provider=provider)
602
+ dt = _read_pairs(dt_csv, provider=provider)
603
+
604
+ # Internal key uses "side" (not side_col) so the join keys are stable;
605
+ # the public side_col name is applied only on the final select.
606
+ key_cols = ["process", "side", "param"]
607
+ out_cols = [*key_cols, "period", "time"]
608
+
609
+ grid = build_entity_dt_grid(domain, dt, key_cols=key_cols)
610
+
611
+ pd_df = lift_dict_to_lookup(
612
+ lookup._pd, ["process", "side", "param", "period"], "v_pd")
613
+ pt_df = lift_dict_to_lookup(
614
+ lookup._pt, ["process", "side", "param", "time"], "v_pt")
615
+ p_df = lift_dict_to_lookup(
616
+ lookup._p, ["process", "side", "param"], "v_p")
617
+
618
+ periods = [d for (d, _t) in dt]
619
+ fold = build_fold_frame(
620
+ pbt=lookup._pbt,
621
+ pbt_key_cols=key_cols,
622
+ out_key_cols=out_cols,
623
+ ts_for_d=lookup._ts_for_d,
624
+ tb_for_d=lookup._tb_for_d,
625
+ pe_for_d=lookup._pe_for_d,
626
+ stoch_entities=lookup._stoch_process,
627
+ stoch_filter_cols=["process"], # M1: process-only stoch filter.
628
+ periods=periods,
629
+ )
630
+
631
+ out = (
632
+ grid
633
+ .join(pd_df, on=["process", "side", "param", "period"], how="left")
634
+ .join(pt_df, on=["process", "side", "param", "time"], how="left")
635
+ .join(p_df, on=["process", "side", "param"], how="left")
636
+ )
637
+ if fold is not None:
638
+ out = out.join(fold, on=out_cols, how="left")
639
+ else:
640
+ out = out.with_columns(
641
+ pl.lit(None, dtype=pl.Float64).alias("v_fold"))
642
+
643
+ out = out.with_columns(
644
+ coalesce_value([
645
+ pl.col("v_fold"), # branches 1-2 (fold)
646
+ pl.col("v_pd"), # branch 3 (period-first)
647
+ pl.col("v_pt"), # branch 4
648
+ pl.col("v_p"), # branch 5
649
+ pl.lit(0.0), # branch 6 (no def1 / no class-default)
650
+ ])
651
+ )
652
+ rendered = collect_value_frame(out, key_cols=out_cols, engine=engine)
653
+ # Apply the public side column name only at the end.
654
+ return rendered.rename({"side": side_col}).select(
655
+ ["process", side_col, "param", "period", "time", "value"])
656
+
657
+
658
+ def derive_pdtProcess_source(
659
+ input_dir: Path, solve_data_dir: Path,
660
+ *,
661
+ provider: "object | None" = None,
662
+ ) -> pl.DataFrame:
663
+ """Materialise ``pdtProcess_source`` (mod L1265, 6-branch per-side).
664
+
665
+ Columns: ``process, source, param, period, time, value`` (all Utf8).
666
+ Domain: ``process_source_sourceSinkTimeParam_in_use × steps_in_use``.
667
+ """
668
+ return _derive_pdtProcess_side(
669
+ pbt_csv=input_dir / "pbt_process_source.csv",
670
+ pd_csv=input_dir / "pd_process_source.csv",
671
+ pt_csv=input_dir / "pt_process_source.csv",
672
+ p_csv=input_dir / "p_process_source.csv",
673
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
674
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
675
+ period_branch_csv=solve_data_dir / "period__branch.csv",
676
+ group_process_csv=input_dir / "group__process.csv",
677
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
678
+ domain_csv=solve_data_dir / "process_source_sourceSinkTimeParam_in_use.csv",
679
+ dt_csv=solve_data_dir / "steps_in_use.csv",
680
+ side_col="source",
681
+ provider=provider,
682
+ )
683
+
684
+
685
+ def emit_pdtProcess_source(input_dir: Path, solve_data_dir: Path,
686
+ *, provider) -> None:
687
+ """Emit ``pdtProcess_source`` to the Provider."""
688
+ _emit(
689
+ provider, "solve_data/pdtProcess_source.csv",
690
+ _derive_pdtProcess_side_vectorized(
691
+ pbt_csv=input_dir / "pbt_process_source.csv",
692
+ pd_csv=input_dir / "pd_process_source.csv",
693
+ pt_csv=input_dir / "pt_process_source.csv",
694
+ p_csv=input_dir / "p_process_source.csv",
695
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
696
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
697
+ period_branch_csv=solve_data_dir / "period__branch.csv",
698
+ group_process_csv=input_dir / "group__process.csv",
699
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
700
+ domain_csv=(
701
+ solve_data_dir
702
+ / "process_source_sourceSinkTimeParam_in_use.csv"),
703
+ dt_csv=solve_data_dir / "steps_in_use.csv",
704
+ side_col="source",
705
+ provider=provider,
706
+ ),
707
+ )
708
+
709
+
710
+ def derive_pdtProcess_sink(
711
+ input_dir: Path, solve_data_dir: Path,
712
+ *,
713
+ provider: "object | None" = None,
714
+ ) -> pl.DataFrame:
715
+ """Materialise ``pdtProcess_sink`` (mod L1279, 6-branch per-side).
716
+
717
+ Columns: ``process, sink, param, period, time, value`` (all Utf8).
718
+ Domain: ``process_sink_sourceSinkTimeParam_in_use × steps_in_use``.
719
+ """
720
+ return _derive_pdtProcess_side(
721
+ pbt_csv=input_dir / "pbt_process_sink.csv",
722
+ pd_csv=input_dir / "pd_process_sink.csv",
723
+ pt_csv=input_dir / "pt_process_sink.csv",
724
+ p_csv=input_dir / "p_process_sink.csv",
725
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
726
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
727
+ period_branch_csv=solve_data_dir / "period__branch.csv",
728
+ group_process_csv=input_dir / "group__process.csv",
729
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
730
+ domain_csv=solve_data_dir / "process_sink_sourceSinkTimeParam_in_use.csv",
731
+ dt_csv=solve_data_dir / "steps_in_use.csv",
732
+ side_col="sink",
733
+ provider=provider,
734
+ )
735
+
736
+
737
+ def emit_pdtProcess_sink(input_dir: Path, solve_data_dir: Path,
738
+ *, provider) -> None:
739
+ """Emit ``pdtProcess_sink`` to the Provider."""
740
+ _emit(
741
+ provider, "solve_data/pdtProcess_sink.csv",
742
+ _derive_pdtProcess_side_vectorized(
743
+ pbt_csv=input_dir / "pbt_process_sink.csv",
744
+ pd_csv=input_dir / "pd_process_sink.csv",
745
+ pt_csv=input_dir / "pt_process_sink.csv",
746
+ p_csv=input_dir / "p_process_sink.csv",
747
+ period_time_first_csv=solve_data_dir / "first_timesteps.csv",
748
+ solve_branch_csv=solve_data_dir / "solve_branch__time_branch.csv",
749
+ period_branch_csv=solve_data_dir / "period__branch.csv",
750
+ group_process_csv=input_dir / "group__process.csv",
751
+ group_stochastic_csv=input_dir / "groupIncludeStochastics.csv",
752
+ domain_csv=(
753
+ solve_data_dir
754
+ / "process_sink_sourceSinkTimeParam_in_use.csv"),
755
+ dt_csv=solve_data_dir / "steps_in_use.csv",
756
+ side_col="sink",
757
+ provider=provider,
758
+ ),
759
+ )