optiwindnet 0.2.2__tar.gz → 0.2.3__tar.gz

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 (212) hide show
  1. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/CHANGELOG.md +37 -0
  2. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/PKG-INFO +1 -1
  3. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/__init__.py +5 -4
  4. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/_core.py +88 -11
  5. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/_core.pyi +8 -0
  6. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/gurobi.py +5 -4
  7. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/ortools.py +42 -59
  8. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/pyomo.py +53 -72
  9. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/scip.py +35 -54
  10. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/api.py +107 -58
  11. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/api_utils.py +30 -6
  12. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/augmentation.py +37 -35
  13. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/baselines/_core.py +3 -3
  14. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/baselines/hgs.py +125 -27
  15. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/baselines/lkh.py +41 -39
  16. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/baselines/utils.py +17 -16
  17. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/clustering.py +3 -3
  18. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/crossings.py +19 -19
  19. optiwindnet-0.2.3/optiwindnet/data/East Anglia THREE.osm.pbf +0 -0
  20. optiwindnet-0.2.3/optiwindnet/data/Hollandse Kust Noord.osm.pbf +0 -0
  21. optiwindnet-0.2.3/optiwindnet/data/Hornsea 2.osm.pbf +0 -0
  22. optiwindnet-0.2.3/optiwindnet/data/Norfolk Vanguard West.osm.pbf +0 -0
  23. optiwindnet-0.2.3/optiwindnet/data/Revolution.osm.pbf +0 -0
  24. optiwindnet-0.2.3/optiwindnet/data/Sunrise.osm.pbf +0 -0
  25. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/db/migrate.py +2 -2
  26. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/db/model.py +87 -25
  27. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/db/storage.py +14 -13
  28. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/geometric.py +48 -257
  29. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/constructor.py +31 -26
  30. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/priorityqueue.py +7 -5
  31. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/importer.py +76 -31
  32. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/interarraylib.py +99 -80
  33. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/mesh.py +20 -162
  34. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/pathfinding.py +129 -72
  35. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/plotting.py +12 -12
  36. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/repair.py +18 -16
  37. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/svg.py +13 -13
  38. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/synthetic.py +3 -2
  39. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/utils.py +0 -6
  40. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/version.py +3 -3
  41. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet.egg-info/PKG-INFO +1 -1
  42. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet.egg-info/SOURCES.txt +12 -2
  43. optiwindnet-0.2.3/optiwindnet.egg-info/scm_file_list.json +276 -0
  44. optiwindnet-0.2.3/optiwindnet.egg-info/scm_version.json +8 -0
  45. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/pyproject.toml +1 -0
  46. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/conftest.py +16 -1
  47. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/helpers.py +47 -1
  48. optiwindnet-0.2.3/tests/isolation.py +122 -0
  49. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/paths.py +1 -1
  50. optiwindnet-0.2.3/tests/test_MILP.py +400 -0
  51. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_api_WindFarmNetwork.py +74 -2
  52. optiwindnet-0.2.3/tests/test_baselines_utils.py +48 -0
  53. optiwindnet-0.2.3/tests/test_end_to_end.py +143 -0
  54. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_geometric.py +163 -0
  55. optiwindnet-0.2.3/tests/test_hgs.py +227 -0
  56. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_importer.py +17 -5
  57. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_interarraylib.py +105 -2
  58. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_mesh.py +0 -36
  59. optiwindnet-0.2.3/tests/test_plotting.py +125 -0
  60. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_repair.py +53 -0
  61. optiwindnet-0.2.3/tests/test_svg.py +391 -0
  62. optiwindnet-0.2.3/tests/test_themes.py +26 -0
  63. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_utils.py +1 -24
  64. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/update_expected_values.py +31 -10
  65. optiwindnet-0.2.2/_run_chain_tests.py +0 -137
  66. optiwindnet-0.2.2/optiwindnet/weighting.py +0 -20
  67. optiwindnet-0.2.2/tests/test_MILP.py +0 -235
  68. optiwindnet-0.2.2/tests/test_end_to_end.py +0 -106
  69. optiwindnet-0.2.2/tests/test_svg.py +0 -212
  70. optiwindnet-0.2.2/tests/test_themes.py +0 -8
  71. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/.github/workflows/release.yml +0 -0
  72. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/.zenodo.json +0 -0
  73. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/CITATION.cff +0 -0
  74. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/LICENSE.txt +0 -0
  75. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/README.md +0 -0
  76. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/cplex.py +0 -0
  77. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/MILP/fscip.py +0 -0
  78. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/__init__.py +0 -0
  79. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/baselines/__init__.py +0 -0
  80. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Albatros.osm.pbf +0 -0
  81. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Amrumbank West.osm.pbf +0 -0
  82. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Anholt.yaml +0 -0
  83. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Arkona.osm.pbf +0 -0
  84. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/BARD Offshore 1.osm.pbf +0 -0
  85. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Baltic 2.osm.pbf +0 -0
  86. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Baltic Eagle.osm.pbf +0 -0
  87. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Beatrice.osm.pbf +0 -0
  88. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Belwind.osm.pbf +0 -0
  89. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Borkum Riffgrund 1.yaml +0 -0
  90. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Borkum Riffgrund 2.yaml +0 -0
  91. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Borkum Riffgrund 3.osm.pbf +0 -0
  92. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Borssele.yaml +0 -0
  93. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Butendiek.yaml +0 -0
  94. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/CECEP Yangjiang Nanpeng Island.osm.pbf +0 -0
  95. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/CGN Rudong Demonstration.osm.pbf +0 -0
  96. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Cazzaro-2022.yaml +0 -0
  97. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Cazzaro-2022G-140.yaml +0 -0
  98. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Cazzaro-2022G-210.yaml +0 -0
  99. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Coastal Virginia.osm.pbf +0 -0
  100. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/DanTysk.yaml +0 -0
  101. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Deutsche Bucht.osm.pbf +0 -0
  102. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Dogger Bank A.yaml +0 -0
  103. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Dogger Bank B.osm.pbf +0 -0
  104. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Dogger Bank C.osm.pbf +0 -0
  105. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Dudgeon.yaml +0 -0
  106. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/East Anglia ONE.yaml +0 -0
  107. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Fecamp.osm.pbf +0 -0
  108. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Galloper Inner.osm.pbf +0 -0
  109. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Gemini 1.osm.pbf +0 -0
  110. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Gemini 2.osm.pbf +0 -0
  111. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Global Tech 1.osm.pbf +0 -0
  112. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Gode Wind 1.yaml +0 -0
  113. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Greater Changhua 1.osm.pbf +0 -0
  114. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Greater Gabbard Inner.yaml +0 -0
  115. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Gwynt y Mor.yaml +0 -0
  116. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/He Dreiht.osm.pbf +0 -0
  117. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Hohe See.osm.pbf +0 -0
  118. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Hollandse Kust Zuid.osm.pbf +0 -0
  119. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Horns Rev 1.yaml +0 -0
  120. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Horns Rev 2.yaml +0 -0
  121. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Horns Rev 3.yaml +0 -0
  122. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Hornsea One.yaml +0 -0
  123. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Hornsea Two West.yaml +0 -0
  124. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Huizhou Gangkou 1.osm.pbf +0 -0
  125. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Huizhou Gangkou 2.osm.pbf +0 -0
  126. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Humber Gateway.osm.pbf +0 -0
  127. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Inch Cape.osm.pbf +0 -0
  128. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Kaskasi.osm.pbf +0 -0
  129. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Kriegers Flak A.osm.pbf +0 -0
  130. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Kriegers Flak B.osm.pbf +0 -0
  131. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Laoting Bodhi Island.osm.pbf +0 -0
  132. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Lillgrund.osm.pbf +0 -0
  133. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Lincs.osm.pbf +0 -0
  134. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/London Array.yaml +0 -0
  135. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Luchterduinen.osm.pbf +0 -0
  136. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Meerwind.osm.pbf +0 -0
  137. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Merkur.osm.pbf +0 -0
  138. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Mermaid.osm.pbf +0 -0
  139. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Moray East.yaml +0 -0
  140. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Moray West.yaml +0 -0
  141. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Neart na Gaoithe.osm.pbf +0 -0
  142. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Noirmoutier.osm.pbf +0 -0
  143. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Nordsee One.osm.pbf +0 -0
  144. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Nordsee Ost.osm.pbf +0 -0
  145. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Norther.osm.pbf +0 -0
  146. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Northwind.osm.pbf +0 -0
  147. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Nysted.osm.pbf +0 -0
  148. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Ormonde.yaml +0 -0
  149. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Princess Amalia.osm.pbf +0 -0
  150. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Race Bank.yaml +0 -0
  151. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Rampion.yaml +0 -0
  152. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Rentel.osm.pbf +0 -0
  153. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Riffgat.osm.pbf +0 -0
  154. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Robin Rigg.osm.pbf +0 -0
  155. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Rudong H10.osm.pbf +0 -0
  156. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Rudong H6.osm.pbf +0 -0
  157. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Rudong H8.osm.pbf +0 -0
  158. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/R/303/270dsand 2.yaml" +0 -0
  159. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/SPIC Binhai North H2.osm.pbf +0 -0
  160. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Saint-Brieuc.osm.pbf +0 -0
  161. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Saint-Nazaire.osm.pbf +0 -0
  162. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Sandbank.osm.pbf +0 -0
  163. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Seagreen.osm.pbf +0 -0
  164. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Shengsi 2.osm.pbf +0 -0
  165. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Sheringham Shoal.osm.pbf +0 -0
  166. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Sofia.yaml +0 -0
  167. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Taylor-2023.yaml +0 -0
  168. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Thanet.yaml +0 -0
  169. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Thor.osm.pbf +0 -0
  170. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Trianel Windpark Borkum.osm.pbf +0 -0
  171. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Triton Knoll.yaml +0 -0
  172. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Tr/303/251port.osm.pbf" +0 -0
  173. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Veja Mate.osm.pbf +0 -0
  174. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Vineyard Wind 1.osm.pbf +0 -0
  175. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Walney 1.yaml +0 -0
  176. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Walney 2.yaml +0 -0
  177. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Walney Extension.yaml +0 -0
  178. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/West of Duddon Sands.yaml +0 -0
  179. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Westermost Rough.osm.pbf +0 -0
  180. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Wikinger.osm.pbf +0 -0
  181. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Yi-2019.yaml +0 -0
  182. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Yunlin.osm.pbf +0 -0
  183. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/data/Zhejiang Jiaxing 1.osm.pbf +0 -0
  184. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/db/__init__.py +0 -0
  185. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/db/_core.py +0 -0
  186. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/db/model.pyi +0 -0
  187. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/ClassicEsauWilliams.py +0 -0
  188. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/CrossingPreventingEW.py +0 -0
  189. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/EW_presolver.py +0 -0
  190. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/NonBranchingEW.py +0 -0
  191. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/ObstacleBypassingEW.py +0 -0
  192. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/__init__.py +0 -0
  193. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/heuristics/_deprecation.py +0 -0
  194. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/interface.py +0 -0
  195. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/py.typed +0 -0
  196. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet/themes.py +0 -0
  197. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet.egg-info/dependency_links.txt +0 -0
  198. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet.egg-info/requires.txt +0 -0
  199. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/optiwindnet.egg-info/top_level.txt +0 -0
  200. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/setup.cfg +0 -0
  201. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/sitecustomize.py +0 -0
  202. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/__init__.py +0 -0
  203. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/locations/example_1ss_300wt.osm.pbf +0 -0
  204. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/locations/example_4ss_300wt.osm.pbf +0 -0
  205. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/locations/example_location.yaml +0 -0
  206. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/solutions.pkl +0 -0
  207. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_augmentation.py +0 -0
  208. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_constructor.py +0 -0
  209. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_crossings.py +0 -0
  210. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_db.py +0 -0
  211. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_lkh.py +0 -0
  212. {optiwindnet-0.2.2 → optiwindnet-0.2.3}/tests/test_pathfinding.py +0 -0
