optiwindnet 0.0.2__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 (130) hide show
  1. optiwindnet/MILP/__init__.py +0 -0
  2. optiwindnet/MILP/core.py +55 -0
  3. optiwindnet/MILP/ortools.py +328 -0
  4. optiwindnet/MILP/pyomo.py +383 -0
  5. optiwindnet/__init__.py +23 -0
  6. optiwindnet/augmentation.py +404 -0
  7. optiwindnet/baselines/__init__.py +51 -0
  8. optiwindnet/baselines/hgs.py +207 -0
  9. optiwindnet/baselines/lkh.py +264 -0
  10. optiwindnet/crossings.py +373 -0
  11. optiwindnet/data/Albatros.osm.pbf +0 -0
  12. optiwindnet/data/Amrumbank West.osm.pbf +0 -0
  13. optiwindnet/data/Anholt.yaml +132 -0
  14. optiwindnet/data/Arkona.osm.pbf +0 -0
  15. optiwindnet/data/BARD Offshore 1.osm.pbf +0 -0
  16. optiwindnet/data/Baltic 2.osm.pbf +0 -0
  17. optiwindnet/data/Baltic Eagle.osm.pbf +0 -0
  18. optiwindnet/data/Beatrice.osm.pbf +0 -0
  19. optiwindnet/data/Belwind.osm.pbf +0 -0
  20. optiwindnet/data/Borkum Riffgrund 1.yaml +106 -0
  21. optiwindnet/data/Borkum Riffgrund 2.yaml +102 -0
  22. optiwindnet/data/Borssele.yaml +214 -0
  23. optiwindnet/data/Butendiek.yaml +119 -0
  24. optiwindnet/data/CECEP Yangjiang Nanpeng Island.osm.pbf +0 -0
  25. optiwindnet/data/CGN Rudong Demonstration.osm.pbf +0 -0
  26. optiwindnet/data/Cazzaro-2022.yaml +20 -0
  27. optiwindnet/data/Cazzaro-2022G-140.yaml +64 -0
  28. optiwindnet/data/Cazzaro-2022G-210.yaml +81 -0
  29. optiwindnet/data/DanTysk.yaml +126 -0
  30. optiwindnet/data/Deutsche Bucht.osm.pbf +0 -0
  31. optiwindnet/data/Dogger Bank A.yaml +122 -0
  32. optiwindnet/data/Dudgeon.yaml +92 -0
  33. optiwindnet/data/East Anglia ONE.yaml +136 -0
  34. optiwindnet/data/Fecamp.osm.pbf +0 -0
  35. optiwindnet/data/Galloper Inner.osm.pbf +0 -0
  36. optiwindnet/data/Gemini 1.osm.pbf +0 -0
  37. optiwindnet/data/Gemini 2.osm.pbf +0 -0
  38. optiwindnet/data/Global Tech 1.osm.pbf +0 -0
  39. optiwindnet/data/Gode Wind 1.yaml +83 -0
  40. optiwindnet/data/Greater Gabbard Inner.yaml +181 -0
  41. optiwindnet/data/Gwynt y Mor.yaml +199 -0
  42. optiwindnet/data/Hohe See.osm.pbf +0 -0
  43. optiwindnet/data/Horns Rev 1.yaml +107 -0
  44. optiwindnet/data/Horns Rev 2.yaml +137 -0
  45. optiwindnet/data/Horns Rev 3.yaml +75 -0
  46. optiwindnet/data/Hornsea One.yaml +232 -0
  47. optiwindnet/data/Hornsea Two West.yaml +146 -0
  48. optiwindnet/data/Humber Gateway.osm.pbf +0 -0
  49. optiwindnet/data/Kaskasi.osm.pbf +0 -0
  50. optiwindnet/data/Kriegers Flak A.osm.pbf +0 -0
  51. optiwindnet/data/Kriegers Flak B.osm.pbf +0 -0
  52. optiwindnet/data/Laoting Bodhi Island.osm.pbf +0 -0
  53. optiwindnet/data/Lillgrund.osm.pbf +0 -0
  54. optiwindnet/data/Lincs.osm.pbf +0 -0
  55. optiwindnet/data/London Array.yaml +202 -0
  56. optiwindnet/data/Luchterduinen.osm.pbf +0 -0
  57. optiwindnet/data/Meerwind.osm.pbf +0 -0
  58. optiwindnet/data/Merkur.osm.pbf +0 -0
  59. optiwindnet/data/Mermaid.osm.pbf +0 -0
  60. optiwindnet/data/Moray East.yaml +145 -0
  61. optiwindnet/data/Moray West.yaml +95 -0
  62. optiwindnet/data/Neart na Gaoithe.osm.pbf +0 -0
  63. optiwindnet/data/Nordsee One.osm.pbf +0 -0
  64. optiwindnet/data/Nordsee Ost.osm.pbf +0 -0
  65. optiwindnet/data/Norther.osm.pbf +0 -0
  66. optiwindnet/data/Northwind.osm.pbf +0 -0
  67. optiwindnet/data/Nysted.osm.pbf +0 -0
  68. optiwindnet/data/Ormonde.yaml +51 -0
  69. optiwindnet/data/Princess Amalia.osm.pbf +0 -0
  70. optiwindnet/data/Race Bank.yaml +121 -0
  71. optiwindnet/data/Rampion.yaml +147 -0
  72. optiwindnet/data/Rentel.osm.pbf +0 -0
  73. optiwindnet/data/Riffgat.osm.pbf +0 -0
  74. optiwindnet/data/Robin Rigg.osm.pbf +0 -0
  75. optiwindnet/data/Rudong H10.osm.pbf +0 -0
  76. optiwindnet/data/Rudong H6.osm.pbf +0 -0
  77. optiwindnet/data/Rudong H8.osm.pbf +0 -0
  78. optiwindnet/data/R/303/270dsand 2.yaml" +145 -0
  79. optiwindnet/data/SPIC Binhai North H2.osm.pbf +0 -0
  80. optiwindnet/data/Saint-Brieuc.osm.pbf +0 -0
  81. optiwindnet/data/Saint-Nazaire.osm.pbf +0 -0
  82. optiwindnet/data/Sandbank.osm.pbf +0 -0
  83. optiwindnet/data/Seagreen.osm.pbf +0 -0
  84. optiwindnet/data/Shengsi 2.osm.pbf +0 -0
  85. optiwindnet/data/Sheringham Shoal.osm.pbf +0 -0
  86. optiwindnet/data/Taylor-2023.yaml +162 -0
  87. optiwindnet/data/Thanet.yaml +123 -0
  88. optiwindnet/data/Trianel Windpark Borkum.osm.pbf +0 -0
  89. optiwindnet/data/Triton Knoll.yaml +114 -0
  90. optiwindnet/data/Veja Mate.osm.pbf +0 -0
  91. optiwindnet/data/Walney 1.yaml +74 -0
  92. optiwindnet/data/Walney 2.yaml +78 -0
  93. optiwindnet/data/Walney Extension.yaml +129 -0
  94. optiwindnet/data/West of Duddon Sands.yaml +132 -0
  95. optiwindnet/data/Westermost Rough.osm.pbf +0 -0
  96. optiwindnet/data/Wikinger.osm.pbf +0 -0
  97. optiwindnet/data/Yi-2019.yaml +65 -0
  98. optiwindnet/data/Zhejiang Jiaxing 1.osm.pbf +0 -0
  99. optiwindnet/db/__init__.py +0 -0
  100. optiwindnet/db/modelv0.py +93 -0
  101. optiwindnet/db/modelv1.py +94 -0
  102. optiwindnet/db/modelv2.py +112 -0
  103. optiwindnet/db/storagev0.py +251 -0
  104. optiwindnet/db/storagev1.py +310 -0
  105. optiwindnet/db/storagev2.py +402 -0
  106. optiwindnet/geometric.py +1135 -0
  107. optiwindnet/heuristics/ClassicEsauWilliams.py +387 -0
  108. optiwindnet/heuristics/CrossingPreventingEW.py +593 -0
  109. optiwindnet/heuristics/EW_presolver.py +334 -0
  110. optiwindnet/heuristics/NonBranchingEW.py +631 -0
  111. optiwindnet/heuristics/ObstacleBypassingEW.py +1279 -0
  112. optiwindnet/heuristics/__init__.py +8 -0
  113. optiwindnet/heuristics/priorityqueue.py +46 -0
  114. optiwindnet/importer.py +406 -0
  115. optiwindnet/interarraylib.py +844 -0
  116. optiwindnet/interface.py +309 -0
  117. optiwindnet/mesh.py +1368 -0
  118. optiwindnet/pathfinding.py +729 -0
  119. optiwindnet/plotting.py +349 -0
  120. optiwindnet/repair.py +380 -0
  121. optiwindnet/svg.py +306 -0
  122. optiwindnet/synthetic.py +178 -0
  123. optiwindnet/utils.py +80 -0
  124. optiwindnet/version.py +21 -0
  125. optiwindnet/weighting.py +19 -0
  126. optiwindnet-0.0.2.dist-info/METADATA +88 -0
  127. optiwindnet-0.0.2.dist-info/RECORD +130 -0
  128. optiwindnet-0.0.2.dist-info/WHEEL +5 -0
  129. optiwindnet-0.0.2.dist-info/licenses/LICENSE +21 -0
  130. optiwindnet-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,383 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/
