π Quick StartΒΆ
Concepts: Problem Β· Optimization approaches Advanced API: π Quick Start
This notebook shows the minimum steps needed to optimize an electrical network for a wind farm with OptiWindNet.
StepsΒΆ
An OptiWindNet network optimization involves two components:
A WindFarmNetwork containing the problem data and options.
A Router defining the algorithm used to solve it.
The network optimization can be performed in the following steps:
Create a WindFarmNetwork instance
Define a router
Run the optimization and get the results
1. Create a WindFarmNetwork instanceΒΆ
The WindFarmNetwork component stores the problem data, invokes its Router during optimization, and stores the resulting topology and routed network. See π¦ WindFarmNetwork for its key methods.
[1]:
from optiwindnet.api import WindFarmNetwork
Load location dataΒΆ
Note: For details about OptiWindNetβs load_repository(), check ποΈ Locations.
[2]:
from optiwindnet.api import load_repository
locations = load_repository()
L = locations.doggerA
Initialize a WindFarmNetwork instance with a prebuilt L and a desired maximum cable capacity (See Data Input for various input formats.)
[3]:
wfn = WindFarmNetwork(L=L, cables=8)
results = [] # (router, topology, length) of each example
Plot the location to confirm that wfn contains the intended data.
Tip. In a notebook, put
wfnas the last line of a cell to plot its current state.
Before optimization (no
Gyet), it renders the location geometryL.If
Gexists (for example, after a call to optimize()), it automatically rendersG.Call wfn.plot_location() explicitly to plot only the location geometry. See π Plotting for the available plotting methods.
[4]:
wfn
[4]:
2. Define a RouterΒΆ
Three built-in routers are available in OptiWindNet, one per optimization approach:
Router |
Approach |
|---|---|
constructive heuristic β sub-second |
|
meta-heuristic β configurable time budget |
|
exact optimization β reports an optimality gap |
Their trade-offs are compared in Optimization approaches. See π¦ WindFarmNetwork for details about routers.
EWRouterΒΆ
Create an EWRouter instance with no arguments to use its defaults. Every argument is optional.
[5]:
from optiwindnet.api import EWRouter
[6]:
ew_router = EWRouter()
HGSRouterΒΆ
To use this router, create an instance of the HGSRouter class. The only required argument is time_limit, which defines how long the optimization is allowed to run (in seconds).
[7]:
from optiwindnet.api import HGSRouter
[8]:
hgs_router = HGSRouter(time_limit=1, seed=1234567)
MILPRouterΒΆ
Create a MILPRouter instance with the following required arguments:
solver_name: the MILP solver to use (e.g.,
'ortools.cp_sat','gurobi','ortools.cbcbox')time_limit: maximum time allowed for solving (in seconds)
mip_gap: acceptable optimality gap (e.g.,
0.005= 0.5%)
Optional arguments:
verbose (default: False): set to
Trueto display solver progress and detailed logssolver_options: dictionary of solver-specific parameters
model_options: advanced model settings (e.g., topology, feeder configuration)
[9]:
from optiwindnet.api import MILPRouter
[10]:
milp_router = MILPRouter(
solver_name='ortools.cp_sat',
time_limit=20,
mip_gap=0.005,
solver_options={'random_seed': 1234567},
)
3. Run the optimizationΒΆ
EWRouter (sub-second)ΒΆ
[11]:
%%timeit
res_ew = wfn.optimize(router=ew_router)
37.3 ms Β± 478 ΞΌs per loop (mean Β± std. dev. of 7 runs, 10 loops each)
[12]:
wfn.solution_info()
[12]:
{'router': 'EWRouter', 'capacity': 8, 'method': 'biased_EW', 'iterations': 84}
[13]:
results.append(
(
wfn.solution_info()['router'],
wfn.G.graph['topology'].value,
f'{wfn.length():.1f}',
)
)
results[-1]
[13]:
('EWRouter', 'branched', '261192.1')
Plot the optimized network.
Note: Call wfn.plot() explicitly to plot the optimized network. See π Plotting for the available plotting methods.
[14]:
wfn
[14]:
Note: Each call to .optimize() on the same WindFarmNetwork instance overwrites its stored solution and related information, including the network graph, cost, and length. Compare the updated length or plot with the preceding result to observe the replacement.
HGSRouter (fast β radial or ringed topology)ΒΆ
[15]:
res_hgs = wfn.optimize(router=hgs_router)
[16]:
wfn.solution_info()
[16]:
{'router': 'HGSRouter',
'capacity': 8,
'solver_name': 'HGS-CVRP',
'complete': False,
'feeders_above_min': None,
'feeders_exact': False,
'balanced': False,
'ringed': False,
'repair': True,
'max_retries': 10,
'nbGranular': 20,
'mu': 25,
'lambda_': 40,
'nbElite': 4,
'nbClose': 5,
'nbIterPenaltyManagement': 100,
'targetFeasible': 0.2,
'penaltyDecrease': 0.85,
'penaltyIncrease': 1.2,
'seed': 1234567,
'nbIter': 20000,
'nbIterTraces': 500,
'timeLimit': 1,
'useSwapStar': True,
'runtime': 1.00029728}
[17]:
results.append(
(
wfn.solution_info()['router'],
wfn.G.graph['topology'].value,
f'{wfn.length():.1f}',
)
)
results[-1]
[17]:
('HGSRouter', 'radial', '243214.0')
[18]:
wfn
[18]:
Note that the HGSRouter produces a radial or ringed topology, never a branched one. The three topologies are defined in Network topologies; the figure below contrasts them.
[19]:
from IPython.display import HTML
from optiwindnet.api import ModelOptions
from optiwindnet.synthetic import toyfarm
[20]:
wfn_toy = WindFarmNetwork(L=toyfarm(), cables=4)
opt_milp = {
'mip_gap': 0.001,
'time_limit': 5,
'solver_name': 'ortools.cp_sat',
'solver_options': {'random_seed': 1234567},
}
panels = []
for topology in ('radial', 'branched', 'ringed'):
wfn_toy.optimize(
router=MILPRouter(
**opt_milp,
model_options=ModelOptions(topology=topology, feeder_limit='minimum'),
)
)
caption = f'<p style="color:#888">{topology.capitalize()}</p>'
svg = wfn_toy.plot(infobox=False, tight=True).data
panels.append(f'<div style="flex:1">{caption}{svg}</div>')
HTML(f'<div style="display:flex">{"".join(panels)}</div>')
[20]:
Radial
Branched
Ringed
MILPRouterΒΆ
MILP reports a bound and an optimality gap for its length model; routing detours can increase the delivered length. See Exact optimization.
Note:
MILPRouter warm-starts by default: it reuses the solution already stored in the WindFarmNetwork, and builds one with a fast heuristic when there is none or the stored one does not fit the model β see Warm-starting.
To solve cold instead, pass
warmup=False;warmup_timeis the budget for building a warm start.Set
verbose=Trueto see warm-start messages in the logs.
[21]:
res_milp = wfn.optimize(router=milp_router)
[22]:
wfn.solution_info()
[22]:
{'router': 'MILPRouter',
'capacity': 8,
'solver_name': 'ortools.cp_sat',
'mip_gap': 0.005,
'time_limit': 20,
'topology': <Topology.BRANCHED: 'branched'>,
'feeder_route': <FeederRoute.SEGMENTED: 'segmented'>,
'feeder_limit': <FeederLimit.UNLIMITED: 'unlimited'>,
'balanced': False,
'runtime': 20.029517,
'bound': 229556.31575552459,
'objective': 240715.159901454,
'relgap': 0.04635704768448201,
'termination': 'FEASIBLE'}
[23]:
results.append(
(
wfn.solution_info()['router'],
wfn.G.graph['topology'].value,
f'{wfn.length():.1f}',
)
)
results[-1]
[23]:
('MILPRouter', 'branched', '240866.2')
The two length values describe different stages of the solution. wfn.solution_info()['objective'] is the length optimized by the MILP on the available-links graph, before route reconstruction. wfn.length() sums the physical cable routes after PathFinder introduces any detours required by the navigation mesh. The routed length can therefore be slightly longer. Use objective to interpret the solverβs
bound and relative gap, and wfn.length() for the final installed cable length. If cable prices are supplied, wfn.cost() reports the cost of the routed network. Length and cost often track each other, but the MILP gap is a guarantee about modeled length, not a bound on cost suboptimality. See Objective and reported cost.
[24]:
wfn
[24]:
Compare the results of the three routers:
[25]:
from prettyTables import Table
Table(
results,
headers=('router', 'topology', 'length (m)'),
)
[25]:
| router | topology | length (m) |
|---|---|---|
| EWRouter | branched | 261192.1 |
| HGSRouter | radial | 243214.0 |
| MILPRouter | branched | 240866.2 |