Skip to content

API Helpers (Internal)

These functions support the public API by handling input validation, tolerance management, and symmetrization workflows.

They act as a bridge between user-facing functions and the core symmetry engine.


Overview

The API layer is responsible for:

  • Validating user inputs
  • Preparing numerical tolerances
  • Applying symmetry operations to molecular geometry
  • Evaluating candidate symmetrizations

Notes

  • These functions are internal to the API layer and not intended for direct use
  • They rely heavily on internal core structures
  • Changes here may affect public API behavior but not the underlying symmetry engine

api

minimalsym.py — Public API for molecular symmetry analysis.

_validate_mol

_validate_mol(mol, geom_tol, min_atoms=1)

Validate common inputs for public API functions.

Parameters:

  • mol (Atoms) –
  • geom_tol (float) –

    Must be positive.

  • min_atoms (int, default: 1 ) –

    Minimum number of atoms required (default 1).

Raises:

  • ImportError — if ase is not installed.
  • TypeError — if mol is not an ase.Atoms object.
  • ValueError — if geom_tol <= 0 or mol has fewer than min_atoms atoms.
Source code in minimalsym/api.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def _validate_mol(mol, geom_tol, min_atoms=1):
    """
    Validate common inputs for public API functions.

    Parameters
    ----------
    mol : ase.Atoms
    geom_tol : float
        Must be positive.
    min_atoms : int
        Minimum number of atoms required (default 1).

    Raises
    ------
    ImportError  — if ase is not installed.
    TypeError    — if mol is not an ase.Atoms object.
    ValueError   — if geom_tol <= 0 or mol has fewer than min_atoms atoms.
    """
    try:
        from ase import Atoms as _Atoms
    except ImportError:
        raise ImportError("ase is required but not installed.")

    if not isinstance(mol, _Atoms):
        raise TypeError(f"Expected an ase.Atoms object, got {type(mol).__name__}.")
    if geom_tol <= 0:
        raise ValueError(f"Tolerance must be positive, got {geom_tol}.")
    if len(mol) < min_atoms:
        raise ValueError(
            f"Molecule must have at least {min_atoms} atom(s), got {len(mol)}."
        )

_set_tolerances

_set_tolerances(mol: Atoms, geom_tol: float, eigen_tol: float | None = None)

Set geometric and eigenvalue tolerances on a molecule.

This function initializes the tolerance parameters used throughout symmetry detection and classification, storing them in mol.info.

Parameters:

  • mol (Atoms) –

    Molecule to annotate with tolerance values.

  • geom_tol (float) –

    Geometric tolerance (Å). Controls positional equivalence under symmetry operations.

  • eigen_tol (float or None, default: None ) –

    Relative tolerance for comparing moments of inertia. If None, a value is automatically estimated from geom_tol and the molecular size.

Notes
  • geom_tol governs geometric equivalence (e.g., atom mapping).
  • eigen_tol governs classification via inertia eigenvalues (e.g., linear vs symmetric top).
  • If not provided, eigen_tol is derived to maintain consistency with the geometric tolerance.
Side Effects

Updates: mol.info["geom_tol"] mol.info["eigen_tol"]

Source code in minimalsym/api.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def _set_tolerances(mol: Atoms, geom_tol:float, eigen_tol:float|None = None):
    """
    Set geometric and eigenvalue tolerances on a molecule.

    This function initializes the tolerance parameters used throughout
    symmetry detection and classification, storing them in
    `mol.info`.

    Parameters
    ----------
    mol : Atoms
        Molecule to annotate with tolerance values.
    geom_tol : float
        Geometric tolerance (Å). Controls positional equivalence under
        symmetry operations.
    eigen_tol : float or None, optional
        Relative tolerance for comparing moments of inertia. If None,
        a value is automatically estimated from `geom_tol` and the
        molecular size.

    Notes
    -----
    - `geom_tol` governs geometric equivalence (e.g., atom mapping).
    - `eigen_tol` governs classification via inertia eigenvalues
      (e.g., linear vs symmetric top).
    - If not provided, `eigen_tol` is derived to maintain consistency
      with the geometric tolerance.

    Side Effects
    ------------
    Updates:
        mol.info["geom_tol"]
        mol.info["eigen_tol"]
    """
    mol.info["geom_tol"] = geom_tol
    if eigen_tol is None:
        mol.info["eigen_tol"] = _estimate_eigen_tol(
            mol.positions, mol.get_masses(), geom_tol
        )
    else:
        mol.info["eigen_tol"] = eigen_tol

