def test_no_default_args(self): """Test that empty dict is returned if function has no default arguments""" def dummy_func(a, b): # pylint: disable=unused-argument pass res = pu._get_default_args(dummy_func) assert not res
def test_get_default_args(self): """Test that default arguments are correctly extracted""" def dummy_func( a, b, c=8, d=[0, 0.65], e=np.array([4]), f=None ): # pylint: disable=unused-argument,dangerous-default-value pass res = pu._get_default_args(dummy_func) expected = {"c": (2, 8), "d": (3, [0, 0.65]), "e": (4, np.array([4])), "f": (5, None)} assert res == expected
def construct(self, args, kwargs=None): """Constructs a representation of the quantum circuit. The user should never have to call this method. This method is called automatically the first time :meth:`QNode.evaluate` or :meth:`QNode.jacobian` is called. It executes the quantum function, stores the resulting sequence of :class:`~.operation.Operation` instances, and creates the variable mapping. Args: args (tuple): Represent the free parameters passed to the circuit. Here we are not concerned with their values, but with their structure. Each free param is replaced with a :class:`~.variable.Variable` instance. kwargs (dict): Additional keyword arguments may be passed to the quantum circuit function, however PennyLane does not support differentiating with respect to keyword arguments. Instead, keyword arguments are useful for providing data or 'placeholders' to the quantum circuit function. """ # pylint: disable=too-many-branches,too-many-statements self.queue = [] self.ev = [] # temporary queue for EVs if kwargs is None: kwargs = {} # flatten the args, replace each with a Variable instance with a unique index temp = [Variable(idx) for idx, val in enumerate(_flatten(args))] self.num_variables = len(temp) # store the nested shape of the arguments for later unflattening self.model = args # arrange the newly created Variables in the nested structure of args variables = unflatten(temp, args) # get default kwargs that weren't passed keyword_sig = _get_default_args(self.func) self.keyword_defaults = {k: v[1] for k, v in keyword_sig.items()} self.keyword_positions = {v[0]: k for k, v in keyword_sig.items()} keyword_values = {} keyword_values.update(self.keyword_defaults) keyword_values.update(kwargs) if self.cache: # caching mode, must use variables for kwargs # wrap each keyword argument as a Variable kwarg_variables = {} for key, val in keyword_values.items(): temp = [Variable(idx, name=key) for idx, _ in enumerate(_flatten(val))] kwarg_variables[key] = unflatten(temp, val) Variable.free_param_values = np.array(list(_flatten(args))) Variable.kwarg_values = {k: np.array(list(_flatten(v))) for k, v in keyword_values.items()} # set up the context for Operation entry if QNode._current_context is None: QNode._current_context = self else: raise QuantumFunctionError('QNode._current_context must not be modified outside this method.') # generate the program queue by executing the quantum circuit function try: if self.cache: # caching mode, must use variables for kwargs # so they can be updated without reconstructing res = self.func(*variables, **kwarg_variables) else: # no caching, fine to directly pass kwarg values res = self.func(*variables, **keyword_values) finally: # remove the context QNode._current_context = None #---------------------------------------------------------- # check the validity of the circuit # quantum circuit function return validation if isinstance(res, qml.operation.Observable): if res.return_type is qml.operation.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, qml.operation.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.name)) # check that all ev's are returned, in the correct order if res != tuple(self.ev): raise QuantumFunctionError("All measured observables must be returned in the " "order they are measured.") self.ev = list(res) #: list[Observable]: returned observables self.ops = self.queue + self.ev #: list[Operation]: combined list of circuit operations # list all operations except for the identity non_identity_ops = [op for op in self.ops if not isinstance(op, qml.ops.Identity)] # contains True if op is a CV, False if it is a discrete variable are_cvs = [isinstance(op, qml.operation.CV) for op in non_identity_ops] if not all(are_cvs) and any(are_cvs): raise QuantumFunctionError("Continuous and discrete operations are not " "allowed in the same quantum circuit.") # TODO: we should enforce plugins using the Device.capabilities dictionary to specify # whether they are qubit or CV devices, and remove this logic here. self.type = 'CV' if all(are_cvs) else 'qubit' # map each free variable to the operations 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 keyword arguments self.variable_deps.setdefault(p.idx, []).append(ParameterDependency(op, j)) # generate directed acyclic graph self.circuit = CircuitGraph(self.ops, self.variable_deps) #: dict[int->str]: map from free parameter index to the gradient method to be used with that parameter self.grad_method_for_par = {k: self._best_method(k) for k in self.variable_deps}