@@ -1,3 +1,40 @@
1
+ # v0.2.3
2
+
3
+ [Commit history since v0.2.2](https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/-/compare/v0.2.2...v0.2.3)
4
+
5
+ Drop-in replacement for v0.2.2. The APIs deprecated in v0.2.2 are still available and still emit `DeprecationWarning`; they are scheduled for removal in v0.3.
6
+
7
+ ## Important Changes
8
+ - **Balanced Subtrees Actually Balanced (MILP and HGS)**: `balanced=True` promises subtree loads differing at most by one unit, but both solver families only bounded those loads from below, letting a subtree grow up to `capacity`. This is now fixed consistently on both sides:
9
+ - *MILP*: the upper bound `ceil(T / feeders)` is now enforced across the pyomo, SCIP and OR-Tools model builders.
10
+ - *HGS*: `hgs_cvrp()` used to request more slack nodes than there were routes whenever `T < feeders * (capacity - 1)`, forcing the surplus through clipped `inf` arcs and inflating the reported objective. The balanced sub-problem is now solved at `capacity_effective = ceil(T / feeders)`, reported in `solver_details`.
11
+ - **Exact Feeder Count (MILP and HGS)**: a new, mirrored way to pin the feeder count instead of only bounding it from above. Because `balanced` is only expressible once the feeder count is pinned, this makes balanced solutions reachable above the minimum feeder count.
12
+ - *MILP*: `feeder_limit="exactly"` pins the feeder count to `max_feeders` (whereas `"specified"` remains an upper bound).
13
+ - *HGS*: `hgs_cvrp(vehicles=F, vehicles_exact=True)` — exposed as `HGSRouter(feeder_limit=F, feeder_exact=True)` — pins the feeder count to `F`, whereas `vehicles` alone remains an upper bound that HGS-CVRP normally undershoots. It currently requires `balanced=True` and a single substation; `F` must lie between `ceil(T / capacity)` and `T`.
14
+ - **Reversible lat/lon Coordinates**: `L_from_yaml()` and `L_from_pbf()` now project all coordinates into the single UTM zone holding the most turbines (instead of the zone of the first point), minimizing distortion for the bulk of the layout, and retain that zone as the graph attributes `utm_zone_number` and `utm_zone_letter`. This makes `VertexC` reversible back to lat/lon via `utm.to_latlon()`. Multi-zone `.yaml` input no longer raises an assertion.
15
+ - **Tunable `EWRouter`**: the `method` and `bias_margin` parameters of `heuristics.constructor()` are now exposed on `EWRouter`, giving access to the `esau_williams`, `biased_EW`, `rootlust` and `radial_EW` methods from the high-level API.
16
+ - **Concurrent HiGHS**: the pyomo HiGHS solver now runs a concurrent branch-and-bound tree search, in line with the other MILP backends. This speedup requires `highspy` v1.15 or newer; older versions accept the setting but run the search serially.
17
+ - **New Locations**: added Revolution, Sunrise, Hornsea 2, Norfolk Vanguard West, East Anglia 3 and Hollandse Kust Noord.
18
+
19
+ ## Bug Fixes
20
+ - **Multi-Root Warmstart Eligibility**: `is_warmstart_eligible()` compared only the first root's feeder count against the feeder limit, while the model constrains the total across all roots.
21
+ - **`PathFinder` Malformed Chain**: two spanning fences of the same subtree meeting at a single chain-end vertex form a dead-end pocket, which could leave a chain short an access cone. Chain detection is now keyed on the subtree alone within a chain-end vertex.
22
+ - **`scaffolded()` Correctness**: fence hops and shortened-contour hops are now converted to primed edges (in both `PathFinder.scaffolded()` and `interarraylib.scaffolded()`), and a supertriangle/clone id collision was fixed.
23
+ - **Sexagesimal Coordinate Parsing**: `_translate_latlonstr()` failed to reset minutes/seconds between coordinates, corrupting the parsed values.
24
+
25
+ ## Deprecated
26
+ - `WindFarmNetwork.from_yaml()` is renamed to `WindFarmNetwork.from_own_yaml()`; the old name still works and emits a `DeprecationWarning`.
27
+ - Reminder — the following remain available in this release and will be removed in v0.3. Users are advised to migrate now:
28
+ - Standalone EW heuristics (`ClassicEW`, `CPEW`, `NBEW`, `OBEW`, `EW_presolver`) → `heuristics.constructor()` (or the high-level `WindFarmNetwork`/`EWRouter`). See the [Legacy heuristics migration guide](https://optiwindnet.readthedocs.io/stable/notebooks/14-Legacy_heuristics.html), which pairs each legacy call with its `constructor()` equivalent.
29
+ - `optiwindnet.interface` (`heuristic_wrapper()`, `HeuristicFactory`) → `WindFarmNetwork`/`EWRouter`.
30
+ - HGS aliases `hgs_multiroot()` / `iterative_hgs_cvrp()` → `hgs_cvrp()`.
31
+ - LKH entry points `lkh()` / `iterative_lkh()` → `lkh3()`.
32
+
33
+ ## Refactoring & Maintenance
34
+ - **Docstrings**: project-wide docstring formatting pass — standardized markup of string values for Sphinx rendering, unicode arrows, and docstrings for the database model.
35
+ - **Test Coverage**: expanded coverage for `geometric`, `interarraylib`, `plotting`, `svg`, `repair`, `themes` and `baselines.utils`.
36
+ - **CI**: isolated OR-Tools in a shared subprocess to resolve solver DLL conflicts, switched SCIP download to GitHub, bumped SCIPOptSuite, installed python tooling from conda, and added manual-dispatch pipeline targets.
37
+
1
38
  # v0.2.2
2
39
 
3
40
  [Commit history since v0.2.1](https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/-/compare/v0.2.1...v0.2.2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optiwindnet
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Tool for designing and optimizing the electrical cable network of offshore wind farms
5
5
  Author-email: DTU Wind Energy <mikf@dtu.dk>, Mauricio Souza de Alencar <ma___@dtu.dk>, Amir Arasteh <am___@dtu.dk>
6
6
  License-Expression: MIT
@@ -33,14 +33,15 @@ __all__ = (
33
33
  def solver_factory(solver_name: str) -> Solver:
34
34
  """Create a Solver object tied to the specified external MILP solver.
35
35
 
36
- Note that the only solver that is a dependency of OptiWindNet is 'ortools'.
36
+ Note that the only solver that is a dependency of OptiWindNet is ``'ortools'``.
37
37
  Check OptiWindNet's documentation on how to install optional solvers.
38
38
 
39
- Legacy compatibility: if solver_name == 'ortools' then the CP-SAT backend is used.
39
+ Legacy compatibility: if ``solver_name == 'ortools'`` then the CP-SAT backend
40
+ is used.
40
41
 
41
42
  Args:
42
- solver_name: one of 'ortools.cp_sat', 'ortools.gscip',
43
- 'ortools.highs', 'cplex', 'gurobi', 'cbc', 'scip', 'highs'.
43
+ solver_name: one of ``'ortools.cp_sat'``, ``'ortools.gscip'``,
44
+ ``'ortools.highs'``, ``'cplex'``, ``'gurobi'``, ``'cbc'``, ``'scip'``, ``'highs'``.
44
45
 
45
46
  Returns:
46
47
  Solver instance that can produce solutions for the cable routing problem.
@@ -3,13 +3,16 @@
3
3
 
4
4
  import abc
5
5
  import logging
6
+ import math
6
7
  import os
7
8
  import sys
8
9
  from collections.abc import Mapping
9
10
  from dataclasses import asdict, dataclass, field
10
11
  from enum import StrEnum, auto
12
+ from inspect import cleandoc
11
13
  from itertools import chain
12
14
  from pathlib import Path
15
+ from textwrap import indent
13
16
  from typing import Any
14
17
 
15
18
  import networkx as nx
@@ -19,7 +22,7 @@ from ..interarraylib import G_from_S
19
22
  from ..pathfinding import PathFinder
20
23
 
21
24
  _lggr = logging.getLogger(__name__)
22
- error, info = _lggr.error, _lggr.info
25
+ error, info, warn = _lggr.error, _lggr.info, _lggr.warning
23
26
 
24
27
 
25
28
  def physical_core_count() -> int:
@@ -76,7 +79,7 @@ class Topology(StrEnum):
76
79
 
77
80
 
78
81
  class FeederRoute(StrEnum):
79
- 'If feeder routes must be "straight" or can be detoured ("segmented").'
82
+ "If feeder routes must be ``'straight'`` or can be detoured (``'segmented'``)."
80
83
 
81
84
  STRAIGHT = auto()
82
85
  SEGMENTED = auto()
@@ -84,11 +87,15 @@ class FeederRoute(StrEnum):
84
87
 
85
88
 
86
89
  class FeederLimit(StrEnum):
87
- 'Whether to limit the maximum number of feeders, if set to "specified",'
88
-
89
- ' additional kwarg "max_feeders" must be given.'
90
+ """Whether to limit the number of feeders. Both ``'specified'`` (an upper
91
+ bound) and ``'exactly'`` (an exact count) require the additional kwarg
92
+ ``'max_feeders'``. Option ``'balanced'`` is only enforceable if the feeder
93
+ count is pinned to a single value, i.e. ``'minimum'``, ``'exactly'``, or
94
+ ``'specified'`` with ``'max_feeders'`` at the minimum.
95
+ """
90
96
 
91
97
  UNLIMITED = auto()
98
+ EXACTLY = auto()
92
99
  SPECIFIED = auto()
93
100
  MINIMUM = auto()
94
101
  MIN_PLUS1 = auto()
@@ -97,6 +104,74 @@ class FeederLimit(StrEnum):
97
104
  DEFAULT = UNLIMITED
98
105
 
99
106
 
107
+ def feeder_and_load_bounds(
108
+ T: int,
109
+ capacity: int,
110
+ feeder_limit: FeederLimit,
111
+ max_feeders: int,
112
+ balanced: bool,
113
+ ) -> tuple[int, int | None, int | None, int | None]:
114
+ """Derive the feeder-count and feeder-load bounds a model must enforce.
115
+
116
+ The feeder count is bounded below by ``min_feeders = ceil(T/capacity)``
117
+ regardless of ``feeder_limit`` (a valid inequality). ``feeders_ub`` is
118
+ ``None`` when the count is unbounded above; when it equals ``feeders_lb``,
119
+ the count is pinned and callers should emit an equality constraint.
120
+
121
+ Balanced subtrees (loads differing at most by one unit) are only expressible
122
+ with a pinned feeder count ``F``, in which case the loads must lie in
123
+ ``{T // F, ceil(T / F)}``. A load bound of ``None`` means "do not emit":
124
+ either ``balanced`` is off, or it is not enforceable (a warning is issued),
125
+ or the bound is already implied by the flow variable's own bounds.
126
+
127
+ Returns:
128
+ ``(feeders_lb, feeders_ub, load_lb, load_ub)``
129
+ """
130
+ min_feeders = math.ceil(T / capacity)
131
+ if feeder_limit is FeederLimit.UNLIMITED:
132
+ feeders_lb, feeders_ub = min_feeders, None
133
+ elif feeder_limit is FeederLimit.MINIMUM:
134
+ feeders_lb = feeders_ub = min_feeders
135
+ elif feeder_limit is FeederLimit.EXACTLY:
136
+ if max_feeders < min_feeders:
137
+ raise ValueError('max_feeders is below the minimum necessary')
138
+ if max_feeders > T:
139
+ raise ValueError('max_feeders is above the number of terminals')
140
+ feeders_lb = feeders_ub = max_feeders
141
+ elif feeder_limit is FeederLimit.SPECIFIED:
142
+ if max_feeders < min_feeders:
143
+ raise ValueError('max_feeders is below the minimum necessary')
144
+ feeders_lb, feeders_ub = min_feeders, max_feeders
145
+ elif feeder_limit in (
146
+ FeederLimit.MIN_PLUS1,
147
+ FeederLimit.MIN_PLUS2,
148
+ FeederLimit.MIN_PLUS3,
149
+ ):
150
+ plus = int(feeder_limit.value[-1])
151
+ feeders_lb, feeders_ub = min_feeders, min_feeders + plus
152
+ else:
153
+ raise NotImplementedError('Unknown value:', feeder_limit)
154
+
155
+ if not balanced:
156
+ return feeders_lb, feeders_ub, None, None
157
+ if feeders_lb != feeders_ub:
158
+ warn(
159
+ 'Model option <balanced = True> requires a single possible feeder'
160
+ f' count, but <feeder_limit = {feeder_limit.value.upper()}> allows a'
161
+ ' range: model will not enforce balanced subtrees.'
162
+ )
163
+ return feeders_lb, feeders_ub, None, None
164
+ F = feeders_lb
165
+ load_lb, load_ub = T // F, math.ceil(T / F)
166
+ # bounds at the extremes are already implied by the flow variable's bounds
167
+ return (
168
+ feeders_lb,
169
+ feeders_ub,
170
+ load_lb if load_lb > 1 else None,
171
+ load_ub if load_ub < capacity else None,
172
+ )
173
+
174
+
100
175
  class ModelOptions(dict):
101
176
  """Hold options for the modelling of the cable routing problem.
102
177
 
@@ -119,7 +194,8 @@ class ModelOptions(dict):
119
194
  max_feeders=(
120
195
  int,
121
196
  0,
122
- 'Maximum number of feeders (used only if <feeder_limit = "specified">)',
197
+ 'Number of feeders: the maximum if <feeder_limit = "specified">, '
198
+ 'the exact count if <feeder_limit = "exactly">',
123
199
  ),
124
200
  )
125
201
 
@@ -149,13 +225,14 @@ class ModelOptions(dict):
149
225
  @classmethod
150
226
  def help(cls):
151
227
  for k, v in cls.hints.items():
228
+ doc = indent(cleandoc(v.__doc__ or ''), ' ')
152
229
  print(
153
230
  f'{k} in {{'
154
231
  + ', '.join(
155
232
  f'"{m}"' for n, m in v.__members__.items() if n != 'DEFAULT'
156
233
  )
157
234
  + f'}} default: {cls.hints[k].DEFAULT.value}\n'
158
- f' {v.__doc__}\n'
235
+ f'{doc}\n'
159
236
  )
160
237
  for name, (kind, default, desc) in cls.simple.items():
161
238
  print(f'{name} [{kind.__name__}] default: {default}\n {desc}\n')
@@ -242,11 +319,11 @@ class Solver(abc.ABC):
242
319
  Args:
243
320
  time_limit: maximum time (s) the solver is allowed to run.
244
321
  mip_gap: relative difference from incumbent solution to lower bound
245
- at which the search may be stopped before time_limit is reached.
322
+ at which the search may be stopped before ``time_limit`` is reached.
246
323
  options: additional options to pass to solver (see solver manual).
247
324
 
248
325
  Returns:
249
- General information about the solution search (use get_solution() for
326
+ General information about the solution search (use ``get_solution()`` for
250
327
  the actual solution).
251
328
  """
252
329
  pass
@@ -289,7 +366,7 @@ class Solver(abc.ABC):
289
366
  """Create a topology graph from the solution to the MILP model.
290
367
 
291
368
  Returns:
292
- Graph topology `S` from the solution.
369
+ Graph topology ``S`` from the solution.
293
370
  """
294
371
  metadata = self.metadata
295
372
  S = nx.Graph(R=metadata.R, T=metadata.T)
@@ -342,7 +419,7 @@ class PoolHandler(abc.ABC):
342
419
 
343
420
  @abc.abstractmethod
344
421
  def _objective_at(self, index: int) -> float:
345
- "Get objective value from solution pool at position `index`"
422
+ "Get objective value from solution pool at position ``index``"
346
423
  pass
347
424
 
348
425
  @abc.abstractmethod
@@ -11,6 +11,13 @@ from typing import Any
11
11
  import networkx as nx
12
12
 
13
13
  def physical_core_count() -> int: ...
14
+ def feeder_and_load_bounds(
15
+ T: int,
16
+ capacity: int,
17
+ feeder_limit: FeederLimit,
18
+ max_feeders: int,
19
+ balanced: bool,
20
+ ) -> tuple[int, int | None, int | None, int | None]: ...
14
21
 
15
22
  class OWNWarmupFailed(Exception): ...
16
23
  class OWNSolutionNotFound(Exception): ...
@@ -27,6 +34,7 @@ class FeederRoute(StrEnum):
27
34
 
28
35
  class FeederLimit(StrEnum):
29
36
  UNLIMITED: str
37
+ EXACTLY: str
30
38
  SPECIFIED: str
31
39
  MINIMUM: str
32
40
  MIN_PLUS1: str
@@ -28,10 +28,11 @@ error, info = _lggr.error, _lggr.info
28
28
 
29
29
  class SolverGurobi(SolverPyomo, PoolHandler):
30
30
  """
31
- For setting `threadlimit`, the SolverGurobi instance must have this option in
32
- its `options` attribute before the call to .set_problem().
31
+ For setting ``threadlimit``, the SolverGurobi instance must have this option in
32
+ its ``options`` attribute before the call to :meth:`set_problem`.
33
+
34
+ Example::
33
35
 
34
- Example:
35
36
  solver.options.update(threadlimit=8)
36
37
  solver.set_problem(...)
37
38
  solver.solve(...)
@@ -62,7 +63,7 @@ class SolverGurobi(SolverPyomo, PoolHandler):
62
63
  warmstart: nx.Graph | None = None,
63
64
  ):
64
65
  """
65
- This will keep the Gurobi license in use until a call to `get_solution()`.
66
+ This will keep the Gurobi license in use until a call to :meth:`get_solution`.
66
67
  """
67
68
  solver = pyo.SolverFactory(
68
69
  'gurobi_persistent',
@@ -2,7 +2,6 @@
2
2
  # https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/
3
3
 
4
4
  import logging
5
- import math
6
5
  from datetime import timedelta
7
6
  from itertools import chain
8
7
  from typing import Any
@@ -24,6 +23,7 @@ from ._core import (
24
23
  SolutionInfo,
25
24
  Solver,
26
25
  Topology,
26
+ feeder_and_load_bounds,
27
27
  physical_core_count,
28
28
  )
29
29
 
@@ -72,7 +72,8 @@ class SolverORTools(Solver, PoolHandler):
72
72
  """OR-Tools MathOpt wrapper using the selected backend.
73
73
 
74
74
  This class wraps and changes the behavior of MathOpt in order to save all
75
- solutions found to a pool. Meant to be used with `investigate_pool()`.
75
+ solutions found to a pool. Meant to be used with
76
+ :meth:`.PoolHandler._investigate_pool`.
76
77
  """
77
78
 
78
79
  name: str
@@ -120,8 +121,8 @@ class SolverORTools(Solver, PoolHandler):
120
121
  ) -> SolutionInfo:
121
122
  """Wrapper for MathOpt solve() that saves all solutions.
122
123
 
123
- This method uses a MIP_SOLUTION callback to fill a solution pool stored
124
- in the attribute self.solutions.
124
+ This method uses a ``MIP_SOLUTION`` callback to fill a solution pool stored
125
+ in the attribute ``self.solutions``.
125
126
  """
126
127
  try:
127
128
  model = self.model
@@ -315,13 +316,16 @@ def make_min_length_model(
315
316
  Args:
316
317
  A: graph with the available edges to choose from
317
318
  capacity: maximum link flow capacity
318
- topology: one of Topology.{BRANCHED, RADIAL}
319
+ topology: one of ``Topology.{BRANCHED, RADIAL}``
319
320
  feeder_route:
320
- FeederRoute.SEGMENTED -> feeder routes may be detoured around subtrees;
321
- FeederRoute.STRAIGHT -> feeder routes must be straight, direct lines
322
- feeder_limit: one of FeederLimit.{MINIMUM, UNLIMITED, SPECIFIED,
323
- MIN_PLUS1, MIN_PLUS2, MIN_PLUS3}
324
- max_feeders: only used if feeder_limit is FeederLimit.SPECIFIED
321
+ ``FeederRoute.SEGMENTED`` feeder routes may be detoured around subtrees;
322
+ ``FeederRoute.STRAIGHT`` feeder routes must be straight, direct lines
323
+ feeder_limit: one of ``FeederLimit.{MINIMUM, UNLIMITED, EXACTLY, SPECIFIED,
324
+ MIN_PLUS1, MIN_PLUS2, MIN_PLUS3}``
325
+ balanced: enforce subtree loads differing at most by one unit (only
326
+ possible if ``feeder_limit`` pins the feeder count to a single value)
327
+ max_feeders: upper bound if ``feeder_limit`` is ``FeederLimit.SPECIFIED``,
328
+ exact count if it is ``FeederLimit.EXACTLY``, unused otherwise
325
329
  """
326
330
  R = A.graph['R']
327
331
  T = A.graph['T']
@@ -429,62 +433,41 @@ def make_min_length_model(
429
433
  )
430
434
 
431
435
  # feeder limits
432
- min_feeders = math.ceil(T / k)
436
+ feeders_lb, feeders_ub, load_lb, load_ub = feeder_and_load_bounds(
437
+ T, k, feeder_limit, max_feeders, balanced
438
+ )
439
+ if feeders_ub is not None and feeder_limit.name.startswith('MIN_PLUS'):
440
+ # derived from the minimum: surface it in the solution's metadata
441
+ max_feeders = feeders_ub
433
442
  all_feeder_vars_sum = sum(link_[t, r] for r in _R for t in _T)
434
- if feeder_limit is FeederLimit.UNLIMITED:
443
+ if feeders_lb == feeders_ub:
444
+ m.add_linear_constraint(
445
+ all_feeder_vars_sum == feeders_lb, name='feeder_limit_eq'
446
+ )
447
+ elif feeders_ub is None:
435
448
  # valid inequality: number of feeders is at least the minimum
436
449
  m.add_linear_constraint(
437
- all_feeder_vars_sum >= min_feeders, name='feeder_limit_lb'
450
+ all_feeder_vars_sum >= feeders_lb, name='feeder_limit_lb'
438
451
  )
439
- if balanced:
440
- warn(
441
- 'Model option <balanced = True> is incompatible with <feeder_limit'
442
- ' = UNLIMITED>: model will not enforce balanced subtrees.'
443
- )
444
452
  else:
445
- is_equal_not_range = False
446
- if feeder_limit is FeederLimit.SPECIFIED:
447
- if max_feeders == min_feeders:
448
- is_equal_not_range = True
449
- elif max_feeders < min_feeders:
450
- raise ValueError('max_feeders is below the minimum necessary')
451
- elif feeder_limit is FeederLimit.MINIMUM:
452
- is_equal_not_range = True
453
- elif feeder_limit is FeederLimit.MIN_PLUS1:
454
- max_feeders = min_feeders + 1
455
- elif feeder_limit is FeederLimit.MIN_PLUS2:
456
- max_feeders = min_feeders + 2
457
- elif feeder_limit is FeederLimit.MIN_PLUS3:
458
- max_feeders = min_feeders + 3
459
- else:
460
- raise NotImplementedError('Unknown value:', feeder_limit)
461
- if is_equal_not_range:
453
+ m.add_linear_constraint(
454
+ expr=all_feeder_vars_sum,
455
+ lb=feeders_lb,
456
+ ub=feeders_ub,
457
+ name='feeder_limit_interval',
458
+ )
459
+
460
+ # enforce balanced subtrees (subtree loads differ at most by one unit)
461
+ if load_lb is not None:
462
+ for t, r in stars:
462
463
  m.add_linear_constraint(
463
- all_feeder_vars_sum == min_feeders, name='feeder_limit_eq'
464
+ flow_[t, r] >= link_[t, r] * load_lb, name=f'balanced_lb_{t}~r{-r}'
464
465
  )
465
- else:
466
+ if load_ub is not None:
467
+ for t, r in stars:
466
468
  m.add_linear_constraint(
467
- expr=all_feeder_vars_sum,
468
- lb=min_feeders,
469
- ub=max_feeders,
470
- name='feeder_limit_interval',
469
+ flow_[t, r] <= link_[t, r] * load_ub, name=f'balanced_ub_{t}~r{-r}'
471
470
  )
472
- # enforce balanced subtrees (subtree loads differ at most by one unit)
473
- if balanced:
474
- if is_equal_not_range:
475
- feeder_min_load = T // min_feeders
476
- if feeder_min_load < capacity:
477
- for t, r in stars:
478
- m.add_linear_constraint(
479
- flow_[t, r] >= link_[t, r] * feeder_min_load,
480
- name=f'balanced_{t}~r{-r}',
481
- )
482
- else:
483
- warn(
484
- 'Model option <balanced = True> is incompatible with '
485
- 'having a range of possible feeder counts: model will '
486
- 'not enforce balanced subtrees.'
487
- )
488
471
 
489
472
  # radial or branched topology
490
473
  if topology is Topology.RADIAL:
@@ -552,9 +535,9 @@ _make_min_length_model_fingerprint = fun_fingerprint(make_min_length_model)
552
535
  def warmup_model(
553
536
  model: mathopt.Model, metadata: ModelMetadata, S: nx.Graph
554
537
  ) -> mathopt.Model:
555
- """Set initial solution into `model`.
538
+ """Set initial solution into ``model``.
556
539
 
557
- Changes `model` and `metadata` in-place.
540
+ Changes ``model`` and ``metadata`` in-place.
558
541
 
559
542
  Args:
560
543
  model: CP-SAT model to apply the solution to.
@@ -2,7 +2,6 @@
2
2
  # https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/
3
3
 
4
4
  import logging
5
- import math
6
5
  from collections import namedtuple
7
6
  from itertools import chain
8
7
  from typing import Any
@@ -24,6 +23,7 @@ from ._core import (
24
23
  SolutionInfo,
25
24
  Solver,
26
25
  Topology,
26
+ feeder_and_load_bounds,
27
27
  physical_core_count,
28
28
  )
29
29
 
@@ -68,7 +68,10 @@ _default_options = dict(
68
68
  liftAndProjectCuts='off',
69
69
  residualCapacityCuts='off',
70
70
  ),
71
- highs={},
71
+ highs=dict(
72
+ parallel='on',
73
+ # threads=0, # 0 means automatic and is HiGHS's default
74
+ ),
72
75
  scip={},
73
76
  )
74
77
 
@@ -223,12 +226,17 @@ class SolverPyomoAppsi(Solver):
223
226
  exc.args += ('.set_problem() must be called before .solve()',)
224
227
  raise
225
228
  applied_options = self.options | options
229
+ for key, value in applied_options.items():
230
+ if key in solver.config:
231
+ solver.config[key] = value
232
+ else:
233
+ solver._solver_options[key] = value
226
234
  stopping = {
227
235
  _optkey[name].time_limit: time_limit,
228
236
  _optkey[name].mip_gap: mip_gap,
229
237
  }
230
238
  self.stopping = stopping
231
- for k, v in (applied_options | stopping).items():
239
+ for k, v in stopping.items():
232
240
  solver.config[k] = v
233
241
  solver.config.load_solution = False
234
242
  solver.config.stream_solver = verbose
@@ -290,13 +298,16 @@ def make_min_length_model(
290
298
  Args:
291
299
  A: graph with the available edges to choose from
292
300
  capacity: maximum link flow capacity
293
- topology: one of Topology.{BRANCHED, RADIAL}
301
+ topology: one of ``Topology.{BRANCHED, RADIAL}``
294
302
  feeder_route:
295
- FeederRoute.SEGMENTED -> feeder routes may be detoured around subtrees;
296
- FeederRoute.STRAIGHT -> feeder routes must be straight, direct lines
297
- feeder_limit: one of FeederLimit.{MINIMUM, UNLIMITED, SPECIFIED,
298
- MIN_PLUS1, MIN_PLUS2, MIN_PLUS3}
299
- max_feeders: only used if feeder_limit is FeederLimit.SPECIFIED
303
+ ``FeederRoute.SEGMENTED`` feeder routes may be detoured around subtrees;
304
+ ``FeederRoute.STRAIGHT`` feeder routes must be straight, direct lines
305
+ feeder_limit: one of ``FeederLimit.{MINIMUM, UNLIMITED, EXACTLY, SPECIFIED,
306
+ MIN_PLUS1, MIN_PLUS2, MIN_PLUS3}``
307
+ balanced: enforce subtree loads differing at most by one unit (only
308
+ possible if ``feeder_limit`` pins the feeder count to a single value)
309
+ max_feeders: upper bound if ``feeder_limit`` is ``FeederLimit.SPECIFIED``,
310
+ exact count if it is ``FeederLimit.EXACTLY``, unused otherwise
300
311
  """
301
312
  R = A.graph['R']
302
313
  T = A.graph['T']
@@ -431,76 +442,46 @@ def make_min_length_model(
431
442
  )
432
443
 
433
444
  # feeder limits
434
- min_feeders = math.ceil(T / m.k)
435
- if feeder_limit is FeederLimit.UNLIMITED:
445
+ feeders_lb, feeders_ub, load_lb, load_ub = feeder_and_load_bounds(
446
+ T, capacity, feeder_limit, max_feeders, balanced
447
+ )
448
+ if feeders_ub is not None and feeder_limit.name.startswith('MIN_PLUS'):
449
+ # derived from the minimum: surface it in the solution's metadata
450
+ max_feeders = feeders_ub
451
+ if feeders_lb == feeders_ub:
452
+ m.cons_feeder_limit_eq = pyo.Constraint(
453
+ rule=(lambda m: sum(m.link_[t, r] for r in _R for t in _T) == feeders_lb),
454
+ name='feeder_limit_eq',
455
+ )
456
+ else:
436
457
  # valid inequality: number of feeders is at least the minimum
437
458
  m.cons_feeder_limit_lb = pyo.Constraint(
438
- rule=(lambda m: sum(m.link_[t, r] for r in _R for t in _T) >= min_feeders),
459
+ rule=(lambda m: sum(m.link_[t, r] for r in _R for t in _T) >= feeders_lb),
439
460
  name='feeder_limit_lb',
440
461
  )
441
- if balanced:
442
- warn(
443
- 'Model option <balanced = True> is incompatible with <feeder_limit'
444
- ' = UNLIMITED>: model will not enforce balanced subtrees.'
445
- )
446
- else:
447
- is_equal_not_range = False
448
- if feeder_limit is FeederLimit.SPECIFIED:
449
- if max_feeders == min_feeders:
450
- is_equal_not_range = True
451
- elif max_feeders < min_feeders:
452
- raise ValueError('max_feeders is below the minimum necessary')
453
- elif feeder_limit is FeederLimit.MINIMUM:
454
- is_equal_not_range = True
455
- elif feeder_limit is FeederLimit.MIN_PLUS1:
456
- max_feeders = min_feeders + 1
457
- elif feeder_limit is FeederLimit.MIN_PLUS2:
458
- max_feeders = min_feeders + 2
459
- elif feeder_limit is FeederLimit.MIN_PLUS3:
460
- max_feeders = min_feeders + 3
461
- else:
462
- raise NotImplementedError('Unknown value:', feeder_limit)
463
- if is_equal_not_range:
464
- m.cons_feeder_limit_eq = pyo.Constraint(
465
- rule=(
466
- lambda m: sum(m.link_[t, r] for r in _R for t in _T) == min_feeders
467
- ),
468
- name='feeder_limit_eq',
469
- )
470
- else:
471
- m.cons_feeder_limit_lb = pyo.Constraint(
472
- rule=(
473
- lambda m: sum(m.link_[t, r] for r in _R for t in _T) >= min_feeders
474
- ),
475
- name='feeder_limit_lb',
476
- )
462
+ if feeders_ub is not None:
477
463
  m.cons_feeder_limit_ub = pyo.Constraint(
478
464
  rule=(
479
- lambda m: sum(m.link_[t, r] for r in _R for t in _T) <= max_feeders
465
+ lambda m: sum(m.link_[t, r] for r in _R for t in _T) <= feeders_ub
480
466
  ),
481
467
  name='feeder_limit_ub',
482
468
  )
483
- # enforce balanced subtrees (subtree loads differ at most by one unit)
484
- if balanced:
485
- if is_equal_not_range:
486
- feeder_min_load = T // min_feeders
487
- if feeder_min_load < capacity:
488
- m.cons_balanced = pyo.Constraint(
489
- m.T,
490
- m.R,
491
- rule=(
492
- lambda m, t, r: (
493
- m.flow_[t, r] >= m.link_[t, r] * feeder_min_load
494
- )
495
- ),
496
- name='balanced',
497
- )
498
- else:
499
- warn(
500
- 'Model option <balanced = True> is incompatible with '
501
- 'having a range of possible feeder counts: model will '
502
- 'not enforce balanced subtrees.'
503
- )
469
+
470
+ # enforce balanced subtrees (subtree loads differ at most by one unit)
471
+ if load_lb is not None:
472
+ m.cons_balanced_lb = pyo.Constraint(
473
+ m.T,
474
+ m.R,
475
+ rule=(lambda m, t, r: m.flow_[t, r] >= m.link_[t, r] * load_lb),
476
+ name='balanced_lb',
477
+ )
478
+ if load_ub is not None:
479
+ m.cons_balanced_ub = pyo.Constraint(
480
+ m.T,
481
+ m.R,
482
+ rule=(lambda m, t, r: m.flow_[t, r] <= m.link_[t, r] * load_ub),
483
+ name='balanced_ub',
484
+ )
504
485
 
505
486
  # radial or branched topology
506
487
  if topology is Topology.RADIAL:
@@ -581,9 +562,9 @@ _make_min_length_model_fingerprint = fun_fingerprint(make_min_length_model)
581
562
  def warmup_model(
582
563
  model: pyo.ConcreteModel, metadata: ModelMetadata, S: nx.Graph
583
564
  ) -> pyo.ConcreteModel:
584
- """Set initial solution into `model`.
565
+ """Set initial solution into ``model``.
585
566
 
586
- Changes `model` and `metadata` in-place.
567
+ Changes ``model`` and ``metadata`` in-place.
587
568
 
588
569
  Args:
589
570
  model: pyomo model to apply the solution to.