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,574 @@
1
+ """Reserve calculated params — per-solve emitters.
2
+
3
+ Fired per-solve from ``_emit_solve_time.run`` at batches 43, 44 and 49.
4
+
5
+ Three public entry points:
6
+
7
+ * :func:`emit_pdtReserve_upDown_group` — mod L1319. 4-branch hourly
8
+ resolution for reserve groups, emitting ``pdtReserve_upDown_group.csv``
9
+ ``(reserve, upDown, group, param, period, time, value)``.
10
+ * :func:`emit_process_reserve_upDown_node_active_and_prundt` — mod
11
+ L1321-1322. Filters ``process_reserve_upDown_node`` by nonzero summed
12
+ reservation across the matching ``reserve__upDown__group`` × ``dt``
13
+ cross product, then emits ``prundt = process_reserve_upDown_node_active
14
+ × dt``.
15
+ * :func:`emit_process_reserve_filters_and_reliability` — mod L1655 /
16
+ L1660-1668. Emits the reliability fallback (default 1) and two
17
+ ``> 0`` filter sets (``increase_reserve_ratio`` /
18
+ ``large_failure_ratio``) plus the ``process_large_failure`` projection.
19
+
20
+ Output CSVs (7 total):
21
+
22
+ * ``pdtReserve_upDown_group.csv``
23
+ * ``process_reserve_upDown_node_active.csv``
24
+ * ``prundt.csv``
25
+ * ``p_process_reserve_upDown_node_reliability.csv``
26
+ * ``process_reserve_upDown_node_increase_reserve_ratio.csv``
27
+ * ``process_reserve_upDown_node_large_failure_ratio.csv``
28
+ * ``process_large_failure.csv``
29
+
30
+ Reuse note
31
+ ----------
32
+
33
+ ``flextool.engine_polars._reserve`` consumes these CSVs as RHS / domain
34
+ inputs (``prundt`` for v_reserve, ``pdtReserve_upDown_group_reservation``
35
+ for the balance constraint, etc.) but does not derive them — it is a
36
+ constraint-builder that runs *after* preprocessing. No code is shared
37
+ across the port boundary; this module mirrors the legacy emitter's
38
+ shape verbatim for byte-identical parity.
39
+
40
+ Float values formatted with ``repr(float(v))`` for byte-identical
41
+ parity with the legacy emitter.
42
+ """
43
+ from __future__ import annotations
44
+
45
+ from pathlib import Path
46
+
47
+ import polars as pl
48
+
49
+ from flextool.engine_polars._emit_provider_io import (
50
+ _emit,
51
+ _provider_key,
52
+ )
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # I/O contract: legacy emitters in this family used
57
+ # ``path.open("w") + fh.write(...)`` with plain ``\n`` line terminators
58
+ # (no CRLF), so the default polars ``write_csv`` line ending matches
59
+ # byte-for-byte. Float cells are pre-rendered with ``repr(float(v))``
60
+ # by the ``derive_*`` builders, so the underlying frame is all-``Utf8``
61
+ # and polars does no extra numeric formatting at emission time.
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ def _to_utf8_frame(
66
+ headers: tuple[str, ...],
67
+ rows: list[tuple],
68
+ ) -> pl.DataFrame:
69
+ """Build an all-``Utf8`` polars frame from a header tuple + row list.
70
+
71
+ Each cell is taken verbatim when already a string, otherwise via
72
+ ``str(v)``. All ``derive_*`` builders in this module pre-render
73
+ float cells through ``repr(float(v))`` so byte parity with the
74
+ legacy emit holds.
75
+ """
76
+ cols: dict[str, list[str]] = {h: [] for h in headers}
77
+ for row in rows:
78
+ for h, v in zip(headers, row):
79
+ cols[h].append(v if isinstance(v, str) else str(v))
80
+ return pl.DataFrame(cols, schema={h: pl.Utf8 for h in headers})
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Native-frame readers — same shape as _emit_inflow_scaling / _pdt_lookup
85
+ # helpers. The legacy emitter wrote plain text with ``repr(v)``; these
86
+ # read the in-memory polars frame directly via
87
+ # ``provider.get(_provider_key(path))`` instead of round-tripping through
88
+ # CSV text.
89
+ #
90
+ # Type-fidelity contract — reproduce *exactly* what ``line.split(",")``
91
+ # over a ``DataFrame.write_csv`` serialisation would have yielded:
92
+ #
93
+ # * Key columns (dict-key / structural string positions) were string
94
+ # cells. ``write_csv`` serialises ``null`` → ``""`` and any scalar
95
+ # to its textual form; we coerce each key cell with :func:`_cell_str`
96
+ # and apply the original truthiness guard to the *string* form so a
97
+ # null cell is skipped while a literal ``"0"`` is kept.
98
+ # * Value columns were re-coerced with ``float(...)``. We apply the same
99
+ # coercion to the native cell and widen the legacy ``except
100
+ # ValueError`` to ``except (ValueError, TypeError)`` so a null value
101
+ # cell is skipped (matching the legacy empty-string ``ValueError``).
102
+ #
103
+ # ``provider.get`` returns data rows only (no header), so there is no
104
+ # header row to skip; an empty / missing frame yields the same empty
105
+ # output the legacy loop produced.
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ def _cell_str(value: "object | None") -> str:
110
+ """Reproduce a CSV cell string for a native frame value.
111
+
112
+ ``DataFrame.write_csv`` renders ``null`` as the empty string and every
113
+ other scalar as its textual form; the legacy ``line.split(",")`` then
114
+ read those strings back. Mirror that here so dict keys / structural
115
+ strings stay byte-identical to the legacy CSV round-trip.
116
+ """
117
+ return "" if value is None else str(value)
118
+
119
+
120
+ def _read_singles(path: Path,
121
+ *, provider: "object | None" = None) -> list[str]:
122
+ """First-column reader (data rows only)."""
123
+ df = provider.get(_provider_key(path))
124
+ if df is None:
125
+ return []
126
+ out: list[str] = []
127
+ for row in df.iter_rows():
128
+ if not row:
129
+ continue
130
+ c0 = _cell_str(row[0])
131
+ if c0:
132
+ out.append(c0)
133
+ return out
134
+
135
+
136
+ def _read_pairs(path: Path,
137
+ *, provider: "object | None" = None) -> list[tuple[str, str]]:
138
+ """First-two-column reader (data rows only)."""
139
+ df = provider.get(_provider_key(path))
140
+ if df is None:
141
+ return []
142
+ out: list[tuple[str, str]] = []
143
+ for row in df.iter_rows():
144
+ if len(row) < 2:
145
+ continue
146
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
147
+ if c0 and c1:
148
+ out.append((c0, c1))
149
+ return out
150
+
151
+
152
+ def _read_n_col(path: Path, n: int,
153
+ *, provider: "object | None" = None) -> list[tuple[str, ...]]:
154
+ """First-n-column reader; rows with any empty key skipped."""
155
+ df = provider.get(_provider_key(path))
156
+ if df is None:
157
+ return []
158
+ out: list[tuple[str, ...]] = []
159
+ for row in df.iter_rows():
160
+ if len(row) < n:
161
+ continue
162
+ cells = tuple(_cell_str(row[i]) for i in range(n))
163
+ if all(cells):
164
+ out.append(cells)
165
+ return out
166
+
167
+
168
+ def _read_pairs_to_dict(path: Path, key_col: int,
169
+ *, provider: "object | None" = None,
170
+ ) -> dict[str, list[str]]:
171
+ """Two-col frame → {row[key_col]: [row[other_col], ...]} preserving order."""
172
+ out: dict[str, list[str]] = {}
173
+ df = provider.get(_provider_key(path))
174
+ if df is None:
175
+ return out
176
+ other_col = 1 - key_col
177
+ for row in df.iter_rows():
178
+ if len(row) < 2:
179
+ continue
180
+ c0, c1 = _cell_str(row[0]), _cell_str(row[1])
181
+ if c0 and c1:
182
+ cells = (c0, c1)
183
+ out.setdefault(cells[key_col], []).append(cells[other_col])
184
+ return out
185
+
186
+
187
+ def _read_pbt_reserve(
188
+ path: Path,
189
+ *, provider: "object | None" = None,
190
+ ) -> dict[tuple[str, str, str, str, str, str, str], float]:
191
+ """``pbt_reserve__upDown__group.csv`` →
192
+ {(r, ud, g, param, branch, ts, t): float}. Malformed / non-numeric
193
+ rows silently skipped (matches legacy)."""
194
+ out: dict[tuple[str, str, str, str, str, str, str], float] = {}
195
+ df = provider.get(_provider_key(path))
196
+ if df is None:
197
+ return out
198
+ for row in df.iter_rows():
199
+ if len(row) < 8:
200
+ continue
201
+ c = [_cell_str(row[i]) for i in range(7)]
202
+ if all(c):
203
+ try:
204
+ out[(c[0], c[1], c[2], c[3], c[4], c[5], c[6])] = float(row[7])
205
+ except (ValueError, TypeError):
206
+ continue
207
+ return out
208
+
209
+
210
+ def _read_pt_reserve(
211
+ path: Path,
212
+ *, provider: "object | None" = None,
213
+ ) -> dict[tuple[str, str, str, str, str], float]:
214
+ """``pt_reserve__upDown__group.csv`` →
215
+ {(r, ud, g, param, t): float}."""
216
+ out: dict[tuple[str, str, str, str, str], float] = {}
217
+ df = provider.get(_provider_key(path))
218
+ if df is None:
219
+ return out
220
+ for row in df.iter_rows():
221
+ if len(row) < 6:
222
+ continue
223
+ c = [_cell_str(row[i]) for i in range(5)]
224
+ if all(c):
225
+ try:
226
+ out[(c[0], c[1], c[2], c[3], c[4])] = float(row[5])
227
+ except (ValueError, TypeError):
228
+ continue
229
+ return out
230
+
231
+
232
+ def _read_p_reserve(
233
+ path: Path,
234
+ *, provider: "object | None" = None,
235
+ ) -> dict[tuple[str, str, str, str], float]:
236
+ """``p_reserve__upDown__group.csv`` → {(r, ud, g, param): float}."""
237
+ out: dict[tuple[str, str, str, str], float] = {}
238
+ df = provider.get(_provider_key(path))
239
+ if df is None:
240
+ return out
241
+ for row in df.iter_rows():
242
+ if len(row) < 5:
243
+ continue
244
+ c = [_cell_str(row[i]) for i in range(4)]
245
+ if all(c):
246
+ try:
247
+ out[(c[0], c[1], c[2], c[3])] = float(row[4])
248
+ except (ValueError, TypeError):
249
+ continue
250
+ return out
251
+
252
+
253
+ # Reserve param table fixed across the writers — flextool_base.dat L183
254
+ # defines only the ``reservation`` reserveTimeParam. Default per
255
+ # reserveParam_defaults (mod L493) is 0 for reservation.
256
+ _RESERVE_TIME_PARAMS: tuple[str, ...] = ("reservation",)
257
+ _RESERVATION_DEFAULT: float = 0.0
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # write_pdtReserve_upDown_group (mod L1319)
262
+ # ---------------------------------------------------------------------------
263
+
264
+
265
+ def _resolve_pdtReserve(
266
+ r: str, ud: str, g: str, param: str, d: str, t: str,
267
+ *,
268
+ pbt: dict[tuple[str, str, str, str, str, str, str], float],
269
+ pt: dict[tuple[str, str, str, str, str], float],
270
+ p: dict[tuple[str, str, str, str], float],
271
+ ts_for_d: dict[str, list[str]],
272
+ tb_for_d: dict[str, list[str]],
273
+ pe_for_d: dict[str, list[str]],
274
+ g_stoch: bool,
275
+ ) -> float:
276
+ """4-branch fallback mirroring ``pdtReserve_upDown_group``'s mod body.
277
+
278
+ Branches mirror ``pdtProcess``'s branches 1, 2, 4, 5 (no period axis,
279
+ no def1 — and the scalar branch returns the table's default of 0
280
+ for ``reservation`` when the (r, ud, g) row is missing).
281
+ """
282
+ # Branch 1: stochastic + outer-d's ts/tb.
283
+ if g_stoch:
284
+ ts_list = ts_for_d.get(d, ())
285
+ tb_list = tb_for_d.get(d, ())
286
+ total = 0.0
287
+ hit = False
288
+ for tb in tb_list:
289
+ for ts in ts_list:
290
+ v = pbt.get((r, ud, g, param, tb, ts, t))
291
+ if v is not None:
292
+ total += v
293
+ hit = True
294
+ if hit:
295
+ return total
296
+ # Branch 2: parent period pe of d, tb from solve_branch[pe], ts from
297
+ # period__time_first[d].
298
+ ts_list = ts_for_d.get(d, ())
299
+ pe_list = pe_for_d.get(d, ())
300
+ if pe_list and ts_list:
301
+ total = 0.0
302
+ hit = False
303
+ for pe in pe_list:
304
+ for tb in tb_for_d.get(pe, ()):
305
+ for ts in ts_list:
306
+ v = pbt.get((r, ud, g, param, tb, ts, t))
307
+ if v is not None:
308
+ total += v
309
+ hit = True
310
+ if hit:
311
+ return total
312
+ # Branch 3: time axis.
313
+ v = pt.get((r, ud, g, param, t))
314
+ if v is not None:
315
+ return v
316
+ # Branch 4: scalar (with table default 0 for ``reservation``).
317
+ v = p.get((r, ud, g, param))
318
+ if v is not None:
319
+ return v
320
+ return _RESERVATION_DEFAULT
321
+
322
+
323
+ def derive_pdtReserve_upDown_group(
324
+ input_dir: Path, solve_data_dir: Path,
325
+ *, provider: "object | None" = None,
326
+ ) -> pl.DataFrame:
327
+ """Build the canonical ``pdtReserve_upDown_group`` frame
328
+ ``(reserve, upDown, group, param, period, time, value)``.
329
+
330
+ Value column is pre-rendered with ``repr(float(v))`` for byte
331
+ parity with the legacy emitter.
332
+ """
333
+ pbt = _read_pbt_reserve(
334
+ input_dir / "pbt_reserve__upDown__group.csv", provider=provider,
335
+ )
336
+ pt = _read_pt_reserve(
337
+ input_dir / "pt_reserve__upDown__group.csv", provider=provider,
338
+ )
339
+ p = _read_p_reserve(
340
+ input_dir / "p_reserve__upDown__group.csv", provider=provider,
341
+ )
342
+
343
+ ts_for_d = _read_pairs_to_dict(
344
+ solve_data_dir / "first_timesteps.csv", 0, provider=provider,
345
+ )
346
+ tb_for_d = _read_pairs_to_dict(
347
+ solve_data_dir / "solve_branch__time_branch.csv", 0, provider=provider,
348
+ )
349
+ pe_for_d = _read_pairs_to_dict(
350
+ solve_data_dir / "period__branch.csv", 1, provider=provider,
351
+ )
352
+ groups_stoch = frozenset(
353
+ _read_singles(
354
+ input_dir / "groupIncludeStochastics.csv", provider=provider,
355
+ )
356
+ )
357
+
358
+ rug = _read_n_col(
359
+ solve_data_dir / "reserve__upDown__group.csv", 3, provider=provider,
360
+ )
361
+ dt = _read_pairs(solve_data_dir / "steps_in_use.csv", provider=provider)
362
+
363
+ rows: list[tuple] = []
364
+ for (r, ud, g) in rug:
365
+ g_stoch = g in groups_stoch
366
+ for param in _RESERVE_TIME_PARAMS:
367
+ for (d, t) in dt:
368
+ val = _resolve_pdtReserve(
369
+ r, ud, g, param, d, t,
370
+ pbt=pbt, pt=pt, p=p,
371
+ ts_for_d=ts_for_d,
372
+ tb_for_d=tb_for_d,
373
+ pe_for_d=pe_for_d,
374
+ g_stoch=g_stoch,
375
+ )
376
+ rows.append((r, ud, g, param, d, t, repr(float(val))))
377
+ return _to_utf8_frame(
378
+ ("reserve", "upDown", "group", "param", "period", "time", "value"),
379
+ rows,
380
+ )
381
+
382
+
383
+ def emit_pdtReserve_upDown_group(
384
+ input_dir: Path, solve_data_dir: Path,
385
+ *, provider,
386
+ ) -> None:
387
+ """Emit ``pdtReserve_upDown_group`` to the Provider."""
388
+ _emit(
389
+ provider, "solve_data/pdtReserve_upDown_group.csv",
390
+ derive_pdtReserve_upDown_group(
391
+ input_dir, solve_data_dir, provider=provider,
392
+ ),
393
+ )
394
+
395
+
396
+ # ---------------------------------------------------------------------------
397
+ # write_process_reserve_upDown_node_active_and_prundt (mod L1321-1322)
398
+ # ---------------------------------------------------------------------------
399
+
400
+
401
+ def _compute_process_reserve_active(
402
+ input_dir: Path, solve_data_dir: Path,
403
+ *, provider: "object | None" = None,
404
+ ) -> tuple[list[tuple[str, str, str, str]], list[tuple[str, str]]]:
405
+ """Shared computation for the active/prundt pair — returns the
406
+ active ``(p, r, ud, n)`` list and the ``dt`` pair list."""
407
+ pdt_reserve: dict[tuple[str, str, str, str, str, str], float] = {}
408
+ pdt_path = solve_data_dir / "pdtReserve_upDown_group.csv"
409
+ pdt_df = provider.get(_provider_key(pdt_path))
410
+ if pdt_df is not None:
411
+ for row in pdt_df.iter_rows():
412
+ if len(row) < 7:
413
+ continue
414
+ c = [_cell_str(row[i]) for i in range(6)]
415
+ if all(c):
416
+ try:
417
+ pdt_reserve[(c[0], c[1], c[2], c[3], c[4], c[5])] = float(row[6])
418
+ except (ValueError, TypeError):
419
+ continue
420
+
421
+ rug_by_ru: dict[tuple[str, str], list[str]] = {}
422
+ for r, ud, g in _read_n_col(
423
+ solve_data_dir / "reserve__upDown__group.csv", 3, provider=provider,
424
+ ):
425
+ rug_by_ru.setdefault((r, ud), []).append(g)
426
+
427
+ dt = _read_pairs(solve_data_dir / "steps_in_use.csv", provider=provider)
428
+ prun = _read_n_col(
429
+ input_dir / "process__reserve__upDown__node.csv", 4, provider=provider,
430
+ )
431
+
432
+ active_rows: list[tuple[str, str, str, str]] = []
433
+ for (p, r, ud, n) in prun:
434
+ groups = rug_by_ru.get((r, ud), ())
435
+ total = 0.0
436
+ for g in groups:
437
+ for (d, t) in dt:
438
+ total += pdt_reserve.get(
439
+ (r, ud, g, "reservation", d, t), 0.0,
440
+ )
441
+ if total != 0.0:
442
+ active_rows.append((p, r, ud, n))
443
+ return active_rows, dt
444
+
445
+
446
+ def emit_process_reserve_upDown_node_active_and_prundt(
447
+ input_dir: Path, solve_data_dir: Path,
448
+ *, provider,
449
+ ) -> None:
450
+ """Emit ``process_reserve_upDown_node_active_and_prundt`` to the Provider."""
451
+ active_rows, dt = _compute_process_reserve_active(
452
+ input_dir, solve_data_dir, provider=provider,
453
+ )
454
+ _emit(
455
+ provider,
456
+ "solve_data/process_reserve_upDown_node_active.csv",
457
+ _to_utf8_frame(
458
+ ("process", "reserve", "upDown", "node"), active_rows,
459
+ ),
460
+ )
461
+ prundt_rows: list[tuple[str, str, str, str, str, str]] = []
462
+ for (p, r, ud, n) in active_rows:
463
+ for (d, t) in dt:
464
+ prundt_rows.append((p, r, ud, n, d, t))
465
+ _emit(
466
+ provider,
467
+ "solve_data/prundt.csv",
468
+ _to_utf8_frame(
469
+ ("process", "reserve", "upDown", "node", "period", "time"),
470
+ prundt_rows,
471
+ ),
472
+ )
473
+
474
+
475
+ # ---------------------------------------------------------------------------
476
+ # write_process_reserve_filters_and_reliability (mod L1655, L1660-1668)
477
+ # ---------------------------------------------------------------------------
478
+
479
+
480
+ def _read_p_process_reserve(
481
+ path: Path,
482
+ *, provider: "object | None" = None,
483
+ ) -> dict[tuple[str, str, str, str, str], float]:
484
+ """``p_process__reserve__upDown__node.csv`` →
485
+ {(process, reserve, upDown, node, param): float}."""
486
+ out: dict[tuple[str, str, str, str, str], float] = {}
487
+ df = provider.get(_provider_key(path))
488
+ if df is None:
489
+ return out
490
+ for row in df.iter_rows():
491
+ if len(row) < 6:
492
+ continue
493
+ c = [_cell_str(row[i]) for i in range(5)]
494
+ if all(c):
495
+ try:
496
+ out[(c[0], c[1], c[2], c[3], c[4])] = float(row[5])
497
+ except (ValueError, TypeError):
498
+ continue
499
+ return out
500
+
501
+
502
+ def _compute_reserve_filters(
503
+ input_dir: Path, solve_data_dir: Path,
504
+ *, provider: "object | None" = None,
505
+ ) -> tuple[
506
+ list[tuple[str, str, str, str, str]], # reliability rows (with value)
507
+ list[tuple[str, str, str, str]], # increase_reserve_ratio rows
508
+ list[tuple[str, str, str, str]], # large_failure_ratio rows
509
+ list[tuple[str]], # process_large_failure rows
510
+ ]:
511
+ """Shared scan of the four reserve-filter frames. All value cells
512
+ pre-stringified via ``repr(float(v))``.
513
+ """
514
+ p_prn = _read_p_process_reserve(
515
+ input_dir / "p_process__reserve__upDown__node.csv", provider=provider,
516
+ )
517
+ active = _read_n_col(
518
+ solve_data_dir / "process_reserve_upDown_node_active.csv", 4,
519
+ provider=provider,
520
+ )
521
+
522
+ rel_rows: list[tuple[str, str, str, str, str]] = []
523
+ incr_rows: list[tuple[str, str, str, str]] = []
524
+ lf_rows: list[tuple[str, str, str, str]] = []
525
+ for (p, r, ud, n) in active:
526
+ v = p_prn.get((p, r, ud, n, "reliability"), 1.0)
527
+ if v == 0.0:
528
+ v = 1.0
529
+ rel_rows.append((p, r, ud, n, repr(float(v))))
530
+ if p_prn.get((p, r, ud, n, "increase_reserve_ratio"), 0.0) > 0:
531
+ incr_rows.append((p, r, ud, n))
532
+ if p_prn.get((p, r, ud, n, "large_failure_ratio"), 0.0) > 0:
533
+ lf_rows.append((p, r, ud, n))
534
+
535
+ process_lf = [
536
+ (p,) for p in dict.fromkeys(p for (p, _, _, _) in lf_rows)
537
+ ]
538
+ return rel_rows, incr_rows, lf_rows, process_lf
539
+
540
+
541
+ def emit_process_reserve_filters_and_reliability(
542
+ input_dir: Path, solve_data_dir: Path,
543
+ *, provider,
544
+ ) -> None:
545
+ """Emit ``process_reserve_filters_and_reliability`` to the Provider."""
546
+ rel_rows, incr_rows, lf_rows, process_lf = _compute_reserve_filters(
547
+ input_dir, solve_data_dir, provider=provider,
548
+ )
549
+ _emit(
550
+ provider,
551
+ "solve_data/p_process_reserve_upDown_node_reliability.csv",
552
+ _to_utf8_frame(
553
+ ("process", "reserve", "upDown", "node", "value"), rel_rows,
554
+ ),
555
+ )
556
+ _emit(
557
+ provider,
558
+ "solve_data/process_reserve_upDown_node_increase_reserve_ratio.csv",
559
+ _to_utf8_frame(
560
+ ("process", "reserve", "upDown", "node"), incr_rows,
561
+ ),
562
+ )
563
+ _emit(
564
+ provider,
565
+ "solve_data/process_reserve_upDown_node_large_failure_ratio.csv",
566
+ _to_utf8_frame(
567
+ ("process", "reserve", "upDown", "node"), lf_rows,
568
+ ),
569
+ )
570
+ _emit(
571
+ provider,
572
+ "solve_data/process_large_failure.csv",
573
+ _to_utf8_frame(("process",), process_lf),
574
+ )