Skip to content

sampler

qemcmc.sampler

Proposal

Proposal(model)

Bases: ABC

Abstract base class for producing proposals for Markov Chain Monte Carlo algorithms.

Subclasses implement the proposal mechanism by defining an update(state) method that generates a candidate state from the current one (e.g. single-spin flips, block updates, or quantum proposals).

Parameters:

Name Type Description Default
model EnergyModel

Energy model defining the target distribution over spin configurations.

required
Source code in src/qemcmc/sampler/proposal.py
def __init__(self, model: EnergyModel):
    self.model = model
    self.n_spins = model.n_spins

update abstractmethod

update(state)

Generate a candidate state from the current state using the proposal mechanism.

This method should be implemented by subclasses to define the specific proposal strategy (e.g., single-spin flips, block updates, or quantum proposals).

Parameters:

Name Type Description Default
state MCMCState

The current state of the Markov chain.

required

Returns:

Type Description
MCMCState

A new candidate state.

Source code in src/qemcmc/sampler/proposal.py
@abstractmethod
def update(self, state: MCMCState) -> MCMCState:
    """
    Generate a candidate state from the current state using the proposal mechanism.

    This method should be implemented by subclasses to define the specific proposal strategy
    (e.g., single-spin flips, block updates, or quantum proposals).

    Parameters
    ----------
    state : MCMCState
        The current state of the Markov chain.

    Returns
    -------
    MCMCState
        A new candidate state.
    """
    pass

ClassicalProposal

ClassicalProposal(model, method='uniform')

Bases: Proposal

Classical Markov Chain Monte Carlo proposer.

This class implements purely classical proposal mechanisms for MCMC. New candidate states are generated either by sampling a completely random (uniform) configuration, or by performing a local single-spin or two-spin flip.

Parameters:

Name Type Description Default
model EnergyModel

Energy model defining the target Boltzmann distribution.

required
method str

Proposal mechanism used to generate candidate states.

  • "uniform" : propose a completely random spin configuration.
  • "local" : flip a single randomly chosen spin.
  • "2-local" : flip two randomly chosen spins.

Default is "uniform".

'uniform'
Source code in src/qemcmc/sampler/classical_proposal.py
def __init__(self, model: EnergyModel, method: str = "uniform"):
    if method not in self.METHODS:
        raise ValueError(f"Method '{method}' is not supported. Choose from 'uniform', 'local', or '2-local'.")
    super().__init__(model)
    self.method = method

update_uniform

update_uniform(current_state_bitstring)

Proposes a new state by generating a random bitstring.

Parameters:

Name Type Description Default
current_state_bitstring str

The current state represented as a bitstring (not used, but required by the API).

required

Returns:

Type Description
str

A new random state bitstring of the same length.

Source code in src/qemcmc/sampler/classical_proposal.py
def update_uniform(self, current_state_bitstring: str) -> str:
    """
    Proposes a new state by generating a random bitstring.

    Parameters
    ----------
    current_state_bitstring : str
        The current state represented as a bitstring (not used, but required by the API).

    Returns
    -------
    str
        A new random state bitstring of the same length.
    """

    return get_random_state(len(current_state_bitstring))

update_local

update_local(current_state_bitstring)

Proposes a new state by flipping a single randomly chosen spin.

Parameters:

Name Type Description Default
current_state_bitstring str

The current state represented as a bitstring.

required

Returns:

Type Description
str

The new state bitstring after flipping one spin.

Source code in src/qemcmc/sampler/classical_proposal.py
def update_local(self, current_state_bitstring: str) -> str:
    """
    Proposes a new state by flipping a single randomly chosen spin.

    Parameters
    ----------
    current_state_bitstring : str
        The current state represented as a bitstring.

    Returns
    -------
    str
        The new state bitstring after flipping one spin.
    """

    # Randomly choose which spin to flip
    choice = np.random.randint(0, self.n_spins)

    # Flip the chosen spin
    c_s = list(current_state_bitstring)
    c_s[choice] = str(int(c_s[choice]) ^ 1)

    # Return the new state as a bitstring
    return "".join(c_s)

update_2local

update_2local(current_state_bitstring)

Proposes a new state by flipping two randomly chosen spins.

