Skip to content

kups.potential.classical.blocking

Blocking sphere potential for excluded volume constraints.

This module implements hard-sphere repulsion using blocking spheres that create infinite energy barriers. Useful for preventing particle overlap with framework atoms in porous materials (e.g., zeolites, MOFs) or enforcing geometric constraints.

Particles inside blocking spheres experience infinite repulsion, automatically rejecting Monte Carlo moves that violate spatial constraints.

BlockingSpheresParameters

Parameters defining blocking sphere positions and radii.

Attributes:

Name Type Description
radii Array

Sphere radii, shape (n_spheres,)

positions Array

Sphere centers, shape (n_spheres, 3)

system Index[SystemId]

System assignment per sphere

motif Index[MotifId]

Motif assignment per sphere

Source code in src/kups/potential/classical/blocking.py
@dataclass
class BlockingSpheresParameters:
    """Parameters defining blocking sphere positions and radii.

    Attributes:
        radii: Sphere radii, shape `(n_spheres,)`
        positions: Sphere centers, shape `(n_spheres, 3)`
        system: System assignment per sphere
        motif: Motif assignment per sphere
    """

    radii: Array
    positions: Array
    system: Index[SystemId]
    motif: Index[MotifId]

    def __post_init__(self):
        if isinstance(self.radii, Array):
            assert (*self.radii.shape, 3) == self.positions.shape

BlockingSpheresPotentialInput

Input for blocking spheres energy calculation.

Attributes:

Name Type Description
parameters BlockingSpheresParameters

Blocking sphere positions and radii

particles Table[ParticleId, Points]

Indexed particle data with positions, system, and motif index

unitcell MaybeUnitCell

Optional unit cell for periodic boundary conditions

edges Edges[Literal[2]]

Particle-sphere pairs to check for blocking

Source code in src/kups/potential/classical/blocking.py
@dataclass
class BlockingSpheresPotentialInput[
    Points: _BlockingParticles,
    MaybeUnitCell: UnitCell | None,
]:
    """Input for blocking spheres energy calculation.

    Attributes:
        parameters: Blocking sphere positions and radii
        particles: Indexed particle data with positions, system, and motif index
        unitcell: Optional unit cell for periodic boundary conditions
        edges: Particle-sphere pairs to check for blocking
    """

    parameters: BlockingSpheresParameters
    particles: Table[ParticleId, Points]
    unitcell: MaybeUnitCell
    edges: Edges[Literal[2]]

BlockingSpheresSumComposer

Bases: SumComposer[State, BlockingSpheresPotentialInput[Points, UnitCell], Ptch]

Composer for blocking spheres potential in energy summation.

Attributes:

Name Type Description
particles_view View[State, Table[ParticleId, Points]]

Extracts indexed particle data from state

systems_view View[State, Table[SystemId, S]]

Extracts indexed systems from state

parameters_view View[State, BlockingSpheresParameters]

Extracts blocking sphere parameters from state

neighborlist_view View[State, NearestNeighborList]

Extracts neighbor list instance from state

probe Probe[State, Ptch, IsBlockingSpheresProbe] | None

Probe providing a IsBlockingSpheresProbe

Source code in src/kups/potential/classical/blocking.py
@dataclass
class BlockingSpheresSumComposer[
    State,
    Ptch: Patch,
    S: HasUnitCell,
    Points: _BlockingParticles,
](SumComposer[State, BlockingSpheresPotentialInput[Points, UnitCell], Ptch]):
    """Composer for blocking spheres potential in energy summation.

    Attributes:
        particles_view: Extracts indexed particle data from state
        systems_view: Extracts indexed systems from state
        parameters_view: Extracts blocking sphere parameters from state
        neighborlist_view: Extracts neighbor list instance from state
        probe: Probe providing a IsBlockingSpheresProbe
    """

    particles_view: View[State, Table[ParticleId, Points]] = field(static=True)
    systems_view: View[State, Table[SystemId, S]] = field(static=True)
    parameters_view: View[State, BlockingSpheresParameters] = field(static=True)
    neighborlist_view: View[State, NearestNeighborList] = field(static=True)
    probe: Probe[State, Ptch, IsBlockingSpheresProbe] | None = field(static=True)

    def __call__(self, state: State, patch: Ptch | None):  # type: ignore[reportReturnType]
        particles = self.particles_view(state)
        systems = self.systems_view(state)
        parameters = self.parameters_view(state)
        neighborlist = self.neighborlist_view(state)

        if patch is not None and self.probe is not None:
            n_sys = particles.data.system.num_labels
            patched_state = patch(
                state, systems.set_data(jnp.ones((n_sys,), dtype=jnp.bool_))
            )
            probe_result = self.probe(state, patch)
            neighborlist = probe_result.neighborlist
            particles = self.particles_view(patched_state)

        # Build cutoffs: remap sphere system indices into systems index space
        seg_ids = parameters.system.indices_in(tuple(systems.keys))
        max_radii = jax.ops.segment_max(parameters.radii, seg_ids, len(systems.keys))
        cutoffs = Table(systems.keys, max_radii)

        # Build sphere rh as Indexed[ParticleId, _BlockingSpherePoints]
        p = parameters.positions.shape[0]
        sphere_inclusion = Index(
            tuple(InclusionId(lab) for lab in parameters.system.keys),
            parameters.system.indices,
        )
        sphere_exclusion = Index(
            tuple(ExclusionId(-1 - i) for i in range(p)),
            jnp.arange(p),
        )
        spheres = Table.arange(
            _BlockingSpherePoints(
                positions=parameters.positions,
                system=parameters.system,
                inclusion=sphere_inclusion,
                exclusion=sphere_exclusion,
            ),
            label=ParticleId,
        )

        edges = neighborlist(particles, spheres, systems, cutoffs)
        unitcell = systems.data.unitcell
        return Sum(
            Summand(
                BlockingSpheresPotentialInput(parameters, particles, unitcell, edges)
            )
        )

