Quick Start Guide
The following guide walks through the solution of a water network design (des
) problem using two mixed-integer linear programming formulations (MILP and MILP-R) of the problem specification. This is to enable solution using the readily-available open-source mixed-integer linear programming solver Cbc. Other formulations rely on the availability of mixed-integer nonlinear programming solvers that support user-defined nonlinear functions in JuMP. However, these solvers (e.g., Juniper, KNITRO) either require additional effort to register user-defined functions or are proprietary and require a commercial license.
Installation
The latest stable release of WaterModels can be installed using the Julia package manager with
] add WaterModels
For the current development version, install the package using
] add WaterModels#master
Next, test that the package works by executing
] test WaterModels
Install Cbc using
] add Cbc
Solving a Network Design Problem
Once the above dependencies have been installed, obtain the files shamir.inp
and shamir.json
. Here, shamir.inp
is an EPANET file describing a simple seven-node, eight-link water distribution network with one reservoir, six junctions, and eight pipes. In accord, shamir.json
is a JSON file specifying possible pipe diameters and associated costs per unit length, per diameter setting. The combination of data from these two files provides the required information to set up a corresponding network design problem, where the goal is to select the most cost-efficient pipe diameters while satisfying all demand in the network.
First, the diameter and cost data from the shamir.json
problem specification must be appended to the initial data provided by the EPANET shamir.inp
file. To read in the EPANET data, execute the following:
using WaterModels
data = parse_file("shamir.inp")
Next, to read in the JSON diameter and cost data, execute the following:
modifications = parse_file("shamir.json")
To merge these data together, InfrastructureModels is used to update the original network data using
WaterModels._IM.update_data!(data, modifications)
Finally, the MILP formulation for the network design specification can be solved using
import Cbc
run_des(data, MILPWaterModel, Cbc.Optimizer)
By default, no breakpoints are used for the linear approximation of each head loss curve, and the problem is infeasible. These approximations can be more finely discretized by using additional arguments to the run_des
function. For example, to employ five breakpoints per head loss curve in this formulation, the following can be executed:
run_des(data, MILPWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>5))
Note that this takes much longer to solve due to the use of more binary variables. However, because of the finer discretization, a better approximation of the physics is attained.
Instead of linear approximation, head loss curves can also be linearly outer-approximated via the MILP-R formulation. This formulation employs less strict requirements and avoids the use of binary variables, but solutions (e.g., diameters) may not necessarily be feasible with respect to the full (nonconvex) water network physics. To employ five outer approximation points per (positive or negative) head loss curve in this formulation, the following can be executed
run_des(data, MILPRWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>5))
Obtaining Results
The run
commands in WaterModels return detailed results data in the form of a Julia Dict
. This dictionary can be saved for further processing as follows:
result = run_des(data, MILPRWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>5))
For example, the algorithm's runtime and final objective value can be accessed with,
result["solve_time"]
result["objective"]
The "solution"
field contains detailed information about the solution produced by the run
method. For example, the following dictionary comprehension can be used to inspect the flows in the solution:
Dict(name => data["q"] for (name, data) in result["solution"]["link"])
For more information about WaterModels result data see the WaterModels Result Data Format section.
Accessing Different Formulations
The MILP formulations discussed above assume access to a mixed-integer programming (MIP) solver. Mixed-integer nonconvex formulations can be solved with dedicated solvers, as well. For example, the full mixed-integer nonconvex formulation for design (NLP) can be solved via
import KNITRO
run_des(data, NCNLPWaterModel, KNITRO.Optimizer)
and the mixed-integer convex formulation (MICP) can be solved via
run_des(data, MICPWaterModel, KNITRO.Optimizer)
Modifying Network Data
The following example demonstrates one way to perform multiple WaterModels solves while modifing network data in Julia.
run_des(data, MILPRWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>5))
data["junction"]["3"]["demand"] *= 2.0
data["junction"]["4"]["demand"] *= 2.0
data["junction"]["5"]["demand"] *= 2.0
run_des(data, MILPRWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>5))
Note that the greater demands in the second problem result in an overall larger network cost. For additional details about the network data, see the WaterModels Network Data Format section.
Alternative Methods for Building and Solving Models
The following example demonstrates how to break a run_des
call into separate model building and solving steps. This allows inspection of the JuMP model created by WaterModels for the problem.
wm = instantiate_model(data, MILPRWaterModel, WaterModels.build_des)
print(wm.model)
result = WaterModels._IM.optimize_model!(wm, optimizer=Cbc.Optimizer)