# 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."""Contains a function that computes the fourier series ofa quantum expectation value."""importwarningsfromfunctoolsimportwrapsfrominspectimportsignatureimportnumpyasnpfromautorayimportnumpyasanpimportpennylaneasqmldef_reconstruct_equ(fun,num_frequency,x0=None,f0=None,interface=None):r"""Reconstruct a univariate Fourier series with consecutive integer frequencies, using trigonometric interpolation and equidistant shifts. This technique is based on `Dirichlet kernels <https://en.wikipedia.org/wiki/Dirichlet_kernel>`_, see `Vidal and Theis (2018) <https://arxiv.org/abs/1812.06323>`_ or `Wierichs et al. (2022) <https://doi.org/10.22331/q-2022-03-30-677>`_. Args: fun (callable): Univariate finite Fourier series to reconstruct. It must have signature ``float -> float`` . num_frequency (int): Number of integer frequencies in ``fun``. All integer frequencies below ``num_frequency`` are assumed to be present in ``fun`` as well; if they are not, the output is correct put the reconstruction could have been performed with fewer evaluations of ``fun`` . x0 (float): Center to which to shift the reconstruction. The points at which ``fun`` is evaluated are *not* affected by ``x0`` . f0 (float): Value of ``fun`` at zero; Providing ``f0`` saves one evaluation of ``fun``. interface (str): Which auto-differentiation framework to use as interface. This determines in which interface the output reconstructed function is intended to be used. Returns: callable: Reconstructed Fourier series with ``num_frequency`` frequencies. This function is a purely classical function. Furthermore, it is fully differentiable. """ifnotabs(int(num_frequency))==num_frequency:raiseValueError(f"num_frequency must be a non-negative integer, got {num_frequency}")a=(num_frequency+0.5)/np.pib=0.5/np.pishifts_pos=qml.math.arange(1,num_frequency+1)/ashifts_neg=-shifts_pos[::-1]shifts=qml.math.concatenate([shifts_neg,[0.0],shifts_pos])shifts=anp.asarray(shifts,like=interface)f0=fun(0.0)iff0isNoneelsef0evals=(list(map(fun,shifts[:num_frequency]))+[f0]+list(map(fun,shifts[num_frequency+1:])))evals=anp.asarray(evals,like=interface)x0=anp.asarray(np.float64(0.0),like=interface)ifx0isNoneelsex0def_reconstruction(x):"""Univariate reconstruction based on equidistant shifts and Dirichlet kernels. The derivative at of ``sinc`` are not well-implemented in TensorFlow and Autograd, use the Fourier transform reconstruction if this derivative is needed. """_x=x-x0-shiftsreturnqml.math.tensordot(qml.math.sinc(a*_x)/qml.math.sinc(b*_x),evals,axes=[[0],[0]],)return_reconstruction_warn_text_f0_ignored=("The provided value of the function at zero will be ignored due to the ""provided shift values. This may lead to additional evaluations of the ""function to be reconstructed.")def_reconstruct_gen(fun,spectrum,shifts=None,x0=None,f0=None,interface=None):r"""Reconstruct a univariate (real-valued) Fourier series with given spectrum. Args: fun (callable): Univariate finite Fourier series to reconstruct. It must have signature ``float -> float`` . spectrum (Collection): Frequency spectrum of the Fourier series; non-positive frequencies are ignored. shifts (Sequence): Shift angles at which to evaluate ``fun`` for the reconstruction. Chosen equidistantly within the interval :math:`[0, 2\pi/f_\text{max}]` if ``shifts=None`` , where :math:`f_\text{max}` is the biggest frequency in ``spectrum``. x0 (float): Center to which to shift the reconstruction. The points at which ``fun`` is evaluated are *not* affected by ``x0`` . f0 (float): Value of ``fun`` at zero; If :math:`0` is among the ``shifts`` and ``f0`` is provided, one evaluation of ``fun`` is saved. interface (str): Which auto-differentiation framework to use as interface. This determines in which interface the output reconstructed function is intended to be used. Returns: callable: Reconstructed Fourier series with :math:`R` frequencies in ``spectrum`` . This function is a purely classical function. Furthermore, it is fully differentiable. """# pylint: disable=unused-argument, too-many-argumentshave_f0=f0isnotNonehave_shifts=shiftsisnotNonespectrum=anp.asarray(spectrum,like=interface)spectrum=spectrum[spectrum>0]f_max=qml.math.max(spectrum)# If no shifts are provided, choose equidistant onesneed_f0=Trueifnothave_shifts:R=qml.math.shape(spectrum)[0]shifts=qml.math.arange(-R,R+1)*2*np.pi/(f_max*(2*R+1))*Rzero_idx=Relifhave_f0:zero_idx=qml.math.where(qml.math.isclose(shifts,qml.math.zeros_like(shifts[0])))zero_idx=zero_idx[0][0]if(len(zero_idx)>0andlen(zero_idx[0])>0)elseNoneneed_f0=zero_idxisnotNone# Take care of shifts close to zero if f0 was providedifhave_f0andneed_f0:# Only one shift may be zero at a timeshifts=qml.math.concatenate([shifts[zero_idx:zero_idx+1],shifts[:zero_idx],shifts[zero_idx+1:]])shifts=anp.asarray(shifts,like=interface)evals=anp.asarray([f0]+list(map(fun,shifts[1:])),like=interface)else:shifts=anp.asarray(shifts,like=interface)ifhave_f0andnotneed_f0:warnings.warn(_warn_text_f0_ignored)evals=anp.asarray(list(map(fun,shifts)),like=interface)L=len(shifts)# Construct the coefficient matrix case by caseC1=qml.math.ones((L,1))C2=qml.math.cos(qml.math.tensordot(shifts,spectrum,axes=0))C3=qml.math.sin(qml.math.tensordot(shifts,spectrum,axes=0))C=qml.math.hstack([C1,C2,C3])# Solve the system of linear equationscond=qml.math.linalg.cond(C)ifcond>1e8:warnings.warn(f"The condition number of the Fourier transform matrix is very large: {cond}.",UserWarning,)W=qml.math.linalg.solve(C,evals)# Extract the Fourier coefficientsR=(L-1)//2a0=W[0]a=anp.asarray(W[1:R+1],like=interface)b=anp.asarray(W[R+1:],like=interface)x0=anp.asarray(np.float64(0.0),like=interface)ifx0isNoneelsex0# Construct the Fourier seriesdef_reconstruction(x):"""Univariate reconstruction based on arbitrary shifts."""x=x-x0return(a0+qml.math.tensordot(qml.math.cos(spectrum*x),a,axes=[[0],[0]])+qml.math.tensordot(qml.math.sin(spectrum*x),b,axes=[[0],[0]]))return_reconstructiondef_parse_ids(ids,info_dict):"""Parse different formats of ``ids`` into the right dictionary format, potentially using the information in ``info_dict`` to complete it. """ifidsisNone:# Infer all id information from info_dictreturn{outer_key:inner_dict.keys()forouter_key,inner_dictininfo_dict.items()}ifisinstance(ids,str):# ids only provides a single argument name but no parameter indicesreturn{ids:info_dict[ids].keys()}ifnotisinstance(ids,dict):# ids only provides argument names but no parameter indicesreturn{_id:info_dict[_id].keys()for_idinids}returnidsdef_parse_shifts(shifts,R,arg_name,par_idx,atol,need_f0):"""Processes shifts for a single reconstruction and determines wheter the function at the reconstruction point, ``f0`` will be needed. """# pylint: disable=too-many-arguments_shifts=shifts.get(arg_name)if_shiftsisnotNone:_shifts=_shifts.get(par_idx)if_shiftsisnotNone:# Check whether the _shifts have the correct sizeiflen(_shifts)!=2*R+1:raiseValueError(f"The number of provided shifts ({len(_shifts)}) does not fit to the "f"number of frequencies (2R+1={2*R+1}) for parameter {par_idx} in "f"argument {arg_name}.")ifany(qml.math.isclose(_shifts,qml.math.zeros_like(_shifts),rtol=0,atol=atol)):# If 0 is among the shifts, f0 is neededreturn_shifts,True# If 0 is not among the shifts, f0 is not neededreturn_shifts,(Falseorneed_f0)# If no shifts are given, f0 is needed alwaysreturn_shifts,Truedef_prepare_jobs(ids,nums_frequency,spectra,shifts,atol):r"""For inputs to reconstruct, determine how the given information yields function reconstruction tasks and collect them into a dictionary ``jobs``. Also determine whether the function at zero is needed. Args: ids (dict or Sequence or str): Indices for the QNode parameters with respect to which the QNode should be reconstructed as a univariate function, per QNode argument. Each key of the dict, entry of the list, or the single ``str`` has to be the name of an argument of ``qnode`` . If a ``dict`` , the values of ``ids`` have to contain the parameter indices for the respective array-valued QNode argument represented by the key. These indices always are tuples, i.e. ``()`` for scalar and ``(i,)`` for one-dimensional arguments. If a ``list`` , the parameter indices are inferred from ``nums_frequency`` if given or ``spectra`` else. If ``None``, all keys present in ``nums_frequency`` / ``spectra`` are considered. nums_frequency (dict[dict]): Numbers of integer frequencies -- and biggest frequency -- per QNode parameter. The keys have to be argument names of ``qnode`` and the inner dictionaries have to be mappings from parameter indices to the respective integer number of frequencies. If the QNode frequencies are not contiguous integers, the argument ``spectra`` should be used to save evaluations of ``qnode`` . Takes precedence over ``spectra`` and leads to usage of equidistant shifts. spectra (dict[dict]): Frequency spectra per QNode parameter. The keys have to be argument names of ``qnode`` and the inner dictionaries have to be mappings from parameter indices to the respective frequency spectrum for that parameter. Ignored if ``nums_frequency!=None``. shifts (dict[dict]): Shift angles for the reconstruction per QNode parameter. The keys have to be argument names of ``qnode`` and the inner dictionaries have to be mappings from parameter indices to the respective shift angles to be used for that parameter. For :math:`R` non-zero frequencies, there must be :math:`2R+1` shifts given. Ignored if ``nums_frequency!=None``. atol (float): Absolute tolerance used to analyze shifts lying close to 0. Returns: dict[dict]: Indices for the QNode parameters with respect to which the QNode will be reconstructed. Cast to the dictionary structure explained above. If the input ``ids`` was a dictionary, it is returned unmodified. callable: The reconstruction method to use, one out of two internal methods. dict[dict[dict]]: Keyword arguments for the reconstruction method specifying how to carry out the reconstruction. The outer-most keys are QNode argument names, the middle keys are parameter indices like the inner keys of ``nums_frequency`` or ``spectra`` and the inner-most dictionary contains the keyword arguments, i.e. the keys are keyword argument names for the reconstruction method bool: Whether any of the reconstruction jobs will require the evaluation of the function at the position of reconstruction itself. """ifnums_frequencyisNone:ifspectraisNone:raiseValueError("Either nums_frequency or spectra must be given.")ids=_parse_ids(ids,spectra)ifshiftsisNone:shifts={}need_f0=Falserecon_fn=_reconstruct_genjobs={}# If no shifts are provided, compute themforarg_name,inner_dictinids.items():_jobs={}forpar_idxininner_dict:# Determine spectrum and number of frequencies, discounting for 0_spectrum=spectra[arg_name][par_idx]R=len(_spectrum)-1_shifts,need_f0=_parse_shifts(shifts,R,arg_name,par_idx,atol,need_f0)# Store jobifR>0:_jobs[par_idx]={"shifts":_shifts,"spectrum":_spectrum}else:# R=0 belongs to a constant function_jobs[par_idx]=Nonejobs[arg_name]=_jobselse:jobs={}need_f0=Trueids=_parse_ids(ids,nums_frequency)recon_fn=_reconstruct_equforarg_name,inner_dictinids.items():_jobs={}forpar_idxininner_dict:_num_frequency=nums_frequency[arg_name][par_idx]_jobs[par_idx]={"num_frequency":_num_frequency}if_num_frequency>0elseNonejobs[arg_name]=_jobsreturnids,recon_fn,jobs,need_f0
[docs]defreconstruct(qnode,ids=None,nums_frequency=None,spectra=None,shifts=None):r"""Reconstruct an expectation value QNode along a single parameter direction. This means we restrict the QNode to vary only one parameter, a univariate restriction. For common quantum gates, such restrictions are finite Fourier series with known frequency spectra. Thus they may be reconstructed using Dirichlet kernels or a non-uniform Fourier transform. Args: qnode (pennylane.QNode): Quantum node to be reconstructed, representing a circuit that outputs an expectation value. ids (dict or Sequence or str): Indices for the QNode parameters with respect to which the QNode should be reconstructed as a univariate function, per QNode argument. Each key of the dict, entry of the list, or the single ``str`` has to be the name of an argument of ``qnode`` . If a ``dict`` , the values of ``ids`` have to contain the parameter indices for the respective array-valued QNode argument represented by the key. These indices always are tuples, i.e., ``()`` for scalar and ``(i,)`` for one-dimensional arguments. If a ``list`` , the parameter indices are inferred from ``nums_frequency`` if given or ``spectra`` else. If ``None``, all keys present in ``nums_frequency`` / ``spectra`` are considered. nums_frequency (dict[dict]): Numbers of integer frequencies -- and biggest frequency -- per QNode parameter. The keys have to be argument names of ``qnode`` and the inner dictionaries have to be mappings from parameter indices to the respective integer number of frequencies. If the QNode frequencies are not contiguous integers, the argument ``spectra`` should be used to save evaluations of ``qnode`` . Takes precedence over ``spectra`` and leads to usage of equidistant shifts. spectra (dict[dict]): Frequency spectra per QNode parameter. The keys have to be argument names of ``qnode`` and the inner dictionaries have to be mappings from parameter indices to the respective frequency spectrum for that parameter. Ignored if ``nums_frequency!=None``. shifts (dict[dict]): Shift angles for the reconstruction per QNode parameter. The keys have to be argument names of ``qnode`` and the inner dictionaries have to be mappings from parameter indices to the respective shift angles to be used for that parameter. For :math:`R` non-zero frequencies, there must be :math:`2R+1` shifts given. Ignored if ``nums_frequency!=None``. Returns: function: Function which accepts the same arguments as the QNode and one additional keyword argument ``f0`` to provide the QNode value at the given arguments. When called, this function will return a dictionary of dictionaries, formatted like ``nums_frequency`` or ``spectra`` , that contains the univariate reconstructions per QNode parameter. For each provided ``id`` in ``ids``, the QNode is restricted to varying the single QNode parameter corresponding to the ``id`` . This univariate function is then reconstructed via a Fourier transform or Dirichlet kernels, depending on the provided input. Either the frequency ``spectra`` of the QNode with respect to its input parameters or the numbers of frequencies, ``nums_frequency`` , per parameter must be provided. For quantum-circuit specific details, we refer the reader to `Vidal and Theis (2018) <https://arxiv.org/abs/1812.06323>`__ , `Vidal and Theis (2020) <https://www.frontiersin.org/articles/10.3389/fphy.2020.00297/full>`__ , `Schuld, Sweke and Meyer (2021) <https://journals.aps.org/pra/abstract/10.1103/PhysRevA.103.032430>`__ , and `Wierichs, Izaac, Wang and Lin (2022) <https://doi.org/10.22331/q-2022-03-30-677>`__ . An introduction to the concept of quantum circuits as Fourier series can also be found in the `Quantum models as Fourier series <https://pennylane.ai/qml/demos/tutorial_expressivity_fourier_series>`__ and `General parameter-shift rules <https://pennylane.ai/qml/demos/tutorial_general_parshift>`__ demos as well as the :mod:`qml.fourier <pennylane.fourier>` module docstring. **Example** Consider the following QNode: .. code-block:: python dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def circuit(x, Y): qml.RX(x, wires=0) qml.RY(Y[0], wires=0) qml.RY(Y[1], wires=1) qml.CNOT(wires=[0, 1]) qml.RY(5* Y[1], wires=1) return qml.expval(qml.Z(0) @ qml.Z(1)) x = 0.4 Y = np.array([1.9, -0.5]) f = 2.3 circuit_value = circuit(x, Y) It has three variational parameters ``x`` (a scalar) and two entries of ``Y`` (an array-like). A reconstruction job could then be with respect to the two entries of ``Y``, which enter the circuit with one and six integer frequencies, respectively (see the additional examples below for details on how to obtain the frequency spectrum if it is not known): >>> nums_frequency = {"Y": {(0,): 1, (1,): 6}} >>> with qml.Tracker(circuit.device) as tracker: ... rec = qml.fourier.reconstruct(circuit, {"Y": [(0,), (1,)]}, nums_frequency)(x, Y) >>> rec.keys() dict_keys(['Y']) >>> print(*rec["Y"].items(), sep="\n") ((0,), <function _reconstruct_equ.<locals>._reconstruction at 0x7fbd685aee50>) ((1,), <function _reconstruct_equ.<locals>._reconstruction at 0x7fbd6866eee0>) >>> recon_Y0 = rec["Y"][(0,)] >>> recon_Y1 = rec["Y"][(1,)] >>> np.isclose(recon_Y0(Y[0]), circuit_value) True >>> np.isclose(recon_Y1(Y[1]+1.3), circuit(x, Y+np.eye(2)[1]*1.3)) True We successfully reconstructed the dependence on the two entries of ``Y`` , keeping ``x`` and the respective other entry in ``Y`` at their initial values. Let us also see how many executions of the device were used to obtain the reconstructions: >>> tracker.totals {'batches': 15, 'simulations': 15, 'executions': 15} The example above used that we already knew the frequency spectra of the QNode of interest. However, this is in general not the case and we may need to compute the spectrum first. This can be done with :func:`.fourier.qnode_spectrum` : >>> spectra = qml.fourier.qnode_spectrum(circuit)(x, Y) >>> spectra.keys() dict_keys(['x', 'Y']) >>> spectra["x"] {(): [-1.0, 0.0, 1.0]} >>> print(*spectra["Y"].items(), sep="\n") ((0,), [-1.0, 0.0, 1.0]) ((1,), [-6.0, -5.0, -4.0, -1.0, 0.0, 1.0, 4.0, 5.0, 6.0]) For more detailed explanations, usage details and additional examples, see the usage details section below. .. details:: :title: Usage Details **Input formatting** As described briefly above, the essential inputs to ``reconstruct`` that provide information about the QNode are given as dictionaries of dictionaries, where the outer keys reference the argument names of ``qnode`` and the inner keys reference the parameter indices within each array-valued QNode argument. These parameter indices always are tuples, so that for scalar-valued QNode parameters, the parameter index is ``()`` by convention and the ``i`` -th parameter of a one-dimensional array can be accessed via ``(i,)`` . For example, providing ``nums_frequency`` - for a scalar argument: ``nums_frequency = {"x": {(): 4}}`` - for a one-dimensional argument: ``nums_frequency = {"Y": {(0,): 2, (1,): 9, (4,): 1}}`` - for a three-dimensional argument: ``nums_frequency = {"Z": {(0, 2, 5): 2, (1, 1, 4): 1}}`` This applies to ``nums_frequency`` , ``spectra`` , and ``shifts`` . Note that the information provided in ``nums_frequency`` / ``spectra`` is essential for the correctness of the reconstruction. On the other hand, the input format for ``ids`` is flexible and allows a collection of parameter indices for each QNode argument name (as a ``dict`` ), a collection of argument names (as a ``list``, ``set``, ``tuple`` or similar), or a single argument name (as a ``str`` ) to be defined. For ``ids=None`` , all argument names contained in ``nums_frequency`` -- or ``spectra`` if ``nums_frequency`` is not used -- are considered. For inputs that do not specify parameter indices per QNode argument name (all formats but ``dict`` ), these parameter indices are inferred from ``nums_frequency`` / ``spectra`` . **Reconstruction cost** The reconstruction cost -- in terms of calls to ``qnode`` -- depend on the number of frequencies given via ``nums_frequency`` or ``spectra`` . A univariate reconstruction for :math:`R` frequencies takes :math:`2R+1` evaluations. If multiple univariate reconstructions are performed at the same point with various numbers of frequencies :math:`R_k` , the cost are :math:`1+2\sum_k R_k` if the shift :math:`0` is used in all of them. This is in particular the case if ``nums_frequency`` or ``spectra`` with ``shifts=None`` is used. If the number of frequencies is too large or the given frequency spectrum contains more than the spectrum of ``qnode`` , the reconstruction is performed suboptimally but remains correct. For integer-valued spectra with gaps, the equidistant reconstruction is thus suboptimal and the non-equidistant version method be used (also see the examples below). **Numerical stability** In general, the reconstruction with equidistant shifts for equidistant frequencies (used if ``nums_frequency`` is provided) is more stable numerically than the more general Fourier reconstruction (used if ``nums_frequency=None`` ). If the system of equations to be solved in the Fourier transform is ill-conditioned, a warning is raised as the output might become unstable. Examples for this are shift values or frequencies that lie very close to each other. **Differentiability** The returned scalar functions are differentiable in all interfaces with respect to their scalar input variable. They expect these inputs to be in the same interface as the one used by the QNode. More advanced differentiability, for example of the reconstructions with respect to QNode properties, is not supported reliably yet. .. warning:: When using ``TensorFlow`` or ``Autograd`` *and* ``nums_frequency`` , the reconstructed functions are not differentiable at the point of reconstruction. One workaround for this is to use ``spectra`` as input instead and to thereby use the Fourier transform instead of Dirichlet kernels. Alternatively, the original QNode evaluation can be used. **More examples** Consider the QNode from the example above, now with an additional, tunable frequency ``f`` for the Pauli-X rotation that is controlled by ``x`` : .. code-block:: python @qml.qnode(dev) def circuit(x, Y, f=1.0): qml.RX(f * x, wires=0) qml.RY(Y[0], wires=0) qml.RY(Y[1], wires=1) qml.CNOT(wires=[0, 1]) qml.RY(5* Y[1], wires=1) return qml.expval(qml.Z(0) @ qml.Z(1)) f = 2.3 circuit_value = circuit(x, Y) We repeat the reconstruction job for the dependence on ``Y[1]`` . Note that even though information about ``Y[0]`` is contained in ``nums_frequency`` , ``ids`` determines which reconstructions are performed. >>> with qml.Tracker(circuit.device) as tracker: ... rec = qml.fourier.reconstruct(circuit, {"Y": [(1,)]}, nums_frequency)(x, Y) >>> tracker.totals {'executions': 13} As expected, we required :math:`2R+1=2\cdot 6+1=13` circuit executions. However, not all frequencies below :math:`f_\text{max}=6` are present in the circuit, so that a reconstruction using knowledge of the full frequency spectrum will be cheaper: >>> spectra = {"Y": {(1,): [0., 1., 4., 5., 6.]}} >>> with tracker: ... rec = qml.fourier.reconstruct(circuit, {"Y": [(1,)]}, None, spectra)(x, Y) >>> tracker.totals {'executions': 9} We again obtain the full univariate dependence on ``Y[1]`` but with considerably fewer executions on the quantum device. Once we obtained the classical function that describes the dependence, no additional circuit evaluations are performed: >>> with tracker: ... for Y1 in np.arange(-np.pi, np.pi, 20): ... rec["Y"][(1,)](-2.1) >>> tracker.totals {} If we want to reconstruct the dependence of ``circuit`` on ``x`` , we cannot use ``nums_frequency`` if ``f`` is not an integer. One could rescale ``x`` to obtain the frequency :math:`1` again, or directly use ``spectra`` . We will combine the latter with another reconstruction with respect to ``Y[0]`` : >>> spectra = {"x": {(): [0., f]}, "Y": {(0,): [0., 1.]}} >>> with tracker: ... rec = qml.fourier.reconstruct(circuit, None, None, spectra)(x, Y, f=f) >>> tracker.totals {'executions': 5} >>> recon_x = rec["x"][()] >>> np.isclose(recon_x(x+0.5), circuit(x+0.5, Y, f=f) True Note that by convention, the parameter index for a scalar variable is ``()`` and that the frequency :math:`0` always needs to be included in the spectra. Furthermore, we here skipped the input ``ids`` so that the reconstruction was performed for all keys in ``spectra`` . The reconstruction with a single non-zero frequency costs three evaluations of ``circuit`` for each, ``x`` and ``Y[0]`` . Performing both reconstructions at the same position allowed us to save one of the evaluations and reduce the number of calls to :math:`5`. """# pylint: disable=cell-var-from-loop, unused-argumentatol=1e-8ids,recon_fn,jobs,need_f0=_prepare_jobs(ids,nums_frequency,spectra,shifts,atol)sign_fn=qnode.funcifisinstance(qnode,qml.QNode)elseqnodearg_names=list(signature(sign_fn).parameters.keys())arg_idx_from_names={arg_name:ifori,arg_nameinenumerate(arg_names)}@wraps(qnode)defwrapper(*args,f0=None,**kwargs):iff0isNoneandneed_f0:f0=qnode(*args,**kwargs)interface=qml.math.get_interface(args[0])defconstant_fn(x):"""Univariate reconstruction of a constant Fourier series."""returnf0# Carry out the reconstruction jobsreconstructions={}forarg_name,inner_dictinjobs.items():_reconstructions={}arg_idx=arg_idx_from_names[arg_name]forpar_idx,jobininner_dict.items():ifjobisNone:_reconstructions[par_idx]=constant_fnelse:iflen(qml.math.shape(args[arg_idx]))==0:shift_vec=qml.math.ones_like(args[arg_idx])x0=args[arg_idx]else:shift_vec=qml.math.zeros_like(args[arg_idx])shift_vec=qml.math.scatter_element_add(shift_vec,par_idx,1.0)x0=args[arg_idx][par_idx]def_univariate_fn(x):new_arg=args[arg_idx]+shift_vec*xnew_args=args[:arg_idx]+(new_arg,)+args[arg_idx+1:]returnqnode(*new_args,**kwargs)_reconstructions[par_idx]=recon_fn(_univariate_fn,**job,x0=x0,f0=f0,interface=interface)reconstructions[arg_name]=_reconstructionsreturnreconstructionsreturnwrapper