Placement functions#
pasted._placement#
Atom-placement algorithms and post-placement repulsion relaxation.
No file I/O; no metrics. All placement functions return a
list[tuple[float, float, float]] of Cartesian coordinates.
Placement modes#
place_gasUniform random placement inside a sphere or axis-aligned box, followed by
relax_positions()to resolve any steric clashes.place_chainRandom-walk chain grown from the origin with optional directional bias (
chain_persist) and global axis drift (chain_bias).place_shellCentral atom at the origin surrounded by shell atoms at a uniform radial distance, with optional tail atoms attached via short random walks.
place_maxentMaximum-entropy placement via L-BFGS minimization of an angular repulsion potential. Requires a
regionspec. The full C++ L-BFGS loop (HAS_MAXENT_LOOP = True) is ~10–22× faster than the Python steepest-descent fallback.
Affine transform#
_affine_move applies an optional affine transform (stretch/compress one
axis, shear one axis pair, optional per-atom jitter) to a position array.
It is shared between StructureGenerator (applied
once per structure before relax_positions) and
StructureOptimizer (applied per MC step when
allow_affine_moves=True).
Relaxation#
relax_positions resolves steric clashes by L-BFGS minimization of the
harmonic penalty energy
\(E = \sum_{i<j} \frac{1}{2} \max(0, r_{ij}^{\min} - d_{ij})^2\),
where \(r_{ij}^{\min} = \text{cov\_scale} \cdot (r_i + r_j)\).
- pasted._placement.add_hydrogen(atoms: list[str], rng: Random, *, region: str | None = None, charge: int = 0, mult: int = 1) list[str][source]#
Append hydrogen atoms when H is in the pool but absent from atoms.
Enhanced in v0.4.4 with two new guards:
Parity-aware count — the sampled H count is adjusted by ±1 so that the total electron count satisfies the charge/multiplicity parity:
(Σ Z_i − charge + n_H) % 2 == (mult − 1) % 2. This prevents most charge/multiplicity rejections that previously occurred after the (potentially expensive) H-list allocation.Volume cap — when region is given the H count is hard-capped so that the total packing fraction of the new atoms stays below
_PACKING_HARD(0.64, random-close-packing limit for monodisperse spheres). This is independent of the density validation inStructureGenerator; that check operates on the non-hydrogen heavy atoms specified by the user, while this cap limits hydrogen inflation within a fixed region after heavy-atom placement.The base sample is still drawn from
n_H_raw = 1 + round(uniform(0, 1) × n_current × 1.2), which preserves the original distribution as closely as possible while respecting the two constraints above.- Parameters:
atoms – Current element list (must not already contain
"H"; returns atoms unchanged when it does).rng – Seeded random-number generator.
region – Bounding-region spec
"sphere:R"|"box:L"|"box:LX,LY,LZ". Used to derive the volume cap.None→ uncapped (original behaviour, kept for back-compat when region is not available).charge – Total system charge (default: 0). Used for parity calculation.
mult – Spin multiplicity (default: 1). Used for parity calculation.
- Returns:
Original list with
n_H"H"entries appended. The original list is not modified.- Return type:
- pasted._placement.place_chain(atoms: list[str], bond_lo: float, bond_hi: float, branch_prob: float, persist: float, rng: Random, chain_bias: float = 0.0) tuple[list[str], list[tuple[float, float, float]]][source]#
Random-walk atom-chain growth with branching and directional persistence.
- Parameters:
atoms – Element symbols (order is preserved).
bond_lo – Bond-length range (Å).
bond_hi – Bond-length range (Å).
branch_prob – Probability that an atom becomes a new branch tip rather than replacing the existing tip (0 = linear, 1 = fully branched tree).
persist –
Directional persistence ∈ [0, 1]. A new step direction d is accepted only when
dot(d, prev_dir) ≥ persist − 1.0.0 → fully random (may self-tangle)
0.5 → rear 120° cone excluded (default)
1.0 → front hemisphere only, nearly straight chain
rng – Seeded random-number generator.
chain_bias –
Global-axis drift strength ∈ [0, 1] (default: 0.0).
After the first bond is placed its direction becomes the 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||
0.0 → no bias; behavior identical to previous versions
- 0.3 → moderate elongation; shape_aniso ≥ 0.5 rate rises from
~40% to ~70% for n = 20 atoms
1.0 → strong elongation; nearly rod-like for small n
chain_biasandpersistare complementary:persistcontrols local turn angles between consecutive bonds;chain_biasimposes a global preferred axis regardless of chain length.
- Returns:
Always
len(atoms)positions.- Return type:
- pasted._placement.place_gas(atoms: list[str], region: str, rng: Random) tuple[list[str], list[tuple[float, float, float]]][source]#
Place all atoms uniformly at random inside region.
No clash checking is performed — call
relax_positions()afterwards. Uniform random placement is used for performance predictability across all density regimes.- Parameters:
atoms – Element symbols.
region –
"sphere:R"|"box:L"|"box:LX,LY,LZ"rng – Seeded random-number generator.
- Returns:
Always
len(atoms)positions.- Return type:
- Raises:
ValueError – On unrecognised region spec.
- pasted._placement.place_maxent(atoms: list[str], region: str, cov_scale: float, rng: Random, maxent_steps: int = 300, maxent_lr: float = 0.05, maxent_cutoff_scale: float = 2.5, trust_radius: float = 0.5, convergence_tol: float = 0.001, seed: int | None = None) tuple[list[str], list[tuple[float, float, float]]][source]#
Place atoms to maximize angular entropy subject to distance constraints.
Implements constrained maximum-entropy placement: atoms are initialized inside region at random, then iteratively repositioned so that each atom’s neighborhood directions become as uniformly distributed over the sphere as possible — the solution to
max S = −∫ p(Ω) ln p(Ω) dΩ s.t. d_ij ≥ cov_scale × (r_i + r_j) ∀ i,j
The angular repulsion potential
U = Σ_{i} Σ_{j,k ∈ N(i), j≠k} 1 / (1 − cos θ_{jk} + ε)
is minimised by L-BFGS (m=7, Armijo backtracking) when the C++ extension
_maxent_core.place_maxent_cppis available (HAS_MAXENT_LOOP), or by steepest descent otherwise. A per-atom trust radius caps the maximum displacement per step, replacing the fixedmaxent_lrunit-norm clip of the steepest-descent fallback.After every gradient step the mandatory distance-constraint relaxation is applied (L-BFGS penalty, identical to
_relax_core).Stability measures applied per step:
Per-atom trust-radius clamp: the step is uniformly rescaled so no atom moves more than trust_radius Å, preventing L-BFGS overshooting.
Soft restoring force: atoms that drift outside the initial region radius are gently pulled back toward the center of mass.
Centre-of-mass pinning: the center of mass is re-centered to the origin after each step so the whole structure does not drift.
- Parameters:
atoms – Element symbols.
region – Initial placement region:
"sphere:R"|"box:L"|"box:LX,LY,LZ".cov_scale – Pyykkö distance scale factor.
rng – Seeded random-number generator.
maxent_steps – Gradient-descent / L-BFGS outer iterations (default: 300).
maxent_lr – Learning rate used only by the Python steepest-descent fallback (default: 0.05). Ignored when the C++ loop is active.
maxent_cutoff_scale – Neighbor cutoff = this factor × median covalent-radius sum (default: 2.5). Larger values include more neighbors in the angular calculation. The median is computed in O(N) via
numpy.medianon the per-atom radii array; see Implementation notes below.trust_radius – Per-atom maximum displacement per step (Å, default: 0.5). Used by the C++ L-BFGS loop; steepest-descent fallback uses unit-norm clip scaled by maxent_lr instead.
convergence_tol – Early-termination threshold: the loop stops when the RMS gradient per atom falls below this value (Å⁻¹·a.u., default: 1e-3). Set to
0to disable early termination and always run maxent_steps iterations. Ignored by the Python steepest-descent fallback.seed – Optional integer seed forwarded to the steric-clash relaxation for the coincident-atom edge case.
None→ non-deterministic (default).
- Returns:
(atoms, positions) – Always
len(atoms)positions.Implementation notes
——————–
The angular-repulsion neighbor cutoff is derived from the median covalent
radius of the element pool using the identity
median(rᵢ + rⱼ) = 2 · median(rᵢ), which holds for all built-in elementpools. This allows the cutoff to be computed in O(N) via a single
numpy.mediancall over the per-atom radii array rather than enumeratingall N*(N+1)/2 pairwise sums
.. code-block:: python – median_sum = float(np.median(radii)) * 2.0
The resulting
ang_cutoffvalue is numerically identical to thepairwise-median approach for all tested element pools (C, N, O, H, S, …).
- pasted._placement.place_shell(atoms: list[str], center_sym: str, coord_lo: int, coord_hi: int, shell_lo: float, shell_hi: float, tail_lo: float, tail_hi: float, rng: Random) tuple[list[str], list[tuple[float, float, float]]][source]#
Center atom at origin + coordination shell + tail atoms.
No clash checking is performed — call
relax_positions()afterwards.- Parameters:
atoms – Element symbols; must contain at least one occurrence of center_sym.
center_sym – Symbol of the atom placed at the origin.
coord_lo – Coordination-number range (number of shell atoms).
coord_hi – Coordination-number range (number of shell atoms).
shell_lo – Shell radius range (Å).
shell_hi – Shell radius range (Å).
tail_lo – Tail bond-length range (Å).
tail_hi – Tail bond-length range (Å).
rng – Seeded random-number generator.
- Returns:
Center atom is first; always
len(atoms)positions.- Return type:
(ordered_atoms, positions)
- pasted._placement.relax_positions(atoms: list[str], positions: list[tuple[float, float, float]], cov_scale: float, max_cycles: int = 500, *, seed: int | None = None) tuple[list[tuple[float, float, float]], bool][source]#
Resolve interatomic distance violations by L-BFGS penalty minimization.
For every pair (i, j) whose distance falls below
cov_scale × (r_i + r_j)(Pyykkö single-bond covalent radii), a harmonic penalty energy is accumulated and its gradient used to drive atoms apart. The C++ path minimizesE = Σ_{i<j} ½ · max(0, threshold − d_ij)²via L-BFGS; convergence is declared when E < 1 × 10⁻⁶.When the compiled C++ extension (
pasted._ext._relax_core) is available the optimization runs in native code; otherwise the pure-Python/NumPy Gauss-Seidel fallback is used transparently.- Parameters:
atoms – Element symbols, one per atom.
positions – Initial Cartesian coordinates (Å).
cov_scale – Scale factor applied to the sum of covalent radii.
max_cycles – Maximum number of L-BFGS iterations (C++ path) or Gauss-Seidel sweeps (Python fallback). The C++ solver exits early when E < 1e-6, so the limit is rarely reached for typical structures.
seed – Optional integer seed for the one-time pre-perturbation jitter (C++ path) or the coincident-atom RNG (Python fallback).
None→ non-deterministic. Pass the generator’s master seed here for full reproducibility.
- Returns:
converged is
Falseonly when max_cycles was reached with violations still present; the structure is still returned and usable.- Return type:
(relaxed_positions, converged)
Note
v0.2.6 — O(N) neighbor-cutoff computation in place_maxent
The angular-repulsion neighbor cutoff (ang_cutoff) is now derived via
float(numpy.median(radii)) * 2.0 instead of sorting all N*(N+1)/2
pairwise radius sums. The identity median(rᵢ + rⱼ) = 2 · median(rᵢ)
holds for all built-in element pools and produces a numerically identical
result, while reducing the pre-loop setup from O(N² log N) to O(N).
See the place_maxent() docstring for benchmark
numbers.