IsBlockingSpheresProbe

Bases: Protocol

Probe result for blocking spheres incremental updates.

Bundles changed particle indices with the updated neighbor list, enabling efficient re-evaluation when only a subset of particles move.

Source code in src/kups/potential/classical/blocking.py
class IsBlockingSpheresProbe(Protocol):
    """Probe result for blocking spheres incremental updates.

    Bundles changed particle indices with the updated neighbor list,
    enabling efficient re-evaluation when only a subset of particles move.
    """

    @property
    def changed_particle_idx(self) -> Array: ...
    @property
    def neighborlist(self) -> NearestNeighborList: ...

IsBlockingSpheresState

Bases: Protocol

Protocol for states providing all inputs for the blocking spheres potential.

Source code in src/kups/potential/classical/blocking.py
class IsBlockingSpheresState(Protocol):
    """Protocol for states providing all inputs for the blocking spheres potential."""

    @property
    def particles(self) -> Table[ParticleId, _BlockingParticles]: ...
    @property
    def systems(self) -> Table[SystemId, HasUnitCell]: ...
    @property
    def blocking_spheres_parameters(self) -> BlockingSpheresParameters: ...
    @property
    def blocking_spheres_neighborlist(self) -> NearestNeighborList: ...

blocking_spheres_energy(inp)

Calculate blocking spheres potential energy.

Returns infinite energy for particles inside blocking spheres.

Parameters:

Name Type Description Default
inp BlockingSpheresPotentialInput[Points, UC]

Potential input containing particles, spheres, and edges

required

Returns:

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

Energy and patch with infinite energy for blocked particles.

Source code in src/kups/potential/classical/blocking.py
def blocking_spheres_energy[
    Points: _BlockingParticles,
    UC: UnitCell | None,
](
    inp: BlockingSpheresPotentialInput[Points, UC],
) -> WithPatch[Table[SystemId, Energy], IdPatch]:
    """Calculate blocking spheres potential energy.

    Returns infinite energy for particles inside blocking spheres.

    Args:
        inp: Potential input containing particles, spheres, and edges

    Returns:
        Energy and patch with infinite energy for blocked particles.
    """
    edge_idx = inp.edges.indices.indices
    particle_idx, sph_idx = edge_idx[:, 0], edge_idx[:, 1]
    batch_data = inp.particles.data.system.indices[particle_idx]
    diffs = (
        inp.particles.data.positions[particle_idx] - inp.parameters.positions[sph_idx]
    )
    if inp.unitcell is not None:
        cell = inp.unitcell[batch_data]
        diffs = cell.wrap(diffs)
    dists = jnp.linalg.norm(diffs, axis=-1)
    radii = inp.parameters.radii[sph_idx]
    raw_energies = jnp.where(
        (dists < radii)
        & (
            inp.particles.data.motif.indices[particle_idx]
            == inp.parameters.motif.indices[sph_idx]
        ),
        jnp.inf,
        0.0,
    )
    batch_idx = Index(inp.particles.data.system.keys, batch_data)
    energies = batch_idx.sum_over(raw_energies)
    return WithPatch(energies, IdPatch())

make_blocking_spheres_from_state(state, probe=None, *, compute_position_and_unitcell_gradients=False)

make_blocking_spheres_from_state(
    state: Lens[State, IsBlockingSpheresState],
    probe: None = None,
    *,
    compute_position_and_unitcell_gradients: Literal[
        False
    ] = ...,
) -> Potential[State, EmptyType, EmptyType, Any]
make_blocking_spheres_from_state(
    state: Lens[State, IsBlockingSpheresState],
    probe: None = None,
    *,
    compute_position_and_unitcell_gradients: Literal[True],
) -> Potential[State, PositionAndUnitCell, EmptyType, Any]
make_blocking_spheres_from_state(
    state: Lens[State, IsBlockingSpheresState],
    probe: Probe[State, P, IsBlockingSpheresProbe],
    *,
    compute_position_and_unitcell_gradients: Literal[
        False
    ] = ...,
) -> Potential[State, EmptyType, EmptyType, P]
make_blocking_spheres_from_state(
    state: Lens[State, IsBlockingSpheresState],
    probe: Probe[State, P, IsBlockingSpheresProbe],
    *,
    compute_position_and_unitcell_gradients: Literal[True],
) -> Potential[State, PositionAndUnitCell, EmptyType, P]

