Skip to content

kups.application.mcmc

IsMCMCFixedData

Bases: Protocol

Contract for data from the fixed reader group.

Source code in src/kups/application/mcmc/analysis.py
class IsMCMCFixedData(Protocol):
    """Contract for data from the fixed reader group."""

    @property
    def systems(self) -> Table[SystemId, HasTemperature]: ...

IsMCMCState

Bases: Protocol

Protocol for states compatible with MCMC logging.

Source code in src/kups/application/mcmc/logging.py
class IsMCMCState(Protocol):
    """Protocol for states compatible with MCMC logging."""

    @property
    def particles(self) -> Buffered[ParticleId, MCMCParticles]: ...
    @property
    def groups(self) -> Buffered[GroupId, MCMCGroup]: ...
    @property
    def systems(self) -> Table[SystemId, MCMCSystems]: ...
    @property
    def motifs(self) -> Table[MotifParticleId, MotifParticles]: ...
    @property
    def translation_params(self) -> Table[SystemId, ParameterSchedulerState]: ...
    @property
    def rotation_params(self) -> Table[SystemId, ParameterSchedulerState]: ...
    @property
    def reinsertion_params(self) -> Table[SystemId, ParameterSchedulerState]: ...
    @property
    def exchange_params(self) -> Table[SystemId, ParameterSchedulerState]: ...
    @property
    def lj_parameters(
        self,
    ) -> WithCache[
        GlobalTailCorrectedLennardJonesParameters, PotentialOut[EmptyType, EmptyType]
    ]: ...
    @property
    def ewald_parameters(
        self,
    ) -> WithCache[EwaldParameters, EwaldCache[EmptyType, EmptyType]]: ...

IsMCMCStepData

Bases: Protocol

Contract for data from the per_step reader group.

Source code in src/kups/application/mcmc/analysis.py
class IsMCMCStepData(Protocol):
    """Contract for data from the per_step reader group."""

    @property
    def particle_count(self) -> Table[tuple[SystemId, MotifId], Array]: ...
    @property
    def systems(self) -> Table[SystemId, IsMCMCSystemStepData]: ...

IsMCMCSystemStepData

Bases: Protocol

Contract for per-system step data.

Source code in src/kups/application/mcmc/analysis.py
class IsMCMCSystemStepData(Protocol):
    """Contract for per-system step data."""

    @property
    def potential_energy(self) -> Array: ...
    @property
    def guest_stress(self) -> StressResult: ...

MCMCAnalysisResult dataclass

Results from MCMC simulation analysis for a single system.

Attributes:

Name Type Description
energy BlockAverageResult

Average total potential energy with SEM (eV).

loading BlockAverageResult

Average particle count per species with SEM (dimensionless).

heat_of_adsorption BlockAverageResult

Per-species heat of adsorption with SEM (eV).

total_heat_of_adsorption BlockAverageResult

Composition-weighted total heat of adsorption (eV).

Source code in src/kups/application/mcmc/analysis.py
@plain_dataclass
class MCMCAnalysisResult:
    """Results from MCMC simulation analysis for a single system.

    Attributes:
        energy: Average total potential energy with SEM (eV).
        loading: Average particle count per species with SEM (dimensionless).
        heat_of_adsorption: Per-species heat of adsorption with SEM (eV).
        total_heat_of_adsorption: Composition-weighted total heat of adsorption (eV).
    """

    energy: BlockAverageResult
    loading: BlockAverageResult
    heat_of_adsorption: BlockAverageResult
    total_heat_of_adsorption: BlockAverageResult
    stress: BlockAverageResult | None = None
    stress_potential: BlockAverageResult | None = None
    stress_tail_correction: BlockAverageResult | None = None
    stress_ideal_gas: BlockAverageResult | None = None
    pressure: BlockAverageResult | None = None
    pressure_potential: BlockAverageResult | None = None
    pressure_tail_correction: BlockAverageResult | None = None
    pressure_ideal_gas: BlockAverageResult | None = None

MCMCGroup

Molecular group state for rigid-body MCMC.

Attributes:

Name Type Description
system Index[SystemId]

Index mapping each group to its parent system.