Parameters:

Name Type Description Default
current_state_bitstring str

The current state represented as a bitstring.

required

Returns:

Type Description
str

The new state bitstring after flipping two spins.

Source code in src/qemcmc/sampler/classical_proposal.py
def update_2local(self, current_state_bitstring: str) -> str:
    """
    Proposes a new state by flipping two randomly chosen spins.

    Parameters
    ----------
    current_state_bitstring : str
        The current state represented as a bitstring.

    Returns
    -------
    str
        The new state bitstring after flipping two spins.
    """
    # Randomly choose which two spins to flip
    choices = np.random.choice(self.n_spins, size=2, replace=False)

    # Flip the chosen spins
    c_s = list(current_state_bitstring)
    for choice in choices:
        c_s[choice] = str(int(c_s[choice]) ^ 1)

    # Return the new state as a bitstring
    return "".join(c_s)

QeProposal

QeProposal(
    model,
    gamma,
    time,
    r=None,
    delta_t=None,
    coarse_graining=None,
    coupling_weights=None,
    m=1,
)

Bases: Proposal

Quantum-enhanced Markov Chain Monte Carlo sampler.

This class implements the proposal mechanism of the quantum-enhanced MCMC algorithm. Candidate states are generated by simulating the quantum time evolution of a transverse-field Hamiltonian.

The quantum proposal circuit is constructed using :class:CircuitMaker and can optionally operate on coarse-grained subgroups of spins to improve scalability.

Parameters:

Name Type Description Default
model EnergyModel

Energy model defining the target Boltzmann distribution.

required
gamma float | tuple[float, float]

Transverse field strength (Γ). If a tuple is provided, a value is sampled uniformly from the range [min, max] at each step.

required
time float | tuple[float, float]

Total evolution time. If a tuple is provided, a value is sampled uniformly from the range [min, max] at each step.

required
r int | None

Number of Trotter steps. Mutually exclusive with delta_t. If provided, the Trotter step size is derived as Δt = t / r and r stays fixed while Δt varies with t. A warning is issued at init if the resulting Δt range falls outside [0.1, 2.0]. By default None.

None
delta_t float | None

Trotter step size. Mutually exclusive with r. If provided, the number of Trotter steps is derived as r = floor(t / Δt) per step, so r varies with t while Δt stays fixed. A warning is issued if Δt falls outside [0.1, 2.0]. By default None.

None
coarse_graining CoarseGraining | None

A coarse-graining scheme to define spin subgroups. If None, no coarse-graining is used. By default None.

None
coupling_weights list[float | tuple[float, float]] | None

Weights for the coupling tensors. If a tuple is provided, a weight is sampled uniformly from the range. This allows for dynamic adjustment of the influence of different terms in the Hamiltonian during the proposal step. By default None. Note that this is further adjusted by (1 - gamma) to balance the influence of the problem Hamiltonian with the mixer term. Divide by (1 - gamma) if needed. Also, note that the coupling weights should include the mixing term.

None
m int

Number of subgroups to partition the spins into for sequential updates. By default 1.

1
Notes

The proposal step simulates the time evolution under a transverse-field Hamiltonian defined by the energy model and measures the resulting state to produce a candidate configuration. This proposal is then accepted or rejected using the Metropolis criterion to ensure convergence to the target Boltzmann distribution.

Exactly one of r or delta_t should be passed in as an argument. If neither is given, a default Δt of 0.8 is used.

Source code in src/qemcmc/sampler/qe_proposal.py
def __init__(
    self,
    model: EnergyModel,
    gamma: float | tuple[float, float],
    time: float | tuple[float, float],
    r: Optional[int] = None,
    delta_t: Optional[float] = None,
    coarse_graining: Optional[CoarseGraining] = None,
    coupling_weights: Optional[list[float | tuple[float, float]]] = None,
    m: int = 1,
):
    super().__init__(model)

    if coupling_weights is not None:
        if len(coupling_weights) != len(model.normalised_couplings):
            raise ValueError(
                f"Length of coupling_weights ({len(coupling_weights)}) must match the number of couplings in the model ({len(model.normalised_couplings)}), including constraint terms."
            )
        self.coupling_weights = coupling_weights
    else:
        self.coupling_weights = list(np.ones(len(model.normalised_couplings)))

    self.gamma = self._validate_gamma(gamma)
    self.time = self._validate_time(time)
    self.r = self._validate_r(r)
    self.delta_t = self._validate_delta_t(delta_t)
    self._validate_trotter_params()
    self.m = m
    self.method = "quantum"

    self.CM = CircuitMaker(self.model)

    self.cg = coarse_graining or CoarseGraining(model.n_spins)

