API Documentation
Module Overview
OptimalTransportNetworks.OptimalTransportNetworks
— ModuleOptimal Transport Networks in Spatial Equilibrium
Pure Julia implementation of the model and algorithms presented in:
Fajgelbaum, P. D., & Schaal, E. (2020). Optimal transport networks in spatial equilibrium. Econometrica, 88(4), 1411-1452.
The library is based on the JuMP modeling framework for mathematical optimization in Julia, and roughly follows the MATLAB OptimalTransportNetworkToolbox (v1.0.4b) provided by the authors. Compared to MATLAB, the Julia library presents a simplified interface. Notably, the graph
object contains both the graph structure and all data parameterizing the network, whereas the param
object only contains (non-spatial) model parameters.
Exported Functions
Problem Setup
init_paramaters()
- Create parameters dictionary
create_graph()
- Create network graph dictionary
apply_geography()
- (Optional) apply geographical features to alter the graph edge weights (network building and traversing costs)
Compute Optimal Network + Refine Solution in Non-Convex Cases
optimal_network()
- Compute optimal network given parameters and graph. Can also be used to just solve the model on a given network.
annealing()
- Refine solution using simulated annealing in non-convex cases (automatically called in optimal_network() if param[:annealing] == true))
Plot Graph (Incl. Network Solution)
plot_graph()
- Plot network graph and optimal infrastructure levels
Helper Functions to Manipulate Graphs
find_node()
- Find index of node that is closest to a given pair of coordinates
add_node()
- Add new node to graph with given coordinates and connected neighbors
remove_node()
- Remove node from graph
Examples
# Convex case
param = init_parameters(K = 10)
graph = create_graph(param)
graph[:Zjn][61] = 10.0
results = optimal_network(param, graph)
plot_graph(graph, results[:Ijk])
# Nonconvex case, disabling automatic annealing
param = init_parameters(K = 10, gamma = 2, annealing = false)
graph = create_graph(param)
graph[:Zjn][61] = 10.0
results = optimal_network(param, graph)
# Run annealing
results_annealing = annealing(param, graph, results[:Ijk])
# Comparison
plot_graph(graph, results[:Ijk])
plot_graph(graph, results_annealing[:Ijk])
Functions Index
OptimalTransportNetworks.add_node
OptimalTransportNetworks.annealing
OptimalTransportNetworks.apply_geography
OptimalTransportNetworks.create_auxdata
OptimalTransportNetworks.create_custom
OptimalTransportNetworks.create_graph
OptimalTransportNetworks.create_map
OptimalTransportNetworks.create_square
OptimalTransportNetworks.create_triangle
OptimalTransportNetworks.dict_to_namedtuple
OptimalTransportNetworks.find_node
OptimalTransportNetworks.get_model
OptimalTransportNetworks.init_parameters
OptimalTransportNetworks.namedtuple_to_dict
OptimalTransportNetworks.optimal_network
OptimalTransportNetworks.plot_graph
OptimalTransportNetworks.remove_node
OptimalTransportNetworks.represent_edges
Exported Functions
OptimalTransportNetworks.init_parameters
— Functioninit_parameters(; kwargs...) -> Dict
Returns a param
dict with the model parameters. These are independent of the graph structure and dimensions.
Keyword Arguments
alpha::Float64=0.5
: Cobb-Douglas coefficient on final good c^alpha * h^(1-alpha)beta::Float64=1
: Parameter governing congestion in transport costgamma::Float64=1
: Elasticity of transport cost relative to infrastructureK::Float64=1
: Amount of concrete/asphalt (infrastructure budget)sigma::Float64=5
: Elasticity of substitution across goods (CES)rho::Float64=2
: Curvature in utility (c^alpha * h^(1-alpha))^(1-rho)/(1-rho)a::Float64=0.8
: Curvature of the production function L^alphaN::Int64=1
: Number of goods traded in the economy (used for checks increate_graph()
)labor_mobility::Any=false
: Switch for labor mobility (true/false or 'partial')cross_good_congestion::Bool=false
: Switch for cross-good congestionnu::Float64=2
: Elasticity of substitution b/w goods in transport costs if cross-good congestionm::Vector{Float64}=ones(N)
: Vector of weights Nx1 in the cross congestion cost functionannealing::Bool=true
: Switch for the use of annealing at the end of iterations (only if gamma > beta)verbose::Bool=true
: Switch to turn on/off text output (from Ipopt or other optimizers)duality::Bool=true
: Switch to turn on/off duality whenever available (fixed labor and beta <= 1).warm_start::Bool=true
: Use the previous solution as a warm start for the next iterationkappa_min::Float64=1e-5
: Minimum value for road capacities Kmin_iter::Int64=20
: Minimum number of iterationsmax_iter::Int64=200
: Maximum number of iterationstol::Float64=1e-5
: Tolerance for convergence of road capacities K
Optional Parameters
optimizer = Ipopt.Optimizer
: Optimizer to be usedoptimizer_attr::Dict
: Dict of attributes passed to the optimizer (e.g.Dict(:tol => 1e-5)
)model_attr::Dict
: Dict of tuples (length 2) passed to the model (e.g.Dict(:backend => (MOI.AutomaticDifferentiationBackend(), MathOptSymbolicAD.DefaultBackend()))
to use Symbolic AD)model::Function
: For custom models => a function that taks an optimizer and an 'auxdata' structure as created bycreate_auxdata()
as input and returns a fully parameterized JuMP modelrecover_allocation::Function
: For custom models => a function that takes a solved JuMP model and 'auxdata' structure as input and returns the allocation variables. In particular, it should return a dict with symbol keys returning at least objects :welfare => scalar welfare measure, :Pjn => prices, :PCj => aggregate consumption, and :Qjkn => flows.
Examples
param = init_parameters(K = 10, labor_mobility = true)
OptimalTransportNetworks.create_graph
— Functioncreate_graph(param, w = 11, h = 11; type = "map", kwargs...) -> Dict
Initialize the underlying graph, population and productivity parameters.
Arguments
param::Dict
: Dict that contains the model parameters (only needed for checks)w::Int64=11
: Number of nodes along the width of the underlying graph if type != "custom" (integer)h::Int64=11
: Number of nodes along the height of the underlying graph if type != "custom" (integer, odd if triangle)
Keyword Arguments
type::String="map"
: Either "map", "square", "triangle", or "custom"adjacency::BitMatrix
: J x J Adjacency matrix (only used for custom network)x::Vector{Float64}
: x coordinate (longitude) of each node (only used for custom network)y::Vector{Float64}
: y coordinate (latitude) of each node (only used for custom network)omega::Vector{Float64}
: Vector of Pareto weights for each node or region in partial mobility case (default ones(J or nregions))Zjn::Matrix{Float64}
: J x N matrix of producties per node (j = 1:J) and good (n = 1:N) (default ones(J, N))Lj::Vector{Float64}
: Vector of populations in each node (j = 1:J) (only for fixed labour case)Hj::Vector{Float64}
: Vector of immobile good in each node (j = 1:J) (e.g. housing, default ones(J))Lr::Vector{Float64}
: Vector of populations in each region (r = 1:nregions) (only for partial mobility)region::Vector{Int64}
: Vector indicating region of each location (only for partial mobility)
Examples
graph = create_graph(init_parameters())
OptimalTransportNetworks.apply_geography
— Functionapply_geography(graph, geography; kwargs...) -> Dict
Update the network building costs of a graph based on geographical features and remove edges impeded by geographical barriers. Aversion to altitude changes rescales building infrastructure costs delta_i
by (see also user manual to MATLAB toolbox):
1 + alpha_up * max(0, Z2-Z1)^beta_up + alpha_down * max(0, Z1-Z2)^beta_down
and similarly for graph traversal costs delta_tau
.
Arguments
graph
: Dict or NamedTuple that contains the network graph to which the geographical features will be applied.geography
: Dict or NamedTuple representing the geographical features, with the following fields:z::Vector{Float64}
: (Optional) J x 1 vector containing the z-coordinate (elevation) for each node, ornothing
if no elevation data.z_is_friction::Bool
: (Optional) logical value indicate thatz
represents friction rather than elevation. In that case, the measure of building cost is the average friction of the two nodes mean(Z1,Z2) rather than the difference Z2-Z1.obstacles::Matrix{Int64}
: (Optional) Nobs x 2 matrix specifying (i, j) pairs of nodes that are connected by obstacles, where Nobs is the number of obstacles, ornothing
if no obstacles.
Keyword Arguments
across_obstacle_delta_i::Float64=Inf
: Rescaling parameter for building cost that crosses an obstacle.along_obstacle_delta_i::Float64=Inf
: Rescaling parameter for building cost that goes along an obstacle.across_obstacle_delta_tau::Float64=Inf
: Rescaling parameter for transport cost that crosses an obstacle.along_obstacle_delta_tau::Float64=Inf
: Rescaling parameter for transport cost that goes along an obstacle.alpha_up_i::Float64=0
: Building cost scale parameter for roads that go up in elevation.beta_up_i::Float64=1
: Building cost elasticity parameter for roads that go up in elevation.alpha_up_tau::Float64=0
: Transport cost scale parameter for roads that go up in elevation.beta_up_tau::Float64=1
: Transport cost elasticity parameter for roads that go up in elevation.alpha_down_i::Float64=0
: Building cost scale parameter for roads that go down in elevation.beta_down_i::Float64=1
: Building cost elasticity parameter for roads that go down in elevation.alpha_down_tau::Float64=0
: Transport cost scale parameter for roads that go down in elevation.beta_down_tau::Float64=1
: Transport cost elasticity parameter for roads that go down in elevation.
Examples
graph = create_graph(init_parameters())
geography = (z = 10*(rand(graph[:J]) .> 0.95), obstacles = [1 15; 70 72])
updated_graph = apply_geography(graph, geography)
plot_graph(updated_graph, geography = geography, obstacles = true)
OptimalTransportNetworks.optimal_network
— Functionoptimal_network(param, graph; I0=nothing, Il=nothing, Iu=nothing,
verbose=false, return_model=0, solve_allocation = false) -> Dict
Solve for the optimal network by solving the inner problem and the outer problem by iterating over the FOCs.
Arguments
param
: Dict or NamedTuple that contains the model's parametersgraph
: Dict or NamedTuple that contains the underlying graph (created bycreate_graph()
function)I0::Matrix{Float64}=nothing
: (Optional) J x J matrix providing the initial guess for the iterationsIl::Matrix{Float64}=nothing
: (Optional) J x J matrix providing exogenous lower bound on infrastructure levelsIu::Matrix{Float64}=nothing
: (Optional) J x J matrix providing exogenous upper bound on infrastructure levelsverbose::Bool=false
: (Optional) tell IPOPT to display resultsreturn_model::Int=0
: (Optional) return the JuMP model and correspondingrecover_allocation()
function: 1 just returns these before solving the model, while 2 solves the model + optimal network and returns the two alongside the results.solve_allocation::Bool=false
: (Optional) just solve the model with existing infrastructure I0 and return the results.
Examples
param = init_parameters(K = 10, duality = false)
graph = create_graph(param)
graph[:Zjn][61] = 10.0
results = optimal_network(param, graph)
plot_graph(graph, results[:Ijk])
OptimalTransportNetworks.annealing
— Functionannealing(param, graph, I0; kwargs...)
Runs the simulated annealing method starting from network I0
. Only sensible if param.gamma > param.beta
.
Arguments
param
: Dict or NamedTuple that contains the model's parametersgraph
: Dict or NamedTuple that contains the underlying graphI0
: (optional) provides the initial guess for the iterations
Keyword Arguments
verbose::Bool=false
: (Optional) tell IPOPT to display resultsperturbation_method::String="random rebranching"
: Method to be used to perturbate the network ("random" is purely random, works horribly; "shake" applies a gaussian blur along a random direction, works alright; "rebranching" (deterministic) and "random rebranching" (default) is the algorithm described in Appendix A.4 in the paper, works nicely)preserve_central_symmetry::Bool=false
: Only applies to "shake" methodpreserve_vertical_symmetry::Bool=false
: Only applies to "shake" methodpreserve_horizontal_symmetry::Bool=false
: Only applies to "shake" methodsmooth_network::Bool=true
: Whether to smooth the network after each perturbation ifperturbation_method
is "random"smoothing_radius::Float64=0.25
: Parameters of the Gaussian blur ifperturbation_method
is "random" or "shake"mu_perturbation::Float64=log(0.3)
: Parameters of the Gaussian blur ifperturbation_method
is "shake"sigma_perturbation::Float64=0.05
: Parameters of the Gaussian blur ifperturbation_method
is "shake"num_random_perturbations::Int64=1
: Number of links to be randomly affected ("random" and "random rebranching" only)display::Bool
: Display the graph in each iteration as we got_start::Float64=100
: Initial temperaturet_end::Float64=1
: Final temperaturet_step::Float64=0.9
: Speed of coolingnum_deepening::Int64=4
: Number of FOC iterations between candidate drawsIu::Matrix{Float64}=Inf * ones(J, J)
: J x J matrix of upper bounds on network infrastructure IjkIl::Matrix{Float64}=zeros(J, J)
: J x J matrix of lower bounds on network infrastructure Ijkfinal_model::JuMPModel
: (Optionally) a readily parameterized JuMP model to be used (fromoptimal_network()
)recover_allocation::Function
: Therecover_allocation()
function corresponding tofinal_model
allocation::Dict
: The result fromrecover_allocation()
from a previous solution of the model: to skip an initial resolve without perturbations.
Examples
# Nonconvex case, disabling automatic annealing
param = init_parameters(K = 10, gamma = 2, annealing = false, duality = false)
graph = create_graph(param)
graph[:Zjn][61] = 10.0
results = optimal_network(param, graph)
# Run annealing
results_annealing = annealing(param, graph, results[:Ijk])
# Comparison
plot_graph(graph, results[:Ijk])
plot_graph(graph, results_annealing[:Ijk])
OptimalTransportNetworks.plot_graph
— Functionplot_graph(graph, edges = nothing; kwargs...) -> Plots.Plot
Plot a graph visualization with various styling options.
Arguments
graph::Dict
: The network graph (created withcreate_graph()
)edges::Matrix{Float64}=nothing
: Matrix of edge weights (J x J)
Keyword Arguments
grid::Bool=false
: Show gridlinesaxis::Tuple=([], false)
: Axis ticks and labels (see Plots.jl docs, default disable axis)margin::Real=0mm
: Margin around plotaspect_ratio::Symbol=:equal
: Plot aspect ratio (set to a real number (h/w) if not :equal)height::Int=600
: Plot height in pixels (width is proportional to height / aspect_ratio, but also depends on the relative ranges of the x and y coordinates of the graph)map::Vector=nothing
: Values mapped to graph for background heatmapmap_color::Symbol=:YlOrBr_4
: Colorscale for background heatmapmesh::Bool=false
: Show mesh lines between nodesmesh_color::Symbol=:grey90
: Color for mesh linesmesh_style::Symbol=:dash
: Linestyle for mesh linesmesh_transparency::Real=1
: Opacity for mesh linesedges::Bool=true
: Show edges between nodesedge_color::Symbol=:blue
: Edge color or color gradientedge_scaling::Bool=false
: Size edges based on raw valuesedge_transparency::Union{Bool,Real}=true
: Transparency for edgesedge_min::Real
: Minimum edge value for scalingedge_max::Real
: Maximum edge value for scalingedge_min_thickness::Real=0.1
: Minimum thickness for edgesedge_max_thickness::Real=2
: Maximum thickness for edgesarrows::Bool=false
: Show arrowheads on edgesarrow_scale::Real=1
: Scaling factor for arrowheadsarrow_style::String="long"
: Style of arrowheads ("long" or "thin")nodes::Bool=true
: Show nodesnode_sizes::Vector=ones(J)
: Sizes for nodesnode_sizes_scale::Real=75
: Overall scaling for node sizesnode_shades::Vector=nothing
: Shades mapped to nodesnode_color::Symbol=:purple
: Node color or color gradientnode_stroke_width::Real=0
: Stroke width for node outlinesnode_stroke_color::Symbol=nothing
: Stroke color for node outlinesgeography=nothing
: Dict or NamedTuple with geography data, see alsoapply_geography()
obstacles::Bool=false
: Show obstacles from geographyobstacle_color::Symbol=:black
: Color for obstaclesobstacle_thickness::Real=3
: Thickness for obstacles
Examples
param = init_parameters(K = 10, duality = false)
graph = create_graph(param)
graph[:Zjn][51] = 10.0
results = optimal_network(param, graph)
plot_graph(graph, results[:Ijk])
OptimalTransportNetworks.find_node
— Functionfind_node(graph, x, y) -> Int64
Returns the index of the node closest to the coordinates (x,y) on the graph.
Arguments
graph::Dict
: Dict that contains the underlying graph (created by create_graph())x::Float64
: x coordinate on the graph (between 1 and w)y::Float64
: y coordinate on the graph (between 1 and h)
OptimalTransportNetworks.add_node
— Functionadd_node(graph, x, y, neighbors) -> Dict
Add a node in position (x,y) and list of neighbors. The new node is given an index J+1. Returns an updated graph
object.
Arguments
graph::Dict
: Dict that contains the underlying graph (created by create_graph())x::Float64
: x coordinate of the new node (any real number)y::Float64
: y coordinate of the new node (any real number)neighbors::Vector{Int64}
: Vector of nodes to which it is connected (1 x n list of node indices between 1 and J, where n is an arbitrary # of neighbors)
Notes
The cost matrices delta_tau
and delta_i
are parametrized as a function of Euclidean distance between nodes. The new node is given population 1e-6 and productivity equal to the minimum productivity in the graph.
OptimalTransportNetworks.remove_node
— Functionremove_node(graph, i) -> Dict
Removes node i from the graph, returning an updated graph
object.
Arguments
graph::Dict
: Dict that contains the underlying graph (created by create_graph())i::Int64
: index of the mode to be removed (integer between 1 and graph[:J])
Documented Internal Functions
OptimalTransportNetworks.dict_to_namedtuple
— Functiondict_to_namedtuple(dict)
Convert a dictionary to a NamedTuple.
If the input is already a NamedTuple, it is returned unchanged. Otherwise, it creates a new NamedTuple from the dictionary's keys and values.
Arguments
dict
: A dictionary or NamedTuple to be converted.
Returns
- A NamedTuple equivalent to the input dictionary.
OptimalTransportNetworks.namedtuple_to_dict
— Functionnamedtuple_to_dict(namedtuple)
Convert a NamedTuple to a dictionary.
If the input is already a dictionary, it is returned unchanged. Otherwise, it creates a new dictionary from the NamedTuple's pairs.
Arguments
namedtuple
: A NamedTuple or dictionary to be converted.
Returns
- A dictionary equivalent to the input NamedTuple.
OptimalTransportNetworks.create_map
— Functioncreate_map(w, h) -> Dict
Creates a square graph structure with width w
and height h
(nodes have 8 neighbors in total, along horizontal and vertical dimensions and diagonals)
Arguments
w
: Width of graph (i.e. the number of nodes along horizontal dimension), must be an integerh
: Height of graph (i.e. the number of nodes along vertical dimension), must be an integer
OptimalTransportNetworks.create_square
— Functioncreate_square(w, h) -> Dict
Creates a square graph structure with width w and height h (nodes have 4 neighbors in total, along horizontal and vertical dimensions, NOT diagonals)
Arguments
w
: width of graph (ie. the number of nodes along horizontal dimension), must be an integerh
: height of graph (ie. the number of nodes along vertical dimension), must be an integer
OptimalTransportNetworks.create_triangle
— Functioncreate_triangle(w, h) -> Dict
Creates a triangular graph structure with width w
and height h
(each node is the center of a hexagon and each node has 6 neighbors, horizontal and along the two diagonals)
Arguments
w
: Width of graph (i.e. the max number of nodes along horizontal dimension), must be an integerh
: Height of graph (i.e. the max number of nodes along vertical dimension), must be an odd integer
OptimalTransportNetworks.create_custom
— Functioncreate_custom(adjacency, x, y) -> Dict
Creates a custom graph structure with given adjacency matrix, x and y vectors of coordinates.
Arguments
adjacency
: Adjacency matrixx
: Vector of x coordinates of locationsy
: Vector of y coordinates of locations
OptimalTransportNetworks.represent_edges
— Functionrepresent_edges(graph)
Creates a NamedTuple providing detailed representation of the graph edges.
Arguments
graph
: NamedTuple that contains the underlying graph (created bydict_to_namedtuple(create_graph())
)
Returns
- A NamedTuple with the following fields:
A
: J x ndeg matrix where each column represents an edge. The value is 1 if the edge starts at node J, -1 if it ends at node J, and 0 otherwise.Apos
: J x ndeg matrix where each column represents an edge. The value is the positive part of the edge flow.Aneg
: J x ndeg matrix where each column represents an edge. The value is the negative part of the edge flow.edge_start
: J x ndeg matrix where each column represents an edge. The value is the starting node of the edge.edge_end
: J x ndeg matrix where each column represents an edge. The value is the ending node of the edge.
OptimalTransportNetworks.create_auxdata
— Functioncreate_auxdata(param, graph, edges, I)
Creates the auxdata structure that contains all the auxiliary parameters for estimation
Arguments
param
: NamedTuple that contains the model's parameters (created bydict_to_namedtuple(init_parameters())
)graph
: NamedTuple that contains the underlying graph (created bydict_to_namedtuple(create_graph())
)edges
: NamedTuple that contains the edges of the graph (created byrepresent_edges()
)I
: J x J symmetric matrix of current infrastructure (investments)
Returns
- A NamedTuple with the following fields:
param
: The input parameter NamedTuple.graph
: The input graph NamedTuple.edges
: The edges of the graph.kappa
: The kappa matrix: I^gamma / delta_taukappa_ex
: The extracted kappa values (ndeg x 1)
OptimalTransportNetworks.get_model
— Functionget_model(auxdata)
Construct the appropriate JuMP model based on the auxiliary data.
Arguments
auxdata
: Auxiliary data required for constructing the model (created bycreate_auxdata()
).
Returns
model
: The constructed JuMP model.recover_allocation
: A function to recover the allocation from the model solution.