Skip to content

kups.potential.classical

Classical molecular mechanics force fields.

This module provides standard force field components used in molecular simulations: non-bonded interactions (Lennard-Jones, Coulomb, Ewald) and bonded terms (harmonic bonds, angles, dihedrals). All potentials support tail corrections, cutoffs, and neighbor lists.

Available Potentials

  • Lennard-Jones: Van der Waals interactions with optional tail corrections
  • Coulomb: Electrostatic interactions
  • Ewald: Long-range electrostatics via Ewald summation
  • Harmonic: Bonded interactions (bonds, angles)
  • Cosine Angle: UFF-style cosine angle bending
  • Morse: Anharmonic bond stretching with proper dissociation
  • Dihedral: Torsion potentials (UFF-style)
  • Inversion: Out-of-plane/improper potentials (UFF-style)

Each potential provides a make_*_potential factory function that constructs a configured Potential instance.

CosineAngleParameters

UFF-style cosine angle potential parameters.

Attributes:

Name Type Description
labels tuple[Label, ...]

Species labels, shape (n_species,).

theta0 Array

Equilibrium angles [radians], shape (n_species, n_species, n_species).

k Array

Force constants [energy], shape (n_species, n_species, n_species).

linear_tol Array

Tolerance for detecting linear angles [radians].

Source code in src/kups/potential/classical/cosine_angle.py
@dataclass
class CosineAngleParameters:
    """UFF-style cosine angle potential parameters.

    Attributes:
        labels: Species labels, shape `(n_species,)`.
        theta0: Equilibrium angles [radians], shape `(n_species, n_species, n_species)`.
        k: Force constants [energy], shape `(n_species, n_species, n_species)`.
        linear_tol: Tolerance for detecting linear angles [radians].
    """

    labels: tuple[Label, ...] = field(static=True)  # (n_species,)
    theta0: Array  # (n_species, n_species, n_species)
    k: Array  # (n_species, n_species, n_species)
    linear_tol: Array = field(default_factory=lambda: jnp.radians(5))

    @classmethod
    def from_uff(
        cls,
        labels: tuple[str, ...],
        bond_angle: Array,
        bond_radius: Array,
        electronegativity: Array,
        effective_charge: Array,
        linear_tol: Array = jnp.radians(5),
    ) -> "CosineAngleParameters":
        r"""Create angle parameters using UFF formulas.

        Computes angle parameters from per-species atomic properties:
        - $\theta_0$ from central atom's bond angle
        - $K$ from Eq. 13 using bond lengths and effective charges

        Args:
            labels: Species labels, shape `(n_species,)`
            bond_angle: Natural valence angle [radians], shape `(n_species,)`
            bond_radius: Valence bond radii [A], shape `(n_species,)`
            electronegativity: GMP electronegativity, shape `(n_species,)`
            effective_charge: Effective atomic charge Z*, shape `(n_species,)`
            linear_tol: Tolerance for detecting linear angles [radians]

        Returns:
            CosineAngleParameters with full interaction matrices
        """
        nt = bond_angle.shape[0]

        # theta0 from central atom (index j in i-j-k)
        theta0 = bond_angle[None, :, None] * jnp.ones((nt, nt, nt))

        # Compute bond lengths for all pairs
        r_bond = compute_uff_bond_length(bond_radius, electronegativity)

        # r_ij = r_bond[i, j], r_jk = r_bond[j, k]
        r_ij = r_bond[:, :, None]
        r_jk = r_bond[None, :, :]

        # r_ik using law of cosines
        cos_theta0 = jnp.cos(theta0)
        r_ik_sq = r_ij**2 + r_jk**2 - 2 * r_ij * r_jk * cos_theta0
        r_ik = jnp.sqrt(jnp.maximum(r_ik_sq, 1e-10))

        # Force constant (Eq. 13)
        Z_i = effective_charge[:, None, None]
        Z_k = effective_charge[None, None, :]
        prefactor = 664.12 / (r_ij * r_jk)
        term = 3 * r_ij * r_jk * (1 - cos_theta0**2) - r_ik_sq * cos_theta0
        K = jnp.abs(prefactor * Z_i * Z_k / (r_ik**5) * term)

        return cls(
            labels=tuple(map(Label, labels)), theta0=theta0, k=K, linear_tol=linear_tol
        )

from_uff(labels, bond_angle, bond_radius, electronegativity, effective_charge, linear_tol=jnp.radians(5)) classmethod

Create angle parameters using UFF formulas.

Computes angle parameters from per-species atomic properties: - \(\theta_0\) from central atom's bond angle - \(K\) from Eq. 13 using bond lengths and effective charges

Parameters:

Name Type Description Default
labels tuple[str, ...]

Species labels, shape (n_species,)

required
bond_angle Array

Natural valence angle [radians], shape (n_species,)

required
bond_radius Array

Valence bond radii [A], shape (n_species,)

required
electronegativity Array

GMP electronegativity, shape (n_species,)

required
effective_charge Array

Effective atomic charge Z*, shape (n_species,)

required
linear_tol Array

Tolerance for detecting linear angles [radians]

radians(5)

Returns:

Type Description
CosineAngleParameters

CosineAngleParameters with full interaction matrices

Source code in src/kups/potential/classical/cosine_angle.py
@classmethod
def from_uff(
    cls,
    labels: tuple[str, ...],
    bond_angle: Array,
    bond_radius: Array,
    electronegativity: Array,
    effective_charge: Array,
    linear_tol: Array = jnp.radians(5),
) -> "CosineAngleParameters":
    r"""Create angle parameters using UFF formulas.

    Computes angle parameters from per-species atomic properties:
    - $\theta_0$ from central atom's bond angle
    - $K$ from Eq. 13 using bond lengths and effective charges

    Args:
        labels: Species labels, shape `(n_species,)`
        bond_angle: Natural valence angle [radians], shape `(n_species,)`
        bond_radius: Valence bond radii [A], shape `(n_species,)`
        electronegativity: GMP electronegativity, shape `(n_species,)`
        effective_charge: Effective atomic charge Z*, shape `(n_species,)`
        linear_tol: Tolerance for detecting linear angles [radians]

    Returns:
        CosineAngleParameters with full interaction matrices
    """
    nt = bond_angle.shape[0]

    # theta0 from central atom (index j in i-j-k)
    theta0 = bond_angle[None, :, None] * jnp.ones((nt, nt, nt))

    # Compute bond lengths for all pairs
    r_bond = compute_uff_bond_length(bond_radius, electronegativity)

    # r_ij = r_bond[i, j], r_jk = r_bond[j, k]
    r_ij = r_bond[:, :, None]
    r_jk = r_bond[None, :, :]

    # r_ik using law of cosines
    cos_theta0 = jnp.cos(theta0)
    r_ik_sq = r_ij**2 + r_jk**2 - 2 * r_ij * r_jk * cos_theta0
    r_ik = jnp.sqrt(jnp.maximum(r_ik_sq, 1e-10))

    # Force constant (Eq. 13)
    Z_i = effective_charge[:, None, None]
    Z_k = effective_charge[None, None, :]
    prefactor = 664.12 / (r_ij * r_jk)
    term = 3 * r_ij * r_jk * (1 - cos_theta0**2) - r_ik_sq * cos_theta0
    K = jnp.abs(prefactor * Z_i * Z_k / (r_ik**5) * term)

    return cls(
        labels=tuple(map(Label, labels)), theta0=theta0, k=K, linear_tol=linear_tol
    )

DihedralParameters

UFF dihedral/torsion potential parameters.

Attributes:

Name Type Description
labels tuple[Label, ...]

Species labels, shape (n_species,).

V Array

Barrier heights [energy], shape (n_species, n_species, n_species, n_species).

n Array

Periodicities, shape (n_species, n_species, n_species, n_species).

phi0 Array

Equilibrium dihedral angles [radians], shape (n_species, n_species, n_species, n_species).

