Source code for mimiq_qiskit.backend

"""Qiskit ``BackendV2`` that runs Qiskit circuits on MIMIQ.

:class:`MimiqBackend` accepts any of:

- a :class:`mimiqcircuits.RemoteConnection` / ``MimiqConnection`` for the
  cloud path. It is wrapped in
  :class:`mimiqcircuits.backends.MimiqRemoteBackend` so its capabilities,
  limits, and execute semantics carry over.
- a :class:`mimiqcircuits.backends.Backend` instance (any local or remote
  MIMIQ simulator), used as is.
- a plain callable ``(circuit, *, nsamples, seed) -> QCSResults``, an
  escape hatch for custom executors and tests.

A batch of circuits is submitted to MIMIQ in a single job, because MIMIQ
accepts a list of circuits and returns a list of results. So
``backend.run([qc1, qc2, ...])`` is one round trip rather than one per
circuit.

The advertised ``Target`` lists the gates the converter understands, so
``transpile(qc, backend=mimiq_backend)`` decomposes unsupported gates into
that set before they reach the converter.
"""

from __future__ import annotations

from typing import Any, Callable

from qiskit.circuit import Measure, Reset
from qiskit.circuit.library import (
    CCXGate,
    CHGate,
    CPhaseGate,
    CRXGate,
    CRYGate,
    CRZGate,
    CSGate,
    CSdgGate,
    CSwapGate,
    CSXGate,
    CUGate,
    CXGate,
    CYGate,
    CZGate,
    DCXGate,
    ECRGate,
    HGate,
    IGate,
    PhaseGate,
    RXGate,
    RXXGate,
    RYGate,
    RYYGate,
    RZGate,
    RZXGate,
    RZZGate,
    SdgGate,
    SGate,
    SwapGate,
    SXdgGate,
    SXGate,
    TdgGate,
    TGate,
    UGate,
    XGate,
    YGate,
    ZGate,
    iSwapGate,
)
from qiskit.circuit import Parameter
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import Target

from mimiq_qiskit.__version__ import __version__
from mimiq_qiskit.converter import qiskit_to_mimiq
from mimiq_qiskit.job import MimiqJob


# Default qubit count advertised to Qiskit. The MIMIQ cloud accepts much
# bigger circuits, but Qiskit's ``Target`` needs a concrete number for
# coupling-map-free (all-to-all) backends. ``run`` itself never rejects a
# larger circuit; this only bounds ``transpile(qc, backend=...)``. Override
# with ``num_qubits=``.
_DEFAULT_NUM_QUBITS = 64

# Execution knobs forwarded to MIMIQ ``execute`` when set (non-``None``).
# These are MIMIQ-specific and have no Qiskit equivalent.
_RUN_OPTION_KEYS = (
    "bonddim",
    "entdim",
    "mpscutoff",
    "mpsmethod",
    "mpotraversal",
    "timelimit",
    "noisemodel",
    "label",
)


def _build_target(num_qubits: int) -> Target:
    """Register supported gates on an all-to-all Target.

    Passing ``properties=None`` for each ``add_instruction`` tells the
    transpiler the gate is available on every qubit or qubit pair with no
    calibration data, which is what a software simulator needs.
    """
    target = Target(num_qubits=num_qubits, description="MIMIQ simulator")

    theta = Parameter("theta")
    phi = Parameter("phi")
    lam = Parameter("lam")
    gam = Parameter("gam")

    one_q: list = [
        IGate(), XGate(), YGate(), ZGate(), HGate(),
        SGate(), SdgGate(), TGate(), TdgGate(),
        SXGate(), SXdgGate(),
        RXGate(theta), RYGate(theta), RZGate(theta),
        PhaseGate(theta), UGate(theta, phi, lam),
    ]
    two_q: list = [
        CXGate(), CYGate(), CZGate(), CHGate(),
        SwapGate(), iSwapGate(),
        CSGate(), CSdgGate(), CSXGate(),
        ECRGate(), DCXGate(),
        CPhaseGate(theta),
        CRXGate(theta), CRYGate(theta), CRZGate(theta),
        CUGate(theta, phi, lam, gam),
        RXXGate(theta), RYYGate(theta), RZZGate(theta), RZXGate(theta),
    ]
    three_q: list = [CCXGate(), CSwapGate()]

    for gate in one_q + two_q + three_q:
        target.add_instruction(gate, properties=None)

    target.add_instruction(Measure(), properties=None)
    target.add_instruction(Reset(), properties=None)
    # Barriers pass through the transpiler regardless of the Target, so
    # registering them here would be redundant.

    return target


