Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix uninitialised attributes in QCVV samples #1075

Merged
merged 4 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions supermarq-benchmarks/supermarq/qcvv/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ class Sample:
"""The raw (i.e. pre-compiled) sample circuit."""
data: dict[str, Any]
"""The corresponding data about the circuit"""
probabilities: dict[str, float] = field(init=False)
probabilities: dict[str, float] | None = None
"""The probabilities of the computational basis states"""
job: css.Job | None = None
"""The superstaq job corresponding to the sample. Defaults to None if no job is
associated with the sample."""
compiled_circuit: cirq.Circuit = field(init=False)
compiled_circuit: cirq.Circuit | None = None
"""The compiled circuit. Only used if the circuits are compiled for a specific
target."""

Expand All @@ -55,7 +55,7 @@ def target(self) -> str:
# If there is a job then get the target
return self.job.target()

if hasattr(self, "probabilities"):
if self.probabilities is not None:
# If no job, but probabilities have been calculated, infer that a local
# simulator was used.
return "Local simulator"
Expand All @@ -69,7 +69,7 @@ def circuit(self) -> cirq.Circuit:
The circuit used for the experiment. Defaults to the compiled circuit if available
and if not returns the raw circuit.
"""
if hasattr(self, "compiled_circuit"):
if self.compiled_circuit is not None:
return self.compiled_circuit

return self.raw_circuit
Expand Down Expand Up @@ -330,9 +330,7 @@ def _retrieve_jobs(self) -> dict[str, str]:
return {}

statuses = {}
waiting_samples = [
sample for sample in self.samples if not hasattr(sample, "probabilities")
]
waiting_samples = [sample for sample in self.samples if sample.probabilities is None]
for sample in tqdm(waiting_samples, "Retrieving jobs"):
if sample.job is None:
continue
Expand All @@ -354,7 +352,7 @@ def _has_raw_data(self) -> None:
Raises:
RuntimeError: If any samples already have probabilities stored.
"""
if any(hasattr(sample, "probabilities") for sample in self.samples):
if any(sample.probabilities is not None for sample in self.samples):
raise RuntimeError(
"Some samples have already been run. Re-running the experiment will"
"overwrite these results. If this is the desired behaviour use `overwrite=True`"
Expand Down Expand Up @@ -435,7 +433,7 @@ def collect_data(self, force: bool = False) -> bool:
if not force:
return False

completed_samples = [sample for sample in self.samples if hasattr(sample, "probabilities")]
completed_samples = [sample for sample in self.samples if sample.probabilities is not None]

if not len(completed_samples) == len(self.samples):
print("Some samples do not have probability results.")
Expand Down
6 changes: 3 additions & 3 deletions supermarq-benchmarks/supermarq/qcvv/base_experiment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,8 @@ def test_retrieve_jobs_not_all_submitted(
statuses = abc_experiment._retrieve_jobs()

assert statuses == {"example_job_id": "Queued"}
assert not hasattr(sample_circuits[0], "probabilities")
assert not hasattr(sample_circuits[1], "probabilities")
assert sample_circuits[0].probabilities is None
assert sample_circuits[1].probabilities is None


def test_retrieve_jobs_nothing_to_retrieve(
Expand Down Expand Up @@ -445,7 +445,7 @@ def test_retrieve_jobs_all_submitted(

# Check probabilities correctly updated
assert sample_circuits[1].probabilities == {"00": 5 / 15, "01": 0.0, "10": 0.0, "11": 10 / 15}
assert not hasattr(sample_circuits[0], "probabilities")
assert sample_circuits[0].probabilities is None


def test_collect_data_no_samples(abc_experiment: BenchmarkingExperiment[ExampleResults]) -> None:
Expand Down
29 changes: 20 additions & 9 deletions supermarq-benchmarks/supermarq/qcvv/irb.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from __future__ import annotations

import random
import warnings
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from typing import Union # noqa: MDA400
Expand Down Expand Up @@ -504,16 +505,26 @@ def _process_probabilities(self, samples: Sequence[Sample]) -> pd.DataFrame:
"""

records = []
missing_count = 0 # Count the number of samples that do not have probabilities saved
for sample in samples:
records.append(
{
"clifford_depth": sample.data["num_cycles"],
"circuit_depth": sample.data["circuit_depth"],
"experiment": sample.data["experiment"],
"single_qubit_gates": sample.data["single_qubit_gates"],
"two_qubit_gates": sample.data["two_qubit_gates"],
**sample.probabilities,
}
if sample.probabilities is not None:
records.append(
{
"clifford_depth": sample.data["num_cycles"],
"circuit_depth": sample.data["circuit_depth"],
"experiment": sample.data["experiment"],
"single_qubit_gates": sample.data["single_qubit_gates"],
"two_qubit_gates": sample.data["two_qubit_gates"],
**sample.probabilities,
}
)
else:
missing_count += 1

if missing_count > 0:
warnings.warn(
f"{missing_count} sample(s) are missing probabilities. "
"These samples have been omitted."
)

return pd.DataFrame(records)
Expand Down
24 changes: 24 additions & 0 deletions supermarq-benchmarks/supermarq/qcvv/irb_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,30 @@ def test_irb_process_probabilities(irb_experiment: IRB) -> None:
pd.testing.assert_frame_equal(expected_data, data)


def test_irb_process_probabilities_missing_probs(irb_experiment: IRB) -> None:
samples = [
Sample(
raw_circuit=cirq.Circuit(),
data={
"num_cycles": 20,
"circuit_depth": 23,
"experiment": "example",
"single_qubit_gates": 10,
"two_qubit_gates": 15,
},
)
]

with pytest.warns(
UserWarning,
match=r"1 sample\(s\) are missing probabilities. These samples have been omitted.",
):
data = irb_experiment._process_probabilities(samples)

expected_data = pd.DataFrame()
pd.testing.assert_frame_equal(expected_data, data)


def test_irb_build_circuit(irb_experiment: IRB) -> None:
with patch("supermarq.qcvv.irb.random_single_qubit_clifford") as mock_random_clifford:
mock_random_clifford.side_effect = [
Expand Down
60 changes: 36 additions & 24 deletions supermarq-benchmarks/supermarq/qcvv/xeb.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

import itertools
import random
import warnings
from collections.abc import Iterable, Sequence
from dataclasses import dataclass, field
from dataclasses import dataclass

import cirq
import numpy as np
Expand All @@ -35,9 +36,9 @@
class XEBSample(Sample):
"""The samples used in XEB experiments."""

target_probabilities: dict[str, float] = field(init=False)
target_probabilities: dict[str, float] | None = None
"""The target probabilities obtained through a noiseless simulator"""
sample_probabilities: dict[str, float] = field(init=False)
sample_probabilities: dict[str, float] | None = None
"""The sample probabilities obtained from the chosen target"""

def sum_target_probs_square(self) -> float:
Expand All @@ -49,7 +50,7 @@ def sum_target_probs_square(self) -> float:
Returns:
float: The sum of squared target probabilities.
"""
if not hasattr(self, "target_probabilities"):
if self.target_probabilities is None:
raise RuntimeError("`target_probabilities` have not yet been initialised")

return sum(prob**2 for prob in self.target_probabilities.values())
Expand All @@ -64,10 +65,10 @@ def sum_target_cross_sample_probs(self) -> float:
Returns:
float: The dot product between the sample and target probabilities.
"""
if not hasattr(self, "target_probabilities"):
if self.target_probabilities is None:
raise RuntimeError("`target_probabilities` have not yet been initialised")

if not hasattr(self, "sample_probabilities"):
if self.sample_probabilities is None:
raise RuntimeError("`sample_probabilities` have not yet been initialised")

return sum(
Expand Down Expand Up @@ -247,32 +248,43 @@ def _process_probabilities(
Returns:
A data frame of the full results needed to analyse the experiment.
"""

samples = list(samples)
missing_count = 0
for sample in samples:
sample.sample_probabilities = sample.probabilities
sample.probabilities = {}
if sample.probabilities is None:
missing_count += 1
samples.remove(sample)
else:
sample.sample_probabilities = sample.probabilities
sample.probabilities = {}
if missing_count > 0:
warnings.warn(
f"{missing_count} sample(s) are missing `probabilities`. "
"These samples have been omitted."
)

for sample in tqdm.notebook.tqdm(samples, desc="Evaluating circuits"):
sample.target_probabilities = self._simulate_sample(sample)

records = []
for sample in samples:
target_probabilities = {
f"p({key})": value for key, value in sample.target_probabilities.items()
}
sample_probabilities = {
f"p^({key})": value for key, value in sample.sample_probabilities.items()
}
records.append(
{
"cycle_depth": sample.data["num_cycles"],
"circuit_depth": sample.data["circuit_depth"],
**target_probabilities,
**sample_probabilities,
"sum_p(x)p(x)": sample.sum_target_probs_square(),
"sum_p(x)p^(x)": sample.sum_target_cross_sample_probs(),
if sample.sample_probabilities is not None and sample.target_probabilities is not None:
target_probabilities = {
f"p({key})": value for key, value in sample.target_probabilities.items()
}
)
sample_probabilities = {
f"p^({key})": value for key, value in sample.sample_probabilities.items()
}
records.append(
{
"cycle_depth": sample.data["num_cycles"],
"circuit_depth": sample.data["circuit_depth"],
**target_probabilities,
**sample_probabilities,
"sum_p(x)p(x)": sample.sum_target_probs_square(),
"sum_p(x)p^(x)": sample.sum_target_cross_sample_probs(),
}
)
return pd.DataFrame(records)

def _simulate_sample(self, sample: XEBSample) -> dict[str, float]:
Expand Down
27 changes: 27 additions & 0 deletions supermarq-benchmarks/supermarq/qcvv/xeb_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,33 @@ def test_xeb_process_probabilities(xeb_experiment: XEB) -> None:
pd.testing.assert_frame_equal(expected_data, data)


def test_xeb_process_probabilities_missing_probs(xeb_experiment: XEB) -> None:
qubits = cirq.LineQubit.range(2)

samples = [
XEBSample(
raw_circuit=cirq.Circuit(
[
cirq.X(qubits[0]),
cirq.X(qubits[1]),
cirq.CX(qubits[0], qubits[1]),
cirq.X(qubits[0]),
cirq.X(qubits[1]),
cirq.measure(qubits),
]
),
data={"circuit_depth": 3, "num_cycles": 1, "two_qubit_gate": "CX"},
)
]

with pytest.warns(
UserWarning,
match=r"1 sample\(s\) are missing `probabilities`. These samples have been omitted.",
):
data = xeb_experiment._process_probabilities(samples)
pd.testing.assert_frame_equal(data, pd.DataFrame())


def test_xebsample_sum_probs_square_no_values() -> None:
sample = XEBSample(raw_circuit=cirq.Circuit(), data={})
with pytest.raises(RuntimeError, match="`target_probabilities` have not yet been initialised"):
Expand Down