Fitting a pattern to a boundary#

The candidate-field and finite-pattern families of Layout families generate geometry that a fitter scales into the boundary. Pass fitter= to replace it. A candidate fitter scales a field, keeps what lies inside and selects an exact count. A rigid fitter finds the largest single scale and translation that puts all \(N\) points inside.

import matplotlib.pyplot as plt
import numpy as np

import oowl
from oowl import boundaries as B
from oowl import layouts as L
from oowl import strings as S

boundary = B.SmoothBlob(area=np.pi * 3000**2, roughness=0.2, seed=5)
ring = B.Annulus(1.0, 0.55, area=30_000_000)
ring_span = ring.bounds[2] - ring.bounds[0]

The five fitter classes#

RigidPatternFitter is the rigid strategy behind PoissonDisc and String. SelectedCandidateFitter is the candidate strategy. SelectedGridFitter and SelectedCircularFitter only change the recorded method, and SelectedContourFitter also defaults to three phases. The candidate field belongs to the layout, so Grid gets the same scale from either candidate fitter.

roster = {
    "RigidPatternFitter": L.PoissonDisc(80, seed=0, fitter=L.RigidPatternFitter()),
    "SelectedCandidateFitter": L.Grid(80, seed=0, fitter=L.SelectedCandidateFitter()),
    "SelectedGridFitter": L.Grid(80, seed=0, fitter=L.SelectedGridFitter()),
    "SelectedCircularFitter": L.Circular(80, seed=0, fitter=L.SelectedCircularFitter()),
    "SelectedContourFitter": L.Contour(80, seed=0, fitter=L.SelectedContourFitter()),
}
for name, layout in roster.items():
    fit = oowl.Sampler(boundary, layout).sample()[0].metadata["fit"]
    print(f"{name:24s} records {fit['method']:20s} scale {fit['scale']:7.1f} m")
RigidPatternFitter       records poisson_disc         scale  6178.3 m
SelectedCandidateFitter  records selected_candidates  scale   611.4 m
SelectedGridFitter       records selected_grid        scale   611.4 m
SelectedCircularFitter   records selected_circular    scale   603.6 m
SelectedContourFitter    records selected_contours    scale   660.7 m

Where the strategy is visible#

An annulus is the hard case for a rigid fit, since every point adds a shifted copy of the hole to the intersection of feasible translations. Grid and Contour use candidates that already lie inside the ring, and the refit of Grid starts from them. PoissonDisc passes the fitter a verified hint from its hole-aware generation. The straight rows of String cannot straddle the hole at any larger scale, so it keeps to one side of the ring.

guide = S.Parallel(
    S.Straight(), n_strings=4, relative_spacing=0.14, rotation_spread=5, seed=6
)
ring_layouts = {
    "Grid": L.Grid(60, seed=4),
    "Contour": L.Contour(60, seed=4),
    "PoissonDisc": L.PoissonDisc(60, seed=4),
    "String": L.String(guide, 60, seed=4),
}

fig, axes = plt.subplots(1, 4, figsize=(18, 4.8))
for ax, (name, layout) in zip(axes, ring_layouts.items()):
    farm = oowl.Sampler(ring, layout).sample()[0]
    farm.plot(
        ax=ax,
        s=14,
        title=(
            f"{name}: spans {100 * np.ptp(farm.x) / ring_span:.0f}% of the ring\n"
            f"closest pair {farm.minimum_spacing():.0f} m"
        ),
    )
fig.tight_layout()
../_images/c45097f21560e0267e65afc1efa0cc6e257bbe991f69b3243ad25b43b749c104.png

Rigid pattern fitting#

For normalized pattern points \(\mathbf p_i\) and admissible region \(B\), a scale \(s\) is feasible when some common translation \(\mathbf t\) satisfies \(s\mathbf p_i+\mathbf t\in B\) for every point. The feasible translations are

\[ T(s)=\bigcap_{i=1}^{N}\left(B-s\mathbf p_i\right). \]

RigidPatternFitter scans \(s\) down from the largest scale the bounding box allows, since on concave, holed or multipart boundaries \(T(s)\) can be empty at one scale and not at a larger one, and then refines the scale within the step that first fits. A placement counts only once every point passes the boundary’s containment test, and its translation is a point inside \(T(s)\). For Grid and Circular, candidate fitters reuse it as pattern_fitter for a final refit, which can only enlarge the selected pattern.