Skip to content

kups.potential.classical.ewald

Ewald electrostatics with cached, incremental reciprocal-space updates.

parameters prepares cutoffs and lattice shifts, reciprocal implements the numerical kernels, and potential composes energies and cache updates.

ewald_short_range_energy = GraphPairEnergy(ewald_short_range_pair) module-attribute

Ewald short-range graph evaluator derived from the shared pair term.

EwaldCache

Compensated structure factors, prefactors and component outputs for updates.

Source code in src/kups/potential/classical/ewald/potential.py
@dataclass
class EwaldCache[Gradient, Hessian]:
    """Compensated structure factors, prefactors and component outputs for updates."""

    structure_factor: KahanSummand[Array]  # (n_groups, n_kvecs, 2)
    prefactor: Array  # (n_groups, n_kvecs)
    short_range: KahanSummand[PotentialOut[Gradient, Hessian]]
    long_range: KahanSummand[PotentialOut[Gradient, Hessian]]
    self_interaction: KahanSummand[PotentialOut[Gradient, Hessian]]
    net_charge: KahanSummand[PotentialOut[Gradient, Hessian]]
    exclusion: KahanSummand[PotentialOut[Gradient, Hessian]]

    @classmethod
    @no_jax_tracing
    def make[G, H](
        cls, n_sys: int, n_kvecs: int, gradient: G = EMPTY, hessian: H = EMPTY
    ) -> EwaldCache[G, H]:
        """Create zeroed caches with the supplied derivative shapes.

        Args:
            n_sys: Number of systems.
            n_kvecs: Number of reciprocal vectors per system, including padding.
            gradient: Template defining cached gradient shapes.
            hessian: Template defining cached Hessian shapes.

        Returns:
            Structure-factor, prefactor and component-output caches set to zero.
        """
        out = PotentialOut(
            Table.arange(jnp.zeros(n_sys, dtype=float), label=SystemId),
            gradient,
            hessian,
        )
        return EwaldCache(
            KahanSummand.init(jnp.zeros((n_sys, n_kvecs, 2), dtype=float)),
            jnp.zeros((n_sys, n_kvecs), dtype=float),
            KahanSummand.init(tree_zeros_like(out)),
            KahanSummand.init(tree_zeros_like(out)),
            KahanSummand.init(tree_zeros_like(out)),
            KahanSummand.init(tree_zeros_like(out)),
            KahanSummand.init(tree_zeros_like(out)),
        )

make(n_sys, n_kvecs, gradient=EMPTY, hessian=EMPTY) classmethod

Create zeroed caches with the supplied derivative shapes.

Parameters:

Name Type Description Default
n_sys int

Number of systems.

required
n_kvecs int

Number of reciprocal vectors per system, including padding.

required
gradient G

Template defining cached gradient shapes.

EMPTY
hessian H

Template defining cached Hessian shapes.

EMPTY

Returns:

Type Description
EwaldCache[G, H]

Structure-factor, prefactor and component-output caches set to zero.

Source code in src/kups/potential/classical/ewald/potential.py
@classmethod
@no_jax_tracing
def make[G, H](
    cls, n_sys: int, n_kvecs: int, gradient: G = EMPTY, hessian: H = EMPTY
) -> EwaldCache[G, H]:
    """Create zeroed caches with the supplied derivative shapes.

    Args:
        n_sys: Number of systems.
        n_kvecs: Number of reciprocal vectors per system, including padding.
        gradient: Template defining cached gradient shapes.
        hessian: Template defining cached Hessian shapes.

    Returns:
        Structure-factor, prefactor and component-output caches set to zero.
    """
    out = PotentialOut(
        Table.arange(jnp.zeros(n_sys, dtype=float), label=SystemId),
        gradient,
        hessian,
    )
    return EwaldCache(
        KahanSummand.init(jnp.zeros((n_sys, n_kvecs, 2), dtype=float)),
        jnp.zeros((n_sys, n_kvecs), dtype=float),
        KahanSummand.init(tree_zeros_like(out)),
        KahanSummand.init(tree_zeros_like(out)),
        KahanSummand.init(tree_zeros_like(out)),
        KahanSummand.init(tree_zeros_like(out)),
        KahanSummand.init(tree_zeros_like(out)),
    )

EwaldCacheDeltaPatch

Bases: Patch[State]

Add each accepted system's structure-factor change to the compensated cache.

Source code in src/kups/potential/classical/ewald/potential.py
@dataclass
class EwaldCacheDeltaPatch[State, Gradient, Hessian](Patch[State]):
    """Add each accepted system's structure-factor change to the compensated cache."""

    structure_factor_delta: Array
    system_idx: Index[SystemId]
    lens: Lens[State, EwaldCache[Gradient, Hessian]] = field(static=True)

    def __call__(self, state: State, accept: Accept) -> State:
        """Apply structure-factor updates for accepted systems.

        Args:
            state: State containing the Ewald cache.
            accept: Per-system acceptance mask.

        Returns:
            State with accepted structure factors and their compensation updated.
        """
        mask = accept[self.system_idx]
        delta = self.structure_factor_delta
        return self.lens.focus(lambda cache: cache.structure_factor).apply(
            state,
            lambda old_sf: jax.tree.map(
                lambda new, old: where_broadcast_last(mask, new, old),
                old_sf + delta,
                old_sf,
            ),
        )

__call__(state, accept)

Apply structure-factor updates for accepted systems.

Parameters:

Name Type Description Default
state State

State containing the Ewald cache.

required
accept Accept

Per-system acceptance mask.

required

Returns:

Type Description
State

State with accepted structure factors and their compensation updated.

Source code in src/kups/potential/classical/ewald/potential.py
def __call__(self, state: State, accept: Accept) -> State:
    """Apply structure-factor updates for accepted systems.

    Args:
        state: State containing the Ewald cache.
        accept: Per-system acceptance mask.

    Returns:
        State with accepted structure factors and their compensation updated.
    """
    mask = accept[self.system_idx]
    delta = self.structure_factor_delta
    return self.lens.focus(lambda cache: cache.structure_factor).apply(
        state,
        lambda old_sf: jax.tree.map(
            lambda new, old: where_broadcast_last(mask, new, old),
            old_sf + delta,
            old_sf,
        ),
    )

EwaldCachePatch

Bases: Patch[State]

Accept or reject each system's structure factor and prefactor.

Source code in src/kups/potential/classical/ewald/potential.py
@dataclass
class EwaldCachePatch[State, Gradient, Hessian](Patch[State]):
    """Accept or reject each system's structure factor and prefactor."""

    new_structure_factor: KahanSummand[Array]
    new_prefactor: Array
    system_idx: Index[SystemId]
    lens: Lens[State, EwaldCache[Gradient, Hessian]] = field(static=True)

    def __call__(self, state: State, accept: Accept) -> State:
        """Apply structure-factor updates for accepted systems.

        Args:
            state: State containing the Ewald cache.
            accept: Per-system acceptance mask.

        Returns:
            State with accepted structure factors, their compensation and
            prefactors updated.
        """
        mask = accept[self.system_idx]
        update = (self.new_structure_factor, self.new_prefactor)
        return self.lens.focus(lambda c: (c.structure_factor, c.prefactor)).apply(
            state,
            lambda current: jax.tree.map(
                lambda new, old: where_broadcast_last(mask, new, old), update, current
            ),
        )

__call__(state, accept)

Apply structure-factor updates for accepted systems.

Parameters:

Name Type Description Default
state State

State containing the Ewald cache.

required
accept Accept

Per-system acceptance mask.

required

Returns:

Type Description
State

State with accepted structure factors, their compensation and

State

prefactors updated.

Source code in src/kups/potential/classical/ewald/potential.py
def __call__(self, state: State, accept: Accept) -> State:
    """Apply structure-factor updates for accepted systems.

    Args:
        state: State containing the Ewald cache.
        accept: Per-system acceptance mask.

    Returns:
        State with accepted structure factors, their compensation and
        prefactors updated.
    """
    mask = accept[self.system_idx]
    update = (self.new_structure_factor, self.new_prefactor)
    return self.lens.focus(lambda c: (c.structure_factor, c.prefactor)).apply(
        state,
        lambda current: jax.tree.map(
            lambda new, old: where_broadcast_last(mask, new, old), update, current
        ),
    )

EwaldLongRangeComposer

Build current particles and retain previous values for incremental updates.

