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

Simultaneous readout #4307

Merged
merged 15 commits into from Aug 10, 2021
Merged

Conversation

dstrain115
Copy link
Collaborator

Adds an experiment for simultaneous (parallel) readout.

This experiment initializes a number of bitstrings(trials) and initializes the qubits to those bitstrings and immediately measures them It then estimates zero state and one state errors averaged over all of the trials where the given qubit was initialized to |0> or |1> respectively.

This change also deprecates SingleQubitReadoutResult in favor of an equivalent but more generally named ReadoutExperimentResult.

@dstrain115 dstrain115 requested review from cduck, mrwojtek, vtomole and a team as code owners July 9, 2021 20:38
@dstrain115 dstrain115 requested a review from viathor July 9, 2021 20:38
@google-cla google-cla bot added the cla: yes Makes googlebot stop complaining. label Jul 9, 2021
@dstrain115 dstrain115 requested a review from karlunho July 9, 2021 20:38
Copy link
Collaborator

@MichaelBroughton MichaelBroughton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this code could have been worked in a little more harmoniously with pre-existing simultaneous readout calibration routines which employed a slightly different methodology instead of creating two things that have very similar names that do only slightly different things ? The estimate_single_qubit_readout_errors works in a parallel setting, but it does not use random bitstrings like this method. Could we maybe add a flag to it that tells it to use random bitstrings or not use random bitstrings and then just need one function for this instead of two ?

@dstrain115
Copy link
Collaborator Author

Do you think this code could have been worked in a little more harmoniously with pre-existing simultaneous readout calibration routines which employed a slightly different methodology instead of creating two things that have very similar names that do only slightly different things ? The estimate_single_qubit_readout_errors works in a parallel setting, but it does not use random bitstrings like this method. Could we maybe add a flag to it that tells it to use random bitstrings or not use random bitstrings and then just need one function for this instead of two ?

What do you think of this now? estimate_single_qubit_readout_errorsnow just calls the parallel version with specific parameters, thus preserving backwards compatibility and also simplifying the code.

(Note to self, I need to add a test/check for bit_strings that do not have a |0> and |1> state for each qubit)

Copy link
Collaborator

@MichaelBroughton MichaelBroughton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Doug, good first pass! Left a few comments, some big, some small. Overall I think the code might have a subtle math mistake and could be shortened/tightened up a little bit here and there. There's also a few minor nits.

@@ -96,6 +96,7 @@

