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,445 @@
1
+ """DC power flow physics — linearised AC transmission constraints.
2
+
3
+ Mirrors the .mod's ``dc_flow_eq`` family (flextool.mod:3236-3244)::
4
+
5
+ s.t. dc_flow_eq {p in connection_dc_power_flow,
6
+ (p, source, sink) in process_source_toSink,
7
+ (p, 'sink', b_out) in process__side__block,
8
+ (b_out, d, t) in block__period__step
9
+ : source in node_dc_power_flow
10
+ && sink in node_dc_power_flow} :
11
+ v_flow[p, source, sink, d, t] * p_entity_unitsize[p]
12
+ =
13
+ p_connection_susceptance[p] * (v_angle[source, d, t] - v_angle[sink, d, t])
14
+
15
+ In flextool's V1 DC PF scenarios all participating nodes sit on the
16
+ default (hourly) block, so the block-aware filter
17
+ (``(p, 'sink', b_out) in process__side__block`` × ``(b_out, d, t) in
18
+ block__period__step``) reduces to the plain ``(d, t)`` set. Our
19
+ emission follows that V1 simplification: we index dc_flow_eq over
20
+ ``connection_dc_power_flow × process_source_toSink × dt`` filtered to
21
+ ``source, sink in node_dc_power_flow``. Block-aware extension is
22
+ out of scope until a fixture exercises non-default blocks for DC PF
23
+ nodes (none do as of v51).
24
+
25
+ Inputs
26
+ ------
27
+ * ``input/node_dc_power_flow.csv`` single-column ``node``
28
+ * ``input/connection_dc_power_flow.csv`` single-column ``process``
29
+ * ``input/node_reference_angle.csv`` single-column ``node`` (angle pinned to 0)
30
+ * ``input/p_connection_susceptance.csv`` two-column ``process,p_connection_susceptance``
31
+
32
+ Reference-angle policy
33
+ ----------------------
34
+ Reference selection is handled by the
35
+ :mod:`flextool.input_derivation._dc_power_flow` derivation, which
36
+ emits the ``node_reference_angle.csv`` file. Per connected component
37
+ of the DC PF subnetwork, exactly one node carries angle = 0:
38
+
39
+ * If the group has ``reference_node`` set explicitly, that node is used.
40
+ * Otherwise BFS finds connected components (via
41
+ ``connection__node__node`` adjacency) and picks the node with the
42
+ largest ``existing`` capacity in each component.
43
+
44
+ The engine reads the resulting CSV verbatim and pins
45
+ ``v_angle[ref, d, t] = 0`` via the angle Var's per-row tight bound (the
46
+ .mod uses the same trick — ``p_angle_lower = p_angle_upper = 0`` for
47
+ ref nodes). The non-reference angle bounds are ±π (the .mod literal
48
+ ``3.14159265``); polar_high's scalar-bound Var declaration uses ±π as
49
+ the loose bound, then emits an explicit equality ``v_angle[ref] = 0``
50
+ for reference nodes (since polar_high Var bounds are scalar).
51
+ """
52
+ from __future__ import annotations
53
+
54
+ from pathlib import Path
55
+ from typing import TYPE_CHECKING
56
+
57
+ import polars as pl
58
+
59
+ from polar_high import Param, Where
60
+
61
+ from ._axis_enums import cast_dim
62
+
63
+
64
+ if TYPE_CHECKING:
65
+ from polar_high.engine import Var
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Feature detection
70
+
71
+ def has_feature(d) -> bool:
72
+ """True iff DC power flow data is populated and non-empty.
73
+
74
+ Activation requires both at least one node in ``node_dc_power_flow``
75
+ AND at least one connection in ``connection_dc_power_flow``. A
76
+ fixture with header-only CSVs (most of the v51 fixtures) returns
77
+ False.
78
+ """
79
+ nd = getattr(d, "node_dc_power_flow", None)
80
+ cd = getattr(d, "connection_dc_power_flow", None)
81
+ if nd is None or cd is None:
82
+ return False
83
+ return nd.height > 0 and cd.height > 0
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Data loading
88
+
89
+ def load_data(
90
+ inp_dir: str | Path,
91
+ *,
92
+ provider: "object | None" = None,
93
+ ) -> dict:
94
+ """Read DC power flow frames via the Provider (disk-fallback for
95
+ off-cascade test harnesses).
96
+
97
+ Returns a dict with keys matching the FlexData field names:
98
+
99
+ node_dc_power_flow pl.DataFrame | None # cols: (n,)
100
+ connection_dc_power_flow pl.DataFrame | None # cols: (p,)
101
+ node_reference_angle pl.DataFrame | None # cols: (n,)
102
+ p_connection_susceptance Param | None # dims: (p,)
103
+
104
+ All values are ``None`` (or empty) when the feature is inactive
105
+ (header-only frames, which is what the
106
+ :func:`flextool.input_derivation._dc_power_flow.derive_dc_power_flow`
107
+ pass writes for non-DC-PF scenarios).
108
+
109
+ Step 2.5-F Phase B
110
+ ------------------
111
+
112
+ The four frames are now produced by
113
+ :mod:`flextool.input_derivation._dc_power_flow` and placed on the
114
+ cascade-input :class:`FlexDataProvider` under
115
+ ``input/node_dc_power_flow``, ``input/connection_dc_power_flow``,
116
+ ``input/node_reference_angle``, ``input/p_connection_susceptance``.
117
+ In-cascade the Provider is the authoritative source; the disk-read
118
+ arm is preserved exclusively for off-cascade fixture loaders that
119
+ seed inputs to ``input/`` and invoke the loader without a Provider.
120
+ """
121
+ inp = Path(inp_dir)
122
+
123
+ blank = dict(
124
+ node_dc_power_flow = None,
125
+ connection_dc_power_flow = None,
126
+ node_reference_angle = None,
127
+ p_connection_susceptance = None,
128
+ process_source_toSink_dc = None,
129
+ )
130
+
131
+ def _frame_for(key: str, path: Path) -> "pl.DataFrame | None":
132
+ if provider is None or not provider.has(key):
133
+ return None
134
+ df = provider.get(key)
135
+ if df is None or df.height == 0:
136
+ return None
137
+ return df
138
+
139
+ def _project_single(df: "pl.DataFrame | None", col_in: str,
140
+ col_out: str) -> "pl.DataFrame | None":
141
+ if df is None:
142
+ return None
143
+ if col_in in df.columns and col_in != col_out:
144
+ df = df.rename({col_in: col_out})
145
+ return df.select(col_out)
146
+
147
+ nd_raw = _frame_for("input/node_dc_power_flow",
148
+ inp / "node_dc_power_flow.csv")
149
+ cd_raw = _frame_for("input/connection_dc_power_flow",
150
+ inp / "connection_dc_power_flow.csv")
151
+ rd_raw = _frame_for("input/node_reference_angle",
152
+ inp / "node_reference_angle.csv")
153
+
154
+ nd = _project_single(nd_raw, "node", "n")
155
+ cd = _project_single(cd_raw, "process", "p")
156
+ rd = _project_single(rd_raw, "node", "n")
157
+
158
+ pcs_raw = _frame_for("input/p_connection_susceptance",
159
+ inp / "p_connection_susceptance.csv")
160
+ pcs_param = None
161
+ if pcs_raw is not None:
162
+ df = pcs_raw.rename(
163
+ {c: r for c, r in [
164
+ ("process", "p"),
165
+ ("p_connection_susceptance", "value"),
166
+ ] if c in pcs_raw.columns}
167
+ ).select("p", "value")
168
+ # The Provider may carry the value as Utf8 (mirroring the legacy
169
+ # CSV emission); cast to Float64 so Param's downstream consumers
170
+ # see numeric values regardless of source.
171
+ if df["value"].dtype == pl.Utf8:
172
+ df = df.with_columns(pl.col("value").cast(pl.Float64))
173
+ pcs_param = Param(("p",), df)
174
+
175
+ if nd is None and cd is None and rd is None and pcs_param is None:
176
+ return blank
177
+
178
+ # Forward-direction (p, source, sink) for DC PF arcs. The cascade's
179
+ # ``process_source_sink`` doubles up 2-way connections (both arc
180
+ # orientations), but the .mod's dc_flow_eq is indexed over
181
+ # ``process_source_toSink`` which is one direction per arc. Read it
182
+ # from the Provider's ``solve_data/process_source_toSink`` key (the
183
+ # cascade writes it via _emit_calc_params.derive_process_source_toSink).
184
+ sst = None
185
+ if provider is not None and provider.has("solve_data/process_source_toSink"):
186
+ sst = provider.get("solve_data/process_source_toSink")
187
+ if sst is not None and sst.height > 0:
188
+ sst = sst.rename({
189
+ c: r for c, r in [
190
+ ("process", "p"),
191
+ ("source", "source"),
192
+ ("sink", "sink"),
193
+ ] if c in sst.columns
194
+ }).select("p", "source", "sink")
195
+ else:
196
+ sst = None
197
+
198
+ return dict(
199
+ node_dc_power_flow = nd,
200
+ connection_dc_power_flow = cd,
201
+ node_reference_angle = rd,
202
+ p_connection_susceptance = pcs_param,
203
+ process_source_toSink_dc = sst,
204
+ )
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Variable + constraint emission
209
+
210
+ # Same float literal flextool's preprocessing (preprocessing/dc_angle_bounds.py)
211
+ # uses for the non-reference upper / lower angle bound. An 8-digit
212
+ # truncation of π — preserved here for parity with flextool's MPS bit
213
+ # pattern. See flextool.mod:1680-1681.
214
+ _PI_LITERAL = 3.14159265
215
+
216
+
217
+ def dc_arcs_frame(d) -> "pl.DataFrame | None":
218
+ """Return the DC-power-flow arc set ``(p, source, sink)``.
219
+
220
+ A DC arc is a ``connection_dc_power_flow`` connection whose both
221
+ endpoints sit in ``node_dc_power_flow``, in the one-direction-per-arc
222
+ orientation (``process_source_toSink_dc`` when available, else the
223
+ dual-direction ``process_source_sink`` fallback for off-cascade
224
+ harnesses). Returns ``None`` when no such arcs exist.
225
+
226
+ Factored out of :func:`add_variables` so ``model.py`` can compute the
227
+ DC subset that ``dc_flow_eq`` ranges over without owning the
228
+ ``v_flow_back`` Var (which is now shared across ALL
229
+ ``method_2way_1var_off`` arcs, DC + non-DC).
230
+ """
231
+ if (d.process_source_sink is None
232
+ or d.connection_dc_power_flow is None
233
+ or d.connection_dc_power_flow.height == 0):
234
+ return None
235
+ # source/sink carry the entity-union (``e``) Enum; ``n`` of
236
+ # node_dc_power_flow carries the node-only Enum. Lift the latter
237
+ # to ``e`` so ``is_in`` matches dtypes.
238
+ _dc_n_e = d.node_dc_power_flow.with_columns(
239
+ cast_dim(pl.col("n"), None, "e"))["n"]
240
+ arcs_src = (getattr(d, "process_source_toSink_dc", None)
241
+ if getattr(d, "process_source_toSink_dc", None) is not None
242
+ else d.process_source_sink)
243
+ if arcs_src is not d.process_source_sink:
244
+ for col in ("p", "source", "sink"):
245
+ target_dtype = d.process_source_sink.schema[col]
246
+ if arcs_src.schema[col] != target_dtype:
247
+ arcs_src = arcs_src.with_columns(
248
+ pl.col(col).cast(target_dtype, strict=False)
249
+ )
250
+ dc_arcs = (arcs_src
251
+ .join(d.connection_dc_power_flow, on="p", how="inner")
252
+ .filter(pl.col("source").is_in(_dc_n_e))
253
+ .filter(pl.col("sink").is_in(_dc_n_e)))
254
+ return dc_arcs if dc_arcs.height > 0 else None
255
+
256
+
257
+ def add_variables(m, d, *, v_flow_back=None) -> "dict[str, Var]":
258
+ """Declare ``v_angle[n, d, t]`` and stash the DC arc frame.
259
+
260
+ ``v_angle`` is indexed by (n, d, t) where n ∈ ``node_dc_power_flow``.
261
+ Bounds are set to the loose ±π for non-reference nodes; reference-angle
262
+ pin to 0 is enforced by ``dc_reference_angle_eq`` below (polar_high
263
+ Vars have scalar bounds, so we can't pin per-row in the Var declaration).
264
+
265
+ The reverse-flow auxiliary ``v_flow_back[p, source, sink, d, t]`` is no
266
+ longer created here — ``model.py`` declares it ONCE over the union of
267
+ all ``method_2way_1var_off`` arcs (DC + non-DC) so the single-signed
268
+ flow ``v_flow ∈ [-cap, +cap]`` can run sink→source on any such arc.
269
+ The shared Var is passed in via *v_flow_back* and stashed in the
270
+ returned dict so :func:`add_constraints` can splice it into
271
+ ``dc_flow_eq`` as ``v_flow - v_flow_back``. The nodeBalance injection
272
+ and the capacity cap (``maxFlow_back``) for the back auxiliary are
273
+ likewise owned by ``model.py`` over the full set.
274
+ """
275
+ if not has_feature(d):
276
+ return {}
277
+ if d.dt is None or d.dt.height == 0:
278
+ return {}
279
+
280
+ # v_angle's index is the cross of node_dc_power_flow × dt.
281
+ angle_idx = d.node_dc_power_flow.join(d.dt, how="cross").select("n", "d", "t")
282
+ if angle_idx.height == 0:
283
+ return {}
284
+
285
+ v_angle = m.add_var(
286
+ "v_angle", ("n", "d", "t"), angle_idx,
287
+ lower=-_PI_LITERAL, upper=_PI_LITERAL,
288
+ )
289
+
290
+ out: dict = {"v_angle": v_angle}
291
+
292
+ dc_arcs = dc_arcs_frame(d)
293
+ if dc_arcs is not None:
294
+ out["dc_arcs"] = dc_arcs
295
+ if v_flow_back is not None:
296
+ out["v_flow_back"] = v_flow_back
297
+
298
+ return out
299
+
300
+
301
+ def add_constraints(m, d, vars: dict, *,
302
+ v_flow=None, p_unitsize=None,
303
+ p_flow_upper_existing=None) -> None:
304
+ """Emit the dc_flow_eq + reference-angle pin.
305
+
306
+ ``v_flow``: the model's ``v_flow[p, source, sink, d, t]`` Var. Required
307
+ when ``connection_dc_power_flow`` is non-empty — without flow the
308
+ angle-only LP would be vacuous.
309
+
310
+ ``p_unitsize``: the ``p_entity_unitsize`` Param indexed by (p,).
311
+ Required for the same reason.
312
+
313
+ ``p_flow_upper_existing``: accepted for backward-compatibility but no
314
+ longer consumed here — the back-flow capacity cap is emitted by
315
+ ``model.py`` as ``maxFlow_back`` over the full ``method_2way_1var_off``
316
+ arc set (a superset of the DC arcs), so capping again here would be
317
+ redundant.
318
+ """
319
+ if not has_feature(d):
320
+ return
321
+ v_angle = vars.get("v_angle")
322
+ if v_angle is None:
323
+ return
324
+
325
+ # ── 1. Reference-angle pin ───────────────────────────────────────────
326
+ # v_angle[ref, d, t] == 0 for ref ∈ node_reference_angle.
327
+ # The .mod preprocesses this as a tight Var bound (p_angle_lower =
328
+ # p_angle_upper = 0 on those rows). polar_high Var bounds are scalar,
329
+ # so emit an explicit equality constraint instead — same algebra.
330
+ if (d.node_reference_angle is not None
331
+ and d.node_reference_angle.height > 0):
332
+ ref_idx = (d.node_reference_angle
333
+ .join(d.dt, how="cross")
334
+ .select("n", "d", "t"))
335
+ if ref_idx.height > 0:
336
+ m.add_cstr(
337
+ "dc_reference_angle_eq",
338
+ over = ref_idx,
339
+ sense = "==",
340
+ lhs_terms = {"angle": Where(v_angle, ref_idx)},
341
+ rhs_terms = {},
342
+ )
343
+
344
+ # ── 2. dc_flow_eq ────────────────────────────────────────────────────
345
+ # (v_flow - v_flow_back) * unitsize[p]
346
+ # == susceptance[p] * (v_angle[source, d, t] - v_angle[sink, d, t])
347
+ # Indexed over (p, source, sink, d, t) where p ∈
348
+ # connection_dc_power_flow AND source, sink ∈ node_dc_power_flow.
349
+ if v_flow is None or p_unitsize is None or d.p_connection_susceptance is None:
350
+ return
351
+ if d.process_source_sink is None:
352
+ return
353
+
354
+ dc_arcs = vars.get("dc_arcs")
355
+ if dc_arcs is None or dc_arcs.height == 0:
356
+ return
357
+
358
+ over = dc_arcs.join(d.dt, how="cross").select("p", "source", "sink", "d", "t")
359
+
360
+ # LHS: (v_flow - v_flow_back) * unitsize. v_flow_back is the
361
+ # non-negative reverse-direction auxiliary (see add_variables). In
362
+ # fixtures with no negative-flow demand the LP keeps v_flow_back at
363
+ # zero and the term reduces to v_flow * unitsize.
364
+ v_flow_back = vars.get("v_flow_back")
365
+ flow_signed = Where(v_flow, dc_arcs)
366
+ if v_flow_back is not None:
367
+ flow_signed = flow_signed - v_flow_back
368
+ lhs_flow = flow_signed * p_unitsize
369
+
370
+ # RHS: susceptance[p] * (v_angle[source, d, t] - v_angle[sink, d, t]).
371
+ # v_angle is indexed by (n, d, t). We need it twice — once aliased as
372
+ # ``source`` and once aliased as ``sink`` — so the join with the per-arc
373
+ # ``over`` frame matches the right column on each side. Build virtual
374
+ # Vars sharing v_angle's column ids but with renamed dim columns.
375
+ from polar_high.engine import Var
376
+
377
+ v_angle_src = Var(
378
+ name=v_angle.name + "__as_source",
379
+ dims=("source", "d", "t"),
380
+ frame=v_angle.frame.rename({"n": "source"}),
381
+ lower=v_angle.lower, upper=v_angle.upper,
382
+ )
383
+ v_angle_snk = Var(
384
+ name=v_angle.name + "__as_sink",
385
+ dims=("sink", "d", "t"),
386
+ frame=v_angle.frame.rename({"n": "sink"}),
387
+ lower=v_angle.lower, upper=v_angle.upper,
388
+ )
389
+ susc = d.p_connection_susceptance # Param over (p,)
390
+ rhs_angle_diff = (Where(v_angle_src, dc_arcs.select("p", "source"))
391
+ - Where(v_angle_snk, dc_arcs.select("p", "sink"))) * susc
392
+
393
+ m.add_cstr(
394
+ "dc_flow_eq",
395
+ over = over,
396
+ sense = "==",
397
+ lhs_terms = {"flow": lhs_flow},
398
+ rhs_terms = {"angle_diff": rhs_angle_diff},
399
+ )
400
+
401
+
402
+ def nodeBalance_back_flow_terms(arcs, v_flow_back, p_unitsize,
403
+ p_step_duration) -> dict:
404
+ """Return the v_flow_back contribution to nodeBalance, keyed by name.
405
+
406
+ The signed flow on a ``method_2way_1var_off`` arc is
407
+ ``v_flow - v_flow_back``. ``v_flow`` is already plumbed into
408
+ ``model.py``'s nb_terms via ``flow_to_n`` / source-side
409
+ ``flow_from_nodeBalance_*`` sets. The ``-v_flow_back`` half mirrors
410
+ those terms with reversed signs:
411
+
412
+ * sink-side gets ``-v_flow_back × unitsize`` (back flow LEAVES sink)
413
+ * source-side gets ``+v_flow_back × unitsize`` (back flow ARRIVES at source)
414
+
415
+ *arcs* is the ``(p, source, sink)`` frame the back auxiliary spans
416
+ (the full ``method_2way_1var_off`` set — DC + non-DC). Returns ``{}``
417
+ when no back flow is active.
418
+ """
419
+ if v_flow_back is None or arcs is None or arcs.height == 0:
420
+ return {}
421
+
422
+ from polar_high import Sum
423
+
424
+ # Sink side: back flow leaves the sink — subtract from sink balance.
425
+ # Build ``flow_from_n_back`` = (p, source, sink, n=sink): same shape
426
+ # as flow_to_n but flips the sign in nodeBalance. We use
427
+ # `Where(... )` with the frame having an explicit ``n`` column to
428
+ # collapse the (p, source, sink) dims into (n, d, t) via Sum.
429
+ sink_as_n = arcs.with_columns(n=pl.col("sink")).select(
430
+ "p", "source", "sink", "n")
431
+ src_as_n = arcs.with_columns(n=pl.col("source")).select(
432
+ "p", "source", "sink", "n")
433
+
434
+ return {
435
+ # back flow at sink: -v_flow_back × unitsize × step_duration
436
+ "back_at_sink": -Sum(
437
+ Where(v_flow_back * p_unitsize, sink_as_n) * p_step_duration,
438
+ over=("p", "source", "sink"),
439
+ ),
440
+ # back flow at source: +v_flow_back × unitsize × step_duration
441
+ "back_at_source": Sum(
442
+ Where(v_flow_back * p_unitsize, src_as_n) * p_step_duration,
443
+ over=("p", "source", "sink"),
444
+ ),
445
+ }