Source code for pennylane.tape.qscript

# Copyright 2018-2022 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
"""
This module defines the QuantumScript object responsible for storing quantum operations and measurements to be
executed by a device.
"""
# pylint: disable=too-many-instance-attributes, protected-access, too-many-public-methods

import contextlib
import copy
from collections import Counter, defaultdict
from typing import List, Union

import pennylane as qml
from pennylane.measurements import (
    ClassicalShadowMP,
    CountsMP,
    ExpectationMP,
    MeasurementProcess,
    ProbabilityMP,
    SampleMP,
    ShadowExpvalMP,
    StateMP,
    VarianceMP,
)
from pennylane.measurements import Shots
from pennylane.operation import Observable, Operator, Operation
from pennylane.queuing import AnnotatedQueue, process_queue

_empty_wires = qml.wires.Wires([])


OPENQASM_GATES = {
    "CNOT": "cx",
    "CZ": "cz",
    "U3": "u3",
    "U2": "u2",
    "U1": "u1",
    "Identity": "id",
    "PauliX": "x",
    "PauliY": "y",
    "PauliZ": "z",
    "Hadamard": "h",
    "S": "s",
    "Adjoint(S)": "sdg",
    "T": "t",
    "Adjoint(T)": "tdg",
    "RX": "rx",
    "RY": "ry",
    "RZ": "rz",
    "CRX": "crx",
    "CRY": "cry",
    "CRZ": "crz",
    "SWAP": "swap",
    "Toffoli": "ccx",
    "CSWAP": "cswap",
    "PhaseShift": "u1",
}
"""
dict[str, str]: Maps PennyLane gate names to equivalent QASM gate names.

Note that QASM has two native gates:

- ``U`` (equivalent to :class:`~.U3`)
- ``CX`` (equivalent to :class:`~.CNOT`)

All other gates are defined in the file stdgates.inc:
https://github.com/Qiskit/openqasm/blob/master/examples/stdgates.inc
"""