Source code in src/kups/potential/classical/dihedral.py
@dataclass
class DihedralParameters:
    """UFF dihedral/torsion potential parameters.

    Attributes:
        labels: Species labels, shape `(n_species,)`.
        V: Barrier heights [energy], shape `(n_species, n_species, n_species, n_species)`.
        n: Periodicities, shape `(n_species, n_species, n_species, n_species)`.
        phi0: Equilibrium dihedral angles [radians],
            shape `(n_species, n_species, n_species, n_species)`.
    """

    labels: tuple[Label, ...] = field(static=True)  # (n_species,)
    V: Array  # (n_species, n_species, n_species, n_species)
    n: Array  # (n_species, n_species, n_species, n_species)
    phi0: Array  # (n_species, n_species, n_species, n_species)

    @classmethod
    def from_uff(
        cls,
        labels: tuple[str, ...],
        bond_angle: Array,
        torsion_sp3: Array,
        torsion_sp2: Array,
        group: Array,
        bond_order: Array | None = None,
        hybridization_tol: float = 5.0 * 3.141592653589793 / 180.0,
    ) -> "DihedralParameters":
        r"""Create dihedral parameters using UFF formulas.

        Computes torsion parameters based on central bond hybridization following
        the UFF paper (Rappe et al. 1992). For dihedral i-j-k-l, parameters depend
        on the central j-k bond and neighboring atoms.

        Args:
            labels: Species labels, shape `(n_species,)`.
            bond_angle: Natural valence angle [radians], shape `(n_species,)`.
            torsion_sp3: sp3 torsional barrier V [kcal/mol], shape `(n_species,)`.
            torsion_sp2: sp2 torsional parameter U [kcal/mol], shape `(n_species,)`.
            group: Periodic table group (1-18), shape `(n_species,)`.
            bond_order: Bond order for j-k bond, shape `(n_species, n_species)`.
            hybridization_tol: Tolerance for hybridization detection [radians].

        Returns:
            DihedralParameters with full 4D interaction matrices.
        """
        nt = bond_angle.shape[0]

        # Determine hybridization from bond angle
        is_sp2 = jnp.abs(bond_angle - jnp.radians(120.0)) < hybridization_tol
        is_sp3 = ~(jnp.abs(bond_angle - jnp.pi) < hybridization_tol) & ~is_sp2

        # Group 6 and main group detection
        is_group6 = group == 6
        is_main_group = group > 0

        # Broadcast to 4D for dihedral i-j-k-l
        is_sp2_i = is_sp2[:, None, None, None]
        is_sp3_j = is_sp3[None, :, None, None]
        is_sp3_k = is_sp3[None, None, :, None]
        is_sp2_j = is_sp2[None, :, None, None]
        is_sp2_k = is_sp2[None, None, :, None]
        is_sp2_l = is_sp2[None, None, None, :]

        is_group6_j = is_group6[None, :, None, None]
        is_group6_k = is_group6[None, None, :, None]
        is_main_j = is_main_group[None, :, None, None]
        is_main_k = is_main_group[None, None, :, None]

        # Torsional parameters broadcast to 4D
        V_j = torsion_sp3[None, :, None, None]
        V_k = torsion_sp3[None, None, :, None]
        U_j = torsion_sp2[None, :, None, None]
        U_k = torsion_sp2[None, None, :, None]

        # Bond order for central j-k bond
        if bond_order is None:
            BO_jk = jnp.ones((1, nt, nt, 1))
        else:
            BO_jk = bond_order[None, :, :, None]

        # V from Eq.17: V = 5 * sqrt(U_j * U_k) * (1 + 4.18 * ln(BO))
        V_eq17 = (
            5.0
            * jnp.sqrt(jnp.abs(U_j * U_k))
            * (1.0 + 4.18 * jnp.log(jnp.maximum(BO_jk, 1e-10)))
        )

        # Initialize arrays
        V_arr = jnp.zeros((nt, nt, nt, nt))
        n_arr = jnp.ones((nt, nt, nt, nt), dtype=jnp.int32)
        phi0_arr = jnp.zeros((nt, nt, nt, nt))

        # === Case 1: sp3-sp3 general ===
        sp3_sp3 = is_sp3_j & is_sp3_k
        V_arr = jnp.where(sp3_sp3, jnp.sqrt(jnp.abs(V_j * V_k)), V_arr)
        n_arr = jnp.where(sp3_sp3, 3, n_arr)
        phi0_arr = jnp.where(sp3_sp3, jnp.pi, phi0_arr)

        # === Case 2: sp3-sp3 Group 6 pair (override n and phi0) ===
        sp3_sp3_group6 = sp3_sp3 & is_group6_j & is_group6_k
        n_arr = jnp.where(sp3_sp3_group6, 2, n_arr)
        phi0_arr = jnp.where(sp3_sp3_group6, jnp.pi / 2, phi0_arr)

        # === Case 3: sp2-sp2 ===
        sp2_sp2 = is_sp2_j & is_sp2_k
        V_arr = jnp.where(sp2_sp2, V_eq17, V_arr)
        n_arr = jnp.where(sp2_sp2, 2, n_arr)
        phi0_arr = jnp.where(sp2_sp2, jnp.pi, phi0_arr)

        # === Case 4: sp3-sp2 general ===
        sp3_sp2 = (is_sp3_j & is_sp2_k) | (is_sp2_j & is_sp3_k)
        V_arr = jnp.where(sp3_sp2, 1.0, V_arr)
        n_arr = jnp.where(sp3_sp2, 6, n_arr)
        phi0_arr = jnp.where(sp3_sp2, 0.0, phi0_arr)

        # === Case 5: sp3-sp2 propene-like (sp2 bonded to another sp2) ===
        propene = (is_sp2_j & is_sp3_k & is_sp2_i) | (is_sp3_j & is_sp2_k & is_sp2_l)
        V_arr = jnp.where(propene, 2.0, V_arr)
        n_arr = jnp.where(propene, 3, n_arr)
        phi0_arr = jnp.where(propene, jnp.pi, phi0_arr)

        # === Case 6: sp3-sp2 with Group 6 sp3 ===
        group6_sp3_sp2 = (is_sp3_j & is_group6_j & is_sp2_k) | (
            is_sp2_j & is_sp3_k & is_group6_k
        )
        V_arr = jnp.where(group6_sp3_sp2, V_eq17, V_arr)
        n_arr = jnp.where(group6_sp3_sp2, 2, n_arr)
        phi0_arr = jnp.where(group6_sp3_sp2, jnp.pi / 2, phi0_arr)

        # === Case 7: Non-main-group (V=0) ===
        non_main = ~is_main_j | ~is_main_k
        V_arr = jnp.where(non_main, 0.0, V_arr)

        return cls(labels=tuple(map(Label, labels)), V=V_arr, n=n_arr, phi0=phi0_arr)

from_uff(labels, bond_angle, torsion_sp3, torsion_sp2, group, bond_order=None, hybridization_tol=5.0 * 3.141592653589793 / 180.0) classmethod

Create dihedral parameters using UFF formulas.

Computes torsion parameters based on central bond hybridization following the UFF paper (Rappe et al. 1992). For dihedral i-j-k-l, parameters depend on the central j-k bond and neighboring atoms.

Parameters:

Name Type Description Default
labels tuple[str, ...]

Species labels, shape (n_species,).

required
bond_angle Array

Natural valence angle [radians], shape (n_species,).

required
torsion_sp3 Array

sp3 torsional barrier V [kcal/mol], shape (n_species,).

required
torsion_sp2 Array

sp2 torsional parameter U [kcal/mol], shape (n_species,).

required
group Array

Periodic table group (1-18), shape (n_species,).

required
bond_order Array | None

Bond order for j-k bond, shape (n_species, n_species).

None
hybridization_tol float

Tolerance for hybridization detection [radians].

5.0 * 3.141592653589793 / 180.0

Returns:

Type Description
DihedralParameters

DihedralParameters with full 4D interaction matrices.

Source code in src/kups/potential/classical/dihedral.py
@classmethod
def from_uff(
    cls,
    labels: tuple[str, ...],
    bond_angle: Array,
    torsion_sp3: Array,
    torsion_sp2: Array,
    group: Array,
    bond_order: Array | None = None,
    hybridization_tol: float = 5.0 * 3.141592653589793 / 180.0,
) -> "DihedralParameters":
    r"""Create dihedral parameters using UFF formulas.

    Computes torsion parameters based on central bond hybridization following
    the UFF paper (Rappe et al. 1992). For dihedral i-j-k-l, parameters depend
    on the central j-k bond and neighboring atoms.

    Args:
        labels: Species labels, shape `(n_species,)`.
        bond_angle: Natural valence angle [radians], shape `(n_species,)`.
        torsion_sp3: sp3 torsional barrier V [kcal/mol], shape `(n_species,)`.
        torsion_sp2: sp2 torsional parameter U [kcal/mol], shape `(n_species,)`.
        group: Periodic table group (1-18), shape `(n_species,)`.
        bond_order: Bond order for j-k bond, shape `(n_species, n_species)`.
        hybridization_tol: Tolerance for hybridization detection [radians].

    Returns:
        DihedralParameters with full 4D interaction matrices.
    """
    nt = bond_angle.shape[0]

    # Determine hybridization from bond angle
    is_sp2 = jnp.abs(bond_angle - jnp.radians(120.0)) < hybridization_tol
    is_sp3 = ~(jnp.abs(bond_angle - jnp.pi) < hybridization_tol) & ~is_sp2

    # Group 6 and main group detection
    is_group6 = group == 6
    is_main_group = group > 0

    # Broadcast to 4D for dihedral i-j-k-l
    is_sp2_i = is_sp2[:, None, None, None]
    is_sp3_j = is_sp3[None, :, None, None]
    is_sp3_k = is_sp3[None, None, :, None]
    is_sp2_j = is_sp2[None, :, None, None]
    is_sp2_k = is_sp2[None, None, :, None]
    is_sp2_l = is_sp2[None, None, None, :]

    is_group6_j = is_group6[None, :, None, None]
    is_group6_k = is_group6[None, None, :, None]
    is_main_j = is_main_group[None, :, None, None]
    is_main_k = is_main_group[None, None, :, None]

    # Torsional parameters broadcast to 4D
    V_j = torsion_sp3[None, :, None, None]
    V_k = torsion_sp3[None, None, :, None]
    U_j = torsion_sp2[None, :, None, None]
    U_k = torsion_sp2[None, None, :, None]

    # Bond order for central j-k bond
    if bond_order is None:
        BO_jk = jnp.ones((1, nt, nt, 1))
    else:
        BO_jk = bond_order[None, :, :, None]

    # V from Eq.17: V = 5 * sqrt(U_j * U_k) * (1 + 4.18 * ln(BO))
    V_eq17 = (
        5.0
        * jnp.sqrt(jnp.abs(U_j * U_k))
        * (1.0 + 4.18 * jnp.log(jnp.maximum(BO_jk, 1e-10)))
    )

    # Initialize arrays
    V_arr = jnp.zeros((nt, nt, nt, nt))
    n_arr = jnp.ones((nt, nt, nt, nt), dtype=jnp.int32)
    phi0_arr = jnp.zeros((nt, nt, nt, nt))

    # === Case 1: sp3-sp3 general ===
    sp3_sp3 = is_sp3_j & is_sp3_k
    V_arr = jnp.where(sp3_sp3, jnp.sqrt(jnp.abs(V_j * V_k)), V_arr)
    n_arr = jnp.where(sp3_sp3, 3, n_arr)
    phi0_arr = jnp.where(sp3_sp3, jnp.pi, phi0_arr)

    # === Case 2: sp3-sp3 Group 6 pair (override n and phi0) ===
    sp3_sp3_group6 = sp3_sp3 & is_group6_j & is_group6_k
    n_arr = jnp.where(sp3_sp3_group6, 2, n_arr)
    phi0_arr = jnp.where(sp3_sp3_group6, jnp.pi / 2, phi0_arr)

    # === Case 3: sp2-sp2 ===
    sp2_sp2 = is_sp2_j & is_sp2_k
    V_arr = jnp.where(sp2_sp2, V_eq17, V_arr)
    n_arr = jnp.where(sp2_sp2, 2, n_arr)
    phi0_arr = jnp.where(sp2_sp2, jnp.pi, phi0_arr)

    # === Case 4: sp3-sp2 general ===
    sp3_sp2 = (is_sp3_j & is_sp2_k) | (is_sp2_j & is_sp3_k)
    V_arr = jnp.where(sp3_sp2, 1.0, V_arr)
    n_arr = jnp.where(sp3_sp2, 6, n_arr)
    phi0_arr = jnp.where(sp3_sp2, 0.0, phi0_arr)

    # === Case 5: sp3-sp2 propene-like (sp2 bonded to another sp2) ===
    propene = (is_sp2_j & is_sp3_k & is_sp2_i) | (is_sp3_j & is_sp2_k & is_sp2_l)
    V_arr = jnp.where(propene, 2.0, V_arr)
    n_arr = jnp.where(propene, 3, n_arr)
    phi0_arr = jnp.where(propene, jnp.pi, phi0_arr)

    # === Case 6: sp3-sp2 with Group 6 sp3 ===
    group6_sp3_sp2 = (is_sp3_j & is_group6_j & is_sp2_k) | (
        is_sp2_j & is_sp3_k & is_group6_k
    )
    V_arr = jnp.where(group6_sp3_sp2, V_eq17, V_arr)
    n_arr = jnp.where(group6_sp3_sp2, 2, n_arr)
    phi0_arr = jnp.where(group6_sp3_sp2, jnp.pi / 2, phi0_arr)

    # === Case 7: Non-main-group (V=0) ===
    non_main = ~is_main_j | ~is_main_k
    V_arr = jnp.where(non_main, 0.0, V_arr)

    return cls(labels=tuple(map(Label, labels)), V=V_arr, n=n_arr, phi0=phi0_arr)

