Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
60d9e03
Adds powerfit core API
IvanChernyshov Mar 14, 2026
91a86d7
Adds self-consistent active-set solver
IvanChernyshov Mar 14, 2026
9b04fc8
Adds new diagnostics for pair constraints
IvanChernyshov Mar 15, 2026
9035368
Adds hard constraint conflict reporting
IvanChernyshov Mar 15, 2026
1d2cb2c
Improves tests
IvanChernyshov Mar 15, 2026
1813149
Cleanups powerfit & removes inverse.py
IvanChernyshov Mar 15, 2026
e72f858
Organizes powerfit into a package
IvanChernyshov Mar 15, 2026
c4c0313
Adds record exports
IvanChernyshov Mar 15, 2026
de7c7aa
Adds powerfit report bundles
IvanChernyshov Mar 15, 2026
1a88aab
Fixes formatting
IvanChernyshov Mar 15, 2026
f8abf54
Adds powerfit JSON serialization helpers
IvanChernyshov Mar 15, 2026
f3fcc89
Improves powerfit docs
IvanChernyshov Mar 15, 2026
dc3ace0
Fixes power-fit related functionality
IvanChernyshov Mar 15, 2026
31b4d31
Refactors power-fit functionality and improves tests
IvanChernyshov Mar 15, 2026
4eaaf42
Extracts 3D functionality to separate helper modules
IvanChernyshov Mar 15, 2026
bceca3f
Adds 2D functionality
IvanChernyshov Mar 16, 2026
5a95bc8
Implements planar diagnostics and normalization
IvanChernyshov Mar 16, 2026
425a78e
Wires planar powerfit realization and active set
IvanChernyshov Mar 16, 2026
93d8010
Implements planar compute diagnostics convenience
IvanChernyshov Mar 16, 2026
1eb23c3
Adds planar compute result convenience
IvanChernyshov Mar 16, 2026
e6bbf29
Finalizes 0.6.0 release scope
IvanChernyshov Mar 16, 2026
c166c10
Adds powerfit diagnostics for connectivity, unaccounted pairs, and ex…
IvanChernyshov Mar 17, 2026
9f9a89d
Adds active-set path diagnostics for powerfit
IvanChernyshov Mar 17, 2026
11530db
Adds sync/release tooling
IvanChernyshov Mar 17, 2026
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
93 changes: 65 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,42 @@ on:
branches: [main]

jobs:
lint:
lint-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Upgrade pip
run: python -m pip install -U pip

- name: Install flake8
run: python -m pip install -U flake8

- name: Run flake8
run: python -m flake8

docs:
- name: Install lint dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .[dev]
- name: Lint
run: flake8 src tests tools
- name: Check notebook exports
run: python tools/export_notebooks.py --check
- name: Check README sync
run: python tools/gen_readme.py --check

docs-and-notebooks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Upgrade pip
run: python -m pip install -U pip

- name: Install documentation dependencies
run: python -m pip install -r docs/requirements.txt

- name: Install full optional stack
run: |
python -m pip install --upgrade pip
python -m pip install .[all]
- name: Validate notebooks
run: python tools/check_notebooks.py
- name: Check generated files
run: |
python tools/export_notebooks.py --check
python tools/gen_readme.py --check
- name: Build docs
env:
PYTHONPATH: ${{ github.workspace }}/src
run: mkdocs build --strict

test:
Expand All @@ -56,19 +56,56 @@ jobs:

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Upgrade pip
run: python -m pip install -U pip

- name: Install build tools
run: python -m pip install -U cmake ninja

- name: Install package
run: python -m pip install .[test]

- name: Run tests
run: pytest -q

