Changelog#
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]#
[0.1.17] - 2026-03-19#
Added#
allow_displacementsparameter forStructureOptimizer.Controls whether atomic-position moves (fragment moves) are performed during optimization.
Value
Behavior
True(default)Fragment moves (atomic displacements) are included in the MC step pool — unchanged from v0.1.16
FalseOnly composition moves (element-type swaps) are executed; atomic coordinates are held fixed for the entire run
Use
allow_displacements=Falsewhen exploring compositional disorder on a pre-relaxed geometry (e.g. a fixed lattice). Passing bothallow_displacements=Falseandallow_composition_moves=Falsesimultaneously raises aValueErrorbecause no move type would remain enabled.Applies to all three optimisation methods:
"annealing","basin_hopping", and"parallel_tempering".CLI:
--no-displacementsflag added to the--optimizemode.opt = StructureOptimizer( n_atoms=50, charge=0, mult=1, objective={"H_atom": 1.0, "Q6": -2.0}, elements=["Cr", "Mn", "Fe", "Co", "Ni"], allow_displacements=False, # composition-only optimization max_steps=5000, seed=42, ) result = opt.run(initial=fixed_geometry)
Fixed#
OptimizationResult.methoddocstring now lists all three valid methods ("annealing","basin_hopping","parallel_tempering"); previously"parallel_tempering"was omitted.
[0.1.16] - 2026-03-19#
Added#
allow_composition_movesparameter forStructureOptimizer.Controls whether element-type swaps are performed during optimisation.
Value
Behaviour
True(default)Each MC step randomly chooses between a fragment move (position change) and a composition move (element-type swap) with equal probability — unchanged from v0.1.15
FalseOnly fragment moves are executed; element types are held fixed for the entire run
Use
allow_composition_moves=Falsewhen the composition is predetermined and should not be modified during optimisation (e.g. optimising the geometry of a fixed stoichiometry).Applies to all three optimisation methods:
"annealing","basin_hopping", and"parallel_tempering".CLI:
--no-composition-movesflag added to the--optimizemode.opt = StructureOptimizer( n_atoms=12, charge=0, mult=1, objective={"H_total": 1.0, "Q6": -2.0}, elements="24,25,26,27,28", allow_composition_moves=False, # position-only optimisation max_steps=5000, seed=42, ) result = opt.run(initial=my_structure)
element_fractionsparameter forStructureGenerator/generate().Specifies relative sampling weights per element as a
{symbol: weight}dict. Weights are normalised internally; elements absent from the dict receive weight1.0. Default (None) keeps the original uniform sampling.gen = StructureGenerator( n_atoms=20, charge=0, mult=1, mode="gas", region="sphere:10", elements="6,7,8", element_fractions={"C": 0.6, "N": 0.3, "O": 0.1}, n_samples=50, seed=0, )
CLI:
--element-fractions SYM:WEIGHT(repeatable).pasted --n-atoms 20 --elements 6,7,8 --charge 0 --mult 1 \ --mode gas --region sphere:10 --n-samples 50 \ --element-fractions C:0.6 --element-fractions N:0.3 --element-fractions O:0.1
element_min_countsandelement_max_countsparameters forStructureGenerator/generate().Hard per-element atom count bounds enforced at sampling time.
Parameter
Type
Effect
element_min_countsdict[str, int] | NoneGuaranteed lower bound; atoms are placed first, remaining slots filled by weighted sampling
element_max_countsdict[str, int] | NoneUpper bound; elements that have reached their cap are excluded from further sampling
Both default to
None(no bounds). The generator raisesValueErrorat construction time when constraints are inconsistent (sum of mins exceedsn_atoms, or any min > its paired max). ARuntimeErroris raised during sampling if all elements are simultaneously capped beforen_atomsis reached.gen = StructureGenerator( n_atoms=15, charge=0, mult=1, mode="gas", region="sphere:10", elements="6,7,8,15,16", element_min_counts={"C": 4}, # at least 4 carbon atoms element_max_counts={"N": 3, "O": 3}, # at most 3 N and 3 O n_samples=100, seed=42, )
CLI:
--element-min-counts SYM:Nand--element-max-counts SYM:N(both repeatable).pasted --n-atoms 15 --elements 6,7,8,15,16 --charge 0 --mult 1 \ --mode gas --region sphere:10 --n-samples 100 \ --element-min-counts C:4 \ --element-max-counts N:3 --element-max-counts O:3
20 new tests across
test_generator.pyandtest_optimizer.py:TestElementFractions(6 tests) — bias validation, unknown/negative/zero weight errors, uniform-weight seed parity, functional-API forwarding.TestElementMinMaxCounts(8 tests) — min/max enforcement, combined constraints, sum-exceeds-n_atoms error, min > max error, unknown element errors, impossible-cap RuntimeError.TestAllowCompositionMoves(6 tests) — default True, composition preservation when disabled (SA and PT), still optimises,reprbehaviour.
[0.1.15] - 2026-03-19#
Changed#
place_maxentnow uses L-BFGS with a per-atom trust radius instead of steepest descent.When
HAS_MAXENT_LOOPisTrue(i.e._maxent_core.place_maxent_cppis available), the entire gradient-descent loop runs in C++:Step
v0.1.14 (Python SD)
v0.1.15 (C++ L-BFGS)
Gradient computation
C++
angular_repulsion_gradientC++ (inlined, same Cell List)
Optimiser
steepest descent, fixed
maxent_lrL-BFGS m=7, Armijo backtracking
Step limit
unit-norm clip ×
maxent_lrper-atom trust radius (default 0.5 Å)
Restoring force
Python NumPy
C++
CoM pinning
Python NumPy
C++
Steric relaxation
Python wrapper → C++
C++ direct (embedded PenaltyEvaluator)
list ↔ ndarray conversion
every step
none
Measured wall-time improvement (n_atoms=8–20, n_samples=20, repeats=10):
Scenario
v0.1.13
v0.1.14
v0.1.15
speedup vs 0.1.14
maxent small (n=8)
~157 ms
~156 ms
~7 ms
~22×
maxent medium (n=15)
~310 ms
~300 ms
~29 ms
~10×
maxent large (n=20)
~320 ms
~310 ms
~30 ms
~10×
Output quality (H_total, 30 structures, 3 seeds): C++ L-BFGS mean ≈ 1.09 vs Python SD mean ≈ 1.04. L-BFGS converges to comparable or better local optima with far fewer wall-clock seconds.
The L-BFGS curvature information reduces the number of steps needed for convergence; the trust-radius cap (uniform step rescaling so no atom moves more than
trust_radiusÅ) replaces the fixedmaxent_lrunit-norm clip and provides better convergence on anisotropic landscapes.New C++ function
place_maxent_cppadded to_maxent.cppand exported frompasted._ext. Signature:place_maxent_cpp(pts, radii, cov_scale, region_radius, ang_cutoff, maxent_steps, trust_radius=0.5, seed=-1) -> ndarray(n,3)
New flag
HAS_MAXENT_LOOPinpasted._ext(bool).Truewhenplace_maxent_cppis available.HAS_MAXENTremainsTruewheneverangular_repulsion_gradientis available (unchanged semantics).place_maxentgains atrust_radiusparameter (float, default0.5Å). Ignored by the steepest-descent fallback (which continues to usemaxent_lrand unit-norm clipping). Themaxent_lrparameter is retained for backward compatibility._optimizer._run_onepatched (Metropolis loop):cov_radius_angresults are pre-computed once per restart into aradiiarray and reused every step, eliminating per-step dict lookups.relax_positionsPython wrapper is bypassed;_relax_core.relax_positionsis called directly whenHAS_RELAXisTrue, eliminating per-steplist → ndarray → listconversions.
_maxent.cpprefactored:angular_repulsion_gradientandplace_maxent_cppnow share a singlebuild_nb/eval_angularpair instead of duplicating neighbour-list and gradient logic.
Added#
OptimizationResult— new return type forStructureOptimizer.run().run()previously returned a singleStructure(the best across all restarts). It now returns anOptimizationResultthat collects all per-restart structures sorted by objective value, highest first.OptimizationResultis list-compatible — indexing, iteration,len(), andbool()all work — while also exposing dedicated metadata:Attribute
Description
bestHighest-scoring
Structure— equivalent toresult[0]all_structuresAll per-restart structures, best-first
objective_scoresScalar objective values, same order
n_restarts_attemptedRestarts that produced a valid initial structure
method"annealing"or"basin_hopping"result.summary()returns a one-line diagnostic, e.g.:restarts=5 best_f=1.2294 worst_f=0.8123 method='annealing'
Migration from v0.1.14: code that uses
opt.run()as aStructuremust add.best:opt.run().best. The CLI is already updated. Code that only iterates or indexes the result works without changes.A
UserWarningis emitted when restarts are skipped due to failed initial-structure generation.OptimizationResultadded topasted.__all__and exported from the top-levelpastednamespace.cli.pyupdated:opt.run()→opt.run().bestin the--optimizecode path.Objective alignment verified — SA and BH reliably improve the user-supplied objective over random gas-mode baselines:
Scenario
Baseline mean
Optimized
maximize
H_total(n=8, C+O)0.495
1.229 (+148%)
maximize
H_spatial − 2×Q6(n=12, C/N/O/H)−0.108
+0.892
Temperature schedule confirmed: T decays exponentially from
T_starttoT_endovermax_steps.n_restartsreturns the global best (not just the last restart’s result).16 new tests in
tests/test_optimizer.py:TestOptimizationResult— list interface,best,summary, sort order,repr,n_restartscount.TestObjectiveAlignment— SA and BH both beat random baseline onH_total; negative weight reduces penalized metric; callable objective works;n_restarts=4best ≥ any single-restart result.
StructureGenerator.generate().GenerationResultis adataclassthat is fully list-compatible (supports indexing, iteration,len(), and boolean coercion) while also exposing per-run rejection metadata:Attribute
Description
structureslist[Structure]— structures that passed all filtersn_attemptedTotal placement attempts
n_passedStructures that passed (equals
len(result))n_rejected_parityAttempts rejected by charge/multiplicity parity check
n_rejected_filterAttempts rejected by metric filters
n_success_targetThe
n_successvalue in effect (Noneif not set)result.summary()returns a one-line diagnostic string.Backward compatibility: all code that treats the return value of
generate()as a list — iteration, indexing,len(),bool()— works without modification. The only breaking change isisinstance(result, list)returningFalse; useisinstance(result, GenerationResult)orhasattr(result, "structures")instead.warnings.warnon silent-failure paths —stream()now emits aUserWarningvia Python’s standardwarningsmodule whenever:any attempts are rejected by the charge/multiplicity parity check,
no structures pass the metric filters after all attempts are exhausted, or
the attempt budget is exhausted before
n_successis reached.
These warnings fire regardless of the
verboseflag. Previously, all such messages were printed tostderronly whenverbose=True, making a silent empty-list return indistinguishable from a successful run in automated pipelines (ASE, high-throughput workflows). A downstreamIndexErrororAttributeErroron an empty list would then point to the wrong place in user code.Callers that want to suppress the warnings can use
warnings.filterwarnings("ignore", category=UserWarning, module="pasted").GenerationResultadded topasted.__all__and exported from the top-levelpastednamespace.11 new tests in
tests/test_generator.pycoveringTestGenerationResult: list-compatibility, indexing,bool,summary,repr, metadata count correctness,UserWarningon filter rejection,UserWarningon parity rejection, and no spurious warnings on clean runs.method="parallel_tempering"added toStructureOptimizer.Parallel Tempering (replica-exchange Monte Carlo) runs
n_replicasindependent Markov chains at a geometric temperature ladder fromT_end(coldest, most selective) toT_start(hottest, most exploratory). Everypt_swap_intervalsteps, adjacent replica pairs attempt a state exchange using the Metropolis criterion:ΔE = (β_k − β_{k+1}) × (f_{k+1} − f_k) accept with probability min(1, exp(ΔE))where β = 1/T and f is the objective value (higher is better). Hot replicas cross energy barriers that trap SA; accepted swaps tunnel good structures from hot replicas down to the cold replica, improving both exploration and exploitation simultaneously.
New parameters:
Parameter
Default
Description
n_replicas4Number of temperature replicas
pt_swap_interval10Attempt replica exchange every N steps
run()returns anOptimizationResultcontaining the global best (tracked across all replicas and all steps) plus each replica’s final state, sorted by objective value.n_restartslaunches independent PT runs and aggregates all results.Measured quality improvement over SA (n=10, C/N/O/P/S,
H_total − Q6objective, 6-seed mean):Method
wall time
H_total (↑)
Q6 (↓)
SA steps=500, restarts=4
460 ms
1.591
0.401
BH steps=200, restarts=4
187 ms
1.597
0.420
PT steps=200, rep=4, restarts=1
102 ms
1.685
0.403
PT steps=500, rep=4, restarts=2
579 ms
1.713
0.293
PT at 102 ms matches SA-4restart quality at 460 ms. PT’s Q6 suppression (0.293) is markedly better than either SA or BH at equivalent wall time.
Parity-preserving composition move (
_composition_movePath 2).The replace fallback — triggered when all atoms are the same element and no atom-pair swap is possible — previously drew a replacement element uniformly from the full pool, which violated the charge/multiplicity parity constraint in up to 64 % of calls when the pool contained a mix of odd-Z and even-Z elements.
The new implementation uses the user’s insight that swapping elements within the same Z-parity class preserves the electron-count parity:
Same-Z-parity replace (primary): replace atom
iwith an element whose atomic number has the same parity asZ(atoms[i]). Net ΔZ is even → parity invariant.Dual opposite-parity replace (fallback when only odd-Z elements differ): replace two atoms simultaneously with elements from the odd-Z pool so that each ΔZ is odd but the total ΔZ is even.
Parity failure rate in the worst case (all-same composition, wide pool): 64 % → 0 %. Normal usage (mixed composition, typical element pools) was already near zero via the primary swap path and is unchanged.
Objective alignment verified for
Q6,H_total,moran_I_chi, andcharge_frustration:Objective
Baseline
Optimized (SA, n=10)
maximize
Q6mean 0.081, max 0.274
0.801 (+192 %)
maximize
H_totalmean 0.495
1.229 (+148 %)
minimize
moran_I_chimean +0.06
−2.27
maximize
charge_frustrationmean 0.010
0.372
Temperature schedule (SA) confirmed as exponential decay T_start → T_end.
n_restartscorrectly returns the global best across all independent runs.10 new tests in
tests/test_optimizer.py:TestParallelTempering— return type, best-first sort, mode label, geometric temperature ladder,n_replicasparameter,repr, bad-method error, PT improves over baseline, multi-restart accumulation.
[0.1.14] - 2026-03-19#
Changed#
pdist/squareformremoved fromcompute_all_metrics.The O(N^2)
scipy.spatial.distance.pdist+squareformcall that dominatedcompute_all_metricsat large N has been replaced throughout by O(N*k) local pair enumeration (k = mean neighbors within cutoff, roughly constant):Path
N=5 000
N=10 000
Scaling
v0.1.13 (
pdist+squareform)~730 ms
~2 880 ms
O(N^2)
v0.1.14 (FlatCellList / cKDTree)
~30 ms
~60 ms
O(N*k)
All seven affected metrics (
H_spatial,RDF_dev,Q4/Q6/Q8,graph_lcc,graph_cc,ring_fraction,charge_frustration,moran_I_chi) now operate on pairs within cutoff only, consistent with the locality assumption shared by the graph and Steinhardt metrics.New C++ function
rdf_h_cpp(pts, cutoff, n_bins)added to_graph_core.cppand exported frompasted._ext.Enumerates pairs within cutoff via
FlatCellListin a single O(N*k) pass and returns{"h_spatial": float, "rdf_dev": float}. Called bycompute_all_metricswhenHAS_GRAPHisTrue.compute_h_spatialsignature changed from(dists: ndarray, n_bins: int)to(pts: ndarray, cutoff: float, n_bins: int).The condensed
distsarray (O(N^2) elements) is no longer accepted. The Python fallback usesscipy.spatial.cKDTree.query_pairsfor O(N*k) pair enumeration within cutoff.compute_rdf_deviationsignature changed from(pts: ndarray, dists: ndarray, n_bins: int)to(pts: ndarray, cutoff: float, n_bins: int).The histogram range is now
[0, cutoff]instead of[0, r_max]wherer_maxwas the maximum pairwise distance. Values will differ from v0.1.13 for the same structure, but are now consistent with the local pair assumption used by all other metrics.compute_steinhardt_per_atomandcompute_steinhardtsignatures changed: thedmatparameter has been removed.The C++ path (
HAS_STEINHARDT) never useddmat. The Python fallback (_steinhardt_per_atom_sparse) now usesscipy.spatial.cKDTreefor neighbor enumeration instead of indexing into a pre-built distance matrix. Both paths accept(pts, l_values, cutoff).compute_angular_entropy(diagnostic, not inALL_METRICS) now usesscipy.spatial.cKDTreeinstead of a full O(N^2) distance matrix.Inconsistent docstrings fixed throughout
_metrics.py:compute_ring_fractionandcompute_charge_frustration: removed stale references tocov_scale * (r_i + r_j)bond detection; updated to describe the cutoff-based adjacency introduced in v0.1.13.compute_moran_I_chi: corrected return-value description._steinhardt_per_atom_sparse: rewritten to reflect removal ofdmat.compute_all_metrics: documents removal ofpdist/squareform.
pasted._ext.__init__:rdf_h_cppadded to__all__and to the_graph_coreimport block.HAS_GRAPH = Truenow implies bothgraph_metrics_cppandrdf_h_cppare available._graph_core.cppmodule docstring updated to v0.1.14;rdf_h_cppbinding and inline documentation added.pyproject.toml: version bumped to0.1.14.
[0.1.13] - 2026-03-19#
Changed#
ring_fractionandcharge_frustrationnow usecutofffor adjacency instead ofcov_scale × (r_i + r_j).Previously these metrics defined a bond as any pair satisfying
d_ij < cov_scale × (r_i + r_j). Becauserelax_positionsguaranteesd_ij >= cov_scale × (r_i + r_j)for every pair on convergence, this criterion was structurally never satisfied in relaxed structures — both metrics returned 0.0 for every output of PASTED, carrying no information.New definition: a pair (i, j) is adjacent when
d_ij <= cutoff, the same cutoff used bygraph_lcc,graph_cc, andmoran_I_chi. All five cutoff-based metrics now share a single unified adjacency.Physical interpretation of the updated metrics:
ring_fraction— fraction of atoms that belong to at least one cycle in the cutoff-adjacency graph. A high value indicates that atoms are densely connected enough to form closed loops at the chosen interaction radius, reflecting structural compactness or clustering.charge_frustration— variance of |Δχ| (absolute Pauling electronegativity difference) across all cutoff-adjacent pairs. High values indicate that each atom is surrounded by a mix of electronegative and electropositive neighbours — i.e. the local electrostatic environment is inconsistent, analogous to geometric frustration in spin systems. Low values indicate compositionally homogeneous neighbourhoods.
Both metrics now produce informative non-zero values for typical PASTED structures (N = 100, mixed elements, auto cutoff ~2.13 Å).
API change: the
cov_scaleparameter ofcompute_ring_fractionandcompute_charge_frustrationhas been renamed tocutoff. The parameter was previously forwarded fromcompute_all_metrics; callers who pass keyword arguments need to update tocutoff=....compute_all_metricsno longer forwardscov_scaleto ring/charge functions; it now forwardscutoffinstead.cov_scaleis retained in thecompute_all_metricssignature for backward compatibility._graph_core.cppupdated to build a single unified adjacency list (d_ij <= cutoff) shared by all five metrics, removing the separatecov_scale-based bond list.README.md fully rewritten to reflect the current feature set (v0.1.12+), including the
maxentmode,StructureOptimizer,n_success,moran_I_chi, unified cutoff, noble gas EN values, and C++ acceleration flags (HAS_GRAPH).pyproject.toml: version bumped to0.1.13.
[0.1.12] - 2026-03-19#
Added#
pasted._ext._graph_core— new C++17 extension that replaces four Python O(N²) metrics bottlenecks with a single O(N·k) FlatCellList pass (k = mean bonded-pair count per atom, approximately constant):Metric
v0.1.11 Python (N=1000)
v0.1.12 C++ (N=1000)
Speedup
ring_fraction~90 ms
—
—
charge_frustration~88 ms
—
—
graph_lcc/graph_cc~35 ms
—
—
moran_I_chi(new)n/a
—
—
metrics TOTAL
~419 ms
~17 ms
~25×
All five metrics (
graph_lcc,graph_cc,ring_fraction,charge_frustration,moran_I_chi) are computed in a single C++ call; theFlatCellList, bonded-pair adjacency list, and cutoff adjacency list are built only once percompute_all_metricsinvocation. AHAS_GRAPHflag inpasted._extcontrols transparent fallback to the Python path when the extension is absent.moran_I_chi— new metric: Moran’s I spatial autocorrelation for Pauling electronegativity, added toALL_METRICSand exported frompasted:I = (N / W) * Σ_{i≠j} w_ij (χ_i − χ̄)(χ_j − χ̄) / Σ_i (χ_i − χ̄)²where w_ij = 1 when d_ij ≤ cutoff (step-function weight; uses the existing
cutoffparameter — no new API parameter).Interpretation:
I ≈ 0 : random spatial arrangement of electronegativity (desired for disordered structures)
I > 0 : same-electronegativity atoms cluster spatially
I < 0 : alternating high/low electronegativity (NaCl-like order)
Note: Moran’s I is not bounded to [-1, 1] for sparse weight matrices.
HAS_GRAPHflag added topasted._ext.CLI
--filterhelp text anddocs/cli.mdupdated with amoran_I_chiexample:--filter "moran_I_chi:-0.1:0.1"selects structures with spatially random electronegativity arrangement.
Changed#
Noble gas Pauling electronegativity values updated in
_PAULING_EN:Element
Before
After
Rationale
He, Ne, Ar, Rn
1.0
4.0
changed — no stable compounds known
Kr
1.0
3.0
KrF₂ known; Allen/Allred-Rochow scale estimate
Xe
1.0
2.6
XeF₂/XeO₃ well characterised; literature estimate
graph_metrics_cppnow computes all five metrics in a single FlatCellList pass; the former separatemoran_I_chi_cppcall has been inlined, halving the number of spatial-index builds percompute_all_metricscall.compute_all_metricsdocstring no longer hardcodes the metric count; it now refers tolen(ALL_METRICS)to avoid future update churn.setup.py: fourthPybind11Extensionentry added for_graph_core.pyproject.toml: version bumped to0.1.12.
Removed#
bond_strain_rmsremoved fromALL_METRICS,compute_all_metrics, the public API (pasted.__init__), and_metrics.pyentirely.Rationale:
relax_positionsguaranteesd_ij >= cov_scale * (r_i + r_j)for every pair on convergence, sobond_strain_rmsis structurally zero under normal usage (cov_scale = 1.0) and carries no information about the generated structures.Migration: remove
"bond_strain_rms"from any--filterorobjectivedict.ALL_METRICSnow has 13 keys (12 after removingbond_strain_rms, then back to 13 after addingmoran_I_chi).
[0.1.11] - 2026-03-19#
Changed#
_relax_coresolver replaced: Gauss-Seidel → L-BFGS. The per-cyclecheck_and_pushGauss-Seidel loop in_relax.cpphas been replaced by a global L-BFGS minimization of the harmonic steric-clash penalty energy:E = Σ_{i<j} ½ · max(0, cov_scale·(rᵢ + rⱼ) − dᵢⱼ)²The gradient is computed analytically; pair enumeration still uses
FlatCellListfor N ≥ 64 (O(N) per evaluation) and an O(N²) full-pair loop for N < 64 — identical to v0.1.10.Additional fixes (applied during test validation):
ENERGY_TOLtightened from1e-6to1e-12. The convergence criterion is on the total penalty energy, so1e-6permitted per-pair residual overlaps up to √(2×10⁻⁶) ≈ 1.4×10⁻³ Å — too coarse for the existing test suite (tolerance 1e-5 Å).1e-12bounds per-pair residuals to ≤ 1.4×10⁻⁶ Å.Jitter scope narrowed from all coordinates to coincident-pair atoms only (d < 1e-10 Å), matching the v0.1.10 GS behaviour. The unconditional jitter made
relax_positions(seed=None)non- deterministic for normal structures, breaking the optimizer reproducibility test.
Key behavioral differences vs v0.1.10:
v0.1.10 (Gauss-Seidel)
v0.1.11 (L-BFGS)
Convergence on dense random structures
0 % (1500 cycles)
100 %
N = 5000, normal density
2.28 s
0.044 s (~52×)
N = 5000, highly dense packing
3.04 s
0.084 s (~36×)
External dependencies
none
none
setup.pychanges required—
none
The L-BFGS implementation (history depth m = 7, Armijo backtracking line search) is written entirely in C++17 standard library — no Eigen, no OpenMP, no new build-time dependencies. A thin
Vecstruct backed bystd::vector<double>provides the required linear algebra;-O3produces code equivalent to an Eigen-based implementation.converged = Truewhen E < 1 × 10⁻⁶ (all overlaps resolved).A one-time pre-perturbation jitter (σ ≈ 1 × 10⁻⁶ × max_r, seeded by the
seedparameter) prevents zero-gradient singularities at exactly coincident atom positions. The perturbation is negligible on the final geometry (~3 × 10⁻⁸ Å for hydrogen).max_cyclessemantics forrelax_positions(C++ path only): Previously counted Gauss-Seidel sweeps; now counts L-BFGS outer iterations. The Python-side defaultrelax_cycles = 1500is unchanged and backward-compatible — L-BFGS exits early when E < 1 × 10⁻⁶, so the limit is rarely reached.seedsemantics forrelax_positions(C++ path only): Previously seeded the per-push random direction for coincident atoms. Now seeds the one-time pre-perturbation jitter. Downstream callers are unaffected.pyproject.toml: version bumped to0.1.11.
[0.1.10] - 2026-03-18#
Added#
pasted._ext._steinhardt_core— new C++17 extension for Steinhardt Q_l computation, replacing the dense O(N²) Python/scipy path with a sparse O(N·k) algorithm (k = mean neighbor count).Path
N=2000
Speed-up vs dense Python
Dense Python (original)
~35 s
1×
Sparse Python fallback
~0.21 s
~164×
C++ (
_steinhardt_core)~17 ms
~2 000×
The extension uses a
FlatCellListspatial index (same pattern as_relax_core) for neighbor finding, and evaluates spherical harmonics via the standard associated Legendre polynomial three-term recurrence — no scipy call inside the hot loop. The symmetry|Y_l^{-m}|² = |Y_l^m|²halves the number of harmonic evaluations by computing only m = 0..l.When the extension is absent, a sparse Python/NumPy fallback (
_steinhardt_per_atom_sparse) provides the same O(N·k) complexity usingnp.bincountfor accumulation. The publiccompute_steinhardt_per_atomfunction dispatches transparently.HAS_STEINHARDTflag added topasted._ext.
Changed#
setup.py: thirdPybind11Extensionentry added for_steinhardt_core.pyproject.toml: version bumped to0.1.10.
[0.1.9] - 2026-03-18#
Added#
Three MM-level structural descriptors added to
ALL_METRICS(and therefore available as--filtertargets on the CLI):Metric
Description
bond_strain_rmsRMS relative deviation of bonded-pair distances from their Pyykkö ideal lengths
ring_fractionFraction of atoms that belong to at least one ring, detected via Union-Find spanning-tree construction
charge_frustrationVariance of Pauling electronegativity differences across bonded pairs
Bond detection uses the same
cov_scale × (r_i + r_j)threshold asrelax_positions, keeping the definition of a “bond” consistent across placement, relaxation, and metric computation.Example usage::
from pasted import generate structs = generate( n_atoms=14, charge=0, mult=1, mode="chain", elements="6,7,8,1", n_samples=50, seed=0, filters=["bond_strain_rms:-:0.15", "ring_fraction:-:0.3"], )Pauling electronegativity table (
pasted._atoms._PAULING_EN) covering Z = 1–106 (Pauling 1960 / IUPAC 2016). Noble gases and elements without a literature value return the module-level constantPAULING_EN_FALLBACK(1.0). The public accessorpauling_electronegativity(sym)is exported from the top-levelpastednamespace.compute_all_metrics()now accepts an optionalcov_scale: float = 1.0keyword argument, forwarded to the three new MM-level descriptors. Existing call sites withoutcov_scaleare unaffected (default preserved).pasted.PAULING_EN_FALLBACK,pasted.pauling_electronegativity,pasted.compute_bond_strain_rms,pasted.compute_ring_fraction, andpasted.compute_charge_frustrationadded to the public API and__all__.21 new tests in
tests/test_metrics.pycoveringTestComputeBondStrainRms(5 tests),TestComputeRingFraction(5 tests),TestComputeChargeFrustration(5 tests), and updated integration tests forcompute_all_metrics(4 tests) andpasses_filters(1 test).
Changed#
ALL_METRICSexpanded from 10 to 13 keys.compute_all_metricsdocstring updated: “ten” → “thirteen”.pyproject.toml: version bumped to0.1.9.
[0.1.8] - 2026-03-18#
Added#
Python 3.13 officially supported.
cp313wheels are now built and tested in CI. The C++ extensions (_relax_core,_maxent_core) compile and load correctly under Python 3.13.
Fixed#
__version__is now derived dynamically from package metadata viaimportlib.metadata.version("pasted")instead of being hardcoded in__init__.py. This eliminates the version skew that causedpasted.__version__to report"0.1.4"even after upgrading to a newer release. Falls back to"unknown"when the package is not installed (e.g., running directly from the source tree withoutpip install).
Changed#
pyproject.toml: version bumped to0.1.8.
[0.1.7] - 2026-03-18#
Added#
n_successparameter forStructureGenerator/generate()(n_success: int | None = None, CLI:--n-success N).Generation stops as soon as N structures have passed all filters, without exhausting the full
n_samplesattempt budget.n_samplesn_successBehavior
> 0
NoneOriginal behavior: attempt exactly
n_samplestimes> 0
N
Stop at N successes or
n_samplesattempts, whichever comes first0
N
Unlimited attempts; stop only when N structures have passed
n_samples=0withoutn_successraisesValueErrorto prevent accidental infinite loops. Ifn_samplesis exhausted beforen_successis reached, the structures collected so far are returned with a warning — never an empty result or an exception.StructureGenerator.stream()— yields each passing structure immediately rather than collecting all results into a list first.Incremental file output: each structure is written to disk the moment it passes, so a
Ctrl-Cmid-run still produces valid XYZ output up to that point.Early termination: combined with
n_success, the caller receives results without waiting for the full attempt budget to be exhausted.
generate()now delegates tostream()internally — behavior and return type are unchanged for existing callers. The CLI usesstream()and appends each structure to the output file immediately on PASS.
Changed#
StructureGenerator.__repr__now includesn_success.CLI
--n-sampleshelp text updated to document the0 = unlimitedsemantics.pyproject.toml: version bumped to0.1.7.
[0.1.6] - 2026-03-18#
Added#
Cell List spatial partitioning in
_relax.cppand_maxent.cpp.Both C++ extension modules now use a flat 3-D Cell List to restrict neighbor searches to a 27-cell neighborhood instead of scanning all atom pairs. A linked-list style flat
vector<int>grid (FlatCellList) is rebuilt once per relaxation cycle, avoiding the per-cycle heap allocation overhead of anunordered_map-based grid.Strategy is selected automatically based on atom count:
N
_relax_core_maxent_core< 64
O(N²) full-pair loop
O(N³) full-pair
≥ 64
O(N) Cell List
O(N²) Cell List
Cell size is computed automatically —
cov_scale × 2 × max(radii)for_relax_core;cutofffor_maxent_core— with no new API parameters.Measured speed-ups vs pure-Python/NumPy fallback:
relax_positions:N
Python (ms)
C++ (ms)
Speed-up
20
2.1
0.09
22×
200
87
13
6.6×
500
544
43
12.7×
1000
2237
113
19.8×
angular_repulsion_gradient:N
Python (ms)
C++ (ms)
Speed-up
30
10.8
0.07
153×
100
154
3.0
52×
200
881
19
46×
chain_biasparameter forplace_chain/StructureGenerator/generate()(chain_bias: float = 0.0, CLI:--chain-bias).The direction of the first bond placed becomes a global bias axis. Every subsequent step direction is blended toward that axis before normalization::
d_biased = d + axis * chain_bias d_final = d_biased / ||d_biased||
Effect on
shape_aniso ≥ 0.5rate (n = 20, branch_prob = 0.0):chain_biasmean shape_aniso
≥ 0.5 rate
0.0 (default)
0.40
33%
0.3
0.55
63%
0.6
0.74
92%
1.0
0.89
100%
Default is
0.0— fully backward-compatible.
Fixed#
Distance violation check in
_relax.cppchanged fromd² >= thr²tod >= thrthroughout. The squared comparison caused atoms sitting exactly at the threshold distance to be re-flagged as violating in the following cycle due to floating-point rounding, preventing convergence.
Changed#
_relax.cpp: Cell List threshold raised from 32 to 64 after benchmarking showed thatunordered_map-based grid reconstruction at N ≈ 32–63 is slower than the full-pair loop.pyproject.toml: version bumped to0.1.6.
[0.1.5] - 2026-03-18#
Added#
src/pasted/_ext/sub-package — C++ extensions reorganized into separate source files by function:_relax.cpp→_ext._relax_core: distance constraint relaxation loop (used by all placement modes)._maxent.cpp→_ext._maxent_core: angular repulsion gradient (maxentmode only)._ext/__init__.py: exposes independentHAS_RELAX/HAS_MAXENTflags so that a build failure in one module does not disable the other.
Optional C++ extension (
pasted._ext, built viapybind11). When a C++17 toolchain is present at install time, two inner-loop hotspots are compiled to native code. When absent, pure-Python/NumPy fallbacks are used transparently — no user-facing API change.relax_positions: per-cycle pair-repulsion loop rewritten as a tight C++ double loop, eliminating the(n, n, 3)NumPy broadcast diff array allocated on every iteration. Typical speed-up: 5–20× for 10–100 atoms._angular_repulsion_gradient: O(N³) Python doubleforloop replaced by a cache-friendly C++ loop. Speed-up: 20–50× formaxentmode.
seedparameter forrelax_positions(seed: int | None = None). The RNG used for the coincident-atom edge case (distance < 1e-10 Å) is now seeded deterministically when a value is provided.StructureGeneratorautomatically forwards its master seed.seedparameter forplace_maxent— threaded through to the two internalrelax_positionscalls.
Fixed#
Pure-Python fallback for
relax_positionsnow creates a singlenp.random.default_rng(seed)instance before the loop instead of callingnp.random.default_rng()(unseeded) on every coincident-atom hit, fixing a latent non-reproducibility bug.
Changed#
pyproject.toml:pybind11>=2.12added to[build-system].requiresand[project.optional-dependencies].dev.setup.py(new file): declares twoPybind11Extensionentries, one per C++ source file.pyproject.toml: version bumped to0.1.5.
[0.1.4] - 2026-03-17#
Added#
Documentation of
maxentmode algorithmic behavior, numerical stability measures, and parameter tuning guidelines.Clarification that
compute_angular_entropyis a placement-quality diagnostic excluded fromALL_METRICSand XYZ comment lines.
[0.1.3] - 2026-03-17#
Added#
--mode maxent— maximum-entropy placement mode. Atoms are initialized at random, then iteratively repositioned by gradient descent on an angular repulsion potential so that each atom’s neighbor directions become as uniformly distributed over the sphere as the Pyykkö distance constraints allow. Implements the constrained-maximum-entropy solution tomax S = −∫ p(Ω) ln p(Ω) dΩsubject tod_ij ≥ cov_scale·(r_i + r_j).place_maxent(atoms, region, cov_scale, rng, ...)— low-level placement function, exported from the public API.compute_angular_entropy(positions, cutoff)— diagnostic metric: mean per-atom Shannon entropy of neighbor direction distributions. Not included inALL_METRICSor XYZ comment lines._angular_repulsion_gradient(pts, cutoff)— internal NumPy gradient of the angular repulsion potentialU = Σ 1/(1 − cos θ + ε).Three new CLI flags:
--maxent-steps(default 300),--maxent-lr(default 0.05),--maxent-cutoff-scale(default 2.5).tests/test_maxent.py— 15 tests covering gradient, placement, entropy metric, and generator integration.
Changed#
StructureGeneratornow acceptsmode="maxent".__init__.pyexports:place_maxent,compute_angular_entropy.
[0.1.2] - 2026-03-17#
Added#
StructureOptimizer— objective-based structure optimization that maximizes a user-defined disorder metric instead of sampling randomly.Two methods:
"annealing"(Simulated Annealing with exponential cooling) and"basin_hopping"(Metropolis with per-step relaxation).Fragment coordinate move: atoms with local Q6 above
frag_thresholdare preferentially displaced to break accidentally ordered regions.Composition move: element types of two atoms are swapped to explore composition space alongside geometry.
parse_objective_spec(["METRIC:WEIGHT", ...])— utility for converting CLI strings to a weight dict.compute_steinhardt_per_atom— public function returning per-atom Q_l arrays of shape(n,), used internally by the optimizer.--optimizeCLI flag enabling optimization mode while preserving full backward compatibility with sampling mode.tests/test_optimizer.py— 25 tests covering helpers, construction, and allrun()paths.
Changed#
compute_steinhardtrefactored to delegate tocompute_steinhardt_per_atom, eliminating code duplication.__init__.pyexports:StructureOptimizer,parse_objective_spec,compute_steinhardt_per_atom.
[0.1.1] - 2026-03-17#
Added#
pasted.py— direct-run entry point at the project root, enablingpython pasted.pywithout a priorpip install.conftest.pyat the project root to preventpasted.pyfrom shadowing thesrc/pasted/package duringpytestcollection.CI badges (status, PyPI version, Python versions, License) in
README.md..github/workflows/ci.yml— GitHub Actions workflow:lint(ruff),test(Python 3.10/3.11/3.12 matrix),typecheck(mypy),build(sdist + wheel artifact).
Fixed#
ruff format --checkremoved from CI lint job to prevent version-skew failures between environments.
Changed#
CI actions updated to Node.js 24 compatible versions:
actions/checkout@v4→@v5,actions/setup-python@v5→@v6._metrics.py:_sph_harmwrappers now returnnp.ndarrayvianp.asarray(), eliminatingno-any-returnmypy errors.pyproject.toml: added[tool.mypy.overrides]forpasted._metricsto suppresswarn_unused_ignoreson thescipy < 1.15compatibility branch.pyproject.toml: pytest runs with--import-mode=importlib.licensefield updated to SPDX string format ("MIT") per PEP 639.
[0.1.0] - 2026-03-17#
Added#
Initial release.
StructureGeneratorclass API andgenerate()functional wrapper.Structuredataclass with.to_xyz()and.write_xyz()helpers.Three placement modes:
gas,chain,shell.Ten disorder metrics:
H_atom,H_spatial,H_total,RDF_dev,shape_aniso,Q4,Q6,Q8,graph_lcc,graph_cc.pastedCLI entry-point.src/layout with separated concerns:_atoms,_placement,_metrics,_io,_generator,cli.pyproject.tomlwithruff,mypy, andpytestdev extras.