import json
from typing import Dict, Optional, Tuple
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 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`, list): Basis to measure.
Can be a qibo gate or a callable that accepts a qubit,
for example: ``lambda q: gates.RX(q, 0.2)``
or a list of these, if a different basis will be used for each
measurement qubit.
Default is 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: Gate = Z,
p0: Optional["ProbsType"] = None,
p1: Optional["ProbsType"] = None,
):
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)
# list of measurement pulses implementing the gate
# relevant for experiments only
self.pulses = None
# saving basis for __repr__ ans save to file
if not isinstance(basis, list):
self.basis_gates = len(q) * [basis]
else:
self.basis_gates = basis
self.init_args = q
self.init_kwargs = {
"register_name": register_name,
"collapse": collapse,
"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
if not isinstance(basis, list):
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.",
)
self.basis = []
for qubit, basis_cls in zip(self.target_qubits, basis):
gate = basis_cls(qubit).basis_rotation()
if gate is not None:
self.basis.append(gate)
@staticmethod
def _get_bitflip_tuple(qubits: Tuple[int], probs: "ProbsType") -> 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]:
"""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 `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)
# 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)
# 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] def to_json(self):
"""Serializes the measurement gate to json."""
encoding = json.loads(super().to_json())
encoding.pop("_control_qubits")
encoding.update({"basis": [g.__name__ for g in self.basis_gates]})
return json.dumps(encoding)
[docs] @classmethod
def load(cls, payload):
"""Constructs a measurement gate starting from a json serialized
one."""
args = json.loads(payload)
# drop general serialization data, unused in this specialized loader
for key in ("name", "init_args", "_class"):
args.pop(key)
qubits = args.pop("_target_qubits")
args["basis"] = [getattr(gates, g) for g in args["basis"]]
args.update(args.pop("init_kwargs"))
return cls(*qubits, **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 (int): Dictionary mapping original qubit indices to new ones.
Returns:
A :class:`qibo.gates.Gate.M` object of the original gate
type targeting the given qubits.
Example:
.. testcode::
from qibo import models, gates
measurement = gates.M(0, 1)
c = models.Circuit(3)
c.add(measurement.on_qubits({0: 0, 1: 2}))
assert c.queue[0].result is measurement.result
print(c.draw())
.. testoutput::
q0: ─M─
q1: ─|─
q2: ─M─
"""
qubits = (qubit_map.get(q) for q in self.qubits)
gate = self.__class__(*qubits, **self.init_kwargs)
gate.result = self.result
return gate