Source code in src/kups/potential/classical/ewald/potential.py
@dataclass
class EwaldLongRangeComposer[
    State,
    Ptch: Patch[Any],
]:
    """Build current particles and retain previous values for incremental updates."""

    particles: View[State, Table[ParticleId, IsEwaldPointData]] = field(static=True)
    systems: View[State, Table[SystemId, HasCell[Periodic3D]]] = field(static=True)
    probe: Probe[State, Ptch, WithIndices[ParticleId, IsEwaldPointData]] | None = field(
        static=True
    )
    parameters: View[State, EwaldParameters] = field(static=True)
    cache: Lens[State, EwaldCache[Any, Any]] | None = field(static=True)

    def __call__(
        self, state: State, patch: Ptch | None
    ) -> Sum[EwaldLongRangeInput[State]]:
        """Compose the reciprocal-energy input for a full evaluation or proposal.

        Args:
            state: State providing particles, systems, parameters and optional cache.
            patch: Optional proposed state patch to inspect with the particle probe.

        Returns:
            A single-summand input with current particles and any probed previous values.
        """
        particles = self.particles(state)
        changes = None
        if patch is not None and self.probe is not None:
            updates = self.probe(state, patch)
            changes = WithIndices(
                updates.indices, (particles[updates.indices], updates.data)
            )
            # Only derivatives read the patched particles; energy-only
            # evaluation leaves the update to dead-code elimination.
            particles = particles.update(updates.indices, updates.data)
        inp = EwaldLongRangeInput(
            PointCloud(particles, self.systems(state)),
            self.parameters(state),
            self.cache.get(state) if self.cache else None,
            self.cache,
            changes,
        )
        return Sum(Summand(inp))

__call__(state, patch)

Compose the reciprocal-energy input for a full evaluation or proposal.

Parameters:

Name Type Description Default
state State

State providing particles, systems, parameters and optional cache.

required
patch Ptch | None

Optional proposed state patch to inspect with the particle probe.

required

Returns:

Type Description
Sum[EwaldLongRangeInput[State]]

A single-summand input with current particles and any probed previous values.

Source code in src/kups/potential/classical/ewald/potential.py
def __call__(
    self, state: State, patch: Ptch | None
) -> Sum[EwaldLongRangeInput[State]]:
    """Compose the reciprocal-energy input for a full evaluation or proposal.

    Args:
        state: State providing particles, systems, parameters and optional cache.
        patch: Optional proposed state patch to inspect with the particle probe.

    Returns:
        A single-summand input with current particles and any probed previous values.
    """
    particles = self.particles(state)
    changes = None
    if patch is not None and self.probe is not None:
        updates = self.probe(state, patch)
        changes = WithIndices(
            updates.indices, (particles[updates.indices], updates.data)
        )
        # Only derivatives read the patched particles; energy-only
        # evaluation leaves the update to dead-code elimination.
        particles = particles.update(updates.indices, updates.data)
    inp = EwaldLongRangeInput(
        PointCloud(particles, self.systems(state)),
        self.parameters(state),
        self.cache.get(state) if self.cache else None,
        self.cache,
        changes,
    )
    return Sum(Summand(inp))

EwaldLongRangeInput

Current particles and optional changed rows for a cached update.

changes_from_prev holds the previous and current values of the changed rows; energy-only evaluation reads only these, derivatives point_cloud.

Source code in src/kups/potential/classical/ewald/potential.py
@dataclass
class EwaldLongRangeInput[State]:
    """Current particles and optional changed rows for a cached update.

    ``changes_from_prev`` holds the previous and current values of the changed
    rows; energy-only evaluation reads only these, derivatives ``point_cloud``.
    """

    point_cloud: PointCloud[IsEwaldPointData, HasCell[Periodic3D]]
    parameters: EwaldParameters
    cache: EwaldCache[Any, Any] | None = None
    cache_lens: Lens[State, EwaldCache[Any, Any]] | None = None
    changes_from_prev: (
        WithIndices[ParticleId, tuple[IsEwaldPointData, IsEwaldPointData]] | None
    ) = None

    @property
    def volume(self) -> Array:
        """Cell volumes in ų, shaped ``(n_systems,)``."""
        return self.point_cloud.systems.data.cell.volume

    @property
    def kvecs(self) -> Array:
        """Convert the stored integer shifts to Cartesian reciprocal vectors.

        The result is in 1/Å, shaped ``(n_systems, n_kvecs, 3)``.
        """
        sys_idx = self.point_cloud.systems.index
        return triangular_3x3_matmul(
            self.point_cloud.systems.data.cell.inverse_vectors.mT[:, None] * 2 * jnp.pi,
            self.parameters.reciprocal_lattice_shifts[sys_idx],
            lower=False,
        )

kvecs property

Convert the stored integer shifts to Cartesian reciprocal vectors.

The result is in 1/Å, shaped (n_systems, n_kvecs, 3).

volume property

Cell volumes in ų, shaped (n_systems,).

EwaldParameterEstimates

Estimated optimal Ewald parameters for given accuracy.

Source code in src/kups/potential/classical/ewald/parameters.py
@dataclass
class EwaldParameterEstimates:
    """Estimated optimal Ewald parameters for given accuracy."""

    alpha: float
    real_cutoff: float
    k_max: float
    error_real: float
    error_recip: float

EwaldParameters

Per-system cutoffs and reciprocal shifts prepared before evaluation.

reciprocal_lattice_shifts stores sphere-filtered shifts padded for batching. reciprocal_shift_bound always stores three axis bounds. (0, 0, 0) selects explicit sums over the shifts; other bounds select factored phase grids over that box, from which the shifts are gathered. Rebuild parameters and the structure-factor cache when changing the cell or cutoff. reciprocal_dtype is the precision of structure-factor evaluations; float32 is much faster on GPUs with slow double precision.

Source code in src/kups/potential/classical/ewald/parameters.py
@dataclass
class EwaldParameters:
    """Per-system cutoffs and reciprocal shifts prepared before evaluation.

    ``reciprocal_lattice_shifts`` stores sphere-filtered shifts padded for
    batching. ``reciprocal_shift_bound`` always stores three axis bounds.
    ``(0, 0, 0)`` selects explicit sums over the shifts; other bounds select
    factored phase grids over that box, from which the shifts are gathered.
    Rebuild parameters and the structure-factor cache when changing the cell
    or cutoff. ``reciprocal_dtype`` is the precision of structure-factor
    evaluations; ``float32`` is much faster on GPUs with slow double precision.
    """

    alpha: Table[SystemId, Array]  # (n_graphs,)
    cutoff: Table[SystemId, Array]  # (n_graphs,)
    reciprocal_lattice_shifts: Table[SystemId, Array]  # (n_graphs, n_kvecs, 3)
    k_max: Table[SystemId, Array]
    reciprocal_shift_bound: ReciprocalGridBound = field(static=True, default=(0, 0, 0))
    reciprocal_particle_chunk_size: int = field(
        static=True, default_factory=_default_reciprocal_particle_chunk
    )
    reciprocal_k_chunk_size: int = field(
        static=True, default=_DEFAULT_RECIPROCAL_K_CHUNK
    )
    reciprocal_dtype: DTypeLike = field(static=True, default=jnp.float64)

    @classmethod
    @no_jax_tracing
    def make(
        cls,
        charges: Table[ParticleId, IsEwaldPointData],
        cell: Table[SystemId, HasCell[Periodic3D]],
        epsilon_total: float = 1e-8,
        real_cutoff: float | None = None,
        *,
        reciprocal_particle_chunk_size: int | None = None,
        reciprocal_k_chunk_size: int = _DEFAULT_RECIPROCAL_K_CHUNK,
        reciprocal_dtype: DTypeLike = jnp.float64,
    ) -> EwaldParameters:
        """Estimate each system's parameters and prepare its reciprocal shifts.

        Args:
            charges: Particle table with charges and system assignments.
            cell: System table with periodic cells.
            epsilon_total: Target combined real-space and reciprocal-space error.
            real_cutoff: Optional fixed real-space cutoff in Å.
            reciprocal_particle_chunk_size: Maximum particles per response tile;
                defaults to 8192 on GPU and 1024 elsewhere.
            reciprocal_k_chunk_size: Maximum explicit k-vectors per tile; also sets the grid workspace budget.
            reciprocal_dtype: Precision of structure-factor evaluations.

        Returns:
            Ewald parameters with stored integer shifts and reciprocal cutoffs.
        """
        n_systems = len(cell.keys)
        sys_idx = np.asarray(charges.data.system.indices_in(cell.keys))
        charge_values = np.asarray(charges.data.charges)
        cell_list = [cell.data.cell[i] for i in range(n_systems)]
        estimates = [
            estimate_ewald_parameters(
                charge_values[sys_idx == i],
                u,
                real_cutoff=real_cutoff,
                epsilon_total=epsilon_total,
            )
            for i, u in enumerate(cell_list)
        ]
        return cls.from_cutoffs(
            cell,
            alpha=cell.set_data(jnp.asarray([est.alpha for est in estimates])),
            cutoff=cell.set_data(jnp.asarray([est.real_cutoff for est in estimates])),
            k_max=cell.set_data(jnp.asarray([est.k_max for est in estimates])),
            reciprocal_particle_chunk_size=reciprocal_particle_chunk_size,
            reciprocal_k_chunk_size=reciprocal_k_chunk_size,
            reciprocal_dtype=reciprocal_dtype,
        )

    @classmethod
    @no_jax_tracing
    def from_cutoffs(
        cls,
        cell: Table[SystemId, HasCell[Periodic3D]],
        alpha: Table[SystemId, Array],
        cutoff: Table[SystemId, Array],
        k_max: Table[SystemId, Array],
        *,
        reciprocal_particle_chunk_size: int | None = None,
        reciprocal_k_chunk_size: int = _DEFAULT_RECIPROCAL_K_CHUNK,
        reciprocal_dtype: DTypeLike = jnp.float64,
    ) -> EwaldParameters:
        """Prepare reciprocal shifts once from supplied physical cutoffs.

        Args:
            cell: System table with periodic cells.
            alpha: Screening parameters in 1/Å.
            cutoff: Real-space cutoffs in Å.
            k_max: Reciprocal cutoffs in 1/Å.
            reciprocal_particle_chunk_size: Maximum particles per response tile;
                defaults to 8192 on GPU and 1024 elsewhere.
            reciprocal_k_chunk_size: Maximum k-vectors per tile.
            reciprocal_dtype: Precision of structure-factor evaluations.

        Returns:
            Parameters with stored integer shifts and reciprocal cutoffs.
        """
        if reciprocal_particle_chunk_size is None:
            reciprocal_particle_chunk_size = _default_reciprocal_particle_chunk()
        shifts, bound = prepare_reciprocal_shifts(
            cell.data.cell,
            k_max[cell.index],
            grid=jax.default_backend() in RECIPROCAL_GRID_BACKENDS,
        )
        return cls(
            alpha=alpha,
            cutoff=cutoff,
            reciprocal_lattice_shifts=cell.set_data(shifts),
            k_max=k_max,
            reciprocal_shift_bound=bound,
            reciprocal_particle_chunk_size=reciprocal_particle_chunk_size,
            reciprocal_k_chunk_size=reciprocal_k_chunk_size,
            reciprocal_dtype=reciprocal_dtype,
        )

