Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to this project are documented in this file.

The format is based on *Keep a Changelog*, and this project follows *Semantic Versioning*.

## [0.4.2] - 2026-03-04

### Changed

- Vendored Voro++: updated the vendored snapshot to include the upstream numeric robustness fix for
power/Laguerre (radical) pruning (fixes rare cross-platform edge cases in fully periodic power
tessellations).
- Removed the previously vendored local `nextafter`-based `max_radius` inflation patch (no longer needed).

## [0.4.1] - 2026-02-16

### Fixed

- Vendored Voro++: inflate the stored global `max_radius` by 1 ULP (via `nextafter`) in
power/Laguerre mode to make radical pruning robust across platforms.
- Removed a Python-side workaround that recomputed fully periodic orthorhombic power tessellations
via the periodic (triclinic) backend when periodic face-shift assignment failed.
- Updated documentation to reflect the patched vendored Voro++ snapshot.

## [0.4.0] - 2026-02-15

Initial public release.
Expand Down
4 changes: 4 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ This project vendors and links against the following third-party software:
- License: See `vendor/voro++/LICENSE`

pyvoro2 is licensed under the MIT License (see `LICENSE`). The included Voro++ code remains under its original license.

Local modifications:

- None. The vendored Voro++ snapshot is kept unmodified (aside from being vendored into this repository).
20 changes: 5 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# pyvoro2