3
+
4
+ import math
5
+ from collections import namedtuple, defaultdict
6
+ from itertools import chain
7
+ import networkx as nx
8
+
9
+ import pyomo.environ as pyo
10
+ from pyomo.contrib.solver.base import SolverBase
11
+ from pyomo.opt import SolverResults
12
+
13
+ from ..crossings import edgeset_edgeXing_iter, gateXing_iter
14
+ from ..interarraylib import fun_fingerprint, G_from_S
15
+ from ..pathfinding import PathFinder
16
+
17
+
18
+ # solver option name mapping (pyomo should have taken care of this)
19
+ _common_options = namedtuple('common_options', 'mipgap timelimit')
20
+ _optname = defaultdict(lambda: _common_options(*_common_options._fields))
21
+ _optname['cbc'] = _common_options('ratioGap', 'seconds')
22
+ _optname['highs'] = _common_options('mip_rel_gap', 'time_limit')
23
+ _optname['scip'] = _common_options('limits/gap', 'limits/time')
24
+
25
+
26
+ def make_min_length_model(A: nx.Graph, capacity: int, *,
27
+ gateXings_constraint: bool = False,
28
+ gates_limit: bool | int = False,
29
+ branching: bool = True) -> pyo.ConcreteModel:
30
+ '''
31
+ Build ILP Pyomo model for the collector system length minimization.
32
+ `A` is the graph with the available edges to choose from.
33
+
34
+ `capacity`: cable capacity
35
+
36
+ `gateXing_constraint`: if gates and edges are forbidden to cross.
37
+
38
+ `gates_limit`: if True, use the minimum feasible number of gates
39
+ (total for all roots); if False, no limit is imposed; if a number,
40
+ use it as the limit.
41
+
42
+ `branching`: if root branches are paths (False) or can be trees (True).
43
+ '''
44
+ R = A.graph['R']
45
+ T = A.graph['T']
46
+ d2roots = A.graph['d2roots']
47
+ A_nodes = nx.subgraph_view(A, filter_node=lambda n: n >= 0)
48
+ W = sum(w for _, w in A_nodes.nodes(data='power', default=1))
49
+
50
+ # Sets
51
+ _T = range(T)
52
+ _R = range(-R, 0)
53
+
54
+ E = tuple(((u, v) if u < v else (v, u))
55
+ for u, v in A_nodes.edges())
56
+ # using directed node-node links -> create the reversed tuples
57
+ Eʹ = tuple((v, u) for u, v in E)
58
+ # set of feeders to all roots
59
+ stars = tuple((t, r) for t in _T for r in _R)
60
+
61
+ # Create model
62
+ m = pyo.ConcreteModel()
63
+ m.T = pyo.RangeSet(0, T - 1)
64
+ m.R = pyo.RangeSet(-R, -1)
65
+ m.linkset = pyo.Set(initialize=E + Eʹ + stars)
66
+
67
+ ##############
68
+ # Parameters #
69
+ ##############
70
+
71
+ m.k = pyo.Param(domain=pyo.PositiveIntegers,
72
+ name='capacity', default=capacity)
73
+ m.weight_ = pyo.Param(
74
+ m.linkset,
75
+ domain=pyo.PositiveReals,
76
+ name='link_weight',
77
+ initialize=lambda m, u, v: (A.edges[(u, v)]['length'] if v >= 0
78
+ else d2roots[u, v])
79
+ )
80
+
81
+ #############
82
+ # Variables #
83
+ #############
84
+
85
+ m.link_ = pyo.Var(m.linkset, domain=pyo.Binary, initialize=0)
86
+ def flow_bounds(m, u, v):
87
+ return (0, (m.k if v < 0 else m.k - 1))
88
+ m.flow_ = pyo.Var(m.linkset, domain=pyo.NonNegativeIntegers,
89
+ bounds=flow_bounds, initialize=0)
90
+
91
+ ###############
92
+ # Constraints #
93
+ ###############
94
+
95
+ # total number of edges must be equal to number of non-root nodes
96
+ m.cons_edges_eq_nodes = pyo.Constraint(
97
+ rule=lambda m: sum(m.link_.values()) == T
98
+ )
99
+
100
+ # enforce a single directed edge between each node pair
101
+ m.cons_one_diEdge = pyo.Constraint(
102
+ E,
103
+ rule=lambda m, u, v: m.link_[u, v] + m.link_[v, u] <= 1
104
+ )
105
+
106
+ # gate-edge crossings
107
+ if gateXings_constraint:
108
+ m.cons_gateXedge = pyo.Constraint(
109
+ gateXing_iter(A),
110
+ rule=lambda m, u, v, r, n: (m.link_[u, v]
111
+ + m.link_[v, u]
112
+ + m.link_[r, n] <= 1)
113
+ )
114
+
115
+ # edge-edge crossings
116
+ def edgeXedge_rule(m, *vertices):
117
+ lhs = sum((m.link_[u, v] + m.link_[v, u])
118
+ for u, v in zip(vertices[::2],
119
+ vertices[1::2]))
120
+ return lhs <= 1
121
+ doubleXings = []
122
+ tripleXings = []
123
+ for Xing in edgeset_edgeXing_iter(A.graph['diagonals']):
124
+ if len(Xing) == 2:
125
+ doubleXings.append(Xing)
126
+ else:
127
+ tripleXings.append(Xing)
128
+ if doubleXings:
129
+ m.cons_edgeXedge = pyo.Constraint(doubleXings,
130
+ rule=edgeXedge_rule)
131
+ if tripleXings:
132
+ m.cons_edgeXedgeXedge = pyo.Constraint(tripleXings,
133
+ rule=edgeXedge_rule)
134
+
135
+ # bind flow to link activation
136
+ m.cons_link_used_iff_demand_lb = pyo.Constraint(
137
+ m.linkset,
138
+ rule=(lambda m, u, v:
139
+ m.flow_[(u, v)] <= m.link_[(u, v)]*(m.k if v < 0 else (m.k - 1)))
140
+ )
141
+ m.cons_link_used_iff_demand_ub = pyo.Constraint(
142
+ m.linkset,
143
+ rule=lambda m, u, v: m.link_[(u, v)] <= m.flow_[(u, v)]
144
+ )
145
+
146
+ # flow conservation with possibly non-unitary node power
147
+ m.cons_flow_conservation = pyo.Constraint(
148
+ m.T,
149
+ rule=(lambda m, u:
150
+ (sum((m.flow_[u, v] - m.flow_[v, u])
151
+ for v in A_nodes.neighbors(u))
152
+ + sum(m.flow_[u, r] for r in _R)
153
+ == A.nodes[u].get('power', 1))
154
+ )
155
+ )
156
+
157
+ # gates limit
158
+ if gates_limit:
159
+ def gates_limit_eq_rule(m):
160
+ return (sum(m.link_[t, r] for r in _R for t in _T)
161
+ == math.ceil(T/m.k))
162
+
163
+ def gates_limit_ub_rule(m):
164
+ return (sum(m.link_[t, r] for r in _R for t in _T)
165
+ <= gates_limit)
166
+
167
+ m.gates_limit = pyo.Constraint(rule=(gates_limit_eq_rule
168
+ if isinstance(gates_limit, bool)
169
+ else gates_limit_ub_rule))
170
+
171
+ # non-branching
172
+ if not branching:
173
+ # just need to limit incoming edges since the outgoing are
174
+ # limited by the m.cons_one_out_edge
175
+ m.non_branching = pyo.Constraint(
176
+ m.T,
177
+ rule=lambda m, u: (sum(m.link_[v, u] for v in A_nodes.neighbors(u))
178
+ <= 1)
179
+ )
180
+
181
+ # assert all nodes are connected to some root
182
+ m.cons_all_nodes_connected = pyo.Constraint(
183
+ rule=lambda m: sum(m.flow_[t, r] for r in _R for t in _T) == W
184
+ )
185
+
186
+ # valid inequalities
187
+ m.cons_min_gates_required = pyo.Constraint(
188
+ rule=lambda m: (sum(m.link_[t, r] for r in _R for t in _T)
189
+ >= math.ceil(T/m.k))
190
+ )
191
+ m.cons_incoming_demand_limit = pyo.Constraint(
192
+ m.T,
193
+ rule=lambda m, u: (sum(m.flow_[v, u] for v in A_nodes.neighbors(u))
194
+ <= m.k - A.nodes[u].get('power', 1))
195
+ )
196
+ m.cons_one_out_edge = pyo.Constraint(
197
+ m.T,
198
+ rule=lambda m, u:(
199
+ sum(m.link_[u, v] for v in chain(A_nodes.neighbors(u), _R)) == 1)
200
+ )
201
+
202
+
203
+ #############
204
+ # Objective #
205
+ #############
206
+
207
+ m.length = pyo.Objective(
208
+ expr=lambda m: pyo.sum_product(m.weight_, m.link_),
209
+ sense=pyo.minimize,
210
+ )
211
+
212
+ ##################
213
+ # Store metadata #
214
+ ##################
215
+
216
+ m.handle = A.graph['handle']
217
+ m.name = A.graph.get('name', 'unnamed')
218
+ m.method_options = dict(gateXings_constraint=gateXings_constraint,
219
+ gates_limit=gates_limit,
220
+ branching=branching)
221
+
222
+ m.fun_fingerprint = fun_fingerprint()
223
+ m.warmed_by = None
224
+ return m
225
+
226
+
227
+ def warmup_model(model: pyo.ConcreteModel, S: nx.Graph) \
228
+ -> pyo.ConcreteModel:
229
+ '''
230
+ Changes `model` in-place.
231
+ '''
232
+ for u, v, reverse in S.edges(data='reverse'):
233
+ u, v = (u, v) if ((u < v) == reverse) else (v, u)
234
+ model.link_[(u, v)] = 1
235
+ model.flow_[(u, v)] = S[u][v]['load']
236
+ model.warmed_by = S.graph['creator']
237
+ return model
238
+
239
+
240
+ def S_from_solution(model: pyo.ConcreteModel, solver: SolverBase,
241
+ result: SolverResults) -> nx.Graph:
242
+ '''
243
+ Create a topology `S` with the solution in `model` by `solver`.
244
+ '''
245
+
246
+ # Metadata
247
+ R, T, k = len(model.R), len(model.T), model.k.value
248
+ # if 'highs.Highs' in str(solver):
249
+ if hasattr(solver, 'highs_options'):
250
+ solver_name = 'highs'
251
+ elif solver.name.endswith('direct'):
252
+ solver_name = solver.name[:-6].rstrip('_')
253
+ elif solver.name.endswith('persistent'):
254
+ solver_name = solver.name[:-10].rstrip('_')
255
+ else:
256
+ solver_name = solver.name
257
+ bound = result['Problem'][0]['Lower bound']
258
+ objective = result['Problem'][0]['Upper bound']
259
+ # create a topology graph S from the solution
260
+ S = nx.Graph(
261
+ R=R, T=T,
262
+ handle=model.handle,
263
+ capacity=k,
264
+ objective=objective,
265
+ bound=bound,
266
+ runtime=result['Solver'][0]['Wallclock time'],
267
+ termination=result['Solver'][0]['Termination condition'].name,
268
+ gap=1. - bound/objective,
269
+ creator='MILP.pyomo.' + solver_name,
270
+ has_loads=True,
271
+ method_options=dict(
272
+ solver_name=solver_name,
273
+ mipgap=solver.options[_optname[solver_name].mipgap],
274
+ timelimit=solver.options[_optname[solver_name].timelimit],
275
+ fun_fingerprint=model.fun_fingerprint,
276
+ **model.method_options,
277
+ ),
278
+ # solver_details=dict(
279
+ # )
280
+ )
281
+
282
+ if model.warmed_by is not None:
283
+ S.graph['warmstart'] = model.warmed_by
284
+
285
+ # Graph data
286
+ # Get active links and if flow is reversed (i.e. from small to big)
287
+ rev_from_link = {
288
+ (u, v): u < v for (u, v), use in model.link_.items() if use.value > 0.5
289
+ }
290
+ S.add_weighted_edges_from(
291
+ ((u, v, round(model.flow_[u, v].value))
292
+ for (u, v) in rev_from_link.keys()), weight='load'
293
+ )
294
+ # set the 'reverse' edge attribute
295
+ nx.set_edge_attributes(S, rev_from_link, name='reverse')
296
+ # propagate loads from edges to nodes
297
+ subtree = -1
298
+ for r in range(-R, 0):
299
+ for u, v in nx.edge_dfs(S, r):
300
+ S.nodes[v]['load'] = S[u][v]['load']
301
+ if u == r:
302
+ subtree += 1
303
+ S.nodes[v]['subtree'] = subtree
304
+ rootload = 0
305
+ for nbr in S.neighbors(r):
306
+ rootload += S.nodes[nbr]['load']
307
+ S.nodes[r]['load'] = rootload
308
+
309
+ return S
310
+
311
+
312
+ def gurobi_investigate_pool(P, A, model, solver, result):
313
+ '''Go through the Gurobi's solutions checking which has the shortest length
314
+ after applying the detours with PathFinder.'''
315
+ # initialize incumbent total length
316
+ solver_model = solver._solver_model
317
+ Λ = float('inf')
318
+ num_solutions = solver_model.getAttr('SolCount')
319
+ print(f'Solution pool has {num_solutions} solutions.')
320
+ # Pool = iter(sorted((cplex.solution.pool.get_objective_value(i), i)
321
+ # for i in range(cplex.solution.pool.get_num()))[1:])
322
+ # model comes loaded with minimal-length undetoured solution
323
+ for i in range(num_solutions):
324
+ solver_model.setParam('SolutionNumber', i)
325
+ λ = solver_model.getAttr('PoolObjVal')
326
+ if λ > Λ:
327
+ print(f'Pool investigation over - next best undetoured length: {λ:.3f}')
328
+ break
329
+ for omovar, gurvar in solver._pyomo_var_to_solver_var_map.items():
330
+ omovar.set_value(round(gurvar.Xn), skip_validation=True)
331
+ S = S_from_solution(model, solver=solver, result=result)
332
+ G = G_from_S(S, A)
333
+ Hʹ = PathFinder(G, planar=P, A=A).create_detours()
334
+ Λʹ = Hʹ.size(weight='length')
335
+ if Λʹ < Λ:
336
+ H, Λ = Hʹ, Λʹ
337
+ pool_index = i
338
+ pool_objective = λ
339
+ print(f'Incumbent has (detoured) length: {Λ:.3f}')
340
+ H.graph['solver_details']['pool_index'] = pool_index
341
+ if pool_index > 0:
342
+ H.graph['solver_details']['pool_objective'] = pool_objective
343
+ return H
344
+
345
+
346
+ def cplex_load_solution_from_pool(solver, soln):
347
+ cplex = solver._solver_model
348
+ vals = cplex.solution.pool.get_values(soln)
349
+ vars_to_load = solver._pyomo_var_to_ndx_map.keys()
350
+ for pyomo_var, val in zip(vars_to_load, vals):
351
+ if solver._referenced_variables[pyomo_var] > 0:
352
+ pyomo_var.set_value(val, skip_validation=True)
353
+
354
+
355
+ def cplex_investigate_pool(P, A, model, solver, result):
356
+ '''Go through the CPLEX solutions checking which has the shortest length
357
+ after applying the detours with PathFinder.'''
358
+ cplex = solver._solver_model
359
+ # initialize incumbent total length
360
+ Λ = float('inf')
361
+ print(f'Solution pool has {cplex.solution.pool.get_num()} solutions.')
362
+ Pool = iter(sorted((cplex.solution.pool.get_objective_value(i), i)
363
+ for i in range(cplex.solution.pool.get_num()))[1:])
364
+ # model comes loaded with minimal-length undetoured solution
365
+ while True:
366
+ S = S_from_solution(model, solver=solver, result=result)
367
+ G = G_from_S(S, A)
368
+ Hʹ = PathFinder(G, planar=P, A=A).create_detours()
369
+ Λʹ = Hʹ.size(weight='length')
370
+ if Λʹ < Λ:
371
+ H, Λ = Hʹ, Λʹ
372
+ print(f'Incumbent has (detoured) length: {Λ:.3f}')
373
+ # check if next best solution is worth processing
374
+ try:
375
+ λ, soln = next(Pool)
376
+ except StopIteration:
377
+ print('Pool exhausted.')
378
+ break
379
+ if λ > Λ:
380
+ print(f'Done with pool - next best undetoured length: {λ:.3f}')
381
+ break
382
+ cplex_load_solution_from_pool(solver, soln)
383
+ return H
@@ -0,0 +1,23 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/
3
+
4
+ # author, version, license, and long description
5
+ __version__ = '0.0.1'
6
+ __author__ = 'Mauricio Souza de Alencar'
7
+
8
+ __doc__ = """
9
+ `interarray` implements extensions to the Esau-Williams heuristic for the
10
+ capacitaded minimum spanning tree (CMST) problem.
11
+
12
+ https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/
13
+ """
14
+
15
+ __license__ = "LGPL-2.1-or-later"
16
+
17
+ try: # pragma: no cover
18
+ # version.py created when installing optiwindnet
19
+ from optiwindnet import version
20
+ __version__ = version.__version__
21
+ __release__ = version.__version__
22
+ except BaseException: # pragma: no cover
23
+ pass