from_cutoffs(cell, alpha, cutoff, k_max, *, reciprocal_particle_chunk_size=None, reciprocal_k_chunk_size=_DEFAULT_RECIPROCAL_K_CHUNK, reciprocal_dtype=jnp.float64) classmethod

Prepare reciprocal shifts once from supplied physical cutoffs.

Parameters:

Name Type Description Default
cell Table[SystemId, HasCell[Periodic3D]]

System table with periodic cells.

required
alpha Table[SystemId, Array]

Screening parameters in 1/Å.

required
cutoff Table[SystemId, Array]

Real-space cutoffs in Å.

required
k_max Table[SystemId, Array]

Reciprocal cutoffs in 1/Å.

required
reciprocal_particle_chunk_size int | None

Maximum particles per response tile; defaults to 8192 on GPU and 1024 elsewhere.

None
reciprocal_k_chunk_size int

Maximum k-vectors per tile.

_DEFAULT_RECIPROCAL_K_CHUNK
reciprocal_dtype DTypeLike

Precision of structure-factor evaluations.

float64

Returns:

Type Description
EwaldParameters

Parameters with stored integer shifts and reciprocal cutoffs.

Source code in src/kups/potential/classical/ewald/parameters.py
@classmethod
@no_jax_tracing
def from_cutoffs(
    cls,
    cell: Table[SystemId, HasCell[Periodic3D]],
    alpha: Table[SystemId, Array],
    cutoff: Table[SystemId, Array],
    k_max: Table[SystemId, Array],
    *,
    reciprocal_particle_chunk_size: int | None = None,
    reciprocal_k_chunk_size: int = _DEFAULT_RECIPROCAL_K_CHUNK,
    reciprocal_dtype: DTypeLike = jnp.float64,
) -> EwaldParameters:
    """Prepare reciprocal shifts once from supplied physical cutoffs.

    Args:
        cell: System table with periodic cells.
        alpha: Screening parameters in 1/Å.
        cutoff: Real-space cutoffs in Å.
        k_max: Reciprocal cutoffs in 1/Å.
        reciprocal_particle_chunk_size: Maximum particles per response tile;
            defaults to 8192 on GPU and 1024 elsewhere.
        reciprocal_k_chunk_size: Maximum k-vectors per tile.
        reciprocal_dtype: Precision of structure-factor evaluations.

    Returns:
        Parameters with stored integer shifts and reciprocal cutoffs.
    """
    if reciprocal_particle_chunk_size is None:
        reciprocal_particle_chunk_size = _default_reciprocal_particle_chunk()
    shifts, bound = prepare_reciprocal_shifts(
        cell.data.cell,
        k_max[cell.index],
        grid=jax.default_backend() in RECIPROCAL_GRID_BACKENDS,
    )
    return cls(
        alpha=alpha,
        cutoff=cutoff,
        reciprocal_lattice_shifts=cell.set_data(shifts),
        k_max=k_max,
        reciprocal_shift_bound=bound,
        reciprocal_particle_chunk_size=reciprocal_particle_chunk_size,
        reciprocal_k_chunk_size=reciprocal_k_chunk_size,
        reciprocal_dtype=reciprocal_dtype,
    )

make(charges, cell, epsilon_total=1e-08, real_cutoff=None, *, reciprocal_particle_chunk_size=None, reciprocal_k_chunk_size=_DEFAULT_RECIPROCAL_K_CHUNK, reciprocal_dtype=jnp.float64) classmethod

Estimate each system's parameters and prepare its reciprocal shifts.

Parameters:

Name Type Description Default
charges Table[ParticleId, IsEwaldPointData]

Particle table with charges and system assignments.

required
cell Table[SystemId, HasCell[Periodic3D]]

System table with periodic cells.

required
epsilon_total float

Target combined real-space and reciprocal-space error.

1e-08
real_cutoff float | None

Optional fixed real-space cutoff in Å.

None
reciprocal_particle_chunk_size int | None

Maximum particles per response tile; defaults to 8192 on GPU and 1024 elsewhere.

None
reciprocal_k_chunk_size int

Maximum explicit k-vectors per tile; also sets the grid workspace budget.

_DEFAULT_RECIPROCAL_K_CHUNK
reciprocal_dtype DTypeLike

Precision of structure-factor evaluations.

float64

Returns:

Type Description
EwaldParameters

Ewald parameters with stored integer shifts and reciprocal cutoffs.

Source code in src/kups/potential/classical/ewald/parameters.py
@classmethod
@no_jax_tracing
def make(
    cls,
    charges: Table[ParticleId, IsEwaldPointData],
    cell: Table[SystemId, HasCell[Periodic3D]],
    epsilon_total: float = 1e-8,
    real_cutoff: float | None = None,
    *,
    reciprocal_particle_chunk_size: int | None = None,
    reciprocal_k_chunk_size: int = _DEFAULT_RECIPROCAL_K_CHUNK,
    reciprocal_dtype: DTypeLike = jnp.float64,
) -> EwaldParameters:
    """Estimate each system's parameters and prepare its reciprocal shifts.

    Args:
        charges: Particle table with charges and system assignments.
        cell: System table with periodic cells.
        epsilon_total: Target combined real-space and reciprocal-space error.
        real_cutoff: Optional fixed real-space cutoff in Å.
        reciprocal_particle_chunk_size: Maximum particles per response tile;
            defaults to 8192 on GPU and 1024 elsewhere.
        reciprocal_k_chunk_size: Maximum explicit k-vectors per tile; also sets the grid workspace budget.
        reciprocal_dtype: Precision of structure-factor evaluations.

    Returns:
        Ewald parameters with stored integer shifts and reciprocal cutoffs.
    """
    n_systems = len(cell.keys)
    sys_idx = np.asarray(charges.data.system.indices_in(cell.keys))
    charge_values = np.asarray(charges.data.charges)
    cell_list = [cell.data.cell[i] for i in range(n_systems)]
    estimates = [
        estimate_ewald_parameters(
            charge_values[sys_idx == i],
            u,
            real_cutoff=real_cutoff,
            epsilon_total=epsilon_total,
        )
        for i, u in enumerate(cell_list)
    ]
    return cls.from_cutoffs(
        cell,
        alpha=cell.set_data(jnp.asarray([est.alpha for est in estimates])),
        cutoff=cell.set_data(jnp.asarray([est.real_cutoff for est in estimates])),
        k_max=cell.set_data(jnp.asarray([est.k_max for est in estimates])),
        reciprocal_particle_chunk_size=reciprocal_particle_chunk_size,
        reciprocal_k_chunk_size=reciprocal_k_chunk_size,
        reciprocal_dtype=reciprocal_dtype,
    )

