Skip to main content
IBM Quantum Platform

Lösen Sie das Sherrington-Kirkpatrick-Modell mit dem „ ParityQC -Parity-Twine-Optimizer“

Geschätzte Ausführungszeit: 10 Sekunden auf einem Nighthawk- r2 -Prozessor. (HINWEIS: Dies ist lediglich eine Schätzung. (Die Laufzeit kann variieren.)


Lernziele

  • Verwenden Sie den Parity Twine Optimizer, um das Sherrington-Kirkpatrick-Modell zu lösen.
  • Erfahren Sie, welche Optionen für den Parity Twine Optimizer verfügbar sind und welche Ergebnisse ausgegeben werden.

Hintergrund

In diesem Tutorial wird gezeigt, wie man das Sherrington-Kirkpatrick-Modell mit dem „Parity Twine Optimizer“ von ParityQC löst.

Es enthält Code, mit dem sich das Problem vollständig lokal in einer Form formulieren lässt, die mit dem Parity Twine Optimizer kompatibel ist.

Das Sherrington-Kirkpatrick-Modell

Das Sherrington-Kirkpatrick-Modell (SK-Modell) ist ein grundlegendes Modell der statistischen Mechanik, insbesondere im Rahmen der Untersuchung von Spingläsern. Im Gegensatz zum Standard-Ising-Modell, bei dem sich die Wechselwirkungen in der Regel auf die nächsten Nachbarn beschränken, handelt es sich beim SK-Modell um ein Modell mit unbegrenzter Reichweite, was bedeutet, dass jeder Spin mit jedem anderen Spin im System wechselwirkt. Dies führt zu einer äußerst komplexen, „unebenen“ Energielandschaft, die durch zahlreiche lokale Minima gekennzeichnet ist – ein typisches Merkmal des glasartigen Verhaltens.

Das Kernmerkmal des SK-Modells liegt in der Frustration. In dem Modell sind die Wechselwirkungsstärken JijJ_{ij} zwischen den Spins zufällig zwischen positiven und negativen Werten verteilt. Dies führt zu Situationen (zum Beispiel bei dreieckförmigen Anordnungen), in denen die Spins nicht so angeordnet werden können, dass alle Wechselwirkungen gleichzeitig minimiert werden. Da im SK-Modell jeder Spin mit jedem anderen Spin wechselwirkt, verstärkt sich diese Frustration global, was zu einem Geflecht widersprüchlicher Einschränkungen führt.

Mathematische Formulierung

Der Zustand des Systems wird durch eine Menge von NN Ising-Spins definiert, si∈{+1,−1}s_i \in \{+1, -1 \} . Die Energie einer bestimmten Konfiguration ergibt sich aus dem Hamilton-Operator:

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

wobei JijJ_{ij} die Kopplungsstärke zwischen dem Spin ii und dem Spin jj ist.

Im SK-Modell sind die Kopplungen JijJ_{ij} unabhängige und identisch verteilte Zufallsvariablen. Um sicherzustellen, dass die Energie im Verhältnis zu NN als N→∞N \rightarrow \infty groß bleibt, muss die Varianz der Kopplungen mit der Anzahl der Teilchen skalieren:

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

Der Grundzustand eines bestimmten HH ist die spezifische Konfiguration der Spins ( s1,s2,...sns_1, s_2, ...s_n ), die die Energie minimiert. Die Bestimmung des Grundzustands ist ein NP-schweres Optimierungsproblem.


Anforderungen

Bevor Sie mit dieser Anleitung beginnen, stellen Sie bitte sicher, dass folgende Komponenten installiert sind:

  • Qiskit Functions Catalog IBM Client (pip install qiskit-ibm-catalog)
  • Qiskit-Add-on „Optimization Mapper“ (pip install qiskit_addon_opt_mapper)
  • NumPy (pip install numpy)

Außerdem benötigen Sie die Berechtigung, auf die Twine-Optimizer-Funktion „ ParityQC “ zuzugreifen. Um Zugang zu beantragen, füllen Sie bitte dieses Formular aus.


Einrichtung

(Dieser Code setzt voraus, dass Sie Ihr Konto bereits in Ihrer lokalen Umgebung gespeichert haben.)

Importieren Sie zunächst alle für dieses Tutorial erforderlichen Pakete.

import numpy as np

from qiskit_ibm_catalog import QiskitFunctionsCatalog

Laden Sie den „Parity Twine Optimizer“ aus dem Katalog „ Qiskit Functions “:

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

Schritt 1: Definieren Sie das Problem als Zielfunktion

Anstatt das SK-Problem wie beim Marktaufteilungsproblem aus einer Bibliothek zu beziehen, formulieren Sie es direkt.

Die Funktion generate_sk_problem formuliert das SK-Problem direkt im erforderlichen Wörterbuchformat. Die einzige erforderliche Eingabe n ist die Anzahl der Drehungen im Modell.

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

Verwenden Sie die Funktion, _generate_couplings_sk_model um die zufälligen Kopplungsterme im SK-Modell zu berechnen. Um die Kopplungen besser steuern zu können, können Sie optionale Argumente verwenden, die im Docstring der Funktion erläutert werden.

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

Schritt 2: Lösen Sie das Problem mit dem „Parity Twine Optimizer“

Mit den oben genannten Funktionen können Sie das SK-Problem formulieren und mithilfe des Twine-Optimierers sowie eines ausgewählten Backends unter IBM Quantum® eine Lösung finden.

Um die Funktion auszuführen, wählen Sie ein geeignetes Backend aus, zum Beispiel „ibm_phoenix“.

Optional können Sie Optionen nutzen, um die Übermittlung genauer zu steuern:

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

wobei eine ganze Zahl ist shots , die die Anzahl der Schaltkreisaufrufe angibt, postprocessing_level festlegt, ob das Ergebnis einer Nachbearbeitung unterzogen wird, transpile_only bestimmt, ob das Problem lediglich in einen Schaltkreis transpiliert (und nicht gelöst) wird, und eine Kennung zur Identifizierung des Auftrags auf IBM Quantum Platform job_tags ist.

Die Größe des SK-Modells wird durch NN definiert, die Anzahl der miteinander wechselwirkenden Spins. Sobald Sie „ NN “ auswählen, generiert der obige Code die Aufgabe für n_spins.

Führen Sie den Optimierer aus:

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

Überprüfen Sie den Auftragsstatus:

# Monitor the job status
function_job.status()

Ergebnisse abrufen:

result = function_job.result()

result

Das Ergebnis hat folgende Form:

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

wobei das Wörterbuch solution den in der Aufgabe definierten Qubits entspricht und deren optimierte Spinwerte für den Hamilton-Operator des SK-Modells angibt. Diese spezifische Abfolge von Spins in der optimalen Lösung stellt die Konfiguration dar, die die Gesamtenergie des Systems auf der Grundlage der gegebenen zufälligen Wechselwirkungsstärken minimiert. Im SK-Modell kann dies als der Zustand mit der niedrigsten Energie eines ungeordneten magnetischen Systems betrachtet werden.

metadata liefert Informationen zur Transpilierung (Anzahl der 2-Qubit-Gatter/Tiefe, verwendete Gatter, aktive Qubits) sowie zu verschiedenen Laufzeiten.


Nächste Schritte

Empfehlungen
War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.