_estimate_eigen_tol

_estimate_eigen_tol(positions: array, masses: array, geom_tol: float, factor: float = 2.0)

Estimate a relative eigenvalue tolerance from geometric tolerance.

The tolerance is derived by propagating positional uncertainty (geom_tol) into the expected variation of the eigenvalues of the moment of inertia tensor.

Parameters:

  • positions ((ndarray, shape(N, 3))) –

    Atomic positions.

  • masses ((ndarray, shape(N))) –

    Atomic masses.

  • geom_tol (float) –

    Geometric tolerance (Å).

  • factor (float, default: 2.0 ) –

    Empirical scaling factor. Default is 2.0.

Returns:

  • float

    Estimated relative tolerance for inertia eigenvalue comparison.

Notes

The scaling follows:

`eigen_tol` ~ `geom_tol` / L

where L is a characteristic molecular size derived from the moment of inertia tensor. Larger molecules therefore require tighter relative tolerances.

The estimate is based on the largest eigenvalue of the inertia tensor, ensuring robustness across anisotropic geometries.

Source code in minimalsym/api.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def _estimate_eigen_tol(positions: np.array, masses:np.array, geom_tol:float, factor:float=2.0):
    """
    Estimate a relative eigenvalue tolerance from geometric tolerance.

    The tolerance is derived by propagating positional uncertainty
    (`geom_tol`) into the expected variation of the eigenvalues of the
    moment of inertia tensor.

    Parameters
    ----------
    positions : ndarray, shape (N, 3)
        Atomic positions.
    masses : ndarray, shape (N,)
        Atomic masses.
    geom_tol : float
        Geometric tolerance (Å).
    factor : float, optional
        Empirical scaling factor. Default is 2.0.

    Returns
    -------
    float
        Estimated relative tolerance for inertia eigenvalue comparison.

    Notes
    -----
    The scaling follows:

        `eigen_tol` ~ `geom_tol` / L

    where L is a characteristic molecular size derived from the moment
    of inertia tensor. Larger molecules therefore require tighter
    relative tolerances.

    The estimate is based on the largest eigenvalue of the inertia tensor,
    ensuring robustness across anisotropic geometries.
    """
    moit = _jit_calcmoit(positions, masses)
    evals_mol, _ = np.linalg.eigh(moit)
    scale = np.max(evals_mol)
    eigen_tol = factor * geom_tol / np.sqrt(scale / np.sum(masses))
    return eigen_tol

_get_ancestor

_get_ancestor(parent, atom_num)

Path-compression union-find: return (root, path) for atom_num.

Parameters:

  • parent (np.ndarray of int) –
  • atom_num (int) –

Returns:

  • tuple(int, list[int])

    (root_index, nodes_on_path_to_root)

Source code in minimalsym/api.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def _get_ancestor(parent, atom_num):
    """
    Path-compression union-find: return (root, path) for *atom_num*.

    Parameters
    ----------
    parent : np.ndarray of int
    atom_num : int

    Returns
    -------
    : tuple(int, list[int])
        (root_index, nodes_on_path_to_root)
    """
    ancestor_line = [atom_num]
    while parent[atom_num] != atom_num:
        atom_num = parent[atom_num]
        ancestor_line.append(atom_num)
    return atom_num, ancestor_line

_union_find_pass

_union_find_pass(atom_map, parent, symel_indices)