Create a blocking spheres potential, optionally with incremental updates.

Parameters:

Name Type Description Default
state Any

Lens into the sub-state providing particles, systems, blocking sphere parameters, and neighbor list.

required
probe Any

Probe returning a IsBlockingSpheresProbe; None for full recomputation.

None
compute_position_and_unitcell_gradients bool

When True, compute gradients w.r.t. particle positions and lattice vectors.

False

Returns:

Type Description
Any

Configured blocking spheres Potential.

Source code in src/kups/potential/classical/blocking.py
def make_blocking_spheres_from_state(
    state: Any,
    probe: Any = None,
    *,
    compute_position_and_unitcell_gradients: bool = False,
) -> Any:
    """Create a blocking spheres potential, optionally with incremental updates.

    Args:
        state: Lens into the sub-state providing particles, systems, blocking sphere
            parameters, and neighbor list.
        probe: Probe returning a IsBlockingSpheresProbe; ``None`` for full
            recomputation.
        compute_position_and_unitcell_gradients: When ``True``, compute gradients
            w.r.t. particle positions and lattice vectors.

    Returns:
        Configured blocking spheres Potential.
    """
    gradient_lens: Any = EMPTY_LENS
    patch_idx_view: Any = None
    if compute_position_and_unitcell_gradients:
        gradient_lens = SimpleLens[BlockingSpheresPotentialInput, PositionAndUnitCell](
            lambda x: PositionAndUnitCell(
                x.particles.map_data(lambda p: p.positions),
                Table(x.particles.data.system.keys, x.unitcell),
            )
        )
        patch_idx_view = position_and_unitcell_idx_view
    if probe is not None:
        patch_idx_view = patch_idx_view or empty_patch_idx_view
    return make_blocking_spheres_potential(
        state.focus(lambda x: x.particles),
        state.focus(lambda x: x.systems),
        state.focus(lambda x: x.blocking_spheres_parameters),
        state.focus(lambda x: x.blocking_spheres_neighborlist),
        probe,
        gradient_lens,
        EMPTY_LENS,
        EMPTY_LENS,
        patch_idx_view,
    )

make_blocking_spheres_potential(particles_view, systems_view, parameters_view, neighborlist_view, probe, gradient_lens, hessian_lens, hessian_idx_view, patch_idx_view=None)

Create blocking sphere potential for excluded volume constraints.

Parameters:

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

Extracts indexed particle data from state

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

Extracts indexed systems from state

required
parameters_view View[State, BlockingSpheresParameters]

Extracts blocking sphere parameters (positions, radii)

required
neighborlist_view View[State, NearestNeighborList]

Extracts neighbor list instance

required
probe Probe[State, Ptch, IsBlockingSpheresProbe] | None

Probe returning a IsBlockingSpheresProbe; None for full recomputation

required
gradient_lens Lens[BlockingSpheresPotentialInput[Points, UnitCell], 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

Returns:

Type Description
PotentialFromEnergy[State, BlockingSpheresPotentialInput[Points, UnitCell], Gradients, Hessians, Ptch]

Blocking sphere potential.

Source code in src/kups/potential/classical/blocking.py
def make_blocking_spheres_potential[
    State,
    Gradients,
    Hessians,
    Ptch: Patch,
    Points: _BlockingParticles,
    S: HasUnitCell,
](
    particles_view: View[State, Table[ParticleId, Points]],
    systems_view: View[State, Table[SystemId, S]],
    parameters_view: View[State, BlockingSpheresParameters],
    neighborlist_view: View[State, NearestNeighborList],
    probe: Probe[State, Ptch, IsBlockingSpheresProbe] | None,
    gradient_lens: Lens[BlockingSpheresPotentialInput[Points, UnitCell], Gradients],
    hessian_lens: Lens[Gradients, Hessians],
    hessian_idx_view: View[State, Hessians],
    patch_idx_view: View[State, PotentialOut[Gradients, Hessians]] | None = None,
) -> PotentialFromEnergy[
    State,
    BlockingSpheresPotentialInput[Points, UnitCell],
    Gradients,
    Hessians,
    Ptch,
]:
    """Create blocking sphere potential for excluded volume constraints.

    Args:
        particles_view: Extracts indexed particle data from state
        systems_view: Extracts indexed systems from state
        parameters_view: Extracts blocking sphere parameters (positions, radii)
        neighborlist_view: Extracts neighbor list instance
        probe: Probe returning a IsBlockingSpheresProbe; ``None`` for full recomputation
        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

    Returns:
        Blocking sphere potential.
    """
    return PotentialFromEnergy(
        blocking_spheres_energy,
        BlockingSpheresSumComposer(
            particles_view=particles_view,
            systems_view=systems_view,
            parameters_view=parameters_view,
            neighborlist_view=neighborlist_view,
            probe=probe,
        ),
        hessian_idx_view=hessian_idx_view,
        hessian_lens=hessian_lens,
        gradient_lens=gradient_lens,
        patch_idx_view=patch_idx_view,
        cache_lens=None,
    )