Skip to main content

mixingmatrix

Compute the edge weights that make averaging, gossip or a random walk converge as fast as possible on a given graph.

This is the fastest-mixing Markov chain problem (Boyd, Diaconis & Xiao 2004) and its free-weight variant FDLA (Xiao & Boyd 2004).

pip install mixingmatrix

Quick start

import networkx as nx
import mixingmatrix

G = nx.random_geometric_graph(60, 0.25, seed=3)
sol = mixingmatrix.solve(G)

sol.weights          # scipy.sparse.csr_matrix, the mixing matrix
sol.to_dict()        # {(u, v): weight}, in your own node labels
sol.slem             # 0.946 — the mixing rate; lower is faster
sol.certified_gap    # 4.1e-04 — the true optimum is within this of sol.slem

print(sol.summary())

Two fields are worth reading every time:

  • sol.status"optimal" means the solver's stopping test was met. Anything else ("max_iter", "time_limit") means it stopped early. The matrix is still valid and usable.
  • sol.certified_gap — how far from optimal you might be. Stays valid even when the solve stopped early.

Is it worth it on your graph?

mixingmatrix.compare(G)
       method    slem  spectral_gap  consensus_rounds  speedup  runtime
      optimal  0.9461       0.05392             249.3    2.29x    10.49
   metropolis  0.9761       0.02395             569.9     1.00     0.001
   max_degree  0.9817       0.01834             746.2     0.76     0.001
best_constant  0.9817       0.01834             746.2     0.76     0.001

speedup is the reduction in rounds to consensus against Metropolis-Hastings, which is what most gossip code already uses. Typical gains across graph families are 1.4x–2.3x, occasionally more on graphs with a bottleneck.

What to call, when

Situation Call
Not sure it's worth doing compare(G)
Up to ~500 nodes solve(G)
Above ~1000 nodes, or an unfamiliar graph solve(G, time_limit=60)
A few edges changed since last time update(sol, changes=[...])
Need full precision, under ~100 nodes solve(G, method="cvxpy")
Distributed averaging, not a Markov chain solve(G, allow_negative=True)
Non-uniform target distribution solve(G, stationary=pi)
Above 10,000 nodes metropolis_hastings(G)

Set time_limit on anything large. Every iterate the solver produces is a valid mixing matrix, so a time limit returns the best one found rather than nothing.

Topology changes

If a few edges are added or removed, don't call solve again:

sol2 = mixingmatrix.update(sol, changes=[("remove", 3, 7), ("add", 1, 9)])
sol2.update_kind      # "certified_free" | "warm" | "cold"

Many single-edge changes provably don't move the optimum, and update skips the solve entirely when that's the case. On a 60-node graph, removing a zero-weight edge takes 0 solver iterations instead of 7745.

See docs/incremental.md.

The optimum is sparse

The optimal weights are often zero on a large fraction of edges, and those edges can be deleted without changing the mixing rate at all:

sol.zero_edges              # edges with weight 0
sol.support_fraction        # fraction of edges actually carrying weight
mixingmatrix.can_remove(sol, u, v).is_free    # True = provably safe to delete

On the 60-node example above, 98 of 260 edges get weight zero. If you are designing a network rather than weighting an existing one, this is often the more useful output.

API

# Solve
mixingmatrix.solve(G, method="auto", tol=1e-6, time_limit=None, ...)
mixingmatrix.update(sol, graph=None, changes=None)
mixingmatrix.compare(G, methods=(...))

# Other weightings, for comparison
mixingmatrix.metropolis_hastings(G)
mixingmatrix.max_degree(G)
mixingmatrix.best_constant(G)
mixingmatrix.lazy_random_walk(G, p=0.5)

# Measure any mixing matrix
mixingmatrix.slem(B)
mixingmatrix.spectral_gap(B)
mixingmatrix.consensus_rounds(B, eps=1e-6)
mixingmatrix.mixing_time(B, eps=1e-4)
mixingmatrix.is_valid(B, G)

# Run the chain
mixingmatrix.gossip(B, x0, rounds=50)
mixingmatrix.rounds_to_consensus(B, x0, eps=1e-6)

# Prove an edge change is safe
mixingmatrix.can_remove(sol, u, v)
mixingmatrix.can_improve(sol, u, v)

Command line:

mixingmatrix solve graph.edgelist --out weights.npz
mixingmatrix compare graph.edgelist
mixingmatrix info graph.edgelist

Full reference: docs/api.md.

Performance

One core, tol=1e-6, random 4-regular graphs:

Nodes Time Result
50 1.7 s converged, gap 7e-05
100 3.2 s converged, gap 2e-04
200 17 s converged, gap 2e-03
400 77 s hit iteration cap, gap 5e-03
800 41 s hit iteration cap, gap 2e-02

Above 1000 nodes, use time_limit and read certified_gap. The solver runs up to 10,000 nodes in a few hundred MB, but does not fully converge at that size — you get a valid matrix better than the baselines, with a large but reliable gap estimate.

Graph structure matters more than size at the top end: the same 10,000-node solve is far cheaper on a grid than on a random regular graph.

If solves are unexpectedly slow, try OMP_NUM_THREADS=1. Multi-threaded BLAS can be dramatically slower on the small matrices this solver uses.

Limitations

  • Graph must be connected and undirected.
  • tol is a residual tolerance, not an error bound. Use certified_gap for accuracy.
  • method="smoothing" cannot run above 2000 nodes.
  • Non-reversible chains (symmetric=False) are not implemented.
  • A better spectral gap does not always mean better decentralised-SGD performance (Vogels et al., NeurIPS 2022).

More detail: docs/limits.md.

Documentation

Runnable examples: examples/quickstart.py, examples/incremental.py.

Contributing

See CONTRIBUTING.md. Bug reports and feature requests go to the issue tracker. Participants are expected to follow the Code of Conduct.

Citing

See CITATION.cff. The papers this implements:

  • Boyd, Diaconis & Xiao, Fastest Mixing Markov Chain on a Graph, SIAM Review 46(4), 2004.
  • Xiao & Boyd, Fast linear iterations for distributed averaging, Systems & Control Letters 53(1), 2004.

MIT licensed.

Release files for mixingmatrix 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mixingmatrix 0.2.0
File Size Uploaded
mixingmatrix-0.2.0.tar.gz 117.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mixingmatrix 0.2.0
File Interpreter ABI Platform
mixingmatrix-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 197.5 kB

Release files / mixingmatrix-0.2.0.tar.gz

Download URL mixingmatrix-0.2.0.tar.gz
Size 117.5 kB
Tags Source
SHA-256 checksum
How to use checksums
7d60f5c2f82ad6f68435a1aa62fa3af1e68ae3acaf180ca9d8d363334bb1d55a
BLAKE2b-256 checksum
How to use checksums
7387b224bd975eca3176a9e2a4d1b4104d29a8c1da54215d3caa62679c0decaa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Release files / mixingmatrix-0.2.0-py3-none-any.whl

Download URL mixingmatrix-0.2.0-py3-none-any.whl
Size 80.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e4a5ffa9ebf125af5949b705a8c036ad45c38cb647da16c2cea9cad14b26618c
BLAKE2b-256 checksum
How to use checksums
b61edaf158f5cd6153e16b75998671f357fd0f1c3be2660606b6142df315de6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page