[![CI](https://github.com/IvanChernyshov/pyvoro2/actions/workflows/ci.yml/badge.svg)](https://github.com/IvanChernyshov/pyvoro2/actions/workflows/ci.yml) [![Docs](https://github.com/IvanChernyshov/pyvoro2/actions/workflows/docs.yml/badge.svg)](https://github.com/IvanChernyshov/pyvoro2/actions/workflows/docs.yml) [![PyPI](https://img.shields.io/pypi/v/pyvoro2.svg)](https://pypi.org/project/pyvoro2/) [![Python Versions](https://img.shields.io/pypi/pyversions/pyvoro2.svg)](https://pypi.org/project/pyvoro2/) [![License](https://img.shields.io/pypi/l/pyvoro2.svg)](https://github.com/IvanChernyshov/pyvoro2/blob/main/LICENSE)

**Documentation:** https://IvanChernyshov.github.io/pyvoro2/


Expand All @@ -19,7 +18,7 @@ matter physics — especially **periodic boundary conditions** and **neighbor gr

pyvoro2 is designed to be **honest and predictable**:

- it vendors and wraps **unmodified** upstream Voro++;
- it vendors and wraps an upstream Voro++ snapshot (with a small numeric robustness patch for power/Laguerre diagrams);
- the core tessellation modes are **standard Voronoi** and **power/Laguerre**.

## Quickstart
Expand Down Expand Up @@ -100,19 +99,10 @@ For stricter post-hoc checks, see:
- `pyvoro2.validate_tessellation(..., level='strict')`
- `pyvoro2.validate_normalized_topology(..., level='strict')`

## Platform note (macOS)

On some macOS builds, fully periodic **power/Laguerre** tessellations can
occasionally produce a non-reciprocal face/neighbor graph. In that case,
requesting `return_face_shifts=True` may raise a `ValueError` because pyvoro2
cannot assign consistent periodic shifts.

Workarounds:

- Avoid requesting shifts (`return_face_shifts=False`), or
- disable shift validation (`validate_face_shifts=False`, shifts may be
unreliable), or
- run strict periodic power workflows on Linux/Windows.
Note: pyvoro2 vendors a Voro++ snapshot that includes the upstream numeric robustness fix for
*power/Laguerre* mode (radical pruning). This avoids rare cross-platform edge cases where fully
periodic power tessellations could yield a non-reciprocal face/neighbor graph under aggressive
floating-point codegen.

## Why use pyvoro2

Expand Down
17 changes: 6 additions & 11 deletions docs/guide/domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,13 @@ Notes:
- `return_face_shifts=True` is supported whenever at least one axis is periodic.
- For non-periodic axes, query points outside the bounds are treated as out-of-domain.

Platform note (macOS):
Robustness note:

- On some macOS builds (compiler/CPU-dependent), fully periodic **power/Laguerre**
tessellations can occasionally produce a non-reciprocal face/neighbor graph.
In that case, requesting `return_face_shifts=True` may raise a `ValueError`
because pyvoro2 cannot assign consistent periodic shifts.

Workarounds (choose what matches your workflow):

- Disable shift validation: `validate_face_shifts=False` (shifts may be unreliable).
- Avoid requesting shifts: `return_face_shifts=False`.
- Use a non-macOS build for strict periodic power workflows.
- pyvoro2 vendors a Voro++ snapshot that includes the upstream numeric robustness fix for fully
periodic **power/Laguerre** tessellations (radical pruning).
- If `return_face_shifts=True` fails with a `ValueError`, it is typically due to a
genuine geometric degeneracy (for example, nearly co-spherical sites) rather than
a platform-specific issue.

## `PeriodicCell` (triclinic, fully periodic)

Expand Down
7 changes: 6 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ matter physics — especially **periodic boundary conditions** and **neighbor gr

pyvoro2 is designed to be **honest and predictable**:

- it vendors and wraps **unmodified** upstream Voro++;
- it vendors and wraps an upstream Voro++ snapshot (with a small numeric robustness patch for power/Laguerre diagrams);
- the core tessellation modes are **standard Voronoi** and **power/Laguerre**.

## Quickstart
Expand Down Expand Up @@ -93,6 +93,11 @@ For stricter post-hoc checks, see:
- `pyvoro2.validate_tessellation(..., level='strict')`
- `pyvoro2.validate_normalized_topology(..., level='strict')`

Note: pyvoro2 vendors a Voro++ snapshot that includes the upstream numeric robustness fix for
*power/Laguerre* mode (radical pruning). This avoids rare cross-platform edge cases where fully
periodic power tessellations could yield a non-reciprocal face/neighbor graph under aggressive
floating-point codegen.

## Why use pyvoro2

Voro++ is fast and feature-rich, but it is a C++ library with a low-level API.
Expand Down
5 changes: 3 additions & 2 deletions docs/project/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ At the core, pyvoro2 exposes only two mathematically standard tessellations:
Voro++ is a widely used C++ library for computing Voronoi cells efficiently in 3D.
It implements robust algorithms and is commonly used in computational physics and materials science.

pyvoro2 vendors an **unmodified** snapshot of upstream Voro++ and builds its Python extension against it.
This keeps the core geometry trustworthy and makes it easy to track upstream behavior.
pyvoro2 vendors a snapshot of upstream Voro++ and builds its Python extension against it.
The vendored snapshot includes the upstream numeric robustness fix for *power/Laguerre* (radical) pruning,
which avoids rare cross-platform edge cases in fully periodic power tessellations.

## When should you use pyvoro2?

Expand Down
2 changes: 1 addition & 1 deletion src/pyvoro2/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

# Keep this as a simple assignment so scikit-build-core can extract it via regex.
__version__ = '0.4.0'
__version__ = '0.4.2'
138 changes: 15 additions & 123 deletions src/pyvoro2/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,85 +342,6 @@ def compute(
else tuple(bool(x) for x in domain.periodic)
)
is_periodic = isinstance(domain, OrthorhombicCell) and any(periodic_flags)
full_periodic_ortho = (
isinstance(domain, OrthorhombicCell)
and periodic_flags == (True, True, True)
)

def _compute_full_periodic_ortho_via_periodic_backend(
*,
mode: Literal['standard', 'power'],
rr: np.ndarray | None,
) -> list[dict[str, Any]]:
"""Compute fully periodic OrthorhombicCell via Voro++ periodic containers.

This is a fallback path for rare platform-dependent issues in the
rectangular container backend (or its face bookkeeping).

It remaps points into the primary cell, shifts the origin to (0,0,0),
calls the periodic container backend, then shifts outputs back.
"""
assert isinstance(domain, OrthorhombicCell)
(xmin, xmax), (ymin, ymax), (zmin, zmax) = bounds
origin = np.asarray((xmin, ymin, zmin), dtype=np.float64)
bx = float(xmax - xmin)
by = float(ymax - ymin)
bz = float(zmax - zmin)

# Wrap into the primary domain (half-open in periodic axes)
pts_w = domain.remap_cart(pts, return_shifts=False)
pts_i = pts_w - origin
cell_params = (bx, 0.0, by, 0.0, 0.0, bz)

if mode == 'standard':
cells_i = core.compute_periodic_standard(
pts_i,
ids_internal,
cell_params,
(nx, ny, nz),
init_mem,
opts,
)
elif mode == 'power':
if rr is None:
raise ValueError('internal error: rr is required for power mode')
cells_i = core.compute_periodic_power(
pts_i,
ids_internal,
rr,
cell_params,
(nx, ny, nz),
init_mem,
opts,
)
else:
raise ValueError(f'unknown mode: {mode}')

if include_empty:
# Voro++ remaps inserted points into the primary cell; mirror that
# for any empty-cell records we inject.
_add_empty_cells_inplace(cells_i, n=n, sites=pts_i, opts=opts)

# Shift outputs back to the original bounds origin.
if float(xmin) != 0.0 or float(ymin) != 0.0 or float(zmin) != 0.0:
ox, oy, oz = float(origin[0]), float(origin[1]), float(origin[2])
for cc in cells_i:
s = cc.get('site')
if s is not None and len(s) == 3:
cc['site'] = [
float(s[0]) + ox,
float(s[1]) + oy,
float(s[2]) + oz,
]
if return_vertices:
vv = cc.get('vertices')
if vv:
cc['vertices'] = [
[float(x) + ox, float(y) + oy, float(z) + oz]
for (x, y, z) in vv
]
return cells_i

if return_face_shifts:
if not is_periodic:
raise ValueError(
Expand Down Expand Up @@ -479,50 +400,21 @@ def _compute_full_periodic_ortho_via_periodic_backend(
if return_face_shifts:
assert isinstance(domain, OrthorhombicCell)
a, b, cvec = domain.lattice_vectors
try:
_add_periodic_face_shifts_inplace(
cells,
lattice_vectors=(a, b, cvec),
periodic_mask=periodic_flags,
mode=mode,
radii=(
np.asarray(radii, dtype=np.float64)
if radii is not None
else None
),
search=int(face_shift_search),
tol=face_shift_tol,
validate=bool(validate_face_shifts),
repair=bool(repair_face_shifts),
)
except ValueError:
# Fallback for rare platform-dependent issues (observed on macOS)
# where the rectangular periodic backend can produce a face/neighbor
# graph that prevents consistent shift assignment.
if full_periodic_ortho and mode == 'power':
cells = _compute_full_periodic_ortho_via_periodic_backend(
mode=mode,
rr=rr,
)
# Re-run shift assignment on fallback output.
_add_periodic_face_shifts_inplace(
cells,
lattice_vectors=(a, b, cvec),
periodic_mask=(True, True, True),
mode=mode,
radii=(
np.asarray(radii, dtype=np.float64)
if radii is not None
else None
),
search=int(face_shift_search),
tol=face_shift_tol,
validate=bool(validate_face_shifts),
repair=bool(repair_face_shifts),
)
else:
raise

_add_periodic_face_shifts_inplace(
cells,
lattice_vectors=(a, b, cvec),
periodic_mask=periodic_flags,
mode=mode,
radii=(
np.asarray(radii, dtype=np.float64)
if radii is not None
else None
),
search=int(face_shift_search),
tol=face_shift_tol,
validate=bool(validate_face_shifts),
repair=bool(repair_face_shifts),
)
if ids_user is not None:
_remap_ids_inplace(cells, ids_user)

Expand Down
Loading