Merge equivalence classes for all atoms over the given symel indices.

Parameters:

  • atom_map ((ndarray, shape(n_atoms, n_symels))) –
  • parent (np.ndarray of int, shape (n_atoms,) — modified in-place) –
  • symel_indices (iterable of int) –
Source code in minimalsym/api.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def _union_find_pass(atom_map, parent, symel_indices):
    """
    Merge equivalence classes for all atoms over the given symel indices.

    Parameters
    ----------
    atom_map : np.ndarray, shape (n_atoms, n_symels)
    parent : np.ndarray of int, shape (n_atoms,)  — modified in-place
    symel_indices : iterable of int
    """
    for atom in range(atom_map.shape[0]):
        for s in symel_indices:
            w = atom_map[atom, s]
            owner_w, ancestor_w = _get_ancestor(parent, w)
            owner_atom, ancestor_atom = _get_ancestor(parent, atom)
            for idx in ancestor_w:
                parent[idx] = owner_atom
            for idx in ancestor_atom:
                parent[idx] = owner_atom

    # Final path-compression pass to flatten all chains
    for ii in range(len(parent)):
        owner, ancestor = _get_ancestor(parent, ii)
        for idx in ancestor:
            parent[idx] = owner

_project_linear

_project_linear(mol, sea, asym_symtext)

Project atoms onto the molecular axis for linear groups (C0v / D0h).

For C0v all atoms are collapsed onto the z-axis. For D0h atoms come in inversion-related pairs; the central atom (if any) is placed at the origin.

Parameters:

  • mol (Atoms) –

    Molecule being symmetrized (modified in-place).

  • sea (SEA) –

    Symmetry-equivalent-atoms group to process.

  • asym_symtext (Symtext) –
Source code in minimalsym/api.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
def _project_linear(mol, sea, asym_symtext):
    """
    Project atoms onto the molecular axis for linear groups (C0v / D0h).

    For C0v all atoms are collapsed onto the z-axis.
    For D0h atoms come in inversion-related pairs; the central atom (if any)
    is placed at the origin.

    Parameters
    ----------
    mol : ase.Atoms
        Molecule being symmetrized (modified in-place).
    sea : SEA
        Symmetry-equivalent-atoms group to process.
    asym_symtext : Symtext
    """
    z = np.array([0.0, 0.0, 1.0])
    atom_i = sea.subset[0]

    if asym_symtext.pg.family == "C":
        # C0v: all atoms lie on z; zero out x and y.
        for atom_j in sea.subset:
            mol.positions[atom_j, :] = np.array([0.0, 0.0, np.dot(mol.positions[atom_j, :], z)])
        return

    # Maybe should ask elif asym_symtext.pg.family == "D":
    # D0h: atoms come in inversion-related pairs.
    if atom_i == asym_symtext.atom_map[atom_i, 1]:
        mol.positions[atom_i, :] = np.array([0.0, 0.0, 0.0])
    else:
        mol.positions[atom_i, :] = np.array([0.0, 0.0, np.dot(mol.positions[atom_i, :], z)])

    for atom_j in sea.subset[1:]:
        mol.positions[atom_j, :] = np.array([0.0, 0.0, np.dot(mol.positions[atom_j, :], z)])
        if atom_j == asym_symtext.atom_map[atom_i, 1]:
            # Inversion partner of atom_i: negate its axial position.
            mol.positions[atom_j, :] = np.dot(-np.eye(3), mol.positions[atom_i, :])

_project_atom

_project_atom(mol, atom_i, asym_symtext)

Project the SEA representative atom_i onto the symmetry element that fixes it.

Searches for a non-identity symel g such that atom_map[atom_i, g] == atom_i, then applies the appropriate geometric projection:

  • i / S_n : place the atom at the origin.
  • C_n axis : keep only the projection along the axis vector.
  • sigma plane : subtract the normal component (project into the plane).

