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,451 @@
1
+ """Parse MATPOWER .m case files and convert to FlexTool Spine DB.
2
+
3
+ This module provides:
4
+ - Dataclasses for MATPOWER bus, generator, branch and case data
5
+ - ``read_matpower(filepath)`` — parse a .m file into a ``MatpowerCase``
6
+ - ``create_flextool_db_from_matpower(case, db_path)`` — build a ready-to-solve
7
+ FlexTool Spine DB from parsed MATPOWER data
8
+
9
+ The converter creates a DC-OPF-ready FlexTool database with:
10
+ - One commodity node per generator (fuel cost = linear cost coefficient)
11
+ - One unit per generator (constant_efficiency, efficiency=1.0)
12
+ - One node per bus (demand as negative inflow)
13
+ - One connection per branch (with reactance for DC power flow)
14
+ - A single AC network group with dc_power_flow_with_angles
15
+ - A minimal single-timestep timeline for dispatch
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import re
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+
24
+ from spinedb_api import Array, DatabaseMapping, Map, import_data
25
+
26
+ from flextool.update_flextool import FLEXTOOL_DB_VERSION
27
+ from flextool.update_flextool.db_migration import migrate_database
28
+ from flextool.update_flextool.initialize_database import initialize_database
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # MATPOWER data structures
35
+ # ---------------------------------------------------------------------------
36
+
37
+ @dataclass
38
+ class MatpowerBus:
39
+ """Single bus from mpc.bus matrix."""
40
+ bus_id: int
41
+ bus_type: int # 1=PQ, 2=PV, 3=ref (slack)
42
+ pd: float # real power demand (MW)
43
+ qd: float # reactive power demand (MVAr) — unused for DC
44
+
45
+
46
+ @dataclass
47
+ class MatpowerGen:
48
+ """Single generator from mpc.gen + mpc.gencost matrices."""
49
+ bus: int
50
+ pmax: float # MW
51
+ pmin: float # MW
52
+ status: int # 1=online, 0=offline
53
+ cost_coeffs: list[float] = field(default_factory=list)
54
+ # Polynomial cost coefficients [c2, c1, c0] where cost = c2*P^2 + c1*P + c0
55
+
56
+
57
+ @dataclass
58
+ class MatpowerBranch:
59
+ """Single branch from mpc.branch matrix."""
60
+ fbus: int
61
+ tbus: int
62
+ r: float # resistance (p.u.)
63
+ x: float # reactance (p.u.)
64
+ rate_a: float # thermal limit (MVA), 0 = unlimited
65
+ ratio: float # transformer tap ratio (0 = line, nonzero = transformer)
66
+ status: int # 1=online
67
+
68
+
69
+ @dataclass
70
+ class MatpowerCase:
71
+ """Complete parsed MATPOWER case."""
72
+ name: str
73
+ base_mva: float
74
+ buses: list[MatpowerBus]
75
+ generators: list[MatpowerGen]
76
+ branches: list[MatpowerBranch]
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # MATPOWER parser
81
+ # ---------------------------------------------------------------------------
82
+
83
+ def _strip_comments(text: str) -> str:
84
+ """Remove MATLAB line comments (% ...) and inline comments."""
85
+ lines = []
86
+ for line in text.splitlines():
87
+ # Remove everything after the first % that's not inside a string
88
+ idx = line.find('%')
89
+ if idx >= 0:
90
+ line = line[:idx]
91
+ lines.append(line)
92
+ return '\n'.join(lines)
93
+
94
+
95
+ def _extract_matrix(text: str, name: str) -> list[list[float]]:
96
+ """Extract a matrix block ``mpc.<name> = [ ... ];`` and parse rows.
97
+
98
+ Each row is semicolon-terminated within the brackets.
99
+ """
100
+ # Match mpc.<name> = [ ... ];
101
+ pattern = rf'mpc\.{name}\s*=\s*\[(.*?)\];'
102
+ match = re.search(pattern, text, re.DOTALL)
103
+ if not match:
104
+ return []
105
+ block = match.group(1)
106
+ rows: list[list[float]] = []
107
+ for row_str in block.split(';'):
108
+ row_str = row_str.strip()
109
+ if not row_str:
110
+ continue
111
+ values = row_str.split()
112
+ rows.append([float(v) for v in values])
113
+ return rows
114
+
115
+
116
+ def _extract_scalar(text: str, name: str) -> float | None:
117
+ """Extract ``mpc.<name> = <value>;``."""
118
+ pattern = rf'mpc\.{name}\s*=\s*([\d.eE+-]+)\s*;'
119
+ match = re.search(pattern, text)
120
+ if match:
121
+ return float(match.group(1))
122
+ return None
123
+
124
+
125
+ def _extract_function_name(text: str) -> str:
126
+ """Extract the function name from ``function mpc = <name>``."""
127
+ match = re.search(r'function\s+mpc\s*=\s*(\w+)', text)
128
+ if match:
129
+ return match.group(1)
130
+ return "unknown_case"
131
+
132
+
133
+ def read_matpower(filepath: str | Path) -> MatpowerCase:
134
+ """Parse a MATPOWER .m file and return a ``MatpowerCase``.
135
+
136
+ Parameters
137
+ ----------
138
+ filepath : str or Path
139
+ Path to the .m file.
140
+
141
+ Returns
142
+ -------
143
+ MatpowerCase
144
+ Parsed case with buses, generators, branches and cost data.
145
+ """
146
+ filepath = Path(filepath)
147
+ raw_text = filepath.read_text(encoding='utf-8')
148
+ text = _strip_comments(raw_text)
149
+
150
+ case_name = _extract_function_name(text)
151
+ base_mva = _extract_scalar(text, 'baseMVA')
152
+ if base_mva is None:
153
+ base_mva = 100.0
154
+
155
+ # Parse bus data
156
+ # Columns: bus_i, type, Pd, Qd, Gs, Bs, area, Vm, Va, baseKV, zone, Vmax, Vmin
157
+ bus_rows = _extract_matrix(text, 'bus')
158
+ buses = [
159
+ MatpowerBus(
160
+ bus_id=int(row[0]),
161
+ bus_type=int(row[1]),
162
+ pd=row[2],
163
+ qd=row[3],
164
+ )
165
+ for row in bus_rows
166
+ ]
167
+
168
+ # Parse generator data
169
+ # Columns: bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin
170
+ gen_rows = _extract_matrix(text, 'gen')
171
+ generators = [
172
+ MatpowerGen(
173
+ bus=int(row[0]),
174
+ pmax=row[8], # Pmax is column 9 (0-indexed: 8)
175
+ pmin=row[9], # Pmin is column 10 (0-indexed: 9)
176
+ status=int(row[7]),
177
+ )
178
+ for row in gen_rows
179
+ ]
180
+
181
+ # Parse generator cost data
182
+ # Format type 2 (polynomial): type, startup, shutdown, n, c(n-1), ..., c0
183
+ gencost_rows = _extract_matrix(text, 'gencost')
184
+ for i, row in enumerate(gencost_rows):
185
+ if i < len(generators):
186
+ cost_type = int(row[0])
187
+ if cost_type == 2:
188
+ n = int(row[3])
189
+ coeffs = row[4:4 + n] # c(n-1), c(n-2), ..., c0
190
+ generators[i].cost_coeffs = coeffs
191
+ else:
192
+ logger.warning("Generator %d has unsupported cost type %d", i, cost_type)
193
+
194
+ # Parse branch data
195
+ # Columns: fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax
196
+ branch_rows = _extract_matrix(text, 'branch')
197
+ branches = [
198
+ MatpowerBranch(
199
+ fbus=int(row[0]),
200
+ tbus=int(row[1]),
201
+ r=row[2],
202
+ x=row[3],
203
+ rate_a=row[5], # rateA is column 6 (0-indexed: 5)
204
+ ratio=row[8], # ratio is column 9 (0-indexed: 8)
205
+ status=int(row[10]),
206
+ )
207
+ for row in branch_rows
208
+ ]
209
+
210
+ return MatpowerCase(
211
+ name=case_name,
212
+ base_mva=base_mva,
213
+ buses=buses,
214
+ generators=generators,
215
+ branches=branches,
216
+ )
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # FlexTool DB creation from MATPOWER data
221
+ # ---------------------------------------------------------------------------
222
+
223
+ def create_flextool_db_from_matpower(
224
+ case: MatpowerCase,
225
+ db_path: str,
226
+ *,
227
+ scenario_name: str = "dc_opf_test",
228
+ alternative_name: str = "base",
229
+ template_json: str | None = None,
230
+ ) -> str:
231
+ """Create a FlexTool Spine DB from parsed MATPOWER data.
232
+
233
+ The DB is initialized from the FlexTool master template and then
234
+ populated with entities/parameters for a single-timestep DC-OPF
235
+ dispatch problem.
236
+
237
+ Parameters
238
+ ----------
239
+ case : MatpowerCase
240
+ Parsed MATPOWER case.
241
+ db_path : str
242
+ File path for the output .sqlite DB.
243
+ scenario_name : str
244
+ Name for the scenario (default ``dc_opf_test``).
245
+ alternative_name : str
246
+ Name for the alternative (default ``base``).
247
+ template_json : str or None
248
+ Path to the FlexTool template JSON. If None, uses the default
249
+ ``schemas/spinedb_schema.json`` relative to the project root.
250
+
251
+ Returns
252
+ -------
253
+ str
254
+ The ``sqlite:///`` URL for the created database.
255
+ """
256
+ if template_json is None:
257
+ from flextool._resources import package_data_path
258
+ template_json = str(package_data_path("schemas/spinedb_schema.json"))
259
+
260
+ # Step 1: Initialize from template and migrate to latest schema
261
+ initialize_database(template_json, db_path)
262
+ migrate_database(db_path)
263
+
264
+ url = f"sqlite:///{db_path}"
265
+
266
+ # Step 2: Add MATPOWER data
267
+ with DatabaseMapping(url) as db:
268
+ _add_matpower_data(db, case, alternative_name)
269
+ db.commit_session("Add MATPOWER case data")
270
+
271
+ # Step 3: Add scenario
272
+ with DatabaseMapping(url) as db:
273
+ # The template already creates a 'base' alternative.
274
+ # Add scenario and link it.
275
+ _count, errors = import_data(
276
+ db,
277
+ scenarios=[(scenario_name, True)],
278
+ scenario_alternatives=[(scenario_name, alternative_name)],
279
+ )
280
+ if errors:
281
+ logger.warning("Scenario import errors: %s", errors)
282
+ db.commit_session("Add scenario")
283
+
284
+ return url
285
+
286
+
287
+ def _add_matpower_data(
288
+ db: DatabaseMapping,
289
+ case: MatpowerCase,
290
+ alternative: str,
291
+ ) -> None:
292
+ """Populate a FlexTool DB with MATPOWER case entities and parameters."""
293
+ entities: list[tuple] = []
294
+ entity_alternatives: list[tuple] = []
295
+ parameter_values: list[tuple] = []
296
+
297
+ def add_entity(class_name: str, name: str | tuple) -> None:
298
+ """Add an entity and its entity_alternative in one call."""
299
+ entities.append((class_name, name))
300
+ entity_alternatives.append((class_name, name, alternative, True))
301
+
302
+ # Find the reference (slack) bus
303
+ ref_bus_id: int | None = None
304
+ for bus in case.buses:
305
+ if bus.bus_type == 3:
306
+ ref_bus_id = bus.bus_id
307
+ break
308
+ if ref_bus_id is None:
309
+ # Fallback: use bus 1
310
+ ref_bus_id = case.buses[0].bus_id if case.buses else 1
311
+
312
+ # --- Bus nodes ---
313
+ for bus in case.buses:
314
+ node_name = f"bus_{bus.bus_id}"
315
+ add_entity("node", node_name)
316
+
317
+ # Enable node balance constraint
318
+ parameter_values.append(("node", node_name, "node_type", "balance", alternative))
319
+
320
+ if bus.pd != 0.0:
321
+ # Negative inflow = demand in FlexTool
322
+ parameter_values.append(("node", node_name, "inflow", -bus.pd, alternative))
323
+
324
+ # Large penalties to avoid slack usage
325
+ parameter_values.append(("node", node_name, "penalty_up", 100000.0, alternative))
326
+ parameter_values.append(("node", node_name, "penalty_down", 100000.0, alternative))
327
+
328
+ # --- Generators (only those with Pmax > 0 and status=1) ---
329
+ gen_index = 0
330
+ for gen in case.generators:
331
+ gen_index += 1
332
+ if gen.pmax <= 0 or gen.status == 0:
333
+ continue
334
+
335
+ gen_name = f"gen_{gen_index}"
336
+ commodity_node_name = f"commodity_gen{gen_index}"
337
+ commodity_name = f"fuel_gen{gen_index}"
338
+ bus_node_name = f"bus_{gen.bus}"
339
+
340
+ # Linear cost coefficient (c1) — $/MWh
341
+ c1 = 0.0
342
+ if len(gen.cost_coeffs) >= 2:
343
+ # For n=3: coeffs = [c2, c1, c0]
344
+ c1 = gen.cost_coeffs[-2] # c1 is the second-to-last
345
+
346
+ # Commodity and commodity node. The node must declare
347
+ # ``node_type = "commodity"`` — otherwise FlexTool treats it as a
348
+ # default ``none`` balance node with no source of inflow, and the
349
+ # unit's input flow ends up served by ``vq_state_up`` slack at
350
+ # the bus's ``penalty_up``. See LATENT BUG B2.
351
+ add_entity("commodity", commodity_name)
352
+ add_entity("node", commodity_node_name)
353
+ add_entity("commodity__node", (commodity_name, commodity_node_name))
354
+
355
+ parameter_values.append((
356
+ "node", commodity_node_name, "node_type", "commodity", alternative,
357
+ ))
358
+
359
+ # Set commodity price = marginal cost
360
+ parameter_values.append(("commodity", commodity_name, "price", c1, alternative))
361
+
362
+ # Unit
363
+ add_entity("unit", gen_name)
364
+ add_entity("unit__inputNode", (gen_name, commodity_node_name))
365
+ add_entity("unit__outputNode", (gen_name, bus_node_name))
366
+
367
+ # Unit parameters
368
+ parameter_values.append(("unit", gen_name, "existing", gen.pmax, alternative))
369
+ parameter_values.append(("unit", gen_name, "conversion_method", "constant_efficiency", alternative))
370
+ parameter_values.append(("unit", gen_name, "efficiency", 1.0, alternative))
371
+
372
+ # --- Branches / connections ---
373
+ for branch in case.branches:
374
+ if branch.status != 1:
375
+ continue
376
+ if branch.x == 0.0:
377
+ # Zero reactance means infinite susceptance — skip
378
+ logger.warning("Skipping branch %d-%d with zero reactance", branch.fbus, branch.tbus)
379
+ continue
380
+
381
+ conn_name = f"line_{branch.fbus}_{branch.tbus}"
382
+ from_node = f"bus_{branch.fbus}"
383
+ to_node = f"bus_{branch.tbus}"
384
+
385
+ add_entity("connection", conn_name)
386
+ add_entity("connection__node__node", (conn_name, from_node, to_node))
387
+
388
+ parameter_values.append(("connection", conn_name, "existing", branch.rate_a, alternative))
389
+ parameter_values.append(("connection", conn_name, "reactance", branch.x, alternative))
390
+ parameter_values.append(("connection", conn_name, "transfer_method", "regular", alternative))
391
+
392
+ # --- DC power flow group ---
393
+ group_name = "ac_network"
394
+ add_entity("group", group_name)
395
+
396
+ for bus in case.buses:
397
+ add_entity("group__node", (group_name, f"bus_{bus.bus_id}"))
398
+
399
+ parameter_values.append(("group", group_name, "transfer_method", "dc_power_flow_with_angles", alternative))
400
+ parameter_values.append(("group", group_name, "base_MVA", case.base_mva, alternative))
401
+ parameter_values.append(("group", group_name, "reference_node", f"bus_{ref_bus_id}", alternative))
402
+
403
+ # --- Timeline / timeset / solve / model ---
404
+ timeline_name = "tl_dispatch"
405
+ timeset_name = "ts_dispatch"
406
+ solve_name = "dispatch"
407
+ model_name = "flexTool"
408
+ period_name = "p1"
409
+
410
+ # Timeline entity with a single 1-hour timestep
411
+ add_entity("timeline", timeline_name)
412
+ timestep_duration_map = Map(["t0001"], [1.0])
413
+ parameter_values.append(("timeline", timeline_name, "timestep_duration", timestep_duration_map, alternative))
414
+
415
+ # Timeset entity linking to the timeline
416
+ add_entity("timeset", timeset_name)
417
+ parameter_values.append(("timeset", timeset_name, "timeline", timeline_name, alternative))
418
+ # timeset_duration: map from first timestep to count of steps
419
+ timeset_duration_map = Map(["t0001"], [1.0])
420
+ parameter_values.append(("timeset", timeset_name, "timeset_duration", timeset_duration_map, alternative))
421
+
422
+ # Solve entity
423
+ add_entity("solve", solve_name)
424
+ # period_timeset: map period -> timeset
425
+ period_timeset_map = Map([period_name], [timeset_name])
426
+ parameter_values.append(("solve", solve_name, "period_timeset", period_timeset_map, alternative))
427
+ parameter_values.append(("solve", solve_name, "solve_mode", "single_solve", alternative))
428
+ # realized_periods
429
+ realized_periods = Array([period_name])
430
+ parameter_values.append(("solve", solve_name, "realized_periods", realized_periods, alternative))
431
+
432
+ # Model entity
433
+ add_entity("model", model_name)
434
+ parameter_values.append(("model", model_name, "version", FLEXTOOL_DB_VERSION, alternative))
435
+ parameter_values.append(("model", model_name, "inflation_rate", 0.0, alternative))
436
+ # model.solves
437
+ solves_array = Array([solve_name])
438
+ parameter_values.append(("model", model_name, "solves", solves_array, alternative))
439
+
440
+ # --- Import everything ---
441
+ count, errors = import_data(
442
+ db,
443
+ entities=entities,
444
+ entity_alternatives=entity_alternatives,
445
+ parameter_values=parameter_values,
446
+ )
447
+ if errors:
448
+ for err in errors:
449
+ logger.error("import_data error: %s", err)
450
+ raise RuntimeError(f"Failed to import MATPOWER data: {len(errors)} errors")
451
+ logger.info("Imported %d items into FlexTool DB", count)