from cirq.experiments import (
estimate_single_qubit_readout_errors,
estimate_parallel_readout_errors,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not a huge fan of having these two seperate methods. Reason being is that estimate_single_qubit_readout_errors can still run "parallel operations" in the same way that estimate_parallel_readout_errors does (it just won't use randomized computational basis states). I do also appreciate the need for name consistency with our parallel_p00 and isolated_p11_error etc. So how about this:

  1. rename this to estimate_single_qubit_readout_errors_parallel
  2. In a seperate PR add estimate_single_qubit_readout_errors_isolated and deprecate estimate_single_qubit_readout_errors.

What do you think ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this to "estimate_correlated_single_qubit_readout_errors"

# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing module docstring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

bit_strings: Optional[List[int]] = None,
) -> ReadoutExperimentResult:

"""Estimate readout error for qubits simultaneously.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "Estimate single qubit readout error using parallel operations."

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 42 to 44
For each trial, prepare a bitstring of random |0〉and |1〉states for
each state. Measure each qubit. Capture the errors per qubit of
zero and one state over each triel.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little hard to digest for me. What do you think of ?

"For each trial, prepare and then measure a random computational basis bitstring on qubits using parallel gates. Returns a SingleQubitReadoutCalibrationResult which can be used to compute readout errors for each qubit in qubits as well as other quantities." ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

triel -> trial

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done;

zero and one state over each triel.

Args:
sampler: The quantum engine or simulator to run the circuits.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "The cirq.Sampler that will be used to run the readout characterization circuits." So that the docs infra can pickup the link to cirq.Sampler.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 86 to 94
if trials_per_batch is not None:
if trials_per_batch <= 0:
raise ValueError("Must provide non-zero trials_per_batch for readout calibration.")
num_batchs = trials // trials_per_batch
if trials % trials_per_batch > 0:
num_batchs += 1
else:
num_batchs = 1
trials_per_batch = trials
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we think of this slight reworking ? It's a little easier to digest I think.

if trials_per_batch is None:
   trials_per_batch = trials

if trials_per_batch <= 0:
  raise ValueError("Must provide non-zero trials_per_batch for readout calibration.")

num_batches = (trials + trials_per_batch - 1) // trials_per_batch

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 117 to 129
zero_state_trials: Dict[cirq.Qid, List[float]] = {q: [] for q in qubits}
one_state_trials: Dict[cirq.Qid, List[float]] = {q: [] for q in qubits}
for batch_result in results:
for trial_idx, trial_result in enumerate(batch_result):
for idx, q in enumerate(qubits):
had_x_gate = (bit_strings[idx] >> trial_idx) & 1
if had_x_gate:
one_state_trials[q].append(1 - np.mean(trial_result.measurements[repr(q)]))
else:
zero_state_trials[q].append(np.mean(trial_result.measurements[repr(q)]))

zero_state_errors = {q: np.mean(zero_state_trials[q]) for q in qubits}
one_state_errors = {q: np.mean(one_state_trials[q]) for q in qubits}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried this math might have some wiggle room for errors here. If I break up some batches into the following sizes:
trials = 22
num_batches = 3
and batch_sizes of [10, 10, 2]
Would this code then weight each of the batch sizes equally and increase the weight of the final 2 trial run disproportionately to the other 10 trial runs. Am I missing something here ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good catch! I think it may have been okay since the repetitions are the same for each trial, but I have fixed this to count up the trials anyway, since I think the previous code was error prone if not outright broken.

Comment on lines 95 to 112
for batch in range(num_batchs):
circuit = circuits.Circuit()
single_sweeps = []
for idx, q in enumerate(qubits):
sym_val = f'bit_{idx}'
current_trial = bit_strings[idx]
trial_range = range(batch * trials_per_batch, (batch + 1) * trials_per_batch)
circuit.append(ops.X(q) ** sympy.Symbol(sym_val))
single_sweeps.append(
study.Points(
key=sym_val, points=[(current_trial >> bit) & 1 for bit in trial_range]
)
)

circuit.append(ops.measure_each(*qubits, key_func=repr))
total_sweeps = study.Zip(*single_sweeps)
all_circuits.append(circuit)
all_sweeps.append(total_sweeps)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a stylistic choice here so feel free to ignore, what do you think of seperating things out a little bit to something like (untested):

flip_symbols = sympy.symbols(f'flip_0:{len(qubits)}')
flip_circuit = cirq.Circuit([cirq.X(q) ** s for q, s in zip(qubits, flip_symbols)], [ops.measure_each(*qubits, key_func=repr)])
circuits = [flip_circuit] * num_batches

for batch in range(num_batches):
  # single_sweeps = []
  # for i, current_symbol in enumerate(flip_symbols):
  #   current_sweep = study.Points(key=current_symbol, points=flip_values[batch][i])
  #   single_sweeps.append(ref_sweep)
  single_sweeps = [study.Points(key=symb, points=flip_values[batch][i]) for i, symb in enumerate(flip_symbols)]
  total_sweeps = study.Zip(*single_sweeps)
  all_sweeps.append(total_sweeps)

Where flip_values is a 3d numpy boolean array built from bit_strings, who's first dimension indexes batch, second dimension indexes qubit and third dimension indexes flip value (T/F) ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically did this, but kept it as a 2d array without the batch dimension.

cirq-core/cirq/experiments/readout_experiment_result.py Outdated Show resolved Hide resolved
Comment on lines 26 to 31
@deprecated(
deadline="v0.13",
fix="use cirq.experiments.ReadoutExperimentResult instead",
name="cirq.experiments.SingleQubitReadoutCalibrationResult",
)
class SingleQubitReadoutCalibrationResult(ReadoutExperimentResult):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From our discussion offline, we didn't think there was a compelling enough reason to want to rename this class with no major changes to existing properties or functionality. So this is fine to leave for now and maybe update in a future PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted change.

trials: The number of bitstrings to prepare.
trials_per_batch: If provided, split the experiment into batches
with this number of trials in each batch.
bit_strings: A list of ints that specifies the bit strings for each
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very strange that these are raveled into a 1d list, and backwards to what I would have expected. Why not keep as a 2d array?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to 2d numpy array.

Copy link
Collaborator

@MichaelBroughton MichaelBroughton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few more nits/questions, then we'll be good to merge.

@@ -72,6 +72,7 @@
)

from cirq.experiments.single_qubit_readout_calibration import (
estimate_correlated_single_qubit_readout_errors,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the name change here ? We aren't really estimating the correlations between single qubits.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed back to parallel

Comment on lines 14 to 16
"""Module for supporting single qubit readout experiments using
either correlated or uncorrelated readout statistics.
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: multi line module docstring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

if repetitions <= 0:
raise ValueError("Must provide non-zero repetition for readout calibration.")
if bit_strings is None:
bit_strings = np.array([[random.randint(0, 1) for _ in range(trials)] for _ in qubits])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: np.random.randint(0, 2, size=(len(qubits), trials))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Done.

if trials_per_batch <= 0:
raise ValueError("Must provide non-zero trials_per_batch for readout calibration.")

all_circuits = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: not needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines 134 to 135
bit_strings: Optional numpy array of shape (qubits, trials) where the
first dimension is the qubit (ordered by the qubit order from
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Wouldn't it make more sense to reverse the dimensions here ? That way If the shape is [1, 9] I know I have one trial on nine qubits and it's easy to unpack each bitstring with a simple for bit_string in bit_strings: (See below)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 160 to 164
raise ValueError(
'bit_strings must be numpy array '
f'of shape (qubits, trials) ({len(qubits)}, {trials}) '
f"but was {bit_strings.shape if hasattr(bit_strings, 'shape') else None}"
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about also checking for invalid bitstrings ? Right now I could pass [[47], [23]]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added check.

@@ -67,7 +77,7 @@ def test_estimate_single_qubit_readout_errors_no_noise():
def test_estimate_single_qubit_readout_errors_with_noise():
qubits = cirq.LineQubit.range(5)
sampler = NoisySingleQubitReadoutSampler(p0=0.1, p1=0.2, seed=1234)
repetitions = 1000
repetitions = 2000
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the bump here ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted.

repetitions: The number of measurement repetitions to perform for
each trial.
trials: The number of bitstrings to prepare.
trials_per_batch: If provided, split the experiment into batches
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other random question: Why is trials_per_batch something we want at the experiment level instead of allowing the user to construct a custom cirq.Sampler where they can make arbitrary calls to run_batch and then behind the scenes it get's broken up into chunks by the sampler ? This feels like a strange place to have something like this am I missing something ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, but we don't actually have this sampler right now. The other thing is that this is a sweep (not a batch), so it's a bit harder to break up. We could deprecate this parameter once we have a proper batching sampler maybe?

@dstrain115
Copy link
Collaborator Author

Fixed most of the review comments, but wanted to take a moment to think about the numpy transposition since I am not totally sure which is better.

@mpharrigan
Copy link
Collaborator

re: numpy transposition. If you cirq.measure(*qubits, key='z') the resulting results.measurements['z'] has the "qubit axis" as the second dimension

Copy link
Collaborator

@MichaelBroughton MichaelBroughton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

raise ValueError("Must provide non-zero repetition for readout calibration.")
if bit_strings is None:
bit_strings = np.random.randint(0, 2, size=(trials, len(qubits)))
else:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: else not needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is worth keeping in, since it avoids a scan of the bit_string array when it is not passed in. This probably has a slight performance gain. We don't need to do the sanity checks if we generate the bit_string array.

@dstrain115 dstrain115 merged commit eb72fb5 into quantumlib:master Aug 10, 2021
MichaelBroughton added a commit that referenced this pull request Aug 11, 2021
CirqBot pushed a commit that referenced this pull request Aug 11, 2021
rht pushed a commit to rht/Cirq that referenced this pull request May 1, 2023
Adds an experiment for simultaneous (parallel) readout.

This experiment (estimate_parallel_single_qubit_readout_errors)  initializes a number of bitstrings(trials) and initializes the qubits to those bitstrings and immediately measures them It then estimates zero state and one state errors averaged over all of the trials where the given qubit was initialized to |0> or |1> respectively.  

This change also collapses the isolated single qubit readout experiment to use this function as well.
rht pushed a commit to rht/Cirq that referenced this pull request May 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Makes googlebot stop complaining.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants