Using qml.fourier for function approximation

Hi!

I am trying to using qml.fourier module to approximate a function say cos(x). I am able to approximate the function with my quantum circuit. However, I was trying to extract the fourier coefficients of this trained quantum circuit and plot the Fourier series expansion. As I already know that the Fourier coefficients are c_0= 0 c_-1 = 0.5 and c_1=0.5. I need only one data encoding gate (RX) to approximate this simple function. However, if I use 2 rotation gates in the encoding layer, the output of the qml.fourier.coefficients is c_-2=0.5 and c_2=0.5 and the other coefficients to zero. I get the correct results if I use only one gate, by setting r=1 and removing qml.RX(x, wires=1, id='x') in the code. Can someone please let me know how to resolve this issue? I know that lowpass filter is used to solve the problem of showing higher order coefficients to the lower degree expansion. The problem I stated is the other way around. Thank you in advance for your kind support! Please see the code that I use below:

import pennylane as qml
from pennylane import numpy as np
from pennylane.fourier import *
from functools import partial
import numpy as nnp
import matplotlib.pyplot as plt

r = 2 #number of wires
dev = qml.device('default.qubit', wires=r)


@qml.qnode(dev)
def circuit(w, x):
    qml.RX(w[0], wires=0)
    qml.RX(x, wires=0, id="x")
    qml.RX(x, wires=1, id='x')
    qml.RX(w[1], wires=0)
    return qml.expval(qml.PauliZ(wires=0))


weights = np.array([0.1, 0.1], requires_grad=True) #initial weights

X = np.linspace(0, 2 * np.pi, 20, requires_grad=True) #input points

opt = qml.AdamOptimizer(0.3) #optimizer

def cost(circuit, w, x):
    '''Cost function which account for function approximation of cos(x)'''
    return np.mean((circuit(w, x) - nnp.cos(x)) ** 2)

#Training the model
for step in range(20):
    # select batch of data
    batch_index = np.random.randint(0, len(X), (20,))
    x_batch = X[batch_index]
    # update the weights by one optimizer step
    weights = opt.step(lambda w: cost(circuit, w, x_batch), weights)

freqs = circuit_spectrum(circuit)(weights, 0.2)  #frequencies available in the circuit
print(freqs['x'])

degree = r
partial_circuit = partial(circuit, weights) #fixing the weights of the circuit
coeffs = coefficients(partial_circuit, 1, degree, lowpass_filter=True) #fourier coeffcients
print(coeffs)
conj_coeff = np.conjugate(coeffs)


def fourier_expansion(x):
    """Generate a truncated Fourier series of degree"""
    res = coeffs[0]
    for i in range(1, 2 * degree):
        exponent = complex(0, (freqs['x'][i-1]) * x)
        res += coeffs[i] * np.exp(exponent) + conj_coeff[i] * np.exp(-exponent)
    return np.real(res)

f_points = []
p_points = []
a_points = []

for j in X:
    f_points.append(fourier_expansion(j))
    p_points.append(circuit(weights, j))
    a_points.append(nnp.cos(j))

plt.plot(f_points, label='fourier series')
plt.plot(p_points, label='output')
plt.plot(a_points, label='target func')
plt.legend()
plt.show()

And, finally, make sure to include the versions of your packages. Specifically, show us the output of qml.about():

Name: PennyLane
Version: 0.35.1
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: GitHub - PennyLaneAI/pennylane: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Author:
Author-email:
License: Apache License 2.0
Location: c:\users\hedge\miniconda3\envs\quantum_fourier_model\lib\site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info: Windows-10-10.0.22631-SP0
Python version: 3.10.13
Numpy version: 1.26.4
Scipy version: 1.12.0
Installed devices:

  • default.clifford (PennyLane-0.35.1)
  • default.gaussian (PennyLane-0.35.1)
  • default.mixed (PennyLane-0.35.1)
  • default.qubit (PennyLane-0.35.1)
  • default.qubit.autograd (PennyLane-0.35.1)
  • default.qubit.jax (PennyLane-0.35.1)
  • default.qubit.legacy (PennyLane-0.35.1)
  • default.qubit.tf (PennyLane-0.35.1)
  • default.qubit.torch (PennyLane-0.35.1)
  • default.qutrit (PennyLane-0.35.1)
  • null.qubit (PennyLane-0.35.1)
  • lightning.qubit (PennyLane_Lightning-0.35.1)

I found a mistake in defining the fourier series. I think that the range in the for loop should be (1, degree). Thank you!

1 Like

Hi @Pratibha_Hegde ,

Thank you for posting the solution!

Hi @CatalinaAlbornoz , thank you for your reply. This was one issue I found with my code. However, it didn’t fix my problem I described previously. If you have any comments, I would very much appreciate it. Thanks in advance!

