Skip to content

Circle packing

Circle Packing

Pack circles of given radii to minimize overlap and overall bounding box size, without any nesting or graph structure. Circle packing is one of the simplest examples of optimization for visualization.

%config InlineBackend.figure_formats = ['svg']
import pathlib

import numpy as np
from IPython.display import SVG
from matplotlib import pyplot as plt

from vizopt.animation import (
    SnapshotCallback,
    chronophotograph,
)
from vizopt.base import OptimConfig
from vizopt.templates.circle_packing import CirclePackingOptimizer, plot_circles
fig_folder = pathlib.Path("output") / "figs"
fig_folder.mkdir(parents=True, exist_ok=True)
rng = np.random.default_rng(0)
n_circles = 30
radii = rng.uniform(0.01, 1, size=30).tolist()
initial_node_xys = np.random.rand(n_circles, 2).astype(np.float32) * 5
optimizer = CirclePackingOptimizer(
    radii=radii,
    weight_total_size=10.0,
    collision_offset=0.0,
    initial_node_xys=initial_node_xys,
)
plot_circles(initial_node_xys, radii)

output

The optimizer compresses x and y independently (the total size penalty treats each axis separately), so circles are pushed into a compact rectangular arrangement rather than a circular cluster.

cb = SnapshotCallback(every=50)
result = optimizer.optimize(OptimConfig(n_iters=500, learning_rate=0.03), callback=cb)
optimizer.plot()

output

Animation

svg_str = optimizer.animate_svg(cb, fps=5, log_scale=True, loss_curve_height=200)

SVG(data=svg_str)

output

Chronophotography

A static composite that overlays evenly-spaced snapshots: early frames appear as faint ghosts, the final frame is fully opaque.

fig = chronophotograph(
    optimizer.problem_, cb.snapshots, n_frames=10, alpha_end=0.8, alpha_start=0.5
)
plt.show()

output