[docs]class QuantumScript: """The state preparation, operations, and measurements that represent instructions for execution on a quantum device. Args: ops (Iterable[Operator]): An iterable of the operations to be performed measurements (Iterable[MeasurementProcess]): All the measurements to be performed prep (Iterable[Operator]): Any state preparations to perform at the start of the circuit Keyword Args: name (str): a name given to the quantum script _update=True (bool): Whether or not to set various properties on initialization. Setting ``_update=False`` reduces computations if the script is only an intermediary step. .. seealso:: :class:`pennylane.tape.QuantumTape` **Example:** .. code-block:: python from pennylane.tape import QuantumScript prep = [qml.BasisState(np.array([1,1]), wires=(0,"a"))] ops = [qml.RX(0.432, 0), qml.RY(0.543, 0), qml.CNOT((0,"a")), qml.RX(0.133, "a")] qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))], prep) >>> list(qscript) [BasisState(array([1, 1]), wires=[0, "a"]), RX(0.432, wires=[0]), RY(0.543, wires=[0]), CNOT(wires=[0, 'a']), RX(0.133, wires=['a']), expval(PauliZ(wires=[0]))] >>> qscript.operations [BasisState(array([1, 1]), wires=[0, "a"]), RX(0.432, wires=[0]), RY(0.543, wires=[0]), CNOT(wires=[0, 'a']), RX(0.133, wires=['a'])] >>> qscript.measurements [expval(PauliZ(wires=[0]))] Iterating over the quantum script can be done by: >>> for op in qscript: ... print(op) BasisState(array([1, 1]), wires=[0, "a"]) RX(0.432, wires=[0]) RY(0.543, wires=[0]) CNOT(wires=[0, 'a']) RX(0.133, wires=['a']) expval(PauliZ(wires=[0]))' Quantum scripts also support indexing and length determination: >>> qscript[0] BasisState(array([1, 1]), wires=[0, "a"]) >>> len(qscript) 6 Once constructed, the script can be executed directly on a quantum device using the :func:`~.pennylane.execute` function: >>> dev = qml.device('default.qubit', wires=(0,'a')) >>> qml.execute([qscript], dev, gradient_fn=None) [array([-0.77750694])] ``ops``, ``measurements``, and ``prep`` are converted to lists upon initialization, so those arguments accept any iterable object: >>> qscript = QuantumScript((qml.PauliX(i) for i in range(3))) >>> qscript.circuit [PauliX(wires=[0]), PauliX(wires=[1]), PauliX(wires=[2])] """ do_queue = False """Whether or not to queue the object. Assumed ``False`` for a vanilla Quantum Script, but may be True for its child Quantum Tape.""" def __init__(self, ops=None, measurements=None, prep=None, name=None, _update=True): self.name = name self._prep = [] if prep is None else list(prep) self._ops = [] if ops is None else list(ops) self._measurements = [] if measurements is None else list(measurements) self._par_info = [] """list[dict[str, Operator or int]]: Parameter information. Values are dictionaries containing the corresponding operation and operation parameter index.""" self._trainable_params = [] self._graph = None self._specs = None self._output_dim = 0 self._batch_size = None self._qfunc_output = None self.wires = _empty_wires self.num_wires = 0 self.is_sampled = False self.all_sampled = False self._obs_sharing_wires = [] """list[.Observable]: subset of the observables that share wires with another observable, i.e., that do not have their own unique set of wires.""" self._obs_sharing_wires_id = [] if _update: self._update() def __repr__(self): return f"<{self.__class__.__name__}: wires={self.wires.tolist()}, params={self.num_params}>" @property def hash(self): """int: returns an integer hash uniquely representing the quantum script""" fingerprint = [] fingerprint.extend(op.hash for op in self.operations) fingerprint.extend(m.hash for m in self.measurements) fingerprint.extend(self.trainable_params) return hash(tuple(fingerprint)) def __iter__(self): """list[.Operator, .MeasurementProcess]: Return an iterator to the underlying quantum circuit object.""" return iter(self.circuit) def __getitem__(self, idx): """list[.Operator]: Return the indexed operator from underlying quantum circuit object.""" return self.circuit[idx] def __len__(self): """int: Return the number of operations and measurements in the underlying quantum circuit object.""" return len(self.circuit) # ======================================================== # QSCRIPT properties # ======================================================== @property def interface(self): """str, None: automatic differentiation interface used by the quantum script (if any)""" return None @property def circuit(self): """Returns the underlying quantum circuit as a list of operations and measurements. The circuit is created with the assumptions that: * The ``operations`` attribute contains quantum operations and mid-circuit measurements and * The ``measurements`` attribute contains terminal measurements. Note that the resulting list could contain MeasurementProcess objects that some devices may not support. Returns: list[.Operator, .MeasurementProcess]: the quantum circuit containing quantum operations and measurements """ return self.operations + self.measurements @property def operations(self) -> List[Operator]: """Returns the state preparations and operations on the quantum script. Returns: list[.Operator]: quantum operations >>> ops = [qml.QubitStateVector([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) >>> qscript.operations [QubitStateVector([0, 1], wires=[0]), RX(0.432, wires=[0])] """ return self._prep + self._ops @property def observables(self) -> List[Union[MeasurementProcess, Observable]]: """Returns the observables on the quantum script. Returns: list[.MeasurementProcess, .Observable]]: list of observables **Example** >>> ops = [qml.QubitStateVector([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) >>> qscript.observables [expval(PauliZ(wires=[0]))] """ # TODO: modify this property once devices # have been refactored to accept and understand recieving # measurement processes rather than specific observables. obs = [] for m in self.measurements: if m.obs is not None: m.obs.return_type = m.return_type obs.append(m.obs) else: obs.append(m) return obs @property def measurements(self) -> List[MeasurementProcess]: """Returns the measurements on the quantum script. Returns: list[.MeasurementProcess]: list of measurement processes **Example** >>> ops = [qml.QubitStateVector([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) >>> qscript.measurements [expval(PauliZ(wires=[0]))] """ return self._measurements @property def samples_computational_basis(self): """Determines if any of the measurements are in the computational basis.""" return any(o.samples_computational_basis for o in self.measurements) @property def num_params(self): """Returns the number of trainable parameters on the quantum script.""" return len(self.trainable_params) @property def batch_size(self): r"""The batch size of the quantum script inferred from the batch sizes of the used operations for parameter broadcasting. .. seealso:: :attr:`~.Operator.batch_size` for details. Returns: int or None: The batch size of the quantum script if present, else ``None``. """ return self._batch_size @property def output_dim(self): """The (inferred) output dimension of the quantum script.""" return self._output_dim @property def diagonalizing_gates(self) -> List[Operation]: """Returns the gates that diagonalize the measured wires such that they are in the eigenbasis of the circuit observables. Returns: List[~.Operation]: the operations that diagonalize the observables """ rotation_gates = [] with qml.queuing.QueuingManager.stop_recording(): for observable in self.observables: # some observables do not have diagonalizing gates, # in which case we just don't append any with contextlib.suppress(qml.operation.DiagGatesUndefinedError): rotation_gates.extend(observable.diagonalizing_gates()) return rotation_gates ##### Update METHODS ############### def _update(self): """Update all internal metadata regarding processed operations and observables""" self._graph = None self._specs = None self._update_circuit_info() # Updates wires, num_wires, is_sampled, all_sampled; O(ops+obs) self._update_par_info() # Updates _par_info; O(ops+obs) # The following line requires _par_info to be up to date self._update_trainable_params() # Updates the _trainable_params; O(1) self._update_observables() # Updates _obs_sharing_wires and _obs_sharing_wires_id self._update_batch_size() # Updates _batch_size; O(ops) # The following line requires _batch_size to be up to date self._update_output_dim() # Updates _output_dim; O(obs) def _update_circuit_info(self): """Update circuit metadata Sets: wires (~.Wires): Wires num_wires (int): Number of wires is_sampled (bool): Whether any measurement is of type ``Sample`` or ``Counts`` all_sampled (bool): Whether all measurements are of type ``Sample`` or ``Counts`` """ self.wires = qml.wires.Wires.all_wires(dict.fromkeys(op.wires for op in self)) self.num_wires = len(self.wires) is_sample_type = [ isinstance(m, (SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP)) for m in self.measurements ] self.is_sampled = any(is_sample_type) self.all_sampled = all(is_sample_type) def _update_par_info(self): """Update the parameter information list. Each entry in the list with an operation and an index into that operation's data. Sets: _par_info (list): Parameter information """ self._par_info = [] for idx, op in enumerate(self.operations): self._par_info.extend( {"op": op, "op_idx": idx, "p_idx": i} for i, d in enumerate(op.data) ) for idx, m in enumerate(self.measurements): if m.obs is not None: self._par_info.extend( {"op": m.obs, "op_idx": idx, "p_idx": i} for i, d in enumerate(m.obs.data) ) def _update_trainable_params(self): """Set the trainable parameters Sets: _trainable_params (list[int]): Script parameter indices of trainable parameters Call `_update_par_info` before `_update_trainable_params` """ self._trainable_params = list(range(len(self._par_info))) def _update_observables(self): """Update information about observables, including the wires that are acted upon and identifying any observables that share wires. Sets: _obs_sharing_wires (list[~.Observable]): Observables that share wires with any other observable _obs_sharing_wires_id (list[int]): Indices of the measurements that contain the observables in _obs_sharing_wires """ obs_wires = [wire for m in self.measurements for wire in m.wires if m.obs is not None] self._obs_sharing_wires = [] self._obs_sharing_wires_id = [] if len(obs_wires) != len(set(obs_wires)): c = Counter(obs_wires) repeated_wires = {w for w in obs_wires if c[w] > 1} for i, m in enumerate(self.measurements): if m.obs is not None and len(set(m.wires) & repeated_wires) > 0: self._obs_sharing_wires.append(m.obs) self._obs_sharing_wires_id.append(i) def _update_batch_size(self): """Infer the batch_size of the quantum script from the batch sizes of its operations and check the latter for consistency. Sets: _batch_size (int): The common batch size of the quantum script operations, if any has one """ candidate = None for op in self.operations: op_batch_size = getattr(op, "batch_size", None) if op_batch_size is None: continue if candidate: if op_batch_size != candidate: raise ValueError( "The batch sizes of the quantum script operations do not match, they include " f"{candidate} and {op_batch_size}." ) else: candidate = op_batch_size self._batch_size = candidate def _update_output_dim(self): """Update the dimension of the output of the quantum script. Sets: self._output_dim (int): Size of the quantum script output (when flattened) This method makes use of `self.batch_size`, so that `self._batch_size` needs to be up to date when calling it. Call `_update_batch_size` before `_update_output_dim` """ self._output_dim = 0 for m in self.measurements: # attempt to infer the output dimension if isinstance(m, ProbabilityMP): # TODO: what if we had a CV device here? Having the base as # 2 would have to be swapped to the cutoff value self._output_dim += 2 ** len(m.wires) elif not isinstance(m, StateMP): self._output_dim += 1 if self.batch_size: self._output_dim *= self.batch_size # ======================================================== # Parameter handling # ======================================================== @property def data(self): """Alias to :meth:`~.get_parameters` and :meth:`~.set_parameters` for backwards compatibilities with operations.""" return self.get_parameters(trainable_only=False) @data.setter def data(self, params): self.set_parameters(params, trainable_only=False) @property def trainable_params(self): """Store or return a list containing the indices of parameters that support differentiability. The indices provided match the order of appearence in the quantum circuit. Setting this property can help reduce the number of quantum evaluations needed to compute the Jacobian; parameters not marked as trainable will be automatically excluded from the Jacobian computation. The number of trainable parameters determines the number of parameters passed to :meth:`~.set_parameters`, and changes the default output size of method :meth:`~.get_parameters()`. .. note:: For devices that support native backpropagation (such as ``default.qubit.tf`` and ``default.qubit.autograd``), this property contains no relevant information when using backpropagation to compute gradients. **Example** >>> ops = [qml.RX(0.432, 0), qml.RY(0.543, 0), ... qml.CNOT((0,"a")), qml.RX(0.133, "a")] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) >>> qscript.trainable_params [0, 1, 2] >>> qscript.trainable_params = [0] # set only the first parameter as trainable >>> qscript.get_parameters() [0.432] """ return self._trainable_params @trainable_params.setter def trainable_params(self, param_indices): """Store the indices of parameters that support differentiability. Args: param_indices (list[int]): parameter indices """ if any(not isinstance(i, int) or i < 0 for i in param_indices): raise ValueError("Argument indices must be non-negative integers.") num_params = len(self._par_info) if any(i > num_params for i in param_indices): raise ValueError(f"Quantum Script only has {num_params} parameters.") self._trainable_params = sorted(set(param_indices))
[docs] def get_operation(self, idx): """Returns the trainable operation, the operation index and the corresponding operation argument index, for a specified trainable parameter index. Args: idx (int): the trainable parameter index Returns: tuple[.Operation, int, int]: tuple containing the corresponding operation, operation index and an integer representing the argument index, for the provided trainable parameter. """ # get the index of the parameter in the script t_idx = self.trainable_params[idx] # get the info for the parameter info = self._par_info[t_idx] return info["op"], info["op_idx"], info["p_idx"]
[docs] def get_parameters( self, trainable_only=True, operations_only=False, **kwargs ): # pylint:disable=unused-argument """Return the parameters incident on the quantum script operations. The returned parameters are provided in order of appearance on the quantum script. Args: trainable_only (bool): if True, returns only trainable parameters operations_only (bool): if True, returns only the parameters of the operations excluding parameters to observables of measurements **Example** >>> ops = [qml.RX(0.432, 0), qml.RY(0.543, 0), ... qml.CNOT((0,"a")), qml.RX(0.133, "a")] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) By default, all parameters are trainable and will be returned: >>> qscript.get_parameters() [0.432, 0.543, 0.133] Setting the trainable parameter indices will result in only the specified parameters being returned: >>> qscript.trainable_params = [1] # set the second parameter as trainable >>> qscript.get_parameters() [0.543] The ``trainable_only`` argument can be set to ``False`` to instead return all parameters: >>> qscript.get_parameters(trainable_only=False) [0.432, 0.543, 0.133] """ if trainable_only: params = [] for p_idx in self.trainable_params: op = self._par_info[p_idx]["op"] if operations_only and hasattr(op, "return_type"): continue op_idx = self._par_info[p_idx]["p_idx"] params.append(op.data[op_idx]) return params # If trainable_only=False, return all parameters # This is faster than the above and should be used when indexing `_par_info` is not needed params = [d for op in self.operations for d in op.data] if operations_only: return params for m in self.measurements: if m.obs is not None: params.extend(m.obs.data) return params
[docs] def set_parameters(self, params, trainable_only=True): """Set the parameters incident on the quantum script operations. Args: params (list[float]): A list of real numbers representing the parameters of the quantum operations. The parameters should be provided in order of appearance in the quantum script. trainable_only (bool): if True, set only trainable parameters **Example** >>> ops = [qml.RX(0.432, 0), qml.RY(0.543, 0), ... qml.CNOT((0,"a")), qml.RX(0.133, "a")] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) By default, all parameters are trainable and can be modified: >>> qscript.set_parameters([0.1, 0.2, 0.3]) >>> qscript.get_parameters() [0.1, 0.2, 0.3] Setting the trainable parameter indices will result in only the specified parameters being modifiable. Note that this only modifies the number of parameters that must be passed. >>> qscript.trainable_params = [0, 2] # set the first and third parameter as trainable >>> qscript.set_parameters([-0.1, 0.5]) >>> qscript.get_parameters(trainable_only=False) [-0.1, 0.2, 0.5] The ``trainable_only`` argument can be set to ``False`` to instead set all parameters: >>> qscript.set_parameters([4, 1, 6], trainable_only=False) >>> qscript.get_parameters(trainable_only=False) [4, 1, 6] """ if trainable_only: iterator = zip(self.trainable_params, params) required_length = self.num_params else: iterator = enumerate(params) required_length = len(self._par_info) if len(params) != required_length: raise ValueError("Number of provided parameters does not match.") for idx, p in iterator: op = self._par_info[idx]["op"] op.data[self._par_info[idx]["p_idx"]] = p op._check_batching(op.data) self._update_batch_size() self._update_output_dim()
# ======================================================== # MEASUREMENT SHAPE # # We can extract the private static methods to a new class later # ======================================================== @staticmethod def _single_measurement_shape(measurement_process, device, shots): """Auxiliary function of shape that determines the output shape of a quantum script with a single measurement. Args: measurement_process (MeasurementProcess): the measurement process associated with the single measurement device (pennylane.Device): a PennyLane device shots (~.Shots): object defining number and batches of shots Returns: tuple: output shape """ return measurement_process.shape(device, shots) @staticmethod def _multi_homogenous_measurement_shape(mps, device, shots): """Auxiliary function of shape that determines the output shape of a quantum script with multiple homogenous measurements. .. note:: Assuming multiple probability measurements where not all probability measurements have the same number of wires specified, the output shape of the quantum script is a sum of the output shapes produced by each probability measurement. Consider the `qml.probs(wires=[0]), qml.probs(wires=[1,2])` multiple probability measurement with an analytic device as an example. The output shape will be a one element tuple `(6,)`, where the element `6` is equal to `2 ** 1 + 2 ** 2 = 6`. The base of each term is determined by the number of basis states and the exponent of each term comes from the length of the wires specified for the probability measurements: `1 == len([0]) and 2 == len([1, 2])`. """ shape = tuple() # We know that there's one type of measurement, gather it from the first one if isinstance(mps[0], StateMP): raise ValueError( "Getting the output shape of a quantum script with multiple state measurements is not supported." ) shot_vector = shots.shot_vector if len(shot_vector) <= 1: if isinstance(mps[0], (ExpectationMP, VarianceMP)): shape = (len(mps),) elif isinstance(mps[0], ProbabilityMP): wires_num_set = {len(meas.wires) for meas in mps} same_num_wires = len(wires_num_set) == 1 if same_num_wires: # All probability measurements have the same number of # wires, gather the length from the first one len_wires = len(mps[0].wires) dim = mps[0]._get_num_basis_states(len_wires, device) shape = (len(mps), dim) else: # There are a varying number of wires that the probability # measurement processes act on shape = (sum(2 ** len(m.wires) for m in mps),) elif isinstance(mps[0], SampleMP): dim = mps[0].shape(device, shots) shape = (len(mps),) + dim[1:] # No other measurement type to check else: shape = QuantumScript._shape_shot_vector_multi_homogenous(mps, device, shots) return shape @staticmethod def _shape_shot_vector_multi_homogenous(mps, device, shots): """Auxiliary function for determining the output shape of the quantum script for multiple homogenous measurements for a device with a shot vector. Note: it is assumed that getting the output shape of a script with multiple state measurements is not supported. """ shape = tuple() shot_vector = shots.shot_vector # Shot vector was defined if isinstance(mps[0], (ExpectationMP, VarianceMP)): num = sum(shottup.copies for shottup in shot_vector) shape = (num, len(mps)) elif isinstance(mps[0], ProbabilityMP): wires_num_set = {len(meas.wires) for meas in mps} same_num_wires = len(wires_num_set) == 1 if not same_num_wires: # There is a varying number of wires that the probability # measurement processes act on # TODO: revisit when issues with this case are resolved raise ValueError( "Getting the output shape of a quantum script with multiple probability measurements " "along with a device that defines a shot vector is not supported." ) # All probability measurements have the same number of # wires, gather the length from the first one len_wires = len(mps[0].wires) dim = mps[0]._get_num_basis_states(len_wires, device) shape = sum(s.copies for s in shot_vector), len(mps), dim elif isinstance(mps[0], SampleMP): shape = [] for shot_val in shot_vector: num_shots = shot_val.shots if num_shots != 1: shape.extend((num_shots, len(mps)) for _ in range(shot_val.copies)) else: shape.extend((len(mps),) for _ in range(shot_val.copies)) shape = tuple(shape) return shape def _shape_legacy(self, device): """Produces the output shape of the quantum script by inspecting its measurements and the device used for execution. .. note:: The computed shape is not stored because the output shape may be dependent on the device used for execution. Args: device (pennylane.Device): the device that will be used for the script execution Raises: ValueError: raised for unsupported cases for example when the script contains heterogeneous measurements Returns: Union[tuple[int], list[tuple[int]]]: the output shape(s) of the quantum script result **Example:** .. code-block:: pycon >>> dev = qml.device('default.qubit', wires=2) >>> qs = QuantumScript(measurements=[qml.state()]) >>> qs.shape(dev) (1, 4) """ output_shape = tuple() shots = ( Shots(device._raw_shot_sequence) if device.shot_vector is not None else Shots(device.shots) ) if len(self.measurements) == 1: output_shape = self._single_measurement_shape(self.measurements[0], device, shots) else: num_measurements = len({type(meas) for meas in self.measurements}) if num_measurements == 1: output_shape = self._multi_homogenous_measurement_shape( self.measurements, device, shots ) else: raise ValueError( "Getting the output shape of a quantum script that contains multiple types of " "measurements is unsupported." ) if len(shots.shot_vector) <= 1 and self.batch_size is not None: # insert the batch dimension output_shape = output_shape[:1] + (self.batch_size,) + output_shape[1:] return output_shape
[docs] def shape(self, device): """Produces the output shape of the quantum script by inspecting its measurements and the device used for execution. .. note:: The computed shape is not stored because the output shape may be dependent on the device used for execution. Args: device (pennylane.Device): the device that will be used for the script execution Returns: Union[tuple[int], tuple[tuple[int]]]: the output shape(s) of the quantum script result **Examples** .. code-block:: pycon >>> qml.enable_return() >>> dev = qml.device('default.qubit', wires=2) >>> qs = QuantumScript(measurements=[qml.state()]) >>> qs.shape(dev) (4,) >>> m = [qml.state(), qml.expval(qml.PauliZ(0)), qml.probs((0,1))] >>> qs = QuantumScript(measurements=m) >>> qs.shape(dev) ((4,), (), (4,)) """ if not qml.active_return(): return self._shape_legacy(device) shots = ( Shots(device._raw_shot_sequence) if device.shot_vector is not None else Shots(device.shots) ) if len(shots.shot_vector) > 1 and self.batch_size is not None: raise NotImplementedError( "Parameter broadcasting when using a shot vector is not supported yet." ) shapes = tuple(meas_process.shape(device, shots) for meas_process in self.measurements) if self.batch_size is not None: shapes = tuple((self.batch_size,) + shape for shape in shapes) if len(shapes) == 1: return shapes[0] if len(shots.shot_vector) > 1: # put the shot vector axis before the measurement axis shapes = tuple(zip(*shapes)) return shapes
@property def _numeric_type_legacy(self): """Returns the expected numeric type of the script result by inspecting its measurements. Raises: ValueError: raised for unsupported cases for example when the script contains heterogeneous measurements Returns: type: the numeric type corresponding to the result type of the script **Example:** >>> qscript = QuantumScript(measurements=[qml.state()]) >>> qscript.numeric_type complex """ measurement_types = {type(meas) for meas in self.measurements} if len(measurement_types) > 1: raise ValueError( "Getting the numeric type of a quantum script that contains multiple types of measurements is unsupported." ) # Note: if one of the sample measurements contains outputs that # are real, then the entire result will be real if measurement_types.pop() is SampleMP: return next((float for mp in self.measurements if mp.numeric_type is float), int) return self.measurements[0].numeric_type @property def numeric_type(self): """Returns the expected numeric type of the quantum script result by inspecting its measurements. Returns: Union[type, Tuple[type]]: The numeric type corresponding to the result type of the quantum script, or a tuple of such types if the script contains multiple measurements **Example:** .. code-block:: pycon >>> qml.enable_return() >>> dev = qml.device('default.qubit', wires=2) >>> qs = QuantumScript(measurements=[qml.state()]) >>> qs.numeric_type complex """ if not qml.active_return(): return self._numeric_type_legacy types = tuple(observable.numeric_type for observable in self.measurements) return types[0] if len(types) == 1 else types # ======================================================== # Transforms: QuantumScript to QuantumScript # ========================================================
[docs] def copy(self, copy_operations=False): """Returns a shallow copy of the quantum script. Args: copy_operations (bool): If True, the operations are also shallow copied. Otherwise, if False, the copied operations will simply be references to the original operations; changing the parameters of one script will likewise change the parameters of all copies. Returns: QuantumScript : a shallow copy of the quantum script """ if copy_operations: # Perform a shallow copy of all operations in the state prep, operation, and measurement # queues. The operations will continue to share data with the original script operations # unless modified. _prep = [copy.copy(op) for op in self._prep] _ops = [copy.copy(op) for op in self._ops] _measurements = [copy.copy(op) for op in self.measurements] else: # Perform a shallow copy of the state prep, operation, and measurement queues. The # operations within the queues will be references to the original script operations; # changing the original operations will always alter the operations on the copied script. _prep = self._prep.copy() _ops = self._ops.copy() _measurements = self.measurements.copy() new_qscript = self.__class__(ops=_ops, measurements=_measurements, prep=_prep) new_qscript._graph = None if copy_operations else self._graph new_qscript._specs = None new_qscript.wires = copy.copy(self.wires) new_qscript.num_wires = self.num_wires new_qscript.is_sampled = self.is_sampled new_qscript.all_sampled = self.all_sampled new_qscript._update_par_info() new_qscript.trainable_params = self.trainable_params.copy() new_qscript._obs_sharing_wires = self._obs_sharing_wires new_qscript._obs_sharing_wires_id = self._obs_sharing_wires_id new_qscript._batch_size = self.batch_size new_qscript._output_dim = self.output_dim new_qscript._qfunc_output = copy.copy(self._qfunc_output) return new_qscript
def __copy__(self): return self.copy(copy_operations=True)
[docs] def expand(self, depth=1, stop_at=None, expand_measurements=False): """Expand all operations to a specific depth. Args: depth (int): the depth the script should be expanded stop_at (Callable): A function which accepts a queue object, and returns ``True`` if this object should *not* be expanded. If not provided, all objects that support expansion will be expanded. expand_measurements (bool): If ``True``, measurements will be expanded to basis rotations and computational basis measurements. **Example** Consider the following nested quantum script: >>> prep = [qml.BasisState(np.array([1, 1]), wires=[0, 'a'])] >>> nested_script = QuantumScript([qml.Rot(0.543, 0.1, 0.4, wires=0)]) >>> ops = [nested_script, qml.CNOT(wires=[0, 'a']), qml.RY(0.2, wires='a')] >>> measurements = [qml.probs(wires=0), qml.probs(wires='a')] >>> qscript = QuantumScript(ops, measurements, prep) The nested structure is preserved: >>> qscript.operations [BasisState(tensor([1, 1], requires_grad=True), wires=[0, 'a']), <QuantumScript: wires=[0], params=3>, CNOT(wires=[0, 'a']), RY(0.2, wires=['a'])] Calling ``.expand`` will return a script with all nested scripts expanded, resulting in a single script of quantum operations: >>> new_qscript = qscript.expand(depth=2) >>> new_qscript.operations [PauliX(wires=[0]), PauliX(wires=['a']), RZ(0.543, wires=[0]), RY(0.1, wires=[0]), RZ(0.4, wires=[0]), CNOT(wires=[0, 'a']), RY(0.2, wires=['a'])] """ new_script = qml.tape.tape.expand_tape( self, depth=depth, stop_at=stop_at, expand_measurements=expand_measurements ) new_script._update() return new_script
[docs] def adjoint(self): """Create a quantum script that is the adjoint of this one. Adjointed quantum scripts are the conjugated and transposed version of the original script. Adjointed ops are equivalent to the inverted operation for unitary gates. Returns: ~.QuantumScript: the adjointed script """ with qml.QueuingManager.stop_recording(): ops_adj = [qml.adjoint(op, lazy=False) for op in reversed(self._ops)] adj = self.__class__(ops=ops_adj, measurements=self.measurements, prep=self._prep) if self.do_queue: qml.QueuingManager.append(adj) return adj
[docs] def unwrap(self): """A context manager that unwraps a quantum script with tensor-like parameters to NumPy arrays. Returns: ~.QuantumScript: the unwrapped quantum script **Example** >>> with tf.GradientTape(): ... qscript = QuantumScript([qml.RX(tf.Variable(0.1), 0), ... qml.RY(tf.constant(0.2), 0), ... qml.RZ(tf.Variable(0.3), 0)]) ... with qscript.unwrap(): ... print("Trainable params:", qscript.trainable_params) ... print("Unwrapped params:", qscript.get_parameters()) Trainable params: [0, 2] Unwrapped params: [0.1, 0.3] >>> qscript.get_parameters() [<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.1>, <tf.Tensor: shape=(), dtype=float32, numpy=0.2>, <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.3>] """ return qml.tape.UnwrapTape(self)
# ======================================================== # Transforms: QuantumScript to Information # ======================================================== @property def graph(self): """Returns a directed acyclic graph representation of the recorded quantum circuit: >>> ops = [qml.QubitStateVector([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0))]) >>> qscript.graph <pennylane.circuit_graph.CircuitGraph object at 0x7fcc0433a690> Note that the circuit graph is only constructed once, on first call to this property, and cached for future use. Returns: .CircuitGraph: the circuit graph object """ if self._graph is None: self._graph = qml.CircuitGraph( self.operations, self.observables, self.wires, self._par_info, self.trainable_params ) return self._graph @property def specs(self): """Resource information about a quantum circuit. Returns: dict[str, Union[defaultdict,int]]: dictionaries that contain quantum script specifications **Example** >>> ops = [qml.Hadamard(0), qml.RX(0.26, 1), qml.CNOT((1,0)), ... qml.Rot(1.8, -2.7, 0.2, 0), qml.Hadamard(1), qml.CNOT((0, 1))] >>> qscript = QuantumScript(ops, [qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))]) Asking for the specs produces a dictionary of useful information about the circuit: >>> qscript.specs['num_observables'] 1 >>> qscript.specs['gate_sizes'] defaultdict(<class 'int'>, {1: 4, 2: 2}) >>> print(qscript.specs['resources']) wires: 2 gates: 6 depth: 4 shots: 0 gate_types: {'Hadamard': 2, 'RX': 1, 'CNOT': 2, 'Rot': 1} """ if self._specs is None: resources = qml.resource.resource._count_resources( self, shots=0 ) # pylint: disable=protected-access self._specs = { "resources": resources, "gate_sizes": defaultdict(int), "gate_types": defaultdict(int), } for op in self.operations: # don't use op.num_wires to allow for flexible gate classes like QubitUnitary self._specs["gate_sizes"][len(op.wires)] += 1 self._specs["gate_types"][op.name] += 1 self._specs["num_operations"] = resources.num_gates self._specs["num_observables"] = len(self.observables) self._specs["num_diagonalizing_gates"] = len(self.diagonalizing_gates) self._specs["num_used_wires"] = self.num_wires self._specs["num_trainable_params"] = self.num_params self._specs["depth"] = resources.depth return self._specs # pylint: disable=too-many-arguments
[docs] def draw( self, wire_order=None, show_all_wires=False, decimals=None, max_length=100, show_matrices=False, ): """Draw the quantum script as a circuit diagram. See :func:`~.drawer.tape_text` for more information. Args: wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit show_all_wires (bool): If True, all wires, including empty wires, are printed. decimals (int): How many decimal points to include when formatting operation parameters. Default ``None`` will omit parameters from operation labels. max_length (Int) : Maximum length of a individual line. After this length, the diagram will begin anew beneath the previous lines. show_matrices=False (bool): show matrix valued parameters below all circuit diagrams Returns: str: the circuit representation of the quantum script """ return qml.drawer.tape_text( self, wire_order=wire_order, show_all_wires=show_all_wires, decimals=decimals, max_length=max_length, show_matrices=show_matrices, )
[docs] def to_openqasm(self, wires=None, rotations=True, measure_all=True, precision=None): """Serialize the circuit as an OpenQASM 2.0 program. Measurements are assumed to be performed on all qubits in the computational basis. An optional ``rotations`` argument can be provided so that output of the OpenQASM circuit is diagonal in the eigenbasis of the quantum script's observables. The measurement outputs can be restricted to only those specified in the script by setting ``measure_all=False``. .. note:: The serialized OpenQASM program assumes that gate definitions in ``qelib1.inc`` are available. Args: wires (Wires or None): the wires to use when serializing the circuit rotations (bool): in addition to serializing user-specified operations, also include the gates that diagonalize the measured wires such that they are in the eigenbasis of the circuit observables. measure_all (bool): whether to perform a computational basis measurement on all qubits or just those specified in the script precision (int): decimal digits to display for parameters Returns: str: OpenQASM serialization of the circuit """ wires = wires or self.wires # add the QASM headers qasm_str = 'OPENQASM 2.0;\ninclude "qelib1.inc";\n' if self.num_wires == 0: # empty circuit return qasm_str # create the quantum and classical registers qasm_str += f"qreg q[{len(wires)}];\n" qasm_str += f"creg c[{len(wires)}];\n" # get the user applied circuit operations operations = self.operations if rotations: # if requested, append diagonalizing gates corresponding # to circuit observables operations += self.diagonalizing_gates # decompose the queue # pylint: disable=no-member just_ops = QuantumScript(operations) operations = just_ops.expand( depth=2, stop_at=lambda obj: obj.name in OPENQASM_GATES ).operations # create the QASM code representing the operations for op in operations: try: gate = OPENQASM_GATES[op.name] except KeyError as e: raise ValueError(f"Operation {op.name} not supported by the QASM serializer") from e wire_labels = ",".join([f"q[{wires.index(w)}]" for w in op.wires.tolist()]) params = "" if op.num_params > 0: # If the operation takes parameters, construct a string # with parameter values. if precision is not None: params = "(" + ",".join([f"{p:.{precision}}" for p in op.parameters]) + ")" else: # use default precision params = "(" + ",".join([str(p) for p in op.parameters]) + ")" qasm_str += f"{gate}{params} {wire_labels};\n" # apply computational basis measurements to each quantum register # NOTE: This is not strictly necessary, we could inspect self.observables, # and then only measure wires which are requested by the user. However, # some devices which consume QASM require all registers be measured, so # measure all wires by default to be safe. if measure_all: for wire in range(len(wires)): qasm_str += f"measure q[{wire}] -> c[{wire}];\n" else: measured_wires = qml.wires.Wires.all_wires([m.wires for m in self.measurements]) for w in measured_wires: wire_indx = self.wires.index(w) qasm_str += f"measure q[{wire_indx}] -> c[{wire_indx}];\n" return qasm_str
[docs] @classmethod def from_queue(cls, queue): """Construct a QuantumScript from an AnnotatedQueue.""" return cls(*process_queue(queue))
[docs]def make_qscript(fn): """Returns a function that generates a qscript from a quantum function without any operation queuing taking place. This is useful when you would like to manipulate or transform the qscript created by a quantum function without evaluating it. Args: fn (function): the quantum function to generate the qscript from Returns: function: The returned function takes the same arguments as the quantum function. When called, it returns the generated quantum script without any queueing occuring. **Example** Consider the following quantum function: .. code-block:: python def qfunc(x): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) qml.RX(x, wires=0) We can use ``make_qscript`` to extract the qscript generated by this quantum function, without any of the operations being queued by any existing queuing contexts: >>> with qml.queuing.AnnotatedQueue() as active_queue: ... _ = qml.RY(1.0, wires=0) ... qs = make_qscript(qfunc)(0.5) >>> qs.operations [Hadamard(wires=[0]), CNOT(wires=[0, 1]), RX(0.5, wires=[0])] Note that the currently recording queue did not queue any of these quantum operations: >>> active_queue.queue [RY(1.0, wires=[0])] """ def wrapper(*args, **kwargs): with AnnotatedQueue() as q: result = fn(*args, **kwargs) qscript = QuantumScript.from_queue(q) qscript._qfunc_output = result return qscript return wrapper