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.
- optiwindnet/MILP/__init__.py +0 -0
- optiwindnet/MILP/core.py +55 -0
- optiwindnet/MILP/ortools.py +328 -0
- optiwindnet/MILP/pyomo.py +383 -0
- optiwindnet/__init__.py +23 -0
- optiwindnet/augmentation.py +404 -0
- optiwindnet/baselines/__init__.py +51 -0
- optiwindnet/baselines/hgs.py +207 -0
- optiwindnet/baselines/lkh.py +264 -0
- optiwindnet/crossings.py +373 -0
- optiwindnet/data/Albatros.osm.pbf +0 -0
- optiwindnet/data/Amrumbank West.osm.pbf +0 -0
- optiwindnet/data/Anholt.yaml +132 -0
- optiwindnet/data/Arkona.osm.pbf +0 -0
- optiwindnet/data/BARD Offshore 1.osm.pbf +0 -0
- optiwindnet/data/Baltic 2.osm.pbf +0 -0
- optiwindnet/data/Baltic Eagle.osm.pbf +0 -0
- optiwindnet/data/Beatrice.osm.pbf +0 -0
- optiwindnet/data/Belwind.osm.pbf +0 -0
- optiwindnet/data/Borkum Riffgrund 1.yaml +106 -0
- optiwindnet/data/Borkum Riffgrund 2.yaml +102 -0
- optiwindnet/data/Borssele.yaml +214 -0
- optiwindnet/data/Butendiek.yaml +119 -0
- optiwindnet/data/CECEP Yangjiang Nanpeng Island.osm.pbf +0 -0
- optiwindnet/data/CGN Rudong Demonstration.osm.pbf +0 -0
- optiwindnet/data/Cazzaro-2022.yaml +20 -0
- optiwindnet/data/Cazzaro-2022G-140.yaml +64 -0
- optiwindnet/data/Cazzaro-2022G-210.yaml +81 -0
- optiwindnet/data/DanTysk.yaml +126 -0
- optiwindnet/data/Deutsche Bucht.osm.pbf +0 -0
- optiwindnet/data/Dogger Bank A.yaml +122 -0
- optiwindnet/data/Dudgeon.yaml +92 -0
- optiwindnet/data/East Anglia ONE.yaml +136 -0
- optiwindnet/data/Fecamp.osm.pbf +0 -0
- optiwindnet/data/Galloper Inner.osm.pbf +0 -0
- optiwindnet/data/Gemini 1.osm.pbf +0 -0
- optiwindnet/data/Gemini 2.osm.pbf +0 -0
- optiwindnet/data/Global Tech 1.osm.pbf +0 -0
- optiwindnet/data/Gode Wind 1.yaml +83 -0
- optiwindnet/data/Greater Gabbard Inner.yaml +181 -0
- optiwindnet/data/Gwynt y Mor.yaml +199 -0
- optiwindnet/data/Hohe See.osm.pbf +0 -0
- optiwindnet/data/Horns Rev 1.yaml +107 -0
- optiwindnet/data/Horns Rev 2.yaml +137 -0
- optiwindnet/data/Horns Rev 3.yaml +75 -0
- optiwindnet/data/Hornsea One.yaml +232 -0
- optiwindnet/data/Hornsea Two West.yaml +146 -0
- optiwindnet/data/Humber Gateway.osm.pbf +0 -0
- optiwindnet/data/Kaskasi.osm.pbf +0 -0
- optiwindnet/data/Kriegers Flak A.osm.pbf +0 -0
- optiwindnet/data/Kriegers Flak B.osm.pbf +0 -0
- optiwindnet/data/Laoting Bodhi Island.osm.pbf +0 -0
- optiwindnet/data/Lillgrund.osm.pbf +0 -0
- optiwindnet/data/Lincs.osm.pbf +0 -0
- optiwindnet/data/London Array.yaml +202 -0
- optiwindnet/data/Luchterduinen.osm.pbf +0 -0
- optiwindnet/data/Meerwind.osm.pbf +0 -0
- optiwindnet/data/Merkur.osm.pbf +0 -0
- optiwindnet/data/Mermaid.osm.pbf +0 -0
- optiwindnet/data/Moray East.yaml +145 -0
- optiwindnet/data/Moray West.yaml +95 -0
- optiwindnet/data/Neart na Gaoithe.osm.pbf +0 -0
- optiwindnet/data/Nordsee One.osm.pbf +0 -0
- optiwindnet/data/Nordsee Ost.osm.pbf +0 -0
- optiwindnet/data/Norther.osm.pbf +0 -0
- optiwindnet/data/Northwind.osm.pbf +0 -0
- optiwindnet/data/Nysted.osm.pbf +0 -0
- optiwindnet/data/Ormonde.yaml +51 -0
- optiwindnet/data/Princess Amalia.osm.pbf +0 -0
- optiwindnet/data/Race Bank.yaml +121 -0
- optiwindnet/data/Rampion.yaml +147 -0
- optiwindnet/data/Rentel.osm.pbf +0 -0
- optiwindnet/data/Riffgat.osm.pbf +0 -0
- optiwindnet/data/Robin Rigg.osm.pbf +0 -0
- optiwindnet/data/Rudong H10.osm.pbf +0 -0
- optiwindnet/data/Rudong H6.osm.pbf +0 -0
- optiwindnet/data/Rudong H8.osm.pbf +0 -0
- optiwindnet/data/R/303/270dsand 2.yaml" +145 -0
- optiwindnet/data/SPIC Binhai North H2.osm.pbf +0 -0
- optiwindnet/data/Saint-Brieuc.osm.pbf +0 -0
- optiwindnet/data/Saint-Nazaire.osm.pbf +0 -0
- optiwindnet/data/Sandbank.osm.pbf +0 -0
- optiwindnet/data/Seagreen.osm.pbf +0 -0
- optiwindnet/data/Shengsi 2.osm.pbf +0 -0
- optiwindnet/data/Sheringham Shoal.osm.pbf +0 -0
- optiwindnet/data/Taylor-2023.yaml +162 -0
- optiwindnet/data/Thanet.yaml +123 -0
- optiwindnet/data/Trianel Windpark Borkum.osm.pbf +0 -0
- optiwindnet/data/Triton Knoll.yaml +114 -0
- optiwindnet/data/Veja Mate.osm.pbf +0 -0
- optiwindnet/data/Walney 1.yaml +74 -0
- optiwindnet/data/Walney 2.yaml +78 -0
- optiwindnet/data/Walney Extension.yaml +129 -0
- optiwindnet/data/West of Duddon Sands.yaml +132 -0
- optiwindnet/data/Westermost Rough.osm.pbf +0 -0
- optiwindnet/data/Wikinger.osm.pbf +0 -0
- optiwindnet/data/Yi-2019.yaml +65 -0
- optiwindnet/data/Zhejiang Jiaxing 1.osm.pbf +0 -0
- optiwindnet/db/__init__.py +0 -0
- optiwindnet/db/modelv0.py +93 -0
- optiwindnet/db/modelv1.py +94 -0
- optiwindnet/db/modelv2.py +112 -0
- optiwindnet/db/storagev0.py +251 -0
- optiwindnet/db/storagev1.py +310 -0
- optiwindnet/db/storagev2.py +402 -0
- optiwindnet/geometric.py +1135 -0
- optiwindnet/heuristics/ClassicEsauWilliams.py +387 -0
- optiwindnet/heuristics/CrossingPreventingEW.py +593 -0
- optiwindnet/heuristics/EW_presolver.py +334 -0
- optiwindnet/heuristics/NonBranchingEW.py +631 -0
- optiwindnet/heuristics/ObstacleBypassingEW.py +1279 -0
- optiwindnet/heuristics/__init__.py +8 -0
- optiwindnet/heuristics/priorityqueue.py +46 -0
- optiwindnet/importer.py +406 -0
- optiwindnet/interarraylib.py +844 -0
- optiwindnet/interface.py +309 -0
- optiwindnet/mesh.py +1368 -0
- optiwindnet/pathfinding.py +729 -0
- optiwindnet/plotting.py +349 -0
- optiwindnet/repair.py +380 -0
- optiwindnet/svg.py +306 -0
- optiwindnet/synthetic.py +178 -0
- optiwindnet/utils.py +80 -0
- optiwindnet/version.py +21 -0
- optiwindnet/weighting.py +19 -0
- optiwindnet-0.0.2.dist-info/METADATA +88 -0
- optiwindnet-0.0.2.dist-info/RECORD +130 -0
- optiwindnet-0.0.2.dist-info/WHEEL +5 -0
- optiwindnet-0.0.2.dist-info/licenses/LICENSE +21 -0
- 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
|
optiwindnet/__init__.py
ADDED
|
@@ -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
|