Hi @Pratibha_Hegde,
Would you be able to post the latest version of your code using PennyLane v0.37?
Also please post your output for qml.about. One of my colleagues will try to replicate the issue next week. Thanks!

1 Like

Hi @CatalinaAlbornoz ,

Here is my updated code:

import pennylane as qml
from pennylane import numpy as np
from pennylane.fourier import *
from functools import partial
import numpy as nnp
import matplotlib.pyplot as plt

r = 2 #number of wires
dev = qml.device('default.qubit', wires=r)


@qml.qnode(dev)
def circuit(w, x):
    qml.RX(w[0], wires=0)
    qml.RX(x, wires=0, id="x")
    qml.RY(x, wires=1, id='x')
    qml.RX(w[1], wires=0)
    return qml.expval(qml.PauliZ(wires=0))


weights = np.array([0.1, 0.1], requires_grad=True) #initial weights

X = np.linspace(0, 2 * np.pi, 20, requires_grad=True) #input points

opt = qml.AdamOptimizer(0.3) #optimizer

def cost(circuit, w, x):
    '''Cost function which account for function approximation of cos(x)'''
    return np.mean((circuit(w, x) - nnp.cos(x)) ** 2)

#Training the model
for step in range(200):
    # select batch of data
    batch_index = np.random.randint(0, len(X), (20,))
    x_batch = X[batch_index]
    # update the weights by one optimizer step
    weights = opt.step(lambda w: cost(circuit, w, x_batch), weights)

freqs = circuit_spectrum(circuit)(weights, 0.2)  #frequencies available in the circuit
print(freqs['x'])

degree = r
partial_circuit = partial(circuit, weights) #fixing the weights of the circuit
coeffs = coefficients(partial_circuit, 1, degree, lowpass_filter=True) #fourier coeffcients
print(coeffs)
conj_coeff = np.conjugate(coeffs)


def fourier_expansion(x):
    """Generate a truncated Fourier series of degree"""
    res = coeffs[0]
    for i in range(1, degree):
        exponent = complex(0, (freqs['x'][i-1]) * x)
        res += coeffs[i] * np.exp(exponent) + conj_coeff[i] * np.exp(-exponent)
    return np.real(res)

f_points = []
p_points = []
a_points = []

for j in X:
    f_points.append(fourier_expansion(j))
    p_points.append(circuit(weights, j))
    a_points.append(nnp.cos(j))

plt.plot(f_points, label='fourier series')
plt.plot(p_points, label='output')
plt.plot(a_points, label='target func')
plt.legend()
plt.show()

Here is the output of qml.about()

Name: PennyLane
Version: 0.37.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: c:\users\hedge\pycharmprojects\cfd-quantum\venv\lib\site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, packaging, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info:           Windows-10-10.0.22631-SP0
Python version:          3.11.9
Numpy version:           1.26.4
Scipy version:           1.14.1
Installed devices:
- default.clifford (PennyLane-0.37.0)
- default.gaussian (PennyLane-0.37.0)
- default.mixed (PennyLane-0.37.0)
- default.qubit (PennyLane-0.37.0)
- default.qubit.autograd (PennyLane-0.37.0)
- default.qubit.jax (PennyLane-0.37.0)
- default.qubit.legacy (PennyLane-0.37.0)
- default.qubit.tf (PennyLane-0.37.0)
- default.qubit.torch (PennyLane-0.37.0)
- default.qutrit (PennyLane-0.37.0)
- default.qutrit.mixed (PennyLane-0.37.0)
- default.tensor (PennyLane-0.37.0)
- null.qubit (PennyLane-0.37.0)
- lightning.qubit (PennyLane_Lightning-0.37.0)

Hi @Pratibha_Hegde!

I think you’re doing everything exactly right, up to a re-ordering of the Fourier coefficients, introduced by np.fft.fftn. Basically, the coefficients are ordered according to the frequencies

[0, 1.0, 2.0, ... r, -r, -r+1, ..., -1.0]

which is… unexpected, I suppose :sweat_smile:

So modifying your fourier_expansion to this:

ordered_coeffs = [*coeffs[r+1:], *coeffs[:r+1]]
def fourier_expansion(x):
    """Generate a truncated Fourier series of degree"""
    return np.dot(np.array(ordered_coeffs), np.exp(1j * np.array(freqs["x"]) * x))

will fix the issue, basically simply by virtue of the ordered_coeffs which undo this funny quirk of the numpy output.

I hope this helps! Let us know in case there are any other issues :slight_smile:

Happy Fourier transforming :ocean:

1 Like

Dear @Wierichs, thank you very much for clarifying this and correcting my code! Indeed, the order of coefficients was not as expected! Now the code is working fine and consistent also for other examples. Thank you once again!

Amazing, happy to hear that!