class DefaultTensorTF(DefaultTensor): """Experimental TensorFlow Tensor Network simulator device for PennyLane. **Short name:** ``default.tensor.tf`` This experimental device extends ``default.tensor`` by making use of the TensorFlow backend of TensorNetwork. As a result, it supports classical backpropagation as a means to compute the Jacobian. This can be faster than the parameter-shift rule for analytic quantum gradients when the number of parameters to be optimized is large. To use this device, you will need to install TensorFlow and TensorNetwork: .. code-block:: bash pip install tensornetwork>=0.2 tensorflow>=2.0 **Example** The ``default.tensor.tf`` device supports end-to-end classical backpropagation with the TensorFlow interface. Using this method, the created QNode is a 'white-box', and is tightly integrated with your TensorFlow computation: >>> dev = qml.device("default.tensor.tf", wires=1) >>> @qml.qnode(dev, interface="tf", diff_method="backprop") >>> def circuit(x): ... qml.RX(x[1], wires=0) ... qml.Rot(x[0], x[1], x[2], wires=0) ... return qml.expval(qml.PauliZ(0)) >>> vars = tf.Variable([0.2, 0.5, 0.1]) >>> with tf.GradientTape() as tape: ... res = circuit(vars) >>> tape.gradient(res, vars) <tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.2526717e-01, -1.0086454e+00, 1.3877788e-17], dtype=float32)> In this mode, you must use the ``"tf"`` interface, as TensorFlow is used as the device backend. Args: wires (int): number of subsystems in the quantum state represented by the device shots (None, int): Number of circuit evaluations/random samples to return when sampling from the device. Defaults to ``None`` if not specified, which means that the device returns analytical results. representation (str): Underlying representation used for the tensor network simulation. Valid options are "exact" (no approximations made) or "mps" (simulated quantum state is approximated as a Matrix Product State). contraction_method (str): Method used to perform tensor network contractions. Only applicable for the "exact" representation. Valid options are "auto", "greedy", "branch", or "optimal". See documentation of the `TensorNetwork library <https://tensornetwork.readthedocs.io/en/latest/>`_ for more information about contraction methods. """ # pylint: disable=too-many-instance-attributes name = "PennyLane TensorNetwork (TensorFlow) simulator plugin" short_name = "default.tensor.tf" _operation_map = copy.copy(DefaultTensor._operation_map) _operation_map.update({ "PhaseShift": lambda phi: tf.linalg.diag(ops.PhaseShift(phi)), "RX": ops.RX, "RY": ops.RY, "RZ": lambda theta: tf.linalg.diag(ops.RZ(theta)), "Rot": ops.Rot, "CRX": ops.CRX, "CRY": ops.CRY, "CRZ": lambda theta: tf.linalg.diag(ops.CRZ(theta)), "CRot": ops.CRot, }) backend = "tensorflow" _reshape = staticmethod(tf.reshape) _array = staticmethod(tf.constant) _asarray = staticmethod(tf.convert_to_tensor) _real = staticmethod(tf.math.real) _imag = staticmethod(tf.math.imag) _abs = staticmethod(tf.abs) _squeeze = staticmethod(tf.squeeze) _expand_dims = staticmethod(tf.expand_dims) C_DTYPE = ops.C_DTYPE R_DTYPE = ops.R_DTYPE @classmethod def capabilities(cls): capabilities = super().capabilities().copy() capabilities.update(passthru_interface="tf", ) return capabilities
class DefaultTensorTF(DefaultTensor): """Experimental TensorFlow Tensor Network simulator device for PennyLane. **Short name:** ``default.tensor.tf`` This experimental device extends ``default.tensor`` by making use of the TensorFlow backend of TensorNetwork. As a result, it supports classical backpropagation as a means to compute the Jacobian. This can be faster than the parameter-shift rule for analytic quantum gradients when the number of parameters to be optimized is large. To use this device, you will need to install TensorFlow and TensorNetwork: .. code-block:: bash pip install tensornetwork>=0.2 tensorflow>=2.0 **Example** The ``default.tensor.tf`` device supports various differentiation modes. * *End-to-end classical backpropagation with the TensorFlow interface*. Using this method, the created QNode is a 'white-box', and is tightly integrated with your TensorFlow computation: >>> dev = qml.device("default.tensor.tf", wires=1) >>> @qml.qnode(dev, interface="tf", diff_method="backprop") >>> def circuit(x): ... qml.RX(x[1], wires=0) ... qml.Rot(x[0], x[1], x[2], wires=0) ... return qml.expval(qml.PauliZ(0)) >>> vars = tf.Variable([0.2, 0.5, 0.1]) >>> with tf.GradientTape() as tape: ... res = circuit(vars) >>> tape.gradient(res, vars) <tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.2526717e-01, -1.0086454e+00, 1.3877788e-17], dtype=float32)> In this mode, you must use the ``"tf"`` interface, as TensorFlow is used as the device backend. * *Device differentiation*. Using this method, the created QNode is a 'black-box' to your classical computation. PennyLane will automatically accept classical tensors from any supported interface, and query the device directly for the quantum gradient when required. >>> dev = qml.device("default.tensor.tf", wires=1) >>> @qml.qnode(dev, interface="autograd", diff_method="device") >>> def circuit(x): ... qml.RX(x[1], wires=0) ... qml.Rot(x[0], x[1], x[2], wires=0) ... return qml.expval(qml.PauliZ(0)) >>> grad_fn = qml.grad(circuit, argnum=[0]) >>> print(grad_fn([0.2, 0.5, 0.1])) ([array(-0.22526717), array(-1.00864546), array(6.9388939e-18)],) In this mode, even though TensorFlow is used as the device backend, it is independent of the chosen QNode interface. In the example above, we combine ``default.tensor.tf`` with the ``autograd`` interface. It can also be used with the ``torch`` and the ``tf`` interface. In addition to end-to-end classical backpropagation and device differentiation, the ``default.tensor.tf`` device also supports ``parameter-shift`` and ``finite-diff`` differentiation methods. Args: wires (int): number of subsystems in the quantum state represented by the device shots (int): Number of circuit evaluations/random samples to return when sampling from the device. Defaults to 1000 if not specified. representation (str): Underlying representation used for the tensor network simulation. Valid options are "exact" (no approximations made) or "mps" (simulated quantum state is approximated as a Matrix Product State). contraction_method (str): Method used to perform tensor network contractions. Only applicable for the "exact" representation. Valid options are "auto", "greedy", "branch", or "optimal". See documentation of the `TensorNetwork library <https://tensornetwork.readthedocs.io/en/latest/>`_ for more information about contraction methods. """ # pylint: disable=too-many-instance-attributes name = "PennyLane TensorNetwork (TensorFlow) simulator plugin" short_name = "default.tensor.tf" _capabilities = { "model": "qubit", "tensor_observables": True, "provides_jacobian": True, "passthru_interface": "tf", } _operation_map = copy.copy(DefaultTensor._operation_map) _operation_map.update( { "PhaseShift": lambda phi: tf.linalg.diag(ops.PhaseShift(phi)), "RX": ops.RX, "RY": ops.RY, "RZ": lambda theta: tf.linalg.diag(ops.RZ(theta)), "Rot": ops.Rot, "CRX": ops.CRX, "CRY": ops.CRY, "CRZ": lambda theta: tf.linalg.diag(ops.CRZ(theta)), "CRot": ops.CRot, } ) backend = "tensorflow" _reshape = staticmethod(tf.reshape) _array = staticmethod(tf.constant) _asarray = staticmethod(tf.convert_to_tensor) _real = staticmethod(tf.math.real) _imag = staticmethod(tf.math.imag) _abs = staticmethod(tf.abs) _squeeze = staticmethod(tf.squeeze) _expand_dims = staticmethod(tf.expand_dims) C_DTYPE = ops.C_DTYPE R_DTYPE = ops.R_DTYPE def __init__(self, wires, shots=1000, representation="exact", contraction_method="auto"): self.variables = [] """List[tf.Variable]: Free parameters, cast to TensorFlow variables, for this circuit.""" self.res = None """tf.tensor[tf.float64]: result from the last circuit execution""" self.op_params = {} """dict[Operation, List[Any, tf.Variable]]: A mapping from each operation in the queue, to the corresponding list of parameter values. These values can be Python numeric types, NumPy arrays, or TensorFlow variables.""" self.tape = None """tf.GradientTape: the gradient tape under which all tensor network modifications must be made""" super().__init__( wires, shots=shots, representation=representation, contraction_method=contraction_method, ) def reset(self): self.res = None self.variables = [] super().reset() def execution_context(self): self.tape = tf.GradientTape(persistent=True) return self.tape def pre_apply(self): super().pre_apply() self.op_params = {} for operation in self.op_queue: # Copy the operation parameters to the op_params dictionary. # Note that these are the unwrapped parameters, so PennyLane # free parameters will be represented as Variable instances. self.op_params[operation] = operation.data[:] # Loop through the free parameter reference dictionary for _, par_dep_list in self.parameters.items(): if not par_dep_list: # parameter is not used within circuit v = tf.Variable(0, dtype=self.R_DTYPE) self.variables.append(v) continue # get the first parameter dependency for each free parameter first = par_dep_list[0] # For the above parameter dependency, get the corresponding # operation parameter variable, and get the numeric value. # Convert the resulting value to a TensorFlow tensor. val = first.op.data[first.par_idx].val mult = first.op.data[first.par_idx].mult v = tf.Variable(val / mult, dtype=self.R_DTYPE) # Mark the variable to be watched by the gradient tape, # and append it to the variable list. self.variables.append(v) for p in par_dep_list: # Replace the existing Variable free parameter in the op_params dictionary # with the corresponding tf.Variable parameter. # Note that the free parameter might be scaled by the # variable.mult scaling factor. mult = p.op.data[p.par_idx].mult self.op_params[p.op][p.par_idx] = v * mult # check that no Variables remain in the op_params dictionary values = [item for sublist in self.op_params.values() for item in sublist] assert not any( isinstance(v, Variable) for v in values ), "A pennylane.Variable instance was not correctly converted to a tf.Variable" # flatten the variables list in case of nesting self.variables = tf.nest.flatten(self.variables) self.tape.watch(self.variables) for operation in self.op_queue: # Apply each operation, but instead of passing operation.parameters # (which contains the evaluated numeric parameter values), # pass op_params[operation], which contains numeric values # for fixed parameters, and tf.Variable objects for free parameters. super().apply(operation.name, operation.wires, self.op_params[operation]) def apply(self, operation, wires, par): # individual operations are already applied inside self.pre_apply() pass def execute(self, queue, observables, parameters=None, **kwargs): # pylint: disable=bad-super-call results = super(DefaultTensor, self).execute(queue, observables, parameters=parameters) with self.tape: # convert the results list into a single tensor self.res = tf.stack(results) if kwargs.get("return_native_type", False): return self.res # return the results as a NumPy array return self.res.numpy() def jacobian(self, queue, observables, parameters): """Calculates the Jacobian of the device circuit using TensorFlow backpropagation. Args: queue (list[Operation]): operations to be applied to the device observables (list[Observable]): observables to be measured parameters (dict[int, ParameterDependency]): reference dictionary mapping free parameter values to the operations that depend on them Returns: array[float]: Jacobian matrix of size (``num_params``, ``num_wires``) """ self.reset() self.execute(queue, observables, parameters=parameters) jac = self.tape.jacobian(self.res, self.variables, experimental_use_pfor=False) # TODO use unconnected_gradients=tf.UnconnectedGradients.ZERO instead of the following? jac = [i if i is not None else tf.zeros(self.res.shape, dtype=tf.float64) for i in jac] jac = tf.stack(jac) return jac.numpy().T