EwaldPotential

Bases: SummedPotential[State, Gradients, Hessians, P]

Complete Ewald potential with named access to each component term.

Source code in src/kups/potential/classical/ewald/potential.py
@dataclass
class EwaldPotential[State, Gradients, Hessians, P: Patch[Any]](
    SummedPotential[State, Gradients, Hessians, P]
):
    """Complete Ewald potential with named access to each component term."""

    @property
    def short_range(self) -> Potential[State, Gradients, Hessians, P]:
        """Real-space screened Coulomb potential."""
        return self.potentials[0]

    @property
    def long_range(self) -> Potential[State, Gradients, Hessians, P]:
        """Reciprocal-space potential."""
        return self.potentials[1]

    @property
    def self_interaction(self) -> Potential[State, Gradients, Hessians, P]:
        """Subtract each charge's interaction with its screening cloud."""
        return self.potentials[2]

    @property
    def net_charge(self) -> Potential[State, Gradients, Hessians, P]:
        """Neutralizing-background correction for net-charged systems."""
        return self.potentials[3]

    @property
    def exclusion_correction(self) -> Potential[State, Gradients, Hessians, P]:
        """Subtract excluded pair interactions; requires exclusions enabled."""
        return self.potentials[4]

exclusion_correction property

Subtract excluded pair interactions; requires exclusions enabled.

long_range property

Reciprocal-space potential.

net_charge property

Neutralizing-background correction for net-charged systems.

self_interaction property

Subtract each charge's interaction with its screening cloud.

short_range property

Real-space screened Coulomb potential.

IsChargedTemplate

Bases: HasPositions, HasCharges, Protocol

Properties needed to prepare intramolecular Coulomb energies.

Source code in src/kups/potential/classical/ewald/potential.py
class IsChargedTemplate(HasPositions, HasCharges, Protocol):
    """Properties needed to prepare intramolecular Coulomb energies."""

IsEwaldPointData

Bases: HasCharges, NeighborListPoints, Protocol

Particle data required by Ewald: charges, positions, system/inclusion/exclusion indices.

Source code in src/kups/potential/classical/ewald/parameters.py
class IsEwaldPointData(HasCharges, NeighborListPoints, Protocol):
    """Particle data required by Ewald: charges, positions, system/inclusion/exclusion indices."""

estimate_ewald_parameters(charges, cell, /, real_cutoff=None, alpha=None, epsilon_total=1e-08)

Estimate cutoffs and screening for one system before JIT compilation.

Split epsilon_total equally between real and reciprocal error targets.

Parameters:

Name Type Description Default
charges Array | ndarray

Charges in e for one system, shaped (n_particles,).

required
cell Cell[AnyPeriodicity]

Single simulation cell.

required
real_cutoff float | None

Optional fixed real-space cutoff in Å; otherwise optimized.

None
alpha float | None

Optional screening parameter in 1/Å; ignored with a fixed real cutoff.

None
epsilon_total float

Target combined real-space and reciprocal-space error.

1e-08

Returns:

Type Description
EwaldParameterEstimates

Screening, cutoffs and estimated errors.

Source code in src/kups/potential/classical/ewald/parameters.py
@no_jax_tracing
def estimate_ewald_parameters(
    charges: Array | np.ndarray,
    cell: Cell[AnyPeriodicity],
    /,
    real_cutoff: float | None = None,
    alpha: float | None = None,
    epsilon_total: float = 1e-8,
) -> EwaldParameterEstimates:
    """Estimate cutoffs and screening for one system before JIT compilation.

    Split ``epsilon_total`` equally between real and reciprocal error targets.

    Args:
        charges: Charges in e for one system, shaped ``(n_particles,)``.
        cell: Single simulation cell.
        real_cutoff: Optional fixed real-space cutoff in Å; otherwise optimized.
        alpha: Optional screening parameter in 1/Å; ignored with a fixed real cutoff.
        epsilon_total: Target combined real-space and reciprocal-space error.

    Returns:
        Screening, cutoffs and estimated errors.
    """
    charges_np = np.asarray(charges)
    net_charge = float(charges_np.astype(np.float64).sum())
    if abs(net_charge) > 1e-6:
        warnings.warn(
            f"System is not charge neutral (net charge {net_charge:.4g} e); "
            "the neutralizing-background correction will be applied.",
            stacklevel=2,
        )
    volume = np.asarray(cell.volume)
    squared_charge = np.vdot(charges_np, charges_np)
    n_particles = charges_np.size
    max_radius = float(np.min(cell.perpendicular_lengths, axis=0)) / 2
    target = epsilon_total / 2
    lattice_length = volume ** (1 / 3)

    def real_error(cutoff: float, alpha: float):
        if cutoff <= 0 or alpha <= 0:
            return np.inf
        return (squared_charge / np.sqrt(n_particles)) * (erfc(alpha * cutoff) / cutoff)

    def reciprocal_error(cutoff: float, alpha: float):
        if cutoff <= 0 or alpha <= 0:
            return np.inf
        exponent = -(cutoff**2) / (4 * alpha**2)
        if exponent < -700:
            return 0.0
        return (squared_charge * alpha / (np.sqrt(n_particles) * np.pi)) * np.exp(
            exponent
        )

    def cutoffs(alpha: float) -> tuple[float, float]:
        return (
            _cutoff_for_error(
                lambda r: real_error(r, alpha), (0.01, min(20.0, max_radius)), target
            ),
            _cutoff_for_error(lambda k: reciprocal_error(k, alpha), (0.1, 5.0), target),
        )

    def total_cost(alpha: float) -> float:
        if alpha <= 0:
            return np.inf
        rc, kc = cutoffs(alpha)
        if rc == np.inf or kc == np.inf:
            return np.inf
        n_kvecs = (2 * kc * lattice_length / 2 / np.pi + 1) ** 3
        real_cost = rc**3 * n_particles / volume * 4 / 3 * np.pi
        return float(real_cost + n_kvecs)

    if real_cutoff is not None:
        # erfc(alpha * cutoff) = cutoff * target fixes alpha for this cutoff.
        result = scipy.optimize.minimize_scalar(
            lambda z: abs(erfc(z) - real_cutoff * target)
        )
        if not result.success:
            raise ValueError("Failed to find alpha for given real space cutoff.")
        alpha_opt = result.x / real_cutoff
        rc_opt = real_cutoff
        kc_opt = 2 * alpha_opt * np.sqrt(-np.log(target))
    else:
        alpha_opt = (
            _minimize_on_grid(total_cost, (0.001, 2), samples=400)
            if alpha is None
            else alpha
        )
        rc_opt, kc_opt = cutoffs(alpha_opt)
        if rc_opt <= 0 or kc_opt <= 0 or rc_opt == np.inf or kc_opt == np.inf:
            raise ValueError("Invalid cutoff values computed")

    return EwaldParameterEstimates(
        alpha=alpha_opt,
        real_cutoff=rc_opt,
        k_max=kc_opt,
        error_real=real_error(rc_opt, alpha_opt),
        error_recip=reciprocal_error(kc_opt, alpha_opt),
    )

ewald_long_range_energy(inp)

Reciprocal energy with an acceptance cache patch.

Parameters:

Name Type Description Default
inp EwaldLongRangeInput[State]

Current particles, reciprocal parameters and optional incremental cache.

required

Returns:

Type Description
WithPatch[Table[SystemId, Energy], Patch[State]]

Per-system energies in eV with the structure-factor acceptance patch.

Source code in src/kups/potential/classical/ewald/potential.py
def ewald_long_range_energy[State](
    inp: EwaldLongRangeInput[State],
) -> WithPatch[Table[SystemId, Energy], Patch[State]]:
    """Reciprocal energy with an acceptance cache patch.

    Args:
        inp: Current particles, reciprocal parameters and optional incremental cache.

    Returns:
        Per-system energies in eV with the structure-factor acceptance patch.
    """
    if inp.changes_from_prev is not None and inp.cache is not None:
        delta = structure_factor_delta(inp)
        new_total = inp.cache.structure_factor.total + delta
        patch = _cache_patch(inp, delta)
    else:
        structure_out, patch = structure_factor(inp)
        new_total = structure_out.total
    energy = long_range(inp, new_total)
    assert energy.shape == (inp.point_cloud.batch_size,), (
        f"Expected energy shape {(inp.point_cloud.batch_size,)} but got {energy.shape}."
    )
    return WithPatch(Table.arange(energy * TO_STANDARD_UNITS, label=SystemId), patch)