build-dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install cmake ninja build twine
- name: Build distributions
run: python -m build
- name: Validate metadata
run: python -m twine check dist/*
- name: Check packaged files
run: python tools/check_dist.py dist
- name: Install built wheel and smoke-test it
run: |
python -m pip install --force-reinstall dist/*.whl
python - <<'PY'
import numpy as np
import pyvoro2 as pv
import pyvoro2.planar as pv2

pts3 = np.array([[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], dtype=float)
cells3 = pv.compute(
pts3,
domain=pv.Box(((-5.0, 5.0), (-5.0, 5.0), (-5.0, 5.0))),
mode='standard',
)
assert len(cells3) == 2

pts2 = np.array([[0.25, 0.5], [0.75, 0.5]], dtype=float)
cells2 = pv2.compute(
pts2,
domain=pv2.Box(((0.0, 1.0), (0.0, 1.0))),
return_edges=True,
)
assert len(cells2) == 2
PY
32 changes: 16 additions & 16 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,36 @@ concurrency:
cancel-in-progress: true

jobs:
build:
build-docs:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- name: Setup Pages
uses: actions/configure-pages@v5

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install documentation dependencies
- name: Install full optional stack
run: |
python -m pip install -U pip
python -m pip install -r docs/requirements.txt

- name: Build docs
env:
PYTHONPATH: ${{ github.workspace }}/src
python -m pip install --upgrade pip
python -m pip install .[all]
- name: Check generated files
run: |
mkdocs build --strict --site-dir site

python tools/export_notebooks.py --check
python tools/gen_readme.py --check
- name: Validate notebooks
run: python tools/check_notebooks.py
- name: Build docs
run: mkdocs build --strict --site-dir site
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: site

deploy:
needs: build
deploy-docs:
needs: build-docs
runs-on: ubuntu-latest
environment:
name: github-pages
Expand Down
107 changes: 107 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,113 @@ 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.6.1] - 2026-03-16

### Added

- Explicit realized-but-unaccounted pair diagnostics in both 3D and planar 2D power-fit realization, including public `UnaccountedRealizedPair` / `UnaccountedRealizedPairError` types and JSON/report export support.
- Structured connectivity diagnostics for low-level fits and self-consistent active-set solves, covering unconstrained points, isolated points, connected components of candidate and active graphs, and whether relative offsets are identified by the data or only by gauge policy.
- Active-set path diagnostics via `result.path_summary` and richer per-iteration `history` rows, so downstream code can distinguish final disconnectedness from transient component splits or transient candidate-absent realized pairs during optimization.
- Repo-root notebook sources plus notebook-export / notebook-check tooling, distribution-content checks, and a one-shot `tools/release_check.py` helper for local publishability validation.

### Changed

- Disconnected standalone fits no longer inherit arbitrary anchor-order gauges: each effective component is centered to mean zero by default, or aligned to the regularization-reference mean when a zero-strength reference is supplied.
- Self-consistent active-set fitting now preserves offsets per connected component of the current active effective graph by aligning each component to the previous iterate, including the final recomputed fit returned to the user.
- `weights_to_radii(...)` and the fitting APIs now support an explicit `weight_shift=` gauge, while keeping `r_min=` as a backward-compatible convenience rather than the primary convention.
- Power-fit reports now serialize connectivity diagnostics, unaccounted realized pairs, realized-diagnostics warnings, and active-set path summaries through the plain-Python report helpers.
- The example notebooks now live in a repo-root `notebooks/` directory and are exported into generated docs pages, while `README.md` and docs deployment are checked for sync in CI.
- The package metadata now includes a convenience `pyvoro2[all]` extra for contributors who want the full optional notebook/docs/release-check stack.
- The optional planar `plot_tessellation(...)` helper now accepts `domain=` and `show_sites=` to match the published guide examples.

### Fixed

- Active-set reports now nest the final low-level fit against the final active constraint subset rather than the full candidate table.
- Periodic self-image boundaries are excluded from the new unaccounted-pair diagnostics, so wrong-shift reporting does not misclassify self-adjacencies as missing candidate pairs.

## [0.6.0] - 2026-03-16

### Added

- New `pyvoro2.planar` namespace with the first 2D public surface: `Box`, `RectangularCell`, `compute`, `locate`, `ghost_cells`, duplicate checking, edge-property annotation, and optional matplotlib visualization helpers.
- Vendored legacy Voro++ 2D backend is now wired into the build as a separate `_core2d` extension target.
- New planar edge-shift reconstruction helper and pre-wheel integration tests that skip cleanly until `_core2d` wheels are available.
- New planar tessellation diagnostics and strict validation helpers: `analyze_tessellation(...)` and `validate_tessellation(...)`.
- New planar normalization helpers: `normalize_vertices(...)`, `normalize_topology(...)`, and `validate_normalized_topology(...)`.
- New `pyvoro2.planar.PlanarComputeResult` for structured wrapper-level compute results carrying raw cells, optional tessellation diagnostics, and optional normalized outputs.
- `pyvoro2.powerfit` realized-boundary matching and self-consistent active-set refinement now support planar 2D domains in addition to the original 3D path.

### Changed

- `pyvoro2.planar.compute(...)` now supports wrapper-level tessellation diagnostics (`return_diagnostics=...`, `tessellation_check=...`) and structured normalization convenience (`normalize='vertices'|'topology'`, `return_result=True`), automatically computing temporary periodic edge shifts/geometry when needed and stripping the temporary fields back out of the raw returned cells unless they were explicitly requested.
- `tools/install_wheel_overlay.py` now understands both `_core` and `_core2d`, so the editable-style wheel-overlay workflow can carry planar support once new wheels are built.
- Package metadata, release notes, and top-level documentation now describe the frozen 0.6.0 release rather than the earlier development snapshot.
- `resolve_pair_bisector_constraints(...)` now accepts both planar (2D) and spatial (3D) point sets, with dimension-aware shift validation and nearest-image resolution.
- Power-fit reports now serialize both 2D and 3D tessellation diagnostics through a shared measure-oriented schema while preserving the existing area/volume-specific fields.

### Fixed

- Periodic 2D edge reconstruction now resolves hidden periodic adjacencies that the legacy backend can surface as negative neighbor ids, so fully periodic planar tessellations expose consistent neighbor/shift data to diagnostics and normalization utilities.

## [0.5.1] - 2026-03-15


### Added

- `tools/install_wheel_overlay.py` to support a wheel-core + repository-source
development workflow, so the compiled extension can come from an installed
wheel while Python imports resolve to `src/pyvoro2`.
- `DEV_PLAN.md` in the repository root with the planned 0.6.x refactoring and
2D implementation roadmap, including the current decision to ship planar 2D
against the existing dedicated 2D backend before considering a later
`voro-dev` migration.

### Changed

- Public API validation and block-grid resolution are now routed through shared
internal helpers (`_inputs.py`, `_domain_geometry.py`) so 3D wrappers and the
power-fit layer no longer duplicate the same coercion and geometry logic.
- Project status metadata is now consistently marked as **beta** across the
package metadata and top-level documentation.

### Fixed

- Power-fit input validation now rejects non-finite point coordinates,
constraint values, confidence weights, and non-finite radius/weight
conversion inputs.
- `resolve_pair_bisector_constraints(...)` now validates external `ids`
consistently, including shape/length and uniqueness checks.
- The quadratic/analytic power-fit solver no longer crashes on zero-confidence
constraints that would otherwise create singular gauge coupling.
- Empty resolved constraint sets now respect L2 regularization and return the
regularization-only solution instead of silently dropping the reference.
- `fit_power_weights(...)` and the active-set driver now return the documented
`numerical_failure` status for linear-algebra and non-finite-iterate failures
instead of surfacing them as uncaught exceptions or misclassified active-set
infeasibility.
- Triclinic nearest-image resolution now warns when a chosen image touches the
`image_search` boundary, making the search-window sensitivity explicit for
skewed periodic cells.

## [0.5.0] - 2026-03-14

### Added

- New `pyvoro2.powerfit` API for inverse power fitting from generic pairwise bisector constraints.
- Power-fitting results now export plain-Python record rows for downstream reporting and diagnostics.
- Hard infeasibility reporting is simplified around explicit contradiction witnesses.
- `resolve_pair_bisector_constraints(...)` as a reusable low-level constraint-resolution primitive.
- `fit_power_weights(...)` with configurable mismatch, hard feasibility, soft penalties, and explicit infeasibility reporting.
- `match_realized_pairs(...)` for purely geometric realized-face matching with optional tessellation diagnostics.
- `solve_self_consistent_power_weights(...)` for hysteretic active-set refinement driven by realized faces.
- Rich per-constraint diagnostics, marginal-pair reporting, and optional final tessellation diagnostics.

### Changed

- The inverse-fitting surface is now math-oriented and chemistry-agnostic.
- Documentation and examples now describe the unified power-fitting workflow.
- The 0.5.x objective-model scope is explicitly documented around the current built-in convex model family.

## [0.4.2] - 2026-03-04

### Changed
Expand Down
78 changes: 47 additions & 31 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

cmake_minimum_required(VERSION 3.20)
project(pyvoro2 LANGUAGES CXX)

Expand All @@ -10,6 +9,7 @@ find_package(pybind11 CONFIG REQUIRED)

# Voro++ sources (vendored)
set(VORO_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/voro++/src")
set(VORO2D_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/voro++/2d/src")

set(VORO_SOURCES
"${VORO_SRC_DIR}/c_loops.cc"
Expand All @@ -24,6 +24,20 @@ set(VORO_SOURCES
"${VORO_SRC_DIR}/wall.cc"
)

set(VORO2D_SOURCES
"${VORO2D_SRC_DIR}/common.cc"
"${VORO2D_SRC_DIR}/cell_2d.cc"
"${VORO2D_SRC_DIR}/container_2d.cc"
"${VORO2D_SRC_DIR}/v_base_2d.cc"
"${VORO2D_SRC_DIR}/v_compute_2d.cc"
"${VORO2D_SRC_DIR}/c_loops_2d.cc"
"${VORO2D_SRC_DIR}/wall_2d.cc"
"${VORO2D_SRC_DIR}/cell_nc_2d.cc"
"${VORO2D_SRC_DIR}/ctr_boundary_2d.cc"
"${VORO2D_SRC_DIR}/ctr_quad_2d.cc"
"${VORO2D_SRC_DIR}/quad_march.cc"
)

pybind11_add_module(_core
cpp/bindings.cpp
${VORO_SOURCES}
Expand All @@ -33,38 +47,40 @@ target_include_directories(_core PRIVATE
"${VORO_SRC_DIR}"
)

# Some compilers warn about old-style casts inside vendored code; keep warnings reasonable.
if (MSVC)
target_compile_options(_core PRIVATE /EHsc)
else()
target_compile_options(_core PRIVATE -O3)
endif()
pybind11_add_module(_core2d
cpp/bindings2d.cpp
${VORO2D_SOURCES}
)

# Place module under the Python package.
#
# On Unix, Python extension modules are built as shared libraries and
# LIBRARY_OUTPUT_DIRECTORY is sufficient. On Windows, extension modules are
# produced as a DLL-like artifact (".pyd") and CMake treats it as a RUNTIME
# output. For editable installs (pip -e / scikit-build-core metadata_editable),
# we must ensure the extension ends up inside the package directory on all
# platforms.
set(_PYVORO2_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/pyvoro2")
set_target_properties(_core PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${_PYVORO2_OUTDIR}"
RUNTIME_OUTPUT_DIRECTORY "${_PYVORO2_OUTDIR}"
ARCHIVE_OUTPUT_DIRECTORY "${_PYVORO2_OUTDIR}"
target_include_directories(_core2d PRIVATE
"${VORO2D_SRC_DIR}"
)

# Multi-config generators (Visual Studio) use per-config output directories.
# Mirror the same location for all configurations so editable builds can find
# the extension module regardless of the selected config.
foreach(_cfg DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
string(TOUPPER "${_cfg}" _cfg_uc)
set_target_properties(_core PROPERTIES
LIBRARY_OUTPUT_DIRECTORY_${_cfg_uc} "${_PYVORO2_OUTDIR}"
RUNTIME_OUTPUT_DIRECTORY_${_cfg_uc} "${_PYVORO2_OUTDIR}"
ARCHIVE_OUTPUT_DIRECTORY_${_cfg_uc} "${_PYVORO2_OUTDIR}"
function(configure_pyvoro_module target_name)
if (MSVC)
target_compile_options(${target_name} PRIVATE /EHsc)
else()
target_compile_options(${target_name} PRIVATE -O3)
endif()

set(_PYVORO2_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/pyvoro2")
set_target_properties(${target_name} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${_PYVORO2_OUTDIR}"
RUNTIME_OUTPUT_DIRECTORY "${_PYVORO2_OUTDIR}"
ARCHIVE_OUTPUT_DIRECTORY "${_PYVORO2_OUTDIR}"
)
endforeach()

install(TARGETS _core DESTINATION pyvoro2)
foreach(_cfg DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
string(TOUPPER "${_cfg}" _cfg_uc)
set_target_properties(${target_name} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY_${_cfg_uc} "${_PYVORO2_OUTDIR}"
RUNTIME_OUTPUT_DIRECTORY_${_cfg_uc} "${_PYVORO2_OUTDIR}"
ARCHIVE_OUTPUT_DIRECTORY_${_cfg_uc} "${_PYVORO2_OUTDIR}"
)
endforeach()
endfunction()

configure_pyvoro_module(_core)
configure_pyvoro_module(_core2d)

install(TARGETS _core _core2d DESTINATION pyvoro2)
Loading
Loading