motif Index[MotifId]

Index mapping each group to its adsorbate species.

Source code in src/kups/application/mcmc/data.py
@dataclass
class MCMCGroup:
    """Molecular group state for rigid-body MCMC.

    Attributes:
        system: Index mapping each group to its parent system.
        motif: Index mapping each group to its adsorbate species.
    """

    system: Index[SystemId]
    motif: Index[MotifId]

    @property
    def labels(self) -> Index[Label]:
        """Group labels derived from motif index."""
        return Index(tuple(Label(str(m)) for m in self.motif.keys), self.motif.indices)

labels property

Group labels derived from motif index.

MCMCLoggedData

HDF5 writer group layout: one-shot host data + per-cycle adsorbate data.

Source code in src/kups/application/mcmc/logging.py
@dataclass
class MCMCLoggedData[S]:
    """HDF5 writer group layout: one-shot host data + per-cycle adsorbate data."""

    fixed: WriterGroupConfig[S, MCMCFixedData]
    per_step: WriterGroupConfig[S, MCMCStepData]

MCMCParticles

Bases: Particles

Particle state for rigid-body MCMC with group membership.

Attributes:

Name Type Description
group Index[GroupId]

Index mapping each particle to its molecular group.

Source code in src/kups/application/mcmc/data.py
@dataclass
class MCMCParticles(Particles):
    """Particle state for rigid-body MCMC with group membership.

    Attributes:
        group: Index mapping each particle to its molecular group.
    """

    group: Index[GroupId]
    motif: Index[MotifParticleId]
    position_gradients: Array = field(default=None)  # type: ignore[assignment]

    def __post_init__(self):
        if self.position_gradients is None:
            object.__setattr__(
                self, "position_gradients", jnp.zeros_like(self.positions)
            )

    @property
    def inclusion(self) -> Index[InclusionId]:
        """Inclusion index derived from the system index."""
        return Index(
            tuple(InclusionId(lab) for lab in self.system.keys),
            self.system.indices,
            self.system.max_count,
        )

    @property
    def exclusion(self) -> Index[ExclusionId]:
        """Exclusion index derived from the group index."""
        return Index(
            tuple(ExclusionId(lab) for lab in self.group.keys),
            self.group.indices,
            self.group.max_count,
        )

    def guest_only(self) -> MCMCParticles:
        # Buffered sanitizes the the data to only include particles with valid group membership.
        return Buffered.arange(self, view=lambda x: x.group).data

exclusion property

Exclusion index derived from the group index.

inclusion property

Inclusion index derived from the system index.

MCMCSystemStepData

Per-system, per-cycle data: energies, move statistics, and stress.

Source code in src/kups/application/mcmc/logging.py
@dataclass
class MCMCSystemStepData:
    """Per-system, per-cycle data: energies, move statistics, and stress."""

    potential_energy: Array
    lj_energy: Array
    ewald_short_range_energy: Array
    ewald_long_range_energy: Array
    ewald_self_energy: Array
    ewald_exclusion_energy: Array
    translation_step_width: Array
    rotation_step_width: Array
    translation_acceptance: Array
    rotation_acceptance: Array
    reinsertion_acceptance: Array
    exchange_acceptance: Array
    guest_stress: StressResult

MCMCSystems

Per-system thermodynamic state for MCMC simulations.

Attributes:

Name Type Description
unitcell UnitCell

Unit cell geometry, batched shape (n_systems,).

temperature Array

Temperature (K), shape (n_systems,).

potential_energy Array

Total potential energy per system (eV), shape (n_systems,).

log_fugacity Array

Log fugacity per species (dimensionless), shape (n_systems, n_species).

