From 2d39ecab58ecbfd5fbe12bec3ff3c03959e594c8 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:57:36 +0400 Subject: [PATCH 1/5] feat: add process tomography routine --- src/qibocal/protocols/__init__.py | 2 + src/qibocal/protocols/process_tomography.py | 370 ++++++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 src/qibocal/protocols/process_tomography.py diff --git a/src/qibocal/protocols/__init__.py b/src/qibocal/protocols/__init__.py index 262203042..1d88ae8b4 100644 --- a/src/qibocal/protocols/__init__.py +++ b/src/qibocal/protocols/__init__.py @@ -27,6 +27,7 @@ from .flux_dependence.qubit_flux_tracking import qubit_flux_tracking from .flux_dependence.resonator_crosstalk import resonator_crosstalk from .flux_dependence.resonator_flux_dependence import resonator_flux +from .process_tomography import process_tomography from .qubit_power_spectroscopy import qubit_power_spectroscopy from .qubit_spectroscopy import qubit_spectroscopy from .qubit_spectroscopy_ef import qubit_spectroscopy_ef @@ -150,4 +151,5 @@ "standard_rb_2q_inter", "optimize_two_qubit_gate", "ramsey_zz", + "process_tomography", ] diff --git a/src/qibocal/protocols/process_tomography.py b/src/qibocal/protocols/process_tomography.py new file mode 100644 index 000000000..4491bf069 --- /dev/null +++ b/src/qibocal/protocols/process_tomography.py @@ -0,0 +1,370 @@ +from collections import defaultdict +from dataclasses import dataclass, field +from itertools import product +from typing import Union + +import numpy as np +import numpy.typing as npt +import plotly.graph_objects as go +from plotly.subplots import make_subplots +from qibo import Circuit, gates +from qibolab import AcquisitionType, ExecutionParameters +from qibolab.platform import Platform +from qibolab.pulses import PulseSequence +from qibolab.qubits import QubitId, QubitPairId + +from qibocal.auto.operation import Data, Parameters, Results, Routine + +PREROTATIONS = [None, "x180", "y90", "-x90"] +POSTROTATIONS = [None, "-y90", "x90"] + + +ProcessTomographyType = np.dtype( + [ + ("probabilities", float), + ] +) +"""Custom dtype for tomography.""" + +Target = Union[QubitId, QubitPairId] +Moments = list[Union[tuple[str], tuple[str, str]]] + + +@dataclass +class ProcessTomographyParameters(Parameters): + circuit: Moments = field(default_factory=list) + """Gates to perform process tomography on.""" + + def __post_init__(self): + self.circuit = [tuple(moment) for moment in self.circuit] + + +@dataclass +class ProcessTomographyData(Data): + """Tomography data.""" + + prerotations: Moments + circuit: Moments + postrotations: Moments + data: dict[Target, npt.NDArray[ProcessTomographyType]] = field(default_factory=dict) + + +def compile( + moments: Moments, platform: Platform, qubits: list[QubitId] +) -> PulseSequence: + sequence = PulseSequence() + phases = defaultdict(float) + for moment in moments: + start = sequence.finish + if moment[0] == "cz": + cz_sequence, cz_phases = platform.pairs[ + tuple(qubits) + ].native_gates.CZ.sequence(start=start) + for q in qubits: + phases[q] -= cz_phases[q] + sequence += cz_sequence + else: + for q, gate in zip(qubits, moment): + phase = phases[q] + if gate == "x180": + sequence.add( + platform.create_RX_pulse(q, start=start, relative_phase=phase) + ) + elif gate == "y180": + sequence.add( + platform.create_RX_pulse( + q, start=start, relative_phase=np.pi / 2 + phase + ) + ) + elif gate == "x90": + sequence.add( + platform.create_RX90_pulse(q, start=start, relative_phase=phase) + ) + elif gate == "y90": + sequence.add( + platform.create_RX90_pulse( + q, start=start, relative_phase=np.pi / 2 + phase + ) + ) + elif gate == "-x90": + sequence.add( + platform.create_RX90_pulse( + q, start=start, relative_phase=np.pi + phase + ) + ) + elif gate == "-y90": + sequence.add( + platform.create_RX90_pulse( + q, start=start, relative_phase=-np.pi / 2 + phase + ) + ) + + start = sequence.finish + for q in qubits: + sequence.add(platform.create_MZ_pulse(q, start=start)) + return sequence + + +def calculate_probabilities(samples: npt.NDArray) -> npt.NDArray: + nshots, nqubits = samples.shape + values, counts = np.unique(samples, axis=0, return_counts=True) + freqs = {"".join([str(x) for x in v]): c for v, c in zip(values, counts)} + assert sum(freqs.values()) == nshots + outcomes = ["{:b}".format(x).zfill(nqubits) for x in range(2**nqubits)] + return np.array([freqs[x] / nshots for x in outcomes]) + + +def _acquisition( + params: ProcessTomographyParameters, platform: Platform, targets: list[Target] +) -> ProcessTomographyData: + """Acquisition protocol for two qubit state tomography experiment.""" + assert len(targets) == 1 + if not isinstance(targets[0], QubitId): + qubits = list(targets[0]) + else: + qubits = list(targets) + + prerotations = list(product(*[PREROTATIONS for _ in qubits])) + postrotations = list(product(*[POSTROTATIONS for _ in qubits])) + + data = ProcessTomographyData(prerotations, params.circuit, postrotations) + options = ExecutionParameters( + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.DISCRIMINATION, + ) + probabilities = [] + for prerot in prerotations: + for postrot in postrotations: + sequence = compile([prerot] + params.circuit + [postrot], platform, qubits) + results = platform.execute_pulse_sequence(sequence, options) + samples = np.stack([results[q].samples for q in qubits]).T + probabilities.append(calculate_probabilities(samples)) + + data.register_qubit( + ProcessTomographyType, + targets[0], + { + "probabilities": np.stack(probabilities), + }, + ) + return data + + +@dataclass +class ProcessTomographyResults(Results): + """Tomography results.""" + + estimated_chi_real: dict[Target, list[list[float]]] = field(default_factory=dict) + estimated_chi_imag: dict[Target, list[list[float]]] = field(default_factory=dict) + target_chi_real: dict[Target, list[list[float]]] = field(default_factory=dict) + target_chi_imag: dict[Target, list[list[float]]] = field(default_factory=dict) + + +GATE_MAP = { + None: lambda q: gates.I(q), + "x180": lambda q: gates.RX(q, theta=np.pi), + "y180": lambda q: gates.RY(q, theta=np.pi), + "x90": lambda q: gates.RX(q, theta=np.pi / 2), + "y90": lambda q: gates.RY(q, theta=np.pi / 2), + "-x90": lambda q: gates.RX(q, theta=-np.pi / 2), + "-y90": lambda q: gates.RY(q, theta=-np.pi / 2), +} + + +def to_circuit(moments: Moments, density_matrix: bool = False) -> Circuit: + nqubits = len(moments[0]) + circuit = Circuit(nqubits, density_matrix=density_matrix) + for moment in moments: + assert len(moment) == nqubits + if moment[0] == "cz": + circuit.add(gates.CZ(0, 1)) + else: + for q, r in enumerate(moment): + if r is not None: + circuit.add(GATE_MAP[r](q)) + circuit.add(gates.M(*range(nqubits))) + return circuit + + +def basis_matrices(rotations: list[tuple[str]]) -> list[npt.NDArray]: + matrices = [] + for rotation in rotations: + g = rotation[0] + matrices.append(GATE_MAP[g](0).matrix()) + for g in rotation[1:]: + matrices[-1] = np.kron(matrices[-1], GATE_MAP[g](0).matrix()) + return matrices + + +def project_psd(matrix): + """Project matrix to the space of positive semidefinite matrices.""" + s, v = np.linalg.eigh(matrix) + s = s * (s > 0) + return v.dot(np.diag(s)).dot(v.conj().T) + + +def state_tomography(data, rotations): + matrices = basis_matrices(rotations) + d = len(matrices[0]) + measurement = np.zeros((0, d**2)) + for u in matrices: + channel = np.kron(u, u.conj()) + measure_channel = channel[np.eye(d, dtype=bool).flatten(), :] + measurement = np.concatenate((measurement, measure_channel)) + + rho_direct_estimate = np.linalg.pinv(measurement).dot(data.flatten()) + rho_direct_estimate = rho_direct_estimate.reshape((d, d)) + rho_direct_estimate_proj = project_psd(rho_direct_estimate) + rho_direct_estimate_proj = rho_direct_estimate_proj / np.trace( + rho_direct_estimate_proj + ) + return rho_direct_estimate_proj + + +def calculate_ideal_basis(d: int): + """Creates density matrix computational basis. + + Args: + d: Density matrix dimension + """ + basis = np.zeros([d**2, d, d], dtype=complex) + for i in range(d): + for j in range(d): + basis[d * i + j, i, j] = 1 + return basis + + +def rotate_to_ideal(rhos, rotations): + d = len(rotations) + ideal_basis = calculate_ideal_basis(int(np.sqrt(d))) + experiment_basis = np.array( + [to_circuit([rot], density_matrix=True)().state() for rot in rotations] + ) + rotation = ideal_basis.reshape((d, d)).dot( + np.linalg.inv(experiment_basis.reshape((d, d))) + ) + return np.einsum("ij,jab->iab", rotation, rhos) + + +def calculate_beta(operators): + d = len(operators) + ideal_basis = calculate_ideal_basis(int(np.sqrt(d))) + beta = np.empty(4 * (d,), dtype=complex) + for m, am in enumerate(operators): + for n, an in enumerate(operators): + for i, rho in enumerate(ideal_basis): + beta[m, n, i] = am.dot(rho.dot(an.conj().T)).flatten() + return beta.reshape((d**2, d**2)) + + +DEFAULT_OPERATORS = np.array( + [ + np.eye(2, dtype=complex), + np.array([[0, 1], [1, 0]], dtype=complex), + -1j * np.array([[0, -1j], [1j, 0]], dtype=complex), + np.array([[1, 0], [0, -1]], dtype=complex), + ] +) + + +def default_operators(d: int): + matrices = DEFAULT_OPERATORS + if d == 4: + return np.copy(matrices) + elif d == 16: + return np.array([np.kron(x, y) for x in matrices for y in matrices]) + raise NotImplementedError + + +def calculate_chi(rho_estimates, operators=None, preparation_rotations=None): + """Calculate channel chi matrix using process tomography. + + Args: + rho_estimates: Density matrix estimates (from state tomography) in + the ideal basis. + operators: Operator basis to write the channel on. + preparation_rotations: Rotations used to prepare initial states. + If not given, ideal basis is assumed. + """ + d = len(rho_estimates) + if operators is None: + operators = default_operators(d) + if preparation_rotations is not None: + rho_estimates = rotate_to_ideal(rho_estimates, preparation_rotations) + + assert len(operators) == d + beta = calculate_beta(operators) + kappa = np.linalg.pinv(beta).T + return kappa.dot(rho_estimates.flatten()).reshape((d, d)) + + +def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: + prerotations = data.prerotations + postrotations = data.postrotations + n = len(postrotations) + results = ProcessTomographyResults() + for target, values in data.data.items(): + probs = values["probabilities"] + estimated_rhos = np.array( + [ + state_tomography(probs[i * n : (i + 1) * n], postrotations) + for i in range(len(prerotations)) + ] + ) + estimated_chi = calculate_chi( + estimated_rhos, preparation_rotations=prerotations + ) + + target_rhos = [ + to_circuit([rot] + data.circuit, density_matrix=True)().state() + for rot in prerotations + ] + target_chi = calculate_chi(target_rhos, preparation_rotations=prerotations) + + results.estimated_chi_real[target] = estimated_chi.real.tolist() + results.estimated_chi_imag[target] = estimated_chi.imag.tolist() + results.target_chi_real[target] = target_chi.real.tolist() + results.target_chi_imag[target] = target_chi.imag.tolist() + + return results + + +def plot_chi(estimated, target): + fig = make_subplots(rows=1, cols=2, subplot_titles=("Reconstruction", "Exact")) + fig.add_trace( + go.Heatmap(z=estimated, coloraxis="coloraxis"), + row=1, + col=1, + ) + fig.add_trace( + go.Heatmap(z=target, coloraxis="coloraxis"), + row=1, + col=2, + ) + fig.update_layout( + coloraxis=dict( + colorscale="plasma", + ), + ) + # Flip the y-axes for both subplots + fig.update_yaxes( + autorange="reversed", row=1, col=1 # Flip the y-axis # First subplot + ) + fig.update_yaxes( + autorange="reversed", row=1, col=2 # Flip the y-axis # Second subplot + ) + return fig + + +def _plot(data: ProcessTomographyData, fit: ProcessTomographyResults, target: Target): + """Plotting for two qubit state tomography.""" + fitting_report = "" + fig_real = plot_chi(fit.estimated_chi_real[target], fit.target_chi_real[target]) + fig_imag = plot_chi(fit.estimated_chi_imag[target], fit.target_chi_imag[target]) + fig_real.update_layout(title="Real") + fig_imag.update_layout(title="Imag") + return [fig_real, fig_imag], fitting_report + + +process_tomography = Routine(_acquisition, _fit, _plot) From 9fde3068f84eee61078ad98aaf305d1e549a8963 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:15:12 +0400 Subject: [PATCH 2/5] chore: improve plot --- src/qibocal/protocols/process_tomography.py | 65 ++++++++++++++++----- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/src/qibocal/protocols/process_tomography.py b/src/qibocal/protocols/process_tomography.py index 4491bf069..02a924535 100644 --- a/src/qibocal/protocols/process_tomography.py +++ b/src/qibocal/protocols/process_tomography.py @@ -8,6 +8,7 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots from qibo import Circuit, gates +from qibo.backends import NumpyBackend from qibolab import AcquisitionType, ExecutionParameters from qibolab.platform import Platform from qibolab.pulses import PulseSequence @@ -304,6 +305,7 @@ def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: postrotations = data.postrotations n = len(postrotations) results = ProcessTomographyResults() + backend = NumpyBackend() for target, values in data.data.items(): probs = values["probabilities"] estimated_rhos = np.array( @@ -317,7 +319,9 @@ def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: ) target_rhos = [ - to_circuit([rot] + data.circuit, density_matrix=True)().state() + backend.execute_circuit( + to_circuit([rot] + data.circuit, density_matrix=True) + ).state() for rot in prerotations ] target_chi = calculate_chi(target_rhos, preparation_rotations=prerotations) @@ -347,24 +351,59 @@ def plot_chi(estimated, target): colorscale="plasma", ), ) - # Flip the y-axes for both subplots - fig.update_yaxes( - autorange="reversed", row=1, col=1 # Flip the y-axis # First subplot - ) - fig.update_yaxes( - autorange="reversed", row=1, col=2 # Flip the y-axis # Second subplot - ) + # Flip the y-axes + fig.update_yaxes(autorange="reversed", row=1, col=1) + fig.update_yaxes(autorange="reversed", row=1, col=2) return fig def _plot(data: ProcessTomographyData, fit: ProcessTomographyResults, target: Target): """Plotting for two qubit state tomography.""" fitting_report = "" - fig_real = plot_chi(fit.estimated_chi_real[target], fit.target_chi_real[target]) - fig_imag = plot_chi(fit.estimated_chi_imag[target], fit.target_chi_imag[target]) - fig_real.update_layout(title="Real") - fig_imag.update_layout(title="Imag") - return [fig_real, fig_imag], fitting_report + + fig = make_subplots( + rows=2, + cols=2, + subplot_titles=( + "Re(Reconstruction)", + "Re(Exact)", + "Im(Reconstruction)", + "Im(Exact)", + ), + vertical_spacing=0.1, + horizontal_spacing=0.08, + ) + fig.add_trace( + go.Heatmap(z=fit.estimated_chi_real[target], coloraxis="coloraxis"), + row=1, + col=1, + ) + fig.add_trace( + go.Heatmap(z=fit.target_chi_real[target], coloraxis="coloraxis"), + row=1, + col=2, + ) + fig.add_trace( + go.Heatmap(z=fit.estimated_chi_imag[target], coloraxis="coloraxis"), + row=2, + col=1, + ) + fig.add_trace( + go.Heatmap(z=fit.target_chi_imag[target], coloraxis="coloraxis"), + row=2, + col=2, + ) + fig.update_layout( + height=800, + coloraxis=dict( + colorscale="RdBu", + ), + ) + # Flip the y-axes + for row in range(1, 3): + for col in range(1, 3): + fig.update_yaxes(autorange="reversed", row=row, col=col) + return [fig], fitting_report process_tomography = Routine(_acquisition, _fit, _plot) From 1aba616c293a8a0b84d6b32929241d309ea372a1 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:16:35 +0400 Subject: [PATCH 3/5] refactor: drop redundant function --- src/qibocal/protocols/process_tomography.py | 43 +++++---------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/src/qibocal/protocols/process_tomography.py b/src/qibocal/protocols/process_tomography.py index 02a924535..85445039d 100644 --- a/src/qibocal/protocols/process_tomography.py +++ b/src/qibocal/protocols/process_tomography.py @@ -152,16 +152,6 @@ def _acquisition( return data -@dataclass -class ProcessTomographyResults(Results): - """Tomography results.""" - - estimated_chi_real: dict[Target, list[list[float]]] = field(default_factory=dict) - estimated_chi_imag: dict[Target, list[list[float]]] = field(default_factory=dict) - target_chi_real: dict[Target, list[list[float]]] = field(default_factory=dict) - target_chi_imag: dict[Target, list[list[float]]] = field(default_factory=dict) - - GATE_MAP = { None: lambda q: gates.I(q), "x180": lambda q: gates.RX(q, theta=np.pi), @@ -300,6 +290,16 @@ def calculate_chi(rho_estimates, operators=None, preparation_rotations=None): return kappa.dot(rho_estimates.flatten()).reshape((d, d)) +@dataclass +class ProcessTomographyResults(Results): + """Tomography results.""" + + estimated_chi_real: dict[Target, list[list[float]]] = field(default_factory=dict) + estimated_chi_imag: dict[Target, list[list[float]]] = field(default_factory=dict) + target_chi_real: dict[Target, list[list[float]]] = field(default_factory=dict) + target_chi_imag: dict[Target, list[list[float]]] = field(default_factory=dict) + + def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: prerotations = data.prerotations postrotations = data.postrotations @@ -334,29 +334,6 @@ def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: return results -def plot_chi(estimated, target): - fig = make_subplots(rows=1, cols=2, subplot_titles=("Reconstruction", "Exact")) - fig.add_trace( - go.Heatmap(z=estimated, coloraxis="coloraxis"), - row=1, - col=1, - ) - fig.add_trace( - go.Heatmap(z=target, coloraxis="coloraxis"), - row=1, - col=2, - ) - fig.update_layout( - coloraxis=dict( - colorscale="plasma", - ), - ) - # Flip the y-axes - fig.update_yaxes(autorange="reversed", row=1, col=1) - fig.update_yaxes(autorange="reversed", row=1, col=2) - return fig - - def _plot(data: ProcessTomographyData, fit: ProcessTomographyResults, target: Target): """Plotting for two qubit state tomography.""" fitting_report = "" From 77406e480d416e827cb502b47ec5bda37ed319ab Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:50:44 +0400 Subject: [PATCH 4/5] fix: circuit simulation --- src/qibocal/protocols/process_tomography.py | 23 +++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/qibocal/protocols/process_tomography.py b/src/qibocal/protocols/process_tomography.py index 85445039d..934f3e385 100644 --- a/src/qibocal/protocols/process_tomography.py +++ b/src/qibocal/protocols/process_tomography.py @@ -16,6 +16,8 @@ from qibocal.auto.operation import Data, Parameters, Results, Routine +from .utils import table_dict, table_html + PREROTATIONS = [None, "x180", "y90", "-x90"] POSTROTATIONS = [None, "-y90", "x90"] @@ -178,6 +180,11 @@ def to_circuit(moments: Moments, density_matrix: bool = False) -> Circuit: return circuit +def simulate_circuit(circuit: Circuit): + backend = NumpyBackend() + return backend.execute_circuit(circuit) + + def basis_matrices(rotations: list[tuple[str]]) -> list[npt.NDArray]: matrices = [] for rotation in rotations: @@ -230,7 +237,10 @@ def rotate_to_ideal(rhos, rotations): d = len(rotations) ideal_basis = calculate_ideal_basis(int(np.sqrt(d))) experiment_basis = np.array( - [to_circuit([rot], density_matrix=True)().state() for rot in rotations] + [ + simulate_circuit(to_circuit([rot], density_matrix=True)).state() + for rot in rotations + ] ) rotation = ideal_basis.reshape((d, d)).dot( np.linalg.inv(experiment_basis.reshape((d, d))) @@ -305,7 +315,6 @@ def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: postrotations = data.postrotations n = len(postrotations) results = ProcessTomographyResults() - backend = NumpyBackend() for target, values in data.data.items(): probs = values["probabilities"] estimated_rhos = np.array( @@ -319,7 +328,7 @@ def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: ) target_rhos = [ - backend.execute_circuit( + simulate_circuit( to_circuit([rot] + data.circuit, density_matrix=True) ).state() for rot in prerotations @@ -336,7 +345,13 @@ def _fit(data: ProcessTomographyResults) -> ProcessTomographyResults: def _plot(data: ProcessTomographyData, fit: ProcessTomographyResults, target: Target): """Plotting for two qubit state tomography.""" - fitting_report = "" + fitting_report = table_html( + table_dict( + [target], + ["Target circuit"], + [str(data.circuit)], + ) + ) fig = make_subplots( rows=2, From ff6583323bb95e89d457c7da174e153f767a0378 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:08:17 +0400 Subject: [PATCH 5/5] docs: add some docstrings --- src/qibocal/protocols/process_tomography.py | 30 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/qibocal/protocols/process_tomography.py b/src/qibocal/protocols/process_tomography.py index 934f3e385..7300b6503 100644 --- a/src/qibocal/protocols/process_tomography.py +++ b/src/qibocal/protocols/process_tomography.py @@ -1,3 +1,9 @@ +"""Process tomography based on https://arxiv.org/abs/quant-ph/9610001 + +Can be used to reconstruct the channel corresponding to the implementation +of a gate or sequence of gates on quantum hardware. +""" + from collections import defaultdict from dataclasses import dataclass, field from itertools import product @@ -27,16 +33,18 @@ ("probabilities", float), ] ) -"""Custom dtype for tomography.""" +"""Custom dtype for process tomography.""" Target = Union[QubitId, QubitPairId] +"""Process tomography works on both single and two qubit circuits.""" Moments = list[Union[tuple[str], tuple[str, str]]] +"""Compact rerpresentation of a circuit on one or two qubits.""" @dataclass class ProcessTomographyParameters(Parameters): circuit: Moments = field(default_factory=list) - """Gates to perform process tomography on.""" + """Circuit for which we reconstruct the channel.""" def __post_init__(self): self.circuit = [tuple(moment) for moment in self.circuit] @@ -47,14 +55,22 @@ class ProcessTomographyData(Data): """Tomography data.""" prerotations: Moments + """Gates used for state preparation.""" circuit: Moments + """Circuit for which we reconstruct the channel.""" postrotations: Moments + """Gates used for rotating to different basis before measurement.""" data: dict[Target, npt.NDArray[ProcessTomographyType]] = field(default_factory=dict) + """Measurement probabilities for all state preparations and measurement bases.""" def compile( moments: Moments, platform: Platform, qubits: list[QubitId] ) -> PulseSequence: + """Compile a circuit given in ``Moments`` format to a qibolab ``PulseSequence``. + + Supports only the following gates: ['cz', 'x180', 'y180', 'x90', 'y90', '-x90', '-y90']. + """ sequence = PulseSequence() phases = defaultdict(float) for moment in moments: @@ -109,6 +125,14 @@ def compile( def calculate_probabilities(samples: npt.NDArray) -> npt.NDArray: + """Converts measurement samples to probabilities. + + Args: + samples: Array of shape ``(nshots, nqubits)``. + + Returns: + Array of probabilities of shape ``(2 ** nqubits,)``. + """ nshots, nqubits = samples.shape values, counts = np.unique(samples, axis=0, return_counts=True) freqs = {"".join([str(x) for x in v]): c for v, c in zip(values, counts)} @@ -120,7 +144,7 @@ def calculate_probabilities(samples: npt.NDArray) -> npt.NDArray: def _acquisition( params: ProcessTomographyParameters, platform: Platform, targets: list[Target] ) -> ProcessTomographyData: - """Acquisition protocol for two qubit state tomography experiment.""" + """Acquisition protocol for process tomography experiment on one or two qubits.""" assert len(targets) == 1 if not isinstance(targets[0], QubitId): qubits = list(targets[0])