EwaldParameters

Ewald summation parameters: convergence settings and reciprocal lattice vectors.

Attributes:

Name Type Description
alpha Table[SystemId, Array]

Ewald screening parameter [1/Ang], shape (n_graphs,).

cutoff Table[SystemId, Array]

Real-space cutoff radius [Ang], shape (n_graphs,).

reciprocal_lattice_shifts Table[SystemId, Array]

Integer k-vector coefficients, shape (n_graphs, n_kvecs, 3).

Source code in src/kups/potential/classical/ewald.py
@dataclass
class EwaldParameters:
    """Ewald summation parameters: convergence settings and reciprocal lattice vectors.

    Attributes:
        alpha: Ewald screening parameter [1/Ang], shape `(n_graphs,)`.
        cutoff: Real-space cutoff radius [Ang], shape `(n_graphs,)`.
        reciprocal_lattice_shifts: Integer k-vector coefficients,
            shape `(n_graphs, n_kvecs, 3)`.
    """

    alpha: Table[SystemId, Array]  # (n_graphs,)
    cutoff: Table[SystemId, Array]  # (n_graphs,)
    reciprocal_lattice_shifts: Table[
        SystemId, Array
    ]  # (n_graphs, n_rec_shifts, 3) integers

    @classmethod
    @no_jax_tracing
    def make(
        cls,
        charges: Table[ParticleId, IsEwaldPointData],
        unitcell: Table[SystemId, HasUnitCell],
        epsilon_total: float = 1e-8,
        real_cutoff: float | None = None,
    ) -> EwaldParameters:
        """Estimate Ewald parameters from indexed particles and systems.

        Splits particles by system index, estimates per-system parameters,
        and zero-pads k-vectors to the maximum count across systems.

        Args:
            charges: Indexed particles with charges and system assignment.
            unitcell: Indexed systems with unit cells.
            epsilon_total: Target total accuracy for the Ewald sum.
            real_cutoff: Optional real-space cutoff override; estimated if not given.

        Returns:
            ``EwaldParameters`` with estimated physics parameters.
        """
        # Unpack Indexed into per-system lists
        n_systems = len(unitcell.keys)
        sys_idx = charges.data.system.indices
        charges_list = [charges.data.charges[sys_idx == i] for i in range(n_systems)]
        unitcell_list = [unitcell.data.unitcell[i] for i in range(n_systems)]

        estimates_list = [
            estimate_ewald_parameters(
                c, u, real_cutoff=real_cutoff, epsilon_total=epsilon_total
            )
            for c, u in zip(charges_list, unitcell_list)
        ]
        shifts_list = [
            kvecs_from_kmax(u, est.k_max)
            for u, est in zip(unitcell_list, estimates_list)
        ]
        max_n_kvecs = max(len(s) for s in shifts_list)
        padded_shifts = jnp.stack(
            [jnp.pad(s, [(0, max_n_kvecs - len(s)), (0, 0)]) for s in shifts_list]
        )
        return cls(
            alpha=Table(
                unitcell.keys,
                jnp.asarray([est.alpha for est in estimates_list]),
            ),
            cutoff=Table(
                unitcell.keys,
                jnp.asarray([est.real_cutoff for est in estimates_list]),
            ),
            reciprocal_lattice_shifts=Table(unitcell.keys, padded_shifts),
        )

make(charges, unitcell, epsilon_total=1e-08, real_cutoff=None) classmethod

Estimate Ewald parameters from indexed particles and systems.

Splits particles by system index, estimates per-system parameters, and zero-pads k-vectors to the maximum count across systems.

Parameters:

Name Type Description Default
charges Table[ParticleId, IsEwaldPointData]

Indexed particles with charges and system assignment.

required
unitcell Table[SystemId, HasUnitCell]

Indexed systems with unit cells.

required
epsilon_total float

Target total accuracy for the Ewald sum.

1e-08
real_cutoff float | None

Optional real-space cutoff override; estimated if not given.

None

Returns:

Type Description
EwaldParameters

EwaldParameters with estimated physics parameters.

Source code in src/kups/potential/classical/ewald.py
@classmethod
@no_jax_tracing
def make(
    cls,
    charges: Table[ParticleId, IsEwaldPointData],
    unitcell: Table[SystemId, HasUnitCell],
    epsilon_total: float = 1e-8,
    real_cutoff: float | None = None,
) -> EwaldParameters:
    """Estimate Ewald parameters from indexed particles and systems.

    Splits particles by system index, estimates per-system parameters,
    and zero-pads k-vectors to the maximum count across systems.

    Args:
        charges: Indexed particles with charges and system assignment.
        unitcell: Indexed systems with unit cells.
        epsilon_total: Target total accuracy for the Ewald sum.
        real_cutoff: Optional real-space cutoff override; estimated if not given.

    Returns:
        ``EwaldParameters`` with estimated physics parameters.
    """
    # Unpack Indexed into per-system lists
    n_systems = len(unitcell.keys)
    sys_idx = charges.data.system.indices
    charges_list = [charges.data.charges[sys_idx == i] for i in range(n_systems)]
    unitcell_list = [unitcell.data.unitcell[i] for i in range(n_systems)]

    estimates_list = [
        estimate_ewald_parameters(
            c, u, real_cutoff=real_cutoff, epsilon_total=epsilon_total
        )
        for c, u in zip(charges_list, unitcell_list)
    ]
    shifts_list = [
        kvecs_from_kmax(u, est.k_max)
        for u, est in zip(unitcell_list, estimates_list)
    ]
    max_n_kvecs = max(len(s) for s in shifts_list)
    padded_shifts = jnp.stack(
        [jnp.pad(s, [(0, max_n_kvecs - len(s)), (0, 0)]) for s in shifts_list]
    )
    return cls(
        alpha=Table(
            unitcell.keys,
            jnp.asarray([est.alpha for est in estimates_list]),
        ),
        cutoff=Table(
            unitcell.keys,
            jnp.asarray([est.real_cutoff for est in estimates_list]),
        ),
        reciprocal_lattice_shifts=Table(unitcell.keys, padded_shifts),
    )

GlobalTailCorrectedLennardJonesParameters

Bases: LennardJonesParameters

Lennard-Jones parameters with analytical long-range correction.

Attributes:

Name Type Description
tail_corrected Array

Enable correction per species pair, shape (n_species, n_species).

Source code in src/kups/potential/classical/lennard_jones.py
@dataclass
class GlobalTailCorrectedLennardJonesParameters(LennardJonesParameters):
    """Lennard-Jones parameters with analytical long-range correction.

    Attributes:
        tail_corrected: Enable correction per species pair, shape ``(n_species, n_species)``.
    """

    tail_corrected: Array  # (n_species, n_species) bool

    @classmethod
    def from_dict(
        cls,
        cutoff: float | Array,
        parameters: dict[str, tuple[float | None, float | None]],
        mixing_rule: MixingRule,
        tail_correction: bool = True,
    ) -> GlobalTailCorrectedLennardJonesParameters:
        """Create tail-corrected parameters from a dict of per-species values.

        Args:
            cutoff: Cutoff radius [Angstrom].
            parameters: Map from species label to ``(sigma, epsilon)`` pair.
                ``None`` values default to ``sigma=1.0``, ``epsilon=0.0``.
            mixing_rule: Combining rule for cross-species interactions.
            tail_correction: Whether to enable tail corrections for all
                non-zero epsilon pairs.
        """
        base = LennardJonesParameters.from_dict(cutoff, parameters, mixing_rule)
        mask = (
            base.epsilon > 0
            if tail_correction
            else jnp.zeros_like(base.epsilon, dtype=bool)
        )
        return cls(
            labels=base.labels,
            sigma=base.sigma,
            epsilon=base.epsilon,
            cutoff=base.cutoff,
            tail_corrected=mask,
        )

from_dict(cutoff, parameters, mixing_rule, tail_correction=True) classmethod

Create tail-corrected parameters from a dict of per-species values.

Parameters:

Name Type Description Default
cutoff float | Array

Cutoff radius [Angstrom].

required
parameters dict[str, tuple[float | None, float | None]]

Map from species label to (sigma, epsilon) pair. None values default to sigma=1.0, epsilon=0.0.

required
mixing_rule MixingRule

Combining rule for cross-species interactions.

required
tail_correction bool

Whether to enable tail corrections for all non-zero epsilon pairs.

True
Source code in src/kups/potential/classical/lennard_jones.py
@classmethod
def from_dict(
    cls,
    cutoff: float | Array,
    parameters: dict[str, tuple[float | None, float | None]],
    mixing_rule: MixingRule,
    tail_correction: bool = True,
) -> GlobalTailCorrectedLennardJonesParameters:
    """Create tail-corrected parameters from a dict of per-species values.

    Args:
        cutoff: Cutoff radius [Angstrom].
        parameters: Map from species label to ``(sigma, epsilon)`` pair.
            ``None`` values default to ``sigma=1.0``, ``epsilon=0.0``.
        mixing_rule: Combining rule for cross-species interactions.
        tail_correction: Whether to enable tail corrections for all
            non-zero epsilon pairs.
    """
    base = LennardJonesParameters.from_dict(cutoff, parameters, mixing_rule)
    mask = (
        base.epsilon > 0
        if tail_correction
        else jnp.zeros_like(base.epsilon, dtype=bool)
    )
    return cls(
        labels=base.labels,
        sigma=base.sigma,
        epsilon=base.epsilon,
        cutoff=base.cutoff,
        tail_corrected=mask,
    )

HarmonicAngleParameters

Harmonic angle potential parameters.

Attributes:

Name Type Description
labels tuple[Label, ...]

Species labels, shape (n_species,)

theta0 Array

Equilibrium angles [degrees], shape (n_species, n_species, n_species)

k Array

Force constants [energy/degree²], shape (n_species, n_species, n_species)

Source code in src/kups/potential/classical/harmonic.py
@dataclass
class HarmonicAngleParameters:
    """Harmonic angle potential parameters.

    Attributes:
        labels: Species labels, shape `(n_species,)`
        theta0: Equilibrium angles [degrees], shape `(n_species, n_species, n_species)`
        k: Force constants [energy/degree²], shape `(n_species, n_species, n_species)`
    """

    labels: tuple[Label, ...] = field(static=True)  # (n_species,)
    theta0: Array  # (n_species, n_species, n_species)
    k: Array  # (n_species, n_species, n_species)

HarmonicBondParameters

Harmonic bond potential parameters.

Attributes:

Name Type Description
labels tuple[Label, ...]

Species labels, shape (n_species,)

