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,709 @@
1
+ """Chain-cluster entity-period parameter writers.
2
+
3
+ Three ``write_*`` helpers emitting the per-(entity, period)
4
+ existing/capacity-max chain that the LP build consumes via
5
+ ``p_entity_*_existing_*`` and ``p_entity_*_capacity_*`` parameters:
6
+
7
+ * :func:`write_p_entity_pre_existing` (12-branch).
8
+ * :func:`write_p_entity_existing_chain` (3 live emits).
9
+ * :func:`write_p_entity_capacity_max_chain` (2 live emits).
10
+
11
+ The chain-existing / chain-cap-max writers are the most state-dependent
12
+ writers in Phase 1: they straddle the per-solve handoff boundary (prior
13
+ solve's ``realized_existing``/``realized_invest``/``divest_cumulative``
14
+ either as in-memory ``SolveHandoff`` carriers or as CSVs left by the
15
+ parent solve's output writer). We keep the legacy dict-keyed lookup
16
+ shape — it's already optimal for the per-row access pattern and the
17
+ intricate branching is much easier to verify against the mod source
18
+ when written procedurally.
19
+
20
+ Byte-for-byte parity with the legacy emitters is the gate; tests live
21
+ in ``tests/engine_polars/test_writer_port_phase1.py``.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING
27
+
28
+ import polars as pl
29
+
30
+ from flextool.engine_polars._emit_provider_io import (
31
+ _emit,
32
+ _provider_key,
33
+ )
34
+ from flextool.engine_polars import _provider_keys as K
35
+ from flextool.engine_polars._provider_translators import read_handoff_frame
36
+
37
+ if TYPE_CHECKING:
38
+ from ._solve_handoff import SolveHandoff # noqa: F401 — retained for legacy callers
39
+
40
+
41
+ def _ed_value_frame(
42
+ rows: list[tuple[str, str, object]],
43
+ ) -> pl.DataFrame:
44
+ """Build a 3-col ``(entity, period, value)`` Utf8 frame whose ``value``
45
+ cells are ``repr(v)`` strings — preserving the *Python type-as-emitted*
46
+ semantics of the legacy ``f"{e},{d},{repr(v)}\\n"`` line builders.
47
+
48
+ Importantly we call ``repr`` directly (NOT ``repr(float(v))``):
49
+ ``write_p_entity_divest_cumulative_max`` accumulates a ``period_sum =
50
+ sum(empty) → int(0)``, and the legacy CSV column for those rows is
51
+ ``"0"``, not ``"0.0"``. Similarly
52
+ ``write_p_entity_existing_integer_count`` emits ``repr(round(cnt))``
53
+ which is an int repr. Coercing to float here would silently break
54
+ byte-parity on either path — see commit notes for the symptom.
55
+ Storing ``value`` as Utf8 preserves the bit-exact precision parity the
56
+ legacy CSV writers achieved through ``repr`` (see
57
+ :mod:`._emit_arc_unions` docstring).
58
+ """
59
+ return pl.DataFrame(
60
+ {
61
+ "entity": [r[0] for r in rows],
62
+ "period": [r[1] for r in rows],
63
+ "value": [repr(r[2]) for r in rows],
64
+ },
65
+ schema={"entity": pl.Utf8, "period": pl.Utf8, "value": pl.Utf8},
66
+ )
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Native-frame readers.
71
+ #
72
+ # These read the in-memory polars frame directly from the Provider via
73
+ # ``provider.get(_provider_key(path))`` instead of round-tripping through
74
+ # CSV text (the legacy ``_provider_open`` + ``csv.reader`` path).
75
+ #
76
+ # Type-fidelity contract — reproduce *exactly* what ``csv.reader`` over a
77
+ # ``DataFrame.write_csv`` serialisation would have yielded:
78
+ #
79
+ # * Key columns (the dict-key / structural string positions) were
80
+ # ``csv.reader`` strings. ``write_csv`` serialises ``null`` → ``""``
81
+ # and any scalar (Enum / Int / Float / Utf8) → its string form. We
82
+ # coerce each key cell with :func:`_cell_str` and apply the original
83
+ # truthiness guard to the *string* form so a null cell is skipped
84
+ # (matching the legacy ``if row[i]`` test) while a literal ``"0"`` is
85
+ # kept.
86
+ # * Value columns were re-coerced with ``float(...)`` / ``int(...)``.
87
+ # We apply the same coercion to the native cell and widen the legacy
88
+ # ``except ValueError`` to ``except (ValueError, TypeError)`` so a
89
+ # null value cell is skipped (matching the legacy CSV behaviour where
90
+ # the empty-string value cell raised ``ValueError``).
91
+ #
92
+ # ``provider.get`` returns data rows only (no header), so there is no
93
+ # header row to skip; an empty / missing frame yields the same empty
94
+ # output the legacy loop produced.
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ def _cell_str(value: "object | None") -> str:
99
+ """Reproduce a ``csv.reader`` cell string for a native frame value.
100
+
101
+ ``DataFrame.write_csv`` renders ``null`` as the empty string and every
102
+ other scalar as its textual form; ``csv.reader`` then reads those
103
+ strings back. Mirror that here so dict keys / structural strings stay
104
+ byte-identical to the legacy CSV round-trip.
105
+ """
106
+ return "" if value is None else str(value)
107
+
108
+
109
+ def _read_singles(path: Path,
110
+ *, provider: "object | None" = None) -> list[str]:
111
+ df = provider.get(_provider_key(path))
112
+ if df is None:
113
+ return []
114
+ out: list[str] = []
115
+ for row in df.iter_rows():
116
+ if not row:
117
+ continue
118
+ c0 = _cell_str(row[0])
119
+ if c0:
120
+ out.append(c0)
121
+ return out
122
+
123
+
124
+ def _read_pairs(path: Path,
125
+ *, provider: "object | None" = None) -> list[tuple[str, str]]:
126
+ df = provider.get(_provider_key(path))
127
+ if df is None:
128
+ return []
129
+ out: list[tuple[str, str]] = []
130
+ for row in df.iter_rows():
131
+ if len(row) < 2:
132
+ continue
133
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
134
+ if c0 and c1:
135
+ out.append((c0, c1))
136
+ return out
137
+
138
+
139
+ def _read_triples(path: Path,
140
+ *, provider: "object | None" = None) -> list[tuple[str, str, str]]:
141
+ df = provider.get(_provider_key(path))
142
+ if df is None:
143
+ return []
144
+ out: list[tuple[str, str, str]] = []
145
+ for row in df.iter_rows():
146
+ if len(row) < 3:
147
+ continue
148
+ c0, c1, c2 = _cell_str(row[0]), _cell_str(row[1]), _cell_str(row[2])
149
+ if c0 and c1 and c2:
150
+ out.append((c0, c1, c2))
151
+ return out
152
+
153
+
154
+ def _load_ed_value_csv(path: Path,
155
+ *, provider: "object | None" = None,
156
+ ) -> dict[tuple[str, str], float]:
157
+ """``[entity, period, value]`` (data rows) → ``{(e, d): v}``."""
158
+ out: dict[tuple[str, str], float] = {}
159
+ df = provider.get(_provider_key(path))
160
+ if df is None:
161
+ return out
162
+ for row in df.iter_rows():
163
+ if len(row) < 3:
164
+ continue
165
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
166
+ if c0 and c1:
167
+ try:
168
+ out[(c0, c1)] = float(row[2])
169
+ except (ValueError, TypeError):
170
+ continue
171
+ return out
172
+
173
+
174
+ def _load_e_value_csv(path: Path,
175
+ *, provider: "object | None" = None,
176
+ ) -> dict[str, float]:
177
+ """``[entity, value]`` (data rows) → ``{e: v}``."""
178
+ out: dict[str, float] = {}
179
+ df = provider.get(_provider_key(path))
180
+ if df is None:
181
+ return out
182
+ for row in df.iter_rows():
183
+ if len(row) < 2:
184
+ continue
185
+ c0 = _cell_str(row[0])
186
+ if c0:
187
+ try:
188
+ out[c0] = float(row[1])
189
+ except (ValueError, TypeError):
190
+ continue
191
+ return out
192
+
193
+
194
+ def _load_param_value_at(
195
+ path: Path, param_col: int, param_value: str,
196
+ key_cols: tuple[int, ...], value_col: int,
197
+ *, provider: "object | None" = None,
198
+ ) -> dict[tuple[str, ...], float]:
199
+ """``[..., param, ..., value]`` filtered to rows where column ``param_col``
200
+ equals ``param_value`` → ``{tuple(row[c] for c in key_cols): value}``."""
201
+ out: dict[tuple[str, ...], float] = {}
202
+ df = provider.get(_provider_key(path))
203
+ if df is None:
204
+ return out
205
+ for row in df.iter_rows():
206
+ if len(row) <= max(param_col, value_col, *key_cols):
207
+ continue
208
+ if _cell_str(row[param_col]) != param_value:
209
+ continue
210
+ key = tuple(_cell_str(row[c]) for c in key_cols)
211
+ if not all(key):
212
+ continue
213
+ try:
214
+ out[key] = float(row[value_col])
215
+ except (ValueError, TypeError):
216
+ continue
217
+ return out
218
+
219
+
220
+ def _read_solve_first_flag(solve_data_dir: Path,
221
+ *, provider: "object | None" = None) -> bool:
222
+ """Read ``solve_data/p_model.csv`` for the ``solveFirst`` flag."""
223
+ pm_path = solve_data_dir / "p_model.csv"
224
+ df = provider.get(_provider_key(pm_path))
225
+ if df is None:
226
+ return False
227
+ for row in df.iter_rows():
228
+ if len(row) >= 2 and _cell_str(row[0]) == "solveFirst":
229
+ try:
230
+ return bool(int(row[1]))
231
+ except (ValueError, TypeError):
232
+ return False
233
+ return False
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # write_p_entity_pre_existing — mod L1886-1895 (12-branch lifetime-method
238
+ # gate × entity-kind × virtual_unitsize trichotomy).
239
+ # ---------------------------------------------------------------------------
240
+
241
+
242
+ def derive_p_entity_pre_existing(
243
+ input_dir: Path, solve_data_dir: Path,
244
+ *, provider: "object | None" = None,
245
+ ) -> pl.DataFrame:
246
+ """Pre-existing capacity per (entity, period) — 12-branch sum where
247
+ exactly one branch fires per (e, d) given the method/kind/unitsize
248
+ trichotomy.
249
+
250
+ Equivalent simplified form (per legacy docstring):
251
+
252
+ method = entity__lifetime_method[e]
253
+ v_existing = pdProcess[e,'existing',d] if e in process
254
+ else pdNode[e,'existing',d] if e in node
255
+ else 0
256
+ v_unit = p_process[e,'virtual_unitsize'] if e in process
257
+ else p_node[e,'virtual_unitsize'] if e in node
258
+ else 0
259
+ if method not in {reinvest_automatic, reinvest_choice, no_investment}: 0
260
+ elif method ∈ {reinvest_choice, no_investment}
261
+ and not (p_years_d[d] <
262
+ Σ_{d_first ∈ period_first} (p_years_d[d_first]
263
+ + edEntity_lifetime[e, d_first])):
264
+ 0
265
+ else: v_existing * v_unit if v_unit else v_existing
266
+
267
+ Output domain: ``entity × period_in_use``. Reads pdProcess /
268
+ pdNode / edEntity_lifetime CSVs that ``write_entity_period_calc_params``
269
+ has emitted earlier in the same per-solve pass.
270
+ """
271
+ process_set = frozenset(_read_singles(input_dir / "process.csv",
272
+ provider=provider))
273
+ node_set = frozenset(_read_singles(input_dir / "node.csv",
274
+ provider=provider))
275
+ entities = _read_singles(input_dir / "entity.csv", provider=provider)
276
+ period_in_use = _read_singles(solve_data_dir / "period_in_use_set.csv",
277
+ provider=provider)
278
+ period_first = _read_singles(solve_data_dir / "period_first.csv",
279
+ provider=provider)
280
+
281
+ lifetime_method: dict[str, str] = {
282
+ e_: m for e_, m in _read_pairs(solve_data_dir / "entity__lifetime_method.csv",
283
+ provider=provider)
284
+ }
285
+ p_years_d = _load_e_value_csv(solve_data_dir / "p_years_d.csv",
286
+ provider=provider)
287
+
288
+ ed_lifetime: dict[tuple[str, str], float] = {}
289
+ for e_, d_, v_ in _read_triples(solve_data_dir / "edEntity_lifetime.csv",
290
+ provider=provider):
291
+ try:
292
+ ed_lifetime[(e_, d_)] = float(v_)
293
+ except ValueError:
294
+ continue
295
+
296
+ # pdProcess.csv / pdNode.csv: [entity, param, period, value]; filter to
297
+ # rows where param == 'existing'.
298
+ pd_existing_proc = _load_param_value_at(
299
+ solve_data_dir / "pdProcess.csv",
300
+ param_col=1, param_value="existing",
301
+ key_cols=(0, 2), value_col=3,
302
+ provider=provider,
303
+ )
304
+ pd_existing_node = _load_param_value_at(
305
+ solve_data_dir / "pdNode.csv",
306
+ param_col=1, param_value="existing",
307
+ key_cols=(0, 2), value_col=3,
308
+ provider=provider,
309
+ )
310
+
311
+ # p_process.csv / p_node.csv: [entity, param, value]; filter to
312
+ # param == 'virtual_unitsize'.
313
+ p_process_vu = _load_param_value_at(
314
+ input_dir / "p_process.csv",
315
+ param_col=1, param_value="virtual_unitsize",
316
+ key_cols=(0,), value_col=2,
317
+ provider=provider,
318
+ )
319
+ p_node_vu = _load_param_value_at(
320
+ input_dir / "p_node.csv",
321
+ param_col=1, param_value="virtual_unitsize",
322
+ key_cols=(0,), value_col=2,
323
+ provider=provider,
324
+ )
325
+
326
+ # Per-entity lifetime gate sum:
327
+ # Σ_{d_first ∈ period_first} (p_years_d[d_first]
328
+ # + edEntity_lifetime[e, d_first])
329
+ def _life_sum(e: str) -> float:
330
+ return sum(
331
+ p_years_d.get(d_first, 0.0) + ed_lifetime.get((e, d_first), 0.0)
332
+ for d_first in period_first
333
+ )
334
+
335
+ rows: list[tuple[str, str, float]] = []
336
+ for e in entities:
337
+ method = lifetime_method.get(e, "")
338
+ is_proc = e in process_set
339
+ is_node = e in node_set
340
+ if is_proc:
341
+ v_unit = p_process_vu.get((e,), 0.0)
342
+ elif is_node:
343
+ v_unit = p_node_vu.get((e,), 0.0)
344
+ else:
345
+ v_unit = 0.0
346
+ gate_sum = (
347
+ _life_sum(e)
348
+ if method in ("reinvest_choice", "no_investment")
349
+ else None
350
+ )
351
+ for d in period_in_use:
352
+ v: float = 0.0
353
+ if method in ("reinvest_automatic", "reinvest_choice", "no_investment"):
354
+ gate_passes = method == "reinvest_automatic" or (
355
+ gate_sum is not None and p_years_d.get(d, 0.0) < gate_sum
356
+ )
357
+ if gate_passes:
358
+ if is_proc:
359
+ pd_e = pd_existing_proc.get((e, d), 0.0)
360
+ elif is_node:
361
+ pd_e = pd_existing_node.get((e, d), 0.0)
362
+ else:
363
+ pd_e = 0.0
364
+ v = pd_e * v_unit if v_unit else pd_e
365
+ rows.append((e, d, v))
366
+
367
+ return _ed_value_frame(rows)
368
+
369
+
370
+ def emit_p_entity_pre_existing(
371
+ input_dir: Path, solve_data_dir: Path,
372
+ *, provider,
373
+ ) -> None:
374
+ """Emit ``p_entity_pre_existing`` to the Provider."""
375
+ _emit(provider, "solve_data/p_entity_pre_existing.csv",
376
+ derive_p_entity_pre_existing(input_dir, solve_data_dir,
377
+ provider=provider))
378
+
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # write_p_entity_existing_chain — mod L1680-1697 (5 cascading entity-capacity
382
+ # params; straddles the per-solve handoff boundary).
383
+ # ---------------------------------------------------------------------------
384
+
385
+
386
+ def _load_realized_from_handoff(
387
+ *, provider: "object | None" = None,
388
+ ) -> tuple[dict[tuple[str, str], float], dict[tuple[str, str], float]]:
389
+ """Resolve ``(realized_existing, realized_invest)`` from the
390
+ Provider's ``handoff/realized_*`` carriers, populated by the
391
+ iteration-start translator.
392
+
393
+ Returns ``(ppec, ppic)`` where ``ppec`` maps
394
+ ``(entity, period) → realized_existing`` and ``ppic`` maps
395
+ ``(entity, period) → realized_invest``. An empty/None handoff
396
+ frame yields an empty mapping — the translator pipeline always
397
+ primes these carriers (header-only frame when no prior handoff
398
+ exists, populated frame otherwise), so this is the sole source of
399
+ truth.
400
+ """
401
+ ppec: dict[tuple[str, str], float] = {}
402
+ ppic: dict[tuple[str, str], float] = {}
403
+ realized_existing = read_handoff_frame(provider, K.HANDOFF_REALIZED_EXISTING)
404
+ realized_invest = read_handoff_frame(provider, K.HANDOFF_REALIZED_INVEST)
405
+ if realized_existing is not None:
406
+ for r in realized_existing.iter_rows(named=True):
407
+ ppec[(str(r["entity"]), str(r["period"]))] = float(r["value"])
408
+ if realized_invest is not None:
409
+ for r in realized_invest.iter_rows(named=True):
410
+ ppic[(str(r["entity"]), str(r["period"]))] = float(r["value"])
411
+ return ppec, ppic
412
+
413
+
414
+ def _compute_p_entity_existing_chain(
415
+ input_dir: Path, solve_data_dir: Path,
416
+ *, provider: "object | None" = None,
417
+ ) -> tuple[
418
+ list[tuple[str, str, float]], # all_existing rows
419
+ list[tuple[str, str, float]], # count rows
420
+ list[tuple[str, str, int]], # integer_count rows (legacy emits int repr)
421
+ list[tuple[str, str, float]], # previously_invested rows
422
+ ]:
423
+ """Compute the four row-streams emitted by
424
+ :func:`emit_p_entity_existing_chain` in their canonical
425
+ (entity-major, period-in-use-minor) order.
426
+
427
+ Kept as a single compute so the orchestrator runs it once and funnels
428
+ each row-stream through :func:`_emit`, so the per-sub-solve
429
+ :mod:`._flex_data_accumulator` captures the resulting frames.
430
+ """
431
+ solve_first = _read_solve_first_flag(solve_data_dir, provider=provider)
432
+ entities = _read_singles(input_dir / "entity.csv", provider=provider)
433
+ periods_in_use = _read_singles(
434
+ solve_data_dir / "period_in_use_set.csv", provider=provider,
435
+ )
436
+
437
+ pre_existing = _load_ed_value_csv(
438
+ solve_data_dir / "p_entity_pre_existing.csv", provider=provider,
439
+ )
440
+ unitsize = _load_e_value_csv(
441
+ solve_data_dir / "p_entity_unitsize.csv", provider=provider,
442
+ )
443
+
444
+ edd_by_ed: dict[tuple[str, str], list[str]] = {}
445
+ for (e, d_h, d) in _read_triples(
446
+ solve_data_dir / "edd_history.csv", provider=provider,
447
+ ):
448
+ edd_by_ed.setdefault((e, d), []).append(d_h)
449
+
450
+ ppec, ppic = _load_realized_from_handoff(provider=provider)
451
+
452
+ ed_history_realized: set[tuple[str, str]] = set(ppec.keys())
453
+ for e_, d_ in _read_pairs(
454
+ solve_data_dir / "ed_history_realized_first.csv", provider=provider,
455
+ ):
456
+ ed_history_realized.add((e_, d_))
457
+
458
+ entity_divest = frozenset(_read_singles(
459
+ solve_data_dir / "entityDivest.csv", provider=provider,
460
+ ))
461
+ p_divested: dict[str, float] = {}
462
+ divest_cumulative = read_handoff_frame(provider, K.HANDOFF_DIVEST_CUMULATIVE)
463
+ if divest_cumulative is not None:
464
+ for r in divest_cumulative.iter_rows(named=True):
465
+ p_divested[str(r["entity"])] = float(r["value"])
466
+
467
+ later_existing: dict[tuple[str, str], float] = {}
468
+ later_invested: dict[tuple[str, str], float] = {}
469
+ if not solve_first:
470
+ for e in entities:
471
+ for d in periods_in_use:
472
+ tot_e = 0.0
473
+ tot_i = 0.0
474
+ for d_h in edd_by_ed.get((e, d), ()):
475
+ if (e, d_h) in ed_history_realized:
476
+ tot_e += ppec.get((e, d_h), 0.0)
477
+ tot_i += ppic.get((e, d_h), 0.0)
478
+ later_existing[(e, d)] = tot_e
479
+ later_invested[(e, d)] = tot_i
480
+
481
+ all_rows: list[tuple[str, str, float]] = []
482
+ count_rows: list[tuple[str, str, float]] = []
483
+ int_count_rows: list[tuple[str, str, int]] = []
484
+ prev_inv_rows: list[tuple[str, str, float]] = []
485
+ for e in entities:
486
+ us = unitsize.get(e, 0.0)
487
+ for d in periods_in_use:
488
+ if solve_first:
489
+ v_all = pre_existing.get((e, d), 0.0)
490
+ else:
491
+ v_all = later_existing.get((e, d), 0.0)
492
+ if e in entity_divest:
493
+ v_all -= p_divested.get(e, 0.0)
494
+ all_rows.append((e, d, v_all))
495
+ cnt = v_all / us if us else 0.0
496
+ count_rows.append((e, d, cnt))
497
+ # round() with a single arg returns int in Py3 → legacy emits
498
+ # ``repr(int)`` (e.g. ``"3"``, not ``"3.0"``). Preserve.
499
+ int_count_rows.append((e, d, round(cnt)))
500
+ v_prev = 0.0 if solve_first else later_invested.get((e, d), 0.0)
501
+ prev_inv_rows.append((e, d, v_prev))
502
+
503
+ return all_rows, count_rows, int_count_rows, prev_inv_rows
504
+
505
+
506
+ def emit_p_entity_existing_chain(
507
+ input_dir: Path, solve_data_dir: Path,
508
+ *, provider,
509
+ ) -> None:
510
+ """Emit ``p_entity_existing_chain`` to the Provider."""
511
+ all_rows, _count_rows, _int_rows, prev_rows = (
512
+ _compute_p_entity_existing_chain(
513
+ input_dir, solve_data_dir, provider=provider,
514
+ )
515
+ )
516
+ _emit(provider, "solve_data/p_entity_all_existing.csv",
517
+ _ed_value_frame(all_rows))
518
+ _emit(provider,
519
+ "solve_data/p_entity_previously_invested_capacity.csv",
520
+ _ed_value_frame(prev_rows))
521
+
522
+
523
+ # ---------------------------------------------------------------------------
524
+ # write_p_entity_capacity_max_chain — mod L1699-1764 (4 cascading entity-
525
+ # capacity ceiling params).
526
+ # ---------------------------------------------------------------------------
527
+
528
+
529
+ def _compute_p_entity_capacity_max_chain(
530
+ input_dir: Path, solve_data_dir: Path,
531
+ *, provider: "object | None" = None,
532
+ ) -> tuple[
533
+ list[tuple[str, str, float]], # max_capacity rows (period_in_use only)
534
+ list[tuple[str, str, float]], # max_units rows (full period_set)
535
+ list[tuple[str, str, float]], # invest_cumulative_max rows
536
+ list[tuple[str, str, float]], # dispatch_capacity_max rows
537
+ ]:
538
+ """Compute the four row-streams emitted by
539
+ :func:`emit_p_entity_capacity_max_chain` in their canonical order.
540
+
541
+ Kept as a single compute so the orchestrator funnels each stream
542
+ through :func:`_emit` for accumulator capture.
543
+ """
544
+ entities = _read_singles(input_dir / "entity.csv", provider=provider)
545
+ periods = _read_singles(solve_data_dir / "period_set.csv", provider=provider)
546
+ period_in_use = frozenset(
547
+ _read_singles(solve_data_dir / "period_in_use_set.csv", provider=provider)
548
+ )
549
+
550
+ all_existing = _load_ed_value_csv(solve_data_dir / "p_entity_all_existing.csv",
551
+ provider=provider)
552
+ cum_max_cap = _load_ed_value_csv(solve_data_dir / "ed_cumulative_max_capacity.csv",
553
+ provider=provider)
554
+ invest_max_period = _load_ed_value_csv(
555
+ solve_data_dir / "ed_invest_max_period.csv",
556
+ provider=provider,
557
+ )
558
+ unitsize = _load_e_value_csv(solve_data_dir / "p_entity_unitsize.csv",
559
+ provider=provider)
560
+ e_invest_max_total = _load_e_value_csv(
561
+ solve_data_dir / "e_invest_max_total.csv",
562
+ provider=provider,
563
+ )
564
+
565
+ invest_cumulative = frozenset(
566
+ _read_pairs(solve_data_dir / "ed_invest_cumulative.csv", provider=provider)
567
+ )
568
+ invest_period = frozenset(
569
+ _read_pairs(solve_data_dir / "ed_invest_period.csv", provider=provider)
570
+ )
571
+ invest_total = frozenset(_read_singles(solve_data_dir / "e_invest_total.csv",
572
+ provider=provider))
573
+ invest_forbidden = frozenset(
574
+ _read_pairs(solve_data_dir / "ed_invest_forbidden_no_investment.csv",
575
+ provider=provider)
576
+ )
577
+ entity_invest = frozenset(_read_singles(solve_data_dir / "entityInvest.csv",
578
+ provider=provider))
579
+
580
+ invest_method_pairs = frozenset(
581
+ _read_pairs(input_dir / "entity__invest_method.csv", provider=provider)
582
+ )
583
+
584
+ p_unc = 1000000.0
585
+ pmaxf_path = input_dir / "p_max_flow_for_unconstrained_variables.csv"
586
+ pmaxf_df = provider.get(_provider_key(pmaxf_path))
587
+ if pmaxf_df is not None:
588
+ max_v: float | None = None
589
+ for row in pmaxf_df.iter_rows():
590
+ if len(row) >= 2 and _cell_str(row[0]):
591
+ try:
592
+ v = float(row[1])
593
+ except (ValueError, TypeError):
594
+ continue
595
+ if max_v is None or v > max_v:
596
+ max_v = v
597
+ if max_v is not None:
598
+ p_unc = max_v
599
+
600
+ edd_invest_by_ed: dict[tuple[str, str], list[str]] = {}
601
+ for (e, d_inv, d) in _read_triples(solve_data_dir / "edd_invest.csv",
602
+ provider=provider):
603
+ edd_invest_by_ed.setdefault((e, d), []).append(d_inv)
604
+
605
+ max_capacity: dict[tuple[str, str], float] = {}
606
+ mc_rows: list[tuple[str, str, float]] = []
607
+ for e in entities:
608
+ in_total = e in invest_total
609
+ has_no_limit = (e, "invest_no_limit") in invest_method_pairs
610
+ for d in periods:
611
+ if d not in period_in_use:
612
+ max_capacity[(e, d)] = 0.0
613
+ continue
614
+ if (e, d) in invest_cumulative:
615
+ v = cum_max_cap.get((e, d), 0.0)
616
+ else:
617
+ v = all_existing.get((e, d), 0.0)
618
+ in_period = (e, d) in invest_period
619
+ imp = invest_max_period.get((e, d), 0.0)
620
+ eim = e_invest_max_total.get(e, 0.0)
621
+ if in_period and not in_total:
622
+ v += imp
623
+ if in_total and not in_period:
624
+ v += eim
625
+ if in_period and in_total:
626
+ v += max(imp, eim)
627
+ if has_no_limit:
628
+ v += p_unc
629
+ max_capacity[(e, d)] = v
630
+ mc_rows.append((e, d, v))
631
+
632
+ mu_rows: list[tuple[str, str, float]] = []
633
+ for e in entities:
634
+ us = unitsize.get(e, 0.0)
635
+ for d in periods:
636
+ mc = max_capacity[(e, d)]
637
+ v = mc / us if us else 0.0
638
+ mu_rows.append((e, d, v))
639
+
640
+ invest_cum_max: dict[tuple[str, str], float] = {}
641
+ icm_rows: list[tuple[str, str, float]] = []
642
+ for e in entities:
643
+ if e not in entity_invest:
644
+ continue
645
+ in_total = e in invest_total
646
+ has_no_limit_method = (e, "invest_no_limit") in invest_method_pairs
647
+ for d in periods:
648
+ if d not in period_in_use:
649
+ invest_cum_max[(e, d)] = 0.0
650
+ continue
651
+ if (e, d) in invest_cumulative:
652
+ v = max(
653
+ 0.0,
654
+ cum_max_cap.get((e, d), 0.0)
655
+ - all_existing.get((e, d), 0.0),
656
+ )
657
+ elif has_no_limit_method:
658
+ v = p_unc
659
+ else:
660
+ v = 0.0
661
+ in_period = (e, d) in invest_period
662
+ if in_period and not in_total:
663
+ per_period_sum = sum(
664
+ invest_max_period.get((e, d_inv), 0.0)
665
+ for d_inv in edd_invest_by_ed.get((e, d), ())
666
+ if (e, d_inv) in invest_period
667
+ and (e, d_inv) not in invest_forbidden
668
+ )
669
+ v += per_period_sum
670
+ if in_total and not in_period:
671
+ v += e_invest_max_total.get(e, 0.0)
672
+ if in_period and in_total:
673
+ per_period_sum = sum(
674
+ invest_max_period.get((e, d_inv), 0.0)
675
+ for d_inv in edd_invest_by_ed.get((e, d), ())
676
+ if (e, d_inv) in invest_period
677
+ and (e, d_inv) not in invest_forbidden
678
+ )
679
+ v += max(
680
+ per_period_sum, e_invest_max_total.get(e, 0.0),
681
+ )
682
+ invest_cum_max[(e, d)] = v
683
+ icm_rows.append((e, d, v))
684
+
685
+ dcm_rows: list[tuple[str, str, float]] = []
686
+ for e in entities:
687
+ for d in periods:
688
+ if d not in period_in_use:
689
+ continue
690
+ v = all_existing.get((e, d), 0.0)
691
+ if e in entity_invest:
692
+ v += invest_cum_max.get((e, d), 0.0)
693
+ dcm_rows.append((e, d, v))
694
+
695
+ return mc_rows, mu_rows, icm_rows, dcm_rows
696
+
697
+
698
+ def emit_p_entity_capacity_max_chain(
699
+ input_dir: Path, solve_data_dir: Path,
700
+ *, provider,
701
+ ) -> None:
702
+ """Emit ``p_entity_capacity_max_chain`` to the Provider."""
703
+ _mc_rows, mu_rows, _icm_rows, dcm_rows = _compute_p_entity_capacity_max_chain(
704
+ input_dir, solve_data_dir, provider=provider,
705
+ )
706
+ _emit(provider, "solve_data/p_entity_max_units.csv",
707
+ _ed_value_frame(mu_rows))
708
+ _emit(provider, "solve_data/p_entity_dispatch_capacity_max.csv",
709
+ _ed_value_frame(dcm_rows))