def _coerce_runner(runner: Any) -> Callable:
    """Normalise the constructor argument into a batch runner
    ``(circuits, *, nsamples, seed, **opts) -> list[QCSResults]``.

    Order of detection matters: ``RemoteConnection`` instances also
    expose ``execute`` (deprecated alias for ``submit``), so we check
    for the connection shape first and route those through
    ``MimiqRemoteBackend`` to get the polling + result-typing logic
    that's already there.
    """
    # 1. RemoteConnection-shaped: has submit + get_results.
    if callable(getattr(runner, "submit", None)) and callable(
        getattr(runner, "get_results", None)
    ):
        from mimiqcircuits.backends.remote import MimiqRemoteBackend

        return _backend_runner(MimiqRemoteBackend(runner))

    # 2. ``Backend`` instance: has execute returning QCSResults.
    if callable(getattr(runner, "execute", None)):
        return _backend_runner(runner)

    # 3. Plain callable, invoked once per circuit. MIMIQ-specific run
    #    options have no meaning here and are ignored.
    if callable(runner):
        def call(circuits, *, nsamples, seed, **_opts):
            return [
                runner(c, nsamples=nsamples, seed=seed) for c in circuits
            ]

        return call

    raise TypeError(
        f"runner must be a mimiqcircuits Backend, a RemoteConnection, "
        f"or a callable (circuit, *, nsamples, seed) -> QCSResults; "
        f"got {type(runner).__name__}"
    )


def _backend_runner(backend) -> Callable:
    """Wrap a MIMIQ backend so a list of circuits is submitted as one job.

    MIMIQ ``execute`` mirrors its input shape, so we always pass a list
    and normalise the result back to a list of ``QCSResults``.
    """
    def call(circuits, *, nsamples, seed, **opts):
        kwargs = {k: v for k, v in opts.items() if v is not None}
        results = backend.execute(
            list(circuits), nsamples=nsamples, seed=seed, **kwargs
        )
        return results if isinstance(results, list) else [results]

    return call


[docs] class MimiqBackend(BackendV2): """Qiskit BackendV2 powered by MIMIQ. Args: runner: A MIMIQ connection, a MIMIQ ``Backend`` instance, or a ``(circuit, *, nsamples, seed) -> QCSResults`` callable. name: Backend name reported to Qiskit. Defaults to ``"mimiq"``. num_qubits: Qubit count advertised on the ``Target``. The MIMIQ cloud handles many more; raise this when transpiling wide circuits against the backend. description: Human-readable backend description. Beyond ``shots`` and ``seed``, ``run`` accepts MIMIQ-specific options (``bonddim``, ``entdim``, ``mpscutoff``, ``mpsmethod``, ``mpotraversal``, ``timelimit``, ``noisemodel``, ``label``) which are forwarded to MIMIQ when set. """ def __init__( self, runner: Any, *, name: str = "mimiq", num_qubits: int = _DEFAULT_NUM_QUBITS, description: str | None = None, provider=None, ): super().__init__( provider=provider, name=name, description=description or "MIMIQ cloud / local simulator", backend_version=__version__, ) self._runner = _coerce_runner(runner) self._num_qubits = num_qubits self._target = _build_target(num_qubits) # ── BackendV2 surface ────────────────────────────────────────────── @property def target(self) -> Target: return self._target @property def num_qubits(self) -> int: return self._num_qubits @property def max_circuits(self) -> int | None: return None @classmethod def _default_options(cls) -> Options: opts = Options(shots=1024, seed=None) opts.update_options(**{k: None for k in _RUN_OPTION_KEYS}) return opts # ── execution helpers (shared with the primitives) ───────────────── def _execute_batch(self, mimiq_circuits, *, shots, seed, **run_opts): """Submit a list of MIMIQ circuits as one job, blocking for the list of ``QCSResults``. Used by ``run`` and by the native Sampler/Estimator primitives. """ return self._runner( list(mimiq_circuits), nsamples=shots, seed=seed, **run_opts ) def _run_options(self, options: dict) -> dict: """Pick out the MIMIQ-specific run options that are set.""" merged = {k: getattr(self.options, k, None) for k in _RUN_OPTION_KEYS} merged.update( {k: options[k] for k in _RUN_OPTION_KEYS if k in options} ) return {k: v for k, v in merged.items() if v is not None} # ── run ────────────────────────────────────────────────────────────
[docs] def run(self, run_input, **options) -> MimiqJob: """Convert and submit one circuit or a list of circuits to MIMIQ. Args: run_input: A ``QuantumCircuit`` or an iterable of them. **options: ``shots`` and ``seed``, plus any of the MIMIQ-specific run options listed on the class. Options set here override the backend defaults for this call. Returns: A :class:`MimiqJob` running the whole batch as one MIMIQ job; call ``job.result()`` to block for the ``qiskit.result.Result``. """ from qiskit import QuantumCircuit # local import keeps import cost low if isinstance(run_input, QuantumCircuit): qcs = [run_input] else: qcs = list(run_input) mimiq_circuits = [qiskit_to_mimiq(qc) for qc in qcs] shots = int(options.get("shots", self.options.shots)) seed = options.get("seed", self.options.seed) run_opts = self._run_options(options) def work(): return self._execute_batch( mimiq_circuits, shots=shots, seed=seed, **run_opts ) job = MimiqJob( backend=self, qiskit_circuits=qcs, work=work, shots=shots, ) job.submit() return job