Parameters:

  • mol (Atoms) –

    Molecule being symmetrized (modified in-place).

  • atom_i (int) –

    Index of the SEA representative atom.

  • asym_symtext (Symtext) –

Raises:

  • Exception

    If an unrecognised symmetry element type is encountered.

Source code in minimalsym/api.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def _project_atom(mol, atom_i, asym_symtext):
    """
    Project the SEA representative atom_i onto the symmetry element that fixes it.

    Searches for a non-identity symel g such that atom_map[atom_i, g] == atom_i,
    then applies the appropriate geometric projection:

    - i / S_n   : place the atom at the origin.
    - C_n axis  : keep only the projection along the axis vector.
    - sigma plane : subtract the normal component (project into the plane).

    Parameters
    ----------
    mol : ase.Atoms
        Molecule being symmetrized (modified in-place).
    atom_i : int
        Index of the SEA representative atom.
    asym_symtext : Symtext

    Raises
    ------
    Exception
        If an unrecognised symmetry element type is encountered.
    """
    for g in range(1, asym_symtext.order):
        if atom_i != asym_symtext.atom_map[atom_i, g]:
            continue
        sym = asym_symtext.symels[g]
        if sym.symbol == "i" or sym.symbol[0] == "S":
            mol.positions[atom_i, :] = np.array([0.0, 0.0, 0.0])
            break
        elif sym.symbol[0] == "C":
            l = np.dot(mol.positions[atom_i, :], sym.vector)
            mol.positions[atom_i, :] = l * sym.vector
        elif sym.symbol[:5] == "sigma":
            l = np.dot(mol.positions[atom_i, :], sym.vector)
            mol.positions[atom_i, :] -= l * sym.vector
        else:
            raise Exception(f"Unexpected symmetry element type: '{sym.symbol}'")

_force_symmetry_from_representative

_force_symmetry_from_representative(mol, atom_i, asym_symtext)

Map the remaining symmetrically equivalent atoms from the (already projected) representative.

For each symel_g in symels (except identity operation), atom_j is the atom mapped to atom_i transformed by symel_g. atom_j is assigned the position of atom_i transformed by symel_g.

Parameters:

  • mol (Atoms) –

    Molecule being symmetrized (modified in-place).

  • atom_i (int) –

    Index of the (already projected) SEA representative.

  • asym_symtext (Symtext) –
Source code in minimalsym/api.py
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
def _force_symmetry_from_representative(mol, atom_i, asym_symtext):
    """
    Map the remaining symmetrically equivalent atoms from the
    (already projected) representative.

    For each symel_g in symels (except identity operation),
    atom_j is the atom mapped to atom_i transformed by symel_g.
    atom_j is assigned the position of atom_i transformed by symel_g.

    Parameters
    ----------
    mol : ase.Atoms
        Molecule being symmetrized (modified in-place).
    atom_i : int
        Index of the (already projected) SEA representative.
    asym_symtext : Symtext
    """
    for g in range(1, asym_symtext.order):
        atom_j = asym_symtext.atom_map[atom_i, g]
        mol.positions[atom_j, :] = np.dot(
            asym_symtext.symels[g].rrep, mol.positions[atom_i, :]
        )

_get_error

_get_error(a_pos: array, b_pos: array) -> float

Returns RMSD of two positions np.arrays.

Parameters:

  • a_pos (array) –

    Positions of molecule A to get RMSD.

  • b_pos (array) –

    Positions of molecule B to get RMSD.

Returns:

  • float

    RMSD of the two positions.

Source code in minimalsym/api.py
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
def _get_error(a_pos: np.array, b_pos: np.array) -> float:
    """
    Returns RMSD of two positions np.arrays.

    Parameters
    ----------
    a_pos : np.array
        Positions of molecule A to get RMSD.
    b_pos : np.array
        Positions of molecule B to get RMSD.

    Returns
    -------
    : float
        RMSD of the two positions.
    """
    difference = a_pos-b_pos
    return np.sqrt((difference**2).sum() / len(a_pos))