x0 Array

Equilibrium bond lengths [Å], shape (n_species, n_species)

k Array

Force constants [energy/Ų], shape (n_species, n_species)

Source code in src/kups/potential/classical/harmonic.py
@dataclass
class HarmonicBondParameters:
    """Harmonic bond potential parameters.

    Attributes:
        labels: Species labels, shape `(n_species,)`
        x0: Equilibrium bond lengths [Å], shape `(n_species, n_species)`
        k: Force constants [energy/Ų], shape `(n_species, n_species)`
    """

    labels: tuple[Label, ...] = field(static=True)  # (n_species,)
    x0: Array  # (n_species, n_species)
    k: Array  # (n_species, n_species)

InversionParameters

UFF-style inversion potential parameters.

Attributes:

Name Type Description
labels tuple[Label, ...]

Species labels, shape (n_species,).

omega0 Array

Equilibrium out-of-plane angles [radians], shape (n_species, n_species, n_species, n_species).

k Array

Force constant or barrier [energy], shape (n_species, n_species, n_species, n_species).

Source code in src/kups/potential/classical/inversion.py
@dataclass
class InversionParameters:
    r"""UFF-style inversion potential parameters.

    Attributes:
        labels: Species labels, shape `(n_species,)`.
        omega0: Equilibrium out-of-plane angles [radians],
            shape `(n_species, n_species, n_species, n_species)`.
        k: Force constant or barrier [energy],
            shape `(n_species, n_species, n_species, n_species)`.
    """

    labels: tuple[Label, ...] = field(static=True)  # (n_species,)
    omega0: Array  # (n_species, n_species, n_species, n_species)
    k: Array  # (n_species, n_species, n_species, n_species)

    @classmethod
    def from_uff(
        cls,
        labels: tuple[str, ...],
        inversion_barrier: Array,
        omega0: Array | None = None,
    ) -> "InversionParameters":
        r"""Create inversion parameters using UFF formulas.

        Args:
            labels: Species labels, shape `(n_species,)`.
            inversion_barrier: Barrier/force constant for each type, shape `(n_species,)`.
            omega0: Equilibrium angle [radians], shape `(n_species,)`.

        Returns:
            InversionParameters with full interaction matrices
        """
        nt = inversion_barrier.shape[0]

        # Broadcast to 4D: center is first index
        k_arr = inversion_barrier[:, None, None, None] * jnp.ones((nt, nt, nt, nt))

        if omega0 is None:
            omega0_arr = jnp.zeros((nt, nt, nt, nt))
        else:
            omega0_arr = omega0[:, None, None, None] * jnp.ones((nt, nt, nt, nt))

        return cls(labels=tuple(map(Label, labels)), omega0=omega0_arr, k=k_arr)

from_uff(labels, inversion_barrier, omega0=None) classmethod

Create inversion parameters using UFF formulas.

Parameters:

Name Type Description Default
labels tuple[str, ...]

Species labels, shape (n_species,).

required
inversion_barrier Array

Barrier/force constant for each type, shape (n_species,).

required
omega0 Array | None

Equilibrium angle [radians], shape (n_species,).

None

Returns:

Type Description
InversionParameters

InversionParameters with full interaction matrices

Source code in src/kups/potential/classical/inversion.py
@classmethod
def from_uff(
    cls,
    labels: tuple[str, ...],
    inversion_barrier: Array,
    omega0: Array | None = None,
) -> "InversionParameters":
    r"""Create inversion parameters using UFF formulas.

    Args:
        labels: Species labels, shape `(n_species,)`.
        inversion_barrier: Barrier/force constant for each type, shape `(n_species,)`.
        omega0: Equilibrium angle [radians], shape `(n_species,)`.

    Returns:
        InversionParameters with full interaction matrices
    """
    nt = inversion_barrier.shape[0]

    # Broadcast to 4D: center is first index
    k_arr = inversion_barrier[:, None, None, None] * jnp.ones((nt, nt, nt, nt))

    if omega0 is None:
        omega0_arr = jnp.zeros((nt, nt, nt, nt))
    else:
        omega0_arr = omega0[:, None, None, None] * jnp.ones((nt, nt, nt, nt))

    return cls(labels=tuple(map(Label, labels)), omega0=omega0_arr, k=k_arr)

LennardJonesParameters

Lennard-Jones potential parameters.

Attributes:

Name Type Description
labels tuple[Label, ...]

Species labels as Index.

sigma Array

Length scale parameters [Å], shape (n_species, n_species).

epsilon Array

Energy well depths [energy units], shape (n_species, n_species).

cutoff Table[SystemId, Array]

Cutoff radius [Å], shape (n_systems,).

Source code in src/kups/potential/classical/lennard_jones.py
@dataclass
class LennardJonesParameters:
    """Lennard-Jones potential parameters.

    Attributes:
        labels: Species labels as ``Index``.
        sigma: Length scale parameters [Å], shape ``(n_species, n_species)``.
        epsilon: Energy well depths [energy units], shape ``(n_species, n_species)``.
        cutoff: Cutoff radius [Å], shape ``(n_systems,)``.
    """

    labels: tuple[Label, ...] = field(static=True)  # (n_species,)
    sigma: Array  # (n_species, n_species) float
    epsilon: Array  # (n_species, n_species) float
    cutoff: Table[SystemId, Array]  # (n_graphs,) float

    @classmethod
    def from_dict(
        cls,
        cutoff: float | Array,
        parameters: dict[str, tuple[float | None, float | None]],
        mixing_rule: MixingRule,
    ) -> LennardJonesParameters:
        """Create parameters from a dict of per-species values.

        Args:
            cutoff: Cutoff radius [Angstrom].
            parameters: Map from species label to ``(sigma, epsilon)`` pair.
                ``None`` values default to ``sigma=1.0``, ``epsilon=0.0``.
            mixing_rule: Combining rule for cross-species interactions.
        """
        labels = tuple(parameters.keys())
        raw = [(s or 1.0, e or 0.0) for s, e in parameters.values()]
        sigma, epsilon = jnp.asarray(raw).T
        cutoff_indexed = Table((SystemId(0),), jnp.array([cutoff]))
        match mixing_rule:
            case "lorentz_berthelot":
                return cls.from_lorentz_berthelot_mixing(
                    labels, sigma, epsilon, cutoff_indexed
                )
            case _ as unreachable:
                assert_never(unreachable)

    @classmethod
    def from_lorentz_berthelot_mixing(
        cls,
        labels: tuple[str, ...],
        sigma: Array,
        epsilon: Array,
        cutoff: Table[SystemId, Array],
    ) -> LennardJonesParameters:
        """Create parameters using Lorentz-Berthelot mixing rules.

        - σᵢⱼ = (σᵢ + σⱼ) / 2 (arithmetic mean)
        - εᵢⱼ = √(εᵢ × εⱼ) (geometric mean)
        """
        assert sigma.ndim == epsilon.ndim == 1
        sigma_matrix = (sigma[:, None] + sigma) / 2
        epsilon_matrix = jnp.sqrt(epsilon[:, None] * epsilon)
        return cls(tuple(map(Label, labels)), sigma_matrix, epsilon_matrix, cutoff)

from_dict(cutoff, parameters, mixing_rule) classmethod

Create parameters from a dict of per-species values.

Parameters:

Name Type Description Default
cutoff float | Array

Cutoff radius [Angstrom].

required
parameters dict[str, tuple[float | None, float | None]]

Map from species label to (sigma, epsilon) pair. None values default to sigma=1.0, epsilon=0.0.

required
mixing_rule MixingRule

Combining rule for cross-species interactions.

required
Source code in src/kups/potential/classical/lennard_jones.py
@classmethod
def from_dict(
    cls,
    cutoff: float | Array,
    parameters: dict[str, tuple[float | None, float | None]],
    mixing_rule: MixingRule,
) -> LennardJonesParameters:
    """Create parameters from a dict of per-species values.

    Args:
        cutoff: Cutoff radius [Angstrom].
        parameters: Map from species label to ``(sigma, epsilon)`` pair.
            ``None`` values default to ``sigma=1.0``, ``epsilon=0.0``.
        mixing_rule: Combining rule for cross-species interactions.
    """
    labels = tuple(parameters.keys())
    raw = [(s or 1.0, e or 0.0) for s, e in parameters.values()]
    sigma, epsilon = jnp.asarray(raw).T
    cutoff_indexed = Table((SystemId(0),), jnp.array([cutoff]))
    match mixing_rule:
        case "lorentz_berthelot":
            return cls.from_lorentz_berthelot_mixing(
                labels, sigma, epsilon, cutoff_indexed
            )
        case _ as unreachable:
            assert_never(unreachable)

from_lorentz_berthelot_mixing(labels, sigma, epsilon, cutoff) classmethod

Create parameters using Lorentz-Berthelot mixing rules.

  • σᵢⱼ = (σᵢ + σⱼ) / 2 (arithmetic mean)
  • εᵢⱼ = √(εᵢ × εⱼ) (geometric mean)
Source code in src/kups/potential/classical/lennard_jones.py
@classmethod
def from_lorentz_berthelot_mixing(
    cls,
    labels: tuple[str, ...],
    sigma: Array,
    epsilon: Array,
    cutoff: Table[SystemId, Array],
) -> LennardJonesParameters:
    """Create parameters using Lorentz-Berthelot mixing rules.

    - σᵢⱼ = (σᵢ + σⱼ) / 2 (arithmetic mean)
    - εᵢⱼ = √(εᵢ × εⱼ) (geometric mean)
    """
    assert sigma.ndim == epsilon.ndim == 1
    sigma_matrix = (sigma[:, None] + sigma) / 2
    epsilon_matrix = jnp.sqrt(epsilon[:, None] * epsilon)
    return cls(tuple(map(Label, labels)), sigma_matrix, epsilon_matrix, cutoff)

MorseBondParameters

Morse bond potential parameters.

Attributes:

Name Type Description
labels tuple[Label, ...]

Species labels, shape (n_species,).

r0 Array

Equilibrium bond lengths [Å], shape (n_species, n_species).

D Array

Bond dissociation energy (well depth), shape (n_species, n_species).

alpha Array

Width parameter [Å⁻¹], shape (n_species, n_species).

