Skip to content

qe_proposal

qemcmc.sampler.qe_proposal

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