Coefficients#
A characteristic property of any Fourier model are its coefficients. Our package can, given a model, calculate the corresponding coefficients.
In the simplest case, this could look as follows:
from qml_essentials.model import Model
from qml_essentials.coefficients import Coefficients
model = Model(
n_qubits=2,
n_layers=1,
circuit_type="Hardware_Efficient",
)
coeffs = Coefficients.get_spectrum(model)
But wait! There is much more to this. Let's keep on reading if you're curious .
Detailled Explanation#
To visualize what happens, let's create a very simplified Fourier model
class Model_Fct:
def __init__(self, c, f):
self.c = c
self.f = f
self.degree = max(f)
def __call__(self, inputs, **kwargs):
return np.sum([c * np.cos(inputs * f) for f, c in zip(self.f, self.c)], axis=0)
This model takes a vector of coefficients and frequencies on instantiation. When called, these coefficients and frequencies are used to compute the output of the model, which is the sum of sine functions determined by the length of the vectors. Let's try that for just two frequencies:
freqs = [1,3]
coeffs = [1,1]
fs = max(freqs) * 2 + 1
model_fct = Model_Fct(coeffs,freqs)
x = np.arange(0,2 * np.pi, 2 * np.pi/fs)
out = model_fct(x)
We can now calculate the Fast Fourier Transform of our model:
X = np.fft.fft(out) / len(out)
X_shift = np.fft.fftshift(X)
X_freq = np.fft.fftfreq(X.size, 1/fs)
X_freq_shift = np.fft.fftshift(X_freq)
np.fft.fftshift
is not required from a technical point of view, but makes our spectrum nicely zero-centered and projected correctly.
The same can be done with our framework, with a neat one-liner:
Note, that applying the shift can be controlled with the optional shift
argument.
Another important point is, that the force_mean
flag is set, and the execution_type
is is implicitly set to expval
.
This is mainly because, we require a single expectation value to calculate the coefficients.
Increasing the Resolution#
You might have noticed that we choose our sampling frequency fs
in such a way, that it just fulfills the Nyquist criterium.
Also the number of samples x
are just enough to sufficiently represent our function.
In such a simplified scenario, this is fine, but there are cases, where we want to have more information both in the time and frequency domain.
Therefore, two additional arguments exist in the get_spectrum
method:
- mfs
: The multiplier for the highest frequency. Increasing this will increase the width of the spectrum
- mts
: The multiplier for the number of time samples. Increasing this will increase the resolution of the time domain and therefore "add" frequencies in between our original frequencies.
- trim
: Whether to remove the Nyquist frequency if spectrum is even. This will result in a symmetric spectrum
Note that, as the frequencies change with the mts
argument, we have to take that into account when calculating the frequencies with the last call.
Feel free to checkout our jupyter notebook if you would like to play around with this.
A sidenote on the performance; Increasing the mts
value effectively increases the input lenght that goes into the model.
This means that mts=2
will require twice the time to compute, which will be very noticable when running noisy simulations.
Power spectral density#
In some cases it can be useful to get the power spectral density (PSD). As calculation of this metric might differ between the different research domains, we included a function to get the PSD of a given spectrum using the following formula:
where \(F\) is the spectrum and \(n_\text{samples}\) the length of the input vector.
model = Model(
n_qubits=4,
n_layers=1,
circuit_type="Circuit_19",
random_seed=1000
)
coeffs, freqs = Coefficients.get_spectrum(model, mfs=1, mts=1, shift=True)
psd = Coefficients.get_psd(coeffs)
Analytic Coefficients#
All of the calculations above were performed by applying a Fast Fourier Transform to the output of our Model. However, we can also calculate the coefficients analytically.
This can be achieved by the so called FourierTree
class:
from qml_essentials.coefficients import FourierTree
fourier_tree = FourierTree(model)
an_coeffs, an_freqs = fourier_tree.get_spectrum(force_mean=True)
Note that while this takes significantly longer to compute, it gives us the precise coefficients, solely depending on the parameters. We can verify this by comparing it to the previous results:
Technical Details#
We use an approach developed by Nemkov et al., which was later extended by Wiedmann et al.. The implementation is also inspired by the corresponding code for Nemkov et al.'s paper.
In Nemkov et al.'s algorithm the first step is to separate Clifford and non-Clifford gates, such that all Clifford gates can be regarded as part of the observable, and the actual circuit only consists of Pauli rotations (cf. qml_essentials.utils.PauliCircuit). The main idea is then to split each Pauli rotation into sine and cosine product terms to obtain the coefficients, which are only dependent on the parameters of the circuit.
Currently, our implementation supports only one input feature, albeit more are theoretical possible.