Source code in src/kups/potential/classical/morse.py
@dataclass
class MorseBondParameters:
    r"""Morse bond potential parameters.

    Attributes:
        labels: Species labels, shape `(n_species,)`.
        r0: Equilibrium bond lengths [Å], shape `(n_species, n_species)`.
        D: Bond dissociation energy (well depth), shape `(n_species, n_species)`.
        alpha: Width parameter [Å⁻¹], shape `(n_species, n_species)`.
    """

    labels: tuple[Label, ...] = field(static=True)  # (n_species,)
    r0: Array  # (n_species, n_species)
    D: Array  # (n_species, n_species)
    alpha: Array  # (n_species, n_species)

    @classmethod
    def from_harmonic(
        cls, labels: tuple[str, ...], r0: Array, k: Array, D: Array
    ) -> "MorseBondParameters":
        r"""Create Morse parameters from harmonic force constant.

        Args:
            labels: Species labels, shape `(n_species,)`
            r0: Equilibrium bond lengths [Å], shape `(n_species, n_species)`
            k: Harmonic force constants [energy/Ų], shape `(n_species, n_species)`
            D: Bond dissociation energies, shape `(n_species, n_species)`

        Returns:
            MorseBondParameters with computed alpha values
        """
        alpha = jnp.sqrt(k / D)
        return cls(labels=tuple(map(Label, labels)), r0=r0, D=D, alpha=alpha)

    @classmethod
    def from_uff(
        cls,
        labels: tuple[str, ...],
        bond_radius: Array,
        electronegativity: Array,
        effective_charge: Array,
        dissociation_energy: Array,
    ) -> "MorseBondParameters":
        r"""Create Morse parameters using UFF bond length/force constant formulas.

        Args:
            labels: Species labels, shape `(n_species,)`
            bond_radius: Valence bond radii [Å], shape `(n_species,)`
            electronegativity: GMP electronegativity, shape `(n_species,)`
            effective_charge: Effective atomic charge Z*, shape `(n_species,)`
            dissociation_energy: Bond dissociation energy D, shape `(n_species, n_species)`

        Returns:
            MorseBondParameters with full interaction matrices
        """
        r0 = compute_uff_bond_length(bond_radius, electronegativity)
        Z_i, Z_j = effective_charge[:, None], effective_charge[None, :]

        # Force constant (Eq. 6): k = 664.12 * Z_i * Z_j / r_ij^3
        k = 664.12 * Z_i * Z_j / (r0**3)
        alpha = jnp.sqrt(k / (2.0 * dissociation_energy))

        return cls(
            labels=tuple(map(Label, labels)), r0=r0, D=dissociation_energy, alpha=alpha
        )

from_harmonic(labels, r0, k, D) classmethod

Create Morse parameters from harmonic force constant.

Parameters:

Name Type Description Default
labels tuple[str, ...]

Species labels, shape (n_species,)

required
r0 Array

Equilibrium bond lengths [Å], shape (n_species, n_species)

required
k Array

Harmonic force constants [energy/Ų], shape (n_species, n_species)

required
D Array

Bond dissociation energies, shape (n_species, n_species)

required

Returns:

Type Description
MorseBondParameters

MorseBondParameters with computed alpha values

Source code in src/kups/potential/classical/morse.py
@classmethod
def from_harmonic(
    cls, labels: tuple[str, ...], r0: Array, k: Array, D: Array
) -> "MorseBondParameters":
    r"""Create Morse parameters from harmonic force constant.

    Args:
        labels: Species labels, shape `(n_species,)`
        r0: Equilibrium bond lengths [Å], shape `(n_species, n_species)`
        k: Harmonic force constants [energy/Ų], shape `(n_species, n_species)`
        D: Bond dissociation energies, shape `(n_species, n_species)`

    Returns:
        MorseBondParameters with computed alpha values
    """
    alpha = jnp.sqrt(k / D)
    return cls(labels=tuple(map(Label, labels)), r0=r0, D=D, alpha=alpha)

from_uff(labels, bond_radius, electronegativity, effective_charge, dissociation_energy) classmethod

Create Morse parameters using UFF bond length/force constant formulas.

Parameters:

Name Type Description Default
labels tuple[str, ...]

Species labels, shape (n_species,)

required
bond_radius Array

Valence bond radii [Å], shape (n_species,)

required
electronegativity Array

GMP electronegativity, shape (n_species,)

required
effective_charge Array

Effective atomic charge Z*, shape (n_species,)

required
dissociation_energy Array

Bond dissociation energy D, shape (n_species, n_species)

required

Returns:

Type Description
MorseBondParameters

MorseBondParameters with full interaction matrices

Source code in src/kups/potential/classical/morse.py
@classmethod
def from_uff(
    cls,
    labels: tuple[str, ...],
    bond_radius: Array,
    electronegativity: Array,
    effective_charge: Array,
    dissociation_energy: Array,
) -> "MorseBondParameters":
    r"""Create Morse parameters using UFF bond length/force constant formulas.

    Args:
        labels: Species labels, shape `(n_species,)`
        bond_radius: Valence bond radii [Å], shape `(n_species,)`
        electronegativity: GMP electronegativity, shape `(n_species,)`
        effective_charge: Effective atomic charge Z*, shape `(n_species,)`
        dissociation_energy: Bond dissociation energy D, shape `(n_species, n_species)`

    Returns:
        MorseBondParameters with full interaction matrices
    """
    r0 = compute_uff_bond_length(bond_radius, electronegativity)
    Z_i, Z_j = effective_charge[:, None], effective_charge[None, :]

    # Force constant (Eq. 6): k = 664.12 * Z_i * Z_j / r_ij^3
    k = 664.12 * Z_i * Z_j / (r0**3)
    alpha = jnp.sqrt(k / (2.0 * dissociation_energy))

    return cls(
        labels=tuple(map(Label, labels)), r0=r0, D=dissociation_energy, alpha=alpha
    )

PairTailCorrectedLennardJonesParameters

Bases: LennardJonesParameters

Lennard-Jones parameters with smooth pairwise tail correction.

Attributes:

Name Type Description
truncation_radius Table[SystemId, Array]

Radius where smoothing begins [Å], shape (n_systems,).

Source code in src/kups/potential/classical/lennard_jones.py
@dataclass
class PairTailCorrectedLennardJonesParameters(LennardJonesParameters):
    """Lennard-Jones parameters with smooth pairwise tail correction.

    Attributes:
        truncation_radius: Radius where smoothing begins [Å], shape ``(n_systems,)``.
    """

    truncation_radius: Table[SystemId, Array]  # (n_graphs,)

cosine_angle_energy(inp)

Compute UFF-style cosine angle energy for all angles.

Calculates energy using the general cosine form:

\[U(\theta) = K [C_0 + C_1 \cos(\theta) + C_2 \cos(2\theta)]\]

For near-linear angles (\(\theta_0\) close to 180deg), uses:

\[U(\theta) = K (1 + \cos\theta)\]

Parameters:

Name Type Description Default
inp CosineAngleInput

Graph potential input with cosine angle parameters

required

Returns:

Type Description
WithPatch[Table[SystemId, Energy], IdPatch]

Total angle energy per system

Source code in src/kups/potential/classical/cosine_angle.py
def cosine_angle_energy(
    inp: CosineAngleInput,
) -> WithPatch[Table[SystemId, Energy], IdPatch]:
    r"""Compute UFF-style cosine angle energy for all angles.

    Calculates energy using the general cosine form:

    $$U(\theta) = K [C_0 + C_1 \cos(\theta) + C_2 \cos(2\theta)]$$

    For near-linear angles ($\theta_0$ close to 180deg), uses:

    $$U(\theta) = K (1 + \cos\theta)$$

    Args:
        inp: Graph potential input with cosine angle parameters

    Returns:
        Total angle energy per system
    """
    graph = inp.graph
    assert graph.edges.indices.indices.shape[1] == 3, (
        "Cosine angle potential only supports triplet interactions (order=3)."
    )
    edg = graph.particles[graph.edges.indices]
    edg_species = edg.labels.indices_in(inp.parameters.labels)
    s0, s1, s2 = edg_species[:, 0], edg_species[:, 1], edg_species[:, 2]
    theta0 = inp.parameters.theta0[s0, s1, s2]
    k = inp.parameters.k[s0, s1, s2]

    # Compute current angle
    v1, v2 = graph.edge_shifts[:, 0], graph.edge_shifts[:, 1]
    cos_theta = jnp.einsum("ij,ij->i", v1, v2) / (
        jnp.linalg.norm(v1, axis=-1) * jnp.linalg.norm(v2, axis=-1)
    )
    cos_theta = jnp.clip(cos_theta, -1.0, 1.0)

    # Check for near-linear case (theta0 close to pi)
    is_linear = jnp.abs(theta0 - jnp.pi) < inp.parameters.linear_tol

    # Linear case: U = K*(1 + cos(theta))
    energy_linear = k * (1.0 + cos_theta)

    # General case: U = K * [C0 + C1*cos(theta) + C2*cos(2*theta)]
    # Using cos(2t) = 2cos^2(t) - 1
    c0, c1, c2 = _compute_cosine_coefficients(theta0)
    cos_2theta = 2.0 * cos_theta**2 - 1.0
    energy_general = k * (c0 + c1 * cos_theta + c2 * cos_2theta)

    edge_energy = jnp.where(is_linear, energy_linear, energy_general)

    total_energies = graph.edge_batch_mask.sum_over(edge_energy)
    return WithPatch(total_energies, IdPatch())

inversion_energy(inp)

Compute UFF-style inversion energy for all inversion centers.

Parameters:

Name Type Description Default
inp InversionInput

Graph potential input with inversion parameters

required

Returns:

Type Description
WithPatch[Table[SystemId, Energy], IdPatch]

Total inversion energy per system

