# Copyright 2018-2021 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 provides the PennyLane :class:`~.tensor` class."""importnumpyasonpfromautogradimportnumpyas_npfromautograd.coreimportVSpacefromautograd.extendimportdefvjp,primitivefromautograd.numpy.numpy_boxesimportArrayBoxfromautograd.numpy.numpy_vspacesimportArrayVSpace,ComplexArrayVSpacefromautograd.tracerimportBoxfrompennylane.operationimportOperator__doc__="NumPy with automatic differentiation support, provided by Autograd and PennyLane."# Hotfix since _np.asarray doesn't have a gradient rule defined.@primitivedefasarray(vals,*args,**kwargs):"""Gradient supporting autograd asarray"""ifisinstance(vals,(onp.ndarray,_np.ndarray)):return_np.asarray(vals,*args,**kwargs)return_np.array(vals,*args,**kwargs)defasarray_gradmaker(ans,*args,**kwargs):"""Gradient maker for asarray"""delans,args,kwargsreturnlambdag:gdefvjp(asarray,asarray_gradmaker,argnums=(0,))
[docs]classtensor(_np.ndarray):"""Constructs a PennyLane tensor for use with Autograd QNodes. The ``tensor`` class is a subclass of ``numpy.ndarray``, providing the same multidimensional, homogeneous data-structure of fixed-size items, with an additional flag to indicate to PennyLane whether the contained data is differentiable or not. .. warning:: PennyLane ``tensor`` objects are only used as part of the Autograd QNode interface. If using another machine learning library such as PyTorch or TensorFlow, use their built-in ``tf.Variable`` and ``torch.tensor`` classes instead. .. warning:: Tensors should be constructed using standard array construction functions provided as part of PennyLane's NumPy implementation, including ``np.array``, ``np.zeros`` or ``np.empty``. The parameters given here refer to a low-level class for instantiating tensors. Args: input_array (array_like): Any data structure in any form that can be converted to an array. This includes lists, lists of tuples, tuples, tuples of tuples, tuples of lists and ndarrays. requires_grad (bool): whether the tensor supports differentiation **Example** The trainability of a tensor can be set on construction via the ``requires_grad`` keyword argument, >>> from pennylane import numpy as np >>> x = np.array([0, 1, 2], requires_grad=True) >>> x tensor([0, 1, 2], requires_grad=True) or in-place by modifying the ``requires_grad`` attribute: >>> x.requires_grad = False tensor([0, 1, 2], requires_grad=False) Since tensors are subclasses of ``np.ndarray``, they can be provided as arguments to any PennyLane-wrapped NumPy function: >>> np.sin(x) tensor([0. , 0.84147098, 0.90929743], requires_grad=True) When composing functions of multiple tensors, if at least one input tensor is differentiable, then the output will also be differentiable: >>> x = np.array([0, 1, 2], requires_grad=False) >>> y = np.zeros([3], requires_grad=True) >>> np.vstack([x, y]) tensor([[0., 1., 2.], [0., 0., 0.]], requires_grad=True) """def__new__(cls,input_array,*args,requires_grad=True,**kwargs):obj=asarray(input_array,*args,**kwargs)ifisinstance(obj,onp.ndarray):obj=obj.view(cls)obj.requires_grad=requires_gradreturnobjdef__array_finalize__(self,obj):# pylint: disable=attribute-defined-outside-initifobjisNone:# pragma: no coverreturnself.requires_grad=getattr(obj,"requires_grad",None)def__repr__(self):string=super().__repr__()returnstring[:-1]+f", requires_grad={self.requires_grad})"def__array_wrap__(self,obj):out_arr=tensor(obj,requires_grad=self.requires_grad)returnsuper().__array_wrap__(out_arr)def__array_ufunc__(self,ufunc,method,*inputs,**kwargs):# pylint: disable=no-member,attribute-defined-outside-init# unwrap any outputs the ufunc might haveoutputs=[i.view(onp.ndarray)foriinkwargs.get("out",())]ifoutputs:# Insert the unwrapped outputs into the keyword# args dictionary, to be passed to ndarray.__array_ufunc__outputs=tuple(outputs)kwargs["out"]=outputselse:# If the ufunc has no ouputs, we simply# create a tuple containing None for all potential outputs.outputs=(None,)*ufunc.nout# unwrap the input arguments to the ufuncargs=[i.unwrap()ifhasattr(i,"unwrap")elseiforiininputs]# call the ndarray.__array_ufunc__ method to compute the result# of the vectorized ufuncres=super().__array_ufunc__(ufunc,method,*args,**kwargs)ifisinstance(res,Operator):returnresifufunc.nout==1:res=(res,)# construct a list of ufunc outputs to returnufunc_output=[(onp.asarray(result)ifoutputisNoneelseoutput)forresult,outputinzip(res,outputs)]# if any of the inputs were trainable, the output is also trainablerequires_grad=any(isinstance(x,onp.ndarray)andgetattr(x,"requires_grad",True)forxininputs)# Iterate through the ufunc outputs and convert each to a PennyLane tensor.# We also correctly set the requires_grad attribute.foriinrange(len(ufunc_output)):# pylint: disable=consider-using-enumerateufunc_output[i]=tensor(ufunc_output[i],requires_grad=requires_grad)iflen(ufunc_output)==1:# the ufunc has a single output so return a single tensorreturnufunc_output[0]# otherwise we must return a tuple of tensorsreturntuple(ufunc_output)def__getitem__(self,*args,**kwargs):item=super().__getitem__(*args,**kwargs)ifnotisinstance(item,tensor):item=tensor(item,requires_grad=self.requires_grad)returnitemdef__hash__(self):ifself.ndim==0:# Allowing hashing if the tensor is a scalar.# We hash both the scalar value *and* the differentiability information,# to match the behaviour of PyTorch.returnhash((self.item(),self.requires_grad))raiseTypeError("unhashable type: 'numpy.tensor'")def__reduce__(self):# Called when pickling the object.# Numpy ndarray uses __reduce__ instead of __getstate__ to prepare an object for# pickling. self.requires_grad needs to be included in the tuple returned by# __reduce__ in order to be preserved in the unpickled object.reduced_obj=super().__reduce__()# The last (2nd) element of this tuple holds the data. Add requires_grad to this:full_reduced_data=reduced_obj[2]+(self.requires_grad,)return(reduced_obj[0],reduced_obj[1],full_reduced_data)def__setstate__(self,reduced_obj)->None:# Called when unpickling the object.# Set self.requires_grad with the last element in the tuple returned by __reduce__:# pylint: disable=attribute-defined-outside-init,no-memberself.requires_grad=reduced_obj[-1]# And call parent's __setstate__ without this element:super().__setstate__(reduced_obj[:-1])
[docs]defunwrap(self):"""Converts the tensor to a standard, non-differentiable NumPy ndarray or Python scalar if the tensor is 0-dimensional. All information regarding differentiability of the tensor will be lost. .. warning:: The returned array is a new view onto the **same data**. That is, the tensor and the returned ``ndarray`` share the same underlying storage. Changes to the tensor object will be reflected within the returned array, and vice versa. **Example** >>> from pennylane import numpy as np >>> x = np.array([1, 2], requires_grad=True) >>> x tensor([1, 2], requires_grad=True) >>> x.unwrap() array([1, 2]) Zero dimensional array are converted to Python scalars: >>> x = np.array(1.543, requires_grad=False) >>> x.unwrap() 1.543 >>> type(x.unwrap()) float The underlying data is **not** copied: >>> x = np.array([1, 2], requires_grad=True) >>> y = x.unwrap() >>> x[0] = 5 >>> y array([5, 2]) >>> y[1] = 7 >>> x tensor([5, 7], requires_grad=True) To create a copy, the ``copy()`` method can be used: >>> x = np.array([1, 2], requires_grad=True) >>> y = x.unwrap().copy() >>> x[0] = 5 >>> y array([1, 2]) """ifself.ndim==0:returnself.view(onp.ndarray).item()returnself.view(onp.ndarray)
[docs]defnumpy(self):"""Converts the tensor to a standard, non-differentiable NumPy ndarray or Python scalar if the tensor is 0-dimensional. This method is an alias for :meth:`~.unwrap`. See :meth:`~.unwrap` for more details. """returnself.unwrap()
[docs]classNonDifferentiableError(Exception):"""Exception raised if attempting to differentiate non-trainable :class:`~.tensor` using Autograd."""
deftensor_to_arraybox(x,*args):"""Convert a :class:`~.tensor` to an Autograd ``ArrayBox``. Args: x (array_like): Any data structure in any form that can be converted to an array. This includes lists, lists of tuples, tuples, tuples of tuples, tuples of lists and ndarrays. Returns: autograd.numpy.numpy_boxes.ArrayBox: Autograd ArrayBox instance of the array Raises: NonDifferentiableError: if the provided tensor is non-differentiable """ifisinstance(x,tensor):ifx.requires_grad:returnArrayBox(x,*args)raiseNonDifferentiableError(f"{x} is non-differentiable. Set the requires_grad attribute to True.")returnArrayBox(x,*args)Box.type_mappings[tensor]=tensor_to_arrayboxVSpace.mappings[tensor]=lambdax:ComplexArrayVSpace(x)ifonp.iscomplexobj(x)elseArrayVSpace(x)