ewald_net_charge_energy(inp)

Uniform neutralizing-background correction -π Q² / (2 V α²) per system.

Parameters:

Name Type Description Default
inp EwaldSelfInput

Particle graph and per-system screening parameters.

required

Returns:

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

Per-system neutralizing-background energies in eV and an identity patch.

Source code in src/kups/potential/classical/ewald/potential.py
def ewald_net_charge_energy(
    inp: EwaldSelfInput,
) -> WithPatch[Table[SystemId, Energy], IdPatch[Any]]:
    """Uniform neutralizing-background correction ``-π Q² / (2 V α²)`` per system.

    Args:
        inp: Particle graph and per-system screening parameters.

    Returns:
        Per-system neutralizing-background energies in eV and an identity patch.
    """
    sys_idx = inp.graph.systems.index
    particles = inp.graph.particles.data
    energies = _net_charge_energy(
        particles.system.sum_over(particles.charges)[sys_idx],
        inp.parameters.alpha[sys_idx],
        inp.graph.systems.data.cell.volume,
    )
    return WithPatch(Table.arange(energies, label=SystemId), IdPatch[Any]())

ewald_self_interaction_energy(inp)

Subtract each charge's interaction with its own Gaussian screening cloud.

Parameters:

Name Type Description Default
inp EwaldSelfInput

Particle graph and per-system screening parameters.

required

Returns:

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

Per-system self-interaction energies in eV and an identity patch.

Source code in src/kups/potential/classical/ewald/potential.py
def ewald_self_interaction_energy(
    inp: EwaldSelfInput,
) -> WithPatch[Table[SystemId, Energy], IdPatch[Any]]:
    """Subtract each charge's interaction with its own Gaussian screening cloud.

    Args:
        inp: Particle graph and per-system screening parameters.

    Returns:
        Per-system self-interaction energies in eV and an identity patch.
    """
    sys_idx = inp.graph.systems.index
    energies = _self_energy(
        inp.graph.particles.data.system.sum_over(inp.graph.particles.data.charges**2)[
            sys_idx
        ],
        inp.parameters.alpha[sys_idx],
    )
    return WithPatch(Table.arange(energies, label=SystemId), IdPatch[Any]())

ewald_short_range_pair_kernel(parameters, charges_i, charges_j, rij, r2, system)

Screened Coulomb pair kernel shared by graph and fused evaluation.

Callers apply the cutoff; pair inputs broadcast to a common shape.

Parameters:

Name Type Description Default
parameters EwaldParameters

Per-system screening parameters.

required
charges_i Array

Left-particle charges in e.

required
charges_j Array

Right-particle charges in e.

required
rij Array

Pair displacement vectors; unused by this radial kernel.

required
r2 Array

Squared pair distances in Ų.

required
system Index[SystemId]

Left-particle system indices.

required

Returns:

Type Description
Array

Pair energies in eV.

Source code in src/kups/potential/classical/ewald/potential.py
def ewald_short_range_pair_kernel(
    parameters: EwaldParameters,
    charges_i: Array,
    charges_j: Array,
    rij: Array,
    r2: Array,
    system: Index[SystemId],
    /,
) -> Array:
    """Screened Coulomb pair kernel shared by graph and fused evaluation.

    Callers apply the cutoff; pair inputs broadcast to a common shape.

    Args:
        parameters: Per-system screening parameters.
        charges_i: Left-particle charges in e.
        charges_j: Right-particle charges in e.
        rij: Pair displacement vectors; unused by this radial kernel.
        r2: Squared pair distances in Ų.
        system: Left-particle system indices.

    Returns:
        Pair energies in eV.
    """
    del rij
    dists = jnp.sqrt(r2)
    alpha = parameters.alpha
    alpha_values = alpha.data[0] if alpha.size == 1 else alpha[system]
    argument = alpha_values * dists
    if jax.default_backend() != "cpu":
        # Direct erfc crashes the GPU fused kernel compiler.
        screened = (1.0 - jax.lax.erf(argument)) / dists
    elif argument.dtype == jnp.float64:
        screened = erfc_over(argument, dists)
    else:
        screened = jax.lax.erfc(argument) / dists
    return TO_STANDARD_UNITS * charges_i * charges_j * screened

kvecs_from_kmax(cell, kmax)

Integer half-space lattice coefficients within the reciprocal kmax sphere.

Parameters:

Name Type Description Default
cell Cell[AnyPeriodicity]

Cell defining the reciprocal lattice.

required
kmax float

Maximum reciprocal-vector magnitude in 1/Å.

required

Returns:

Type Description
ndarray

Integer half-space lattice coefficients shaped (n_kvecs, 3).

Source code in src/kups/potential/classical/ewald/parameters.py
@no_jax_tracing
def kvecs_from_kmax(cell: Cell[AnyPeriodicity], kmax: float) -> np.ndarray:
    """Integer half-space lattice coefficients within the reciprocal kmax sphere.

    Args:
        cell: Cell defining the reciprocal lattice.
        kmax: Maximum reciprocal-vector magnitude in 1/Å.

    Returns:
        Integer half-space lattice coefficients shaped ``(n_kvecs, 3)``.
    """
    vecs = reciprocal_grid_shifts(reciprocal_grid_bound(cell, kmax))
    kvecs = vecs @ _reciprocal_vectors(cell)
    return vecs[np.sum(kvecs**2, axis=-1) <= kmax**2]

long_range(inp, structure_factor)

Compute the unscaled reciprocal sum sum_k P(k) |S(k)|².

Parameters:

Name Type Description Default
inp EwaldLongRangeInput[Any]

System cells, reciprocal-space parameters and, for incremental inputs, the cached prefactor.

required
structure_factor Array

Real/imaginary structure factors, shaped (n_systems, n_kvecs, 2).

required

Returns:

Type Description
Energy

Per-system reciprocal sums before TO_STANDARD_UNITS scaling,

Energy

excluding the neutralizing-background correction.

Source code in src/kups/potential/classical/ewald/potential.py
def long_range(inp: EwaldLongRangeInput[Any], structure_factor: Array) -> Energy:
    """Compute the unscaled reciprocal sum ``sum_k P(k) |S(k)|²``.

    Args:
        inp: System cells, reciprocal-space parameters and, for incremental
            inputs, the cached prefactor.
        structure_factor: Real/imaginary structure factors, shaped ``(n_systems, n_kvecs, 2)``.

    Returns:
        Per-system reciprocal sums before ``TO_STANDARD_UNITS`` scaling,
        excluding the neutralizing-background correction.
    """
    if inp.changes_from_prev is None:
        weights = prefactor(inp)
    else:
        assert inp.cache is not None, "Cache required for structure factor update"
        weights = _cached_prefactor(inp.cache.prefactor, *_prefactor_args(inp))
    return jnp.sum(weights * jnp.sum(structure_factor**2, axis=-1), axis=-1)

make_ewald_exclusion_correction_potential(particles_view, systems_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, cache_lens=None, *, composition=None)

Subtract intramolecular Coulomb interactions, with optional rigid templates.

The graph and template paths share the Coulomb pair kernel. Template energies are linear in molecular counts. Large templates and excluded fixed particles retain geometric evaluation to preserve minimum-image distances and topology. The cache stores the unscaled Coulomb sum, as in the other Ewald components.

