import json
from typing import Dict, Optional, Tuple, Union
from qibo import gates
from qibo.config import raise_error
from qibo.gates.abstract import Gate
from qibo.gates.gates import Z
from qibo.measurements import MeasurementResult
[docs]class M(Gate):
"""The measure gate.
Args:
*q (int): id numbers of the qubits to measure.
It is possible to measure multiple qubits using ``gates.M(0, 1, 2, ...)``.
If the qubits to measure are held in an iterable (eg. list) the ``*``
operator can be used, for example ``gates.M(*[0, 1, 4])`` or
``gates.M(*range(5))``.
register_name (str, optional): Optional name of the register to distinguish it
from other registers when used in circuits.
collapse (bool): Collapse the state vector after the measurement is
performed. Can be used only for single shot measurements.
If ``True`` the collapsed state vector is returned. If ``False``
the measurement result is returned.
basis (:class:`qibo.gates.Gate` or str or list, optional): Basis to measure.
Can be either:
- a qibo gate
- the string representing the gate
- a callable that accepts a qubit, for example: ``lambda q: gates.RX(q, 0.2)``
- a list of the above, if a different basis will be used for each measurement qubit.
Defaults is to :class:`qibo.gates.Z`.
p0 (dict, optional): bitflip probability map. Can be:
A dictionary that maps each measured qubit to the probability
that it is flipped, a list or tuple that has the same length
as the tuple of measured qubits or a single float number.
If a single float is given the same probability will be used
for all qubits.
p1 (dict, optional): bitflip probability map for asymmetric bitflips.
Same as ``p0`` but controls the 1->0 bitflip probability.
If ``p1`` is ``None`` then ``p0`` will be used both for 0->1 and
1->0 bitflips.
"""
def __init__(
self,
*q,
register_name: Optional[str] = None,
collapse: bool = False,
basis: Union[Gate, str] = Z,
p0: Optional["ProbsType"] = None, # type: ignore
p1: Optional["ProbsType"] = None, # type: ignore
):
super().__init__()
self.name = "measure"
self.draw_label = "M"
self.target_qubits = tuple(q)
self.register_name = register_name
self.collapse = collapse
self.result = MeasurementResult(self.target_qubits)
# list of measurement pulses implementing the gate
# relevant for experiments only
self.pulses = None
# saving basis for __repr__ ans save to file
to_gate = lambda x: getattr(gates, x) if isinstance(x, str) else x
if not isinstance(basis, list):
self.basis_gates = len(q) * [to_gate(basis)]
basis = len(self.target_qubits) * [basis]
elif len(basis) != len(self.target_qubits):
raise_error(
ValueError,
f"Given basis list has length {len(basis)} while "
f"we are measuring {len(self.target_qubits)} qubits.",
)
else:
self.basis_gates = [to_gate(g) for g in basis]
self.init_args = q
self.init_kwargs = {
"register_name": register_name,
"collapse": collapse,
"basis": [g.__name__ for g in self.basis_gates],
"p0": p0,
"p1": p1,
}
if collapse:
if p0 is not None or p1 is not None:
raise_error(
NotImplementedError,
"Bitflip measurement noise is not available when collapsing.",
)
if p1 is None:
p1 = p0
if p0 is None:
p0 = p1
self.bitflip_map = (self._get_bitflip_map(p0), self._get_bitflip_map(p1))
# list of gates that will be added to the circuit before the
# measurement, in order to rotate to the given basis
self.basis = []
for qubit, basis_cls in zip(self.target_qubits, self.basis_gates):
gate = basis_cls(qubit).basis_rotation()
if gate is not None:
self.basis.append(gate)
@property
def raw(self) -> dict:
"""Serialize to dictionary.
The values used in the serialization should be compatible with a
JSON dump (or any other one supporting a minimal set of scalar
types). Though the specific implementation is up to the specific
gate.
"""
encoded_simple = super().raw
encoded_simple.update({"measurement_result": self.result.raw})
return encoded_simple
@staticmethod
def _get_bitflip_tuple(
qubits: Tuple[int, ...], probs: "ProbsType" # type: ignore
) -> Tuple[float, ...]:
if isinstance(probs, float):
if probs < 0 or probs > 1: # pragma: no cover
raise_error(ValueError, f"Invalid bitflip probability {probs}.")
return len(qubits) * (probs,)
if isinstance(probs, (tuple, list)):
if len(probs) != len(qubits):
raise_error(
ValueError,
f"{len(qubits)} qubits were measured but the given "
+ f"bitflip probability list contains {len(probs)} values.",
)
return tuple(probs)
if isinstance(probs, dict):
diff = set(probs.keys()) - set(qubits)
if diff:
raise_error(
KeyError,
f"Bitflip map contains {diff} qubits that are not measured.",
)
return tuple(probs[q] if q in probs else 0.0 for q in qubits)
raise_error(TypeError, f"Invalid type {probs} of bitflip map.")
def _get_bitflip_map(self, p: Optional["ProbsType"] = None) -> Dict[int, float]: # type: ignore
"""Creates dictionary with bitflip probabilities."""
if p is None:
return {q: 0 for q in self.qubits}
pt = self._get_bitflip_tuple(self.qubits, p)
return dict(zip(self.qubits, pt))
def has_bitflip_noise(self):
return (
sum(self.bitflip_map[0].values()) > 0
or sum(self.bitflip_map[1].values()) > 0
)
[docs] def add(self, gate):
"""Adds target qubits to a measurement gate.
This method is only used for creating the global measurement gate used
by the :class:`qibo.models.Circuit`.
The user is not supposed to use this method and a ``ValueError`` is
raised if he does so.
Args:
gate: Measurement gate to add its qubits in the current gate.
"""
assert isinstance(gate, self.__class__)
self.target_qubits += gate.target_qubits
self.bitflip_map[0].update(gate.bitflip_map[0])
self.bitflip_map[1].update(gate.bitflip_map[1])
def controlled_by(self, *q):
""""""
raise_error(NotImplementedError, "Measurement gates cannot be controlled.")
def matrix(self, backend=None):
""""""
raise_error(
NotImplementedError, "Measurement gates do not have matrix representation."
)
def apply(self, backend, state, nqubits):
self.result.backend = backend
if not self.collapse:
return state
qubits = sorted(self.target_qubits)
# measure and get result
probs = backend.calculate_probabilities(state, qubits, nqubits)
shot = self.result.add_shot(probs, backend=backend)
# collapse state
return backend.collapse_state(state, qubits, shot, nqubits)
def apply_density_matrix(self, backend, state, nqubits):
self.result.backend = backend
if not self.collapse:
return state
qubits = sorted(self.target_qubits)
# measure and get result
probs = backend.calculate_probabilities_density_matrix(state, qubits, nqubits)
shot = self.result.add_shot(probs, backend=backend)
# collapse state
return backend.collapse_density_matrix(state, qubits, shot, nqubits)
def apply_clifford(self, backend, state, nqubits):
self.result.backend = backend
if not self.collapse:
return state
qubits = sorted(self.target_qubits)
sample = backend.sample_shots(state, qubits, nqubits, 1, self.collapse)
self.result.add_shot_from_sample(sample[0])
return state
[docs] @classmethod
def load(cls, payload):
"""Constructs a measurement gate starting from a json serialized
one."""
args = json.loads(payload)
return cls.from_dict(args)
# Overload on_qubits to copy also gate.result, controlled by can be removed for measurements
[docs] def on_qubits(self, qubit_map) -> "Gate":
"""Creates the same measurement gate targeting different qubits
and preserving the measurement result register.
Args:
qubit_map (dict): dictionary mapping original qubit indices to new ones.
Returns:
:class:`qibo.gates.Gate.M`: object of the original gate type targeting
the given qubits.
Example:
.. testcode::
from qibo import Circuit, gates
measurement = gates.M(0, 1)
circuit = Circuit(3)
circuit.add(measurement.on_qubits({0: 0, 1: 2}))
assert circuit.queue[0].result is measurement.result
circuit.draw()
.. testoutput::
0: ─M─
1: ─|─
2: ─M─
"""
qubits = (qubit_map.get(q) for q in self.qubits)
gate = self.__class__(*qubits, **self.init_kwargs)
gate.result = self.result
return gate