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_gas

Uniform random placement inside a sphere or axis-aligned box, followed by relax_positions() to resolve any steric clashes.

place_chain

Random-walk chain grown from the origin with optional directional bias (chain_persist) and global axis drift (chain_bias).

place_shell

Central atom at the origin surrounded by shell atoms at a uniform radial distance, with optional tail atoms attached via short random walks.

place_maxent

Maximum-entropy placement via L-BFGS minimization of an angular repulsion potential. Requires a region spec. 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 in StructureGenerator; 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:

list[str]

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_bias and persist are complementary: persist controls local turn angles between consecutive bonds; chain_bias imposes a global preferred axis regardless of chain length.

Returns:

Always len(atoms) positions.

Return type:

(atoms, positions)

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:

(atoms, positions)

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_cpp is available (HAS_MAXENT_LOOP), or by steepest descent otherwise. A per-atom trust radius caps the maximum displacement per step, replacing the fixed maxent_lr unit-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.median on 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 0 to 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

  • ——————–

  • **O(N) cutoff computation (****)

  • 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 element

  • pools. This allows the cutoff to be computed in O(N) via a single

  • numpy.median call over the per-atom radii array rather than enumerating

  • all N*(N+1)/2 pairwise sums

  • .. code-block:: python – median_sum = float(np.median(radii)) * 2.0

  • The resulting ang_cutoff value is numerically identical to the

  • pairwise-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 minimizes E = Σ_{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 False only 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.