Source code in src/kups/potential/classical/ewald/potential.py
def make_ewald_exclusion_correction_potential[
    State,
    Ptch: Patch[Any],
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsEwaldPointData]],
    systems_view: View[State, Table[SystemId, HasCell[Periodic3D]]],
    probe: Probe[State, Ptch, IsParticleProbe[IsEwaldPointData]] | None,
    gradient_lens: Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    cache_lens: Lens[State, KahanSummand[PotentialOut[Gradients, Hessians]]]
    | None = None,
    *,
    composition: RigidBodyComposition[State, IsChargedTemplate] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Subtract intramolecular Coulomb interactions, with optional rigid templates.

    The graph and template paths share the Coulomb pair kernel. Template energies
    are linear in molecular counts. Large templates and excluded fixed particles
    retain geometric evaluation to preserve minimum-image distances and topology.
    The cache stores the unscaled Coulomb sum, as in the other Ewald components.
    """
    if composition is not None:
        initial = composition.initial_state
        particles, systems = particles_view(initial), systems_view(initial)
        gradients = gradient_lens(PointCloud(particles, systems))
        if jax.tree.leaves(gradients):
            raise ValueError("Rigid-body composition requires energy-only evaluation")
        coefficients = _molecular_exclusion_coefficients(
            composition, particles.data, systems
        )
        if coefficients is not None:
            prepared = composition.potential(
                coefficients,
                gradients,
                hessian_lens,
                cache_lens,
                patch_idx_view,
            )
            return ScaledPotential(prepared, -1)
    excl_view = pipe(
        particles_view, functools.partial(_ewald_particle_table, excluded_pairs=True)
    )
    excl_probe = _ewald_probe(probe, excluded_pairs=True)
    excl_rg = GraphConstructor(
        particles=excl_view,
        systems=systems_view,
        neighborlist=lambda _: all_connected_neighborlist,
        probe=excl_probe,
    )
    exclusion_correction = PotentialFromEnergy(
        energy_fn=_pairwise_coulomb_energy,
        composer=LocalSumComposer(GraphInputConstructor(excl_rg, lambda x: None)),
        gradient_lens=lens(lambda x: x.graph).nest(gradient_lens),
        hessian_lens=hessian_lens,
        cache_lens=cache_lens,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
    )
    return ScaledPotential(exclusion_correction, -1)

make_ewald_long_range_potential(particles_view, systems_view, parameter_lens, cache_lens, probe=None, gradient_lens=EMPTY_LENS, hessian_lens=EMPTY_LENS, hessian_idx_view=EMPTY_LENS, patch_idx_view=None)

Create the Ewald reciprocal-space (long-range) potential.

Parameters:

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

View of the particle table.

required
systems_view View[State, Table[SystemId, HasCell[Periodic3D]]]

View of the system table and periodic cells.

required
parameter_lens View[State, EwaldParameters]

View of Ewald parameters.

required
cache_lens Lens[State, EwaldCache[Gradients, Hessians]] | None

Optional lens to structure-factor and component-output caches.

required
probe Probe[State, Ptch, WithIndices[ParticleId, IsEwaldPointData]] | None

Optional probe returning proposed particle updates.

None
gradient_lens Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients]

Lens selecting point-cloud variables to differentiate.

EMPTY_LENS
hessian_lens Lens[Gradients, Hessians]

Lens selecting gradient entries to differentiate again.

EMPTY_LENS
hessian_idx_view View[State, Hessians]

View supplying Hessian row and column indices.

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

Optional view supplying indices for output-cache updates.

None

Returns:

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

Reciprocal potential with optional incremental structure-factor updates.

Source code in src/kups/potential/classical/ewald/potential.py
def make_ewald_long_range_potential[
    State,
    Ptch: Patch[Any],
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsEwaldPointData]],
    systems_view: View[State, Table[SystemId, HasCell[Periodic3D]]],
    parameter_lens: View[State, EwaldParameters],
    cache_lens: Lens[State, EwaldCache[Gradients, Hessians]] | None,
    probe: Probe[State, Ptch, WithIndices[ParticleId, IsEwaldPointData]] | None = None,
    gradient_lens: Lens[
        PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients
    ] = EMPTY_LENS,
    hessian_lens: Lens[Gradients, Hessians] = EMPTY_LENS,
    hessian_idx_view: View[State, Hessians] = EMPTY_LENS,
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Create the Ewald reciprocal-space (long-range) potential.

    Args:
        particles_view: View of the particle table.
        systems_view: View of the system table and periodic cells.
        parameter_lens: View of Ewald parameters.
        cache_lens: Optional lens to structure-factor and component-output caches.
        probe: Optional probe returning proposed particle updates.
        gradient_lens: Lens selecting point-cloud variables to differentiate.
        hessian_lens: Lens selecting gradient entries to differentiate again.
        hessian_idx_view: View supplying Hessian row and column indices.
        patch_idx_view: Optional view supplying indices for output-cache updates.

    Returns:
        Reciprocal potential with optional incremental structure-factor updates.
    """
    return PotentialFromEnergy(
        energy_fn=ewald_long_range_energy,
        composer=EwaldLongRangeComposer(
            particles=particles_view,
            systems=systems_view,
            probe=probe,
            parameters=parameter_lens,
            cache=cache_lens,
        ),
        gradient_lens=lens(lambda x: x.point_cloud).nest(gradient_lens),
        hessian_lens=hessian_lens,
        cache_lens=cache_lens.focus(lambda x: x.long_range) if cache_lens else None,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
    )

make_ewald_net_charge_potential(particles_view, systems_view, parameter_view, gradient_lens=EMPTY_LENS, hessian_lens=EMPTY_LENS, hessian_idx_view=EMPTY_LENS, patch_idx_view=None, cache_lens=None, *, composition=None)

Neutralizing-background energy of each system's net charge.

Parameters:

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

View of the particle table.

required
systems_view View[State, Table[SystemId, HasCell[Periodic3D]]]

View of the system table and periodic cells.

required
parameter_view View[State, EwaldParameters]

View of Ewald parameters.

required
gradient_lens Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients]

Lens selecting point-cloud variables to differentiate.

EMPTY_LENS
hessian_lens Lens[Gradients, Hessians]

Lens selecting gradient entries to differentiate again.

EMPTY_LENS
hessian_idx_view View[State, Hessians]

View supplying Hessian row and column indices.

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

Optional view supplying indices for output-cache updates.

None
cache_lens Lens[State, KahanSummand[PotentialOut[Gradients, Hessians]]] | None

Optional lens to the compensated net-charge output cache.

None
composition RigidBodyComposition[State, HasCharges] | None

Optional rigid-body input for an energy-only count sum.

None

Returns:

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

Net-charge potential recomputed from all current charges.