Source code in src/kups/potential/classical/inversion.py
def inversion_energy(
    inp: InversionInput,
) -> WithPatch[Table[SystemId, Energy], IdPatch]:
    r"""Compute UFF-style inversion energy for all inversion centers.

    Args:
        inp: Graph potential input with inversion parameters

    Returns:
        Total inversion energy per system
    """
    graph = inp.graph
    assert graph.edges.indices.indices.shape[1] == 4, (
        "Inversion potential requires 4-body interactions (order=4)."
    )
    edg = graph.particles[graph.edges.indices]
    edg_species = edg.labels.indices_in(inp.parameters.labels)
    # Edge indices: [:, 0] = center j, [:, 1] = i, [:, 2] = k, [:, 3] = l
    s_j, s_i, s_k, s_l = (
        edg_species[:, 0],
        edg_species[:, 1],
        edg_species[:, 2],
        edg_species[:, 3],
    )

    omega0 = inp.parameters.omega0[s_j, s_i, s_k, s_l]
    k = inp.parameters.k[s_j, s_i, s_k, s_l]

    # Get displacement vectors from edge_shifts
    r_ji = graph.edge_shifts[:, 0]  # (n_edges, 3)
    r_jk = graph.edge_shifts[:, 1]  # (n_edges, 3)
    r_jl = graph.edge_shifts[:, 2]  # (n_edges, 3)

    # Normal to the plane k-j-l
    n = jnp.cross(r_jk, r_jl)  # (n_edges, 3)
    n_norm = jnp.linalg.norm(n, axis=-1, keepdims=True)
    eps = 1e-10
    n_unit = n / (n_norm + eps)

    # Out-of-plane angle: sin(omega) = (r_ji . n) / |r_ji|
    r_ji_norm = jnp.linalg.norm(r_ji, axis=-1)
    sin_omega = jnp.einsum("ij,ij->i", r_ji, n_unit) / (r_ji_norm + eps)
    sin_omega = jnp.clip(sin_omega, -1.0, 1.0)
    omega = jnp.arcsin(sin_omega)  # radians

    # Compute UFF coefficients
    sin2_omega0 = jnp.sin(omega0) ** 2
    cos_omega0 = jnp.cos(omega0)

    # Handle sp2 case (omega0 ~ 0) separately to avoid division by zero
    is_sp2 = jnp.abs(omega0) < 1e-6
    c2 = jnp.where(
        is_sp2, 0.0, 1.0 / (4.0 * jnp.where(sin2_omega0 > 1e-10, sin2_omega0, 1.0))
    )
    c1 = jnp.where(is_sp2, -1.0, -4.0 * c2 * cos_omega0)
    c0 = jnp.where(is_sp2, 1.0, c2 * (2.0 * cos_omega0**2 + 1.0))

    # Energy: U = K * [C0 + C1*cos(omega) + C2*cos(2*omega)]
    cos_omega = jnp.cos(omega)
    cos_2omega = jnp.cos(2.0 * omega)

    # For non-sp2, k is the barrier E_barrier at omega=0
    barrier_factor = c0 + c1 + c2
    k_scaled = jnp.where(
        is_sp2,
        k,
        k / jnp.maximum(barrier_factor, 1e-10),
    )
    edge_energy = k_scaled * (c0 + c1 * cos_omega + c2 * cos_2omega)

    total_energies = graph.edge_batch_mask.sum_over(edge_energy)
    return WithPatch(total_energies, IdPatch())

