From 06390c1d0bc2328ac04da58d8504de0497aaec12 Mon Sep 17 00:00:00 2001 From: Roy Kid Date: Fri, 28 Aug 2026 11:29:05 +0200 Subject: [PATCH] chore: drop benchmarks/ and the bench workflow pytest-benchmark suite and local MD scratch are gone. FrameCollection was a leftover type alias; 0.14 uses Record. --- .github/workflows/bench.yml | 66 ------- .gitignore | 3 - benchmarks/compute/test_cluster.py | 27 --- benchmarks/compute/test_correlation.py | 49 ----- benchmarks/compute/test_density.py | 21 -- benchmarks/compute/test_dielectric.py | 113 ----------- benchmarks/compute/test_distribution.py | 54 ----- benchmarks/compute/test_hbond.py | 22 --- benchmarks/compute/test_ml.py | 24 --- benchmarks/compute/test_order.py | 43 ---- benchmarks/compute/test_pair.py | 30 --- benchmarks/compute/test_shape.py | 52 ----- benchmarks/compute/test_spatial.py | 34 ---- benchmarks/compute/test_spectra.py | 55 ------ benchmarks/compute/test_structure.py | 33 ---- benchmarks/compute/test_transport.py | 77 -------- benchmarks/compute/test_voronoi.py | 39 ---- benchmarks/conftest.py | 253 ------------------------ benchmarks/test_box.py | 36 ---- benchmarks/test_frame.py | 32 --- benchmarks/test_topology.py | 38 ---- pyproject.toml | 5 - 22 files changed, 1106 deletions(-) delete mode 100644 .github/workflows/bench.yml delete mode 100644 benchmarks/compute/test_cluster.py delete mode 100644 benchmarks/compute/test_correlation.py delete mode 100644 benchmarks/compute/test_density.py delete mode 100644 benchmarks/compute/test_dielectric.py delete mode 100644 benchmarks/compute/test_distribution.py delete mode 100644 benchmarks/compute/test_hbond.py delete mode 100644 benchmarks/compute/test_ml.py delete mode 100644 benchmarks/compute/test_order.py delete mode 100644 benchmarks/compute/test_pair.py delete mode 100644 benchmarks/compute/test_shape.py delete mode 100644 benchmarks/compute/test_spatial.py delete mode 100644 benchmarks/compute/test_spectra.py delete mode 100644 benchmarks/compute/test_structure.py delete mode 100644 benchmarks/compute/test_transport.py delete mode 100644 benchmarks/compute/test_voronoi.py delete mode 100644 benchmarks/conftest.py delete mode 100644 benchmarks/test_box.py delete mode 100644 benchmarks/test_frame.py delete mode 100644 benchmarks/test_topology.py diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml deleted file mode 100644 index 689d73a8..00000000 --- a/.github/workflows/bench.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Bench - -# Runs the molpy core benchmark suite (benchmarks/ — Box, Atomistic topology, -# Frame) and tracks it over time via github-action-benchmark (pytest-benchmark -# JSON). The suite measures molpy's thin Python facade over the molrs kernels. -# -# Kept off the per-PR path (benchmarks are slow + noisy on shared runners). -# Release-only cadence: at ~180s it was the largest single cost on every -# master push, and per-commit benchmark points on shared runners are noisy -# enough that the dashboard was tracking runner variance as much as molpy. -# One point per release is a cleaner signal; run it by hand between releases -# when a change is expected to move performance. The normal `pytest tests/` -# run does not pick up benchmarks/. - -on: - push: - tags: ['v*'] - workflow_dispatch: - -permissions: - contents: write # push results to the gh-pages dashboard - deployments: write # github-action-benchmark uses deployment status - -jobs: - bench: - name: molpy core (pytest-benchmark) - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: '3.14' - - - name: Install molpy + bench deps - # molrs (molcrafts-molrs, exact-pinned in pyproject.toml) installs as a - # wheel from PyPI. - run: pip install -e ".[dev]" - - - name: Run core benchmarks - run: | - pytest benchmarks/ \ - --benchmark-json=benchmark-result.json \ - --benchmark-columns=min,mean,median,stddev,rounds - - - name: Upload raw benchmark JSON - if: always() - uses: actions/upload-artifact@v7 - with: - name: molpy-core-bench-${{ github.run_id }} - path: benchmark-result.json - if-no-files-found: error - - - name: Publish benchmark history - if: github.repository == 'MolCrafts/molpy' && github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) - uses: benchmark-action/github-action-benchmark@v1 - with: - tool: 'pytest' - output-file-path: benchmark-result.json - github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: true - alert-threshold: '150%' - comment-on-alert: true - fail-on-alert: false diff --git a/.gitignore b/.gitignore index 719ed262..35017686 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,6 @@ a.out chemfile-testcases/ examples/data/ -# Local MD driver scratch (Kokkos builds, slurm logs, npz traces) -benchmarks/md/ - # C extensions *.so diff --git a/benchmarks/compute/test_cluster.py b/benchmarks/compute/test_cluster.py deleted file mode 100644 index d0a2c707..00000000 --- a/benchmarks/compute/test_cluster.py +++ /dev/null @@ -1,27 +0,0 @@ -"""molpy.compute clustering benchmarks: Cluster, ClusterCenters, ClusterProperties.""" - -from __future__ import annotations - -import pytest - -from molpy.compute import Cluster, ClusterCenters, ClusterProperties - -pytestmark = pytest.mark.benchmark - - -def test_cluster(benchmark, cmp_frame, cmp_nlist) -> None: - op = Cluster(min_cluster_size=1) - out = benchmark(op, cmp_frame, cmp_nlist) - assert out is not None - - -def test_cluster_centers(benchmark, cmp_frame, cmp_nlist) -> None: - clusters = Cluster(min_cluster_size=1)(cmp_frame, cmp_nlist) - out = benchmark(ClusterCenters(), cmp_frame, [clusters]) - assert out is not None - - -def test_cluster_properties(benchmark, cmp_frame, cmp_nlist) -> None: - clusters = Cluster(min_cluster_size=1)(cmp_frame, cmp_nlist) - out = benchmark(ClusterProperties(), cmp_frame, [clusters]) - assert isinstance(out, list) and "sizes" in out[0] diff --git a/benchmarks/compute/test_correlation.py b/benchmarks/compute/test_correlation.py deleted file mode 100644 index 9c95b4d9..00000000 --- a/benchmarks/compute/test_correlation.py +++ /dev/null @@ -1,49 +0,0 @@ -"""molpy.compute time-correlation benchmarks: VanHove and reorientation. - -Van Hove G(r, t) and the first/second Legendre reorientational TCFs — both -consume a small position trajectory. VanHove is frame-only; reorientation is -also frame-only — the ``(tail, head)`` endpoints of each tracked bond vector are -read from each frame's core ``bonds`` topology block. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -import molrs - -import molpy as mp -from molpy.compute import LegendreReorientation, VanHove - -pytestmark = pytest.mark.benchmark - - -def _bonded_chain_frames(n_atoms: int = 20, n_frames: int = 6) -> list["molrs.Frame"]: - """A bonded carbon chain over a few frames; ``(tail, head)`` pairs live in-frame.""" - frames = [] - for t in range(n_frames): - mol = mp.Atomistic() - amp = 0.3 + 0.1 * t - atoms = [ - mol.def_atom(element="C", x=float(i), y=amp * (i % 2), z=0.0) - for i in range(n_atoms) - ] - for i in range(n_atoms - 1): - mol.def_bond(atoms[i], atoms[i + 1]) - frames.append(mol.get_topo().to_frame()) - return frames - - -def test_van_hove(benchmark, pos_traj) -> None: - op = VanHove(n_rbins=50, r_max=10.0, lags=[1, 2, 3]) - out = benchmark(op, pos_traj) - assert np.asarray(out.g_self).shape[0] >= 1 - - -def test_legendre_reorientation(benchmark) -> None: - # Bond vectors are read from each frame's core `bonds` block (no `pairs`). - frames = _bonded_chain_frames() - op = LegendreReorientation(max_lag=3) - out = benchmark(op, frames) - assert np.asarray(out.c1).shape[0] >= 1 diff --git a/benchmarks/compute/test_density.py b/benchmarks/compute/test_density.py deleted file mode 100644 index 89a4c197..00000000 --- a/benchmarks/compute/test_density.py +++ /dev/null @@ -1,21 +0,0 @@ -"""molpy.compute density-field benchmarks: LocalDensity and GaussianDensity.""" - -from __future__ import annotations - -import pytest - -from molpy.compute import GaussianDensity, LocalDensity - -pytestmark = pytest.mark.benchmark - - -def test_local_density(benchmark, cmp_frame, cmp_nlist) -> None: - op = LocalDensity(r_max=3.0, diameter=0.0) - out = benchmark(op, cmp_frame, cmp_nlist) - assert isinstance(out, list) and len(out) >= 1 - - -def test_gaussian_density(benchmark, cmp_frame) -> None: - op = GaussianDensity(nx=16, ny=16, nz=16, sigma=1.0) - out = benchmark(op, cmp_frame) - assert isinstance(out, list) and len(out) >= 1 diff --git a/benchmarks/compute/test_dielectric.py b/benchmarks/compute/test_dielectric.py deleted file mode 100644 index c69a66b6..00000000 --- a/benchmarks/compute/test_dielectric.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Dielectric / conductivity benchmarks against molrs Computes (array API). - -Recipe wrappers (ACFAnalyzer, DielectricSusceptibility, IonicConductivity) are -gone — compose raw curves with Fits, same surface as molrs. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import ( - Acf, - DebyeFit, - DebyeRelaxation, - Dielectric, - EinsteinConductivity, - EinsteinHelfandSpectrum, - GreenKuboConductivity, - GreenKuboSpectrum, -) - -pytestmark = pytest.mark.benchmark - - -def test_acf(benchmark) -> None: - # Acf expects (n_frames, n_entities, n_components) - series = np.zeros((32, 1, 3), dtype=np.float64) - series[:, 0, 0] = np.linspace(1.0, 0.2, 32) - - def run(): - return Acf().compute(series, max_lag=8) - - out = benchmark(run) - assert len(out.acf) == 9 - - -def test_debye_relaxation(benchmark) -> None: - rng = np.random.default_rng(0) - M = np.cumsum(rng.normal(size=(40, 3)) * 0.1, axis=0) - - def run(): - return DebyeRelaxation(volume=1000.0, temperature=300.0).compute(M, 1.0, 10) - - raw = benchmark(run) - assert raw["acf"].shape[0] == 11 - - -def test_einstein_helfand_spectrum(benchmark) -> None: - rng = np.random.default_rng(1) - M = np.cumsum(rng.normal(size=(40, 3)) * 0.1, axis=0) - raw = DebyeRelaxation(volume=1000.0, temperature=300.0).compute(M, 1.0, 10) - fit = EinsteinHelfandSpectrum( - dt=1.0, - volume=raw["volume"], - temperature=raw["temperature"], - epsilon_inf=1.0, - zero_lag_variance=raw["zero_lag_variance"], - ) - - def run(): - return fit.fit(raw["acf"]) - - spec = benchmark(run) - assert "frequencies" in spec and "eps_real" in spec - - -def test_green_kubo_spectrum(benchmark) -> None: - j = np.ones((32, 3), dtype=np.float64) - j[:, 1:] = 0.0 - raw = GreenKuboConductivity().compute(j, 1.0, 10) - fit = GreenKuboSpectrum( - dt=1.0, volume=1000.0, temperature=300.0, epsilon_inf=1.0 - ) - - def run(): - return fit.fit(raw["jacf"]) - - spec = benchmark(run) - assert "frequencies" in spec and "eps_real" in spec - - -def test_debye_fit(benchmark) -> None: - phi = np.exp(-np.arange(20, dtype=np.float64) / 5.0) - - def run(): - return DebyeFit().fit(phi, 1.0) - - fit = benchmark(run) - assert fit is not None - - -def test_static_dielectric(benchmark) -> None: - rng = np.random.default_rng(2) - M = rng.normal(size=(30, 3)) - - def run(): - return Dielectric.static_dielectric_constant(M, 1000.0, 300.0, 1.0) - - eps = benchmark(run) - assert np.isfinite(eps) - - -def test_einstein_conductivity_from_dipole(benchmark) -> None: - """Ionic-conductivity EH path: M = Σ q r → EinsteinConductivity.""" - m = np.zeros((40, 3), dtype=np.float64) - m[:, 0] = np.linspace(0.0, 2.0, 40) - - def run(): - return EinsteinConductivity().compute(m, 1.0, 10) - - raw = benchmark(run) - assert raw["msd"].shape[0] == 11 diff --git a/benchmarks/compute/test_distribution.py b/benchmarks/compute/test_distribution.py deleted file mode 100644 index 4ce45c60..00000000 --- a/benchmarks/compute/test_distribution.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Geometric distribution regression benchmarks — ADF / DDF / distance-DF, CDF. - -The molrs kernel reads the atom tuples to histogram from the frame's core -topology blocks (``bonds`` / ``angles`` / ``dihedrals``), so these forward -``([frame])`` only — no separate ``groups`` index array. Regression sizing: one -small perceived chain. -""" - -from __future__ import annotations - -import pytest - -import molpy as mp - -pytestmark = pytest.mark.benchmark - - -@pytest.fixture(scope="module") -def topo_frame(): - """Small carbon chain with coords + perceived angle/dihedral topology.""" - n = 60 - mol = mp.Atomistic() - atoms = [ - mol.def_atom(element="C", x=float(i), y=0.3 * (i % 2), z=0.0) for i in range(n) - ] - for i in range(n - 1): - mol.def_bond(atoms[i], atoms[i + 1]) - return mol.get_topo(gen_angle=True, gen_dihe=True).to_frame() - - -def test_distance_distribution(benchmark, topo_frame): - op = mp.compute.DistanceDistribution(50, 0.0, 6.0) - result = benchmark(lambda: op([topo_frame])) - assert result.density.shape == (50,) - - -def test_angle_distribution(benchmark, topo_frame): - op = mp.compute.AngleDistribution(60, 0.0, 180.0) - result = benchmark(lambda: op([topo_frame])) - assert result.density.shape == (60,) - - -def test_dihedral_distribution(benchmark, topo_frame): - op = mp.compute.DihedralDistribution(60) - result = benchmark(lambda: op([topo_frame])) - assert result.density.shape == (60,) - - -def test_combined_distribution(benchmark, topo_frame): - op = mp.compute.CombinedDistribution( - [("angle", 30, 0.0, 180.0, True), ("angle", 30, 0.0, 180.0, True)] - ) - result = benchmark(lambda: op([topo_frame])) - assert result.ndim == 2 diff --git a/benchmarks/compute/test_hbond.py b/benchmarks/compute/test_hbond.py deleted file mode 100644 index 2c296f1d..00000000 --- a/benchmarks/compute/test_hbond.py +++ /dev/null @@ -1,22 +0,0 @@ -"""molpy.compute hydrogen-bond detection benchmark. - -Per-frame geometric H-bond search from explicit ``(D, H)`` donor pairs and -acceptor indices under the Luzar-Chandler criterion. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import HBondCriterion, HBonds - -pytestmark = pytest.mark.benchmark - - -def test_hbonds(benchmark, cmp_frame) -> None: - donors = np.array([[i, i + 1] for i in range(0, 200, 2)], dtype=np.int64) - acceptors = np.arange(300, 400, dtype=np.int64) - op = HBonds(donors, acceptors, HBondCriterion(dist_cutoff=3.5, angle_cutoff=150.0)) - out = benchmark(op, [cmp_frame]) - assert np.asarray(out.counts).shape == (1,) diff --git a/benchmarks/compute/test_ml.py b/benchmarks/compute/test_ml.py deleted file mode 100644 index a43b4c51..00000000 --- a/benchmarks/compute/test_ml.py +++ /dev/null @@ -1,24 +0,0 @@ -"""molpy.compute ML-primitive benchmarks: PCA and k-means. - -Two-component PCA over a list of descriptor rows, then k-means over the PCA -result (the ``PcaResult`` feeds ``KMeans`` directly). -""" - -from __future__ import annotations - -import pytest - -from molpy.compute import KMeans, Pca - -pytestmark = pytest.mark.benchmark - - -def test_pca(benchmark, descriptor_rows) -> None: - out = benchmark(Pca(), descriptor_rows) - assert out is not None - - -def test_kmeans(benchmark, descriptor_rows) -> None: - pca_result = Pca()(descriptor_rows) - out = benchmark(KMeans(k=3), pca_result) - assert out is not None diff --git a/benchmarks/compute/test_order.py b/benchmarks/compute/test_order.py deleted file mode 100644 index 246ced97..00000000 --- a/benchmarks/compute/test_order.py +++ /dev/null @@ -1,43 +0,0 @@ -"""molpy.compute bond-orientational order benchmarks. - -Steinhardt / Hexatic / Nematic / SolidLiquid — thin shells over -``molrs.compute.order``. Each returns a per-frame list; the bench asserts a -non-empty result so a dispatch regression fails too. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import Hexatic, Nematic, SolidLiquid, Steinhardt - -pytestmark = pytest.mark.benchmark - - -def test_steinhardt(benchmark, cmp_frame, cmp_nlist) -> None: - op = Steinhardt([4, 6], average=True) - out = benchmark(op, cmp_frame, cmp_nlist) - assert isinstance(out, list) and len(out) >= 1 - - -def test_hexatic(benchmark, cmp_frame, cmp_nlist) -> None: - out = benchmark(Hexatic(6), cmp_frame, cmp_nlist) - assert isinstance(out, list) and len(out) >= 1 - - -def test_solid_liquid(benchmark, cmp_frame, cmp_nlist) -> None: - op = SolidLiquid(6, q_threshold=0.7, n_threshold=6) - out = benchmark(op, cmp_frame, cmp_nlist) - assert isinstance(out, list) and len(out) >= 1 - - -def test_nematic(benchmark, cmp_frame) -> None: - # Per-particle directors come from the frame's `orientations` block (one - # `(head, tail)` atom pair per particle) — no external director array. - n = len(np.asarray(cmp_frame["atoms"]["x"])) - idx = np.arange(n, dtype=np.uint32) - cmp_frame["orientations"] = {"atomi": idx, "atomj": (idx + 1) % n} - # Nematic returns (order, eigenvalues, director, q_tensor). - out = benchmark(Nematic(), cmp_frame) - assert isinstance(out, tuple) and len(out) == 4 diff --git a/benchmarks/compute/test_pair.py b/benchmarks/compute/test_pair.py deleted file mode 100644 index 249898a1..00000000 --- a/benchmarks/compute/test_pair.py +++ /dev/null @@ -1,30 +0,0 @@ -"""molpy.compute pair / neighbor benchmarks: NeighborList and RDF. - -The neighbor list is the shared input to most structural ops; g(r) is the -canonical multi-frame accumulator. Each asserts a cheap structural invariant so -a shape or normalization regression fails the bench, not just a perf one. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import RDF, NeighborList - -pytestmark = pytest.mark.benchmark - - -def test_neighborlist(benchmark, cmp_frame) -> None: - nlist = benchmark(NeighborList(cutoff=3.0), cmp_frame) - assert nlist.n_pairs > 0 - assert (np.sqrt(nlist.dist_sq()) <= 3.0 + 1e-9).all() - - -def test_rdf(benchmark, cmp_frames_nlists) -> None: - frames, nlists = cmp_frames_nlists - rdf = RDF(n_bins=50, r_max=3.0) - result = benchmark(rdf, frames, nlists) - g = np.asarray(result.rdf) - assert g.shape == (50,) - assert np.isfinite(g).all() and (g >= 0.0).all() diff --git a/benchmarks/compute/test_shape.py b/benchmarks/compute/test_shape.py deleted file mode 100644 index 91db59c4..00000000 --- a/benchmarks/compute/test_shape.py +++ /dev/null @@ -1,52 +0,0 @@ -"""molpy.compute per-cluster shape benchmarks. - -CenterOfMass / GyrationTensor / InertiaTensor / RadiusOfGyration reduce the -clusters produced by ``Cluster`` into shape descriptors. The clusters and the -centers each op consumes are built outside the timed region. -""" - -from __future__ import annotations - -import pytest - -from molpy.compute import ( - CenterOfMass, - Cluster, - ClusterCenters, - GyrationTensor, - InertiaTensor, - RadiusOfGyration, -) - -pytestmark = pytest.mark.benchmark - - -def _clusters(frame, nlist): - return Cluster(min_cluster_size=1)(frame, nlist) - - -def test_center_of_mass(benchmark, cmp_frame, cmp_nlist) -> None: - clusters = _clusters(cmp_frame, cmp_nlist) - out = benchmark(CenterOfMass(), cmp_frame, [clusters]) - assert out is not None - - -def test_gyration_tensor(benchmark, cmp_frame, cmp_nlist) -> None: - clusters = _clusters(cmp_frame, cmp_nlist) - centers = ClusterCenters()(cmp_frame, [clusters]) - out = benchmark(GyrationTensor(), cmp_frame, [clusters], centers) - assert out is not None - - -def test_inertia_tensor(benchmark, cmp_frame, cmp_nlist) -> None: - clusters = _clusters(cmp_frame, cmp_nlist) - com = CenterOfMass()(cmp_frame, [clusters]) - out = benchmark(InertiaTensor(), cmp_frame, [clusters], com) - assert out is not None - - -def test_radius_of_gyration(benchmark, cmp_frame, cmp_nlist) -> None: - clusters = _clusters(cmp_frame, cmp_nlist) - com = CenterOfMass()(cmp_frame, [clusters]) - out = benchmark(RadiusOfGyration(), cmp_frame, [clusters], com) - assert out is not None diff --git a/benchmarks/compute/test_spatial.py b/benchmarks/compute/test_spatial.py deleted file mode 100644 index fc6b3938..00000000 --- a/benchmarks/compute/test_spatial.py +++ /dev/null @@ -1,34 +0,0 @@ -"""molpy.compute spatial-distribution (SDF) benchmark. - -The 3-D orientation-resolved density on a molecule body-fixed grid. A reference -triplet defines the body frame (Kabsch-aligned to a template); target-atom -density accumulates on the grid. - -The 1-D geometric distributions (DistanceDistribution / AngleDistribution / -DihedralDistribution / CombinedDistribution) are benched in -``test_distribution.py`` — they read their atom tuples from the frame's -``bonds`` / ``angles`` / ``dihedrals`` topology blocks. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import SpatialDistribution - -pytestmark = pytest.mark.benchmark - - -def test_spatial_distribution(benchmark, cmp_frame) -> None: - template = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) - op = SpatialDistribution( - reference=[0, 1, 2], - template=template, - target=list(range(600)), - n=(16, 16, 16), - extent=(4.0, 4.0, 4.0), - bulk_density=0.03, - ) - out = benchmark(op, [cmp_frame]) - assert np.asarray(out.density).size > 0 diff --git a/benchmarks/compute/test_spectra.py b/benchmarks/compute/test_spectra.py deleted file mode 100644 index a161394d..00000000 --- a/benchmarks/compute/test_spectra.py +++ /dev/null @@ -1,55 +0,0 @@ -"""molpy.compute vibrational-spectra benchmarks. - -The spectral transforms take a precomputed autocorrelation curve and the -sampling interval (fs) and return a ``{frequency, intensity}`` spectrum. Power / -IR / VCD take one ACF; Raman / ROA / resonance-Raman take iso + aniso ACFs. -""" - -from __future__ import annotations - -import pytest - -from molpy.compute import ( - IRSpectrum, - PowerSpectrum, - RamanSpectrum, - ResonanceRamanSpectrum, - RoaSpectrum, - VcdSpectrum, -) - -pytestmark = pytest.mark.benchmark - -DT_FS = 0.5 - - -def test_power_spectrum(benchmark, raw_acf) -> None: - out = benchmark(PowerSpectrum(), raw_acf, DT_FS) - assert "frequencies_cm1" in out - - -def test_ir_spectrum(benchmark, raw_acf) -> None: - out = benchmark(IRSpectrum(), raw_acf, DT_FS) - assert "frequencies_cm1" in out - - -def test_vcd_spectrum(benchmark, raw_acf) -> None: - out = benchmark(VcdSpectrum(), raw_acf, DT_FS) - assert "frequencies_cm1" in out - - -def test_raman_spectrum(benchmark, raw_acf) -> None: - op = RamanSpectrum(incident_frequency_cm1=20000.0, temperature_k=300.0) - out = benchmark(op, raw_acf, raw_acf, DT_FS) - assert "frequencies_cm1" in out - - -def test_roa_spectrum(benchmark, raw_acf) -> None: - out = benchmark(RoaSpectrum(averaged=True), raw_acf, raw_acf, DT_FS) - assert "frequencies_cm1" in out - - -def test_resonance_raman_spectrum(benchmark, raw_acf) -> None: - op = ResonanceRamanSpectrum(incident_frequency_cm1=20000.0) - out = benchmark(op, raw_acf, raw_acf, DT_FS) - assert "frequencies_cm1" in out diff --git a/benchmarks/compute/test_structure.py b/benchmarks/compute/test_structure.py deleted file mode 100644 index 0c01ef40..00000000 --- a/benchmarks/compute/test_structure.py +++ /dev/null @@ -1,33 +0,0 @@ -"""molpy.compute structure benchmarks: structure factor, bond order, PMFT. - -StaticStructureFactorDebye (frame only), BondOrder and PMFTXY (frame + nlist) — -thin shells over the molrs diffraction / environment / pmft kernels. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import BondOrder, PMFTXY, StaticStructureFactorDebye - -pytestmark = pytest.mark.benchmark - - -def test_static_structure_factor(benchmark, cmp_frame) -> None: - k = np.linspace(0.5, 8.0, 40) - op = StaticStructureFactorDebye(k) - out = benchmark(op, cmp_frame) - assert isinstance(out, list) and len(out) >= 1 - - -def test_bond_order(benchmark, cmp_frame, cmp_nlist) -> None: - op = BondOrder(n_theta=6, n_phi=6) - out = benchmark(op, cmp_frame, cmp_nlist) - assert isinstance(out, list) and len(out) >= 1 - - -def test_pmft_xy(benchmark, cmp_frame, cmp_nlist) -> None: - op = PMFTXY(x_max=5.0, y_max=5.0, n_x=20, n_y=20) - out = benchmark(op, cmp_frame, cmp_nlist) - assert isinstance(out, list) and len(out) >= 1 diff --git a/benchmarks/compute/test_transport.py b/benchmarks/compute/test_transport.py deleted file mode 100644 index e3a952d2..00000000 --- a/benchmarks/compute/test_transport.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Transport benchmarks against molrs Computes (array API). - -Recipe Trajectory wrappers are gone — bench the same surface as Rust/Python molrs. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import ( - EinsteinConductivity, - GreenKuboConductivity, - MSD, - Onsager, - Persist, -) - -pytestmark = pytest.mark.benchmark - - -def test_msd(benchmark, pos_traj) -> None: - series = benchmark(MSD(), pos_traj) - assert series.mean.shape == (len(pos_traj),) - - -def test_msd_window(benchmark, pos_traj) -> None: - series = benchmark(MSD(method="window"), pos_traj) - assert series.mean.shape == (len(pos_traj),) - - -def test_einstein_conductivity(benchmark) -> None: - m = np.zeros((32, 3)) - m[:, 0] = np.linspace(0.0, 1.0, 32) - - def run(): - return EinsteinConductivity().compute(m, 1.0, 10) - - raw = benchmark(run) - assert raw["msd"].shape[0] == 11 - - -def test_green_kubo_conductivity(benchmark) -> None: - j = np.ones((32, 3)) - j[:, 1:] = 0.0 - - def run(): - return GreenKuboConductivity().compute(j, 1.0, 10) - - raw = benchmark(run) - assert raw["jacf"].shape[0] == 11 - - -def test_onsager_correlation(benchmark) -> None: - p = np.zeros((32, 3)) - p[:, 0] = np.arange(32, dtype=np.float64) - - def run(): - return Onsager.correlation(p, p, 1.0, 8) - - out = benchmark(run) - assert out["correlation"].shape[0] == 9 - - -def test_persist_pair_survival(benchmark) -> None: - coords_i = np.zeros((16, 2, 3)) - coords_j = np.zeros((16, 2, 3)) - coords_j[:, :, 0] = 1.0 - box = np.tile([[10.0, 10.0, 10.0]], (16, 1)) - - def run(): - return Persist.pair_survival_tcf( - coords_i, coords_j, box, 0.1, 3.5, "intermittent", 1.0, 5, False - ) - - out = benchmark(run) - assert out["correlation"].shape[0] >= 1 diff --git a/benchmarks/compute/test_voronoi.py b/benchmarks/compute/test_voronoi.py deleted file mode 100644 index e7d4d173..00000000 --- a/benchmarks/compute/test_voronoi.py +++ /dev/null @@ -1,39 +0,0 @@ -"""molpy.compute radical-Voronoi benchmarks: tessellation, domains, voids. - -RadicalVoronoi builds the power tessellation from positions + radii + box; -``voronoi_domains`` merges same-label cells and ``voronoi_voids`` aggregates -empty cells. (VoronoiIntegration needs a volumetric density grid and is out of -scope for a regression bench.) -""" - -from __future__ import annotations - -import numpy as np -import pytest - -from molpy.compute import RadicalVoronoi, voronoi_domains, voronoi_voids - -pytestmark = pytest.mark.benchmark - - -def test_radical_voronoi(benchmark, voronoi_inputs) -> None: - positions, radii, box = voronoi_inputs - cells = benchmark(RadicalVoronoi(), positions, radii, box) - assert cells.neighbors(0) is not None - - -def test_voronoi_domains(benchmark, voronoi_inputs) -> None: - positions, radii, box = voronoi_inputs - cells = RadicalVoronoi()(positions, radii, box) - rng = np.random.default_rng(2) - labels = (rng.random(len(positions)) > 0.5).astype(np.int64) - out = benchmark(voronoi_domains, cells, labels) - assert isinstance(out, dict) - - -def test_voronoi_voids(benchmark, voronoi_inputs) -> None: - positions, radii, box = voronoi_inputs - cells = RadicalVoronoi()(positions, radii, box) - is_void = np.zeros(len(positions), dtype=bool) - out = benchmark(voronoi_voids, cells, is_void, box.volume) - assert isinstance(out, dict) diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py deleted file mode 100644 index b3bc7caa..00000000 --- a/benchmarks/conftest.py +++ /dev/null @@ -1,253 +0,0 @@ -"""Shared fixtures for the molpy core benchmark suite. - -These benches measure molpy's public ``core/`` surface — the thin Python facade -over the molrs Rust kernels (``Box``, ``Atomistic``, ``Frame``). They live under -``benchmarks/`` (not ``tests/``) so the normal ``pytest tests/`` run does not -pick them up; ``pytest benchmarks/`` (and the bench.yml workflow) runs them. - -Run:: - - pip install -e ".[dev]" # pulls in pytest-benchmark - pytest benchmarks/ --benchmark-only -""" - -from __future__ import annotations - -import numpy as np -import pytest - -import molrs - -import molpy as mp - -# Regression sizing: one small representative size — these benches guard against -# a perf/behaviour regression, not measure peak throughput (see benchmarks/README -# / molrs REG_N=1000). Bump locally if you need a scaling sweep. -SIZES: list[int] = [1_000] -SIZE_IDS: list[str] = ["reg-1k"] - -BOX_LEN: float = 10.0 - - -@pytest.fixture(params=SIZES, ids=SIZE_IDS) -def n(request: pytest.FixtureRequest) -> int: - return request.param - - -@pytest.fixture -def points(n: int) -> np.ndarray: - """N points spanning ``[-L, 2L]`` per axis so wrap/fractional has work to do.""" - rng = np.random.default_rng(0) - return (rng.random((n, 3), dtype=np.float64) * 3.0 - 1.0) * BOX_LEN - - -def make_chain(n: int) -> "mp.Atomistic": - """Linear carbon chain of ``n`` atoms (``n-1`` bonds).""" - mol = mp.Atomistic() - atoms = [mol.def_atom(element="C") for _ in range(n)] - for i in range(n - 1): - mol.def_bond(atoms[i], atoms[i + 1]) - return mol - - -# --------------------------------------------------------------------------- # -# molpy.compute regression-benchmark fixtures # -# --------------------------------------------------------------------------- # -# These are shared by the ``benchmarks/compute/`` tier. Regression sizing: a -# SMALL fixed point cloud (~600 atoms) and a few trajectory frames — enough to -# exercise every ``molpy.compute`` kernel and catch a perf or structural -# regression, NOT to measure peak throughput. pytest-benchmark auto-calibrates -# rounds, so small inputs keep the whole suite fast. - -CMP_N_ATOMS: int = 600 -CMP_BOX_LEN: float = 12.0 -CMP_CUTOFF: float = 3.0 - - -def random_frame( - n: int = CMP_N_ATOMS, box_len: float = CMP_BOX_LEN, seed: int = 0 -) -> "molrs.Frame": - """A cubic periodic frame of ``n`` uniformly random atoms.""" - rng = np.random.default_rng(seed) - xyz = rng.uniform(0.0, box_len, size=(n, 3)) - frame = molrs.Frame() - frame["atoms"] = {"x": xyz[:, 0], "y": xyz[:, 1], "z": xyz[:, 2]} - frame.box = mp.Box.cubic(box_len) - return frame - - -@pytest.fixture -def cmp_frame() -> "molrs.Frame": - """One 600-atom periodic frame.""" - return random_frame() - - -@pytest.fixture -def cmp_frames() -> list["molrs.Frame"]: - """Three independent 600-atom periodic frames.""" - return [random_frame(seed=i) for i in range(3)] - - -@pytest.fixture -def cmp_nlist(cmp_frame: "molrs.Frame"): - """Neighbor list over ``cmp_frame`` at the shared cutoff.""" - from molpy.compute import NeighborList - - return NeighborList(cutoff=CMP_CUTOFF)(cmp_frame) - - -@pytest.fixture -def cmp_frames_nlists(cmp_frames: list["molrs.Frame"]): - """``(frames, nlists)`` for the multi-frame accumulating ops (RDF, ...).""" - from molpy.compute import NeighborList - - nl = NeighborList(cutoff=CMP_CUTOFF) - return cmp_frames, [nl(f) for f in cmp_frames] - - -def _drift_trajectory( - n_frames: int = 13, - box_len: float = 10.0, - velocity: float = 1.0, - with_velocities: bool = False, -): - """Cation (type 1) drifting +velocity/frame in x (wrapped); anion (type 2) fixed.""" - from molpy.core.trajectory import Trajectory - - frames = [] - for i in range(n_frames): - xc = (i * velocity) % box_len - cols = { - "x": np.array([xc, 0.0]), - "y": np.array([0.0, 0.0]), - "z": np.array([0.0, 0.0]), - # Numeric species id. The Frame schema declares `type` as a String - # label; the numeric column the transport computes read is `type_id`. - "type_id": np.array([1, 2], dtype=np.uint32), - } - if with_velocities: - cols["vx"] = np.array([velocity, 0.0]) - cols["vy"] = np.array([0.0, 0.0]) - cols["vz"] = np.array([0.0, 0.0]) - frame = molrs.Frame() - frame["atoms"] = cols - frame.box = mp.Box.cubic(box_len) - frames.append(frame) - return Trajectory(frames) - - -@pytest.fixture -def drift_traj(): - """Two-species drift Trajectory for MCD / Einstein conductivity / Onsager.""" - return _drift_trajectory(velocity=1.0) - - -@pytest.fixture -def current_traj(): - """Two-species Trajectory carrying velocities for Green–Kubo conductivity.""" - return _drift_trajectory(velocity=1.0, with_velocities=True) - - -@pytest.fixture -def pair_traj(): - """A permanently bonded cation-anion pair Trajectory for Persist.""" - from molpy.core.trajectory import Trajectory - - frames = [] - for _ in range(6): - frame = molrs.Frame() - frame["atoms"] = { - "x": np.array([0.0, 0.5]), - "y": np.array([0.0, 0.0]), - "z": np.array([0.0, 0.0]), - "type_id": np.array([1, 2], dtype=np.uint32), - } - frame.box = mp.Box.cubic(100.0) - frames.append(frame) - return Trajectory(frames) - - -@pytest.fixture -def pos_traj() -> list["molrs.Frame"]: - """A few frames of ~300 drifting particles for MSD / VanHove / reorientation.""" - rng = np.random.default_rng(3) - n, box_len = 300, 30.0 - base = rng.uniform(0.0, box_len, size=(n, 3)) - frames = [] - for i in range(8): - xyz = base + i * 0.2 - frame = molrs.Frame() - frame["atoms"] = {"x": xyz[:, 0], "y": xyz[:, 1], "z": xyz[:, 2]} - frame.box = mp.Box.cubic(box_len) - frames.append(frame) - return frames - - -@pytest.fixture -def charge_traj() -> list["molrs.Frame"]: - """20 frames of 8 charged atoms for the dielectric-susceptibility route.""" - rng = np.random.default_rng(5) - n, box_len = 8, 10.0 - frames = [] - for i in range(20): - xyz = rng.random((n, 3)) + i * 0.1 - frame = molrs.Frame() - frame["atoms"] = { - "x": xyz[:, 0], - "y": xyz[:, 1], - "z": xyz[:, 2], - "charge": np.ones(n) * 0.5, - } - frame.box = mp.Box.cubic(box_len) - frames.append(frame) - return frames - - -@pytest.fixture -def ion_traj() -> list["molrs.Frame"]: - """A drifting +/- ion pair over 40 frames for Einstein conductivity.""" - frames = [] - for i in range(40): - frame = molrs.Frame() - frame["atoms"] = { - "x": np.array([1.0 + 0.01 * i, 5.0]), - "y": np.array([0.0, 0.0]), - "z": np.array([0.0, 0.0]), - "charge": np.array([1.0, -1.0]), - } - frame.box = mp.Box.cubic(30.0) - frames.append(frame) - return frames - - -@pytest.fixture -def raw_acf() -> np.ndarray: - """A small raw autocorrelation curve (1-D) for the spectra transforms. - - ``acf_fft`` takes a 1-D series and returns ``max_lag + 1`` lags, - which is exactly the shape the spectral transforms consume. - """ - from molpy.compute import signal - - rng = np.random.default_rng(7) - series = np.ascontiguousarray(rng.standard_normal(512), dtype=np.float64) - return np.asarray(signal.acf_fft(series, 256)) - - -@pytest.fixture -def voronoi_inputs(): - """``(positions, radii, box)`` for the radical-Voronoi tessellation.""" - rng = np.random.default_rng(1) - n = 300 - positions = rng.uniform(0.0, CMP_BOX_LEN, size=(n, 3)) - radii = np.full(n, 1.0) - return positions, radii, mp.Box.cubic(CMP_BOX_LEN) - - -@pytest.fixture -def descriptor_rows(): - """200 eight-dimensional descriptor rows for the PCA / k-means ML ops.""" - from molpy.compute import DescriptorRow - - rng = np.random.default_rng(9) - return [DescriptorRow(rng.random(8)) for _ in range(200)] diff --git a/benchmarks/test_box.py b/benchmarks/test_box.py deleted file mode 100644 index 517e620a..00000000 --- a/benchmarks/test_box.py +++ /dev/null @@ -1,36 +0,0 @@ -"""molpy.core.Box benchmarks: construction and per-point transforms.""" - -from __future__ import annotations - -import numpy as np -import pytest - -import molpy as mp - -from conftest import BOX_LEN - -pytestmark = pytest.mark.benchmark - - -def test_box_cubic_construct(benchmark) -> None: - box = benchmark(mp.Box.cubic, BOX_LEN) - assert box.volume == pytest.approx(BOX_LEN**3) - - -def test_box_make_fractional(benchmark, points: np.ndarray) -> None: - box = mp.Box.cubic(BOX_LEN) - out = benchmark(box.make_fractional, points) - assert out.shape == points.shape - - -def test_box_make_absolute(benchmark, points: np.ndarray) -> None: - box = mp.Box.cubic(BOX_LEN) - frac = box.make_fractional(points) - out = benchmark(box.make_absolute, frac) - assert out.shape == points.shape - - -def test_box_wrap(benchmark, points: np.ndarray) -> None: - box = mp.Box.cubic(BOX_LEN) - out = benchmark(box.wrap, points) - assert out.shape == points.shape diff --git a/benchmarks/test_frame.py b/benchmarks/test_frame.py deleted file mode 100644 index 83df94b6..00000000 --- a/benchmarks/test_frame.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Canonical molrs.Frame benchmarks used by molpy consumers.""" - -from __future__ import annotations - -import numpy as np -import pytest - -import molrs - -pytestmark = pytest.mark.benchmark - - -def _atom_columns(n: int) -> dict[str, np.ndarray]: - rng = np.random.default_rng(0) - return { - "id": np.arange(n, dtype=np.int64), - "x": rng.random(n, dtype=np.float64), - "y": rng.random(n, dtype=np.float64), - "z": rng.random(n, dtype=np.float64), - } - - -def test_frame_create(benchmark, n: int) -> None: - cols = _atom_columns(n) - frame = benchmark(lambda: molrs.Frame(blocks={"atoms": cols})) - assert frame["atoms"].nrows == n - - -def test_frame_block_access(benchmark, n: int) -> None: - frame = molrs.Frame(blocks={"atoms": _atom_columns(n)}) - out = benchmark(lambda: frame["atoms"]["x"]) - assert out.shape == (n,) diff --git a/benchmarks/test_topology.py b/benchmarks/test_topology.py deleted file mode 100644 index 8b5119b3..00000000 --- a/benchmarks/test_topology.py +++ /dev/null @@ -1,38 +0,0 @@ -"""molpy.core.Atomistic topology benchmarks. - -Angle/dihedral perception (``get_topo``) and single-source BFS distances -(``get_topo_distances``) — the core graph operations molpy delegates to the -molrs kernel. Both assert against a linear-chain closed-form reference so a -structural regression fails the bench, not just a perf one. -""" - -from __future__ import annotations - -import pytest - -from conftest import make_chain - -pytestmark = pytest.mark.benchmark - -# Regression sizing: one small representative size (guard, not a scaling study). -SIZES = [1_000] -SIZE_IDS = ["reg-1k"] - - -@pytest.fixture(params=SIZES, ids=SIZE_IDS) -def chain_n(request: pytest.FixtureRequest) -> int: - return request.param - - -def test_get_topo(benchmark, chain_n: int) -> None: - mol = make_chain(chain_n) - topo = benchmark(mol.get_topo, gen_angle=True, gen_dihe=True) - assert topo.n_relations("angles") == chain_n - 2 - assert topo.n_relations("dihedrals") == chain_n - 3 - - -def test_get_topo_distances(benchmark, chain_n: int) -> None: - mol = make_chain(chain_n) - source = next(iter(mol.atoms)) - dists = benchmark(mol.get_topo_distances, source) - assert sorted(dists.values()) == list(range(chain_n)) diff --git a/pyproject.toml b/pyproject.toml index 395bcfdf..66bd523b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ dev = [ "pytest>=6.0.0", "pytest-cov>=3.0.0", "pytest-mock>=3.10.0", - "pytest-benchmark>=5.0", "pytest-xdist>=3.0", "filelock>=3.0", "ruff==0.16.1", @@ -149,12 +148,8 @@ output-format = "full" pythonpath = ["src"] filterwarnings = [ "error", - "ignore:Benchmarks are automatically disabled because xdist plugin is active:pytest_benchmark.logger.PytestBenchmarkWarning", "ignore::pytest.PytestUnraisableExceptionWarning", ] -markers = [ - "benchmark: pytest-benchmark suite under benchmarks/", -] [tool.tox] requires = ["tox>=4.23"]