Source code in src/kups/application/mcmc/data.py
@dataclass
class MCMCSystems:
    """Per-system thermodynamic state for MCMC simulations.

    Attributes:
        unitcell: Unit cell geometry, batched shape ``(n_systems,)``.
        temperature: Temperature (K), shape ``(n_systems,)``.
        potential_energy: Total potential energy per system (eV), shape ``(n_systems,)``.
        log_fugacity: Log fugacity per species (dimensionless), shape ``(n_systems, n_species)``.
    """

    unitcell: UnitCell
    temperature: Array
    potential_energy: Array
    log_fugacity: Array
    unitcell_gradients: UnitCell = field(default=None)  # type: ignore[assignment]

    def __post_init__(self):
        if self.unitcell_gradients is None:
            object.__setattr__(
                self, "unitcell_gradients", tree_zeros_like(self.unitcell)
            )

    @property
    def log_activity(self) -> Array:
        """Log activity: log(fugacity / kT), shape ``(n_systems, n_species)``."""
        return (
            self.log_fugacity - jnp.log(self.temperature * BOLTZMANN_CONSTANT)[:, None]
        )

log_activity property

Log activity: log(fugacity / kT), shape (n_systems, n_species).

RunConfig

Bases: BaseModel

Run configuration for MCMC simulation.

Source code in src/kups/application/mcmc/data.py
class RunConfig(BaseModel):
    """Run configuration for MCMC simulation."""

    out_file: str | Path
    """Path to the output HDF5 file."""
    num_cycles: int
    """Number of production cycles."""
    num_warmup_cycles: int
    """Number of warmup cycles (not written to output)."""
    min_cycle_length: int
    translation_prob: float = 1 / 6
    rotation_prob: float = 1 / 6
    reinsertion_prob: float = 1 / 6
    exchange_prob: float = 1 / 2
    """Ordered move sequence for the palindrome propagator."""
    seed: int | None = None
    """Random seed. ``None`` uses a time-based seed."""

exchange_prob = 1 / 2 class-attribute instance-attribute

Ordered move sequence for the palindrome propagator.

num_cycles instance-attribute

Number of production cycles.

num_warmup_cycles instance-attribute

Number of warmup cycles (not written to output).

out_file instance-attribute

Path to the output HDF5 file.

seed = None class-attribute instance-attribute

Random seed. None uses a time-based seed.

analyze_mcmc(fixed, per_step, n_blocks=None)

Analyze MCMC simulation results from pre-loaded data.

Computes energy, loading (average particle counts), and heat of adsorption per species using block averaging for error estimation. Analysis is performed independently for each system.

Parameters:

Name Type Description Default
fixed IsMCMCFixedData

Fixed (one-shot) logged data containing system metadata.

required
per_step IsMCMCStepData

Per-step logged data containing energies and particle counts.

required
n_blocks int | None

Number of blocks for error estimation. None uses :func:~kups.core.utils.block_average.optimal_block_average.

None

Returns:

Type Description
dict[SystemId, MCMCAnalysisResult]

Per-system analysis results keyed by SystemId.

Source code in src/kups/application/mcmc/analysis.py
@no_jax_tracing
def analyze_mcmc(
    fixed: IsMCMCFixedData,
    per_step: IsMCMCStepData,
    n_blocks: int | None = None,
) -> dict[SystemId, MCMCAnalysisResult]:
    """Analyze MCMC simulation results from pre-loaded data.

    Computes energy, loading (average particle counts), and heat of adsorption
    per species using block averaging for error estimation. Analysis is
    performed independently for each system.

    Args:
        fixed: Fixed (one-shot) logged data containing system metadata.
        per_step: Per-step logged data containing energies and particle counts.
        n_blocks: Number of blocks for error estimation. ``None`` uses
            :func:`~kups.core.utils.block_average.optimal_block_average`.

    Returns:
        Per-system analysis results keyed by ``SystemId``.
    """
    system_keys = fixed.systems.keys
    count_keys = per_step.particle_count.keys
    count_data = per_step.particle_count.data
    temperatures = fixed.systems.data.temperature
    all_energy = per_step.systems.data.potential_energy
    guest_stress = per_step.systems.data.guest_stress
    stress_potential = guest_stress.potential
    stress_tail = guest_stress.tail_correction
    stress_ideal = guest_stress.ideal_gas
    all_stress = stress_potential + stress_tail + stress_ideal

    results: dict[SystemId, MCMCAnalysisResult] = {}
    for i, sys_id in enumerate(system_keys):
        motif_cols = [j for j, label in enumerate(count_keys) if label[0] == sys_id]
        counts = count_data[:, motif_cols].reshape(-1, len(motif_cols))
        energy = all_energy[:, i].reshape(-1)
        s = all_stress[:, i]
        has_stress = bool(jnp.any(s != 0))
        temperature = float(temperatures[i])
        results[sys_id] = _analyze_single_system(
            energy,
            counts,
            temperature,
            n_blocks,
            stress=s if has_stress else None,
            stress_potential=stress_potential[:, i]
            if has_stress and stress_potential is not None
            else None,
            stress_tail_correction=stress_tail[:, i]
            if has_stress and stress_tail is not None
            else None,
            stress_ideal_gas=stress_ideal[:, i]
            if has_stress and stress_ideal is not None
            else None,
        )

    return results

