esfex 0.1.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 (369) hide show
  1. esfex/__init__.py +17 -0
  2. esfex/analysis/__init__.py +45 -0
  3. esfex/analysis/ac_contingency.py +366 -0
  4. esfex/analysis/ac_types.py +49 -0
  5. esfex/analysis/contingency.py +1651 -0
  6. esfex/analysis/frequency.py +370 -0
  7. esfex/analysis/n1_assessment.py +544 -0
  8. esfex/analysis/native_ac_bridge.py +549 -0
  9. esfex/analysis/pandapower_bridge.py +533 -0
  10. esfex/analysis/snapshot_builder.py +207 -0
  11. esfex/bridge/__init__.py +18 -0
  12. esfex/bridge/adapters.py +3814 -0
  13. esfex/bridge/converters.py +2003 -0
  14. esfex/bridge/julia_setup.py +340 -0
  15. esfex/bridge/topology_audit.py +345 -0
  16. esfex/cli.py +792 -0
  17. esfex/config/__init__.py +29 -0
  18. esfex/config/loader.py +636 -0
  19. esfex/config/schema.py +1926 -0
  20. esfex/config/solver.py +692 -0
  21. esfex/icons/battery.svg +7 -0
  22. esfex/icons/busbar.svg +4 -0
  23. esfex/icons/converter.svg +5 -0
  24. esfex/icons/development.svg +6 -0
  25. esfex/icons/electrolizer.svg +6 -0
  26. esfex/icons/esfex.png +0 -0
  27. esfex/icons/esfex.svg +224 -0
  28. esfex/icons/frequency.svg +6 -0
  29. esfex/icons/fuel_entry.svg +4 -0
  30. esfex/icons/fuel_storage.svg +6 -0
  31. esfex/icons/fuel_transport.svg +5 -0
  32. esfex/icons/generator.svg +5 -0
  33. esfex/icons/icon.svg +55 -0
  34. esfex/icons/power_line.svg +53 -0
  35. esfex/icons/results.svg +37 -0
  36. esfex/icons/risk.svg +51 -0
  37. esfex/icons/run.svg +52 -0
  38. esfex/icons/selection.svg +15 -0
  39. esfex/icons/sensibility.svg +32 -0
  40. esfex/icons/system.svg +59 -0
  41. esfex/icons/trafo.svg +73 -0
  42. esfex/icons/validate.svg +33 -0
  43. esfex/io/__init__.py +32 -0
  44. esfex/io/demand.py +500 -0
  45. esfex/io/exporter.py +514 -0
  46. esfex/julia/Manifest.toml +941 -0
  47. esfex/julia/Project.toml +37 -0
  48. esfex/julia/build_sysimage.jl +87 -0
  49. esfex/julia/precompile_workload.jl +210 -0
  50. esfex/julia/src/ESFEX.jl +390 -0
  51. esfex/julia/src/acopf_benchmark.jl +328 -0
  52. esfex/julia/src/dcopf_benchmark.jl +147 -0
  53. esfex/julia/src/electrolyzer.jl +247 -0
  54. esfex/julia/src/master_problem.jl +4611 -0
  55. esfex/julia/src/mga.jl +778 -0
  56. esfex/julia/src/power_system.jl +4162 -0
  57. esfex/julia/src/primary_energy.jl +1709 -0
  58. esfex/julia/src/transmission_ac.jl +762 -0
  59. esfex/julia/src/transmission_acopf.jl +1222 -0
  60. esfex/julia/src/transmission_dc.jl +820 -0
  61. esfex/julia/src/types.jl +2780 -0
  62. esfex/logging_config.py +217 -0
  63. esfex/models/__init__.py +71 -0
  64. esfex/models/adoption_models.py +524 -0
  65. esfex/models/climate_profiles.py +672 -0
  66. esfex/models/country_metadata.py +399 -0
  67. esfex/models/demand_dataset.py +387 -0
  68. esfex/models/demand_ml.py +296 -0
  69. esfex/models/demand_projection.py +628 -0
  70. esfex/models/demand_real_data.py +1755 -0
  71. esfex/models/demand_tft.py +534 -0
  72. esfex/models/demand_training.py +814 -0
  73. esfex/models/demand_zonal_fetchers.py +690 -0
  74. esfex/models/download_cmip6.py +149 -0
  75. esfex/models/download_cmip6_subdaily.py +261 -0
  76. esfex/models/ecvi_gridded.py +774 -0
  77. esfex/models/ev.py +406 -0
  78. esfex/models/ev_adoption.py +35 -0
  79. esfex/models/ev_analysis.py +35 -0
  80. esfex/models/financial_analysis.py +1718 -0
  81. esfex/models/hazard_assessment.py +3898 -0
  82. esfex/models/otec_models.py +275 -0
  83. esfex/models/pixel_features.py +217 -0
  84. esfex/models/solar_pv_models.py +535 -0
  85. esfex/models/solar_rooftop.py +363 -0
  86. esfex/models/tft_inference.py +466 -0
  87. esfex/models/tsam.py +282 -0
  88. esfex/models/wind_models.py +540 -0
  89. esfex/models/zone_centroids.py +243 -0
  90. esfex/paths.py +57 -0
  91. esfex/plugins/__init__.py +12 -0
  92. esfex/plugins/availability_generator/__init__.py +33 -0
  93. esfex/plugins/availability_generator/cli_commands.py +148 -0
  94. esfex/plugins/availability_generator/generator.py +325 -0
  95. esfex/plugins/availability_generator/grid_builder_hook.py +197 -0
  96. esfex/plugins/availability_generator/gui_dialog.py +587 -0
  97. esfex/plugins/availability_generator/otec_cf.py +212 -0
  98. esfex/plugins/availability_generator/plugin.json +8 -0
  99. esfex/plugins/availability_generator/solar_cf.py +322 -0
  100. esfex/plugins/availability_generator/synthetic_cf.py +178 -0
  101. esfex/plugins/availability_generator/wind_cf.py +403 -0
  102. esfex/plugins/manager.py +924 -0
  103. esfex/plugins/protocol.py +218 -0
  104. esfex/runner.py +7026 -0
  105. esfex/sensitivity/__init__.py +9 -0
  106. esfex/sensitivity/engine.py +501 -0
  107. esfex/sensitivity/lp_parser.py +618 -0
  108. esfex/sensitivity/worker.py +67 -0
  109. esfex/topology/__init__.py +49 -0
  110. esfex/topology/network_reducer.py +651 -0
  111. esfex/topology/reduction_map.py +160 -0
  112. esfex/topology/result_expander.py +276 -0
  113. esfex/topology/transformations.py +651 -0
  114. esfex/utils/__init__.py +51 -0
  115. esfex/utils/helpers.py +406 -0
  116. esfex/utils/paths.py +55 -0
  117. esfex/utils/temporal.py +337 -0
  118. esfex/visualization/__init__.py +66 -0
  119. esfex/visualization/app.py +126 -0
  120. esfex/visualization/bridge/__init__.py +0 -0
  121. esfex/visualization/bridge/channel.py +31 -0
  122. esfex/visualization/bridge/js_bridge.py +156 -0
  123. esfex/visualization/bridge/sld_bridge.py +51 -0
  124. esfex/visualization/data/__init__.py +0 -0
  125. esfex/visualization/data/auto_complete.py +681 -0
  126. esfex/visualization/data/connectivity_rules.py +117 -0
  127. esfex/visualization/data/default_colors.py +51 -0
  128. esfex/visualization/data/geo_asset_parser.py +947 -0
  129. esfex/visualization/data/geojson_importer.py +220 -0
  130. esfex/visualization/data/gui_model.py +2778 -0
  131. esfex/visualization/data/serializer.py +3522 -0
  132. esfex/visualization/data/undo.py +54 -0
  133. esfex/visualization/data/validation.py +5679 -0
  134. esfex/visualization/i18n.py +129 -0
  135. esfex/visualization/main_window.py +7425 -0
  136. esfex/visualization/map_widget.py +941 -0
  137. esfex/visualization/modern_widgets.py +370 -0
  138. esfex/visualization/panels/__init__.py +0 -0
  139. esfex/visualization/panels/_dialogs.py +37 -0
  140. esfex/visualization/panels/acdc_converter_form.py +421 -0
  141. esfex/visualization/panels/analysis_panel.py +533 -0
  142. esfex/visualization/panels/auto_complete_dialog.py +157 -0
  143. esfex/visualization/panels/battery_form.py +561 -0
  144. esfex/visualization/panels/bus_form.py +246 -0
  145. esfex/visualization/panels/collapse_button.py +215 -0
  146. esfex/visualization/panels/dashboard_loader.py +1895 -0
  147. esfex/visualization/panels/dashboard_view.py +224 -0
  148. esfex/visualization/panels/doc_viewer.py +691 -0
  149. esfex/visualization/panels/electrolyzer_form.py +329 -0
  150. esfex/visualization/panels/element_tree.py +1320 -0
  151. esfex/visualization/panels/ev_form.py +298 -0
  152. esfex/visualization/panels/freq_converter_form.py +405 -0
  153. esfex/visualization/panels/fuel_entry_form.py +273 -0
  154. esfex/visualization/panels/fuel_form.py +187 -0
  155. esfex/visualization/panels/fuel_route_form.py +376 -0
  156. esfex/visualization/panels/fuel_source_form.py +245 -0
  157. esfex/visualization/panels/fuel_storage_form.py +272 -0
  158. esfex/visualization/panels/generator_form.py +710 -0
  159. esfex/visualization/panels/global_settings_form.py +1421 -0
  160. esfex/visualization/panels/inter_system_link_form.py +473 -0
  161. esfex/visualization/panels/investment_form.py +358 -0
  162. esfex/visualization/panels/line_form.py +424 -0
  163. esfex/visualization/panels/multi_edit.py +133 -0
  164. esfex/visualization/panels/node_form.py +897 -0
  165. esfex/visualization/panels/parse_geo_asset_dialog.py +380 -0
  166. esfex/visualization/panels/plugins_dialog.py +352 -0
  167. esfex/visualization/panels/preferences_dialog.py +584 -0
  168. esfex/visualization/panels/properties.py +214 -0
  169. esfex/visualization/panels/python_console.py +400 -0
  170. esfex/visualization/panels/results_cache.py +149 -0
  171. esfex/visualization/panels/results_charts.py +10457 -0
  172. esfex/visualization/panels/results_charts_bokeh.py +1552 -0
  173. esfex/visualization/panels/results_charts_plotly.py +1690 -0
  174. esfex/visualization/panels/results_dialog.py +1588 -0
  175. esfex/visualization/panels/results_panel.py +2039 -0
  176. esfex/visualization/panels/rooftop_solar_form.py +273 -0
  177. esfex/visualization/panels/script_editor.py +469 -0
  178. esfex/visualization/panels/sensitivity_dialog.py +477 -0
  179. esfex/visualization/panels/simulation_dialog.py +204 -0
  180. esfex/visualization/panels/stochastic_form.py +475 -0
  181. esfex/visualization/panels/system_form.py +323 -0
  182. esfex/visualization/panels/technology_form.py +333 -0
  183. esfex/visualization/panels/toolbar.py +482 -0
  184. esfex/visualization/panels/transformer_form.py +337 -0
  185. esfex/visualization/panels/validation_dialog.py +819 -0
  186. esfex/visualization/panels/visual_style_widget.py +210 -0
  187. esfex/visualization/panels/word_wrap_header.py +102 -0
  188. esfex/visualization/panels/zone_form.py +439 -0
  189. esfex/visualization/preferences.py +237 -0
  190. esfex/visualization/pty_runner.py +248 -0
  191. esfex/visualization/resources/battery_heatmap.html +31 -0
  192. esfex/visualization/resources/battery_heatmap.js +259 -0
  193. esfex/visualization/resources/battery_operation.html +31 -0
  194. esfex/visualization/resources/battery_operation.js +153 -0
  195. esfex/visualization/resources/carbon_penalty.html +31 -0
  196. esfex/visualization/resources/carbon_penalty.js +186 -0
  197. esfex/visualization/resources/cash_flow.html +31 -0
  198. esfex/visualization/resources/cash_flow.js +306 -0
  199. esfex/visualization/resources/chart_theme.js +153 -0
  200. esfex/visualization/resources/custom_chart.html +31 -0
  201. esfex/visualization/resources/custom_chart.js +201 -0
  202. esfex/visualization/resources/d3.v7.min.js +2 -0
  203. esfex/visualization/resources/dashboard.html +270 -0
  204. esfex/visualization/resources/dashboard.js +759 -0
  205. esfex/visualization/resources/electricity_cost.html +31 -0
  206. esfex/visualization/resources/electricity_cost.js +305 -0
  207. esfex/visualization/resources/elk.bundled.js +6696 -0
  208. esfex/visualization/resources/flex_reliability.html +31 -0
  209. esfex/visualization/resources/flex_reliability.js +305 -0
  210. esfex/visualization/resources/font_scale.js +139 -0
  211. esfex/visualization/resources/fuel_supply.html +31 -0
  212. esfex/visualization/resources/fuel_supply.js +190 -0
  213. esfex/visualization/resources/images/layers-2x.png +0 -0
  214. esfex/visualization/resources/images/layers.png +0 -0
  215. esfex/visualization/resources/images/marker-icon-2x.png +0 -0
  216. esfex/visualization/resources/images/marker-icon.png +0 -0
  217. esfex/visualization/resources/images/marker-shadow.png +0 -0
  218. esfex/visualization/resources/images/spritesheet-2x.png +0 -0
  219. esfex/visualization/resources/images/spritesheet.png +0 -0
  220. esfex/visualization/resources/images/spritesheet.svg +156 -0
  221. esfex/visualization/resources/inter_node_flows.html +31 -0
  222. esfex/visualization/resources/inter_node_flows.js +197 -0
  223. esfex/visualization/resources/leaflet.css +661 -0
  224. esfex/visualization/resources/leaflet.draw.css +10 -0
  225. esfex/visualization/resources/leaflet.draw.js +10 -0
  226. esfex/visualization/resources/leaflet.html +96 -0
  227. esfex/visualization/resources/leaflet.js +6 -0
  228. esfex/visualization/resources/map_controller.js +3360 -0
  229. esfex/visualization/resources/mga_annotated_dendrogram.html +18 -0
  230. esfex/visualization/resources/mga_annotated_dendrogram.js +215 -0
  231. esfex/visualization/resources/mga_composition.html +18 -0
  232. esfex/visualization/resources/mga_composition.js +109 -0
  233. esfex/visualization/resources/mga_decision_factors.html +18 -0
  234. esfex/visualization/resources/mga_decision_factors.js +260 -0
  235. esfex/visualization/resources/mga_parcoords.html +25 -0
  236. esfex/visualization/resources/mga_parcoords.js +61 -0
  237. esfex/visualization/resources/mga_pathway.html +25 -0
  238. esfex/visualization/resources/mga_pathway.js +91 -0
  239. esfex/visualization/resources/mga_projection.html +18 -0
  240. esfex/visualization/resources/mga_projection.js +192 -0
  241. esfex/visualization/resources/mga_robustness_frontier.html +18 -0
  242. esfex/visualization/resources/mga_robustness_frontier.js +269 -0
  243. esfex/visualization/resources/mga_similarity.html +18 -0
  244. esfex/visualization/resources/mga_similarity.js +212 -0
  245. esfex/visualization/resources/mga_spatial.html +25 -0
  246. esfex/visualization/resources/mga_spatial.js +101 -0
  247. esfex/visualization/resources/mix_chart.html +31 -0
  248. esfex/visualization/resources/mix_chart.js +331 -0
  249. esfex/visualization/resources/net_load_heatmap.html +31 -0
  250. esfex/visualization/resources/net_load_heatmap.js +192 -0
  251. esfex/visualization/resources/plotly.min.js +8 -0
  252. esfex/visualization/resources/price_duration.html +31 -0
  253. esfex/visualization/resources/price_duration.js +169 -0
  254. esfex/visualization/resources/revenue_profitability.html +31 -0
  255. esfex/visualization/resources/revenue_profitability.js +216 -0
  256. esfex/visualization/resources/sankey.html +31 -0
  257. esfex/visualization/resources/sankey.js +68 -0
  258. esfex/visualization/resources/sld.html +87 -0
  259. esfex/visualization/resources/sld_controller.js +3042 -0
  260. esfex/visualization/resources/system_metrics.html +31 -0
  261. esfex/visualization/resources/system_metrics.js +213 -0
  262. esfex/visualization/resources/tech_performance.html +31 -0
  263. esfex/visualization/resources/tech_performance.js +270 -0
  264. esfex/visualization/resources/terminal.html +184 -0
  265. esfex/visualization/resources/uc_commitment_heatmap.html +31 -0
  266. esfex/visualization/resources/uc_commitment_heatmap.js +111 -0
  267. esfex/visualization/resources/uc_dispatch_stack.html +31 -0
  268. esfex/visualization/resources/uc_dispatch_stack.js +188 -0
  269. esfex/visualization/resources/uc_hourly_price.html +31 -0
  270. esfex/visualization/resources/uc_hourly_price.js +124 -0
  271. esfex/visualization/resources/uc_lmp_by_node.html +21 -0
  272. esfex/visualization/resources/uc_lmp_by_node.js +114 -0
  273. esfex/visualization/resources/uc_loadshed_curtailment.html +31 -0
  274. esfex/visualization/resources/uc_loadshed_curtailment.js +169 -0
  275. esfex/visualization/resources/uc_marginal_tech.html +31 -0
  276. esfex/visualization/resources/uc_marginal_tech.js +160 -0
  277. esfex/visualization/resources/uc_netload_duration.html +21 -0
  278. esfex/visualization/resources/uc_netload_duration.js +109 -0
  279. esfex/visualization/resources/uc_price_duration.html +31 -0
  280. esfex/visualization/resources/uc_price_duration.js +114 -0
  281. esfex/visualization/resources/uc_ramp_distribution.html +21 -0
  282. esfex/visualization/resources/uc_ramp_distribution.js +102 -0
  283. esfex/visualization/resources/uc_storage_soc.html +31 -0
  284. esfex/visualization/resources/uc_storage_soc.js +113 -0
  285. esfex/visualization/resources/world_countries.geojson +1 -0
  286. esfex/visualization/resources/xterm-addon-fit.js +2 -0
  287. esfex/visualization/resources/xterm.css +285 -0
  288. esfex/visualization/resources/xterm.js +2 -0
  289. esfex/visualization/run_output_view.py +242 -0
  290. esfex/visualization/scripting_api.py +1062 -0
  291. esfex/visualization/sld/__init__.py +1 -0
  292. esfex/visualization/sld/graph_builder.py +700 -0
  293. esfex/visualization/sld/sld_results_loader.py +490 -0
  294. esfex/visualization/sld/voltage_colors.py +61 -0
  295. esfex/visualization/sld_widget.py +107 -0
  296. esfex/visualization/splash.py +160 -0
  297. esfex/visualization/theme.py +1855 -0
  298. esfex/visualization/translations/__init__.py +0 -0
  299. esfex/visualization/translations/en.json +2389 -0
  300. esfex/visualization/translations/es.json +2350 -0
  301. esfex/visualization/translations/ja.json +2396 -0
  302. esfex/visualization/workflows/__init__.py +1 -0
  303. esfex/visualization/workflows/_qt_adapters.py +101 -0
  304. esfex/visualization/workflows/_wizard_utils.py +83 -0
  305. esfex/visualization/workflows/data_fetchers.py +501 -0
  306. esfex/visualization/workflows/demand_analysis.py +417 -0
  307. esfex/visualization/workflows/demand_distribution_steps.py +986 -0
  308. esfex/visualization/workflows/demand_distribution_wizard.py +260 -0
  309. esfex/visualization/workflows/demand_estimation_analysis.py +1776 -0
  310. esfex/visualization/workflows/demand_estimation_fetchers.py +1090 -0
  311. esfex/visualization/workflows/demand_estimation_steps.py +3010 -0
  312. esfex/visualization/workflows/demand_estimation_wizard.py +325 -0
  313. esfex/visualization/workflows/ev_advanced_steps.py +1159 -0
  314. esfex/visualization/workflows/ev_fetchers.py +262 -0
  315. esfex/visualization/workflows/ev_steps.py +1075 -0
  316. esfex/visualization/workflows/ev_wizard.py +333 -0
  317. esfex/visualization/workflows/financial_charts.py +653 -0
  318. esfex/visualization/workflows/financial_steps.py +1035 -0
  319. esfex/visualization/workflows/financial_wizard.py +344 -0
  320. esfex/visualization/workflows/grid_mapping_builder.py +1037 -0
  321. esfex/visualization/workflows/grid_mapping_clustering.py +544 -0
  322. esfex/visualization/workflows/grid_mapping_fetchers.py +1839 -0
  323. esfex/visualization/workflows/grid_mapping_inference.py +279 -0
  324. esfex/visualization/workflows/grid_mapping_quality.py +1069 -0
  325. esfex/visualization/workflows/grid_mapping_steps.py +4986 -0
  326. esfex/visualization/workflows/grid_mapping_wizard.py +300 -0
  327. esfex/visualization/workflows/otec_studio/__init__.py +23 -0
  328. esfex/visualization/workflows/otec_studio/cycle_panel.py +285 -0
  329. esfex/visualization/workflows/otec_studio/cycles.py +128 -0
  330. esfex/visualization/workflows/otec_studio/economics.py +138 -0
  331. esfex/visualization/workflows/otec_studio/economics_panel.py +262 -0
  332. esfex/visualization/workflows/otec_studio/engineering.py +186 -0
  333. esfex/visualization/workflows/otec_studio/operation.py +100 -0
  334. esfex/visualization/workflows/otec_studio/operation_panel.py +372 -0
  335. esfex/visualization/workflows/otec_studio/optimization_panel.py +396 -0
  336. esfex/visualization/workflows/otec_studio/optimize.py +183 -0
  337. esfex/visualization/workflows/otec_studio/project.py +282 -0
  338. esfex/visualization/workflows/otec_studio/regional.py +122 -0
  339. esfex/visualization/workflows/otec_studio/regional_panel.py +295 -0
  340. esfex/visualization/workflows/otec_studio/resource.py +139 -0
  341. esfex/visualization/workflows/otec_studio/resource_panel.py +329 -0
  342. esfex/visualization/workflows/otec_studio/uq.py +119 -0
  343. esfex/visualization/workflows/otec_studio/uq_panel.py +343 -0
  344. esfex/visualization/workflows/otec_studio/window.py +337 -0
  345. esfex/visualization/workflows/otec_studio/workers.py +210 -0
  346. esfex/visualization/workflows/otec_studio/zones.py +170 -0
  347. esfex/visualization/workflows/risk_charts.py +949 -0
  348. esfex/visualization/workflows/risk_panels.py +2971 -0
  349. esfex/visualization/workflows/risk_wizard.py +542 -0
  350. esfex/visualization/workflows/solar_adoption_steps.py +1302 -0
  351. esfex/visualization/workflows/solar_analysis.py +337 -0
  352. esfex/visualization/workflows/solar_macro_fetchers.py +731 -0
  353. esfex/visualization/workflows/solar_pv_advanced_steps.py +1435 -0
  354. esfex/visualization/workflows/solar_pv_analysis.py +1385 -0
  355. esfex/visualization/workflows/solar_pv_steps.py +1382 -0
  356. esfex/visualization/workflows/solar_pv_wizard.py +409 -0
  357. esfex/visualization/workflows/solar_rooftop_steps.py +798 -0
  358. esfex/visualization/workflows/solar_rooftop_wizard.py +397 -0
  359. esfex/visualization/workflows/wind_advanced_steps.py +1425 -0
  360. esfex/visualization/workflows/wind_analysis.py +1555 -0
  361. esfex/visualization/workflows/wind_steps.py +1320 -0
  362. esfex/visualization/workflows/wind_wizard.py +396 -0
  363. esfex/zones.py +532 -0
  364. esfex-0.1.0.dist-info/METADATA +421 -0
  365. esfex-0.1.0.dist-info/RECORD +369 -0
  366. esfex-0.1.0.dist-info/WHEEL +5 -0
  367. esfex-0.1.0.dist-info/entry_points.txt +2 -0
  368. esfex-0.1.0.dist-info/licenses/LICENSE +201 -0
  369. esfex-0.1.0.dist-info/top_level.txt +1 -0