make_cosine_angle_potential(particles_view, edges_view, systems_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create UFF-style cosine angle potential for explicitly defined angles.

Parameters:

Name Type Description Default
particles_view View[State, Table[ParticleId, IsBondedParticles]]

Extracts particle data (positions, species) with system index

required
edges_view View[State, Edges[Literal[3]]]

Extracts angle connectivity (triplets)

required
systems_view View[State, Table[SystemId, HasUnitCell]]

Extracts indexed system data (unit cell)

required
parameter_view View[State, CosineAngleParameters] required
probe Probe[State, Ptch, IsEdgeSetGraphProbe[IsBondedParticles, Literal[3]]] | None

Probes particle, edge, and capacity changes for incremental updates

required
gradient_lens Lens[CosineAngleInput, Gradients]

Specifies gradients to compute

required
hessian_lens Lens[Gradients, Hessians]

Specifies Hessians to compute

required
hessian_idx_view View[State, Hessians]

Hessian index structure

required
patch_idx_view View[State, PotentialOut[Gradients, Hessians]] | None

Cached output index structure

None
out_cache_lens Lens[State, PotentialOut[Gradients, Hessians]] | None

Cache location lens

None

Returns:

Type Description
Potential[State, Gradients, Hessians, Ptch]

Cosine angle Potential

Source code in src/kups/potential/classical/cosine_angle.py
def make_cosine_angle_potential[
    State,
    Ptch: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsBondedParticles]],
    edges_view: View[State, Edges[Literal[3]]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    parameter_view: View[State, CosineAngleParameters],
    probe: Probe[State, Ptch, IsEdgeSetGraphProbe[IsBondedParticles, Literal[3]]]
    | None,
    gradient_lens: Lens[CosineAngleInput, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Create UFF-style cosine angle potential for explicitly defined angles.

    Args:
        particles_view: Extracts particle data (positions, species) with system index
        edges_view: Extracts angle connectivity (triplets)
        systems_view: Extracts indexed system data (unit cell)
        parameter_view: Extracts [CosineAngleParameters][kups.potential.classical.cosine_angle.CosineAngleParameters]
        probe: Probes particle, edge, and capacity changes for incremental updates
        gradient_lens: Specifies gradients to compute
        hessian_lens: Specifies Hessians to compute
        hessian_idx_view: Hessian index structure
        patch_idx_view: Cached output index structure
        out_cache_lens: Cache location lens

    Returns:
        Cosine angle [Potential][kups.core.potential.Potential]
    """
    graph_fn = EdgeSetGraphConstructor(
        particles=particles_view, edges=edges_view, systems=systems_view, probe=probe
    )
    composer = LocalGraphSumComposer(
        graph_constructor=graph_fn,
        parameter_view=parameter_view,
    )
    potential = PotentialFromEnergy(
        composer=composer,
        energy_fn=cosine_angle_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )
    return potential

make_dihedral_potential(particles_view, edges_view, systems_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create UFF dihedral potential for explicitly defined dihedrals.

Parameters:

Name Type Description Default
particles_view View[State, Table[ParticleId, IsBondedParticles]]

Extracts particle data (positions, species) with system index

required
edges_view View[State, Edges[Literal[4]]]

Extracts dihedral connectivity (quadruplets)

required
systems_view View[State, Table[SystemId, HasUnitCell]]

Extracts indexed system data (unit cell)

required
parameter_view View[State, DihedralParameters] required
probe Probe[State, Ptch, IsEdgeSetGraphProbe[IsBondedParticles, Literal[4]]] | None

Grouped probe for incremental updates (particles, edges, capacity)

required
gradient_lens Lens[DihedralInput, Gradients]

Specifies gradients to compute

required
hessian_lens Lens[Gradients, Hessians]

Specifies Hessians to compute

required
hessian_idx_view View[State, Hessians]

Hessian index structure

required
patch_idx_view View[State, PotentialOut[Gradients, Hessians]] | None

Cached output index structure

None
out_cache_lens Lens[State, PotentialOut[Gradients, Hessians]] | None

Cache location lens

None

Returns:

Type Description
Potential[State, Gradients, Hessians, Ptch]

UFF dihedral Potential

Source code in src/kups/potential/classical/dihedral.py
def make_dihedral_potential[
    State,
    Ptch: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsBondedParticles]],
    edges_view: View[State, Edges[Literal[4]]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    parameter_view: View[State, DihedralParameters],
    probe: Probe[State, Ptch, IsEdgeSetGraphProbe[IsBondedParticles, Literal[4]]]
    | None,
    gradient_lens: Lens[DihedralInput, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Create UFF dihedral potential for explicitly defined dihedrals.

    Args:
        particles_view: Extracts particle data (positions, species) with system index
        edges_view: Extracts dihedral connectivity (quadruplets)
        systems_view: Extracts indexed system data (unit cell)
        parameter_view: Extracts [DihedralParameters][kups.potential.classical.dihedral.DihedralParameters]
        probe: Grouped probe for incremental updates (particles, edges, capacity)
        gradient_lens: Specifies gradients to compute
        hessian_lens: Specifies Hessians to compute
        hessian_idx_view: Hessian index structure
        patch_idx_view: Cached output index structure
        out_cache_lens: Cache location lens

    Returns:
        UFF dihedral [Potential][kups.core.potential.Potential]
    """
    graph_fn = EdgeSetGraphConstructor(
        particles=particles_view,
        edges=edges_view,
        systems=systems_view,
        probe=probe,
    )
    composer = LocalGraphSumComposer(
        graph_constructor=graph_fn,
        parameter_view=parameter_view,
    )
    potential = PotentialFromEnergy(
        composer=composer,
        energy_fn=dihedral_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )
    return potential

make_ewald_potential(particles_view, systems_view, neighborlist_view, parameter_lens, cache_lens, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, include_exclusion_mask=False)

Create the complete Ewald potential combining all component terms.

Implements the Ewald decomposition: E_total = E_sr + E_lr - E_self - E_excl where each term is computed independently and cached for incremental MC updates. Short-range and exclusion use radius graphs (real-space pairs), long-range uses point clouds (reciprocal space), and self-interaction is per-particle.

Internally converts _ParticleData adding inclusion and exclusion fields for the neighbor list:

  • sr/lr/self: inclusion=system (all particles in same system interact), exclusion=particle_id (self-exclusion).
  • exclusion correction: inclusion=group (only same-molecule pairs), exclusion=particle_id.

Parameters:

Name Type Description Default
particles_view View[State, Table[ParticleId, IsEwaldPointData]]

Indexed particle data (positions, charges, system index).

required
systems_view View[State, Table[SystemId, HasUnitCell]]

Indexed system data (unit cell).

required
neighborlist_view View[State, NearestNeighborList]

Full neighbor list.

required
parameter_lens Lens[State, EwaldParameters]

Lens to EwaldParameters.

required
cache_lens Lens[State, EwaldCache] | None

Lens to EwaldCache, or None.

required
probe Probe[State, Ptch, IsRadiusGraphProbe[IsEwaldPointData]] | None

Probe for incremental updates, or None.

required
gradient_lens Lens[PointCloud[IsEwaldPointData, HasUnitCell], Gradients]

Specifies gradients to compute.

required
hessian_lens Lens[Gradients, Hessians]

Specifies Hessians to compute.

required
hessian_idx_view View[State, Hessians]

Hessian index structure.

required
patch_idx_view View[State, PotentialOut[Gradients, Hessians]] | None

Cached output index structure (optional).

None
include_exclusion_mask bool

Whether to include the exclusion correction.

False

Returns:

Type Description
EwaldPotential[State, Gradients, Hessians, Ptch]

Complete Ewald potential (sum of three or four components).

Source code in src/kups/potential/classical/ewald.py
def make_ewald_potential[
    State,
    Ptch: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsEwaldPointData]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    neighborlist_view: View[State, NearestNeighborList],
    parameter_lens: Lens[State, EwaldParameters],
    cache_lens: Lens[State, EwaldCache] | None,
    probe: Probe[State, Ptch, IsRadiusGraphProbe[IsEwaldPointData]] | None,
    gradient_lens: Lens[PointCloud[IsEwaldPointData, HasUnitCell], Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    include_exclusion_mask: bool = False,
) -> EwaldPotential[State, Gradients, Hessians, Ptch]:
    """Create the complete Ewald potential combining all component terms.

    Implements the Ewald decomposition:
    ``E_total = E_sr + E_lr - E_self - E_excl``
    where each term is computed independently and cached for incremental
    MC updates. Short-range and exclusion use radius graphs (real-space
    pairs), long-range uses point clouds (reciprocal space), and
    self-interaction is per-particle.

    Internally converts ``_ParticleData`` adding ``inclusion`` and
    ``exclusion`` fields for the neighbor list:

    - sr/lr/self: ``inclusion=system`` (all particles in same system
      interact), ``exclusion=particle_id`` (self-exclusion).
    - exclusion correction: ``inclusion=group`` (only same-molecule
      pairs), ``exclusion=particle_id``.

    Args:
        particles_view: Indexed particle data (positions, charges, system index).
        systems_view: Indexed system data (unit cell).
        neighborlist_view: Full neighbor list.
        parameter_lens: Lens to EwaldParameters.
        cache_lens: Lens to EwaldCache, or ``None``.
        probe: Probe for incremental updates, or ``None``.
        gradient_lens: Specifies gradients to compute.
        hessian_lens: Specifies Hessians to compute.
        hessian_idx_view: Hessian index structure.
        patch_idx_view: Cached output index structure (optional).
        include_exclusion_mask: Whether to include the exclusion correction.

    Returns:
        Complete Ewald potential (sum of three or four components).
    """
    # The definition of the ewald potential is only correct when computing the total
    # energy. One cannot directly exclude energy terms, thus, we compute the total coulomb
    # energy and subtract the excluded interactions later.

    @dataclass
    class _ParticleData:
        positions: Array
        charges: Array
        system: Index[SystemId]
        inclusion: Index[InclusionId]
        exclusion: Index[ExclusionId]

    def _convert_particles(
        indexed: Table[ParticleId, IsEwaldPointData],
        inclusion_fn: Callable[[IsEwaldPointData], Index[Any]],
    ) -> Table[ParticleId, _ParticleData]:
        """Convert particles, deriving inclusion from `inclusion_fn`."""
        p = indexed.data
        particle_ids = tuple(indexed.keys)
        excl = Index(particle_ids, jnp.arange(len(particle_ids)), _cls=indexed.cls)
        return Table(
            indexed.keys,
            _ParticleData(
                p.positions,
                p.charges,
                p.system,
                inclusion=inclusion_fn(p).to_cls(InclusionId),
                exclusion=excl.to_cls(ExclusionId),
            ),
        )

    def _make_probe(
        inclusion_fn: Callable[[IsEwaldPointData], Index[Any]],
        neighborlist_override: NearestNeighborList | None = None,
    ) -> Probe[State, Ptch, IsRadiusGraphProbe[_ParticleData]] | None:
        """Wrap `probe` to convert particle data with `inclusion_fn`.

        Args:
            inclusion_fn: Extracts the inclusion Index from particle data.
            neighborlist_override: If set, replaces the probe's neighbor lists
                (e.g., ``AllConnectedNeighborList`` for exclusion correction).
        """
        if probe is None:
            return None
        _p = probe

        @dataclass
        class _ProbeResult:
            particles: WithIndices[ParticleId, _ParticleData]
            neighborlist_after: NearestNeighborList
            neighborlist_before: NearestNeighborList

        def _wrapper(state: State, patch: Ptch) -> _ProbeResult:
            result = _p(state, patch)
            p = result.particles
            d = p.data
            excl = Index(p.indices.keys, p.indices.indices, _cls=p.indices.cls)
            data = _ParticleData(
                d.positions,
                d.charges,
                d.system,
                inclusion=inclusion_fn(d).to_cls(InclusionId),
                exclusion=excl.to_cls(ExclusionId),
            )
            nn_after = neighborlist_override or result.neighborlist_after
            nn_before = neighborlist_override or result.neighborlist_before
            return _ProbeResult(WithIndices(p.indices, data), nn_after, nn_before)

        return _wrapper

    def _make_particles_probe(
        inclusion_fn: Callable[[IsEwaldPointData], Index[InclusionId]],
    ) -> Probe[State, Ptch, WithIndices[ParticleId, _ParticleData]] | None:
        """Wrap `probe` returning only WithIndices (no neighborlists)."""
        full = _make_probe(inclusion_fn)
        if full is None:
            return None

        def _wrapper(
            state: State, patch: Ptch
        ) -> WithIndices[ParticleId, _ParticleData]:
            return full(state, patch).particles

        return _wrapper

    # Atomic view: inclusion = system
    def _system_inclusion(d: IsEwaldPointData) -> Index[InclusionId]:
        return d.system.to_cls(InclusionId)

    atomic_view = pipe(
        particles_view, lambda p: _convert_particles(p, _system_inclusion)
    )
    atomic_probe = _make_probe(_system_inclusion)
    atomic_particles_probe = _make_particles_probe(_system_inclusion)

    sr_potential = make_ewald_short_range_potential(
        particles_view=atomic_view,
        systems_view=systems_view,
        neighborlist_view=neighborlist_view,
        parameter_view=parameter_lens,
        probe=atomic_probe,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
        cache_lens=cache_lens.focus(lambda x: x.short_range) if cache_lens else None,
    )
    lr_potential = make_ewald_long_range_potential(
        particles_view=atomic_view,
        systems_view=systems_view,
        parameter_lens=parameter_lens,
        cache_lens=cache_lens,
        probe=atomic_particles_probe,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
    )
    self_potential = make_ewald_self_interaction_potential(
        particles_view=atomic_view,
        systems_view=systems_view,
        parameter_view=parameter_lens,
        probe=atomic_particles_probe,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
        cache_lens=cache_lens.focus(lambda x: x.self_interaction)
        if cache_lens
        else None,
    )

    # Exclusion view: inclusion = exclusion group
    def _excl_inclusion(d: IsEwaldPointData) -> Index[InclusionId]:
        return d.exclusion.to_cls(InclusionId)

    excl_view = pipe(particles_view, lambda p: _convert_particles(p, _excl_inclusion))
    excl_probe = _make_probe(_excl_inclusion, all_connected_neighborlist)

    excl_rg = RadiusGraphConstructor(
        particles=excl_view,
        systems=systems_view,
        cutoffs=pipe(parameter_lens, lambda p: p.cutoff),
        neighborlist=lambda _: all_connected_neighborlist,
        probe=excl_probe,
    )
    exclusion_correction = PotentialFromEnergy(
        energy_fn=exclusion_correction_energy,
        composer=LocalGraphSumComposer(excl_rg, parameter_lens),
        gradient_lens=NestedLens(
            SimpleLens[GraphPotentialInput, PointCloud](lambda state: state.graph),
            gradient_lens,
        ),
        hessian_lens=hessian_lens,
        cache_lens=cache_lens.focus(lambda x: x.exclusion) if cache_lens else None,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
    )
    if include_exclusion_mask:
        return EwaldPotential(
            (sr_potential, lr_potential, self_potential, exclusion_correction)
        )
    return EwaldPotential((sr_potential, lr_potential, self_potential))

make_global_lennard_jones_tail_correction_potential(particles_view, systems_view, parameter_view, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create analytical long-range tail correction for Lennard-Jones potential.

Source code in src/kups/potential/classical/lennard_jones.py
def make_global_lennard_jones_tail_correction_potential[State, Gradients, Hessians](
    particles_view: View[State, Table[ParticleId, IsLJGraphParticles]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    parameter_view: View[State, GlobalTailCorrectedLennardJonesParameters],
    gradient_lens: Lens[GCLJInp, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, Patch[State]]:
    """Create analytical long-range tail correction for Lennard-Jones potential."""
    return PotentialFromEnergy(
        energy_fn=global_lennard_jones_tail_correction_energy,
        composer=FullGraphSumComposer(
            PointCloudConstructor(
                particles=particles_view,
                systems=systems_view,
                probe_particles=None,
            ),
            parameter_view=parameter_view,
        ),
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )

make_harmonic_angle_potential(particles_view, edges_view, systems_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create harmonic angle potential for explicitly defined angles.

Applies harmonic restraints to specified atom triplets (angles). Angles must be explicitly provided via the input_view edge set as triplets (i-j-k).

Parameters:

Name Type Description Default
particles_view View[State, Table[ParticleId, IsBondedParticles]]

Extracts particle data (positions, species) with system index

required
edges_view View[State, Edges[Literal[3]]]

Extracts angle connectivity (triplets)

required
systems_view View[State, Table[SystemId, HasUnitCell]]

Extracts indexed system data (unit cell)

required
parameter_view View[State, HarmonicAngleParameters] required
probe Probe[State, P, IsEdgeSetGraphProbe[IsBondedParticles, Literal[3]]] | None

Grouped probe for incremental updates (particles, edges, capacity)

required
gradient_lens Lens[HarmonicAngleInput, Gradients]

Specifies gradients to compute

required
hessian_lens Lens[Gradients, Hessians]

Specifies Hessians to compute

required
hessian_idx_view View[State, Hessians]

Hessian index structure

required
patch_idx_view View[State, PotentialOut[Gradients, Hessians]] | None

Cached output index structure

None
out_cache_lens Lens[State, PotentialOut[Gradients, Hessians]] | None

Cache location lens

None

Returns:

Type Description
Potential[State, Gradients, Hessians, P]

Harmonic angle Potential

Source code in src/kups/potential/classical/harmonic.py
def make_harmonic_angle_potential[
    State,
    P: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsBondedParticles]],
    edges_view: View[State, Edges[Literal[3]]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    parameter_view: View[State, HarmonicAngleParameters],
    probe: Probe[State, P, IsEdgeSetGraphProbe[IsBondedParticles, Literal[3]]] | None,
    gradient_lens: Lens[HarmonicAngleInput, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, P]:
    """Create harmonic angle potential for explicitly defined angles.

    Applies harmonic restraints to specified atom triplets (angles). Angles must be
    explicitly provided via the input_view edge set as triplets (i-j-k).

    Args:
        particles_view: Extracts particle data (positions, species) with system index
        edges_view: Extracts angle connectivity (triplets)
        systems_view: Extracts indexed system data (unit cell)
        parameter_view: Extracts [HarmonicAngleParameters][kups.potential.classical.harmonic.HarmonicAngleParameters]
        probe: Grouped probe for incremental updates (particles, edges, capacity)
        gradient_lens: Specifies gradients to compute
        hessian_lens: Specifies Hessians to compute
        hessian_idx_view: Hessian index structure
        patch_idx_view: Cached output index structure
        out_cache_lens: Cache location lens

    Returns:
        Harmonic angle [Potential][kups.core.potential.Potential]
    """
    graph_fn = EdgeSetGraphConstructor(
        particles=particles_view,
        edges=edges_view,
        systems=systems_view,
        probe=probe,
    )
    composer = LocalGraphSumComposer(
        graph_constructor=graph_fn,
        parameter_view=parameter_view,
    )
    potential = PotentialFromEnergy(
        composer=composer,
        energy_fn=harmonic_angle_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )
    return potential

make_harmonic_bond_potential(particles_view, edges_view, systems_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create harmonic bond potential for explicitly defined bonds.

Applies harmonic restraints to specified atom pairs (bonds). Bonds must be explicitly provided via the input_view edge set.

Parameters:

Name Type Description Default
particles_view View[State, Table[ParticleId, IsBondedParticles]]

Extracts particle data (positions, species) with system index

required
edges_view View[State, Edges[Literal[2]]]

Extracts bond connectivity

required
systems_view View[State, Table[SystemId, HasUnitCell]]

Extracts indexed system data (unit cell)

required
parameter_view View[State, HarmonicBondParameters] required
probe Probe[State, P, IsEdgeSetGraphProbe[IsBondedParticles, Literal[2]]] | None

Grouped probe for incremental updates (particles, edges, capacity)

required
gradient_lens Lens[HarmonicBondInput, Gradients]

Specifies gradients to compute

required
hessian_lens Lens[Gradients, Hessians]

Specifies Hessians to compute

required
hessian_idx_view View[State, Hessians]

Hessian index structure

required
patch_idx_view View[State, PotentialOut[Gradients, Hessians]] | None

Cached output index structure

None
out_cache_lens Lens[State, PotentialOut[Gradients, Hessians]] | None

Cache location lens

None

Returns:

Type Description
Potential[State, Gradients, Hessians, P]

Harmonic bond Potential

Source code in src/kups/potential/classical/harmonic.py
def make_harmonic_bond_potential[
    State,
    P: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsBondedParticles]],
    edges_view: View[State, Edges[Literal[2]]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    parameter_view: View[State, HarmonicBondParameters],
    probe: Probe[State, P, IsEdgeSetGraphProbe[IsBondedParticles, Literal[2]]] | None,
    gradient_lens: Lens[HarmonicBondInput, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, P]:
    """Create harmonic bond potential for explicitly defined bonds.

    Applies harmonic restraints to specified atom pairs (bonds). Bonds must be
    explicitly provided via the input_view edge set.

    Args:
        particles_view: Extracts particle data (positions, species) with system index
        edges_view: Extracts bond connectivity
        systems_view: Extracts indexed system data (unit cell)
        parameter_view: Extracts [HarmonicBondParameters][kups.potential.classical.harmonic.HarmonicBondParameters]
        probe: Grouped probe for incremental updates (particles, edges, capacity)
        gradient_lens: Specifies gradients to compute
        hessian_lens: Specifies Hessians to compute
        hessian_idx_view: Hessian index structure
        patch_idx_view: Cached output index structure
        out_cache_lens: Cache location lens

    Returns:
        Harmonic bond [Potential][kups.core.potential.Potential]
    """
    graph_fn = EdgeSetGraphConstructor(
        particles=particles_view,
        edges=edges_view,
        systems=systems_view,
        probe=probe,
    )
    composer = LocalGraphSumComposer(
        graph_constructor=graph_fn,
        parameter_view=parameter_view,
    )
    potential = PotentialFromEnergy(
        composer=composer,
        energy_fn=harmonic_bond_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )
    return potential

make_inversion_potential(particles_view, edges_view, systems_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create UFF-style inversion potential for sp2/sp3 centers.

Parameters:

Name Type Description Default
particles_view View[State, Table[ParticleId, IsBondedParticles]]

Extracts particle data (positions, species) with system index

required
edges_view View[State, Edges[Literal[4]]]

Extracts inversion connectivity (4-tuples: center + 3 neighbors)

required
systems_view View[State, Table[SystemId, HasUnitCell]]

Extracts indexed system data (unit cell)

required
parameter_view View[State, InversionParameters] required
probe Probe[State, Ptch, IsEdgeSetGraphProbe[IsBondedParticles, Literal[4]]] | None

Grouped probe for incremental updates (particles, edges, capacity)

required
gradient_lens Lens[InversionInput, Gradients]

Specifies gradients to compute

required
hessian_lens Lens[Gradients, Hessians]

Specifies Hessians to compute

required
hessian_idx_view View[State, Hessians]

Hessian index structure

required
patch_idx_view View[State, PotentialOut[Gradients, Hessians]] | None

Cached output index structure

None
out_cache_lens Lens[State, PotentialOut[Gradients, Hessians]] | None

Cache location lens

None

Returns:

Type Description
Potential[State, Gradients, Hessians, Ptch]

Inversion Potential

Source code in src/kups/potential/classical/inversion.py
def make_inversion_potential[
    State,
    Ptch: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsBondedParticles]],
    edges_view: View[State, Edges[Literal[4]]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    parameter_view: View[State, InversionParameters],
    probe: Probe[State, Ptch, IsEdgeSetGraphProbe[IsBondedParticles, Literal[4]]]
    | None,
    gradient_lens: Lens[InversionInput, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Create UFF-style inversion potential for sp2/sp3 centers.

    Args:
        particles_view: Extracts particle data (positions, species) with system index
        edges_view: Extracts inversion connectivity (4-tuples: center + 3 neighbors)
        systems_view: Extracts indexed system data (unit cell)
        parameter_view: Extracts [InversionParameters][kups.potential.classical.inversion.InversionParameters]
        probe: Grouped probe for incremental updates (particles, edges, capacity)
        gradient_lens: Specifies gradients to compute
        hessian_lens: Specifies Hessians to compute
        hessian_idx_view: Hessian index structure
        patch_idx_view: Cached output index structure
        out_cache_lens: Cache location lens

    Returns:
        Inversion [Potential][kups.core.potential.Potential]
    """
    graph_fn = EdgeSetGraphConstructor(
        particles=particles_view,
        edges=edges_view,
        systems=systems_view,
        probe=probe,
    )
    composer = LocalGraphSumComposer(
        graph_constructor=graph_fn,
        parameter_view=parameter_view,
    )
    potential = PotentialFromEnergy(
        composer=composer,
        energy_fn=inversion_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )
    return potential

make_lennard_jones_potential(particles_view, systems_view, neighborlist_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create a standard Lennard-Jones potential with sharp cutoff.

Source code in src/kups/potential/classical/lennard_jones.py
def make_lennard_jones_potential[
    State,
    Ptch: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsLJGraphParticles]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    neighborlist_view: View[State, NearestNeighborList],
    parameter_view: View[State, LennardJonesParameters],
    probe: Probe[State, Ptch, IsRadiusGraphProbe[IsLJGraphParticles]] | None,
    gradient_lens: Lens[LJRadiusInp, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Create a standard Lennard-Jones potential with sharp cutoff."""
    graph_fn = RadiusGraphConstructor(
        particles=particles_view,
        systems=systems_view,
        cutoffs=pipe(parameter_view, lambda p: p.cutoff),
        neighborlist=neighborlist_view,
        probe=probe,
    )
    composer = LocalGraphSumComposer(
        graph_constructor=graph_fn,
        parameter_view=parameter_view,
    )
    return PotentialFromEnergy(
        composer=composer,
        energy_fn=lennard_jones_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )

make_morse_bond_potential(particles_view, edges_view, systems_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create Morse bond potential for explicitly defined bonds.

Parameters:

Name Type Description Default
particles_view View[State, Table[ParticleId, IsBondedParticles]]

Extracts particle data (positions, species) with system index

required
edges_view View[State, Edges[Literal[2]]]

Extracts bond connectivity

required
systems_view View[State, Table[SystemId, HasUnitCell]]

Extracts indexed system data (unit cell)

required
parameter_view View[State, MorseBondParameters] required
probe Probe[State, P, IsEdgeSetGraphProbe[IsBondedParticles, Literal[2]]] | None

Grouped probe for incremental updates (particles, edges, capacity)

required
gradient_lens Lens[MorseBondInput, Gradients]

Specifies gradients to compute

required
hessian_lens Lens[Gradients, Hessians]

Specifies Hessians to compute

required
hessian_idx_view View[State, Hessians]

Hessian index structure

required
patch_idx_view View[State, PotentialOut[Gradients, Hessians]] | None

Cached output index structure

None
out_cache_lens Lens[State, PotentialOut[Gradients, Hessians]] | None

Cache location lens

None

Returns:

Type Description
Potential[State, Gradients, Hessians, P]

Morse bond Potential

Source code in src/kups/potential/classical/morse.py
def make_morse_bond_potential[
    State,
    P: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsBondedParticles]],
    edges_view: View[State, Edges[Literal[2]]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    parameter_view: View[State, MorseBondParameters],
    probe: Probe[State, P, IsEdgeSetGraphProbe[IsBondedParticles, Literal[2]]] | None,
    gradient_lens: Lens[MorseBondInput, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, P]:
    """Create Morse bond potential for explicitly defined bonds.

    Args:
        particles_view: Extracts particle data (positions, species) with system index
        edges_view: Extracts bond connectivity
        systems_view: Extracts indexed system data (unit cell)
        parameter_view: Extracts [MorseBondParameters][kups.potential.classical.morse.MorseBondParameters]
        probe: Grouped probe for incremental updates (particles, edges, capacity)
        gradient_lens: Specifies gradients to compute
        hessian_lens: Specifies Hessians to compute
        hessian_idx_view: Hessian index structure
        patch_idx_view: Cached output index structure
        out_cache_lens: Cache location lens

    Returns:
        Morse bond [Potential][kups.core.potential.Potential]
    """
    graph_fn = EdgeSetGraphConstructor(
        particles=particles_view,
        edges=edges_view,
        systems=systems_view,
        probe=probe,
    )
    composer = LocalGraphSumComposer(
        graph_constructor=graph_fn,
        parameter_view=parameter_view,
    )
    potential = PotentialFromEnergy(
        composer=composer,
        energy_fn=morse_bond_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )
    return potential

make_pair_tail_corrected_lennard_jones_potential(particles_view, systems_view, neighborlist_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, out_cache_lens=None)

Create a Lennard-Jones potential with smooth pairwise tail correction.

Source code in src/kups/potential/classical/lennard_jones.py
def make_pair_tail_corrected_lennard_jones_potential[
    State,
    Ptch: Patch,
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsLJGraphParticles]],
    systems_view: View[State, Table[SystemId, HasUnitCell]],
    neighborlist_view: View[State, NearestNeighborList],
    parameter_view: View[State, PairTailCorrectedLennardJonesParameters],
    probe: Probe[State, Ptch, IsRadiusGraphProbe[IsLJGraphParticles]] | None,
    gradient_lens: Lens[PCLJInp, Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    out_cache_lens: Lens[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Create a Lennard-Jones potential with smooth pairwise tail correction."""
    radius_graph_fn = RadiusGraphConstructor(
        particles=particles_view,
        systems=systems_view,
        cutoffs=pipe(parameter_view, lambda p: p.cutoff),
        neighborlist=neighborlist_view,
        probe=probe,
    )
    composer = LocalGraphSumComposer(
        graph_constructor=radius_graph_fn,
        parameter_view=parameter_view,
    )
    return PotentialFromEnergy(
        composer=composer,
        energy_fn=pair_tail_corrected_lennard_jones_energy,
        gradient_lens=gradient_lens,
        hessian_lens=hessian_lens,
        hessian_idx_view=hessian_idx_view,
        cache_lens=out_cache_lens,
        patch_idx_view=patch_idx_view,
    )