analyze_mcmc_file(hdf5_path, n_blocks=None)

Analyze MCMC simulation results from an HDF5 file.

Convenience wrapper that reads the HDF5 file and delegates to :func:analyze_mcmc.

Parameters:

Name Type Description Default
hdf5_path str | Path

Path to HDF5 output file from :func:~kups.application.mcmc.simulation.run_mcmc.

required
n_blocks int | None

Number of blocks for error estimation. None uses :func:~kups.core.utils.block_average.optimal_block_average.

None

Returns:

Type Description
dict[SystemId, MCMCAnalysisResult]

Per-system analysis results keyed by SystemId.

Source code in src/kups/application/mcmc/analysis.py
@no_jax_tracing
def analyze_mcmc_file(
    hdf5_path: str | Path,
    n_blocks: int | None = None,
) -> dict[SystemId, MCMCAnalysisResult]:
    """Analyze MCMC simulation results from an HDF5 file.

    Convenience wrapper that reads the HDF5 file and delegates to
    :func:`analyze_mcmc`.

    Args:
        hdf5_path: Path to HDF5 output file from
            :func:`~kups.application.mcmc.simulation.run_mcmc`.
        n_blocks: Number of blocks for error estimation. ``None`` uses
            :func:`~kups.core.utils.block_average.optimal_block_average`.

    Returns:
        Per-system analysis results keyed by ``SystemId``.
    """
    with HDF5StorageReader[MCMCLoggedData](hdf5_path) as reader:
        fixed = reader.focus_group(lambda s: s.fixed)[...]
        per_step = reader.focus_group(lambda s: s.per_step)[...]

    return analyze_mcmc(fixed, per_step, n_blocks)

make_mcmc_logged_data(state, stress_fn=None)

Create MCMC logging config that logs host particles once and adsorbate buffer per step.

Host particles are identified by having out-of-bounds group indices (they belong to no molecular group). This works for any number of host systems regardless of how particles are interleaved in the buffer.

Parameters:

Name Type Description Default
state S

The initial MCMC state (after padding).

required
stress_fn StateProperty[S, Table[SystemId, StressResult]] | None

Optional stress tensor function. When provided, the guest stress tensor is logged each cycle.

None

Returns:

Type Description
MCMCLoggedData

Configured logging data with host/adsorbate split.