update

update(current_state)

Perform 'm' sequential quantum updates across non-overlapping subgroups.

Parameters:

Name Type Description Default
current_state str

Current state of the system as a bitstring.

required

Returns:

Type Description
str

Updated state of the system as a bitstring.

Source code in src/qemcmc/sampler/qe_proposal.py
def update(self, current_state: str) -> str:
    """
    Perform 'm' sequential quantum updates across non-overlapping subgroups.

    Parameters
    ----------
    current_state : str
        Current state of the system as a bitstring.

    Returns
    -------
    str
        Updated state of the system as a bitstring.
    """

    if not isinstance(current_state, str):
        raise TypeError(f"Bitstring must be of type str, got {type(current_state)}: {current_state!r}")

    # Generate m disjoint partitions of the spins
    partitions = self.cg.get_partitions(m=self.m)

    final_coupling_weights, g, t, r = self.sample_hyperparams()

    working_state = current_state
    for subgroup in partitions:
        # Recalculate local couplings as spins outside the subgroup may have flipped
        local_couplings = self.model.get_subgroup_couplings(subgroup=subgroup, current_state=working_state, coupling_weights=final_coupling_weights)
        working_state = self.CM.update(s=working_state, subgroup_choice=subgroup, local_couplings=local_couplings, gamma=g, time=t, r=r)
    return working_state

sample_hyperparams

sample_hyperparams()

Sample hyperparameters (coupling weights, gamma, time, r) for the current proposal step.

Returns:

Type Description
tuple[list[float], float, float, int]

(final_coupling_weights, gamma, time, r) for this proposal step.

Source code in src/qemcmc/sampler/qe_proposal.py
def sample_hyperparams(self) -> tuple[list[float], float, float, int]:
    """
    Sample hyperparameters (coupling weights, gamma, time, r) for the current proposal step.

    Returns
    -------
    tuple[list[float], float, float, int]
        (final_coupling_weights, gamma, time, r) for this proposal step.
    """

    # Sample gamma and time once per full proposal step
    g = self.gamma if not isinstance(self.gamma, tuple) else np.random.uniform(*self.gamma)
    t = self.time if not isinstance(self.time, tuple) else np.random.uniform(*self.time)

    # Determine r (number of Trotter steps) for this step
    if self.r is not None:
        # Fixed r mode: r stays constant, Δt varies with t
        r = self.r
        dt = t / r
    else:
        # Fixed Δt mode (explicit or default): r varies with t
        dt = self.delta_t if self.delta_t is not None else DEFAULT_DELTA_T
        r = max(1, int(np.floor(t / dt)))

    # Pre-sample weights for all unique coupling weight settings (tuples or floats)
    unique_items = sorted(list(set(self.coupling_weights)), key=lambda x: str(x))
    unique_index = [[i for i, x in enumerate(self.coupling_weights) if x == item] for item in unique_items]

    sampled_weights = np.ones(len(unique_items), dtype=float)
    for i, item in enumerate(unique_items):
        if isinstance(item, (int, float)):
            sampled_weights[i] = item
        elif isinstance(item, tuple):
            sampled_weights[i] = np.random.uniform(*item)

    # Adjust the sampled weights by (1 - gamma) to balance their influence with the mixer term in the Hamiltonian
    sampled_weights_adjusted_by_gamma = (1 - g) * sampled_weights  # Adjust weights by (1 - gamma) to balance with the mixer term

    # Construct the final list of coupling weights for this update step
    final_coupling_weights = np.ones(len(self.coupling_weights))
    for i, indices in enumerate(unique_index):
        for index in indices:
            final_coupling_weights[index] = sampled_weights_adjusted_by_gamma[i]

    return final_coupling_weights, g, t, r