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,690 @@
1
+ """Top-level dispatcher own-compute emitters (arc-unions + entity_period).
2
+
3
+ This module owns the two top-level "dispatcher" preprocessing functions
4
+ that contain inline derivations (loops, projections, unions, gated
5
+ constructions) rather than delegating to sibling emitters.
6
+
7
+ * :func:`emit_process_arc_unions` — emits the 14-set L1 arc-union
8
+ batch in dependency order: ``process__profileProcess__toSink``,
9
+ ``process__source__toProfileProcess``, ``process_profile``,
10
+ ``process_source_toProcess``, ``process_process_toSink``,
11
+ ``process_source_sink_eff``, ``process_source_sink_noEff``,
12
+ ``process_online``, ``process_minload``,
13
+ ``process__commodity__node_co2``, ``process_co2``,
14
+ ``process_source_sink``, ``process_source_sink_alwaysProcess``,
15
+ ``process__source__sink__profile__profile_method_direct``.
16
+
17
+ * :func:`emit_entity_period_calc_params` — emits ``pdProcess``,
18
+ ``pdNode``, ``edEntity_lifetime``, ``ed_fixed_cost``,
19
+ ``p_entity_unitsize`` via the ``PdLookup`` machinery (native shim in
20
+ :mod:`._pdt_lookup`).
21
+
22
+ Parity contract: same CSV header, same per-row formatting, same
23
+ iteration order as the historical preprocessing chain — verified at
24
+ the file level via ``filecmp.cmp(shallow=False)`` in
25
+ ``tests/engine_polars/test_writer_port_phase1.py``.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from pathlib import Path
30
+
31
+ import polars as pl
32
+
33
+ from flextool.engine_polars._emit_provider_io import (
34
+ _emit,
35
+ _provider_key,
36
+ )
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Native-frame row I/O — reads the in-memory polars frame directly from the
41
+ # Provider via ``provider.get(_provider_key(path))`` instead of round-
42
+ # tripping through CSV text. ``_cell_str`` reproduces the ``csv.reader``
43
+ # cell string (``null`` → ``""``, else ``str``) so structural string keys
44
+ # stay byte-identical to the legacy round-trip; ``provider.get`` returns
45
+ # data rows only, so there is no header row to skip.
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ def _cell_str(value: "object | None") -> str:
50
+ """Reproduce a ``csv.reader`` cell string for a native frame value."""
51
+ return "" if value is None else str(value)
52
+
53
+
54
+ def _read_pairs(path: Path,
55
+ *, provider: "object | None" = None) -> list[tuple[str, str]]:
56
+ df = provider.get(_provider_key(path))
57
+ if df is None:
58
+ return []
59
+ out: list[tuple[str, str]] = []
60
+ for row in df.iter_rows():
61
+ if len(row) < 2:
62
+ continue
63
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
64
+ if c0 and c1:
65
+ out.append((c0, c1))
66
+ return out
67
+
68
+
69
+ def _read_n_col(path: Path, n: int,
70
+ *, provider: "object | None" = None) -> list[tuple[str, ...]]:
71
+ df = provider.get(_provider_key(path))
72
+ if df is None:
73
+ return []
74
+ out: list[tuple[str, ...]] = []
75
+ for row in df.iter_rows():
76
+ if len(row) < n:
77
+ continue
78
+ cells = tuple(_cell_str(row[i]) for i in range(n))
79
+ if all(cells):
80
+ out.append(cells)
81
+ return out
82
+
83
+
84
+ def _read_singles(path: Path,
85
+ *, provider: "object | None" = None) -> list[str]:
86
+ df = provider.get(_provider_key(path))
87
+ if df is None:
88
+ return []
89
+ out: list[str] = []
90
+ for row in df.iter_rows():
91
+ if not row:
92
+ continue
93
+ c0 = _cell_str(row[0])
94
+ if c0:
95
+ out.append(c0)
96
+ return out
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Method-constant frozensets — re-exported from
101
+ # :mod:`flextool.input_derivation._method_constants`. These are model
102
+ # invariants from flextool_base.dat:60-95 and are not user-editable.
103
+ # ---------------------------------------------------------------------------
104
+
105
+ from flextool.input_derivation._method_constants import ( # noqa: E402
106
+ METHOD_INDIRECT as _METHOD_INDIRECT,
107
+ METHOD_DIRECT as _METHOD_DIRECT,
108
+ )
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # emit_process_arc_unions — top-level dispatcher own-compute.
113
+ #
114
+ # Each emitted CSV flows through ``_emit`` (via a private ``_compute_*``
115
+ # helper). The wrapper builds the shared input bundle once and threads
116
+ # intermediate frames so dependent CSVs don't re-scan.
117
+ # ---------------------------------------------------------------------------
118
+
119
+
120
+ def _to_frame(rows, header: tuple[str, ...]) -> pl.DataFrame:
121
+ """Dedup rows + materialise as an all-Utf8 polars frame.
122
+
123
+ ``polars.write_csv`` on an all-Utf8 frame produces byte-identical
124
+ output to the legacy ``_write_csv`` helper for plain ASCII data
125
+ (the same shape the dispatcher operates on).
126
+ """
127
+ deduped = list(dict.fromkeys(tuple(r) for r in rows))
128
+ cols = {h: [r[i] for r in deduped] for i, h in enumerate(header)}
129
+ return pl.DataFrame(cols, schema={h: pl.Utf8 for h in header})
130
+
131
+
132
+ def _arc_unions_inputs(input_dir: Path, solve_data_dir: Path,
133
+ *, provider: "object | None" = None) -> dict:
134
+ """Shared input bundle for the arc-union computes in this monolith."""
135
+ METHOD_INDIRECT = _METHOD_INDIRECT
136
+ METHOD_DIRECT = _METHOD_DIRECT
137
+ process_method = _read_pairs(input_dir / "process_method.csv",
138
+ provider=provider)
139
+ sources = _read_pairs(input_dir / "process__source.csv",
140
+ provider=provider)
141
+ sinks = _read_pairs(input_dir / "process__sink.csv",
142
+ provider=provider)
143
+ return {
144
+ "process_method": process_method,
145
+ "sources": sources,
146
+ "sinks": sinks,
147
+ "p_with_indirect": frozenset(
148
+ p for p, m in process_method if m in METHOD_INDIRECT),
149
+ "p_with_direct": frozenset(
150
+ p for p, m in process_method if m in METHOD_DIRECT),
151
+ "has_sink": frozenset(p for p, _ in sinks),
152
+ "has_source": frozenset(p for p, _ in sources),
153
+ "processes": _read_singles(input_dir / "process.csv",
154
+ provider=provider),
155
+ "five_tuple_to_sink": _read_n_col(
156
+ solve_data_dir
157
+ / "process__profileProcess__toSink__profile__profile_method.csv",
158
+ 5,
159
+ provider=provider,
160
+ ),
161
+ "five_tuple_to_source": _read_n_col(
162
+ solve_data_dir
163
+ / "process__source__toProfileProcess__profile__profile_method.csv",
164
+ 5,
165
+ provider=provider,
166
+ ),
167
+ "input_dir": input_dir,
168
+ "solve_data_dir": solve_data_dir,
169
+ }
170
+
171
+
172
+ # ---- (1) process__profileProcess__toSink ------------------------------------
173
+
174
+ def _profile_to_sink_3(inp: dict) -> list[tuple[str, str, str]]:
175
+ return list(dict.fromkeys(
176
+ (p_outer, p, sink)
177
+ for p_outer, p, sink, _f, _fm in inp["five_tuple_to_sink"]
178
+ ))
179
+
180
+
181
+ def _compute_process__profileProcess__toSink(inp: dict) -> pl.DataFrame:
182
+ return _to_frame(
183
+ _profile_to_sink_3(inp), ("process_outer", "process", "sink"),
184
+ )
185
+
186
+
187
+ # ---- (2) process__source__toProfileProcess ----------------------------------
188
+
189
+ def _source_to_profile_3(inp: dict) -> list[tuple[str, str, str]]:
190
+ return list(dict.fromkeys(
191
+ (p, source, p_aux)
192
+ for p, source, p_aux, _f, _fm in inp["five_tuple_to_source"]
193
+ ))
194
+
195
+
196
+ def _compute_process__source__toProfileProcess(inp: dict) -> pl.DataFrame:
197
+ return _to_frame(
198
+ _source_to_profile_3(inp), ("process", "source", "process_aux"),
199
+ )
200
+
201
+
202
+ # ---- (3) process_profile ----------------------------------------------------
203
+
204
+ def _compute_process_profile(inp: dict) -> pl.DataFrame:
205
+ seen: dict[str, None] = {}
206
+ for p, _, _ in _source_to_profile_3(inp):
207
+ seen.setdefault(p, None)
208
+ for p, _, _ in _profile_to_sink_3(inp):
209
+ seen.setdefault(p, None)
210
+ return _to_frame([(p,) for p in seen.keys()], ("process",))
211
+
212
+
213
+ # ---- (4) process_source_toProcess + (5) process_process_toSink --------------
214
+
215
+ def _rows_source_toProcess(inp: dict) -> list[tuple[str, str, str]]:
216
+ excluded_to_profile = frozenset(_source_to_profile_3(inp))
217
+ rows: list[tuple[str, str, str]] = []
218
+ for p, source in inp["sources"]:
219
+ if p in inp["p_with_indirect"]:
220
+ rows.append((p, source, p))
221
+ elif (p in inp["p_with_direct"]
222
+ and p not in inp["has_sink"]
223
+ and (p, source, p) not in excluded_to_profile):
224
+ rows.append((p, source, p))
225
+ return list(dict.fromkeys(rows))
226
+
227
+
228
+ def _rows_process_toSink(inp: dict) -> list[tuple[str, str, str]]:
229
+ excluded_profile_to_sink = frozenset(_profile_to_sink_3(inp))
230
+ rows: list[tuple[str, str, str]] = []
231
+ for p, sink in inp["sinks"]:
232
+ if p in inp["p_with_indirect"]:
233
+ rows.append((p, p, sink))
234
+ elif (p in inp["p_with_direct"]
235
+ and p not in inp["has_source"]
236
+ and (p, p, sink) not in excluded_profile_to_sink):
237
+ rows.append((p, p, sink))
238
+ return list(dict.fromkeys(rows))
239
+
240
+
241
+ def _compute_process_source_toProcess(inp: dict) -> pl.DataFrame:
242
+ return _to_frame(
243
+ _rows_source_toProcess(inp), ("process", "source", "process_aux"),
244
+ )
245
+
246
+
247
+ def _compute_process_process_toSink(inp: dict) -> pl.DataFrame:
248
+ return _to_frame(
249
+ _rows_process_toSink(inp), ("process_outer", "process", "sink"),
250
+ )
251
+
252
+
253
+ # ---- (6) process_source_sink_eff -------------------------------------------
254
+
255
+ def _compute_process_source_sink_eff(
256
+ solve_data_dir: Path,
257
+ *, provider: "object | None" = None,
258
+ ) -> pl.DataFrame:
259
+ sst = _read_n_col(solve_data_dir / "process_source_toSink.csv", 3,
260
+ provider=provider)
261
+ sts = _read_n_col(solve_data_dir / "process_sink_toSource.csv", 3,
262
+ provider=provider)
263
+ union: dict[tuple[str, ...], None] = {}
264
+ for r in sst:
265
+ union.setdefault(r, None)
266
+ for r in sts:
267
+ union.setdefault(r, None)
268
+ return _to_frame(list(union.keys()), ("process", "source", "sink"))
269
+
270
+
271
+ # ---- Helper: shared 6-list bundle for 7/12/13 -------------------------------
272
+
273
+ def _disk_arc_lists(solve_data_dir: Path,
274
+ *, provider: "object | None" = None) -> dict:
275
+ return {
276
+ "sst": _read_n_col(solve_data_dir / "process_source_toSink.csv", 3,
277
+ provider=provider),
278
+ "sts": _read_n_col(solve_data_dir / "process_sink_toSource.csv", 3,
279
+ provider=provider),
280
+ "snk_to_proc": _read_n_col(
281
+ solve_data_dir / "process_sink_toProcess.csv", 3,
282
+ provider=provider),
283
+ "proc_to_src": _read_n_col(
284
+ solve_data_dir / "process_process_toSource.csv", 3,
285
+ provider=provider),
286
+ "proc_to_snk_noConv": _read_n_col(
287
+ solve_data_dir / "process_process_toSink_noConversion.csv", 3,
288
+ provider=provider),
289
+ "src_to_proc_noConv": _read_n_col(
290
+ solve_data_dir / "process_source_toProcess_noConversion.csv", 3,
291
+ provider=provider),
292
+ }
293
+
294
+
295
+ # ---- (7) process_source_sink_noEff -----------------------------------------
296
+
297
+ def _compute_process_source_sink_noEff(
298
+ inp: dict, disk: dict,
299
+ ) -> pl.DataFrame:
300
+ src_to_proc = _rows_source_toProcess(inp)
301
+ proc_to_snk = _rows_process_toSink(inp)
302
+ profile_to_sink_3 = _profile_to_sink_3(inp)
303
+ source_to_profile_3 = _source_to_profile_3(inp)
304
+ union2: dict[tuple[str, ...], None] = {}
305
+ for src in (src_to_proc, proc_to_snk, disk["snk_to_proc"],
306
+ disk["proc_to_src"], profile_to_sink_3, source_to_profile_3,
307
+ disk["proc_to_snk_noConv"], disk["src_to_proc_noConv"]):
308
+ for r in src:
309
+ union2.setdefault(tuple(r), None)
310
+ return _to_frame(list(union2.keys()), ("process", "source", "sink"))
311
+
312
+
313
+ # ---- (8) process_online ----------------------------------------------------
314
+
315
+ def _compute_process_online(solve_data_dir: Path,
316
+ *, provider: "object | None" = None) -> pl.DataFrame:
317
+ a = _read_singles(solve_data_dir / "process_online_linear.csv",
318
+ provider=provider)
319
+ b = _read_singles(solve_data_dir / "process_online_integer.csv",
320
+ provider=provider)
321
+ seen: dict[str, None] = {}
322
+ for p in a + b:
323
+ seen.setdefault(p, None)
324
+ return _to_frame([(p,) for p in seen.keys()], ("process",))
325
+
326
+
327
+ # ---- (9) process_minload ---------------------------------------------------
328
+
329
+ def _compute_process_minload(inp: dict, solve_data_dir: Path,
330
+ *, provider: "object | None" = None) -> pl.DataFrame:
331
+ ctm = _read_pairs(solve_data_dir / "process__ct_method.csv",
332
+ provider=provider)
333
+ p_with_min_load = frozenset(
334
+ p for p, m in ctm if m == "min_load_efficiency"
335
+ )
336
+ minload = [p for p in inp["processes"] if p in p_with_min_load]
337
+ return _to_frame([(p,) for p in minload], ("process",))
338
+
339
+
340
+ # ---- (10) process__commodity__node_co2 + (11) process_co2 ------------------
341
+
342
+ def _rows_pcn_co2(inp: dict, solve_data_dir: Path,
343
+ *, provider: "object | None" = None) -> list[tuple[str, str, str]]:
344
+ cn_co2 = _read_pairs(solve_data_dir / "commodity_node_co2.csv",
345
+ provider=provider)
346
+ arc_endpoints_acc: dict[str, dict[str, None]] = {}
347
+ for p, n in inp["sources"] + inp["sinks"]:
348
+ arc_endpoints_acc.setdefault(p, {})[n] = None
349
+ arc_endpoints: dict[str, frozenset[str]] = {
350
+ p: frozenset(d.keys()) for p, d in arc_endpoints_acc.items()
351
+ }
352
+ rows: list[tuple[str, str, str]] = []
353
+ for p in inp["processes"]:
354
+ nodes_for_p = arc_endpoints.get(p, frozenset())
355
+ for c, n in cn_co2:
356
+ if n in nodes_for_p:
357
+ rows.append((p, c, n))
358
+ return list(dict.fromkeys(rows))
359
+
360
+
361
+ def _compute_process__commodity__node_co2(
362
+ inp: dict, solve_data_dir: Path,
363
+ *, provider: "object | None" = None,
364
+ ) -> pl.DataFrame:
365
+ return _to_frame(
366
+ _rows_pcn_co2(inp, solve_data_dir, provider=provider),
367
+ ("process", "commodity", "node"),
368
+ )
369
+
370
+
371
+ def _compute_process_co2(inp: dict, solve_data_dir: Path,
372
+ *, provider: "object | None" = None) -> pl.DataFrame:
373
+ seen: dict[str, None] = {}
374
+ for p, _, _ in _rows_pcn_co2(inp, solve_data_dir, provider=provider):
375
+ seen.setdefault(p, None)
376
+ return _to_frame([(p,) for p in seen.keys()], ("process",))
377
+
378
+
379
+ # ---- (12) process_source_sink ----------------------------------------------
380
+
381
+ def _compute_process_source_sink(
382
+ inp: dict, disk: dict,
383
+ ) -> pl.DataFrame:
384
+ src_to_proc = _rows_source_toProcess(inp)
385
+ proc_to_snk = _rows_process_toSink(inp)
386
+ profile_to_sink_3 = _profile_to_sink_3(inp)
387
+ source_to_profile_3 = _source_to_profile_3(inp)
388
+ pss_union: dict[tuple[str, ...], None] = {}
389
+ for r in (disk["sst"] + disk["sts"] + src_to_proc + proc_to_snk
390
+ + disk["snk_to_proc"] + disk["proc_to_src"]
391
+ + profile_to_sink_3 + source_to_profile_3
392
+ + disk["proc_to_snk_noConv"] + disk["src_to_proc_noConv"]):
393
+ pss_union.setdefault(tuple(r), None)
394
+ return _to_frame(list(pss_union.keys()), ("process", "source", "sink"))
395
+
396
+
397
+ # ---- (13) process_source_sink_alwaysProcess --------------------------------
398
+
399
+ def _compute_process_source_sink_alwaysProcess(
400
+ inp: dict, disk: dict, solve_data_dir: Path,
401
+ *, provider: "object | None" = None,
402
+ ) -> pl.DataFrame:
403
+ src_to_proc = _rows_source_toProcess(inp)
404
+ proc_to_snk = _rows_process_toSink(inp)
405
+ profile_to_sink_3 = _profile_to_sink_3(inp)
406
+ source_to_profile_3 = _source_to_profile_3(inp)
407
+ src_to_proc_d = _read_n_col(
408
+ solve_data_dir / "process_source_toProcess_direct.csv", 3,
409
+ provider=provider,
410
+ )
411
+ proc_to_snk_d = _read_n_col(
412
+ solve_data_dir / "process_process_toSink_direct.csv", 3,
413
+ provider=provider,
414
+ )
415
+ snk_to_proc_d = _read_n_col(
416
+ solve_data_dir / "process_sink_toProcess_direct.csv", 3,
417
+ provider=provider,
418
+ )
419
+ proc_to_src_d = _read_n_col(
420
+ solve_data_dir / "process_process_toSource_direct.csv", 3,
421
+ provider=provider,
422
+ )
423
+ pssa: dict[tuple[str, ...], None] = {}
424
+ for r in (src_to_proc_d + proc_to_snk_d + snk_to_proc_d + proc_to_src_d
425
+ + src_to_proc + proc_to_snk
426
+ + disk["snk_to_proc"] + disk["proc_to_src"]
427
+ + profile_to_sink_3 + source_to_profile_3
428
+ + disk["proc_to_snk_noConv"] + disk["src_to_proc_noConv"]):
429
+ pssa.setdefault(tuple(r), None)
430
+ return _to_frame(list(pssa.keys()), ("process", "source", "sink"))
431
+
432
+
433
+ # ---- (14) process__source__sink__profile__profile_method_direct ------------
434
+
435
+ def _compute_process__source__sink__profile__profile_method_direct(
436
+ inp: dict, solve_data_dir: Path,
437
+ *, provider: "object | None" = None,
438
+ ) -> pl.DataFrame:
439
+ sst = _read_n_col(solve_data_dir / "process_source_toSink.csv", 3,
440
+ provider=provider)
441
+ profiles = _read_n_col(
442
+ inp["input_dir"] / "process__node__profile__profile_method.csv", 4,
443
+ provider=provider,
444
+ )
445
+ p_n_to_fm: dict[tuple[str, str], list[tuple[str, str]]] = {}
446
+ for p, n, f, fm in profiles:
447
+ p_n_to_fm.setdefault((p, n), []).append((f, fm))
448
+ rows: list[tuple[str, ...]] = []
449
+ for p, source, sink in sst:
450
+ if p not in inp["p_with_direct"]:
451
+ continue
452
+ seen_fm: dict[tuple[str, str], None] = {}
453
+ for f, fm in p_n_to_fm.get((p, source), ()):
454
+ seen_fm.setdefault((f, fm), None)
455
+ for f, fm in p_n_to_fm.get((p, sink), ()):
456
+ seen_fm.setdefault((f, fm), None)
457
+ for f, fm in seen_fm:
458
+ rows.append((p, source, sink, f, fm))
459
+ return _to_frame(
460
+ list(dict.fromkeys(rows)),
461
+ ("process", "source", "sink", "profile", "profile_method"),
462
+ )
463
+
464
+
465
+ # ---- Wrapper ---------------------------------------------------------------
466
+
467
+
468
+ def emit_process_arc_unions(input_dir: Path, solve_data_dir: Path,
469
+ *, provider) -> None:
470
+ """Emit ``process_arc_unions`` to the Provider.
471
+ Emits 7 live frames under ``solve_data/<basename>`` keys via
472
+ :func:`_emit` (dual-key registration); 7 reader-less frames (the
473
+ toSink/toProcess/profile projections + the process_co2 pair) were
474
+ pruned as dead. *solve_data_dir* is retained
475
+ because the shared input bundle still consumes it for sister input
476
+ reads (``_read_n_col`` / ``_read_pairs``).
477
+ """
478
+ inp = _arc_unions_inputs(input_dir, solve_data_dir, provider=provider)
479
+
480
+ _emit(provider, "solve_data/process_source_sink_eff.csv",
481
+ _compute_process_source_sink_eff(solve_data_dir, provider=provider))
482
+ disk = _disk_arc_lists(solve_data_dir, provider=provider)
483
+ _emit(provider, "solve_data/process_source_sink_noEff.csv",
484
+ _compute_process_source_sink_noEff(inp, disk))
485
+ _emit(provider, "solve_data/process_online.csv",
486
+ _compute_process_online(solve_data_dir, provider=provider))
487
+ _emit(provider, "solve_data/process_minload.csv",
488
+ _compute_process_minload(inp, solve_data_dir, provider=provider))
489
+ _emit(provider, "solve_data/process_source_sink.csv",
490
+ _compute_process_source_sink(inp, disk))
491
+ _emit(provider, "solve_data/process_source_sink_alwaysProcess.csv",
492
+ _compute_process_source_sink_alwaysProcess(
493
+ inp, disk, solve_data_dir, provider=provider))
494
+ _emit(provider,
495
+ "solve_data/process__source__sink__profile__profile_method_direct.csv",
496
+ _compute_process__source__sink__profile__profile_method_direct(
497
+ inp, solve_data_dir, provider=provider))
498
+
499
+
500
+ # ---------------------------------------------------------------------------
501
+ # emit_entity_period_calc_params — top-level dispatcher own-compute.
502
+ # ---------------------------------------------------------------------------
503
+
504
+
505
+ # ---- Phase E-b — _compute_* family for each emitted CSV ------------------
506
+ #
507
+ # The wrapper :func:`emit_entity_period_calc_params` constructs the shared
508
+ # input bundle once via :func:`_entity_period_inputs` and feeds private
509
+ # ``_compute_*`` helpers to avoid recomputing the PdLookup scans across
510
+ # the five outputs.
511
+
512
+
513
+ def _entity_period_inputs(input_dir: Path, solve_data_dir: Path,
514
+ *, provider: "object | None" = None) -> dict:
515
+ """Shared input bundle for the 5 entity-period computes."""
516
+ from flextool.engine_polars._pdt_lookup import PdLookup
517
+
518
+ pp = PdLookup(
519
+ pd_csv=input_dir / "pd_process.csv",
520
+ p_csv=input_dir / "p_process.csv",
521
+ period_branch_csv=solve_data_dir / "period__branch.csv",
522
+ provider=provider,
523
+ )
524
+ pn = PdLookup(
525
+ pd_csv=input_dir / "pd_node.csv",
526
+ p_csv=input_dir / "p_node.csv",
527
+ period_branch_csv=solve_data_dir / "period__branch.csv",
528
+ provider=provider,
529
+ )
530
+
531
+ return {
532
+ "pp": pp,
533
+ "pn": pn,
534
+ "process_set": frozenset(_read_singles(input_dir / "process.csv", provider=provider)),
535
+ "node_set": frozenset(_read_singles(input_dir / "node.csv", provider=provider)),
536
+ "process_period_in_use": _read_pairs(
537
+ solve_data_dir / "process__PeriodParam_in_use.csv", provider=provider),
538
+ "node_period_in_use": _read_pairs(
539
+ solve_data_dir / "node__PeriodParam_in_use.csv", provider=provider),
540
+ "period_with_history": _read_singles(
541
+ solve_data_dir / "period_with_history.csv", provider=provider),
542
+ "entities": _read_singles(input_dir / "entity.csv", provider=provider),
543
+ }
544
+
545
+
546
+ def _read_p_table(path: Path,
547
+ *, provider: "object | None" = None,
548
+ ) -> dict[tuple[str, str], float]:
549
+ """Read a 3-col (entity, paramName, value) table, silently skipping
550
+ rows whose value isn't a float. Mirrors the legacy local-loop that
551
+ preceded :func:`emit_entity_period_calc_params`."""
552
+ out: dict[tuple[str, str], float] = {}
553
+ df = provider.get(_provider_key(path))
554
+ if df is None:
555
+ return out
556
+ for row in df.iter_rows():
557
+ if len(row) < 3:
558
+ continue
559
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
560
+ if c0 and c1:
561
+ try:
562
+ out[(c0, c1)] = float(row[2])
563
+ except (ValueError, TypeError):
564
+ continue
565
+ return out
566
+
567
+
568
+ def _compute_pdProcess(inp: dict) -> pl.DataFrame:
569
+ rows: list[tuple[str, str, str, str]] = []
570
+ pp = inp["pp"]
571
+ for (p, param) in inp["process_period_in_use"]:
572
+ for d in inp["period_with_history"]:
573
+ v = pp.get(p, param, d)
574
+ rows.append((p, param, d, repr(v)))
575
+ return pl.DataFrame(
576
+ {
577
+ "process": [r[0] for r in rows],
578
+ "param": [r[1] for r in rows],
579
+ "period": [r[2] for r in rows],
580
+ "value": [r[3] for r in rows],
581
+ },
582
+ schema={"process": pl.Utf8, "param": pl.Utf8,
583
+ "period": pl.Utf8, "value": pl.Utf8},
584
+ )
585
+
586
+
587
+ def _compute_pdNode(inp: dict) -> pl.DataFrame:
588
+ rows: list[tuple[str, str, str, str]] = []
589
+ pn = inp["pn"]
590
+ for (n, param) in inp["node_period_in_use"]:
591
+ for d in inp["period_with_history"]:
592
+ v = pn.get(n, param, d)
593
+ rows.append((n, param, d, repr(v)))
594
+ return pl.DataFrame(
595
+ {
596
+ "node": [r[0] for r in rows],
597
+ "param": [r[1] for r in rows],
598
+ "period": [r[2] for r in rows],
599
+ "value": [r[3] for r in rows],
600
+ },
601
+ schema={"node": pl.Utf8, "param": pl.Utf8,
602
+ "period": pl.Utf8, "value": pl.Utf8},
603
+ )
604
+
605
+
606
+ def _compute_edEntity_lifetime(inp: dict) -> pl.DataFrame:
607
+ pp = inp["pp"]
608
+ pn = inp["pn"]
609
+ process_set = inp["process_set"]
610
+ node_set = inp["node_set"]
611
+ rows: list[tuple[str, str, str]] = []
612
+ for e in inp["entities"]:
613
+ for d in inp["period_with_history"]:
614
+ if e in process_set:
615
+ v = pp.get(e, "lifetime", d)
616
+ elif e in node_set:
617
+ v = pn.get(e, "lifetime", d)
618
+ else:
619
+ v = 0.0
620
+ rows.append((e, d, repr(v)))
621
+ return pl.DataFrame(
622
+ {"entity": [r[0] for r in rows],
623
+ "period": [r[1] for r in rows],
624
+ "value": [r[2] for r in rows]},
625
+ schema={"entity": pl.Utf8, "period": pl.Utf8, "value": pl.Utf8},
626
+ )
627
+
628
+
629
+ def _compute_ed_fixed_cost(inp: dict) -> pl.DataFrame:
630
+ pp = inp["pp"]
631
+ pn = inp["pn"]
632
+ process_set = inp["process_set"]
633
+ node_set = inp["node_set"]
634
+ rows: list[tuple[str, str, str]] = []
635
+ for e in inp["entities"]:
636
+ for d in inp["period_with_history"]:
637
+ v = (1000.0 if e in node_set else 0.0) * pn.get(e, "fixed_cost", d) \
638
+ + (1000.0 if e in process_set else 0.0) * pp.get(e, "fixed_cost", d)
639
+ rows.append((e, d, repr(v)))
640
+ return pl.DataFrame(
641
+ {"entity": [r[0] for r in rows],
642
+ "period": [r[1] for r in rows],
643
+ "value": [r[2] for r in rows]},
644
+ schema={"entity": pl.Utf8, "period": pl.Utf8, "value": pl.Utf8},
645
+ )
646
+
647
+
648
+ def _compute_p_entity_unitsize(input_dir: Path, inp: dict,
649
+ *, provider: "object | None" = None,
650
+ ) -> pl.DataFrame:
651
+ p_process = _read_p_table(input_dir / "p_process.csv", provider=provider)
652
+ p_node = _read_p_table(input_dir / "p_node.csv", provider=provider)
653
+ process_set = inp["process_set"]
654
+ node_set = inp["node_set"]
655
+ rows: list[tuple[str, str]] = []
656
+ for e in inp["entities"]:
657
+ if e in process_set:
658
+ v = (p_process.get((e, "virtual_unitsize"), 0.0)
659
+ or p_process.get((e, "existing"), 0.0)
660
+ or 1000.0)
661
+ elif e in node_set:
662
+ v = (p_node.get((e, "virtual_unitsize"), 0.0)
663
+ or p_node.get((e, "existing"), 0.0)
664
+ or 1000.0)
665
+ else:
666
+ v = 0.0
667
+ rows.append((e, repr(v)))
668
+ return pl.DataFrame(
669
+ {"entity": [r[0] for r in rows],
670
+ "value": [r[1] for r in rows]},
671
+ schema={"entity": pl.Utf8, "value": pl.Utf8},
672
+ )
673
+
674
+
675
+ def emit_entity_period_calc_params(input_dir: Path,
676
+ solve_data_dir: Path,
677
+ *, provider) -> None:
678
+ """Emit ``entity_period_calc_params`` to the Provider.
679
+ Emits the same 5 frames under ``solve_data/<basename>`` keys via
680
+ :func:`_emit` (dual-key registration).
681
+ """
682
+ inp = _entity_period_inputs(input_dir, solve_data_dir, provider=provider)
683
+ _emit(provider, "solve_data/pdProcess.csv", _compute_pdProcess(inp))
684
+ _emit(provider, "solve_data/pdNode.csv", _compute_pdNode(inp))
685
+ _emit(provider, "solve_data/edEntity_lifetime.csv",
686
+ _compute_edEntity_lifetime(inp))
687
+ _emit(provider, "solve_data/ed_fixed_cost.csv",
688
+ _compute_ed_fixed_cost(inp))
689
+ _emit(provider, "solve_data/p_entity_unitsize.csv",
690
+ _compute_p_entity_unitsize(input_dir, inp, provider=provider))