Source code for quchip.results.propagator

"""Propagators of a sequence on the dressed states of a product of levels."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import numpy as np

from quchip.utils.constants import TWO_PI
from quchip.utils.jax_utils import contains_tracer

FRAMES = ("dressed", "lab", "integration")


[docs] @dataclass(frozen=True) class PropagatorResult: """Propagator of a sequence on the dressed states of a product of levels. ``matrix[i, j]`` is ``⟨d_i|U(T, t₀)|d_j⟩`` in the record's frame. Here ``|d_k⟩`` is the dressed state of label k under the solve's approximation and excitation cutoff, and U is the propagator from the first saved time t₀ to the last saved time T. A frame gives each label the energy ``E_k = ground_energy + Σᵢ fᵢ kᵢ``, with the frequencies fᵢ of :attr:`frequencies`, and its matrix is ``exp(i 2π T E_i) ⟨d_i|U|d_j⟩_lab exp(−i 2π t₀ E_j)``. The dressed frame turns each selected device at its dressed frequency and removes the dressed ground energy. An idle label with at most one excitation then keeps amplitude 1, and other labels turn at their residual shifts, such as the static ZZ of two qubits. The integration frame gives the label amplitudes of :meth:`~quchip.results.results.SimulationResult.amplitude_array`, and the lab frame keeps every energy. Attributes ---------- matrix : array_like, shape (d, d) Final label in rows and initial label in columns. labels : tuple of tuple of int Level of each selected device, for each row and column. devices : tuple of str Selected devices. Every other device starts and is projected in level 0. leakage : array_like, shape (d,) Probability ``1 − Σᵢ|matrix[i, j]|²`` that input j leaves the labels. It does not depend on the frame. frame : {"dressed", "lab", "integration"} Frame of :attr:`matrix`. frequencies : dict of str to scalar Frame frequency of each selected device, in GHz. ground_energy : scalar Frame energy of the all-ground label, in GHz. times : tuple of scalar Start time t₀ and end time T, in ns. omitted_channels : tuple of str Collapse channels that the solve left out. The matrix is the propagator of the Hamiltonian alone. batch : SimulationBatchResult The solve of each input, in the order of :attr:`labels`. """ matrix: Any labels: tuple[tuple[int, ...], ...] devices: tuple[str, ...] leakage: Any frame: str frequencies: dict[str, Any] ground_energy: Any times: tuple[Any, Any] omitted_channels: tuple[str, ...] batch: Any
[docs] def fidelity(self, target: Any) -> Any: """Return the average gate fidelity of the propagator against a unitary target. For a target V of dimension d, the average over pure inputs in the labels is ``(Tr(M†M) + |Tr(V†M)|²) / (d(d + 1))``, with M the :attr:`matrix`. Leakage lowers ``Tr(M†M)``, so it lowers the fidelity. The target is written in the frame and label order of this record. Parameters ---------- target : array_like, shape (d, d) Unitary target in the frame and label order of this record. """ xp = _namespace(self.matrix, target) matrix = xp.asarray(self.matrix) target = xp.asarray(target, dtype=complex) dimension = matrix.shape[0] if target.shape != matrix.shape: raise ValueError(f"The target has shape {target.shape}; the propagator has shape {matrix.shape}.") kept = xp.real(xp.sum(xp.conj(matrix) * matrix)) overlap = xp.abs(xp.sum(xp.conj(target) * matrix)) ** 2 return (kept + overlap) / (dimension * (dimension + 1))
def _namespace(*values: Any) -> Any: if contains_tracer(values) or any(type(value).__module__.startswith("jax") for value in values): import jax.numpy as jnp return jnp return np
[docs] def subspace_propagator(batch: Any, devices: tuple[str, ...], labels: tuple[tuple[int, ...], ...], frame: str, omitted_channels: tuple[str, ...]) -> PropagatorResult: """Return the propagator of a batch whose points start in the dressed states of ``labels``. Point j of ``batch`` starts in the dressed state of ``labels[j]``. Row i of the integration-frame matrix projects every final state on the label target of ``labels[i]``. Parameters ---------- batch : SimulationBatchResult Ket solves that share one chip, approximation, frame and time grid. devices : tuple of str Selected devices, in the order of the levels of each label. labels : tuple of tuple of int Level of each selected device, for each input. frame : {"dressed", "lab", "integration"} Frame of the returned matrix. omitted_channels : tuple of str Collapse channels that the solves left out. """ if frame not in FRAMES: raise ValueError(f"frame must be one of {FRAMES}; got {frame!r}.") results = tuple(batch) if len(results) != len(labels): raise ValueError(f"Expected {len(labels)} solves, got {len(results)}.") first = results[0] targets = first._label_targets if targets is None or first.device_info is None: raise RuntimeError("These results have no captured chip for label targets; they were not created by a solve.") backend = first._backend order = tuple(label for label, _ in first.device_info) end = first.times[-1:] vectors, finals = [], [] for levels, result in zip(labels, results): if not backend.is_ket(result._raw_final_state()): raise TypeError("A propagator needs ket solves.") fixed, moving = first._resolve_target(dict(zip(devices, levels)), "integration", end) vectors.append(backend.to_array(fixed).reshape(-1) if moving is None else moving[0]) finals.append(backend.to_array(result._raw_final_state()).reshape(-1)) xp = _namespace(vectors, finals) amplitudes = xp.conj(xp.stack(vectors)) @ xp.stack(finals).T leakage = 1.0 - xp.sum(xp.abs(amplitudes) ** 2, axis=0) frame_map = first._frame_map() positions = [order.index(device) for device in devices] integration = {device: frame_map.frequencies[position] for device, position in zip(devices, positions)} if frame == "integration": frequencies, ground = integration, 0.0 elif frame == "lab": frequencies, ground = {device: 0.0 for device in devices}, 0.0 else: vacuum = (0,) * len(order) ground = targets.energy(vacuum) frequencies = {} for device, position in zip(devices, positions): excited = list(vacuum) excited[position] = 1 frequencies[device] = targets.energy(tuple(excited)) - ground # The integration frame gives label k the energy Σᵢ fᵢ kᵢ; the record's frame replaces it with its own. shift = xp.stack([ xp.asarray(ground + sum(frequencies[device] * level - integration[device] * level for device, level in zip(devices, levels)), dtype=float) for levels in labels ]) start, stop = first.times[0], first.times[-1] matrix = (xp.exp(1j * TWO_PI * stop * shift)[:, None] * amplitudes * xp.exp(-1j * TWO_PI * start * shift)[None, :]) return PropagatorResult( matrix=matrix, labels=tuple(labels), devices=tuple(devices), leakage=leakage, frame=frame, frequencies=frequencies, ground_energy=ground, times=(start, stop), omitted_channels=tuple(omitted_channels), batch=batch, )