Skip to main content
IBM Quantum Platform

Resolva o modelo de Sherrington-Kirkpatrick com o Otimizador de Cordões de Paridade da ParityQC

Estimativa de tempo de execução: 10 segundos em um processador Nighthawk r2. (OBSERVAÇÃO: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)


Resultados do aprendizado

  • Utilize o Parity Twine Optimizer para resolver o modelo de Sherrington-Kirkpatrick.
  • Saiba quais opções do Parity Twine Optimizer estão disponíveis e quais resultados são gerados.

Segundo plano

Este tutorial mostra como resolver o modelo de Sherrington-Kirkpatrick utilizando o Parity Twine Optimizer da ParityQC.

Ele fornece código para formular o problema de forma totalmente local, de maneira que possa interagir com o Otimizador Parity Twine.

O modelo de Sherrington-Kirkpatrick

O modelo de Sherrington-Kirkpatrick (SK) é um modelo fundamental na mecânica estatística, especificamente no âmbito do estudo dos vidros de spin. Ao contrário do modelo de Ising padrão, no qual as interações geralmente se limitam aos vizinhos mais próximos, o modelo SK é um modelo de alcance infinito, o que significa que cada spin interage com todos os outros spins do sistema. Isso leva a um cenário energético altamente complexo e “irregular”, caracterizado por muitos mínimos locais, o que é a marca registrada do comportamento vítreo.

A característica central do modelo SK reside na frustração. No modelo, as intensidades de interação JijJ_ij entre os spins são distribuídas aleatoriamente entre valores positivos e negativos. Isso leva a situações (por exemplo, em arranjos triangulares) em que os spins não podem ser dispostos de forma a minimizar todas as interações simultaneamente. No modelo SK, como cada spin interage com todos os outros spins, essa frustração se agrava globalmente, levando a uma rede de restrições conflitantes.

Formulação matemática

O estado do sistema é definido por um conjunto de NN espins de Ising, si∈{+1,−1}s_i \in \{+1, -1 \} . A energia de uma configuração específica é dada pelo hamiltoniano:

H=−∑1≤i≤j≤NJijsisjH = - \sum_{1 \leq i \leq j \leq N} J_{ij} s_i s_j

onde JijJ_{ij} é a intensidade do acoplamento entre o spin ii e o spin jj.

No modelo SK, os acoplamentos JijJ_{ij} são variáveis aleatórias independentes e com distribuição idêntica. Para garantir que a energia permaneça extensiva (proporcional a NN ) à medida que N→∞N \rightarrow \infty, a variância dos acoplamentos deve variar proporcionalmente ao número de partículas:

Jij∼N(0,J2N)J_{ij} \sim \mathcal{N} \left( 0, \frac{J^2}{N} \right)

O estado fundamental de um determinado sistema de spin-1-2 ( HH ) é a configuração específica dos spins ( s1,s2,...sns_1, s_2, ...s_n ) que minimiza a energia. Encontrar o estado fundamental é um problema de otimização NP-difícil.


Requisitos

Antes de iniciar este tutorial, certifique-se de que os seguintes itens estejam instalados:

  • Qiskit Functions Catalog IBM Cliente (pip install qiskit-ibm-catalog)
  • Componente adicional do Qiskit: Optimization Mapper (pip install qiskit_addon_opt_mapper)
  • NumPy (pip install numpy)

Você também precisa de permissão para acessar a função “ ParityQC ” do Twine Optimizer. Para solicitar acesso, preencha este formulário.


Instalação

(Este código pressupõe que você já tenha salvo sua conta no seu ambiente local.)

Primeiro, importe todos os pacotes necessários para este tutorial.

import numpy as np

from qiskit_ibm_catalog import QiskitFunctionsCatalog

Carregue o Parity Twine Optimizer do catálogo “ Qiskit Functions ”:

catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
function = catalog.load("parityqc/parity-twine-optimizer")

Etapa 1: Definir o problema como uma função-objetivo

Em vez de obter o problema SK a partir de uma biblioteca, como fazemos no caso do problema de divisão de mercado, você o formula diretamente.

A função generate_sk_problem formula o problema SK diretamente no formato de dicionário exigido. A única informação necessária n é o número de giros no modelo.

def generate_sk_problem(
    n: int,
    coupling_mean: float = 0.0,
    coupling_std: float = 1.0,
    local_fields_mean: float = 0.0,
    local_fields_std: float = 0.0,
    edge_density: float = 1.0,
    ensure_extensivity: bool = False,
    seed: int | None = None,
) -> dict:
    """Generate the Sherrington-Kirkpatrick (SK) model with varying
     edge density.

    Samples couplings and local fields via :func:`generate_couplings_sk_model`
    and assembles the corresponding Ising Hamiltonian

        H = -∑_{i<j} J_ij z_i z_j - ∑_i h_i z_i,

    where z_i ∈ {-1, +1}.

    Args:
        n: Number of spins (>= 2).
        coupling_mean: Mean coupling before optional SK scaling.
        coupling_std: Coupling std before optional SK scaling.
        local_fields_mean: Mean longitudinal field.
        local_fields_std: Std of the longitudinal fields.
        edge_density: Fraction of non-zero couplings, in ``[2/n, 1]``.
        ensure_extensivity: Whether to apply the SK 1/n scaling.
        seed: random number generator seed.

    Returns:
        A ``ProblemRepresentation`` encoding the SK Hamiltonian.

    Raises:
        ValueError: If ``n < 2``, ``coupling_std < 0``, ``local_fields_std < 0``,
            or ``edge_density`` is outside ``[2/n, 1]``.
    """
    couplings, local_fields = _generate_couplings_sk_model(
        n=n,
        coupling_mean=coupling_mean,
        coupling_std=coupling_std,
        local_fields_mean=local_fields_mean,
        local_fields_std=local_fields_std,
        edge_density=edge_density,
        ensure_extensivity=ensure_extensivity,
        seed=seed,
    )

    # Handle quadratic terms: coupling[i, j] * zj[i] * zj[j]
    # Only iterate over the upper triangle (i < j)
    sk_problem = {
        str((i, j)): float(couplings[i, j])
        for i in range(n)
        for j in range(i + 1, n)
        if couplings[i, j] != 0
    }

    # Handle linear terms: local_fields[i] * zj[i]
    sk_problem.update(
        {
            str((i,)): float(local_fields[i])
            for i in range(n)
            if local_fields[i] != 0
        }
    )

    return sk_problem

Utilize a função _generate_couplings_sk_model para calcular os termos de acoplamento aleatórios no modelo SK. Para ter mais controle sobre os acoplamentos, você pode usar argumentos opcionais, que são explicados na descrição da função.

def _generate_couplings_sk_model(
    n: int,
    coupling_mean: float = 0.0,
    coupling_std: float = 1.0,
    local_fields_mean: float = 0.0,
    local_fields_std: float = 0.0,
    edge_density: float = 1.0,
    ensure_extensivity: bool = False,
    seed: int | None = None,
) -> tuple[np.ndarray, np.ndarray]:
    """Generate random couplings and local fields for an Ising / SK model.

    Couplings are Gaussian. With ``ensure_extensivity=True`` they follow the
    Sherrington-Kirkpatrick scaling ``J_ij ~ N(coupling_mean/n, coupling_std^2/n)``
    (extensive energy, O(n)); otherwise ``J_ij ~ N(coupling_mean, coupling_std^2)``
    (energy O(n^2)). Fields are ``h_i ~ N(local_fields_mean, local_fields_std^2)``.

    ``edge_density`` sets the fraction of the ``n*(n-1)/2`` possible couplings that
    are non-zero (1 = fully dense). The kept edges always include a random spanning
    tree, so the interaction graph is guaranteed connected. This requires at least
    ``n-1`` edges, so ``edge_density`` must be at least ``2/n``.

    Args:
        n: Number of spins (>= 2).
        coupling_mean: Mean coupling before optional SK scaling.
        coupling_std: Coupling std before optional SK scaling.
        local_fields_mean: Mean longitudinal field.
        local_fields_std: Std of the longitudinal fields.
        edge_density: Fraction of non-zero couplings, in ``[2/n, 1]``.
        ensure_extensivity: Whether to apply the SK 1/n scaling.
        seed: random number generator seed.

    Returns:
        Tuple ``(couplings, fields)``: a symmetric ``(n, n)`` matrix with zero
        diagonal, and an ``(n,)`` field vector.

    Raises:
        ValueError: If ``n < 2``, ``coupling_std < 0``, ``local_fields_std < 0``,
            or ``edge_density`` is outside ``[2/n, 1]``.
    """
    if n < 2:
        raise ValueError(f"n must be >= 2, got {n}")
    if coupling_std < 0 or local_fields_std < 0:
        raise ValueError(
            "coupling_std and local_fields_std must be non-negative"
        )

    # A connected graph on n nodes needs at least n-1 of the n*(n-1)/2 possible
    # edges, so edge_density has a hard lower bound of 2/n.
    min_edge_density = 2.0 / n
    if not min_edge_density <= edge_density <= 1.0:
        raise ValueError(
            f"edge_density must be in [{min_edge_density:.4g}, 1] for n={n} "
            f"(at least n-1 edges are needed to keep the graph connected), "
            f"got {edge_density}"
        )

    rng = np.random.default_rng(seed)

    j_loc, j_scale = (
        (coupling_mean / n, coupling_std / np.sqrt(n))
        if ensure_extensivity
        else (coupling_mean, coupling_std)
    )

    upper_idx = np.triu_indices(n, k=1)
    n_edges = len(upper_idx[0])

    # Select which edges are present.
    if edge_density < 1.0:
        n_keep = int(round(edge_density * n_edges))
        # Map each (i, j) node pair to its position in the flat upper-triangle list.
        pair_to_flat = {
            (int(i), int(j)): idx
            for idx, (i, j) in enumerate(
                zip(upper_idx[0], upper_idx[1], strict=False)
            )
        }

        # Random spanning tree: node perm[k] links to a random earlier node.
        perm = rng.permutation(n)
        keep = np.zeros(n_edges, dtype=bool)
        for k in range(1, n):
            child, parent = perm[k], perm[rng.integers(0, k)]
            i, j = min(child, parent), max(child, parent)
            keep[pair_to_flat[(int(i), int(j))]] = True

        # Fill the remaining budget with random non-tree edges.
        remaining = n_keep - (n - 1)
        if remaining > 0:
            keep[
                rng.choice(
                    np.flatnonzero(~keep), size=remaining, replace=False
                )
            ] = True
    else:
        keep = np.ones(n_edges, dtype=bool)

    n_present = int(keep.sum())
    if j_scale == 0.0:
        vals = np.full(n_present, j_loc)
    else:
        vals = rng.normal(loc=j_loc, scale=j_scale, size=n_present)

    couplings = np.zeros((n, n))
    couplings[upper_idx[0][keep], upper_idx[1][keep]] = vals
    couplings += couplings.T  # symmetrize; diagonal stays zero

    fields = (
        np.full(n, local_fields_mean)
        if local_fields_std == 0.0
        else rng.normal(loc=local_fields_mean, scale=local_fields_std, size=n)
    )

    return couplings, fields

Etapa 2: Resolva o problema usando o Otimizador de Cordas de Paridade

Com as funções acima, você pode definir o problema SK e encontrar uma solução usando o Twine Optimizer e um backend do tipo “ IBM Quantum® ” de sua escolha.

Para executar a função, escolha um backend adequado; por exemplo, ibm_phoenix.

Opcionalmente, utilize as opções para ter maior controle sobre o envio:

options = {
    "shots": 100000,
    "postprocessing_level": 1,
    "transpile_only": False,
    "job_tags": ["sk"],
}

onde shots é um número inteiro que especifica o número de execuções do circuito, postprocessing_level determina se o pós-processamento é aplicado ao resultado, transpile_only determina se o problema é apenas compilado para um circuito (e não resolvido), e job_tags é um rótulo para identificar o trabalho em IBM Quantum Platform.

O tamanho do modelo SK é definido por NN, o número de spins que interagem. Depois de escolher NN, o código acima gera o problema para n_spins.

Execute o otimizador:

n_spins = 50
sk_problem = generate_sk_problem(n_spins)

function_job = function.run(
    problem=sk_problem,
    variable_type="spin",
    backend_name="ibm_phoenix",
    options=options,
)
print(f"Job ID: {function_job.job_id}")

Verifique o status do trabalho:

# Monitor the job status
function_job.status()

Recuperar resultados:

result = function_job.result()

result

O resultado tem a seguinte forma:

{
    'solution': {'0': 1, '1': 1, '10': 1, '11': 1, ... },
    'objective_value':  -240.5425312543882,
    'solution_bitstring': '00001101110100100111001110101001101111111011001110',
    'metadata': {
        'circuit_metrics': {
            'depth': 523,
            'gate_count': 10118,
            'two_qubit_gate_depth': 196,
            'two_qubit_gate_count': 2499,
            'num_qubits': 50,
            'operations': {'sx': 3353, 'rz': 3320, 'cz': 2499, 'delay': 894, 'measure': 50, 'x': 2},
        },
        'solver_info': {
            'variable_mapping': {'0': 0, '1': 1, '10': 2, '11': 3, ... },
            'bitstring_distributions': {
                'before_postprocessing': {'011101110010110111001110011000': 1, ... },
                'after_postprocessing': {'011011110000110101001111011000': 1, ... }
            },
            'best_parameters': {
                'beta': [-0.4602084830507902],
                'gamma': [1.8500357096574955]
            }
        },
        'resource_usage': {
            'RUNNING: MAPPING': {'CPU_TIME': 290.272},
            'RUNNING: OPTIMIZING_FOR_HARDWARE': {'CPU_TIME': 0.494},
            'RUNNING: WAITING_FOR_QPU': {'CPU_TIME': 8.775},
            'RUNNING: EXECUTING_QPU': {'QPU_TIME': 31.0},
            'RUNNING: POST_PROCESSING': {'CPU_TIME': 162.96},
        },
    }
}

onde o dicionário solution corresponde aos qubits definidos no problema e fornece seus valores de spin otimizados para o hamiltoniano do modelo SK. Essa sequência específica de giros na solução ótima representa a configuração que minimiza a energia total do sistema com base nas intensidades de interação aleatórias fornecidas. No modelo SK, isso pode ser considerado como o estado de menor energia de um sistema magnético desordenado.

metadata fornece informações sobre a transpilação (contagem de portas de dois qubits/profundidade, portas utilizadas, qubits ativos) e vários tempos de execução.


Próximas etapas

Recomendações
Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.