This repository is written and maintained by Connor Wilson, who can be reached at [email protected].
It comprises research performed at Northeastern University's Data Visualization Lab by Connor Wilson, Eduardo Puerta, Tarik Crnovrsanin, and Sara Di Bartolomeo, under the advising of Cody Dunne.
A free copy of our paper, Evaluating and extending speedup techniques for optimal crossing minimization in layered graph drawings, is available at https://osf.io/5vq79.
- A Gurobi license is recommended to run this app, but not required. Visit Gurobi for Academics and Researchers for instructions on obtaining an individual academic license
- The only requirement is to fill out an online form, processing typically takes no more than a few minutes.
- Install the required packages
- Run
main.py
to optimize an example Rome-Lib graph.
Defined in src/optimization.py
, LayeredOptimizer
reads as input a path to graph file or a LayeredGraph
object.
If you do not have a Gurobi license, the HiGHSLayeredOptimizer
class defined in src/optimization_open_src.py
can be used instead.
Besides using a different ILP solver on the back end, it functions identically to LayeredOptimizer
, with the exception that some switch techniques are not available.
Supported file types for the input graph include .graphml
, and text files containing an edge list where each line of the file is an edge u, v
such that u
and v
are integer IDs for nodes.
Any path to a Rome-Lib or AT&T graph included in this repo will also be read automatically.
from src.optimization import LayeredOptimizer
from src.optimization_open_src import HiGHSLayeredOptimizer
optimizer = LayeredOptimizer("[path_to_file]")
optimizer_open_source = HiGHSLayeredOptimizer("[path_to_file]")
Then, set the desired options for your optimizer
:
- Direct transitivity is the default. To switch to vertical position transitivity, set
optimizer.vertical_transitivity = True
andoptimizer.direct_transitivity = False
. - Switches from our paper are set using the following options (note that all switches are off by default):
- Symmetry breaking is set using
optimizer.symmetry_breaking = True
- Butterfly reduction is set using
optimizer.butterfly_reduction = True
- Polyhedral constraints is set using
The 3-claw and dome-path constraints can also be toggled individually by instead usingoptimizer.polyhedral_constraints = True
optimizer.claw_constraints, optimizer.dome_path_constraints
- Mirrored variables with symmetry constraints is set using
optimizer.mirror_vars = True
- Cycle constraints is set using
optimizer.cycle_constraints = True
- Leaf node collapse is set using
optimizer.collapse_leaves = True
- Branching on x-vars is set using
optimizer.xvar_branch_priority = True
- Heuristic starting assignments is set using
optimizer.heuristic_start = True
- Continuous variables is set using
optimizer.mip_relax = True
- Additional options that may be helpful are:
optimizer.cutoff_time
sets the amount of time (in seconds) the optimizer will run before terminating and returning its best found solutionoptimizer.draw_graph
generates the image of the layout as an svg in the /Images folder. It is recommended to also setoptimizer.bendiness_reduction
toTrue
for a prettier, edge-length minimized drawing.optimizer.name
sets the name of the file
optimizer.verbose
prints more information about the solving process
Finally, optimize the graph using
optimizer.optimize_layout()
By default, reading a graph by filepath does not read layer information from the file as well (unless the file is one of the provided DAGmar graphs, which are layered).
If the graph you'd like to optimize has predetermined layer assignments, you'll want to load it as a LayeredGraph
object directly instead of providing a file path directly to the optimizer.
This can be done easily by importing src/read_data.py
and calling
from src.read_data import read
g = read("path_to_file", layering=my_layering)
where my_layering
is a Python dictionary mapping node IDs to layer assignments.
g
can then be passed to a layered optimizer:
optimizer = LayeredOptimizer(g)
# Set optimization parameters, e.g.
optimizer.symmetry_breaking = True
optimizer.cutoff_time = 60 # Halt solver after 60 seconds
optimizer.optimize_layout()
-
src/graph.py
contains our custom layered graph class. A layered graph can be created manually by instantiating the object and using the following functions:g.add_node(layer, name=your_custom_name)
adds a node at with layerlayer
. It is recommended to use integers for the names. Integer names 0, 1, 2,... will be used if no name is specified.g.add_edge(n1, n2)
adds an edge between nodes with namesn1
,n2
.g.add_anchors()
adds dummy nodes along long edges.g.relayer()
is required after inserting dummy nodes. It cleans up the graph by removing empty layers, and optimizing a custom graph may error without it.
The graph data can be accessed as follows:
g.nodes
is a list of the node objects. A node objectn
has IDn.name
, layern.layer
, within-layer positionn.y
and dummy node identifiern.is_anchor_node
.g.edges
is the edge objects.g.node_names
is a dictionary mapping node IDs to node objects.g.layers
is a dictionary mapping layer IDs to the list of node objects in that layer.
-
src/experiments.py
contains functions for all experiments run in our paper. Running this file performs every experiment in our paper (note: this takes an extraordinarily long time to complete, likely close to one month of computation). The methods for running the experiment are detailed at the end of the file. -
src/layering.py
contains functions performing the layer assignment step for graphs without predetermined layer assignments, using the algorithms described in our paper. -
src/vis.py
contains a functiondraw_graph
used to generate images such as the one at the start of this readme. It takes as input a layered graph object and a name for the file. -
scripts
contains a number of bash scripts. The ones with "exp" in their name were used to run experiments for our paper on a HPC cluster.