Source code in src/kups/potential/classical/ewald/potential.py
def make_ewald_net_charge_potential[
    State,
    Ptch: Patch[Any],
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsEwaldPointData]],
    systems_view: View[State, Table[SystemId, HasCell[Periodic3D]]],
    parameter_view: View[State, EwaldParameters],
    gradient_lens: Lens[
        PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients
    ] = EMPTY_LENS,
    hessian_lens: Lens[Gradients, Hessians] = EMPTY_LENS,
    hessian_idx_view: View[State, Hessians] = EMPTY_LENS,
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    cache_lens: Lens[State, KahanSummand[PotentialOut[Gradients, Hessians]]]
    | None = None,
    *,
    composition: RigidBodyComposition[State, HasCharges] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Neutralizing-background energy of each system's net charge.

    Args:
        particles_view: View of the particle table.
        systems_view: View of the system table and periodic cells.
        parameter_view: View of Ewald parameters.
        gradient_lens: Lens selecting point-cloud variables to differentiate.
        hessian_lens: Lens selecting gradient entries to differentiate again.
        hessian_idx_view: View supplying Hessian row and column indices.
        patch_idx_view: Optional view supplying indices for output-cache updates.
        cache_lens: Optional lens to the compensated net-charge output cache.
        composition: Optional rigid-body input for an energy-only count sum.

    Returns:
        Net-charge potential recomputed from all current charges.
    """
    if composition is not None:
        initial = composition.initial_state
        particles, systems = particles_view(initial), systems_view(initial)
        # Net charges are linear in the counts, so the energy is quadratic.
        charges = composition.sum_particles(
            particles.data.charges, composition.templates.charges
        )
        scale = _net_charge_energy(
            jnp.ones(systems.size),
            parameter_view(initial).alpha[systems.index],
            systems.data.cell.volume,
        )
        return composition.potential(
            scale[:, None, None] * charges[:, :, None] * charges[:, None, :],
            gradient_lens(PointCloud(particles, systems)),
            hessian_lens,
            cache_lens,
            patch_idx_view,
        )
    return PotentialFromEnergy(
        energy_fn=ewald_net_charge_energy,
        composer=FullSumComposer(
            GraphInputConstructor(
                graph_constructor=GraphConstructor(
                    particles=particles_view,
                    systems=systems_view,
                    neighborlist=lambda _: EmptyNeighborList[Literal[0]](),
                    probe=None,
                ),
                parameter_view=parameter_view,
            )
        ),
        gradient_lens=lens(lambda x: x.graph).nest(gradient_lens),
        hessian_lens=hessian_lens,
        cache_lens=cache_lens,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
    )

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, *, composition=None)

Combine real-space, reciprocal-space and self-interaction energies.

Compute all same-system interactions before subtracting optional molecular exclusions.

Parameters:

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

View of particles with charges and inclusion/exclusion groups.

required
systems_view View[State, Table[SystemId, HasCell[Periodic3D]]]

View of the system table and periodic cells.

required
neighborlist_view View[State, NeighborList[Literal[2]]]

View of the real-space pair neighbor list.

required
parameter_lens Lens[State, EwaldParameters]

Lens to Ewald parameters.

required
cache_lens Lens[State, EwaldCache[Gradients, Hessians]] | None

Optional lens to structure-factor and component-output caches.

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

Optional probe returning changed particles.

required
gradient_lens Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients]

Lens selecting point-cloud variables to differentiate.

required
hessian_lens Lens[Gradients, Hessians]

Lens selecting gradient entries to differentiate again.

required
hessian_idx_view View[State, Hessians]

View supplying Hessian row and column indices.

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

Optional view supplying indices for output-cache updates.

None
include_exclusion_mask bool

Whether to subtract molecular-exclusion pair energies.

False
composition RigidBodyComposition[State, IsChargedTemplate] | None

Optional rigid-body input fixing cells and parameters for energy-only evaluation. Large templates retain geometric exclusions.

None

Returns:

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

Combined Ewald potential with four components, or five with exclusions.

Source code in src/kups/potential/classical/ewald/potential.py
def make_ewald_potential[
    State,
    Ptch: Patch[Any],
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsEwaldPointData]],
    systems_view: View[State, Table[SystemId, HasCell[Periodic3D]]],
    neighborlist_view: View[State, NeighborList[Literal[2]]],
    parameter_lens: Lens[State, EwaldParameters],
    cache_lens: Lens[State, EwaldCache[Gradients, Hessians]] | None,
    probe: Probe[State, Ptch, IsParticleProbe[IsEwaldPointData]] | None,
    gradient_lens: Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], 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,
    *,
    composition: RigidBodyComposition[State, IsChargedTemplate] | None = None,
) -> EwaldPotential[State, Gradients, Hessians, Ptch]:
    """Combine real-space, reciprocal-space and self-interaction energies.

    Compute all same-system interactions before subtracting optional molecular
    exclusions.

    Args:
        particles_view: View of particles with charges and inclusion/exclusion groups.
        systems_view: View of the system table and periodic cells.
        neighborlist_view: View of the real-space pair neighbor list.
        parameter_lens: Lens to Ewald parameters.
        cache_lens: Optional lens to structure-factor and component-output caches.
        probe: Optional probe returning changed particles.
        gradient_lens: Lens selecting point-cloud variables to differentiate.
        hessian_lens: Lens selecting gradient entries to differentiate again.
        hessian_idx_view: View supplying Hessian row and column indices.
        patch_idx_view: Optional view supplying indices for output-cache updates.
        include_exclusion_mask: Whether to subtract molecular-exclusion pair energies.
        composition: Optional rigid-body input fixing cells and parameters for
            energy-only evaluation. Large templates retain geometric exclusions.

    Returns:
        Combined Ewald potential with four components, or five with exclusions.
    """
    atomic_view = pipe(particles_view, _ewald_particle_table)
    atomic_probe = _ewald_probe(probe)
    atomic_particles_probe: (
        Probe[State, Ptch, WithIndices[ParticleId, _EwaldAtomicParticles]] | None
    ) = (
        (lambda state, patch: atomic_probe(state, patch).particles)
        if atomic_probe is not None
        else None
    )

    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_systems = systems_view
    lr_parameters: View[State, EwaldParameters] = parameter_lens
    if composition is not None:
        initial = composition.initial_state
        # State buffers may be donated before this potential is traced again.
        lr_systems = constant(jax.tree.map(jnp.copy, systems_view(initial)))
        lr_parameters = constant(jax.tree.map(jnp.copy, parameter_lens(initial)))
    lr_potential = make_ewald_long_range_potential(
        particles_view=atomic_view,
        systems_view=lr_systems,
        parameter_lens=lr_parameters,
        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,
        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,
        composition=composition,
    )

    net_charge_potential = make_ewald_net_charge_potential(
        particles_view=atomic_view,
        systems_view=systems_view,
        parameter_view=parameter_lens,
        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.net_charge) if cache_lens else None,
        composition=composition,
    )
    components = (sr_potential, lr_potential, self_potential, net_charge_potential)
    if not include_exclusion_mask:
        return EwaldPotential(components)

    exclusion_correction = make_ewald_exclusion_correction_potential(
        particles_view=particles_view,
        systems_view=systems_view,
        probe=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.exclusion) if cache_lens else None,
        composition=composition,
    )
    return EwaldPotential((*components, exclusion_correction))

make_ewald_self_interaction_potential(particles_view, systems_view, parameter_view, gradient_lens=EMPTY_LENS, hessian_lens=EMPTY_LENS, hessian_idx_view=EMPTY_LENS, patch_idx_view=None, cache_lens=None, *, composition=None)

Recompute the inexpensive charge-only sum to avoid drift from cached deltas.

Parameters:

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

View of the particle table.

required
systems_view View[State, Table[SystemId, HasCell[Periodic3D]]]

View of the system table and periodic cells.

required
parameter_view View[State, EwaldParameters]

View of Ewald parameters.

required
gradient_lens Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients]

Lens selecting point-cloud variables to differentiate.

EMPTY_LENS
hessian_lens Lens[Gradients, Hessians]

Lens selecting gradient entries to differentiate again.

EMPTY_LENS
hessian_idx_view View[State, Hessians]

View supplying Hessian row and column indices.

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

Optional view supplying indices for output-cache updates.

None
cache_lens Lens[State, KahanSummand[PotentialOut[Gradients, Hessians]]] | None

Optional lens to the compensated self-interaction output cache.

None
composition RigidBodyComposition[State, HasCharges] | None

Optional rigid-body input for an energy-only count sum.

None

Returns:

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

Self-interaction potential recomputed from all current charges.

Source code in src/kups/potential/classical/ewald/potential.py
def make_ewald_self_interaction_potential[
    State,
    Ptch: Patch[Any],
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsEwaldPointData]],
    systems_view: View[State, Table[SystemId, HasCell[Periodic3D]]],
    parameter_view: View[State, EwaldParameters],
    gradient_lens: Lens[
        PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients
    ] = EMPTY_LENS,
    hessian_lens: Lens[Gradients, Hessians] = EMPTY_LENS,
    hessian_idx_view: View[State, Hessians] = EMPTY_LENS,
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    cache_lens: Lens[State, KahanSummand[PotentialOut[Gradients, Hessians]]]
    | None = None,
    *,
    composition: RigidBodyComposition[State, HasCharges] | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Recompute the inexpensive charge-only sum to avoid drift from cached deltas.

    Args:
        particles_view: View of the particle table.
        systems_view: View of the system table and periodic cells.
        parameter_view: View of Ewald parameters.
        gradient_lens: Lens selecting point-cloud variables to differentiate.
        hessian_lens: Lens selecting gradient entries to differentiate again.
        hessian_idx_view: View supplying Hessian row and column indices.
        patch_idx_view: Optional view supplying indices for output-cache updates.
        cache_lens: Optional lens to the compensated self-interaction output cache.
        composition: Optional rigid-body input for an energy-only count sum.

    Returns:
        Self-interaction potential recomputed from all current charges.
    """
    if composition is not None:
        initial = composition.initial_state
        particles, systems = particles_view(initial), systems_view(initial)
        charge_squares = composition.sum_particles(
            particles.data.charges**2, composition.templates.charges**2
        )
        alpha = parameter_view(initial).alpha[systems.index]
        return composition.potential(
            _self_energy(charge_squares, alpha[:, None]),
            gradient_lens(PointCloud(particles, systems)),
            hessian_lens,
            cache_lens,
            patch_idx_view,
        )
    return PotentialFromEnergy(
        energy_fn=ewald_self_interaction_energy,
        composer=FullSumComposer(
            GraphInputConstructor(
                graph_constructor=GraphConstructor(
                    particles=particles_view,
                    systems=systems_view,
                    neighborlist=lambda _: EmptyNeighborList[Literal[0]](),
                    probe=None,
                ),
                parameter_view=parameter_view,
            )
        ),
        gradient_lens=lens(lambda x: x.graph).nest(gradient_lens),
        hessian_lens=hessian_lens,
        cache_lens=cache_lens,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
    )

make_ewald_short_range_potential(particles_view, systems_view, neighborlist_view, parameter_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None, cache_lens=None)

Create the Ewald real-space (short-range) potential.

Parameters:

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

View of the particle table.

required
systems_view View[State, Table[SystemId, HasCell[Periodic3D]]]

View of the system table and periodic cells.

required
neighborlist_view View[State, NeighborList[Literal[2]]]

View of the real-space pair neighbor list.

required
parameter_view View[State, EwaldParameters]

View of Ewald parameters.

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

Optional probe returning changed particles.

