Skip to content

Planar 2D (pyvoro2.planar)

pyvoro2 now ships a dedicated planar 2D namespace:

import pyvoro2.planar as pv2

This is intentionally separate from the 3D top-level API. The goal is to keep both surfaces explicit and mathematically honest:

  • pyvoro2 is the 3D package,
  • pyvoro2.planar is the 2D package.

The current 2D release scope is deliberately limited to the domains that the vendored legacy backend supports well:

  • pv2.Box
  • pv2.RectangularCell

There is no planar PeriodicCell yet. Rectangular periodic domains can be periodic in either or both planar axes.

Planar compute, locate, and ghost_cells use the same strict native-construction contract as their spatial counterparts. Planar points and queries use two columns, and explicit blocks has length 2. init_mem and block counts are positive exact integers, block_size is a positive finite real scalar, and the same exact 1-GiB known-eager-allocation cap applies before native construction.

Planar bounds are copied into nested built-in-float tuples and rectangular periodicity into built-in-Boolean tuples. Public flags require Python or NumPy Boolean scalars; IDs and counts require exact non-Boolean integers; and explicit tolerances must be finite in their documented positive or non-negative range. Box.from_points rejects empty/non-finite inputs before reduction. Rectangular remapping validates finite points and eps and checks signed-int64 shift range before conversion.

Every inserted planar generator must lie in [lo, hi) on non-periodic axes; periodic axes are remapped first. Generator pairs at squared distance at most 1e-10 always raise before native insertion. The public duplicate mode, threshold, and wrap options control only optional diagnostics above that floor. ghost_cells queries are temporary generators and follow the same rule, while locate queries are not inserted.

Basic compute

import numpy as np
import pyvoro2.planar as pv2

pts = np.array([
    [0.2, 0.2],
    [0.8, 0.2],
    [0.5, 0.8],
], dtype=float)

result = pv2.compute(
    pts,
    domain=pv2.Box(((0.0, 1.0), (0.0, 1.0))),
    return_vertices=True,
    return_edges=True,
)

The default return is the same TessellationResult class used in 3D. Its raw planar records are available as result.cells and remain dimension-specific by design:

  • area instead of volume,
  • edges instead of faces,
  • adjacent_shift is a length-2 periodic image shift when requested.

Power weights

Planar power diagrams accept mathematical weights through the same explicit contract as the 3D compute(...) function:

weights = np.array([-0.1, 0.0, 0.2])
result = pv2.compute(
    pts,
    domain=pv2.Box(((0.0, 1.0), (0.0, 1.0))),
    mode='power',
    weights=weights,
    include_empty=True,
)

The convention is \(\pi_i(x)=\lVert x-p_i\rVert^2-w_i\). Weights have squared-length units and may be negative. Internally, one common global shift converts them to non-negative length-unit backend radii, so a common additive change to all weights leaves areas, adjacency, realized edges, and periodic image shifts unchanged within numerical tolerance. Existing radii= calls remain valid and numerically unchanged in power mode, but supplying both representations is an error. Standard mode rejects either representation. weights= is currently a compute(...) argument only. The input and converted representation must remain finite; non-finite input or overflow during conversion raises ValueError before native computation. Finite representability is necessary for conversion but does not guarantee a numerically resolvable native tessellation. Voro++ evaluates radical geometry with binary64 squared-radius arithmetic, so very large absolute radii**2 values or genuine weight ranges relative to squared coordinate/domain scales can lose geometric resolution. There is no universal safe cutoff: the onset depends on scale, geometry, platform, and compiler, and periodic power tessellations are a particularly sensitive regime. pyvoro2 does not silently weaken validation or alter the requested power geometry in this unsupported regime.

Rectangular periodic cells and edge shifts

For periodic rectangular domains, request return_edge_shifts=True when you need the explicit periodic image of the neighboring site:

cell = pv2.RectangularCell(
    ((0.0, 1.0), (0.0, 1.0)),
    periodic=(True, True),
)

result = pv2.compute(
    pts,
    domain=cell,
    return_vertices=True,
    return_edges=True,
    return_edge_shifts=True,
)

The planar wrapper reconstructs these edge shifts in Python and also repairs a legacy backend quirk where some fully periodic adjacencies can otherwise appear with negative neighbor ids.

locate(...) and ghost_cells(...)

The planar namespace mirrors the 3D operation names:

owners = pv2.locate(pts, [[0.1, 0.2], [0.9, 0.2]], domain=cell)

ghost = pv2.ghost_cells(
    pts,
    [[0.5, 0.5]],
    domain=cell,
    return_vertices=True,
    return_edges=True,
)

So the same three high-level questions exist in both dimensions:

  1. compute every cell,
  2. locate the owner of a query point,
  3. compute the hypothetical cell of a query point without inserting it.

Diagnostics and wrapper-level convenience

Planar compute(...) supports the same kind of post-compute convenience that 3D users already expect, but specialized for 2D semantics:

result = pv2.compute(
    pts,
    domain=cell,
    return_diagnostics=True,
)
diag = result.require_tessellation_diagnostics()

For periodic domains, the wrapper automatically computes the temporary geometry needed for reciprocity checks and then strips it back out of result.cells unless you explicitly requested it. The result's boundary and periodic-shift capability flags describe only that final user-visible geometry.

The same holds for normalization convenience:

result = pv2.compute(
    pts,
    domain=cell,
    return_diagnostics=True,
    normalize='topology',
)

This returns the common pv2.TessellationResult bundling:

  • raw cells,
  • optional tessellation diagnostics,
  • optional normalized vertices,
  • optional normalized topology.

Code that deliberately needs raw cell dictionaries can select the explicit low-level output mode:

cells = pv2.compute(pts, domain=cell, output='cells')
cells, diag = pv2.compute(
    pts,
    domain=cell,
    output='cells',
    return_diagnostics=True,
)

output='cells' cannot be combined with normalization, because normalization is structured output rather than a raw-cell side effect.

Planar normalization

The dedicated planar normalization helpers are:

  • pv2.normalize_vertices(...)
  • pv2.normalize_edges(...)
  • pv2.normalize_topology(...)
  • pv2.validate_normalized_topology(...)

In planar topology work, the globally deduplicated boundary objects are edges, not faces.

Planar plotting

For quick inspection, use the optional matplotlib helper:

from pyvoro2.planar import plot_tessellation

fig, ax = plot_tessellation(result.cells, annotate_ids=True)

Install it with:

pip install "pyvoro2[viz2d]"

or install both 2D and 3D visualization helpers with:

pip install "pyvoro2[viz]"

Planar separator fitting

The canonical separator inverse API supports planar domains as well as 3D domains. Use pyvoro2.inverse for the normal fixed-observation workflow and pyvoro2.inverse.separator for advanced models, realization diagnostics, and the experimental active-set outer loop. The solver vocabulary is shared between 2D and 3D; what changes is the meaning of the realized boundary measure:

  • face area in 3D,
  • edge length in 2D.

The current planar domain restriction still applies here: rectangular periodic cells are supported, but there is no planar oblique-periodic PeriodicCell yet.

See Choosing an API and Separator fitting for the canonical imports and lifecycle status.