Source code in src/kups/application/mcmc/logging.py
def make_mcmc_logged_data[S: IsMCMCState](
    state: S,
    stress_fn: StateProperty[S, Table[SystemId, StressResult]] | None = None,
) -> MCMCLoggedData:
    """Create MCMC logging config that logs host particles once and adsorbate buffer per step.

    Host particles are identified by having out-of-bounds group indices
    (they belong to no molecular group). This works for any number of
    host systems regardless of how particles are interleaved in the buffer.

    Args:
        state: The initial MCMC state (after padding).
        stress_fn: Optional stress tensor function. When provided, the
            guest stress tensor is logged each cycle.

    Returns:
        Configured logging data with host/adsorbate split.
    """

    particles = state.particles
    is_occupied = np.asarray(particles.occupation)
    is_host = ~particles.data.group.valid_mask & is_occupied

    host_positions = np.where(is_host)[0]
    ads_positions = np.where(~is_host)[0]

    host_idx = Index(particles.keys, jnp.asarray(host_positions))
    ads_idx = Index(particles.keys, jnp.asarray(ads_positions))
    host_keys = tuple(particles.keys[int(i)] for i in host_positions)
    ads_keys = tuple(particles.keys[int(i)] for i in ads_positions)

    def fixed_view(state: S) -> MCMCFixedData:
        return MCMCFixedData(
            systems=state.systems,
            motifs=state.motifs,
            host_particles=Table(host_keys, state.particles[host_idx]),
        )

    def step_view(state: S) -> MCMCStepData:
        counts = Index.combine(state.groups.data.system, state.groups.data.motif).counts
        ewald = state.ewald_parameters.cache
        if stress_fn is not None:
            guest_stress = stress_fn(jax.random.key(0), state)
        else:
            z = jnp.zeros((len(state.systems), 3, 3))
            guest_stress = Table(state.systems.keys, StressResult(z, z, z))
        sys_keys = state.systems.keys
        system_step = MCMCSystemStepData(
            potential_energy=state.systems.data.potential_energy,
            lj_energy=state.lj_parameters.cache.total_energies.data,
            ewald_short_range_energy=ewald.short_range.total_energies.data,
            ewald_long_range_energy=ewald.long_range.total_energies.data,
            ewald_self_energy=ewald.self_interaction.total_energies.data,
            ewald_exclusion_energy=ewald.exclusion.total_energies.data,
            translation_step_width=state.translation_params.map_data(
                lambda p: p.value
            ).data,
            rotation_step_width=state.rotation_params.map_data(lambda p: p.value).data,
            translation_acceptance=state.translation_params.map_data(
                lambda p: p.history.values.mean(axis=-1)
            ).data,
            rotation_acceptance=state.rotation_params.map_data(
                lambda p: p.history.values.mean(axis=-1)
            ).data,
            reinsertion_acceptance=state.reinsertion_params.map_data(
                lambda p: p.history.values.mean(axis=-1)
            ).data,
            exchange_acceptance=state.exchange_params.map_data(
                lambda p: p.history.values.mean(axis=-1)
            ).data,
            guest_stress=guest_stress.data,
        )
        return MCMCStepData(
            particles=Table(ads_keys, state.particles[ads_idx]),
            groups=state.groups,
            particle_count=counts,
            systems=Table(sys_keys, system_step),
        )

    return MCMCLoggedData(
        fixed=WriterGroupConfig(fixed_view, Once()),
        per_step=WriterGroupConfig(step_view, EveryNStep(1)),
    )

run_mcmc(key, propagator, state, config, logged_data)

Run a µVT MCMC simulation with warmup and production phases.

Parameters:

Name Type Description Default
key Array

JAX PRNG key.

required
propagator Propagator[State]

Propagator, e.g. from :func:~kups.application.simulations.mcmc_rigid.make_propagator.

required
state State

Initial simulation state.

required
config RunConfig

Run configuration.

required
logged_data MCMCLoggedData

Logging configuration with host/adsorbate split.

required

Returns:

Type Description
State

Final simulation state after production run.

Source code in src/kups/application/mcmc/simulation.py
def run_mcmc[State](
    key: Array,
    propagator: Propagator[State],
    state: State,
    config: RunConfig,
    logged_data: MCMCLoggedData,
) -> State:
    """Run a µVT MCMC simulation with warmup and production phases.

    Args:
        key: JAX PRNG key.
        propagator: Propagator, e.g. from :func:`~kups.application.simulations.mcmc_rigid.make_propagator`.
        state: Initial simulation state.
        config: Run configuration.
        logged_data: Logging configuration with host/adsorbate split.

    Returns:
        Final simulation state after production run.
    """
    chain = key_chain(key)
    logging.info("Warming up (%d cycles)...", config.num_warmup_cycles)
    state = run_warmup_cycles(next(chain), propagator, state, config.num_warmup_cycles)
    logging.info("Production run (%d cycles)...", config.num_cycles)
    logger = CompositeLogger(
        HDF5StorageWriter(config.out_file, logged_data, state, config.num_cycles),
        TqdmLogger(config.num_cycles),
    )
    state = run_simulation_cycles(
        next(chain), propagator, state, config.num_cycles, logger
    )
    logging.info("Done.")
    return state