required
gradient_lens Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients]

Lens selecting point-cloud variables to differentiate.

required
hessian_lens Lens[Gradients, Hessians]

Lens selecting gradient entries to differentiate again.

required
hessian_idx_view View[State, Hessians]

View supplying Hessian row and column indices.

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

Optional view supplying indices for output-cache updates.

None
cache_lens Lens[State, KahanSummand[PotentialOut[Gradients, Hessians]]] | None

Optional lens to the compensated short-range output cache.

None

Returns:

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

Real-space potential using the pair-graph evaluator.

Source code in src/kups/potential/classical/ewald/potential.py
def make_ewald_short_range_potential[
    State,
    Ptch: Patch[Any],
    Gradients,
    Hessians,
](
    particles_view: View[State, Table[ParticleId, IsEwaldPointData]],
    systems_view: View[State, Table[SystemId, HasCell[Periodic3D]]],
    neighborlist_view: View[State, NeighborList[Literal[2]]],
    parameter_view: View[State, EwaldParameters],
    probe: Probe[State, Ptch, IsParticleProbe[IsEwaldPointData]] | None,
    gradient_lens: Lens[PointCloud[IsEwaldPointData, HasCell[Periodic3D]], Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
    cache_lens: Lens[State, KahanSummand[PotentialOut[Gradients, Hessians]]]
    | None = None,
) -> Potential[State, Gradients, Hessians, Ptch]:
    """Create the Ewald real-space (short-range) potential.

    Args:
        particles_view: View of the particle table.
        systems_view: View of the system table and periodic cells.
        neighborlist_view: View of the real-space pair neighbor list.
        parameter_view: View of Ewald parameters.
        probe: Optional probe returning changed particles.
        gradient_lens: Lens selecting point-cloud variables to differentiate.
        hessian_lens: Lens selecting gradient entries to differentiate again.
        hessian_idx_view: View supplying Hessian row and column indices.
        patch_idx_view: Optional view supplying indices for output-cache updates.
        cache_lens: Optional lens to the compensated short-range output cache.

    Returns:
        Real-space potential using the pair-graph evaluator.
    """

    return PotentialFromEnergy(
        energy_fn=ewald_short_range_energy,
        composer=LocalSumComposer(
            GraphInputConstructor(
                graph_constructor=GraphConstructor(
                    particles=particles_view,
                    systems=systems_view,
                    neighborlist=neighborlist_view,
                    probe=probe,
                ),
                parameter_view=parameter_view,
            )
        ),
        gradient_lens=lens(lambda x: x.graph).nest(gradient_lens),
        hessian_lens=hessian_lens,
        cache_lens=cache_lens,
        hessian_idx_view=hessian_idx_view,
        patch_idx_view=patch_idx_view,
    )

prefactor(inp)

Weights 2π/V exp(-k²/(4α²))/k², zero outside the reciprocal cutoff.

Double contributions with nonzero first lattice coefficient for the omitted half-space; the zero plane already contains both signs.

Parameters:

Name Type Description Default
inp EwaldLongRangeInput[Any]

System cells, screening parameters and reciprocal cutoffs.

required

Returns:

Type Description
Array

Weights shaped (n_systems, n_kvecs); zero for k=0 and masked vectors.

Source code in src/kups/potential/classical/ewald/potential.py
def prefactor(inp: EwaldLongRangeInput[Any]) -> Array:
    """Weights ``2π/V exp(-k²/(4α²))/k²``, zero outside the reciprocal cutoff.

    Double contributions with nonzero first lattice coefficient for the omitted
    half-space; the zero plane already contains both signs.

    Args:
        inp: System cells, screening parameters and reciprocal cutoffs.

    Returns:
        Weights shaped ``(n_systems, n_kvecs)``; zero for k=0 and masked vectors.
    """
    return _prefactor(*_prefactor_args(inp))

reciprocal_grid_bound(cell, kmax)

Uniform coefficient bound enclosing the reciprocal kmax sphere.

Parameters:

Name Type Description Default
cell Cell[AnyPeriodicity]

Cell defining the reciprocal lattice.

required
kmax float

Maximum reciprocal-vector magnitude in 1/Å.

required

Returns:

Type Description
int

Integer bound enclosing the reciprocal cutoff sphere.

Source code in src/kups/potential/classical/ewald/parameters.py
@no_jax_tracing
def reciprocal_grid_bound(cell: Cell[AnyPeriodicity], kmax: float) -> int:
    """Uniform coefficient bound enclosing the reciprocal kmax sphere.

    Args:
        cell: Cell defining the reciprocal lattice.
        kmax: Maximum reciprocal-vector magnitude in 1/Å.

    Returns:
        Integer bound enclosing the reciprocal cutoff sphere.
    """
    min_length = np.linalg.svd(_reciprocal_vectors(cell), compute_uv=False).min()
    return int(np.ceil(kmax / min_length))

reciprocal_grid_index(shifts, bound)

Flat positions of shifts in reciprocal_grid_shifts(bound).

Parameters:

Name Type Description Default
shifts Array

Integer shifts inside bound, shaped (..., n_kvecs, 3).

required
bound ReciprocalGridBound

Grid axis bounds.

required

Returns:

Type Description
Array | ndarray

Grid indices shaped (..., n_kvecs); a NumPy array for concrete

Array | ndarray

shifts, so kernels can specialize on them.

Source code in src/kups/potential/classical/ewald/parameters.py
def reciprocal_grid_index(
    shifts: Array, bound: ReciprocalGridBound
) -> Array | np.ndarray:
    """Flat positions of shifts in ``reciprocal_grid_shifts(bound)``.

    Args:
        shifts: Integer shifts inside ``bound``, shaped ``(..., n_kvecs, 3)``.
        bound: Grid axis bounds.

    Returns:
        Grid indices shaped ``(..., n_kvecs)``; a NumPy array for concrete
        shifts, so kernels can specialize on them.
    """
    _, by, bz = bound
    xp = jnp if is_traced(shifts) else np
    ints = xp.asarray(shifts).astype(np.int32)
    return (ints[..., 0] * (2 * by + 1) + ints[..., 1] + by) * (2 * bz + 1) + (
        ints[..., 2] + bz
    )

reciprocal_grid_shifts(bound)

Integer half-space grid: nonnegative x, signed y/z, x-major.

Parameters:

Name Type Description Default
bound int | ReciprocalGridBound

Nonnegative maximum absolute lattice coefficients, or one bound for all three axes.

required

Returns:

Type Description
ndarray

Integer coefficients shaped ((bx + 1) * (2*by + 1) * (2*bz + 1), 3).

Source code in src/kups/potential/classical/ewald/parameters.py
def reciprocal_grid_shifts(bound: int | ReciprocalGridBound) -> np.ndarray:
    """Integer half-space grid: nonnegative x, signed y/z, x-major.

    Args:
        bound: Nonnegative maximum absolute lattice coefficients, or one bound
            for all three axes.

    Returns:
        Integer coefficients shaped ``((bx + 1) * (2*by + 1) * (2*bz + 1), 3)``.
    """
    bx, by, bz = (bound, bound, bound) if isinstance(bound, int) else bound
    axes = (
        np.arange(0, bx + 1),
        np.arange(-by, by + 1),
        np.arange(-bz, bz + 1),
    )
    return np.stack(np.meshgrid(*axes, indexing="ij"), axis=-1).reshape(-1, 3)

structure_factor(inp)

Return the full or incrementally updated structure factor and cache patch.

Parameters:

Name Type Description Default
inp EwaldLongRangeInput[State]

Current particles and optional cache and previous particle values.

required

Returns:

Type Description
KahanSummand[Array]

Compensated structure factors shaped (n_systems, n_kvecs, 2) and

Patch[State]

an acceptance patch, or an identity patch when no cache lens is supplied.

Source code in src/kups/potential/classical/ewald/potential.py
def structure_factor[State](
    inp: EwaldLongRangeInput[State],
) -> tuple[KahanSummand[Array], Patch[State]]:
    """Return the full or incrementally updated structure factor and cache patch.

    Args:
        inp: Current particles and optional cache and previous particle values.

    Returns:
        Compensated structure factors shaped ``(n_systems, n_kvecs, 2)`` and
        an acceptance patch, or an identity patch when no cache lens is supplied.
    """
    if inp.changes_from_prev is None:
        primals, static = _structure_factor_args(inp)
        sk = KahanSummand.init(_structure_factor(*primals, *static))
        return sk, _cache_patch(inp, sk)
    assert inp.cache is not None, "Cache required for structure factor update"
    delta = structure_factor_delta(inp)
    return inp.cache.structure_factor + delta, _cache_patch(inp, delta)