esfex/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ ESFEX: Energy System FlEXibility — Power System Optimization
3
+
4
+ A hybrid Python/Julia optimization framework for power system planning and operation.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+ __author__ = "Manuel Soto Calvo & Han Soo Lee"
9
+
10
+ from esfex.config.loader import load_config
11
+ from esfex.config.schema import ESFEXConfig
12
+
13
+ __all__ = [
14
+ "__version__",
15
+ "load_config",
16
+ "ESFEXConfig",
17
+ ]
@@ -0,0 +1,45 @@
1
+ """Power system dynamic analysis modules.
2
+
3
+ This package provides post-optimization analysis tools for frequency
4
+ stability, N-1 contingency assessment, and related calculations.
5
+ """
6
+
7
+ from esfex.analysis.ac_types import ACPowerFlowResult, ShortCircuitResult
8
+ from esfex.analysis.frequency import (
9
+ FrequencyAnalyzer,
10
+ FrequencyResponse,
11
+ build_gen_freq_params_from_state,
12
+ )
13
+ from esfex.analysis.contingency import ContingencyAnalyzer, ContingencyResult
14
+ from esfex.analysis.ac_contingency import ACContingencyAnalyzer, ACContingencyResult
15
+ from esfex.analysis.n1_assessment import IntegratedN1Analyzer, N1SecurityAssessment
16
+
17
+ __all__ = [
18
+ "ACPowerFlowResult",
19
+ "ShortCircuitResult",
20
+ "FrequencyAnalyzer",
21
+ "FrequencyResponse",
22
+ "build_gen_freq_params_from_state",
23
+ "ContingencyAnalyzer",
24
+ "ContingencyResult",
25
+ "ACContingencyAnalyzer",
26
+ "ACContingencyResult",
27
+ "IntegratedN1Analyzer",
28
+ "N1SecurityAssessment",
29
+ ]
30
+
31
+ # Conditional pandapower exports
32
+ try:
33
+ from esfex.analysis.pandapower_bridge import PandapowerBridge
34
+
35
+ __all__ += ["PandapowerBridge"]
36
+ except ImportError:
37
+ pass
38
+
39
+ # Native AC bridge (always available — no external dependencies)
40
+ try:
41
+ from esfex.analysis.native_ac_bridge import NativeACBridge
42
+
43
+ __all__ += ["NativeACBridge"]
44
+ except ImportError:
45
+ pass
@@ -0,0 +1,366 @@
1
+ """AC N-1 contingency analysis using pandapower.
2
+
3
+ Wraps ``PandapowerBridge`` with the same API as ``ContingencyAnalyzer``
4
+ (DC), but produces AC power flow results including voltage violations
5
+ and reactive power. Falls back to the DC analyzer if pandapower
6
+ diverges for a given contingency.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+ from esfex.analysis.contingency import ContingencyAnalyzer, ContingencyResult
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ACContingencyResult(ContingencyResult):
22
+ """Extended contingency result with AC-specific fields.
23
+
24
+ Inherits all fields from ``ContingencyResult`` and adds voltage
25
+ data from the AC power flow solution.
26
+ """
27
+
28
+ post_vm_pu: dict[str, float] = field(default_factory=dict)
29
+ voltage_violations: list[dict[str, Any]] = field(default_factory=list)
30
+ ac_converged: bool = True
31
+
32
+
33
+ class ACContingencyAnalyzer:
34
+ """N-1 contingency analyzer using AC power flow via pandapower.
35
+
36
+ Uses the same public API as ``ContingencyAnalyzer`` so it can be
37
+ a drop-in replacement when pandapower is available.
38
+
39
+ Parameters
40
+ ----------
41
+ bridge : PandapowerBridge
42
+ Configured bridge (state already set).
43
+ dc_fallback : ContingencyAnalyzer | None
44
+ DC analyzer used as fallback when AC PF diverges.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ bridge: Any,
50
+ dc_fallback: ContingencyAnalyzer | None = None,
51
+ ) -> None:
52
+ self._bridge = bridge
53
+ self._dc_fallback = dc_fallback
54
+
55
+ # ── Main API (matches ContingencyAnalyzer) ──
56
+
57
+ def analyze_generator_loss(
58
+ self,
59
+ snapshot: dict[str, Any],
60
+ gen_element_id: str,
61
+ ) -> ACContingencyResult:
62
+ """Compute post-contingency state after losing a generator.
63
+
64
+ Sets the generator out of service, redistributes its output
65
+ to other dispatchable generators, and reruns AC power flow.
66
+
67
+ Falls back to DC analysis if AC PF diverges.
68
+ """
69
+ from esfex.analysis.ac_types import ACPowerFlowResult # noqa: F401
70
+
71
+ gens_data = snapshot.get("generators", {})
72
+ lost_output = gens_data.get(gen_element_id, {}).get("output_mw", 0.0)
73
+
74
+ if lost_output <= 0:
75
+ return ACContingencyResult(
76
+ contingency_type="generator",
77
+ element_id=gen_element_id,
78
+ element_description=f"Generator {gen_element_id} (offline)",
79
+ is_secure=True,
80
+ ac_converged=True,
81
+ )
82
+
83
+ # Collect pre-contingency state
84
+ pre_gen = {eid: gd.get("output_mw", 0.0) for eid, gd in gens_data.items()}
85
+ pre_flow = {
86
+ eid: ld.get("flow_mw", 0.0)
87
+ for eid, ld in snapshot.get("lines", {}).items()
88
+ }
89
+
90
+ # Trip the generator in the existing network
91
+ net = self._bridge.get_network()
92
+ if net is None:
93
+ return self._dc_fallback_gen(snapshot, gen_element_id)
94
+
95
+ # Set generator out of service
96
+ gen_type = self._find_gen_type(gen_element_id)
97
+ if gen_type:
98
+ self._bridge.set_element_in_service(gen_type, gen_element_id, False)
99
+
100
+ # Redistribute lost generation to remaining dispatchable gens
101
+ self._redistribute_generation(net, gen_element_id, lost_output, gens_data)
102
+
103
+ # Rerun AC power flow
104
+ pf_result = self._bridge.rerun_power_flow()
105
+
106
+ # Restore generator
107
+ if gen_type:
108
+ self._bridge.set_element_in_service(gen_type, gen_element_id, True)
109
+
110
+ if not pf_result.converged:
111
+ log.info("AC PF diverged for gen loss %s, falling back to DC", gen_element_id)
112
+ return self._dc_fallback_gen(snapshot, gen_element_id)
113
+
114
+ # Build post-contingency generation
115
+ post_gen = dict(pre_gen)
116
+ post_gen[gen_element_id] = 0.0
117
+ for gid, p_mw in pf_result.gen_p_mw.items():
118
+ if gid != gen_element_id:
119
+ post_gen[gid] = p_mw
120
+
121
+ # Build post-contingency flows
122
+ post_flow: dict[str, float] = {}
123
+ overloaded = []
124
+ max_overload = 0.0
125
+ for edge_id, p_from in pf_result.line_p_from_mw.items():
126
+ post_flow[edge_id] = p_from
127
+ loading = pf_result.line_loading_pct.get(edge_id, 0.0)
128
+ if loading > 100.0:
129
+ overload_pct = loading - 100.0
130
+ max_overload = max(max_overload, overload_pct)
131
+ line_id = edge_id.replace("edge_", "")
132
+ overloaded.append({
133
+ "line_id": line_id,
134
+ "edge_id": edge_id,
135
+ "flow_mw": round(p_from, 2),
136
+ "capacity_mw": 0.0, # TODO: resolve from snapshot
137
+ "overload_pct": round(overload_pct, 1),
138
+ "loading_pct": round(loading, 1),
139
+ })
140
+
141
+ # Resolve line capacities for overloaded lines
142
+ lines_data = snapshot.get("lines", {})
143
+ for ol in overloaded:
144
+ cap = lines_data.get(ol["edge_id"], {}).get("capacity_mw", 0.0)
145
+ ol["capacity_mw"] = round(cap, 2)
146
+
147
+ return ACContingencyResult(
148
+ contingency_type="generator",
149
+ element_id=gen_element_id,
150
+ element_description=f"Loss of {gen_element_id} ({lost_output:.1f} MW)",
151
+ pre_gen_mw=pre_gen,
152
+ pre_flow_mw=pre_flow,
153
+ post_gen_mw={k: round(v, 2) for k, v in post_gen.items()},
154
+ post_flow_mw=post_flow,
155
+ overloaded_lines=overloaded,
156
+ is_secure=len(overloaded) == 0 and len(pf_result.voltage_violations) == 0,
157
+ max_overload_pct=round(max_overload, 1),
158
+ post_vm_pu=pf_result.bus_vm_pu,
159
+ voltage_violations=pf_result.voltage_violations,
160
+ ac_converged=True,
161
+ )
162
+
163
+ def analyze_line_loss(
164
+ self,
165
+ snapshot: dict[str, Any],
166
+ line_id: str,
167
+ ) -> ACContingencyResult:
168
+ """Compute post-contingency state after losing a transmission line."""
169
+ pre_gen = {
170
+ eid: gd.get("output_mw", 0.0)
171
+ for eid, gd in snapshot.get("generators", {}).items()
172
+ }
173
+ pre_flow = {
174
+ eid: ld.get("flow_mw", 0.0)
175
+ for eid, ld in snapshot.get("lines", {}).items()
176
+ }
177
+
178
+ net = self._bridge.get_network()
179
+ if net is None:
180
+ return self._dc_fallback_line(snapshot, line_id)
181
+
182
+ # Take line out of service
183
+ self._bridge.set_element_in_service("line", line_id, False)
184
+
185
+ # Rerun AC power flow
186
+ pf_result = self._bridge.rerun_power_flow()
187
+
188
+ # Restore line
189
+ self._bridge.set_element_in_service("line", line_id, True)
190
+
191
+ if not pf_result.converged:
192
+ log.info("AC PF diverged for line loss %s, falling back to DC", line_id)
193
+ return self._dc_fallback_line(snapshot, line_id)
194
+
195
+ # Post-contingency flows
196
+ post_flow: dict[str, float] = {}
197
+ overloaded = []
198
+ max_overload = 0.0
199
+ tripped_edge = f"edge_{line_id}"
200
+ lines_data = snapshot.get("lines", {})
201
+
202
+ for edge_id, p_from in pf_result.line_p_from_mw.items():
203
+ post_flow[edge_id] = p_from
204
+ loading = pf_result.line_loading_pct.get(edge_id, 0.0)
205
+ if loading > 100.0:
206
+ overload_pct = loading - 100.0
207
+ max_overload = max(max_overload, overload_pct)
208
+ lid = edge_id.replace("edge_", "")
209
+ cap = lines_data.get(edge_id, {}).get("capacity_mw", 0.0)
210
+ overloaded.append({
211
+ "line_id": lid,
212
+ "edge_id": edge_id,
213
+ "flow_mw": round(p_from, 2),
214
+ "capacity_mw": round(cap, 2),
215
+ "overload_pct": round(overload_pct, 1),
216
+ "loading_pct": round(loading, 1),
217
+ })
218
+
219
+ post_flow[tripped_edge] = 0.0
220
+
221
+ cap_mw = lines_data.get(tripped_edge, {}).get("capacity_mw", 0.0)
222
+
223
+ return ACContingencyResult(
224
+ contingency_type="line",
225
+ element_id=line_id,
226
+ element_description=f"Loss of line {line_id} ({cap_mw:.0f} MW cap)",
227
+ pre_gen_mw=pre_gen,
228
+ pre_flow_mw=pre_flow,
229
+ post_gen_mw={k: round(v, 2) for k, v in pre_gen.items()},
230
+ post_flow_mw=post_flow,
231
+ overloaded_lines=overloaded,
232
+ is_secure=len(overloaded) == 0 and len(pf_result.voltage_violations) == 0,
233
+ max_overload_pct=round(max_overload, 1),
234
+ post_vm_pu=pf_result.bus_vm_pu,
235
+ voltage_violations=pf_result.voltage_violations,
236
+ ac_converged=True,
237
+ )
238
+
239
+ def get_contingency_list(
240
+ self, snapshot: dict[str, Any],
241
+ ) -> list[dict[str, Any]]:
242
+ """Return all possible contingencies (same format as DC version)."""
243
+ contingencies: list[dict[str, Any]] = []
244
+
245
+ gens_data = snapshot.get("generators", {})
246
+ for eid, gdata in gens_data.items():
247
+ output = gdata.get("output_mw", 0.0)
248
+ status = gdata.get("status", 1)
249
+ if status > 0 and output > 0.1:
250
+ contingencies.append({
251
+ "type": "generator",
252
+ "element_id": eid,
253
+ "description": f"Loss: {eid} ({output:.0f} MW)",
254
+ "impact_mw": output,
255
+ })
256
+
257
+ for edge_id, ldata in snapshot.get("lines", {}).items():
258
+ line_id = edge_id.replace("edge_", "")
259
+ flow = abs(ldata.get("flow_mw", 0.0))
260
+ cap = ldata.get("capacity_mw", 0.0)
261
+ contingencies.append({
262
+ "type": "line",
263
+ "element_id": line_id,
264
+ "description": f"Loss: {line_id} ({cap:.0f} MW cap)",
265
+ "impact_mw": flow,
266
+ })
267
+
268
+ contingencies.sort(key=lambda c: c["impact_mw"], reverse=True)
269
+ return contingencies
270
+
271
+ # ── Private helpers ──
272
+
273
+ def _find_gen_type(self, gen_id: str) -> str | None:
274
+ """Determine the pandapower element type for a generator ID."""
275
+ if gen_id in self._bridge._gen_id_to_pp:
276
+ return "gen"
277
+ if gen_id in self._bridge._sgen_id_to_pp:
278
+ return "sgen"
279
+ return None
280
+
281
+ def _redistribute_generation(
282
+ self, net: Any, tripped_id: str, lost_mw: float,
283
+ gens_data: dict[str, Any],
284
+ ) -> None:
285
+ """Redistribute lost generation to remaining dispatchable gens."""
286
+ import pandapower as pp # noqa: F401
287
+
288
+ # Find dispatchable gens with headroom
289
+ headroom: dict[str, float] = {}
290
+ for gen_id, pp_idx in self._bridge._gen_id_to_pp.items():
291
+ if gen_id == tripped_id:
292
+ continue
293
+ if not net.gen.at[pp_idx, "in_service"]:
294
+ continue
295
+ current = gens_data.get(gen_id, {}).get("output_mw", 0.0)
296
+ rated = gens_data.get(gen_id, {}).get("capacity_mw", 0.0)
297
+ room = max(0.0, rated - current)
298
+ if room > 0:
299
+ headroom[gen_id] = room
300
+
301
+ total_headroom = sum(headroom.values())
302
+ if total_headroom <= 0:
303
+ return
304
+
305
+ # Pro-rata increase
306
+ for gen_id, room in headroom.items():
307
+ increase = min(lost_mw, total_headroom) * (room / total_headroom)
308
+ pp_idx = self._bridge._gen_id_to_pp[gen_id]
309
+ current_p = float(net.gen.at[pp_idx, "p_mw"])
310
+ net.gen.at[pp_idx, "p_mw"] = current_p + increase
311
+
312
+ def _dc_fallback_gen(
313
+ self, snapshot: dict, gen_id: str,
314
+ ) -> ACContingencyResult:
315
+ """Fall back to DC contingency for generator loss."""
316
+ if self._dc_fallback is not None:
317
+ dc_result = self._dc_fallback.analyze_generator_loss(snapshot, gen_id)
318
+ return ACContingencyResult(
319
+ contingency_type=dc_result.contingency_type,
320
+ element_id=dc_result.element_id,
321
+ element_description=dc_result.element_description + " [DC fallback]",
322
+ pre_gen_mw=dc_result.pre_gen_mw,
323
+ pre_flow_mw=dc_result.pre_flow_mw,
324
+ post_gen_mw=dc_result.post_gen_mw,
325
+ post_flow_mw=dc_result.post_flow_mw,
326
+ load_shed_mw=dc_result.load_shed_mw,
327
+ overloaded_lines=dc_result.overloaded_lines,
328
+ total_load_shed_mw=dc_result.total_load_shed_mw,
329
+ is_secure=dc_result.is_secure,
330
+ max_overload_pct=dc_result.max_overload_pct,
331
+ ac_converged=False,
332
+ )
333
+ return ACContingencyResult(
334
+ contingency_type="generator",
335
+ element_id=gen_id,
336
+ element_description=f"Loss of {gen_id} [AC diverged, no DC fallback]",
337
+ ac_converged=False,
338
+ )
339
+
340
+ def _dc_fallback_line(
341
+ self, snapshot: dict, line_id: str,
342
+ ) -> ACContingencyResult:
343
+ """Fall back to DC contingency for line loss."""
344
+ if self._dc_fallback is not None:
345
+ dc_result = self._dc_fallback.analyze_line_loss(snapshot, line_id)
346
+ return ACContingencyResult(
347
+ contingency_type=dc_result.contingency_type,
348
+ element_id=dc_result.element_id,
349
+ element_description=dc_result.element_description + " [DC fallback]",
350
+ pre_gen_mw=dc_result.pre_gen_mw,
351
+ pre_flow_mw=dc_result.pre_flow_mw,
352
+ post_gen_mw=dc_result.post_gen_mw,
353
+ post_flow_mw=dc_result.post_flow_mw,
354
+ load_shed_mw=dc_result.load_shed_mw,
355
+ overloaded_lines=dc_result.overloaded_lines,
356
+ total_load_shed_mw=dc_result.total_load_shed_mw,
357
+ is_secure=dc_result.is_secure,
358
+ max_overload_pct=dc_result.max_overload_pct,
359
+ ac_converged=False,
360
+ )
361
+ return ACContingencyResult(
362
+ contingency_type="line",
363
+ element_id=line_id,
364
+ element_description=f"Loss of line {line_id} [AC diverged, no DC fallback]",
365
+ ac_converged=False,
366
+ )
@@ -0,0 +1,49 @@
1
+ """Shared result types for AC power flow and short-circuit analysis.
2
+
3
+ These dataclasses are backend-agnostic — used by both the native Julia
4
+ Newton-Raphson solver (``NativeACBridge``) and the pandapower bridge
5
+ (``PandapowerBridge``).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ @dataclass
15
+ class ACPowerFlowResult:
16
+ """Results from an AC Newton-Raphson power flow."""
17
+
18
+ converged: bool = False
19
+ iterations: int = 0
20
+
21
+ # Per bus: bus_id → values
22
+ bus_vm_pu: dict[str, float] = field(default_factory=dict)
23
+ bus_va_deg: dict[str, float] = field(default_factory=dict)
24
+ bus_p_mw: dict[str, float] = field(default_factory=dict)
25
+ bus_q_mvar: dict[str, float] = field(default_factory=dict)
26
+
27
+ # Per line: edge_id → values
28
+ line_p_from_mw: dict[str, float] = field(default_factory=dict)
29
+ line_q_from_mvar: dict[str, float] = field(default_factory=dict)
30
+ line_p_loss_mw: dict[str, float] = field(default_factory=dict)
31
+ line_loading_pct: dict[str, float] = field(default_factory=dict)
32
+
33
+ # Per generator: gen_id → values
34
+ gen_p_mw: dict[str, float] = field(default_factory=dict)
35
+ gen_q_mvar: dict[str, float] = field(default_factory=dict)
36
+
37
+ # Summary
38
+ total_losses_mw: float = 0.0
39
+ voltage_violations: list[dict[str, Any]] = field(default_factory=list)
40
+
41
+
42
+ @dataclass
43
+ class ShortCircuitResult:
44
+ """Results from IEC 60909 short-circuit analysis."""
45
+
46
+ # Per bus: bus_id → values
47
+ ik_ka: dict[str, float] = field(default_factory=dict) # Initial SC current
48
+ ip_ka: dict[str, float] = field(default_factory=dict) # Peak SC current
49
+ sk_mva: dict[str, float] = field(default_factory=dict) # SC power