def _append_op(self, op): """Append a quantum operation into the circuit queue. Args: op (:class:`~.operation.Operation`): quantum operation to be added to the circuit Raises: ValueError: if `op` does not act on all wires QuantumFunctionError: if state preparations and gates do not precede measured observables """ if op.num_wires == Wires.All: if set(op.wires) != set(range(self.num_wires)): raise QuantumFunctionError( "Operator {} must act on all wires".format(op.name)) # Make sure only existing wires are used. for w in _flatten(op.wires): if w < 0 or w >= self.num_wires: raise QuantumFunctionError( "Operation {} applied to invalid wire {} " "on device with {} wires.".format(op.name, w, self.num_wires)) # observables go to their own, temporary queue if isinstance(op, Observable): if op.return_type is None: self.queue.append(op) else: self.obs_queue.append(op) else: if self.obs_queue: raise QuantumFunctionError( "State preparations and gates must precede measured observables." ) self.queue.append(op) # TODO rename self.queue to self.op_queue
def _default_args(self, kwargs): """Validate the quantum function arguments, apply defaults. Here we apply default values for the auxiliary parameters of :attr:`QNode.func`. Args: kwargs (dict[str, Any]): auxiliary arguments (given using the keyword syntax) Raises: QuantumFunctionError: if the parameter to the quantum function was invalid Returns: dict[str, Any]: all auxiliary arguments (with defaults) """ forbidden_kinds = ( inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD, ) # check the validity of kwargs items for name in kwargs: s = self.func.sig.get(name) if s is None: if self.func.var_keyword: continue # unknown parameter, but **kwargs will take it TODO should it? raise QuantumFunctionError( "Unknown quantum function parameter '{}'.".format(name)) if s.par.kind in forbidden_kinds or s.par.default == inspect.Parameter.empty: raise QuantumFunctionError( "Quantum function parameter '{}' cannot be given using the keyword syntax." .format(name)) # apply defaults for name, s in self.func.sig.items(): default = s.par.default if default != inspect.Parameter.empty: # meant to be given using keyword syntax kwargs.setdefault(name, default) return kwargs
def Interferometer(theta, phi, varphi, wires, mesh='rectangular', beamsplitter='pennylane'): r"""General linear interferometer, an array of beam splitters and phase shifters. For :math:`M` wires, the general interferometer is specified by providing :math:`M(M-1)/2` transmittivity angles :math:`\theta` and the same number of phase angles :math:`\phi`, as well as either :math:`M-1` or :math:`M` additional rotation parameters :math:`\varphi`. For the parametrization of a universal interferometer :math:`M-1` such rotation parameters are sufficient. If :math:`M` rotation parameters are given, the interferometer is over-parametrized, but the resulting circuit is more symmetric, which can be advantageous. By specifying the keyword argument ``mesh``, the scheme used to implement the interferometer may be adjusted: * ``mesh='rectangular'`` (default): uses the scheme described in :cite:`clements2016optimal`, resulting in a *rectangular* array of :math:`M(M-1)/2` beamsplitters arranged in :math:`M` slices and ordered from left to right and top to bottom in each slice. The first beamsplitter acts on wires :math:`0` and :math:`1`: .. figure:: ../../_static/clements.png :align: center :width: 30% :target: javascript:void(0); * ``mesh='triangular'``: uses the scheme described in :cite:`reck1994experimental`, resulting in a *triangular* array of :math:`M(M-1)/2` beamsplitters arranged in :math:`2M-3` slices and ordered from left to right and top to bottom. The first and fourth beamsplitters act on wires :math:`M-1` and :math:`M`, the second on :math:`M-2` and :math:`M-1`, and the third on :math:`M-3` and :math:`M-2`, and so on. .. figure:: ../../_static/reck.png :align: center :width: 30% :target: javascript:void(0); In both schemes, the network of :class:`~.Beamsplitter` operations is followed by :math:`M` (or :math:`M-1`) local :class:`Rotation` Operations. In the latter case, the rotation on the last wire is left out. The rectangular decomposition is generally advantageous, as it has a lower circuit depth (:math:`M` vs :math:`2M-3`) and optical depth than the triangular decomposition, resulting in reduced optical loss. This is an example of a 4-mode interferometer with beamsplitters :math:`B` and rotations :math:`R`, using ``mesh='rectangular'``: .. figure:: ../../_static/layer_interferometer.png :align: center :width: 60% :target: javascript:void(0); .. note:: The decomposition as formulated in :cite:`clements2016optimal` uses a different convention for a beamsplitter :math:`T(\theta, \phi)` than PennyLane, namely: .. math:: T(\theta, \phi) = BS(\theta, 0) R(\phi) For the universality of the decomposition, the used convention is irrelevant, but for a given set of angles the resulting interferometers will be different. If an interferometer consistent with the convention from :cite:`clements2016optimal` is needed, the optional keyword argument ``beamsplitter='clements'`` can be specified. This will result in each :class:`~.Beamsplitter` being preceded by a :class:`Rotation` and thus increase the number of elementary operations in the circuit. Args: theta (array): length :math:`M(M-1)/2` array of transmittivity angles :math:`\theta` phi (array): length :math:`M(M-1)/2` array of phase angles :math:`\phi` varphi (array): length :math:`M` or :math:`M-1` array of rotation angles :math:`\varphi` wires (Sequence[int]): wires the interferometer should act on Keyword Args: mesh (string): the type of mesh to use beamsplitter (str): if ``clements``, the beamsplitter convention from Clements et al. 2016 (https://dx.doi.org/10.1364/OPTICA.3.001460) is used; if ``pennylane``, the beamsplitter is implemented via PennyLane's ``Beamsplitter`` operation. """ if isinstance(beamsplitter, Variable): raise QuantumFunctionError( "The beamsplitter parameter influences the " "circuit architecture and can not be passed as a QNode parameter.") if isinstance(mesh, Variable): raise QuantumFunctionError( "The mesh parameter influences the circuit architecture " "and can not be passed as a QNode parameter.") if not isinstance(wires, Sequence): w = [wires] else: w = wires M = len(w) if M == 1: # the interferometer is a single rotation Rotation(varphi[0], wires=w[0]) return n = 0 # keep track of free parameters if mesh == 'rectangular': # Apply the Clements beamsplitter array # The array depth is N for l in range(M): for k, (w1, w2) in enumerate(zip(w[:-1], w[1:])): #skip even or odd pairs depending on layer if (l + k) % 2 != 1: if beamsplitter == 'clements': Rotation(phi[n], wires=[w1]) Beamsplitter(theta[n], 0, wires=[w1, w2]) else: Beamsplitter(theta[n], phi[n], wires=[w1, w2]) n += 1 elif mesh == 'triangular': # apply the Reck beamsplitter array # The array depth is 2*N-3 for l in range(2 * M - 3): for k in range(abs(l + 1 - (M - 1)), M - 1, 2): if beamsplitter == 'clements': Rotation(phi[n], wires=[w[k]]) Beamsplitter(theta[n], 0, wires=[w[k], w[k + 1]]) else: Beamsplitter(theta[n], phi[n], wires=[w[k], w[k + 1]]) n += 1 # apply the final local phase shifts to all modes for i, p in enumerate(varphi): Rotation(p, wires=[w[i]])
def _construct_metric_tensor(self, *, diag_approx=False): """Construct metric tensor subcircuits for qubit circuits. Constructs a set of quantum circuits for computing a block-diagonal approximation of the Fubini-Study metric tensor on the parameter space of the variational circuit represented by the QNode, using the Quantum Geometric Tensor. If the parameter appears in a gate :math:`G`, the subcircuit contains all gates which precede :math:`G`, and :math:`G` is replaced by the variance value of its generator. Args: diag_approx (bool): iff True, use the diagonal approximation Raises: QuantumFunctionError: if a metric tensor cannot be generated because no generator was defined """ # pylint: disable=too-many-statements, too-many-branches self._metric_tensor_subcircuits = {} for queue, curr_ops, param_idx, _ in self.circuit.iterate_layers(): obs = [] scale = [] Ki_matrices = [] KiKj_matrices = [] Ki_ev = [] KiKj_ev = [] V = None # for each operation in the layer, get the generator and convert it to a variance for n, op in enumerate(curr_ops): gen, s = op.generator w = op.wires if gen is None: raise QuantumFunctionError( "Can't generate metric tensor, operation {}" "has no defined generator".format(op)) # get the observable corresponding to the generator of the current operation if isinstance(gen, np.ndarray): # generator is a Hermitian matrix variance = var(qml.Hermitian(gen, w, do_queue=False)) if not diag_approx: Ki_matrices.append((n, expand(gen, w, self.num_wires))) elif issubclass(gen, Observable): # generator is an existing PennyLane operation variance = var(gen(w, do_queue=False)) if not diag_approx: if issubclass(gen, qml.PauliX): mat = np.array([[0, 1], [1, 0]]) elif issubclass(gen, qml.PauliY): mat = np.array([[0, -1j], [1j, 0]]) elif issubclass(gen, qml.PauliZ): mat = np.array([[1, 0], [0, -1]]) Ki_matrices.append((n, expand(mat, w, self.num_wires))) else: raise QuantumFunctionError( "Can't generate metric tensor, generator {}" "has no corresponding observable".format(gen)) obs.append(variance) scale.append(s) if not diag_approx: # In order to compute the block diagonal portion of the metric tensor, # we need to compute 'second order' <psi|K_i K_j|psi> terms. for i, j in itertools.product(range(len(Ki_matrices)), repeat=2): # compute the matrices representing all K_i K_j terms obs1 = Ki_matrices[i] obs2 = Ki_matrices[j] KiKj_matrices.append( ((obs1[0], obs2[0]), obs1[1] @ obs2[1])) V = np.identity(2**self.num_wires, dtype=np.complex128) # generate the unitary operation to rotate to # the shared eigenbasis of all observables for _, term in Ki_matrices: _, S = linalg.eigh(V.conj().T @ term @ V) V = np.round(V @ S, 15) V = V.conj().T # calculate the eigenvalues for # each observable in the shared eigenbasis for idx, term in Ki_matrices: eigs = np.diag(V @ term @ V.conj().T).real Ki_ev.append((idx, eigs)) for idx, term in KiKj_matrices: eigs = np.diag(V @ term @ V.conj().T).real KiKj_ev.append((idx, eigs)) self._metric_tensor_subcircuits[param_idx] = { "queue": queue, "observable": obs, "Ki_expectations": Ki_ev, "KiKj_expectations": KiKj_ev, "eigenbasis_matrix": V, "result": None, "scale": scale, }
def _check_circuit(self, res): """Check that the generated Operator queue corresponds to a valid quantum circuit. .. note:: The validity of individual Operators is checked already in :meth:`_append_op`. Args: res (Sequence[Observable], Observable): output returned by the quantum function Raises: QuantumFunctionError: an error was discovered in the circuit """ # pylint: disable=too-many-branches # check the return value if isinstance(res, Observable): if res.return_type is ObservableReturnTypes.Sample: # Squeezing ensures that there is only one array of values returned # when only a single-mode sample is requested self.output_conversion = np.squeeze else: self.output_conversion = float self.output_dim = 1 res = (res, ) elif isinstance(res, Sequence) and res and all( isinstance(x, Observable) for x in res): # for multiple observables values, any valid Python sequence of observables # (i.e., lists, tuples, etc) are supported in the QNode return statement. # Device already returns the correct numpy array, so no further conversion is required self.output_conversion = np.asarray self.output_dim = len(res) res = tuple(res) else: raise QuantumFunctionError( "A quantum function must return either a single measured observable " "or a nonempty sequence of measured observables.") # check that all returned observables have a return_type specified for x in res: if x.return_type is None: raise QuantumFunctionError( "Observable '{}' does not have the measurement type specified." .format(x)) # check that all ev's are returned, in the correct order if res != tuple(self.obs_queue): raise QuantumFunctionError( "All measured observables must be returned in the order they are measured." ) # check that no wires are measured more than once m_wires = list(w for ob in res for w in _flatten(ob.wires)) if len(m_wires) != len(set(m_wires)): raise QuantumFunctionError( "Each wire in the quantum circuit can only be measured once.") # True if op is a CV, False if it is a discrete variable (Identity could be either) are_cvs = [ isinstance(op, CV) for op in self.queue + list(res) if not isinstance(op, qml.Identity) ] if not all(are_cvs) and any(are_cvs): raise QuantumFunctionError( "Continuous and discrete operations are not allowed in the same quantum circuit." ) if any(are_cvs) and self.type == "qubit": raise QuantumFunctionError( "Device {} is a qubit device; CV operations are not allowed.". format(self.device.short_name)) if not all(are_cvs) and self.type == "cv": raise QuantumFunctionError( "Device {} is a CV device; qubit operations are not allowed.". format(self.device.short_name)) queue = self.queue if self.device.operations: # replace operations in the queue with any decompositions if required queue = decompose_queue(self.queue, self.device) self.ops = queue + list(res) del self.queue del self.obs_queue
def _construct(self, args, kwargs): """Construct the quantum circuit graph by calling the quantum function. For immutable nodes this method is called the first time :meth:`QNode.evaluate` or :meth:`QNode.jacobian` is called, and for mutable nodes *each time* they are called. It executes the quantum function, stores the resulting sequence of :class:`.Operator` instances, converts it into a circuit graph, and creates the Variable mapping. .. note:: The Variables are only required for analytic differentiation, for evaluation we could simply reconstruct the circuit each time. Args: args (tuple[Any]): Positional arguments passed to the quantum function. During the construction we are not concerned with the numerical values, but with the nesting structure. Each positional argument is replaced with a :class:`~.variable.Variable` instance. kwargs (dict[str, Any]): Auxiliary arguments passed to the quantum function. Raises: QuantumFunctionError: if the :class:`pennylane.QNode`'s _current_context is attempted to be modified inside of this method, the quantum function returns incorrect values or if both continuous and discrete operations are specified in the same quantum circuit """ # pylint: disable=protected-access # remove when QNode_old is gone # pylint: disable=attribute-defined-outside-init, too-many-branches self.args_model = ( args #: nested Sequence[Number]: nested shape of the arguments for later unflattening ) # flatten the args, replace each argument with a Variable instance carrying a unique index arg_vars = [Variable(idx) for idx, _ in enumerate(_flatten(args))] self.num_variables = len(arg_vars) # arrange the newly created Variables in the nested structure of args arg_vars = unflatten(arg_vars, args) # temporary queues for operations and observables self.queue = [] #: list[Operation]: applied operations self.obs_queue = [] #: list[Observable]: applied observables # set up the context for Operator entry if QNode_old._current_context is None: QNode_old._current_context = self else: raise QuantumFunctionError( "QNode._current_context must not be modified outside this method." ) try: # generate the program queue by executing the quantum circuit function if self.mutable: # it's ok to directly pass auxiliary arguments since the circuit is re-constructed each time # (positional args must be replaced because parameter-shift differentiation requires Variables) res = self.func(*arg_vars, **kwargs) else: # must convert auxiliary arguments to named Variables so they can be updated without re-constructing the circuit kwarg_vars = {} for key, val in kwargs.items(): temp = [ Variable(idx, name=key) for idx, _ in enumerate(_flatten(val)) ] kwarg_vars[key] = unflatten(temp, val) res = self.func(*arg_vars, **kwarg_vars) finally: QNode_old._current_context = None # check the validity of the circuit self._check_circuit(res) # map each free variable to the operators which depend on it self.variable_deps = {} for k, op in enumerate(self.ops): for j, p in enumerate(_flatten(op.params)): if isinstance(p, Variable): if p.name is None: # ignore auxiliary arguments self.variable_deps.setdefault(p.idx, []).append( ParameterDependency(op, j)) # generate the DAG self.circuit = CircuitGraph(self.ops, self.variable_deps) # check for operations that cannot affect the output if self.properties.get("vis_check", False): visible = self.circuit.ancestors(self.circuit.observables) invisible = set(self.circuit.operations) - visible if invisible: raise QuantumFunctionError( "The operations {} cannot affect the output of the circuit." .format(invisible))