From 883626ec920f9c9689bc953bfcdd86550e5120f5 Mon Sep 17 00:00:00 2001 From: Orion Martin <40585662+95-martin-orion@users.noreply.github.com> Date: Fri, 10 Jun 2022 08:49:56 -0700 Subject: [PATCH 1/7] Measurement confusion maps --- .../experiments/readout_confusion_matrix.py | 28 ++++ .../readout_confusion_matrix_test.py | 21 +++ cirq-core/cirq/ops/measure_util.py | 9 +- cirq-core/cirq/ops/measure_util_test.py | 4 + cirq-core/cirq/ops/measurement_gate.py | 108 ++++++++++---- cirq-core/cirq/ops/measurement_gate_test.py | 141 ++++++++++++++---- .../json_test_data/MeasurementGate.json | 31 ++++ .../json_test_data/MeasurementGate.repr | 3 +- cirq-core/cirq/sim/simulation_state.py | 30 +++- cirq-core/cirq/sim/simulation_state_test.py | 2 +- 10 files changed, 319 insertions(+), 58 deletions(-) diff --git a/cirq-core/cirq/experiments/readout_confusion_matrix.py b/cirq-core/cirq/experiments/readout_confusion_matrix.py index c7a4526f2b7..cfb98d00541 100644 --- a/cirq-core/cirq/experiments/readout_confusion_matrix.py +++ b/cirq-core/cirq/experiments/readout_confusion_matrix.py @@ -113,6 +113,34 @@ def __init__( if sum(len(q) for q in self._measure_qubits) != len(self._qubits): raise ValueError(f"Repeated qubits not allowed in measure_qubits: {measure_qubits}.") + @classmethod + def from_measurement( + cls, gate: ops.MeasurementGate, qubits: Sequence['cirq.Qid'] + ) -> 'TensoredConfusionMatrices': + """Generates TCM for the confusion map in a MeasurementGate. + + This ignores any invert_mask defined for the gate - it only replicates the confusion map. + + Args: + gate: the MeasurementGate to match. + qubits: qubits the gate is applied to. + + Returns: + TensoredConfusionMatrices matching the confusion map of the given gate. + + Raises: + ValueError: if the gate has no confusion map. + """ + if not gate.confusion_map: + raise ValueError(f"Measurement has no confusion matrices: {gate}") + confusion_matrices = [] + ordered_qubits = [] + for indices, cm in gate.confusion_map.items(): + confusion_matrices.append(cm) + ordered_qubits.append(tuple(qubits[idx] for idx in indices)) + # Use zero for reps/timestamp to mark fake data. + return cls(confusion_matrices, ordered_qubits, repetitions=0, timestamp=0) + @property def repetitions(self) -> int: """The number of repetitions that were used to estimate the confusion matrices.""" diff --git a/cirq-core/cirq/experiments/readout_confusion_matrix_test.py b/cirq-core/cirq/experiments/readout_confusion_matrix_test.py index 228b6f90de3..bfd13e30cb6 100644 --- a/cirq-core/cirq/experiments/readout_confusion_matrix_test.py +++ b/cirq-core/cirq/experiments/readout_confusion_matrix_test.py @@ -83,6 +83,27 @@ def l2norm(result: np.ndarray): assert l2norm(corrected_result) <= l2norm(sampled_result) +def test_from_measurement(): + qubits = cirq.LineQubit.range(3) + confuse_02 = np.array([[0, 1, 0, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 0, 1, 0]]) + confuse_1 = np.array([[0, 1], [1, 0]]) + op = cirq.measure( + *qubits, + key='a', + invert_mask=(True, False), + confusion_map={(0, 2): confuse_02, (1,): confuse_1}, + ) + tcm = cirq.TensoredConfusionMatrices.from_measurement(op.gate, op.qubits) + expected_tcm = cirq.TensoredConfusionMatrices( + [confuse_02, confuse_1], ((qubits[0], qubits[2]), (qubits[1],)), repetitions=0, timestamp=0 + ) + assert tcm == expected_tcm + + no_cm_op = cirq.measure(*qubits, key='a') + with pytest.raises(ValueError, match="Measurement has no confusion matrices"): + _ = cirq.TensoredConfusionMatrices.from_measurement(no_cm_op.gate, no_cm_op.qubits) + + def test_readout_confusion_matrix_raises(): num_qubits = 2 confusion_matrix = get_expected_cm(num_qubits, 0.1, 0.2) diff --git a/cirq-core/cirq/ops/measure_util.py b/cirq-core/cirq/ops/measure_util.py index 79c1423d937..27119b13da1 100644 --- a/cirq-core/cirq/ops/measure_util.py +++ b/cirq-core/cirq/ops/measure_util.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Callable, Iterable, List, overload, Optional, Tuple, TYPE_CHECKING, Union +from typing import Callable, Dict, Iterable, List, overload, Optional, Tuple, TYPE_CHECKING, Union import numpy as np @@ -107,6 +107,7 @@ def measure( *target, key: Optional[Union[str, 'cirq.MeasurementKey']] = None, invert_mask: Tuple[bool, ...] = (), + confusion_map: Optional[Dict[Tuple[int, ...], np.ndarray]] = None, ) -> raw_types.Operation: """Returns a single MeasurementGate applied to all the given qubits. @@ -121,6 +122,10 @@ def measure( invert_mask: A list of Truthy or Falsey values indicating whether the corresponding qubits should be flipped. None indicates no inverting should be done. + confusion_map: A map of qubit index sets (using indices in + `target`) to the 2D confusion matrix for those qubits. Indices + not included use the identity. Applied before invert_mask if both + are provided. Returns: An operation targeting the given qubits with a measurement. @@ -146,7 +151,7 @@ def measure( if key is None: key = _default_measurement_key(targets) qid_shape = protocols.qid_shape(targets) - return MeasurementGate(len(targets), key, invert_mask, qid_shape).on(*targets) + return MeasurementGate(len(targets), key, invert_mask, qid_shape, confusion_map).on(*targets) @overload diff --git a/cirq-core/cirq/ops/measure_util_test.py b/cirq-core/cirq/ops/measure_util_test.py index 15a24c3fac3..ef8084ea9b7 100644 --- a/cirq-core/cirq/ops/measure_util_test.py +++ b/cirq-core/cirq/ops/measure_util_test.py @@ -46,6 +46,10 @@ def test_measure_qubits(): assert cirq.measure(cirq.LineQid.for_qid_shape((1, 2, 3)), key='a') == cirq.MeasurementGate( num_qubits=3, key='a', qid_shape=(1, 2, 3) ).on(*cirq.LineQid.for_qid_shape((1, 2, 3))) + cmap = {(0,): np.array([[0, 1], [1, 0]])} + assert cirq.measure(a, confusion_map=cmap) == cirq.MeasurementGate( + num_qubits=1, key='a', confusion_map=cmap + ).on(a) with pytest.raises(ValueError, match='ndarray'): _ = cirq.measure(np.array([1, 0])) diff --git a/cirq-core/cirq/ops/measurement_gate.py b/cirq-core/cirq/ops/measurement_gate.py index 646ae00cce0..25d9243c29d 100644 --- a/cirq-core/cirq/ops/measurement_gate.py +++ b/cirq-core/cirq/ops/measurement_gate.py @@ -12,11 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Dict, FrozenSet, Iterable, Optional, Tuple, Sequence, TYPE_CHECKING, Union +from typing import ( + Any, + Dict, + FrozenSet, + Iterable, + Optional, + Tuple, + Sequence, + TYPE_CHECKING, + Union, +) import numpy as np -from cirq import protocols, value +from cirq import _compat, protocols, value from cirq.ops import raw_types if TYPE_CHECKING: @@ -40,6 +50,7 @@ def __init__( key: Union[str, 'cirq.MeasurementKey'] = '', invert_mask: Tuple[bool, ...] = (), qid_shape: Tuple[int, ...] = None, + confusion_map: Optional[Dict[Tuple[int, ...], np.ndarray]] = None, ) -> None: """Inits MeasurementGate. @@ -52,10 +63,15 @@ def __init__( Qubits with indices past the end of the mask are not flipped. qid_shape: Specifies the dimension of each qid the measurement applies to. The default is 2 for every qubit. + confusion_map: A map of qubit index sets (using indices in the + operation generated from this gate) to the 2D confusion matrix + for those qubits. Indices not included use the identity. + Applied before invert_mask if both are provided. Raises: - ValueError: If the length of invert_mask is greater than num_qubits. - or if the length of qid_shape doesn't equal num_qubits. + ValueError: If invert_mask or confusion_map have indices + greater than the available qubit indices, or if the length of + qid_shape doesn't equal num_qubits. """ if qid_shape is None: if num_qubits is None: @@ -74,6 +90,9 @@ def __init__( self._invert_mask = invert_mask or () if self.invert_mask is not None and len(self.invert_mask) > self.num_qubits(): raise ValueError('len(invert_mask) > num_qubits') + self._confusion_map = confusion_map or {} + if any(x >= self.num_qubits() for idx in self._confusion_map for x in idx): + raise ValueError('Confusion matrices have index out of bounds.') @property def key(self) -> str: @@ -87,6 +106,10 @@ def mkey(self) -> 'cirq.MeasurementKey': def invert_mask(self) -> Tuple[bool, ...]: return self._invert_mask + @property + def confusion_map(self) -> Optional[Dict[Tuple['cirq.Qid', ...], np.ndarray]]: + return self._confusion_map + def _qid_shape_(self) -> Tuple[int, ...]: return self._qid_shape @@ -98,7 +121,11 @@ def with_key(self, key: Union[str, 'cirq.MeasurementKey']) -> 'MeasurementGate': if key == self.key: return self return MeasurementGate( - self.num_qubits(), key=key, invert_mask=self.invert_mask, qid_shape=self._qid_shape + self.num_qubits(), + key=key, + invert_mask=self.invert_mask, + qid_shape=self._qid_shape, + confusion_map=self.confusion_map, ) def _with_key_path_(self, path: Tuple[str, ...]): @@ -116,14 +143,22 @@ def _with_measurement_key_mapping_(self, key_map: Dict[str, str]): return self.with_key(protocols.with_measurement_key_mapping(self.mkey, key_map)) def with_bits_flipped(self, *bit_positions: int) -> 'MeasurementGate': - """Toggles whether or not the measurement inverts various outputs.""" + """Toggles whether or not the measurement inverts various outputs. + + This only affects the invert_mask, which is applied after confusion + matrices if any are defined. + """ old_mask = self.invert_mask or () n = max(len(old_mask) - 1, *bit_positions) + 1 new_mask = [k < len(old_mask) and old_mask[k] for k in range(n)] for b in bit_positions: new_mask[b] = not new_mask[b] return MeasurementGate( - self.num_qubits(), key=self.key, invert_mask=tuple(new_mask), qid_shape=self._qid_shape + self.num_qubits(), + key=self.key, + invert_mask=tuple(new_mask), + qid_shape=self._qid_shape, + confusion_map=self.confusion_map, ) def full_invert_mask(self) -> Tuple[bool, ...]: @@ -166,12 +201,17 @@ def _circuit_diagram_info_( self, args: 'cirq.CircuitDiagramInfoArgs' ) -> 'cirq.CircuitDiagramInfo': symbols = ['M'] * self.num_qubits() - - # Show which output bits are negated. - if self.invert_mask: - for i, b in enumerate(self.invert_mask): - if b: - symbols[i] = '!M' + flipped_indices = {i for i, x in enumerate(self.full_invert_mask()) if x} + confused_indices = {x for idxs in self.confusion_map for x in idxs} + + # Show which output bits are negated and/or confused. + for i in range(self.num_qubits()): + prefix = '' + if i in flipped_indices: + prefix += '!' + if i in confused_indices: + prefix += '?' + symbols[i] = prefix + symbols[i] # Mention the measurement key. label_map = args.label_map or {} @@ -184,7 +224,7 @@ def _circuit_diagram_info_( return protocols.CircuitDiagramInfo(symbols) def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optional[str]: - if not all(d == 2 for d in self._qid_shape): + if self.confusion_map or not all(d == 2 for d in self._qid_shape): return NotImplemented args.validate_version('2.0') invert_mask = self.invert_mask @@ -202,7 +242,7 @@ def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optio def _quil_( self, qubits: Tuple['cirq.Qid', ...], formatter: 'cirq.QuilFormatter' ) -> Optional[str]: - if not all(d == 2 for d in self._qid_shape): + if self.confusion_map or not all(d == 2 for d in self._qid_shape): return NotImplemented invert_mask = self.invert_mask if len(invert_mask) < len(qubits): @@ -222,28 +262,39 @@ def _op_repr_(self, qubits: Sequence['cirq.Qid']) -> str: args.append(f'key={self.mkey!r}') if self.invert_mask: args.append(f'invert_mask={self.invert_mask!r}') + if self.confusion_map: + proper_map_str = ', '.join( + f"{k!r}: {_compat.proper_repr(v)}" for k, v in self.confusion_map.items() + ) + args.append(f'confusion_map={{{proper_map_str}}}') arg_list = ', '.join(args) return f'cirq.measure({arg_list})' def __repr__(self): - qid_shape_arg = '' + args = [f'{self.num_qubits()!r}', f'{self.mkey!r}', f'{self.invert_mask}'] if any(d != 2 for d in self._qid_shape): - qid_shape_arg = f', {self._qid_shape!r}' - return ( - f'cirq.MeasurementGate(' - f'{self.num_qubits()!r}, ' - f'{self.mkey!r}, ' - f'{self.invert_mask}' - f'{qid_shape_arg})' - ) + args.append(f'qid_shape={self._qid_shape!r}') + if self.confusion_map: + proper_map_str = ', '.join( + f"{k!r}: {_compat.proper_repr(v)}" for k, v in self.confusion_map.items() + ) + args.append(f'confusion_map={{{proper_map_str}}}') + return f'cirq.MeasurementGate({", ".join(args)})' def _value_equality_values_(self) -> Any: - return self.key, self.invert_mask, self._qid_shape + hashable_cmap = frozenset( + (idxs, tuple(v for _, v in np.ndenumerate(cmap))) + for idxs, cmap in self._confusion_map.items() + ) + return self.key, self.invert_mask, self._qid_shape, hashable_cmap def _json_dict_(self) -> Dict[str, Any]: other = {} if not all(d == 2 for d in self._qid_shape): other['qid_shape'] = self._qid_shape + if self.confusion_map: + json_cmap = [(k, v.tolist()) for k, v in self.confusion_map.items()] + other['confusion_map'] = json_cmap return { 'num_qubits': len(self._qid_shape), 'key': self.key, @@ -252,12 +303,15 @@ def _json_dict_(self) -> Dict[str, Any]: } @classmethod - def _from_json_dict_(cls, num_qubits, key, invert_mask, qid_shape=None, **kwargs): + def _from_json_dict_( + cls, num_qubits, key, invert_mask, qid_shape=None, confusion_map=None, **kwargs + ): return cls( num_qubits=num_qubits, key=value.MeasurementKey.parse_serialized(key), invert_mask=tuple(invert_mask), qid_shape=None if qid_shape is None else tuple(qid_shape), + confusion_map={tuple(k): np.array(v) for k, v in confusion_map or []}, ) def _has_stabilizer_effect_(self) -> Optional[bool]: @@ -268,7 +322,7 @@ def _act_on_(self, sim_state: 'cirq.SimulationStateBase', qubits: Sequence['cirq if not isinstance(sim_state, SimulationState): return NotImplemented - sim_state.measure(qubits, self.key, self.full_invert_mask()) + sim_state.measure(qubits, self.key, self.full_invert_mask(), self.confusion_map) return True diff --git a/cirq-core/cirq/ops/measurement_gate_test.py b/cirq-core/cirq/ops/measurement_gate_test.py index 339a582267c..3b844a3aa6e 100644 --- a/cirq-core/cirq/ops/measurement_gate_test.py +++ b/cirq-core/cirq/ops/measurement_gate_test.py @@ -40,6 +40,11 @@ def test_measure_init(num_qubits): ) assert cirq.MeasurementGate(num_qubits, 'a', invert_mask=(True,)).invert_mask == (True,) assert cirq.qid_shape(cirq.MeasurementGate(num_qubits, 'a')) == (2,) * num_qubits + cmap = {(0,): np.array([[0, 1], [1, 0]])} + assert cirq.MeasurementGate(num_qubits, 'a', confusion_map=cmap).confusion_map == cmap + + +def test_measure_init_num_qubit_agnostic(): assert cirq.qid_shape(cirq.MeasurementGate(3, 'a', qid_shape=(1, 2, 3))) == (1, 2, 3) assert cirq.qid_shape(cirq.MeasurementGate(key='a', qid_shape=(1, 2, 3))) == (1, 2, 3) with pytest.raises(ValueError, match='len.* >'): @@ -48,6 +53,8 @@ def test_measure_init(num_qubits): cirq.MeasurementGate(5, 'a', qid_shape=(1, 2)) with pytest.raises(ValueError, match='valid string'): cirq.MeasurementGate(2, qid_shape=(1, 2), key=None) + with pytest.raises(ValueError, match='Confusion matrices have index out of bounds'): + cirq.MeasurementGate(1, 'a', confusion_map={(1,): np.array([[0, 1], [1, 0]])}) with pytest.raises(ValueError, match='Specify either'): cirq.MeasurementGate() @@ -68,9 +75,13 @@ def test_measurement_eq(): lambda: cirq.MeasurementGate(1, 'a'), lambda: cirq.MeasurementGate(1, 'a', invert_mask=()), lambda: cirq.MeasurementGate(1, 'a', qid_shape=(2,)), + lambda: cirq.MeasurementGate(1, 'a', confusion_map={}), ) eq.add_equality_group(cirq.MeasurementGate(1, 'a', invert_mask=(True,))) eq.add_equality_group(cirq.MeasurementGate(1, 'a', invert_mask=(False,))) + eq.add_equality_group( + cirq.MeasurementGate(1, 'a', confusion_map={(0,): np.array([[0, 1], [1, 0]])}) + ) eq.add_equality_group(cirq.MeasurementGate(1, 'b')) eq.add_equality_group(cirq.MeasurementGate(2, 'a')) eq.add_equality_group( @@ -159,6 +170,30 @@ def test_qudit_measure_quil(): ) +def test_confused_measure_qasm(): + q0 = cirq.LineQubit(0) + assert ( + cirq.qasm( + cirq.measure(q0, key='a', confusion_map={(0,): np.array([[0, 1], [1, 0]])}), + args=cirq.QasmArgs(), + default='not implemented', + ) + == 'not implemented' + ) + + +def test_confused_measure_quil(): + q0 = cirq.LineQubit(0) + qubit_id_map = {q0: '0'} + assert ( + cirq.quil( + cirq.measure(q0, key='a', confusion_map={(0,): np.array([[0, 1], [1, 0]])}), + formatter=cirq.QuilFormatter(qubit_id_map=qubit_id_map, measurement_id_map={}), + ) + is None + ) + + def test_measurement_gate_diagram(): # Shows key. assert cirq.circuit_diagram_info( @@ -199,6 +234,26 @@ def test_measurement_gate_diagram(): a: ───!M─── │ b: ───M──── +""", + ) + cirq.testing.assert_has_diagram( + cirq.Circuit(cirq.measure(a, b, confusion_map={(1,): np.array([[0, 1], [1, 0]])})), + """ +a: ───M──── + │ +b: ───?M─── +""", + ) + cirq.testing.assert_has_diagram( + cirq.Circuit( + cirq.measure( + a, b, invert_mask=(False, True), confusion_map={(1,): np.array([[0, 1], [1, 0]])} + ) + ), + """ +a: ───M───── + │ +b: ───!?M─── """, ) cirq.testing.assert_has_diagram( @@ -280,11 +335,37 @@ def test_op_repr(): "key=cirq.MeasurementKey(name='out'), " "invert_mask=(False, True))" ) + assert repr( + cirq.measure( + a, + b, + key='out', + invert_mask=(False, True), + confusion_map={(0,): np.array([[0, 1], [1, 0]])}, + ) + ) == ( + "cirq.measure(cirq.LineQubit(0), cirq.LineQubit(1), " + "key=cirq.MeasurementKey(name='out'), " + "invert_mask=(False, True), " + "confusion_map={(0,): np.array([[0, 1], [1, 0]], dtype=np.int64)})" + ) + + +def test_repr(): + gate = cirq.MeasurementGate( + 3, 'a', (True, False), (1, 2, 3), {(2,): np.array([[0, 1], [1, 0]])} + ) + assert repr(gate) == ( + "cirq.MeasurementGate(3, cirq.MeasurementKey(name='a'), (True, False), " + "qid_shape=(1, 2, 3), confusion_map={(2,): np.array([[0, 1], [1, 0]], dtype=np.int64)})" + ) def test_act_on_state_vector(): a, b = [cirq.LineQubit(3), cirq.LineQubit(1)] - m = cirq.measure(a, b, key='out', invert_mask=(True,)) + m = cirq.measure( + a, b, key='out', invert_mask=(True,), confusion_map={(1,): np.array([[0, 1], [1, 0]])} + ) args = cirq.StateVectorSimulationState( available_buffer=np.empty(shape=(2, 2, 2, 2, 2)), @@ -294,7 +375,7 @@ def test_act_on_state_vector(): dtype=np.complex64, ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [1, 0]} + assert args.log_of_measurement_results == {'out': [1, 1]} args = cirq.StateVectorSimulationState( available_buffer=np.empty(shape=(2, 2, 2, 2, 2)), @@ -306,7 +387,7 @@ def test_act_on_state_vector(): dtype=np.complex64, ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [1, 1]} + assert args.log_of_measurement_results == {'out': [1, 0]} args = cirq.StateVectorSimulationState( available_buffer=np.empty(shape=(2, 2, 2, 2, 2)), @@ -320,16 +401,18 @@ def test_act_on_state_vector(): cirq.act_on(m, args) datastore = cast(cirq.ClassicalDataDictionaryStore, args.classical_data) out = cirq.MeasurementKey('out') - assert args.log_of_measurement_results == {'out': [0, 1]} - assert datastore.records[out] == [(0, 1)] + assert args.log_of_measurement_results == {'out': [0, 0]} + assert datastore.records[out] == [(0, 0)] cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [0, 1]} - assert datastore.records[out] == [(0, 1), (0, 1)] + assert args.log_of_measurement_results == {'out': [0, 0]} + assert datastore.records[out] == [(0, 0), (0, 0)] def test_act_on_clifford_tableau(): a, b = [cirq.LineQubit(3), cirq.LineQubit(1)] - m = cirq.measure(a, b, key='out', invert_mask=(True,)) + m = cirq.measure( + a, b, key='out', invert_mask=(True,), confusion_map={(1,): np.array([[0, 1], [1, 0]])} + ) # The below assertion does not fail since it ignores non-unitary operations cirq.testing.assert_all_implemented_act_on_effects_match_unitary(m) @@ -339,7 +422,7 @@ def test_act_on_clifford_tableau(): prng=np.random.RandomState(), ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [1, 0]} + assert args.log_of_measurement_results == {'out': [1, 1]} args = cirq.CliffordTableauSimulationState( tableau=cirq.CliffordTableau(num_qubits=5, initial_state=8), @@ -348,7 +431,7 @@ def test_act_on_clifford_tableau(): ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [1, 1]} + assert args.log_of_measurement_results == {'out': [1, 0]} args = cirq.CliffordTableauSimulationState( tableau=cirq.CliffordTableau(num_qubits=5, initial_state=10), @@ -358,16 +441,18 @@ def test_act_on_clifford_tableau(): cirq.act_on(m, args) datastore = cast(cirq.ClassicalDataDictionaryStore, args.classical_data) out = cirq.MeasurementKey('out') - assert args.log_of_measurement_results == {'out': [0, 1]} - assert datastore.records[out] == [(0, 1)] + assert args.log_of_measurement_results == {'out': [0, 0]} + assert datastore.records[out] == [(0, 0)] cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [0, 1]} - assert datastore.records[out] == [(0, 1), (0, 1)] + assert args.log_of_measurement_results == {'out': [0, 0]} + assert datastore.records[out] == [(0, 0), (0, 0)] def test_act_on_stabilizer_ch_form(): a, b = [cirq.LineQubit(3), cirq.LineQubit(1)] - m = cirq.measure(a, b, key='out', invert_mask=(True,)) + m = cirq.measure( + a, b, key='out', invert_mask=(True,), confusion_map={(1,): np.array([[0, 1], [1, 0]])} + ) # The below assertion does not fail since it ignores non-unitary operations cirq.testing.assert_all_implemented_act_on_effects_match_unitary(m) @@ -375,14 +460,14 @@ def test_act_on_stabilizer_ch_form(): qubits=cirq.LineQubit.range(5), prng=np.random.RandomState(), initial_state=0 ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [1, 0]} + assert args.log_of_measurement_results == {'out': [1, 1]} args = cirq.StabilizerChFormSimulationState( qubits=cirq.LineQubit.range(5), prng=np.random.RandomState(), initial_state=8 ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [1, 1]} + assert args.log_of_measurement_results == {'out': [1, 0]} args = cirq.StabilizerChFormSimulationState( qubits=cirq.LineQubit.range(5), prng=np.random.RandomState(), initial_state=10 @@ -390,16 +475,22 @@ def test_act_on_stabilizer_ch_form(): cirq.act_on(m, args) datastore = cast(cirq.ClassicalDataDictionaryStore, args.classical_data) out = cirq.MeasurementKey('out') - assert args.log_of_measurement_results == {'out': [0, 1]} - assert datastore.records[out] == [(0, 1)] + assert args.log_of_measurement_results == {'out': [0, 0]} + assert datastore.records[out] == [(0, 0)] cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [0, 1]} - assert datastore.records[out] == [(0, 1), (0, 1)] + assert args.log_of_measurement_results == {'out': [0, 0]} + assert datastore.records[out] == [(0, 0), (0, 0)] def test_act_on_qutrit(): a, b = [cirq.LineQid(3, dimension=3), cirq.LineQid(1, dimension=3)] - m = cirq.measure(a, b, key='out', invert_mask=(True,)) + m = cirq.measure( + a, + b, + key='out', + invert_mask=(True,), + confusion_map={(1,): np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]])}, + ) args = cirq.StateVectorSimulationState( available_buffer=np.empty(shape=(3, 3, 3, 3, 3)), @@ -411,7 +502,7 @@ def test_act_on_qutrit(): dtype=np.complex64, ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [2, 2]} + assert args.log_of_measurement_results == {'out': [2, 0]} args = cirq.StateVectorSimulationState( available_buffer=np.empty(shape=(3, 3, 3, 3, 3)), @@ -423,7 +514,7 @@ def test_act_on_qutrit(): dtype=np.complex64, ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [2, 1]} + assert args.log_of_measurement_results == {'out': [2, 2]} args = cirq.StateVectorSimulationState( available_buffer=np.empty(shape=(3, 3, 3, 3, 3)), @@ -435,4 +526,4 @@ def test_act_on_qutrit(): dtype=np.complex64, ) cirq.act_on(m, args) - assert args.log_of_measurement_results == {'out': [0, 2]} + assert args.log_of_measurement_results == {'out': [0, 0]} diff --git a/cirq-core/cirq/protocols/json_test_data/MeasurementGate.json b/cirq-core/cirq/protocols/json_test_data/MeasurementGate.json index 90fb3dc466d..d107f08f7bb 100644 --- a/cirq-core/cirq/protocols/json_test_data/MeasurementGate.json +++ b/cirq-core/cirq/protocols/json_test_data/MeasurementGate.json @@ -28,5 +28,36 @@ 2, 3 ] + }, + { + "cirq_type": "MeasurementGate", + "num_qubits": 3, + "key": "a", + "invert_mask": [ + true, + false + ], + "confusion_map": [ + [ + [ + 1 + ], + [ + [ + 0, + 1 + ], + [ + 1, + 0 + ] + ] + ] + ], + "qid_shape": [ + 1, + 2, + 3 + ] } ] \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/MeasurementGate.repr b/cirq-core/cirq/protocols/json_test_data/MeasurementGate.repr index 15c1b50dea5..513bbe82473 100644 --- a/cirq-core/cirq/protocols/json_test_data/MeasurementGate.repr +++ b/cirq-core/cirq/protocols/json_test_data/MeasurementGate.repr @@ -1,5 +1,6 @@ [ cirq.MeasurementGate(3, 'z', ()), cirq.MeasurementGate(3, 'z', (True, False, True)), - cirq.MeasurementGate(3, cirq.MeasurementKey(path=('a', 'b'), name='z'), (True, False), (1, 2, 3)), + cirq.MeasurementGate(3, cirq.MeasurementKey(path=('a', 'b'), name='z'), (True, False), qid_shape=(1, 2, 3)), + cirq.MeasurementGate(3, cirq.MeasurementKey(name='a'), (True, False), qid_shape=(1, 2, 3), confusion_map={(1,): np.array([[0, 1], [1, 0]], dtype=np.int64)}), ] diff --git a/cirq-core/cirq/sim/simulation_state.py b/cirq-core/cirq/sim/simulation_state.py index 401d6188777..240802cf821 100644 --- a/cirq-core/cirq/sim/simulation_state.py +++ b/cirq-core/cirq/sim/simulation_state.py @@ -100,7 +100,13 @@ def __init__( def prng(self) -> np.random.RandomState: return self._prng - def measure(self, qubits: Sequence['cirq.Qid'], key: str, invert_mask: Sequence[bool]): + def measure( + self, + qubits: Sequence['cirq.Qid'], + key: str, + invert_mask: Sequence[bool], + confusion_map: Dict[Tuple[int, ...], np.ndarray], + ): """Measures the qubits and records to `log_of_measurement_results`. Any bitmasks will be applied to the measurement record. @@ -111,12 +117,14 @@ def measure(self, qubits: Sequence['cirq.Qid'], key: str, invert_mask: Sequence[ that operations should only store results under keys they have declared in a `_measurement_key_names_` method. invert_mask: The invert mask for the measurement. + confusion_map: The confusion matrices for the measurement. Raises: ValueError: If a measurement key has already been logged to a key. """ bits = self._perform_measurement(qubits) - corrected = [bit ^ (bit < 2 and mask) for bit, mask in zip(bits, invert_mask)] + confused = self._confuse_result(bits, qubits, confusion_map) + corrected = [bit ^ (bit < 2 and mask) for bit, mask in zip(confused, invert_mask)] self._classical_data.record_measurement( value.MeasurementKey.parse_serialized(key), corrected, qubits ) @@ -130,6 +138,24 @@ def _perform_measurement(self, qubits: Sequence['cirq.Qid']) -> List[int]: return self._state.measure(self.get_axes(qubits), self.prng) raise NotImplementedError() + def _confuse_result( + self, + bits: List[int], + qubits: Sequence['cirq.Qid'], + confusion_map: Dict[Tuple[int, ...], np.ndarray], + ): + """Applies confusion matrices to measured results.""" + confused = list(bits) + dims = [q.dimension for q in qubits] + for indices, confuser in confusion_map.items(): + mat_dims = [dims[k] for k in indices] + row = value.big_endian_digits_to_int((bits[k] for k in indices), base=mat_dims) + new_val = self.prng.choice(len(confuser), p=confuser[row]) + new_bits = value.big_endian_int_to_digits(new_val, base=mat_dims) + for i, k in enumerate(indices): + confused[k] = new_bits[i] + return confused + def sample( self, qubits: Sequence['cirq.Qid'], diff --git a/cirq-core/cirq/sim/simulation_state_test.py b/cirq-core/cirq/sim/simulation_state_test.py index 6c64ceee268..87bc80bf1e0 100644 --- a/cirq-core/cirq/sim/simulation_state_test.py +++ b/cirq-core/cirq/sim/simulation_state_test.py @@ -44,7 +44,7 @@ def _act_on_fallback_( def test_measurements(): args = DummySimulationState() - args.measure([cirq.LineQubit(0)], "test", [False]) + args.measure([cirq.LineQubit(0)], "test", [False], {}) assert args.log_of_measurement_results["test"] == [5] From 2fb0721b386bc1728606f908b610cb678346485b Mon Sep 17 00:00:00 2001 From: Orion Martin <40585662+95-martin-orion@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:07:35 -0700 Subject: [PATCH 2/7] format --- cirq-core/cirq/ops/measurement_gate.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/cirq-core/cirq/ops/measurement_gate.py b/cirq-core/cirq/ops/measurement_gate.py index 25d9243c29d..025bdc421b1 100644 --- a/cirq-core/cirq/ops/measurement_gate.py +++ b/cirq-core/cirq/ops/measurement_gate.py @@ -12,17 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import ( - Any, - Dict, - FrozenSet, - Iterable, - Optional, - Tuple, - Sequence, - TYPE_CHECKING, - Union, -) +from typing import Any, Dict, FrozenSet, Iterable, Optional, Tuple, Sequence, TYPE_CHECKING, Union import numpy as np From 55bbaf0ba78be5b30e55d7d785d4b2f6cbf1f42a Mon Sep 17 00:00:00 2001 From: Orion Martin <40585662+95-martin-orion@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:25:33 -0700 Subject: [PATCH 3/7] mypy+format --- cirq-core/cirq/ops/measurement_gate.py | 4 ++-- cirq-core/cirq/ops/measurement_gate_test.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cirq-core/cirq/ops/measurement_gate.py b/cirq-core/cirq/ops/measurement_gate.py index 025bdc421b1..8071b414ac4 100644 --- a/cirq-core/cirq/ops/measurement_gate.py +++ b/cirq-core/cirq/ops/measurement_gate.py @@ -97,7 +97,7 @@ def invert_mask(self) -> Tuple[bool, ...]: return self._invert_mask @property - def confusion_map(self) -> Optional[Dict[Tuple['cirq.Qid', ...], np.ndarray]]: + def confusion_map(self) -> Dict[Tuple[int, ...], np.ndarray]: return self._confusion_map def _qid_shape_(self) -> Tuple[int, ...]: @@ -279,7 +279,7 @@ def _value_equality_values_(self) -> Any: return self.key, self.invert_mask, self._qid_shape, hashable_cmap def _json_dict_(self) -> Dict[str, Any]: - other = {} + other: Dict[str, Any] = {} if not all(d == 2 for d in self._qid_shape): other['qid_shape'] = self._qid_shape if self.confusion_map: diff --git a/cirq-core/cirq/ops/measurement_gate_test.py b/cirq-core/cirq/ops/measurement_gate_test.py index 3b844a3aa6e..cb4f351c757 100644 --- a/cirq-core/cirq/ops/measurement_gate_test.py +++ b/cirq-core/cirq/ops/measurement_gate_test.py @@ -341,7 +341,7 @@ def test_op_repr(): b, key='out', invert_mask=(False, True), - confusion_map={(0,): np.array([[0, 1], [1, 0]])}, + confusion_map={(0,): np.array([[0, 1], [1, 0]], dtype=np.int64)}, ) ) == ( "cirq.measure(cirq.LineQubit(0), cirq.LineQubit(1), " @@ -353,7 +353,7 @@ def test_op_repr(): def test_repr(): gate = cirq.MeasurementGate( - 3, 'a', (True, False), (1, 2, 3), {(2,): np.array([[0, 1], [1, 0]])} + 3, 'a', (True, False), (1, 2, 3), {(2,): np.array([[0, 1], [1, 0]], dtype=np.int64)} ) assert repr(gate) == ( "cirq.MeasurementGate(3, cirq.MeasurementKey(name='a'), (True, False), " From d01d7f040b504a9d77953588d1d93afead5f519d Mon Sep 17 00:00:00 2001 From: Orion Martin <40585662+95-martin-orion@users.noreply.github.com> Date: Fri, 10 Jun 2022 12:21:53 -0700 Subject: [PATCH 4/7] Error on deferred confused measure --- .../cirq/transformers/measurement_transformers.py | 7 +++++++ .../transformers/measurement_transformers_test.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/cirq-core/cirq/transformers/measurement_transformers.py b/cirq-core/cirq/transformers/measurement_transformers.py index 144d8488f0c..24583e5bf1d 100644 --- a/cirq-core/cirq/transformers/measurement_transformers.py +++ b/cirq-core/cirq/transformers/measurement_transformers.py @@ -84,6 +84,8 @@ def defer_measurements( ValueError: If sympy-based classical conditions are used, or if conditions based on multi-qubit measurements exist. (The latter of these is planned to be implemented soon). + NotImplementedError: When attempting to defer a measurement with a + confusion map. (https://github.com/quantumlib/Cirq/issues/5482) """ circuit = transformer_primitives.unroll_circuit_op(circuit, deep=True, tags_to_check=None) @@ -95,6 +97,11 @@ def defer(op: 'cirq.Operation', _) -> 'cirq.OP_TREE': return op gate = op.gate if isinstance(gate, ops.MeasurementGate): + if gate.confusion_map: + raise NotImplementedError( + "Deferring confused measurement is not implemented, but found " + f"measurement with key={gate.key} and non-empty confusion map." + ) key = value.MeasurementKey.parse_serialized(gate.key) targets = [_MeasurementQid(key, q) for q in op.qubits] measurement_qubits[key] = targets diff --git a/cirq-core/cirq/transformers/measurement_transformers_test.py b/cirq-core/cirq/transformers/measurement_transformers_test.py index 2e68aa6abcd..cd1041f2860 100644 --- a/cirq-core/cirq/transformers/measurement_transformers_test.py +++ b/cirq-core/cirq/transformers/measurement_transformers_test.py @@ -286,6 +286,18 @@ def test_sympy_control(): _ = cirq.defer_measurements(circuit) +def test_confusion_map(): + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit( + cirq.measure(q0, q1, key='a', confusion_map={(0,): np.array([[0.9, 0.1], [0.1, 0.9]])}), + cirq.X(q1).with_classical_controls('a'), + ) + with pytest.raises( + NotImplementedError, match='Deferring confused measurement is not implemented' + ): + _ = cirq.defer_measurements(circuit) + + def test_dephase(): q0, q1 = cirq.LineQubit.range(2) circuit = cirq.Circuit( From 096a0a7f5477277a56fc5faa7d7ca7a3bcabe1b1 Mon Sep 17 00:00:00 2001 From: Orion Martin <40585662+95-martin-orion@users.noreply.github.com> Date: Tue, 14 Jun 2022 15:45:37 -0700 Subject: [PATCH 5/7] Also change SimulatesSamples behavior. --- cirq-core/cirq/sim/simulation_state.py | 5 ++++- cirq-core/cirq/sim/simulator.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/sim/simulation_state.py b/cirq-core/cirq/sim/simulation_state.py index 240802cf821..eb8f2e37368 100644 --- a/cirq-core/cirq/sim/simulation_state.py +++ b/cirq-core/cirq/sim/simulation_state.py @@ -144,7 +144,10 @@ def _confuse_result( qubits: Sequence['cirq.Qid'], confusion_map: Dict[Tuple[int, ...], np.ndarray], ): - """Applies confusion matrices to measured results.""" + """Applies confusion matrices to measured results. + + Compare with _confuse_results in cirq-core/cirq/sim/simulator.py. + """ confused = list(bits) dims = [q.dimension for q in qubits] for indices, confuser in confusion_map.items(): diff --git a/cirq-core/cirq/sim/simulator.py b/cirq-core/cirq/sim/simulator.py index e03d7fa39b2..ee69fe6217a 100644 --- a/cirq-core/cirq/sim/simulator.py +++ b/cirq-core/cirq/sim/simulator.py @@ -918,10 +918,12 @@ def sample_measurement_ops( key = gate.key out = np.zeros(shape=(repetitions, len(op.qubits)), dtype=np.int8) inv_mask = gate.full_invert_mask() + cmap = gate.confusion_map for i, q in enumerate(op.qubits): out[:, i] = indexed_sample[:, qubits_to_index[q]] if inv_mask[i]: out[:, i] ^= out[:, i] < 2 + self._confuse_results(out, op.qubits, cmap, seed) if _allow_repeated: if key not in results: results[key] = [] @@ -934,6 +936,28 @@ def sample_measurement_ops( else {k: np.array(v).swapaxes(0, 1) for k, v in results.items()} ) + def _confuse_results( + self, + bits: np.ndarray, + qubits: Sequence['cirq.Qid'], + confusion_map: Dict[Tuple[int, ...], np.ndarray], + seed: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None, + ) -> None: + """Mutates `bits` using the confusion_map. + + Compare with _confuse_result in cirq-core/cirq/sim/simulation_state.py. + """ + prng = value.parse_random_state(seed) + for rep in bits: + dims = [q.dimension for q in qubits] + for indices, confuser in confusion_map.items(): + mat_dims = [dims[k] for k in indices] + row = value.big_endian_digits_to_int((rep[k] for k in indices), base=mat_dims) + new_val = prng.choice(len(confuser), p=confuser[row]) + new_bits = value.big_endian_int_to_digits(new_val, base=mat_dims) + for i, k in enumerate(indices): + rep[k] = new_bits[i] + # When removing this, also remove the check in simulate_sweep_iter. # Basically there should be no "final_step_result" anywhere in the project afterwards. From bcbe37888d2d21953706e4e3e12b05ef5dc485a9 Mon Sep 17 00:00:00 2001 From: Orion Martin <40585662+95-martin-orion@users.noreply.github.com> Date: Wed, 15 Jun 2022 09:01:51 -0700 Subject: [PATCH 6/7] Test SimulatesSamples --- cirq-core/cirq/ops/measurement_gate_test.py | 13 +++++++------ cirq-core/cirq/sim/simulator_test.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/cirq-core/cirq/ops/measurement_gate_test.py b/cirq-core/cirq/ops/measurement_gate_test.py index dcf998bb9cd..ce17956a2f0 100644 --- a/cirq-core/cirq/ops/measurement_gate_test.py +++ b/cirq-core/cirq/ops/measurement_gate_test.py @@ -186,13 +186,14 @@ def test_confused_measure_qasm(): def test_confused_measure_quil(): q0 = cirq.LineQubit(0) qubit_id_map = {q0: '0'} - assert ( - cirq.quil( - cirq.measure(q0, key='a', confusion_map={(0,): np.array([[0, 1], [1, 0]])}), - formatter=cirq.QuilFormatter(qubit_id_map=qubit_id_map, measurement_id_map={}), + with cirq.testing.assert_deprecated(deadline='v1.0', count=3): + assert ( + cirq.quil( + cirq.measure(q0, key='a', confusion_map={(0,): np.array([[0, 1], [1, 0]])}), + formatter=cirq.QuilFormatter(qubit_id_map=qubit_id_map, measurement_id_map={}), + ) + is None ) - is None - ) def test_measurement_gate_diagram(): diff --git a/cirq-core/cirq/sim/simulator_test.py b/cirq-core/cirq/sim/simulator_test.py index 7640f4d228d..af79f1d9808 100644 --- a/cirq-core/cirq/sim/simulator_test.py +++ b/cirq-core/cirq/sim/simulator_test.py @@ -216,6 +216,20 @@ def test_step_sample_measurement_ops_invert_mask(): np.testing.assert_equal(measurements, {'q(0),q(1)': [[True, True]], 'q(2)': [[False]]}) +def test_step_sample_measurement_ops_confusion_map(): + q0, q1, q2 = cirq.LineQubit.range(3) + cmap_01 = {(0, 1): np.array([[0, 1, 0, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 0, 1, 0]])} + cmap_2 = {(0,): np.array([[0, 1], [1, 0]])} + measurement_ops = [ + cirq.measure(q0, q1, confusion_map=cmap_01), + cirq.measure(q2, confusion_map=cmap_2), + ] + step_result = FakeStepResult(ones_qubits=[q2]) + + measurements = step_result.sample_measurement_ops(measurement_ops) + np.testing.assert_equal(measurements, {'q(0),q(1)': [[False, True]], 'q(2)': [[False]]}) + + def test_step_sample_measurement_ops_no_measurements(): step_result = FakeStepResult(ones_qubits=[]) From bba7d5a6b6dfc5b46da3116ce4aaf07deeed74e0 Mon Sep 17 00:00:00 2001 From: Orion Martin <40585662+95-martin-orion@users.noreply.github.com> Date: Wed, 15 Jun 2022 13:38:53 -0700 Subject: [PATCH 7/7] docstring zero note --- cirq-core/cirq/experiments/readout_confusion_matrix.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/experiments/readout_confusion_matrix.py b/cirq-core/cirq/experiments/readout_confusion_matrix.py index cfb98d00541..905f091d514 100644 --- a/cirq-core/cirq/experiments/readout_confusion_matrix.py +++ b/cirq-core/cirq/experiments/readout_confusion_matrix.py @@ -76,7 +76,8 @@ def __init__( the corresponding confusion matrix. repetitions: The number of repetitions that were used to estimate the confusion matrices. - timestamp: The time the data was taken, in seconds since the epoch. + timestamp: The time the data was taken, in seconds since the epoch. This will be + zero for fake data (i.e. data not generated from an experiment). Raises: ValueError: If length of `confusion_matrices` and `measure_qubits` is different or if