Skip to main content
IBM Quantum Platform

Risolvi il modello di Sherrington-Kirkpatrick con l'ottimizzatore Parity Twine di ParityQC

Stima dei tempi di esecuzione: 10 secondi su un processore Nighthawk r2. (NOTA: Si tratta solo di una stima. (La durata potrebbe variare.)


Risultati di apprendimento

  • Utilizza il Parity Twine Optimizer per risolvere il modello di Sherrington-Kirkpatrick.
  • Scopri quali opzioni sono disponibili per il Parity Twine Optimizer e quali risultati vengono generati.

Sfondo

Questo tutorial illustra come risolvere il modello di Sherrington-Kirkpatrick utilizzando l'ottimizzatore Parity Twine di ParityQC.

Fornisce il codice necessario per formulare il problema interamente a livello locale in una forma che possa interfacciarsi con il Parity Twine Optimizer.

Il modello di Sherrington-Kirkpatrick

Il modello di Sherrington-Kirkpatrick (SK) è un modello fondamentale della meccanica statistica, in particolare nell'ambito dello studio dei vetri di spin. A differenza del modello di Ising standard, in cui le interazioni sono solitamente limitate ai vicini più prossimi, il modello SK è un modello a raggio infinito, il che significa che ogni spin interagisce con ogni altro spin del sistema. Ciò porta a un panorama energetico estremamente complesso e “irregolare”, caratterizzato da numerosi minimi locali, che costituiscono il tratto distintivo del comportamento vetroso.

La caratteristica fondamentale del modello SK risiede nella frustrazione. Nel modello, le intensità di interazione JijJ_{ij} tra gli spin sono distribuite in modo casuale tra valori positivi e negativi. Ciò porta a situazioni (ad esempio, negli schemi triangolari) in cui non è possibile disporre gli spin in modo da minimizzare contemporaneamente tutte le interazioni. Nel modello SK, poiché ogni spin interagisce con tutti gli altri spin, questa frustrazione si aggrava a livello globale, dando origine a una rete di vincoli contrastanti.

Formulazione matematica

Lo stato del sistema è definito da un insieme di NN spin di Ising, si∈{+1,−1}s_i \in \{+1, -1 \} . L'energia di una specifica configurazione è data dall'hamiltoniano:

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

dove JijJ_{ij} è l'intensità di accoppiamento tra lo spin ii e lo spin jj.

Nel modello SK, i coefficienti di accoppiamento JijJ_{ij} sono variabili casuali indipendenti e identicamente distribuite. Per garantire che l'energia rimanga estensiva (proporzionale a NN ) come N→∞N \rightarrow \infty, la varianza degli accoppiamenti deve essere proporzionale al numero di particelle:

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

Lo stato fondamentale di una data HH è la configurazione specifica degli spin ( s1,s2,...sns_1, s_2, ...s_n ) che minimizza l'energia. Trovare lo stato fondamentale è un problema di ottimizzazione NP-difficile.


Requisiti

Prima di iniziare questo tutorial, assicurati che siano installati i seguenti componenti:

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

È inoltre necessario disporre dei permessi necessari per accedere alla funzione " ParityQC " di Twine Optimizer. Per richiedere l'accesso, compila questo modulo.


Configura

(Questo codice presuppone che tu abbia già salvato il tuo account nell'ambiente locale.)

Per prima cosa, importa tutti i pacchetti necessari per questo tutorial.

import numpy as np

from qiskit_ibm_catalog import QiskitFunctionsCatalog

Carica il “Parity Twine Optimizer” dal catalogo “ Qiskit Functions ”:

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

Fase 1: Definire il problema come funzione obiettivo

Anziché ricavare il problema SK da una libreria, come facciamo per il problema della suddivisione del mercato, lo si formula direttamente.

La funzione generate_sk_problem formula il problema SK direttamente nel formato dizionario richiesto. L'unico dato da inserire è il n numero di giri previsti dal modello.

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 ``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:
        An 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

Utilizzare la funzione _generate_couplings_sk_model per calcolare i termini di accoppiamento casuali nel modello SK. Per una maggiore controllo sugli accoppiamenti, è possibile utilizzare argomenti opzionali, descritti nella stringa di documentazione della funzione.

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

Fase 2: Risolvere il problema utilizzando il Parity Twine Optimizer

Grazie alle funzioni sopra descritte, è possibile impostare il problema SK e trovare una soluzione utilizzando Twine Optimizer e un backend IBM Quantum® a scelta.

Per eseguire la funzione, scegliere un backend adeguato; ad esempio, ibm_phoenix.

Se lo si desidera, è possibile utilizzare le opzioni per un maggiore controllo sull'invio:

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

dove shots è un numero intero che specifica il numero di esecuzioni del circuito, postprocessing_level determina se al risultato viene applicata una post-elaborazione, transpile_only sceglie se il problema viene solo transpilato in un circuito (senza essere risolto) e job_tags è un’etichetta che identifica il lavoro su IBM Quantum Platform.

La dimensione del modello SK è definita d NN e, ovvero il numero di spin interagenti. Una volta scelto NN, il codice sopra riportato genera il problema per n_spins.

Esegui l'ottimizzatore:

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}")

Verifica lo stato del lavoro:

# Monitor the job status
function_job.status()

Recupera i risultati:

result = function_job.result()

result

Il risultato ha la forma seguente:

{
    '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},
        },
    }
}

dove il dizionario solution corrisponde ai qubit definiti nel problema e fornisce i loro valori di spin ottimizzati per l'Hamiltoniano del modello SK. Questa specifica sequenza di spin nella soluzione ottimale rappresenta la configurazione che minimizza l'energia totale del sistema in base alle intensità di interazione casuali date. Nel modello SK, questo può essere considerato lo stato di minima energia di un sistema magnetico disordinato.

metadata fornisce informazioni sulla transpilazione (numero di gate per due qubit/profondità, gate utilizzati, qubit attivi) e sui vari tempi di esecuzione.


Passi successivi

Suggerimenti
Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.