Quick Start Guide

The following guide walks through the solution of a water network design (des) problem using two mixed-integer linear programming formulations (PWLRD and LRD) 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

Finally, test that the package works as expected by executing

] test WaterModels

Solving a Network Design Problem

Once the above dependencies have been installed, obtain the file shamir.json. Here, shamir.json is a JSON file specifying the network, as well as possible pipe diameters and associated costs, per diameter setting. The file 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.

To read in the data, execute the following:

using WaterModels
data = parse_file("examples/data/json/shamir.json")

Finally, the PWLRD formulation for the network design specification can be solved using

import Cbc
run_des(data, PWLRDWaterModel, Cbc.Optimizer)

By default, two breakpoints are used for the linear approximation of each directed head loss curve. 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:

import JuMP
cbc = JuMP.optimizer_with_attributes(Cbc.Optimizer, "seconds" => 30.0)
run_des(data, PWLRDWaterModel, cbc, ext=Dict(:pipe_breakpoints=>5))

Note that this formulation takes much longer to solve to global optimality due to the use of more binary variables. However, because of the finer discretization, a better approximation of the physics is attained.

Instead of using piecewise-linear envelopes, head loss curves can also be simply outer-approximated via the LRD formulation. This formulation employs less strict requirements and avoids the use of binary variables for piecewise approximation, but solutions (e.g., diameters) may not be as close to feasibility with respect to the full (nonconvex) water network physics. To employ five outer approximation points per directed head loss curve in this formulation, the following can be executed:

run_des(data, LRDWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>5))

This relaxation of the problem turns out to converge to the known globally optimal objective value.

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, LRDWaterModel, Cbc.Optimizer)

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:

flows = Dict(name => data["q"] for (name, data) in result["solution"]["des_pipe"])

To determine the design pipes that were selected via the optimization, the following can be used:

pipes_selected = filter(x -> x.second["status"] == 1, result["solution"]["des_pipe"])

To retrieve the subset of the original pipe dataset, the following can be used:

pipes_subset = filter(x -> x.first in keys(pipes_selected), data["des_pipe"])

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 (NC) can be solved via

import KNITRO
run_des(data, NCWaterModel, KNITRO.Optimizer)

Modifying Network Data

The following example demonstrates one way to perform multiple WaterModels solves while modifying network data:

run_des(data, LRDWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>3))

data["demand"]["3"]["flow_min"] *= 0.5
data["demand"]["3"]["flow_max"] *= 0.5
data["demand"]["3"]["flow_nominal"] *= 0.5

run_des(data, LRDWaterModel, Cbc.Optimizer, ext=Dict(:pipe_breakpoints=>3))

Note that the smaller demands in the second problem result in an overall smaller 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, LRDWaterModel, WaterModels.build_des);

print(wm.model)

result = WaterModels._IM.optimize_model!(wm, optimizer=Cbc.Optimizer)