From 64ae004e74af45bc749bd4fee7d7ba2047dc594c Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 23 Jul 2025 10:24:57 +0200 Subject: [PATCH 01/33] add empty demo template --- .../thumbnail_resourcefulness.png | Bin 0 -> 9331 bytes .../thumbnail_resourcefulness.png | Bin 0 -> 9331 bytes .../tutorial_resourcefulness.metadata.json | 52 +++++++++++++++++ demonstrations/tutorial_resourcefulness.py | 53 ++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 _static/demo_thumbnails/large_demo_thumbnails/thumbnail_resourcefulness.png create mode 100644 _static/demo_thumbnails/regular_demo_thumbnails/thumbnail_resourcefulness.png create mode 100644 demonstrations/tutorial_resourcefulness.metadata.json create mode 100644 demonstrations/tutorial_resourcefulness.py diff --git a/_static/demo_thumbnails/large_demo_thumbnails/thumbnail_resourcefulness.png b/_static/demo_thumbnails/large_demo_thumbnails/thumbnail_resourcefulness.png new file mode 100644 index 0000000000000000000000000000000000000000..43f7523ad8c1fce7bf302033dcadf5a8808d70f5 GIT binary patch literal 9331 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 Date: Wed, 23 Jul 2025 12:23:46 +0200 Subject: [PATCH 02/33] write intro --- .../resourcefulness/figure2_paper.png | Bin 0 -> 30159 bytes demonstrations/tutorial_resourcefulness.py | 62 +++++-- ...torial_resourcefulness_multipartite_gfd.py | 168 ++++++++++++++++++ ...urcefulness_multipartite_gfd_analytical.py | 98 ++++++++++ ...ourcefulness_multipartite_gfd_blockdiag.py | 87 +++++++++ 5 files changed, 398 insertions(+), 17 deletions(-) create mode 100644 _static/demonstration_assets/resourcefulness/figure2_paper.png create mode 100644 demonstrations/tutorial_resourcefulness_multipartite_gfd.py create mode 100644 demonstrations/tutorial_resourcefulness_multipartite_gfd_analytical.py create mode 100644 demonstrations/tutorial_resourcefulness_multipartite_gfd_blockdiag.py diff --git a/_static/demonstration_assets/resourcefulness/figure2_paper.png b/_static/demonstration_assets/resourcefulness/figure2_paper.png new file mode 100644 index 0000000000000000000000000000000000000000..07486e216bee01f2d374a292bf90a0ffbacd609b GIT binary patch literal 30159 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 l<)>(X`Mf6 + """ + pur = np.zeros(n+1) + pur[0] = 1 / (2**n) + pur[-1] = 1 - 1 / (2**n) + return pur + +def plot_purities_analytical(n: int): + # generate plotting data + data_series = [ + me_free_purities(n), + me_ghz_purities(n), + me_w_purities(n), + me_haar_purities(n), + ] + # labels + labels = [ + "Free", + "GHZ", + "W", + "Haar" + ] + + # Grab default color cycle + colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] + + # Create two vertically aligned subplots sharing the x-axis + fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) + + for i, data in enumerate(data_series): + color = colors[i % len(colors)] + ax1.plot(data, label=f'{labels[i]}', color=color) + ax2.plot(np.cumsum(data), label=f'{labels[i]}', color=color) + + ax1.set_ylabel('Purity') + ax2.set_ylabel('Cumulative Purity') + ax2.set_xlabel('Module weight') + + ax1.legend(loc='upper left') + ax2.legend(loc='upper left') + + plt.tight_layout() + plt.show() + +# Example + +plot_purities_analytical(2) + diff --git a/demonstrations/tutorial_resourcefulness_multipartite_gfd_blockdiag.py b/demonstrations/tutorial_resourcefulness_multipartite_gfd_blockdiag.py new file mode 100644 index 0000000000..88a7702a4b --- /dev/null +++ b/demonstrations/tutorial_resourcefulness_multipartite_gfd_blockdiag.py @@ -0,0 +1,87 @@ +## Shows how the adjoint superoperator of an element of SU(2)xSU(2)x...xSU(2) + +import numpy as np +import itertools +import functools + +# single‑qubit Paulis +_pauli_map = { + 'I': np.array([[1,0],[0,1]], dtype=complex), + 'X': np.array([[0,1],[1,0]], dtype=complex), + 'Y': np.array([[0,-1j],[1j,0]],dtype=complex), + 'Z': np.array([[1,0],[0,-1]], dtype=complex), +} + +def adjoint_superoperator(U): + """ + Adjoint superop representing (U X U†) + """ + return np.kron(U.conj(), U) + +def pauli_basis(n): + """ + generates the basis of Pauli operators, and orders it by appearence in the isotypical decomp of Times_i SU(2) + """ + all_strs = [''.join(s) for s in itertools.product('IXYZ', repeat=n)] + sorted_strs = sorted(all_strs, key=lambda s: (n-s.count('I'), s)) + norm = np.sqrt(2**n) + mats = [] + for s in sorted_strs: + factors = [_pauli_map[ch] for ch in s] + M = functools.reduce(lambda A,B: np.kron(A,B), factors) + mats.append(M.reshape(-1)/norm) + B = np.column_stack(mats) + return sorted_strs, B + +def rotate_superoperator(U): + """ + Rotates the superop of the adj of a unitary U to the irrep-sorted Pauli basis + """ + S = adjoint_superoperator(U) + n = int(np.log2(U.shape[0])) + basis,B = pauli_basis(n) + S_rot = B.conj().T @ S @ B + return basis, S_rot + + +# Haar‐random unitary helper +def haar_unitary(N): + """ + Generates a Haar random NxN unitary matrix + """ + X = (np.random.randn(N, N) + 1j*np.random.randn(N, N)) / np.sqrt(2) + Q, R = np.linalg.qr(X) + phases = np.exp(-1j * np.angle(np.diag(R))) + return Q @ np.diag(phases) + + +# we showcase the block-diagonalization in the case of two qubits +n = 2 +Us = [haar_unitary(2) for _ in range(n)] +# U is a tensor product of single-qubit unitaries +U = functools.reduce(lambda A, B: np.kron(A, B), Us) +basis, S_rot = rotate_superoperator(U) +np.set_printoptions( + formatter={'float': lambda x: f"{x:5.2g}"}, + linewidth=200, # default is 75; + threshold=10000 # so it doesn’t summarize large arrays +) +print("Adjoint Superoperator of U in the computational basis") +Uadj = adjoint_superoperator(U) +Uadj_real = Uadj.real +Ur_round = np.round(Uadj_real, 2) +print("Rounded real part:") +print(Ur_round) +Uadj_imag = Uadj.imag +Ur_round = np.round(Uadj_imag, 2) +print("Rounded imag part:") +print(Ur_round) + + +# now round and print +## NOTICE THAT IN PAULI BASIS THE UNITARY ADJOINT ACTION IS ORTHOGONAL +print("Adjoint Superoperator of U in the Irrep basis") +S_real = S_rot.real +Sr_round = np.round(S_real, 2) +print("Rounded real part (the operator is real):") +print(Sr_round) From b5f094f666684dfc4d8480fa92560aa81cd800c5 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 23 Jul 2025 13:53:49 +0200 Subject: [PATCH 03/33] sketched the standard Fourier analysis example --- demonstrations/tutorial_resourcefulness.py | 175 ++++++++++++++++++++- 1 file changed, 167 insertions(+), 8 deletions(-) diff --git a/demonstrations/tutorial_resourcefulness.py b/demonstrations/tutorial_resourcefulness.py index c35d23c2c0..7d115a6d06 100644 --- a/demonstrations/tutorial_resourcefulness.py +++ b/demonstrations/tutorial_resourcefulness.py @@ -21,11 +21,13 @@ is not a function over :mathbb:`R` or :mathbb:`Z`, but a quantum state. "Generalised" indicates that we don't use the standard Fourier transform, but its group-theoretic generalisations [LINK TO RELATED DEMOS]. This is important, because [#Bermejo_Braccia]_ link a resource to a group -- essentially, by defining the set of unitaries that maps resource-free -states to resource-free states as a "representation" of a group. The intuition, however, is exactly the same as in the -standard Fourier transform, where large higher-order Fourier coefficients indicate a less "smooth" function. +states to resource-free states as a "representation" of a group, which gets block-diagonalised to find a generalised Fourier basis. +The intuition, however, is exactly the same as in the standard Fourier transform, where large higher-order Fourier +coefficients indicate a less "smooth" function. -In this tutorial we will explain the idea of generalised Fourier analysis for resource theories first using the standard -Fourier decomposition of a function. We will then consider the same concepts, but analysing the entanglement +In this tutorial we will illustrate the idea of generalised Fourier analysis for resource theories with two simple examples. +First we will look at a standard Fourier decomposition of a function from the perspective of resources, to introduce the +basic idea. Secondly, we will use these concepts to analyse the entanglement resource of quantum states, reproducing Figure 2 in [#Bermejo_Braccia]_. .. figure:: ../_static/demonstration_assets/resourcefulness/figure2_paper.png @@ -36,8 +38,10 @@ a tensor product state with little entanglement has contributions in lower-order Fourier coefficients. The interpolation between the two extremes, exemplified by a Haar random state, has a Fourier spectrum in between. -Luckily, in this case the bases for the subspaces are associated with Pauli operators, and generalised Fourier analysis -can be done by computing Pauli expectations, saving us from diving too deep into representation theory. +Luckily, in the case of entanglement as a resource, the bases for the subspaces are associated with Pauli operators, +and generalised Fourier analysis can be done by computing Pauli expectations. +This saves us from diving too deep into representation theory. In fact, the tutorial should be informative without knowing much +about groups at all! .. note:: Note that all methods discussed here are classical methods to analyse properties of quantum states, @@ -49,17 +53,172 @@ Standard Fourier analysis through the lense of resources -------------------------------------------------------- -[TODO: code up usual Fourier transform, and link to regular representation] +Let's start recalling the standard Fourier transform, and for simplicity we'll work with the +discrete version. Given N real values :math:`x_0,...,x_{N-1}`, that we can interpret [LINK TO OTHER DEMO] as the values +of a function :math:`f(0), ..., f(N-1)` over the integers :math:`x \in {0,...,N-1}`, the Fourier transform +computes the Fourier coefficients +.. math:: + \hat{f}(k) = \frac{1}{\sqrt{N}\sum_{x=0}^{N-1} f(x) e^{\frac{2 \pi i}{N} k x}, k = 0,...,N-1 + +Here, :math:`e^{\frac{2 \pi i}{N} k x}` is a basis for the space of functions over the integers, the so-called *Fourier basis*. + +For example: """ +import matplotlib.pyplot as plt import numpy as np +N = 12 + +def f(x): + """Some function""" + return 0.5*(x-4)**3 + +def f_hat(k): + """Fourier coefficients of f""" + projections = [f(x)*np.exp(2 * np.pi * 1j * k * x / N) for x in range(N)] + return (1/np.sqrt(N)) * np.sum(projections) + +def plot(f, f_hat): + + fig, (ax1, ax2) = plt.subplots(2, 1) + ax1.bar(range(N), [f(x) for x in range(N)], color='dimgray') + ax1.set_title(f"function f") + ax2.bar(range(N), [f_hat(k) for k in range(N)], color='dimgray') + ax2.set_title("Fourier coefficients") + plt.tight_layout() + plt.show() + +plot(f, f_hat) + +###################################################################### +# Now, what kind of resource are we dealing with here? In other words, what +# measure of complexity gets higher when a function has large higher-order Fourier coefficients? +# +# Well, let us look for the function with the least resource or complexity! For this we can just +# work backwards: define a Fourier spectrum that only has a contribution in the lowest order coefficient, +# and inverse Fourier transform to look at the function! +# +# + +def g_hat(k): + """The least complex Fourier spectrum possible""" + if k==0: + return 1 + else: + return 0 + +def g(x): + """Function whose Fourier spectrum is `g_hat`""" + projections = [g_hat(k)*np.exp(-2 * np.pi * 1j * k * x / N) for k in range(N)] + return (1/np.sqrt(N)) * np.sum(projections) + +plot(g, g_hat) + +###################################################################### +# Well, the function is constant. This makes sense, because we know that the decay of the Fourier coefficient +# is related to the smoothness of a function (by defining how often it is differentiablle), and a +# constant function is maximally smooth -- it does not wiggle at all. In other words, +# the resource of the standard Fourier transform is smoothness! +# + +###################################################################### +# Linking resources to group representations +# ------------------------------------------ +# +# Fourier transforms are intimately linked to groups (in fact, the x-domain :math:`0,...,N-1` is strictly speaking a group, +# which you can learn more about [here]. Without expecting you to know group jargon, we have to establish a few concepts +# that generalise the above example to quantum states and more generic resources. The crucial idea is to +# define a resource by fixing a set of vectors (later, quantum states) that are considered resource-free. +# We also need to define unitary matrices that map free vectors to free vectors, and these matrices need to form a "unitary representation" of a group :math:`G`, +# which is a matrix valued function :math:`R(g), g \in G` on the group. +# This is all we need to guarantee that the matrices can be simultaneously block-diagonalised, or written as a +# direct sum of smaller matrix-valued functions over the group, :math:`r(g)`. Some of the blocks may be identical. +# +# These blocks are so-called irreducible representations of the group :math:`G`, which are fundamental concepts in +# group theory [Refer to book]. What is important for us is that these smaller matrix valued functions define a +# subspace [TODO: clarify how this works, it always confused me]. +# +# A Fourier coefficient is nothing but a projection of a vector in :math:`V` onto one of these subspaces. In our standard +# exmaple above, the subspaces are one-dimensional and spanned by the Fourier basis functions :math:`\chi_k(x) = e^{\frac{2 \pi i}{N} k x}`. +# The projection is executed by the sum :math:`\sum_x f(x) \chi_k(x)`. The concept of these functions as irreducible +# representations allows us to generalise the Fourier transform to lots of other groups, and hence, resources. +# +# But what is the representation :math:`R(g)` for the standard Fourier transform? Let's follow the recipe: +# +# 1. We first need to consider our function :math:`f(0), ..., f(N-1)` as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. +# 2. As argued above, the set of smoothness-free vectors correspond to constant functions, :math:`f(0) = ... = f(N-1)`. +# 3. We need a set of unitary matrices that does not change the constantness of the vector. This set is given by permutation matrices, +# which swap the entries of the vector but do not change any of them. +# 4. These matrices actually form a representation :math:`R(g)` for :math:`g \in G = Z_N`, the *regular representation*. +# 5. We are now guaranteed that there is a basis change that diagonalises all matrices :math:`R(g)` together. +# (Note that the Fourier transform is sometimes defined as the basis change that block-diagonalises the regular representation!) As mentioned, this is +# a block-diagonalisation where the blocks happen to be 1-dimensional, as is the rule for all "Abelian" groups. +# 6. The values on the diagonal of :math:`R(g)` under this basis change are exactly the Fourier basis functions `:math:`e^{\frac{2 \pi i}{N} k x}`. +# +# Let's verify this! +# +# First, let us write our function and Fourier spectrum above as vectors: +# + +f_vec = np.array([f(x) for x in range(N)]) +f_hat_vec = np.array([f(k) for k in range(N)]) + +###################################################################### +# The Fourier transform then becomes a matrix multiplication with the matrix: +# +# .. math:: +# F = \frac{1}{\sqrt{N}} \begin{bmatrix} +# 1&1&1&1&\cdots &1 \\ +# 1&\omega&\omega^2&\omega^3&\cdots&\omega^{N-1} \\ +# 1&\omega^2&\omega^4&\omega^6&\cdots&\omega^{2(N-1)}\\ 1&\omega^3&\omega^6&\omega^9&\cdots&\omega^{3(N-1)}\\ +# \vdots&\vdots&\vdots&\vdots&\ddots&\vdots\\ +# 1&\omega^{N-1}&\omega^{2(N-1)}&\omega^{3(N-1)}&\cdots&\omega^{(N-1)(N-1)} +# \end{bmatrix} +# +# Let's check that this is indeed the same as what we did above. +# + +# get the Fourier matrix from scipy +from scipy.linalg import dft +F = dft(N)/np.sqrt(N) + +# TODO: NOT YET THE SAME +print(F.dot(f_vec), f_hat_vec) + + +###################################################################### +# This matrix F is supposed to be the basis transform that diagonalises +# a permutation matrix (which was a unitary representation evaluated at some group element). +# + +# create a permutation matrix +P = np.eye(N) +np.random.shuffle(P) + +# change into the Fourier basis using F +np.set_printoptions(precision=2, suppress=True) +# TODO: Not yet working +print(F @ P @ F.conj().T) + + +###################################################################### +# To recap, we saw that the standard Fourier analysis can be generalised by +# interpreting "smoothness" or "constantness" as a resource, linking it to a vector space and a group +# representation and (block-)diagonalising the representation. The blocks, here one-dimensional, +# form a basis for subspaces, and Fourier coefficients are just projections of some vector (here, +# a function) into these subspaces. +# +# Armed with this recipe, we can now try to analyse entanglement as a resource, +# and density matrices describing quantum states as vectors in a vector space :math:`L(H)`. +# + ###################################################################### # Fourier analysis of entanglement # -------------------------------- # -# [TODO: explain the change to other representations, and why the Pauli stuff works. Reproduce Fig2] +# [TODO] # # From 340a6c5a94bf4b6f5e09de4b49914a6ec482eda9 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Thu, 24 Jul 2025 11:24:51 +0200 Subject: [PATCH 04/33] polish first part, code runs now --- demonstrations/tutorial_resourcefulness.py | 142 +++++++++++++-------- 1 file changed, 87 insertions(+), 55 deletions(-) diff --git a/demonstrations/tutorial_resourcefulness.py b/demonstrations/tutorial_resourcefulness.py index 7d115a6d06..2dea28ee96 100644 --- a/demonstrations/tutorial_resourcefulness.py +++ b/demonstrations/tutorial_resourcefulness.py @@ -22,12 +22,12 @@ standard Fourier transform, but its group-theoretic generalisations [LINK TO RELATED DEMOS]. This is important, because [#Bermejo_Braccia]_ link a resource to a group -- essentially, by defining the set of unitaries that maps resource-free states to resource-free states as a "representation" of a group, which gets block-diagonalised to find a generalised Fourier basis. -The intuition, however, is exactly the same as in the standard Fourier transform, where large higher-order Fourier -coefficients indicate a less "smooth" function. +The intuition, however, is exactly the same as in the standard Fourier transform: large higher-order Fourier +coefficients indicate a less "smooth" or "resourceful" function. In this tutorial we will illustrate the idea of generalised Fourier analysis for resource theories with two simple examples. -First we will look at a standard Fourier decomposition of a function from the perspective of resources, to introduce the -basic idea. Secondly, we will use these concepts to analyse the entanglement +First we will look at a standard Fourier decomposition of a function, but from the perspective of resources,in order to +introduce the basic idea in a familiar setting. Secondly, we will use the same concepts to analyse the entanglement resource of quantum states, reproducing Figure 2 in [#Bermejo_Braccia]_. .. figure:: ../_static/demonstration_assets/resourcefulness/figure2_paper.png @@ -38,16 +38,11 @@ a tensor product state with little entanglement has contributions in lower-order Fourier coefficients. The interpolation between the two extremes, exemplified by a Haar random state, has a Fourier spectrum in between. -Luckily, in the case of entanglement as a resource, the bases for the subspaces are associated with Pauli operators, -and generalised Fourier analysis can be done by computing Pauli expectations. +Luckily, in the case of entanglement as a resource, the generalised Fourier coefficients can be computed using Pauli expectations. This saves us from diving too deep into representation theory. In fact, the tutorial should be informative without knowing much -about groups at all! - -.. note:: - Note that all methods discussed here are classical methods to analyse properties of quantum states, - and of course, they will scale only as much as the mathematical objects involved can be efficiently described classically. - It is a fascinating question in which situations the Fourier coefficients of a physical states could be read out on a quantum computer, which can - sometimes perform the block-diagonalisation efficiently. +about groups at all! But of course, even in this simple case the numerical analysis scales exponentially with the number of qubits +in general. It is a fascinating question in which situations the Fourier coefficients of a physical state could be read out +on a quantum computer, which is known to sometimes perform the block-diagonalisation efficiently. Standard Fourier analysis through the lense of resources @@ -61,9 +56,11 @@ .. math:: \hat{f}(k) = \frac{1}{\sqrt{N}\sum_{x=0}^{N-1} f(x) e^{\frac{2 \pi i}{N} k x}, k = 0,...,N-1 -Here, :math:`e^{\frac{2 \pi i}{N} k x}` is a basis for the space of functions over the integers, the so-called *Fourier basis*. +In words, the Fourier coefficients are projections of the function :math:`f` onto the "Fourier" basis functions +:math:`e^{\frac{2 \pi i}{N} k x}`. Note that we use a normalisation here that is consistent with a unitary transform that +we construct as a matrix below. -For example: +Let's see this equation in action. """ import matplotlib.pyplot as plt @@ -72,21 +69,24 @@ N = 12 def f(x): - """Some function""" + """Some function.""" return 0.5*(x-4)**3 def f_hat(k): - """Fourier coefficients of f""" - projections = [f(x)*np.exp(2 * np.pi * 1j * k * x / N) for x in range(N)] - return (1/np.sqrt(N)) * np.sum(projections) + """Fourier coefficients of f.""" + projection = [ f(x) * np.exp(-2 * np.pi * 1j * k * x / N)/np.sqrt(N) for x in range(N)] + return np.sum(projection) def plot(f, f_hat): fig, (ax1, ax2) = plt.subplots(2, 1) - ax1.bar(range(N), [f(x) for x in range(N)], color='dimgray') + ax1.bar(range(N), [np.real(f(x)) for x in range(N)], color='dimgray') # casting to real is needed in case we perform an inverse FT ax1.set_title(f"function f") - ax2.bar(range(N), [f_hat(k) for k in range(N)], color='dimgray') + + ax2.bar(np.array(range(N))+0.05, [np.imag(f_hat(x)) for x in range(N)], color='lightpink', label="imaginary part") + ax2.bar(range(N), [np.real(f_hat(k)) for k in range(N)], color='dimgray', label="real part") ax2.set_title("Fourier coefficients") + plt.legend() plt.tight_layout() plt.show() @@ -98,29 +98,29 @@ def plot(f, f_hat): # # Well, let us look for the function with the least resource or complexity! For this we can just # work backwards: define a Fourier spectrum that only has a contribution in the lowest order coefficient, -# and inverse Fourier transform to look at the function! +# and apply the inverse Fourier transform to look at the function it corresponds to! # # def g_hat(k): - """The least complex Fourier spectrum possible""" + """The least complex Fourier spectrum possible.""" if k==0: return 1 else: return 0 def g(x): - """Function whose Fourier spectrum is `g_hat`""" - projections = [g_hat(k)*np.exp(-2 * np.pi * 1j * k * x / N) for k in range(N)] - return (1/np.sqrt(N)) * np.sum(projections) + """Function whose Fourier spectrum is `g_hat`.""" + projection = [g_hat(k) * np.exp(2 * np.pi * 1j * k * x / N)/np.sqrt(N) for k in range(N)] + return np.sum(projection) plot(g, g_hat) ###################################################################### # Well, the function is constant. This makes sense, because we know that the decay of the Fourier coefficient -# is related to the smoothness of a function (by defining how often it is differentiablle), and a +# is related to the number of times a function is differentiable, which in turn is the technical definition of smoothness. A # constant function is maximally smooth -- it does not wiggle at all. In other words, -# the resource of the standard Fourier transform is smoothness! +# the resource of the standard Fourier transform is smoothness, and a resource-free function is constant! # ###################################################################### @@ -128,33 +128,52 @@ def g(x): # ------------------------------------------ # # Fourier transforms are intimately linked to groups (in fact, the x-domain :math:`0,...,N-1` is strictly speaking a group, -# which you can learn more about [here]. Without expecting you to know group jargon, we have to establish a few concepts +# which you can learn more about [here]). Without expecting you to know group jargon, we have to establish a few concepts # that generalise the above example to quantum states and more generic resources. The crucial idea is to # define a resource by fixing a set of vectors (later, quantum states) that are considered resource-free. -# We also need to define unitary matrices that map free vectors to free vectors, and these matrices need to form a "unitary representation" of a group :math:`G`, -# which is a matrix valued function :math:`R(g), g \in G` on the group. +# We also need to define unitary matrices that map resource-free vectors to resource-free vectors, +# and these matrices need to form a *unitary representation* of a group :math:`G`, +# which is a matrix-valued function :math:`R(g), g \in G` on the group. # This is all we need to guarantee that the matrices can be simultaneously block-diagonalised, or written as a -# direct sum of smaller matrix-valued functions over the group, :math:`r(g)`. Some of the blocks may be identical. +# direct sum of smaller matrix-valued functions over the group, :math:`r^{\alpha}(g)`. Note that some of the blocks may be identical. +# If you know group theory, then you will recognise that the :math:`r^{\alpha}(g)` are the *irreducible representations* of the group. +# +# [TODO: image] +# # -# These blocks are so-called irreducible representations of the group :math:`G`, which are fundamental concepts in -# group theory [Refer to book]. What is important for us is that these smaller matrix valued functions define a -# subspace [TODO: clarify how this works, it always confused me]. +# What is important for us is that these smaller matrix-valued functions :math:`r^{\alpha}(g)` define a +# subspace :math:`V_{\alpha, j}`, where the index :math:`j` accounts for the fact that there may be several copies of +# an :math:`r^{\alpha}(g)` on the diagonal of :math:`R(g)`, each of which corresponds to one subspace. +# [TODO: BASIS :math:`w^{(i)}_{\alpha, j}`]. +# +# The Generalised Fourier Decomposition (GFD) purity is the length of a projection of a vector :math:`v \in V` onto one of these subspaces :math:`V_{\alpha, j}`, +# +# .. math:: +# \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. # -# A Fourier coefficient is nothing but a projection of a vector in :math:`V` onto one of these subspaces. In our standard -# exmaple above, the subspaces are one-dimensional and spanned by the Fourier basis functions :math:`\chi_k(x) = e^{\frac{2 \pi i}{N} k x}`. -# The projection is executed by the sum :math:`\sum_x f(x) \chi_k(x)`. The concept of these functions as irreducible +# In our standard +# example above, the subspaces are one-dimensional spaces spanned by the Fourier basis functions :math:`\chi_k(x) = e^{\frac{2 \pi i}{N} k x}`. +# The vector space is the space of functions (if this is confusing, think of the function values as arranged in +# N-dimensional vectors :math:`\vec{f}` and :math:`\vec{\chi}_k`). +# The projection is executed by the sum :math:`\sum_x f(x) \chi_k(x) = \langle f, \chi_k`. In other words, the GFD purity +# is the absolute square of the Fourier coefficient, +# +#.. math:: +# \mathcal{P}(\vec{f}) = | \vec{\chi}_k, \vec{f} \rangle |^2 = |\hat{f}|^2. +# +# Generalising from the standard Fourier basis functions to irreducible # representations allows us to generalise the Fourier transform to lots of other groups, and hence, resources. # -# But what is the representation :math:`R(g)` for the standard Fourier transform? Let's follow the recipe: +# So far so good, but what is the representation :math:`R(g)` for the standard Fourier transform? Let's follow the recipe: # # 1. We first need to consider our function :math:`f(0), ..., f(N-1)` as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. -# 2. As argued above, the set of smoothness-free vectors correspond to constant functions, :math:`f(0) = ... = f(N-1)`. -# 3. We need a set of unitary matrices that does not change the constantness of the vector. This set is given by permutation matrices, +# 2. As argued above, the set of resource-free vectors correspond to constant functions, :math:`f(0) = ... = f(N-1)`. +# 3. We need a set of unitary matrices that does not change the "constantness" of the vector. This set is given by permutation matrices, # which swap the entries of the vector but do not change any of them. -# 4. These matrices actually form a representation :math:`R(g)` for :math:`g \in G = Z_N`, the *regular representation*. +# 4. The *circulant* permutation matrices can be shown to form a unitary representation :math:`R(g)` for :math:`g \in G = Z_N`, called the *regular representation*. # 5. We are now guaranteed that there is a basis change that diagonalises all matrices :math:`R(g)` together. # (Note that the Fourier transform is sometimes defined as the basis change that block-diagonalises the regular representation!) As mentioned, this is -# a block-diagonalisation where the blocks happen to be 1-dimensional, as is the rule for all "Abelian" groups. +# a block-diagonalisation where the blocks happen to be 1-dimensional, as is the rule for so-called "Abelian" groups. # 6. The values on the diagonal of :math:`R(g)` under this basis change are exactly the Fourier basis functions `:math:`e^{\frac{2 \pi i}{N} k x}`. # # Let's verify this! @@ -163,7 +182,7 @@ def g(x): # f_vec = np.array([f(x) for x in range(N)]) -f_hat_vec = np.array([f(k) for k in range(N)]) +f_hat_vec = np.array([f_hat(k) for k in range(N)]) ###################################################################### # The Fourier transform then becomes a matrix multiplication with the matrix: @@ -184,31 +203,44 @@ def g(x): from scipy.linalg import dft F = dft(N)/np.sqrt(N) -# TODO: NOT YET THE SAME -print(F.dot(f_vec), f_hat_vec) +# compare to what we previously computed +print(np.allclose(F @ f_vec, f_hat_vec)) ###################################################################### -# This matrix F is supposed to be the basis transform that diagonalises -# a permutation matrix (which was a unitary representation evaluated at some group element). +# Above we claimed that this matrix F is the basis transform that diagonalises +# a permutation matrix (in other words, the unitary representation evaluated at some group element). # -# create a permutation matrix -P = np.eye(N) -np.random.shuffle(P) +# create a circulant permutation matrix +i = np.random.randint(0, N) +first_row = np.zeros(N) +first_row[i] = 1 + +# initialize the matrix with the first row +P = np.zeros((N, N)) +P[0, :] = first_row + +# generate subsequent rows by cyclically shifting the previous row +for i in range(1, N): + P[i, :] = np.roll(P[i-1, :], 1) + # change into the Fourier basis using F np.set_printoptions(precision=2, suppress=True) -# TODO: Not yet working -print(F @ P @ F.conj().T) +P_F = F @ P @ np.linalg.inv(F) + +# check if the resulting matrix is diagonal +# trick: remove diagonal and make sure the remainder is zero +print(np.allclose(P_F - np.diag(np.diagonal(P_F)), np.zeros((N,N)) )) ###################################################################### # To recap, we saw that the standard Fourier analysis can be generalised by # interpreting "smoothness" or "constantness" as a resource, linking it to a vector space and a group # representation and (block-)diagonalising the representation. The blocks, here one-dimensional, -# form a basis for subspaces, and Fourier coefficients are just projections of some vector (here, -# a function) into these subspaces. +# form a basis for subspaces. The GFD purities suggested in [#Bermejo_Braccia]_ as a resource fingerprint +# are just projections of some vector (here, a function) onto the basis vectors and taking the absolute square. # # Armed with this recipe, we can now try to analyse entanglement as a resource, # and density matrices describing quantum states as vectors in a vector space :math:`L(H)`. From cc0ee9e3b1eb3046e969f6f587c8f54e99c53484 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Tue, 29 Jul 2025 14:13:36 +0200 Subject: [PATCH 05/33] outline second part --- demonstrations/tutorial_resourcefulness.py | 110 ++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/demonstrations/tutorial_resourcefulness.py b/demonstrations/tutorial_resourcefulness.py index 2dea28ee96..731f1007e5 100644 --- a/demonstrations/tutorial_resourcefulness.py +++ b/demonstrations/tutorial_resourcefulness.py @@ -144,9 +144,10 @@ def g(x): # What is important for us is that these smaller matrix-valued functions :math:`r^{\alpha}(g)` define a # subspace :math:`V_{\alpha, j}`, where the index :math:`j` accounts for the fact that there may be several copies of # an :math:`r^{\alpha}(g)` on the diagonal of :math:`R(g)`, each of which corresponds to one subspace. -# [TODO: BASIS :math:`w^{(i)}_{\alpha, j}`]. +# Every subspace is spanned by a basis :math:`\{w^{(i)}_{\alpha, j}\}_{i=1}^{\mathrm{dim(V_{\alpha, j})}}`. # # The Generalised Fourier Decomposition (GFD) purity is the length of a projection of a vector :math:`v \in V` onto one of these subspaces :math:`V_{\alpha, j}`, +# computed via the inner product with all basis vectors: # # .. math:: # \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. @@ -250,10 +251,115 @@ def g(x): # Fourier analysis of entanglement # -------------------------------- # -# [TODO] +# We now move to another resource, the entanglement between 2 qubits. We could simply use the Hilbert space :math:`H` of +# the two-qubit states as our vector space :math:`V`. However, the Fourier analysis is richer if we choose the space of +# density matrices :math:`\rho \in L(H)`, which is the space of bounded operators acting on quantum states in a Hilbert space :math:`H`, +# as :math:`V`. The density matrices get transformed by the adjoint action :math:`U \rho U^{\dagger}`. # +# Performing numerics like block-diagonalisation on such a vector space is a little more complicated, and best done by moving from +# matrices :math:`\rho` to "flattened" vectors in :math:`\mathbb{C}^{2n}`, and from adjoint unitary evolution to a superoperator +# which is a :math:`2n x 2n`-dimensional matrix that can be applied to the flattened density matrices. # +import functools + +def haar_unitary(N): + """ + Generates a Haar random NxN unitary matrix + """ + X = (np.random.randn(N, N) + 1j*np.random.randn(N, N)) / np.sqrt(2) + Q, R = np.linalg.qr(X) + phases = np.exp(-1j * np.angle(np.diag(R))) + return Q @ np.diag(phases) + +n = 2 +U = haar_unitary(2**n) +U_vec = np.kron(U.conj(), U) + +psi = np.random.rand(shape=(2**2,)) +rho = np.outer(psi, psi.conj()) +rho_vec = rho.flatten(order='F') +rho_out_vec = U_vec @ rho_vec +rho_out = np.reshape(rho_out_vec, shape=(2**2, 2**2)) + +# show that flattening works +print(np.allclose(rho_out, U @ rho @ U.conj().T )) + + +###################################################################### +# With the vector space fixed, we can now ask what unitaries keep entanglement free density matrices +# entanglement free? Of course, all unitaries that can be written as a tensor product of single-qubit unitaries! +# Such unitaries form a group called :math:`SU(2) x SU(2)`. We claim that they also form a representation of this group +# (the "defining" representation that just consists of the group elements themselves). If this claim is true, +# there must be a basis change into the "Fourier basis" that block-diagonalises all non-entangling unitaries into +# the same block structure. This can be easily checked using the superoperators constructed above! +# + +import itertools + +# single‑qubit Paulis +_pauli_map = { + 'I': np.array([[1,0],[0,1]], dtype=complex), + 'X': np.array([[0,1],[1,0]], dtype=complex), + 'Y': np.array([[0,-1j],[1j,0]],dtype=complex), + 'Z': np.array([[1,0],[0,-1]], dtype=complex), +} + +def pauli_basis(n): + """ + generates the basis of Pauli operators, and orders it by appearence in the isotypical decomp of Times_i SU(2) + """ + all_strs = [''.join(s) for s in itertools.product('IXYZ', repeat=n)] + sorted_strs = sorted(all_strs, key=lambda s: (n-s.count('I'), s)) + norm = np.sqrt(2**n) + mats = [] + for s in sorted_strs: + factors = [_pauli_map[ch] for ch in s] + M = functools.reduce(lambda A,B: np.kron(A,B), factors) + mats.append(M.reshape(-1)/norm) + B = np.column_stack(mats) + return sorted_strs, B + +def rotate_superoperator(U): + """ + Rotates the superop of the adj of a unitary U to the irrep-sorted Pauli basis + """ + S = np.kron(U.conj(), U) + n = int(np.log2(U.shape[0])) + basis,B = pauli_basis(n) + S_rot = B.conj().T @ S @ B + return basis, S_rot + +n = 2 +Us = [haar_unitary(2) for _ in range(n)] +# U is a tensor product of single-qubit unitaries +U = functools.reduce(lambda A, B: np.kron(A, B), Us) + +basis, S_rot = rotate_superoperator(U) +np.set_printoptions( + formatter={'float': lambda x: f"{x:5.2g}"}, + linewidth=200, # default is 75; + threshold=10000 # so it doesn’t summarize large arrays +) + +# now round and print +## NOTICE THAT IN PAULI BASIS THE UNITARY ADJOINT ACTION IS ORTHOGONAL +print("Adjoint Superoperator of U in the Irrep basis") +S_real = S_rot.real +Sr_round = np.round(S_real, 2) +print("Rounded real part (the operator is real):") +print(Sr_round) + +###################################################################### +# Given a new state :math:`rho`, we can compute the GDF purity by transforming it with the same basis transform. +# [TODO: compute result for a few different rhos without knowing the basis] +# + +###################################################################### +# With a bit of knowledge on representation theory, one might recognise that the subspaces +# are spanned by a basis of Pauli words. +# +# [TODO: compute same result with Pauli basis] # # References From 978cce5080a4107d5e27e01bff3ff9ea570990e2 Mon Sep 17 00:00:00 2001 From: Alan Martin Date: Tue, 5 Aug 2025 16:02:30 -0400 Subject: [PATCH 06/33] migrate tutorial_resourcefulness to v2 --- .../tutorial_resourcefulness/demo.py | 272 ++++++++++++++++++ .../tutorial_resourcefulness/metadata.json | 58 ++++ 2 files changed, 330 insertions(+) create mode 100644 demonstrations_v2/tutorial_resourcefulness/demo.py create mode 100644 demonstrations_v2/tutorial_resourcefulness/metadata.json diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py new file mode 100644 index 0000000000..2dea28ee96 --- /dev/null +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -0,0 +1,272 @@ +r""" +Analysing quantum resourcefulness with the generalized Fourier transform +======================================================================== + +.. figure:: ../_static/demo_thumbnails/opengraph_demo_thumbnails/OGthumbnail_resourcefulness.png + :align: center + :width: 70% + :alt: DESCRIPTION. + :target: javascript:void(0) + +Resource theories in quantum information theory ask how "complex" a given quantum state is with respect to a certain +measure of complexity. For example, using the resource of entanglement, we can ask how entangled a quantum state is. Other well-known +resources are Clifford stabilizerness, which measures how close a state is from being prepared by a circuit that only uses +classically simulatable Clifford gates, or Gaussianity, which measures how far away a state is from a so-called "Gaussian state" +that is relatively easy to prepare in quantum optics, and likewise classically simulatable. As the name "resourceful" suggests, +these measures of complexity often relate to how much "effort" states are, for example with respect to classical simulation or +preparation in the lab. + +It turns out that the resourcefulness of quantum states can be investigated with tools from generalised Fourier analysis [#Bermejo_Braccia]_. +Fourier analysis here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case +is not a function over :mathbb:`R` or :mathbb:`Z`, but a quantum state. "Generalised" indicates that we don't use the +standard Fourier transform, but its group-theoretic generalisations [LINK TO RELATED DEMOS]. This is important, because +[#Bermejo_Braccia]_ link a resource to a group -- essentially, by defining the set of unitaries that maps resource-free +states to resource-free states as a "representation" of a group, which gets block-diagonalised to find a generalised Fourier basis. +The intuition, however, is exactly the same as in the standard Fourier transform: large higher-order Fourier +coefficients indicate a less "smooth" or "resourceful" function. + +In this tutorial we will illustrate the idea of generalised Fourier analysis for resource theories with two simple examples. +First we will look at a standard Fourier decomposition of a function, but from the perspective of resources,in order to +introduce the basic idea in a familiar setting. Secondly, we will use the same concepts to analyse the entanglement + resource of quantum states, reproducing Figure 2 in [#Bermejo_Braccia]_. + +.. figure:: ../_static/demonstration_assets/resourcefulness/figure2_paper.png + :align: center + :width: 70% + :alt: Fourier coefficients, or projections into "irreducible subspaces", of different states using 2-qubit entanglement as a resource. + A Bell state, which is maximally entangled, has high contributions in higher-order Fourier coefficients, while + a tensor product state with little entanglement has contributions in lower-order Fourier coefficients. The interpolation + between the two extremes, exemplified by a Haar random state, has a Fourier spectrum in between. + +Luckily, in the case of entanglement as a resource, the generalised Fourier coefficients can be computed using Pauli expectations. +This saves us from diving too deep into representation theory. In fact, the tutorial should be informative without knowing much +about groups at all! But of course, even in this simple case the numerical analysis scales exponentially with the number of qubits +in general. It is a fascinating question in which situations the Fourier coefficients of a physical state could be read out +on a quantum computer, which is known to sometimes perform the block-diagonalisation efficiently. + + +Standard Fourier analysis through the lense of resources +-------------------------------------------------------- + +Let's start recalling the standard Fourier transform, and for simplicity we'll work with the +discrete version. Given N real values :math:`x_0,...,x_{N-1}`, that we can interpret [LINK TO OTHER DEMO] as the values +of a function :math:`f(0), ..., f(N-1)` over the integers :math:`x \in {0,...,N-1}`, the Fourier transform +computes the Fourier coefficients + +.. math:: + \hat{f}(k) = \frac{1}{\sqrt{N}\sum_{x=0}^{N-1} f(x) e^{\frac{2 \pi i}{N} k x}, k = 0,...,N-1 + +In words, the Fourier coefficients are projections of the function :math:`f` onto the "Fourier" basis functions +:math:`e^{\frac{2 \pi i}{N} k x}`. Note that we use a normalisation here that is consistent with a unitary transform that +we construct as a matrix below. + +Let's see this equation in action. +""" + +import matplotlib.pyplot as plt +import numpy as np + +N = 12 + +def f(x): + """Some function.""" + return 0.5*(x-4)**3 + +def f_hat(k): + """Fourier coefficients of f.""" + projection = [ f(x) * np.exp(-2 * np.pi * 1j * k * x / N)/np.sqrt(N) for x in range(N)] + return np.sum(projection) + +def plot(f, f_hat): + + fig, (ax1, ax2) = plt.subplots(2, 1) + ax1.bar(range(N), [np.real(f(x)) for x in range(N)], color='dimgray') # casting to real is needed in case we perform an inverse FT + ax1.set_title(f"function f") + + ax2.bar(np.array(range(N))+0.05, [np.imag(f_hat(x)) for x in range(N)], color='lightpink', label="imaginary part") + ax2.bar(range(N), [np.real(f_hat(k)) for k in range(N)], color='dimgray', label="real part") + ax2.set_title("Fourier coefficients") + plt.legend() + plt.tight_layout() + plt.show() + +plot(f, f_hat) + +###################################################################### +# Now, what kind of resource are we dealing with here? In other words, what +# measure of complexity gets higher when a function has large higher-order Fourier coefficients? +# +# Well, let us look for the function with the least resource or complexity! For this we can just +# work backwards: define a Fourier spectrum that only has a contribution in the lowest order coefficient, +# and apply the inverse Fourier transform to look at the function it corresponds to! +# +# + +def g_hat(k): + """The least complex Fourier spectrum possible.""" + if k==0: + return 1 + else: + return 0 + +def g(x): + """Function whose Fourier spectrum is `g_hat`.""" + projection = [g_hat(k) * np.exp(2 * np.pi * 1j * k * x / N)/np.sqrt(N) for k in range(N)] + return np.sum(projection) + +plot(g, g_hat) + +###################################################################### +# Well, the function is constant. This makes sense, because we know that the decay of the Fourier coefficient +# is related to the number of times a function is differentiable, which in turn is the technical definition of smoothness. A +# constant function is maximally smooth -- it does not wiggle at all. In other words, +# the resource of the standard Fourier transform is smoothness, and a resource-free function is constant! +# + +###################################################################### +# Linking resources to group representations +# ------------------------------------------ +# +# Fourier transforms are intimately linked to groups (in fact, the x-domain :math:`0,...,N-1` is strictly speaking a group, +# which you can learn more about [here]). Without expecting you to know group jargon, we have to establish a few concepts +# that generalise the above example to quantum states and more generic resources. The crucial idea is to +# define a resource by fixing a set of vectors (later, quantum states) that are considered resource-free. +# We also need to define unitary matrices that map resource-free vectors to resource-free vectors, +# and these matrices need to form a *unitary representation* of a group :math:`G`, +# which is a matrix-valued function :math:`R(g), g \in G` on the group. +# This is all we need to guarantee that the matrices can be simultaneously block-diagonalised, or written as a +# direct sum of smaller matrix-valued functions over the group, :math:`r^{\alpha}(g)`. Note that some of the blocks may be identical. +# If you know group theory, then you will recognise that the :math:`r^{\alpha}(g)` are the *irreducible representations* of the group. +# +# [TODO: image] +# +# +# What is important for us is that these smaller matrix-valued functions :math:`r^{\alpha}(g)` define a +# subspace :math:`V_{\alpha, j}`, where the index :math:`j` accounts for the fact that there may be several copies of +# an :math:`r^{\alpha}(g)` on the diagonal of :math:`R(g)`, each of which corresponds to one subspace. +# [TODO: BASIS :math:`w^{(i)}_{\alpha, j}`]. +# +# The Generalised Fourier Decomposition (GFD) purity is the length of a projection of a vector :math:`v \in V` onto one of these subspaces :math:`V_{\alpha, j}`, +# +# .. math:: +# \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. +# +# In our standard +# example above, the subspaces are one-dimensional spaces spanned by the Fourier basis functions :math:`\chi_k(x) = e^{\frac{2 \pi i}{N} k x}`. +# The vector space is the space of functions (if this is confusing, think of the function values as arranged in +# N-dimensional vectors :math:`\vec{f}` and :math:`\vec{\chi}_k`). +# The projection is executed by the sum :math:`\sum_x f(x) \chi_k(x) = \langle f, \chi_k`. In other words, the GFD purity +# is the absolute square of the Fourier coefficient, +# +#.. math:: +# \mathcal{P}(\vec{f}) = | \vec{\chi}_k, \vec{f} \rangle |^2 = |\hat{f}|^2. +# +# Generalising from the standard Fourier basis functions to irreducible +# representations allows us to generalise the Fourier transform to lots of other groups, and hence, resources. +# +# So far so good, but what is the representation :math:`R(g)` for the standard Fourier transform? Let's follow the recipe: +# +# 1. We first need to consider our function :math:`f(0), ..., f(N-1)` as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. +# 2. As argued above, the set of resource-free vectors correspond to constant functions, :math:`f(0) = ... = f(N-1)`. +# 3. We need a set of unitary matrices that does not change the "constantness" of the vector. This set is given by permutation matrices, +# which swap the entries of the vector but do not change any of them. +# 4. The *circulant* permutation matrices can be shown to form a unitary representation :math:`R(g)` for :math:`g \in G = Z_N`, called the *regular representation*. +# 5. We are now guaranteed that there is a basis change that diagonalises all matrices :math:`R(g)` together. +# (Note that the Fourier transform is sometimes defined as the basis change that block-diagonalises the regular representation!) As mentioned, this is +# a block-diagonalisation where the blocks happen to be 1-dimensional, as is the rule for so-called "Abelian" groups. +# 6. The values on the diagonal of :math:`R(g)` under this basis change are exactly the Fourier basis functions `:math:`e^{\frac{2 \pi i}{N} k x}`. +# +# Let's verify this! +# +# First, let us write our function and Fourier spectrum above as vectors: +# + +f_vec = np.array([f(x) for x in range(N)]) +f_hat_vec = np.array([f_hat(k) for k in range(N)]) + +###################################################################### +# The Fourier transform then becomes a matrix multiplication with the matrix: +# +# .. math:: +# F = \frac{1}{\sqrt{N}} \begin{bmatrix} +# 1&1&1&1&\cdots &1 \\ +# 1&\omega&\omega^2&\omega^3&\cdots&\omega^{N-1} \\ +# 1&\omega^2&\omega^4&\omega^6&\cdots&\omega^{2(N-1)}\\ 1&\omega^3&\omega^6&\omega^9&\cdots&\omega^{3(N-1)}\\ +# \vdots&\vdots&\vdots&\vdots&\ddots&\vdots\\ +# 1&\omega^{N-1}&\omega^{2(N-1)}&\omega^{3(N-1)}&\cdots&\omega^{(N-1)(N-1)} +# \end{bmatrix} +# +# Let's check that this is indeed the same as what we did above. +# + +# get the Fourier matrix from scipy +from scipy.linalg import dft +F = dft(N)/np.sqrt(N) + +# compare to what we previously computed +print(np.allclose(F @ f_vec, f_hat_vec)) + + +###################################################################### +# Above we claimed that this matrix F is the basis transform that diagonalises +# a permutation matrix (in other words, the unitary representation evaluated at some group element). +# + +# create a circulant permutation matrix +i = np.random.randint(0, N) +first_row = np.zeros(N) +first_row[i] = 1 + +# initialize the matrix with the first row +P = np.zeros((N, N)) +P[0, :] = first_row + +# generate subsequent rows by cyclically shifting the previous row +for i in range(1, N): + P[i, :] = np.roll(P[i-1, :], 1) + + +# change into the Fourier basis using F +np.set_printoptions(precision=2, suppress=True) +P_F = F @ P @ np.linalg.inv(F) + +# check if the resulting matrix is diagonal +# trick: remove diagonal and make sure the remainder is zero +print(np.allclose(P_F - np.diag(np.diagonal(P_F)), np.zeros((N,N)) )) + + +###################################################################### +# To recap, we saw that the standard Fourier analysis can be generalised by +# interpreting "smoothness" or "constantness" as a resource, linking it to a vector space and a group +# representation and (block-)diagonalising the representation. The blocks, here one-dimensional, +# form a basis for subspaces. The GFD purities suggested in [#Bermejo_Braccia]_ as a resource fingerprint +# are just projections of some vector (here, a function) onto the basis vectors and taking the absolute square. +# +# Armed with this recipe, we can now try to analyse entanglement as a resource, +# and density matrices describing quantum states as vectors in a vector space :math:`L(H)`. +# + +###################################################################### +# Fourier analysis of entanglement +# -------------------------------- +# +# [TODO] +# +# + + +# +# References +# ---------- +# +# .. [#Bermejo_Braccia] +# +# Bermejo, Pablo, Paolo Braccia, Antonio Anna Mele, Nahuel L. Diaz, Andrew E. Deneris, Martin Larocca, and M. Cerezo. "Characterizing quantum resourcefulness via group-Fourier decompositions." arXiv preprint arXiv:2506.19696 (2025). +# + + +###################################################################### +# +# About the author +# ---------------- +# diff --git a/demonstrations_v2/tutorial_resourcefulness/metadata.json b/demonstrations_v2/tutorial_resourcefulness/metadata.json new file mode 100644 index 0000000000..1eaa8fb6fe --- /dev/null +++ b/demonstrations_v2/tutorial_resourcefulness/metadata.json @@ -0,0 +1,58 @@ +{ + "title": "Resourcefulness of quantum states with Fourier analysis", + "authors": [ + { + "username": "mariaschuld" + }, + { + "username": "paolobraccia" + }, + { + "username": "pablobermejo" + } + ], + "executable_stable": true, + "executable_latest": true, + "dateOfPublication": "2025-08-05T09:00:00+00:00", + "dateOfLastModification": "2025-08-05T09:00:00+00:00", + "categories": [ + "Quantum Computing", "Quantum Information Theory", "Fourier analysis" + ], + "tags": [], + "previewImages": [ + { + "type": "thumbnail", + "uri": "/_static/demo_thumbnails/regular_demo_thumbnails/thumbnail_resourcefulness.png" + }, + { + "type": "large_thumbnail", + "uri": "/_static/demo_thumbnails/large_demo_thumbnails/thumbnail_resourcefulness.png" + } + ], + "seoDescription": "Find out how much quantum resource a state has by looking at its generalised Fourier coefficients.", + "doi": "", + "references": [ + { + "id": "Bermejo_Braccia", + "type": "preprint", + "title": "Characterizing quantum resourcefulness via group-Fourier decompositions", + "authors": "Pablo Bermejo, Paolo Braccia, Antonio Anna Mele, N. L. Diaz, Andrew E. Deneris, Martín Larocca, Marco Cerezo", + "year": "2025", + "url": "https://arxiv.org/pdf/2506.19696" + } + ], + "basedOnPapers": ["10.48550/arXiv.2506.19696"], + "referencedByPapers": [], + "relatedContent": [ + { + "type": "demonstration", + "id": "tutorial_resourcefulness", + "weight": 1.0 + }, + { + "type": "demonstration", + "id": "tutorial_resourcefulness_with_fourier_transform", + "weight": 1.0 + } + ] +} From d91f81866f5cc05a8d6c0ec93be8da0016b8f180 Mon Sep 17 00:00:00 2001 From: Alan Martin Date: Tue, 5 Aug 2025 16:07:43 -0400 Subject: [PATCH 07/33] temp rm unverified categories --- demonstrations_v2/tutorial_resourcefulness/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/metadata.json b/demonstrations_v2/tutorial_resourcefulness/metadata.json index 1eaa8fb6fe..1e9d2306bb 100644 --- a/demonstrations_v2/tutorial_resourcefulness/metadata.json +++ b/demonstrations_v2/tutorial_resourcefulness/metadata.json @@ -16,7 +16,7 @@ "dateOfPublication": "2025-08-05T09:00:00+00:00", "dateOfLastModification": "2025-08-05T09:00:00+00:00", "categories": [ - "Quantum Computing", "Quantum Information Theory", "Fourier analysis" + "Quantum Computing" ], "tags": [], "previewImages": [ From e212321a1e0290f57a25992390dadbb8c6768329 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 6 Aug 2025 08:44:13 +0200 Subject: [PATCH 08/33] backup --- demonstrations/tutorial_resourcefulness.py | 285 ++++++++++++++++----- 1 file changed, 220 insertions(+), 65 deletions(-) diff --git a/demonstrations/tutorial_resourcefulness.py b/demonstrations/tutorial_resourcefulness.py index 731f1007e5..b718132855 100644 --- a/demonstrations/tutorial_resourcefulness.py +++ b/demonstrations/tutorial_resourcefulness.py @@ -167,15 +167,17 @@ def g(x): # # So far so good, but what is the representation :math:`R(g)` for the standard Fourier transform? Let's follow the recipe: # -# 1. We first need to consider our function :math:`f(0), ..., f(N-1)` as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. -# 2. As argued above, the set of resource-free vectors correspond to constant functions, :math:`f(0) = ... = f(N-1)`. -# 3. We need a set of unitary matrices that does not change the "constantness" of the vector. This set is given by permutation matrices, +# 1. **Vectorise**. We first need to consider our function :math:`f(0), ..., f(N-1)` as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. +# 2. **Identify free vectors**. As argued above, the set of resource-free vectors correspond to constant functions, :math:`f(0) = ... = f(N-1)`. +# 3. **Identify free linear transformations**. We need a set of unitary matrices that does not change the "constantness" of the vector. This set is given by permutation matrices, # which swap the entries of the vector but do not change any of them. -# 4. The *circulant* permutation matrices can be shown to form a unitary representation :math:`R(g)` for :math:`g \in G = Z_N`, called the *regular representation*. -# 5. We are now guaranteed that there is a basis change that diagonalises all matrices :math:`R(g)` together. +# 4. **Ensure they form a group representation**. The *circulant* permutation matrices can be shown to form a unitary representation :math:`R(g)` for :math:`g \in G = Z_N`, called the *regular representation*. +# We are now guaranteed that there is a basis change that diagonalises all matrices :math:`R(g)` together. # (Note that the Fourier transform is sometimes defined as the basis change that block-diagonalises the regular representation!) As mentioned, this is # a block-diagonalisation where the blocks happen to be 1-dimensional, as is the rule for so-called "Abelian" groups. -# 6. The values on the diagonal of :math:`R(g)` under this basis change are exactly the Fourier basis functions `:math:`e^{\frac{2 \pi i}{N} k x}`. +# 5. **Identify basis for invariant subspaces**. The values on the diagonal of :math:`R(g)` under this basis +# change are exactly the Fourier basis functions `:math:`e^{\frac{2 \pi i}{N} k x}`. +# 6. **GFD purities**. Compute the projections into subspaces, which are the GFD purities. # # Let's verify this! # @@ -251,50 +253,215 @@ def g(x): # Fourier analysis of entanglement # -------------------------------- # -# We now move to another resource, the entanglement between 2 qubits. We could simply use the Hilbert space :math:`H` of -# the two-qubit states as our vector space :math:`V`. However, the Fourier analysis is richer if we choose the space of -# density matrices :math:`\rho \in L(H)`, which is the space of bounded operators acting on quantum states in a Hilbert space :math:`H`, -# as :math:`V`. The density matrices get transformed by the adjoint action :math:`U \rho U^{\dagger}`. +# Now that we have a grasp of how standard Fourier transforms can measure the resource of "smoothness" of a classical function, +# let's apply the same kind of reasoning to the most popular resource of quantum states: multipartite entanglement. +# We can think of multipartite entanglement as a resource of the state of a quantum system that measures how "wiggly" +# the correlations between its constituents are. +# While this is a general statement, we will restrict our attention to systems made of our favourite quantum objects: qubits. +# Just like smoother functions have simpler Fourier spectra (mostly low-momentum components), +# quantum states with simpler entanglement structures, or no entanglement at all like in the case of product states, +# have simpler generalized Fourier spectra, where most of their purity resides in the lower-order "irreps" +# (that we recall is short for irreducible representations, but you can think of them as generalized frequencies). +# On the flip side, highly entangled states spread their purity across more and higher-order (i.e., larger dimensional) irreps, +# similar to how more complex ("wiggly") functions have larger Fourier coefficients at higher frequencies. +# This means that analyzing a state's Fourier spectrum can tell us how "resourceful" or entangled it is. +# +# Let us walk the same steps we took when studying the resource of "smoothness". +# 1. **Vectorise**. Lucklily for us, our objects of interest, the quantum states :math:`\psi \in \mathbb{C}^{2n}`, are already in the form of vectors. +# 2. **Identify free vectors**. It's easy to define the set of free states for multipartite entanglement: they are just product states. +# 3. **Identify free linear transformations**. Now, what unitary transformation of a quantum state does not generate +# entanglement? You guessed it right, local evolutions :math:`U=\Bigotimes U_j` for :math:`U_j \in SU(2)`. +# 4. **Ensure they form a representation**. The set of free operations corresponds to the group :math:`G = SU(2) x SU(2) ... x SU(2)`. +# Unitary matrices :math:`U=\Bigotimes U_j` define the standard representation of :math:`G` over :math:`H`, transforming a +# quantum state :math:`\psi` as per usual: :math:`\psi' = U psi`. +# Again, this implies that we can find a basis of the Hilbert space, where any :math:`U` is simulatenously block-diagonal. +# +# Let's try to block-diagonalise one of the non-entangling unitaries! +# + +#[Show failed attempt to block diagonalise one of them] + +###################################################################### +# What is happening here? It turns out that the non-entangling unitary matrices only have a single block of size :math:`2^n \times 2^n`. +# Technically speaking, the representation :math:`R(g) = U(g)` is irreducible over :math:`H`. +# As a consequence, the invariant subspace is :math:`H` itself, and the GFD purity is simply the purity of the state :math:`psi`, which is :math:`1`. +# This, of course, is not a very informative measure! # -# Performing numerics like block-diagonalisation on such a vector space is a little more complicated, and best done by moving from -# matrices :math:`\rho` to "flattened" vectors in :math:`\mathbb{C}^{2n}`, and from adjoint unitary evolution to a superoperator -# which is a :math:`2n x 2n`-dimensional matrix that can be applied to the flattened density matrices. # - -import functools - -def haar_unitary(N): - """ - Generates a Haar random NxN unitary matrix - """ - X = (np.random.randn(N, N) + 1j*np.random.randn(N, N)) / np.sqrt(2) - Q, R = np.linalg.qr(X) - phases = np.exp(-1j * np.angle(np.diag(R))) - return Q @ np.diag(phases) +# However, rather than a bug, this is a feature of the GFD framework. Indeed, nobody forces us to restrict our attention to :math:`H` and to +# the (standard) representation of non-entangling unitary matrices. +# Afterall, what's the first symptom of entanglement in a quantum state? You guessed right again, the reduced density matrix of some subsystem is mixed! +# Wouldn't it make more sense then to study the multipartite entanglement form the point of view of density matrices? +# It turns out that moving into the space :math:`B(H)` of bounded linear operators in which the density matrices live leads to +# a much more nuanced Fourier spectrum. +# +# Let us walk the steps above again +# 1. **Vectorise**. :math:`L(H)` is a vector space in the technical sense, but one of matrices. We can make this more +# explicit and "vectorize" density matrices :math:`rho=\sum_i,j c_i,j |i\rangle \langle j|` +# as :math:`|rho\rangle \rangle = \sum_i,j c_i,j |i\rangle |j\rangle \in H \otimes H^*`. +# For example: +# n = 2 -U = haar_unitary(2**n) -U_vec = np.kron(U.conj(), U) -psi = np.random.rand(shape=(2**2,)) +# create a random quantum state +psi = np.random.rand(2**n) +# construct the corresponding density matrix rho = np.outer(psi, psi.conj()) +# vectorise it rho_vec = rho.flatten(order='F') -rho_out_vec = U_vec @ rho_vec -rho_out = np.reshape(rho_out_vec, shape=(2**2, 2**2)) -# show that flattening works +###################################################################### +# 2. **Identify free vectors**. The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` +# where each :math:`\rho_j` is a single-qubit state. +# 3. **Identify free linear transformations**. The free operations are still given by non-entangling unitaries, but +# of course, they act differently on density matrices :math:`\rho' = U \rho U^{\dagger}`. +# We can also vectorise this by defining the matrix :math:`\tilde{U}(g) = U \otimes U^*`. We then have that +# :math:`|\rho'\rangle \rangle = \tilde{U} \rho`. +# To demonstrate: +# + +from scipy.stats import unitary_group +from functools import reduce + +# create n haar random single-qubit unitaries +Us = [unitary_group.rvs(dim=2) for _ in range(n)] +# compose them into a non-entangling n-qubit unitary +U = reduce(lambda A, B: np.kron(A, B), Us) +# Vectorise U +U_vec = np.kron(U.conj(), U) + +# evolve the state above by U, using the vectorised objects +rho_out_vec = U_vec @ rho_vec +# reshape the result back into a density matrix +rho_out = np.reshape(rho_out_vec, newshape=(2**2, 2**2)) +# this is the same as the usual adjoint application of U print(np.allclose(rho_out, U @ rho @ U.conj().T )) +###################################################################### +# 4. **Ensure they form a representation**. This "adjoint action" is indeed a valid representation :math:`\tilde{R}(g)` of +# :math:`G = SU(2) x SU(2) ... x SU(2)`, called the *defining representation*. However, it is a different one from before, +# and this time there is a basis transformation that block-diagonalises all matrices in the representation. +# This can be done by computing the eigendecomposition of an arbitrary linear combination of a set of matrices in the representation. +# + +matrices = [] +for i in range(10): + # create n haar random single-qubit unitaries + Us = [unitary_group.rvs(dim=2) for _ in range(n)] + # compose them into a non-entangling n-qubit unitary + U = reduce(lambda A, B: np.kron(A, B), Us) + # Vectorise U + U_vec = np.kron(U.conj(), U) + matrices.append(U_vec) + +# Create a random linear combination of the matrices +alphas = np.random.randn(len(matrices)) + 1j * np.random.randn(len(matrices)) +M_combo = sum(a * M for a, M in zip(alphas, matrices)) + +# Eigendecompose the linear combination +vals, Q = np.linalg.eig(M_combo) ###################################################################### -# With the vector space fixed, we can now ask what unitaries keep entanglement free density matrices -# entanglement free? Of course, all unitaries that can be written as a tensor product of single-qubit unitaries! -# Such unitaries form a group called :math:`SU(2) x SU(2)`. We claim that they also form a representation of this group -# (the "defining" representation that just consists of the group elements themselves). If this claim is true, -# there must be a basis change into the "Fourier basis" that block-diagonalises all non-entangling unitaries into -# the same block structure. This can be easily checked using the superoperators constructed above! +# Let's test this basis change # +Qinv = np.linalg.inv(Q) +B = Qinv @ matrices[0] @ Q +print(B) + +###################################################################### +# B does not look block diagonal. What happened here? +# Well, it is block-diagonal, but we have to reorder the columns and rows of the final matrix. +# This takes a bit of pain, encapsulated in the following function: +# + +from collections import OrderedDict + +def group_rows_cols_by_sparsity(B, tol=0): + """ + Given a binary or general matrix C, this function: + 1. Groups identical rows and columns. + 2. Orders these groups by sparsity (most zeros first). + 3. Returns the permuted matrix C2, and the row & column permutation + matrices P_row, P_col such that C2 = P_row @ C @ P_col. + + Parameters + ---------- + B : ndarray, shape (n, m) + Input matrix. + + Returns + ------- + P_row : ndarray, shape (n, n) + Row permutation matrix. + P_col : ndarray, shape (m, m) + Column permutation matrix. + """ + # Compute boolean mask where |B| >= tol + mask = np.abs(B) >= 1e-8 + # Convert boolean mask to integer (False→0, True→1) + C = mask.astype(int) + # order by sparsity + + n, m = C.shape + + # Helper to get a key tuple and zero count for a vector + def key_and_zeros(vec): + if tol > 0: + bin_vec = (np.abs(vec) < tol).astype(int) + key = tuple(bin_vec) + zero_count = int(np.sum(bin_vec)) + else: + key = tuple(vec.tolist()) + zero_count = int(np.sum(np.array(vec) == 0)) + return key, zero_count + + # Group rows by key + row_groups = OrderedDict() + row_zero_counts = {} + for i in range(n): + key, zc = key_and_zeros(C[i, :]) + row_groups.setdefault(key, []).append(i) + row_zero_counts[key] = zc + + # Sort row groups by zero_count descending + sorted_row_keys = sorted(row_groups.keys(), + key=lambda k: row_zero_counts[k], + reverse=True) + # Flatten row permutation + row_perm = [i for key in sorted_row_keys for i in row_groups[key]] + + # Group columns by key + col_groups = OrderedDict() + col_zero_counts = {} + for j in range(m): + key, zc = key_and_zeros(C[:, j]) + col_groups.setdefault(key, []).append(j) + col_zero_counts[key] = zc + + # Sort column groups by zero_count descending + sorted_col_keys = sorted(col_groups.keys(), + key=lambda k: col_zero_counts[k], + reverse=True) + col_perm = [j for key in sorted_col_keys for j in col_groups[key]] + + # Build permutation matrices + P_row = np.eye(n)[row_perm, :] + P_col = np.eye(m)[:, col_perm] + + return P_row, P_col + +P_row, P_col = group_rows_cols_by_sparsity(B) +Q = Q @ P_col + + + +# We cheat here a little: we already know the basis in which :math:`\tilde{U}` is block-diagonal: the Pauli basis. +# This allows us to construct the matrix implementing the basis change directly. +# + +import functools import itertools # single‑qubit Paulis @@ -307,7 +474,7 @@ def haar_unitary(N): def pauli_basis(n): """ - generates the basis of Pauli operators, and orders it by appearence in the isotypical decomp of Times_i SU(2) + Generates the basis of Pauli operators, and orders it by appearence in the isotypical decomp of Times_i SU(2). """ all_strs = [''.join(s) for s in itertools.product('IXYZ', repeat=n)] sorted_strs = sorted(all_strs, key=lambda s: (n-s.count('I'), s)) @@ -320,36 +487,24 @@ def pauli_basis(n): B = np.column_stack(mats) return sorted_strs, B -def rotate_superoperator(U): - """ - Rotates the superop of the adj of a unitary U to the irrep-sorted Pauli basis - """ - S = np.kron(U.conj(), U) - n = int(np.log2(U.shape[0])) - basis,B = pauli_basis(n) - S_rot = B.conj().T @ S @ B - return basis, S_rot -n = 2 -Us = [haar_unitary(2) for _ in range(n)] -# U is a tensor product of single-qubit unitaries -U = functools.reduce(lambda A, B: np.kron(A, B), Us) - -basis, S_rot = rotate_superoperator(U) -np.set_printoptions( - formatter={'float': lambda x: f"{x:5.2g}"}, - linewidth=200, # default is 75; - threshold=10000 # so it doesn’t summarize large arrays -) - -# now round and print -## NOTICE THAT IN PAULI BASIS THE UNITARY ADJOINT ACTION IS ORTHOGONAL -print("Adjoint Superoperator of U in the Irrep basis") -S_real = S_rot.real -Sr_round = np.round(S_real, 2) -print("Rounded real part (the operator is real):") -print(Sr_round) + +# compute the basis change matrix +basis, B = pauli_basis(n) +# apply basis change +U_vec_rot = B.conj().T @ U_vec @ B + +# make sure the imaginary part is zero, so we don't have to print it +# (this is a property of the Pauli basis) +assert np.isclose(np.sum(U_vec_rot.imag), 0) + +# print the real part +print(np.round(U_vec_rot.real, 2)) + + +# 5. **Identify basis for invariant subspaces**. Will we now find interesting subspaces by simultaneously block-diagonalizing :math:`\tilde{R}`? +# ###################################################################### # Given a new state :math:`rho`, we can compute the GDF purity by transforming it with the same basis transform. # [TODO: compute result for a few different rhos without knowing the basis] From e68546f2fbbf9c58b9d3885e6484bac6164b6df8 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 6 Aug 2025 11:47:22 +0200 Subject: [PATCH 09/33] backup --- .../tutorial_resourcefulness/demo.py | 279 +++++++++++++++++- 1 file changed, 278 insertions(+), 1 deletion(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 2dea28ee96..124725b069 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -250,8 +250,285 @@ def g(x): # Fourier analysis of entanglement # -------------------------------- # -# [TODO] +# Now that we have a grasp of how standard Fourier transforms can measure the resource of "smoothness" of a classical function, +# let's apply the same kind of reasoning to the most popular resource of quantum states: multipartite entanglement. +# We can think of multipartite entanglement as a resource of the state of a quantum system that measures how "wiggly" +# the correlations between its constituents are. +# While this is a general statement, we will restrict our attention to systems made of our favourite quantum objects, qubits. +# Just like smoother functions have simpler Fourier spectra (mostly low-momentum components), +# quantum states with simpler entanglement structures, or no entanglement at all like in the case of product states, +# have simpler generalized Fourier spectra, where most of their purity resides in the lower-order "irreps" +# (that we recall is short for irreducible representations, but you can think of them as generalized frequencies). +# On the flip side, highly entangled states spread their purity across more and higher-order (i.e., larger dimensional) irreps, +# similar to how more complex ("wiggly") functions have larger Fourier coefficients at higher frequencies. +# This means that analyzing a state's Fourier spectrum can tell us how "resourceful" or entangled it is. +# +# Let us walk the same steps we took when studying the resource of "smoothness". +# 1. **Vectorise**. Luckily for us, our objects of interest, the quantum states :math:`\psi \in \mathbb{C}^{2n}`, are already in the form of vectors. +# 2. **Identify free vectors**. It's easy to define the set of free states for multipartite entanglement: tensor products of single-qubit quantum states. +# 3. **Identify free linear transformations**. Now, what unitary transformation of a quantum state does not generate +# entanglement? You guessed it right, "non-entangling" circuits that consist only of single-qubit gates :math:`U=\Bigotimes U_j` for :math:`U_j \in SU(2)`. +# 4. **Ensure they form a representation**. It turns out that non-entangling unitaries are the standard representation +# of the group :math:`G = SU(2) x SU(2) ... x SU(2)` for the vector space :math:`H`, the Hilbert space of the state vectors. +# Again, this implies that we can find a basis of the Hilbert space, where any :math:`U` is simultaneously block-diagonal. +# +# Let's stop here for now, and try to block-diagonalise one of the non-entangling unitaries. We can use an +# eigenvalue decomposition for this. +# + +from scipy.stats import unitary_group +from functools import reduce + +n = 2 + +# create n haar random single-qubit unitaries +Us = [unitary_group.rvs(dim=2) for _ in range(n)] +# compose them into a non-entangling n-qubit unitary +U = reduce(lambda A, B: np.kron(A, B), Us) + +# Block-diagonalise +vals, U_bdiag = np.linalg.eig(U) + +print(U_bdiag) + +###################################################################### +# Wait, this is not a block-diagonal matrix, even if we'd shuffle the rows and columns. What is happening here? +# It turns out that the non-entangling unitary matrices only have a single block of size :math:`2^n \times 2^n`. +# Technically speaking, the representation :math:`R(g) = U(g)` is irreducible over :math:`H`. +# As a consequence, the invariant subspace is :math:`H` itself, and the GFD purity is simply the purity of the state :math:`psi`, which is :math:`1`. +# This, of course, is not a very informative measure! +# +# +# However, rather than a bug, this is a feature of the GFD framework. Indeed, nobody forces us to restrict our attention to :math:`H` and to +# the (standard) representation of non-entangling unitary matrices. +# After all, what's the first symptom of entanglement in a quantum state? You guessed right again, the reduced density matrix of some subsystem is mixed! +# Wouldn't it make more sense then to study the multipartite entanglement form the point of view of density matrices? +# It turns out that moving into the space :math:`B(H)` of bounded linear operators in which the density matrices live leads to +# a much more nuanced Fourier spectrum. +# +# Let us walk the steps above again +# 1. **Vectorise**. :math:`L(H)` is a vector space in the technical sense, but one of matrices. To use the linear algebra +# tricks from before we have to "vectorize" density matrices :math:`rho=\sum_i,j c_i,j |i\rangle \langle j|` +# into vectors :math:`|rho\rangle \rangle = \sum_i,j c_i,j |i\rangle |j\rangle \in H \otimes H^*`. +# For example: +# + +n = 2 + +# create a random quantum state +psi = np.random.rand(2**n) +# construct the corresponding density matrix +rho = np.outer(psi, psi.conj()) +# vectorise it +rho_vec = rho.flatten(order='F') + +###################################################################### +# 2. **Identify free vectors**. The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` +# where each :math:`\rho_j` is a single-qubit state. We only have to write them in flattened form. +# 3. **Identify free linear transformations**. The free operations are still given by non-entangling unitaries, but +# of course, they act on density matrices via :math:`\rho' = U \rho U^{\dagger}`. +# We can also vectorise this operation by defining the matrix :math:`U_{\mathrm{vec}}(g) = U \otimes U^*`. +# We then have that :math:`|\rho'\rangle \rangle = U_{\mathrm{vec}} \rho`. +# To demonstrate: # + + +# Vectorise U +U_vec = np.kron(U.conj(), U) + +# evolve the state above by U, using the vectorised objects +rho_out_vec = U_vec @ rho_vec +# reshape the result back into a density matrix +rho_out = np.reshape(rho_out_vec, newshape=(2**2, 2**2)) +# this is the same as the usual adjoint application of U +print(np.allclose(rho_out, U @ rho @ U.conj().T )) + +###################################################################### +# 4. **Ensure they form a representation**. This "adjoint action" is indeed a valid representation of +# :math:`G = SU(2) x SU(2) ... x SU(2)`, called the *defining representation*. However, it is a different one from before, +# and this time there is a basis transformation that properly block-diagonalises all matrices in the representation. +# To find this transformation we compute the eigendecomposition of an arbitrary linear combination of a set of matrices in the representation. +# + +U_vecs = [] +for i in range(10): + # create n haar random single-qubit unitaries + Ujs = [unitary_group.rvs(dim=2) for _ in range(n)] + # compose them into a non-entangling n-qubit unitary + U = reduce(lambda A, B: np.kron(A, B), Ujs) + # Vectorise U + U_vec = np.kron(U.conj(), U) + U_vecs.append(U_vec) + +# Create a random linear combination of the matrices +alphas = np.random.randn(len(U_vecs)) + 1j * np.random.randn(len(U_vecs)) +M_combo = sum(a * M for a, M in zip(alphas, U_vecs)) + +# Eigendecompose the linear combination +vals, Q = np.linalg.eig(M_combo) + +###################################################################### +# Let's test this basis change +# + +Qinv = np.linalg.inv(Q) +# take one of the vectorised unitaries +U_vec = U_vecs[0] +U_vec_diag = Qinv @ U_vec @ Q + +np.set_printoptions( + formatter={'float': lambda x: f"{x:5.2g}"}, + linewidth=200, # default is 75; increase to fit your array + threshold=10000 # so it doesn’t summarize large arrays +) + +print(U_vec_diag) + +###################################################################### +# But `U0_diag` does not look block diagonal. What happened here? +# Well, it is block-diagonal, but we have to reorder the columns and rows of the final matrix to make this visible. +# This takes a bit of pain, encapsulated in the following function: +# + +from collections import OrderedDict + +def group_rows_cols_by_sparsity(B, tol=0): + """ + Given a matrix B, this function: + 1. Groups identical rows and columns. + 2. Orders these groups by sparsity (most zeros first). + 3. Returns the permuted matrix C2, and the row & column permutation + matrices P_row, P_col such that C2 = P_row @ C @ P_col. + + Parameters + ---------- + B : ndarray, shape (n, m) + Input matrix. + + Returns + ------- + P_row : ndarray, shape (n, n) + Row permutation matrix. + P_col : ndarray, shape (m, m) + Column permutation matrix. + """ + # Compute boolean mask where |B| >= tol + mask = np.abs(B) >= 1e-8 + # Convert boolean mask to integer (False→0, True→1) + C = mask.astype(int) + n, m = C.shape + + # Helper to get a key tuple and zero count for a vector + def key_and_zeros(vec): + if tol > 0: + bin_vec = (np.abs(vec) < tol).astype(int) + key = tuple(bin_vec) + zero_count = int(np.sum(bin_vec)) + else: + key = tuple(vec.tolist()) + zero_count = int(np.sum(np.array(vec) == 0)) + return key, zero_count + + # Group rows by key + row_groups = OrderedDict() + row_zero_counts = {} + for i in range(n): + key, zc = key_and_zeros(C[i, :]) + row_groups.setdefault(key, []).append(i) + row_zero_counts[key] = zc + + # Sort row groups by zero_count descending + sorted_row_keys = sorted(row_groups.keys(), + key=lambda k: row_zero_counts[k], + reverse=True) + # Flatten row permutation + row_perm = [i for key in sorted_row_keys for i in row_groups[key]] + + # Group columns by key + col_groups = OrderedDict() + col_zero_counts = {} + for j in range(m): + key, zc = key_and_zeros(C[:, j]) + col_groups.setdefault(key, []).append(j) + col_zero_counts[key] = zc + + # Sort column groups by zero_count descending + sorted_col_keys = sorted(col_groups.keys(), + key=lambda k: col_zero_counts[k], + reverse=True) + col_perm = [j for key in sorted_col_keys for j in col_groups[key]] + + # Build permutation matrices + P_row = np.eye(n)[row_perm, :] + P_col = np.eye(m)[:, col_perm] + + return P_row, P_col + +P_row, P_col = group_rows_cols_by_sparsity(U_vec_diag) + +Q = Q @ P_col +Qinv = P_row @ Qinv + +U_vec_diag = Qinv @ U_vec @ Q + +print(U_vec_diag) + +###################################################################### +# The reordering made the block structure visible. You can check that any vectorised non-entangling matrix `U_vec` +# has the same block structure if we change the basis via `Qinv @ U_vec @ Q`. +# +# But what basis have we actually changed into? It turns out that `Q` changes into the Pauli basis! +# + +Qinv @ U_vec @ Q + +# We cheat here a little: we already know the basis in which :math:`\tilde{U}` is block-diagonal: the Pauli basis. +# This allows us to construct the matrix implementing the basis change directly. +# + +import functools +import itertools + +# single‑qubit Paulis +_pauli_map = { + 'I': np.array([[1,0],[0,1]], dtype=complex), + 'X': np.array([[0,1],[1,0]], dtype=complex), + 'Y': np.array([[0,-1j],[1j,0]],dtype=complex), + 'Z': np.array([[1,0],[0,-1]], dtype=complex), +} + +def pauli_basis(n): + """ + Generates the basis of Pauli operators, and orders it by appearence in the isotypical decomp of Times_i SU(2). + """ + all_strs = [''.join(s) for s in itertools.product('IXYZ', repeat=n)] + sorted_strs = sorted(all_strs, key=lambda s: (n-s.count('I'), s)) + norm = np.sqrt(2**n) + mats = [] + for s in sorted_strs: + factors = [_pauli_map[ch] for ch in s] + M = functools.reduce(lambda A,B: np.kron(A,B), factors) + mats.append(M.reshape(-1)/norm) + B = np.column_stack(mats) + return sorted_strs, B + + + + +# compute the basis change matrix +basis, B = pauli_basis(n) +# apply basis change +U_vec_rot = B.conj().T @ U_vec @ B + +# make sure the imaginary part is zero, so we don't have to print it +# (this is a property of the Pauli basis) +assert np.isclose(np.sum(U_vec_rot.imag), 0) + +# print the real part +print(np.round(U_vec_rot.real, 2)) + + +# 5. **Identify basis for invariant subspaces**. Will we now find interesting subspaces by simultaneously block-diagonalizing :math:`\tilde{R}`? # From c80c69d2ce5630e0a9b831f7b98b220e9d8aa53a Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 6 Aug 2025 14:59:08 +0200 Subject: [PATCH 10/33] backup --- ...torial_resourcefulness_multipartite_gfd.py | 5 +- .../tutorial_resourcefulness/demo.py | 233 +++++++++++++++++- 2 files changed, 226 insertions(+), 12 deletions(-) diff --git a/demonstrations/tutorial_resourcefulness_multipartite_gfd.py b/demonstrations/tutorial_resourcefulness_multipartite_gfd.py index c0bfad935c..86a32b54ab 100644 --- a/demonstrations/tutorial_resourcefulness_multipartite_gfd.py +++ b/demonstrations/tutorial_resourcefulness_multipartite_gfd.py @@ -71,7 +71,6 @@ def compute_me_purities(op): return purities / (2**n) - ##################################################################### # functions to generate relevant quantum states @@ -133,6 +132,7 @@ def plot_purities(states, labels): ax2.legend(loc='upper left') plt.tight_layout() + plt.savefig("/home/maria/Desktop/purities_truth.png") plt.show() @@ -145,6 +145,9 @@ def plot_purities(states, labels): w_state(n), haar_state(n) ] +print(states[0]) +states = [np.outer(state.conj(), state) for state in states] +print(states[0]) labels = [ "Product", diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 124725b069..671883426d 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -259,6 +259,53 @@ def g(x): # quantum states with simpler entanglement structures, or no entanglement at all like in the case of product states, # have simpler generalized Fourier spectra, where most of their purity resides in the lower-order "irreps" # (that we recall is short for irreducible representations, but you can think of them as generalized frequencies). +# Here are a few examples that we will use below. +# + +import math +import functools + +def ghz_state(n: int): + """The |GHZ_n⟩ state vector for *n* qubits is famous for having maximal entanglement.""" + psi = np.zeros(2 ** n) + psi[0] = 1 / math.sqrt(2) + psi[-1] = 1 / math.sqrt(2) + return psi + + +def w_state(n: int): + """The |W_n⟩ state vector for *n* qubits ....""" + psi = np.zeros(2 ** n) + for q in range(n): + psi[2 ** q] = 1 / math.sqrt(n) + return psi + + +def haar_state(n: int): + """A Haar random state vector for *n* qubits is likely to have an intermediate amount of entanglement.""" + N = 2 ** n + # i.i.d. complex Gaussians + X = (np.random.randn(N, 1) + 1j * np.random.randn(N, 1)) / np.sqrt(2) + # QR on the N×1 “matrix” is just Gram–Schmidt → returns Q (N×1) and R (1×1) + Q, R = np.linalg.qr(X, mode='reduced') + # fix the overall phase so it’s uniformly distributed + phase = np.exp(-1j * np.angle(R[0, 0])) + return (Q[:, 0] * phase) + + +def haar_product_state(n: int): + """A Haar random tensor product state of *n* qubits is maximally unentangled.""" + states = [haar_state(1) for _ in range(n)] + return functools.reduce(lambda A, B: np.kron(A, B), states) + + +n = 2 +states = [ghz_state(n), w_state(n), haar_state(n), haar_product_state(n)] +# move to density matrices +states = [np.outer(state.conj(), state) for state in states] +labels = [ "Product", "GHZ", "W", "Haar"] + +##################################################################### # On the flip side, highly entangled states spread their purity across more and higher-order (i.e., larger dimensional) irreps, # similar to how more complex ("wiggly") functions have larger Fourier coefficients at higher frequencies. # This means that analyzing a state's Fourier spectrum can tell us how "resourceful" or entangled it is. @@ -279,7 +326,6 @@ def g(x): from scipy.stats import unitary_group from functools import reduce -n = 2 # create n haar random single-qubit unitaries Us = [unitary_group.rvs(dim=2) for _ in range(n)] @@ -313,8 +359,6 @@ def g(x): # For example: # -n = 2 - # create a random quantum state psi = np.random.rand(2**n) # construct the corresponding density matrix @@ -392,7 +436,7 @@ def g(x): from collections import OrderedDict -def group_rows_cols_by_sparsity(B, tol=0): +def group_rows_cols_by_sparsity(mat, tol=0): """ Given a matrix B, this function: 1. Groups identical rows and columns. @@ -402,7 +446,7 @@ def group_rows_cols_by_sparsity(B, tol=0): Parameters ---------- - B : ndarray, shape (n, m) + mat : ndarray, shape (n, m) Input matrix. Returns @@ -413,7 +457,7 @@ def group_rows_cols_by_sparsity(B, tol=0): Column permutation matrix. """ # Compute boolean mask where |B| >= tol - mask = np.abs(B) >= 1e-8 + mask = np.abs(mat) >= tol # Convert boolean mask to integer (False→0, True→1) C = mask.astype(int) n, m = C.shape @@ -477,15 +521,72 @@ def key_and_zeros(vec): # The reordering made the block structure visible. You can check that any vectorised non-entangling matrix `U_vec` # has the same block structure if we change the basis via `Qinv @ U_vec @ Q`. # -# But what basis have we actually changed into? It turns out that `Q` changes into the Pauli basis! +# The next step is to +# 5. **Identify basis for invariant subspaces**. +# +# [TRY: Rotate a vector into this basis and only summarise the entries] # +_pauli_map = { + 'I': np.array([[1,0],[0,1]], dtype=complex), + 'X': np.array([[0,1],[1,0]], dtype=complex), + 'Y': np.array([[0,-1j],[1j,0]],dtype=complex), + 'Z': np.array([[1,0],[0,-1]], dtype=complex), +} +factors = [_pauli_map[ch] for ch in 'IX'] +rho_P = functools.reduce(lambda A, B: np.kron(A, B), factors) +rho_P = rho_P.flatten(order="F") +rho_P = Q @ rho_P +print("HERE", rho_P) + + +# vectorise the states we defined earlier +states_vec = [state.flatten(order='F') for state in states] + + + +# change into the block-diagonal basis +states_vec = [Q @ state_vec for state_vec in states_vec] +purities = [[np.vdot(state_vec[0], state_vec[0]), + np.vdot(state_vec[1:4], state_vec[1:4]), + np.vdot(state_vec[4:8], state_vec[4:8]), + np.vdot(state_vec[8:16], state_vec[8:16]), + ] for state_vec in states_vec ] + + -Qinv @ U_vec @ Q +# Grab default color cycle +colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] -# We cheat here a little: we already know the basis in which :math:`\tilde{U}` is block-diagonal: the Pauli basis. -# This allows us to construct the matrix implementing the basis change directly. +# Create two vertically aligned subplots sharing the x-axis +fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) + +for i, data in enumerate(purities): + color = colors[i % len(colors)] + ax1.plot(data, label=f'{i}', color=color) + ax2.plot(np.cumsum(data), label=f'{i}', color=color) + +ax1.set_ylabel('Purity') +ax2.set_ylabel('Cumulative Purity') +ax2.set_xlabel('Module weight') + +ax1.legend(loc='upper left') +ax2.legend(loc='upper left') + +plt.tight_layout() +plt.savefig("/home/maria/Desktop/purities1.png") + +plt.show() + + +########################################################################### +# Bonus section +# -------------- +# +# But what basis have we actually changed into? It turns out that `Q` changes into the Pauli basis. We could have constructed +# `Q` from first principles: # + import functools import itertools @@ -528,9 +629,119 @@ def pauli_basis(n): print(np.round(U_vec_rot.real, 2)) -# 5. **Identify basis for invariant subspaces**. Will we now find interesting subspaces by simultaneously block-diagonalizing :math:`\tilde{R}`? + +###################################################################### +# The different blocks correspond to subspaces spanned by Pauli words with different structures: +# +# * The first block of size 1x1 corresponds to a subspace spanned by the Pauli word operator :math:`\mathbm{1} \otimes \mathbm{1}`. +# * The second two blocks of size 4x4 corresponds to a subspace spanned by to Pauli word operators :math:`\mathbm{1} \otimes P_2` and :math:`P_1 \otimes \mathbm{1}`, where +# :math:`P_1, P_2 \in \{X, Y, Z\}`. +# * The third block of size XxX corresponds to a subspace spanned by Pauli word operators of the form :math:`P_1 \otimes P_2`. +# +# In other words, we didn't need to vectorise everything and use linear algebra tools to compute the GFD purities. We could have +# simply computed the inner product with the rigth set of Pauli operators in :math:`B(H)`: # +def generate_pauli_strings(n: int): + """ + Generate all length‑n strings over the Pauli alphabet ['I','X','Y','Z']. + Returns a list of 4**n strings, e.g. ['IIX', 'IXZ', …]. + """ + return [''.join(p) for p in itertools.product('IXYZ', repeat=n)] + + +def pauli_string_to_matrix(pauli_str: str): + """ + Convert a Pauli string (e.g. 'XIY') to its full 2^n × 2^n matrix. + """ + mats = [_pauli_map[s] for s in pauli_str] + return functools.reduce(lambda A, B: np.kron(A, B), mats) + + +# function to project into the modules +def compute_me_purities(op): + """ + Computes GFD purities of op (assumed to be np.matrix) + by explicitly computing overlaps with Paulis + """ + + if op.ndim == 1: + # state vector + is_vector = True + elif op.ndim == 2 and op.shape[0] == op.shape[1]: + # density/operator + is_vector = False + else: + raise ValueError("`op` must be either a 1D state vector or a square matrix") + + d = op.shape[0] + n = int(np.log2(d)) + + basis = generate_pauli_strings(n) + purities = np.zeros(n + 1) + for belem in basis: + k = n - belem.count('I') + P = pauli_string_to_matrix(belem) + + if is_vector: + ovp = op.conj() @ (P @ op) + else: + ovp = np.trace(op @ P) + + #assert ovp.imag < 1e-10 + purities[k] += (ovp.real) ** 2 + + return purities / (2 ** n) + + +purities = [compute_me_purities(op) for op in states] + +# Grab default color cycle +colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] + +# Create two vertically aligned subplots sharing the x-axis +fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) + +for i, data in enumerate(purities): + color = colors[i % len(colors)] + ax1.plot(data, label=f'{labels[i]}', color=color) + ax2.plot(np.cumsum(data), label=f'{labels[i]}', color=color) + +ax1.set_ylabel('Purity') +ax2.set_ylabel('Cumulative Purity') +ax2.set_xlabel('Module weight') + +ax1.legend(loc='upper left') +ax2.legend(loc='upper left') + +plt.tight_layout() +plt.savefig("/home/maria/Desktop/purities2.png") +plt.show() + +""" +# To illustrate this, we'll consider a few key examples: +# * **Product state**: A "smooth" state with no entanglement. It shows high purity only in the lowest-order irreps, similar to a constant function in classical Fourier analysis. +# * **GHZ state**: A highly structured, maximally entangled state. It behaves like a quantum analog of a high-frequency oscillation, having purity concentrated mostly in the highest-order irreps, creating a clear quantum fingerprint. +# * **W state**: Moderately entangled, somewhat between the GHZ and product states, with a broader yet smoother distribution across the irreps. +# * **Random (Haar) state**: Highly complex but without structured entanglement patterns, its purity is distributed more evenly across the spectrum.# In the code examples we'll show shortly, you'll see precisely these patterns emerge. +# For each state, we compute the purity distribution across generalized Fourier modes (irreps), illustrating exactly how entanglement complexity relates to spectral structure. + +# # Here, show purity plots for these states as computed by the provided code + +# The fascinating aspect here is that while multipartite entanglement can get complex very quickly as we add qubits (due to the exponential increase in possible correlations), +# our generalized Fourier analysis still provides a clear, intuitive fingerprint of a state's entanglement structure. +# Even more intriguingly, just as smoothness guides how we compress classical signals (by discarding higher-order frequencies), the entanglement fingerprint suggests ways we might compress quantum states, +# discarding information in higher-order irreps to simplify quantum simulations and measurements. +# In short, generalized Fourier transforms allow us to understand quantum complexity, much like classical Fourier transforms give insight into smoothness. +# By reading a state's quantum Fourier fingerprint, we gain a clear window into the subtle quantum world of multipartite entanglement. + + +""" + + + + + # # References From e3f070d58bea5b0c416dc71316e6e7568e3c0a70 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Mon, 11 Aug 2025 10:03:30 +0200 Subject: [PATCH 11/33] backup --- .../tutorial_resourcefulness/demo.py | 390 ++++++++++-------- 1 file changed, 215 insertions(+), 175 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 671883426d..9e77c2012f 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -8,48 +8,60 @@ :alt: DESCRIPTION. :target: javascript:void(0) -Resource theories in quantum information theory ask how "complex" a given quantum state is with respect to a certain -measure of complexity. For example, using the resource of entanglement, we can ask how entangled a quantum state is. Other well-known -resources are Clifford stabilizerness, which measures how close a state is from being prepared by a circuit that only uses -classically simulatable Clifford gates, or Gaussianity, which measures how far away a state is from a so-called "Gaussian state" -that is relatively easy to prepare in quantum optics, and likewise classically simulatable. As the name "resourceful" suggests, +Resource theories in quantum information ask how "complex" a given quantum state is with respect to a certain +measure of complexity. For example, using the resource of *entanglement*, we can ask how entangled a quantum state is. Other well-known +resources are *Clifford stabilizerness*, which measures how close a state is from being preparable by a +classically simulatable "Clifford circuit", or *Gaussianity*, which measures how far away a state is from "Gaussian states" +considered simple in quantum optics. As the name "resourceful" suggests, these measures of complexity often relate to how much "effort" states are, for example with respect to classical simulation or preparation in the lab. -It turns out that the resourcefulness of quantum states can be investigated with tools from generalised Fourier analysis [#Bermejo_Braccia]_. -Fourier analysis here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case +It turns out [#Bermejo_Braccia]_ that the resourcefulness of quantum states can be investigated with tools from *generalised Fourier analysis*. +"Fourier analysis" here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case is not a function over :mathbb:`R` or :mathbb:`Z`, but a quantum state. "Generalised" indicates that we don't use the -standard Fourier transform, but its group-theoretic generalisations [LINK TO RELATED DEMOS]. This is important, because -[#Bermejo_Braccia]_ link a resource to a group -- essentially, by defining the set of unitaries that maps resource-free -states to resource-free states as a "representation" of a group, which gets block-diagonalised to find a generalised Fourier basis. -The intuition, however, is exactly the same as in the standard Fourier transform: large higher-order Fourier -coefficients indicate a less "smooth" or "resourceful" function. - -In this tutorial we will illustrate the idea of generalised Fourier analysis for resource theories with two simple examples. -First we will look at a standard Fourier decomposition of a function, but from the perspective of resources,in order to -introduce the basic idea in a familiar setting. Secondly, we will use the same concepts to analyse the entanglement - resource of quantum states, reproducing Figure 2 in [#Bermejo_Braccia]_. +standard Fourier transform, but its group-theoretic generalisations [LINK TO RELATED DEMO]. +[#Bermejo_Braccia]_ suggest to compute a quantity that they call the **Generalised Fourier Decomposition (GFD) Purity**, +and use it as a "footprint" of a state's resource profile. When using the standard Fourier transform, +the GFD purities are just the absolute squares of the normal Fourier coefficients, which is also known as the *power spectrum*. + +The basic idea is to identify the set of unitaries that maps resource-free +states to resource-free states with a *linear representation* of a group. +The basis in which this representation, and hence the free unitaries, are (block-)diagonal, reveals so-called *irreducible subspaces*. +The GFD Purities are then the "weight" a state has in these subspaces. As in standard Fourier analysis, +higher-order Purities indicate a less resourceful function. +This intuition carries over to the generalised case, where more resourceful states have higher weights in higher-order subspaces. + +In this tutorial we will illustrate with two simple examples how to compute the GFD Purities to analyse resource. +To introduce the basic recipe in a familiar setting, we will first look at the power spectrum of a discrete function. +We will then use the same concepts to analyse the *entanglement* +of quantum states as a resource, reproducing Figure 2 in [#Bermejo_Braccia]_. .. figure:: ../_static/demonstration_assets/resourcefulness/figure2_paper.png :align: center :width: 70% - :alt: Fourier coefficients, or projections into "irreducible subspaces", of different states using 2-qubit entanglement as a resource. - A Bell state, which is maximally entangled, has high contributions in higher-order Fourier coefficients, while - a tensor product state with little entanglement has contributions in lower-order Fourier coefficients. The interpolation - between the two extremes, exemplified by a Haar random state, has a Fourier spectrum in between. + :alt: GFD Purities of different states using 2-qubit entanglement as a resource. + A GHZ state, which is maximally entangled, has high contributions in higher-order Purities, while + a product state with no entanglement has contributions in lower-order Purities. The interpolation + between the two extremes, exemplified by a Haar random state, has a spectrum in between. + +While the theory rests in group theory, the tutorial is aimed at readers who don't know much about groups at all, +since everything can be understood by applying standard linear algebra. -Luckily, in the case of entanglement as a resource, the generalised Fourier coefficients can be computed using Pauli expectations. -This saves us from diving too deep into representation theory. In fact, the tutorial should be informative without knowing much -about groups at all! But of course, even in this simple case the numerical analysis scales exponentially with the number of qubits -in general. It is a fascinating question in which situations the Fourier coefficients of a physical state could be read out -on a quantum computer, which is known to sometimes perform the block-diagonalisation efficiently. +.. note:: + Of course, even in this simple case the numerical analysis scales exponentially with the number of qubits, and + everything we present here is only possible to compute for small system sizes. It is a fascinating question in + which situations the GFD Purities of a physical state could be read out + on a quantum computer, which is known to sometimes perform the block-diagonalisation efficiently. Standard Fourier analysis through the lense of resources -------------------------------------------------------- -Let's start recalling the standard Fourier transform, and for simplicity we'll work with the -discrete version. Given N real values :math:`x_0,...,x_{N-1}`, that we can interpret [LINK TO OTHER DEMO] as the values +The power spectrum as GFD Purities +++++++++++++++++++++++++++++++++++ + +Let's start recalling the standard discrete Fourier transform. +Given N real values :math:`x_0,...,x_{N-1}`, that we can interpret [LINK TO OTHER DEMO] as the values of a function :math:`f(0), ..., f(N-1)` over the integers :math:`x \in {0,...,N-1}`, the Fourier transform computes the Fourier coefficients @@ -57,8 +69,7 @@ \hat{f}(k) = \frac{1}{\sqrt{N}\sum_{x=0}^{N-1} f(x) e^{\frac{2 \pi i}{N} k x}, k = 0,...,N-1 In words, the Fourier coefficients are projections of the function :math:`f` onto the "Fourier" basis functions -:math:`e^{\frac{2 \pi i}{N} k x}`. Note that we use a normalisation here that is consistent with a unitary transform that -we construct as a matrix below. +:math:`e^{\frac{2 \pi i}{N} k x}`. Note that we use a normalisation here that is consistent with a unitary transform. Let's see this equation in action. """ @@ -77,30 +88,45 @@ def f_hat(k): projection = [ f(x) * np.exp(-2 * np.pi * 1j * k * x / N)/np.sqrt(N) for x in range(N)] return np.sum(projection) -def plot(f, f_hat): +f_vals = [f(x) for x in range(N)] +f_hat_vals = [f_hat(k) for k in range(N)] - fig, (ax1, ax2) = plt.subplots(2, 1) - ax1.bar(range(N), [np.real(f(x)) for x in range(N)], color='dimgray') # casting to real is needed in case we perform an inverse FT - ax1.set_title(f"function f") +fig, (ax1, ax2) = plt.subplots(2, 1) +ax1.bar(range(N), f_vals, color='dimgray') +ax1.set_title(f"function f") - ax2.bar(np.array(range(N))+0.05, [np.imag(f_hat(x)) for x in range(N)], color='lightpink', label="imaginary part") - ax2.bar(range(N), [np.real(f_hat(k)) for k in range(N)], color='dimgray', label="real part") - ax2.set_title("Fourier coefficients") - plt.legend() - plt.tight_layout() - plt.show() +ax2.bar(np.array(range(N))+0.05, np.imag(f_hat_vals), color='lightpink', label="imaginary part") +ax2.bar(range(N), np.real(f_hat_vals), color='dimgray', label="real part") +ax2.set_title("Fourier coefficients") +plt.legend() +plt.tight_layout() +plt.show() -plot(f, f_hat) ###################################################################### -# Now, what kind of resource are we dealing with here? In other words, what +# Now, we mentioned that the absolute square of the standard Fourier coefficients are the simplest example of +# GFD Purities, so for this case, we can easily compute our quantity of interest! +# + +purities = [np.abs(f_hat(k))**2 for k in range(N)] +plt.plot(purities) +plt.ylabel("GFD purities") +plt.xlabel("k") +plt.tight_layout() +plt.show() + + +###################################################################### +# Smoothness as a resource +# ++++++++++++++++++++++++ +# +# But what kind of resource are we dealing with here? In other words, what # measure of complexity gets higher when a function has large higher-order Fourier coefficients? # -# Well, let us look for the function with the least resource or complexity! For this we can just +# Well, let us look for the function with the *least* resource or complexity! For this we can just # work backwards: define a Fourier spectrum that only has a contribution in the lowest order coefficient, # and apply the inverse Fourier transform to look at the function it corresponds to! # -# def g_hat(k): """The least complex Fourier spectrum possible.""" @@ -114,78 +140,92 @@ def g(x): projection = [g_hat(k) * np.exp(2 * np.pi * 1j * k * x / N)/np.sqrt(N) for k in range(N)] return np.sum(projection) -plot(g, g_hat) +g_hat_vals = [g_hat(x) for x in range(N)] +g_vals = [g(k) for k in range(N)] + +fig, (ax1, ax2) = plt.subplots(2, 1) +ax1.bar(range(N), g_hat_vals, color='dimgray') +ax1.set_title("Fourier coefficients") + +ax2.bar(np.array(range(N))+0.05, np.imag(g_vals), color='lightpink', label="imaginary part") +ax2.bar(range(N), np.real(g_vals), color='dimgray', label="real part") +ax2.set_title(f"function g") + +plt.legend() +plt.tight_layout() +plt.show() ###################################################################### # Well, the function is constant. This makes sense, because we know that the decay of the Fourier coefficient -# is related to the number of times a function is differentiable, which in turn is the technical definition of smoothness. A +# is related to the number of times a function is differentiable, which in turn is the technical definition of "smoothness". A # constant function is maximally smooth -- it does not wiggle at all. In other words, # the resource of the standard Fourier transform is smoothness, and a resource-free function is constant! # ###################################################################### -# Linking resources to group representations -# ------------------------------------------ +# The general recipe +# ++++++++++++++++++ # -# Fourier transforms are intimately linked to groups (in fact, the x-domain :math:`0,...,N-1` is strictly speaking a group, -# which you can learn more about [here]). Without expecting you to know group jargon, we have to establish a few concepts -# that generalise the above example to quantum states and more generic resources. The crucial idea is to -# define a resource by fixing a set of vectors (later, quantum states) that are considered resource-free. -# We also need to define unitary matrices that map resource-free vectors to resource-free vectors, -# and these matrices need to form a *unitary representation* of a group :math:`G`, -# which is a matrix-valued function :math:`R(g), g \in G` on the group. -# This is all we need to guarantee that the matrices can be simultaneously block-diagonalised, or written as a -# direct sum of smaller matrix-valued functions over the group, :math:`r^{\alpha}(g)`. Note that some of the blocks may be identical. -# If you know group theory, then you will recognise that the :math:`r^{\alpha}(g)` are the *irreducible representations* of the group. +# Computing the GFD Purities for the resource of smoothness was very simple. We will now make it more complicated, +# in order to set up the machinery that calculates the Purities for more general cases like quantum entanglement. # -# [TODO: image] +# Find free vectors +# ***************** +# The first step is to find "free vectors". So far we dealt with a discrete function :math:`f(0), ..., f(N-1)`, +# but we can easily write it as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. As argued above, +# the set of resource-free vectors corresponds to constant functions, :math:`f(0) = ... = f(N-1)`. # + +f_vec = np.array([f(x) for x in range(N)]) + +# Find free unitary transformations +# ********************************* +# Next, we need to identify a set of unitary matrices that does not change the +# "constantness" of the vector. In the case above this is given by the permutation matrices, which +# swap the entries of the vector but do not change any of them. # -# What is important for us is that these smaller matrix-valued functions :math:`r^{\alpha}(g)` define a -# subspace :math:`V_{\alpha, j}`, where the index :math:`j` accounts for the fact that there may be several copies of -# an :math:`r^{\alpha}(g)` on the diagonal of :math:`R(g)`, each of which corresponds to one subspace. -# [TODO: BASIS :math:`w^{(i)}_{\alpha, j}`]. -# -# The Generalised Fourier Decomposition (GFD) purity is the length of a projection of a vector :math:`v \in V` onto one of these subspaces :math:`V_{\alpha, j}`, -# -# .. math:: -# \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. -# -# In our standard -# example above, the subspaces are one-dimensional spaces spanned by the Fourier basis functions :math:`\chi_k(x) = e^{\frac{2 \pi i}{N} k x}`. -# The vector space is the space of functions (if this is confusing, think of the function values as arranged in -# N-dimensional vectors :math:`\vec{f}` and :math:`\vec{\chi}_k`). -# The projection is executed by the sum :math:`\sum_x f(x) \chi_k(x) = \langle f, \chi_k`. In other words, the GFD purity -# is the absolute square of the Fourier coefficient, -# -#.. math:: -# \mathcal{P}(\vec{f}) = | \vec{\chi}_k, \vec{f} \rangle |^2 = |\hat{f}|^2. -# -# Generalising from the standard Fourier basis functions to irreducible -# representations allows us to generalise the Fourier transform to lots of other groups, and hence, resources. -# -# So far so good, but what is the representation :math:`R(g)` for the standard Fourier transform? Let's follow the recipe: -# -# 1. We first need to consider our function :math:`f(0), ..., f(N-1)` as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. -# 2. As argued above, the set of resource-free vectors correspond to constant functions, :math:`f(0) = ... = f(N-1)`. -# 3. We need a set of unitary matrices that does not change the "constantness" of the vector. This set is given by permutation matrices, -# which swap the entries of the vector but do not change any of them. -# 4. The *circulant* permutation matrices can be shown to form a unitary representation :math:`R(g)` for :math:`g \in G = Z_N`, called the *regular representation*. -# 5. We are now guaranteed that there is a basis change that diagonalises all matrices :math:`R(g)` together. -# (Note that the Fourier transform is sometimes defined as the basis change that block-diagonalises the regular representation!) As mentioned, this is -# a block-diagonalisation where the blocks happen to be 1-dimensional, as is the rule for so-called "Abelian" groups. -# 6. The values on the diagonal of :math:`R(g)` under this basis change are exactly the Fourier basis functions `:math:`e^{\frac{2 \pi i}{N} k x}`. -# -# Let's verify this! -# -# First, let us write our function and Fourier spectrum above as vectors: +# Identify a group representation +# ******************************* +# This is the most difficult step, which sometimes requires a lot of group-theoretic knowledge. Essentially, we have to associate +# the unitary transformations, or a subset of them, with a *unitary group representation*. A representation is a +# matrix-valued function :math:`R(g), g \in G` on a group with the property that :math:`R(g) R(g') = R(gg')`, +# where :math:`gg'` is the composition of elements according to the group. Without dwelling further, we simply +# recognise that the *circulant* permutation matrices -- those that shift vector +# entries by :math:`s` positions -- can be shown to +# form a unitary representation for the group :math:`g \in G = Z_N`, called the *regular representation*. The group :math:`Z_N` +# are the integers from 0 to N-1 under addition modulo N, which can be seen as the x-domain of our function. +# Every circulant matrix hence gets associated with one x-value. # -f_vec = np.array([f(x) for x in range(N)]) -f_hat_vec = np.array([f_hat(k) for k in range(N)]) +s = 2 + +# create a circulant permutation matrix +first_row = np.zeros(N) +first_row[s] = 1 + +# initialize the matrix with the first row +P = np.zeros((N, N)) +P[0, :] = first_row + +# generate subsequent rows by cyclically shifting the previous row +for i in range(1, N): + P[i, :] = np.roll(P[i-1, :], 1) + +print(P) + ###################################################################### -# The Fourier transform then becomes a matrix multiplication with the matrix: +# Find the basis that block-diagonalises the representation +# ********************************************************* +# This was quite a bit of group jargon, but for a good reason: We are now guaranteed that there is +# a basis change that block-diagonalises *all* circulant permutation matrices. If you know group theory, +# then you will recognise that the blocks reveal the *irreducible representations* of the group. +# What is important is that the blocks form different "irreducible" subspaces which we need to compute +# the GFD Purities in the next step. +# +# In our toy case, and for all *Abelian* groups, the blocks are 1-dimensional, and the basis +# change *diagonalises* every matrix of the representation. And the basis change is nothing other than the Fourier transform. +# In matrix notation, the Fourier transform looks as follows: # # .. math:: # F = \frac{1}{\sqrt{N}} \begin{bmatrix} @@ -203,47 +243,48 @@ def g(x): from scipy.linalg import dft F = dft(N)/np.sqrt(N) -# compare to what we previously computed -print(np.allclose(F @ f_vec, f_hat_vec)) - - -###################################################################### -# Above we claimed that this matrix F is the basis transform that diagonalises -# a permutation matrix (in other words, the unitary representation evaluated at some group element). -# - -# create a circulant permutation matrix -i = np.random.randint(0, N) -first_row = np.zeros(N) -first_row[i] = 1 - -# initialize the matrix with the first row -P = np.zeros((N, N)) -P[0, :] = first_row - -# generate subsequent rows by cyclically shifting the previous row -for i in range(1, N): - P[i, :] = np.roll(P[i-1, :], 1) - +# make sure this is what we previously computed +assert np.allclose(F @ f_vec, f_hat_vals) -# change into the Fourier basis using F +# change the circulant matrix into the Fourier basis using F np.set_printoptions(precision=2, suppress=True) P_F = F @ P @ np.linalg.inv(F) # check if the resulting matrix is diagonal # trick: remove diagonal and make sure the remainder is zero -print(np.allclose(P_F - np.diag(np.diagonal(P_F)), np.zeros((N,N)) )) - +print("is diagonal:", np.allclose(P_F - np.diag(np.diagonal(P_F)), np.zeros((N,N)) )) ###################################################################### -# To recap, we saw that the standard Fourier analysis can be generalised by -# interpreting "smoothness" or "constantness" as a resource, linking it to a vector space and a group -# representation and (block-)diagonalising the representation. The blocks, here one-dimensional, -# form a basis for subspaces. The GFD purities suggested in [#Bermejo_Braccia]_ as a resource fingerprint -# are just projections of some vector (here, a function) onto the basis vectors and taking the absolute square. +# In fact, the Fourier transform is sometimes *defined* as the basis change that diagonalises the regular representation! +# For other representations, however, we need other transforms. +# +# Project into the basis +# ********************** +# We now have a very different perspective on the power spectrum. We wrote a function as a vector, +# and changed into the basis that diagonalises circulant matrices. For every 1-d block, we took the corresponding +# coordinate in the new vector (which we know are the Fourier coefficients) and computed its absolute square. +# +# .. math:: \mathcal{P}(\vec{f}) = |\hat{f}|^2. +# +# More generally, if +# we have different blocks labeled by :math:`\alpha` and their copies by the multiplicity factor :math:`j`, +# then each block marks a subspace spanned by a basis :math:`\{w^{(i)}_{\alpha, j}\} +# The GFD purity is the length of a projection of a new vector :math:`v \in V` onto one of these subspaces, +# +# .. math:: +# \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. # -# Armed with this recipe, we can now try to analyse entanglement as a resource, -# and density matrices describing quantum states as vectors in a vector space :math:`L(H)`. +# + +###################################################################### +# To recap, we saw that by mobilising group theory and linear algebra, +# the standard power spectrum of a function over integers can be interpreted as the "GFD Purities" +# for the resource of "smoothness". Armed with this recipe, we can now try to analyse entanglement as a resource. +# We will first try to work with Dirac vectors as our vectors, and unitary matrices as the representation, +# but see very quickly that this does not lead to rich enough "footprints". +# Instead, we switch to density matrices and their unitary evolution (which need to be vectorised in order to +# use numerical linear algebra tools). This will allow us to find "power spectra" or GFD Purities of states, +# indicating how entangled they are. # ###################################################################### @@ -300,10 +341,10 @@ def haar_product_state(n: int): n = 2 -states = [ghz_state(n), w_state(n), haar_state(n), haar_product_state(n)] +states = [haar_product_state(n), haar_state(n), ghz_state(n), w_state(n)] # move to density matrices states = [np.outer(state.conj(), state) for state in states] -labels = [ "Product", "GHZ", "W", "Haar"] +labels = [ "Product", "Haar", "GHZ", "W"] ##################################################################### # On the flip side, highly entangled states spread their purity across more and higher-order (i.e., larger dimensional) irreps, @@ -311,11 +352,13 @@ def haar_product_state(n: int): # This means that analyzing a state's Fourier spectrum can tell us how "resourceful" or entangled it is. # # Let us walk the same steps we took when studying the resource of "smoothness". -# 1. **Vectorise**. Luckily for us, our objects of interest, the quantum states :math:`\psi \in \mathbb{C}^{2n}`, are already in the form of vectors. -# 2. **Identify free vectors**. It's easy to define the set of free states for multipartite entanglement: tensor products of single-qubit quantum states. -# 3. **Identify free linear transformations**. Now, what unitary transformation of a quantum state does not generate -# entanglement? You guessed it right, "non-entangling" circuits that consist only of single-qubit gates :math:`U=\Bigotimes U_j` for :math:`U_j \in SU(2)`. -# 4. **Ensure they form a representation**. It turns out that non-entangling unitaries are the standard representation +# 1. **Find free vectors**. Luckily for us, our objects of interest, the quantum states :math:`\psi \in \mathbb{C}^{2n}`, +# are already in the form of vectors. It's easy to define the set of free states for multipartite entanglement: +# tensor products of single-qubit quantum states. +# 2. **Find free unitary transformations**. Now, what unitary transformation of a quantum state does not generate +# entanglement? You guessed it right, "non-entangling" circuits that consist only of single-qubit +# gates :math:`U=\Bigotimes U_j` for :math:`U_j \in SU(2)`. +# 3. **Identify a group representation**. It turns out that non-entangling unitaries are the standard representation # of the group :math:`G = SU(2) x SU(2) ... x SU(2)` for the vector space :math:`H`, the Hilbert space of the state vectors. # Again, this implies that we can find a basis of the Hilbert space, where any :math:`U` is simultaneously block-diagonal. # @@ -340,20 +383,21 @@ def haar_product_state(n: int): ###################################################################### # Wait, this is not a block-diagonal matrix, even if we'd shuffle the rows and columns. What is happening here? # It turns out that the non-entangling unitary matrices only have a single block of size :math:`2^n \times 2^n`. -# Technically speaking, the representation :math:`R(g) = U(g)` is irreducible over :math:`H`. -# As a consequence, the invariant subspace is :math:`H` itself, and the GFD purity is simply the purity of the state :math:`psi`, which is :math:`1`. -# This, of course, is not a very informative measure! -# +# Technically speaking, the representation is *irreducible* over :math:`H`. +# As a consequence, the invariant subspace is :math:`H` itself, and the single GFD purity is simply the purity of +# the state :math:`\psi`, which is 1. +# This, of course, is not a very informative footprint -- one that is much too coarse! # -# However, rather than a bug, this is a feature of the GFD framework. Indeed, nobody forces us to restrict our attention to :math:`H` and to +# However, nobody forces us to restrict our attention to :math:`H` and to # the (standard) representation of non-entangling unitary matrices. -# After all, what's the first symptom of entanglement in a quantum state? You guessed right again, the reduced density matrix of some subsystem is mixed! +# After all, what's the first symptom of entanglement in a quantum state? +# You guessed right again, the reduced density matrix of some subsystem is mixed! # Wouldn't it make more sense then to study the multipartite entanglement form the point of view of density matrices? # It turns out that moving into the space :math:`B(H)` of bounded linear operators in which the density matrices live leads to # a much more nuanced Fourier spectrum. # # Let us walk the steps above again -# 1. **Vectorise**. :math:`L(H)` is a vector space in the technical sense, but one of matrices. To use the linear algebra +# 1. **Find free vectors**. :math:`L(H)` is a vector space in the technical sense, but one of matrices. To use the linear algebra # tricks from before we have to "vectorize" density matrices :math:`rho=\sum_i,j c_i,j |i\rangle \langle j|` # into vectors :math:`|rho\rangle \rangle = \sum_i,j c_i,j |i\rangle |j\rangle \in H \otimes H^*`. # For example: @@ -367,46 +411,45 @@ def haar_product_state(n: int): rho_vec = rho.flatten(order='F') ###################################################################### -# 2. **Identify free vectors**. The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` +# The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` # where each :math:`\rho_j` is a single-qubit state. We only have to write them in flattened form. -# 3. **Identify free linear transformations**. The free operations are still given by non-entangling unitaries, but +# 2. **Find free unitary transformations**. The free operations are still given by non-entangling unitaries, but # of course, they act on density matrices via :math:`\rho' = U \rho U^{\dagger}`. # We can also vectorise this operation by defining the matrix :math:`U_{\mathrm{vec}}(g) = U \otimes U^*`. # We then have that :math:`|\rho'\rangle \rangle = U_{\mathrm{vec}} \rho`. # To demonstrate: # - -# Vectorise U -U_vec = np.kron(U.conj(), U) +# vectorise U +Uvec = np.kron(U.conj(), U) # evolve the state above by U, using the vectorised objects -rho_out_vec = U_vec @ rho_vec +rho_out_vec = Uvec @ rho_vec # reshape the result back into a density matrix rho_out = np.reshape(rho_out_vec, newshape=(2**2, 2**2)) # this is the same as the usual adjoint application of U print(np.allclose(rho_out, U @ rho @ U.conj().T )) ###################################################################### -# 4. **Ensure they form a representation**. This "adjoint action" is indeed a valid representation of +# 3. **Identify a group representation**. This "adjoint action" is indeed a valid representation of # :math:`G = SU(2) x SU(2) ... x SU(2)`, called the *defining representation*. However, it is a different one from before, # and this time there is a basis transformation that properly block-diagonalises all matrices in the representation. # To find this transformation we compute the eigendecomposition of an arbitrary linear combination of a set of matrices in the representation. # -U_vecs = [] +Uvecs = [] for i in range(10): # create n haar random single-qubit unitaries Ujs = [unitary_group.rvs(dim=2) for _ in range(n)] # compose them into a non-entangling n-qubit unitary U = reduce(lambda A, B: np.kron(A, B), Ujs) # Vectorise U - U_vec = np.kron(U.conj(), U) - U_vecs.append(U_vec) + Uvec = np.kron(U.conj(), U) + Uvecs.append(Uvec) # Create a random linear combination of the matrices -alphas = np.random.randn(len(U_vecs)) + 1j * np.random.randn(len(U_vecs)) -M_combo = sum(a * M for a, M in zip(alphas, U_vecs)) +alphas = np.random.randn(len(Uvecs)) + 1j * np.random.randn(len(Uvecs)) +M_combo = sum(a * M for a, M in zip(alphas, Uvecs)) # Eigendecompose the linear combination vals, Q = np.linalg.eig(M_combo) @@ -417,8 +460,8 @@ def haar_product_state(n: int): Qinv = np.linalg.inv(Q) # take one of the vectorised unitaries -U_vec = U_vecs[0] -U_vec_diag = Qinv @ U_vec @ Q +Uvec = Uvecs[0] +Uvec_diag = Qinv @ Uvec @ Q np.set_printoptions( formatter={'float': lambda x: f"{x:5.2g}"}, @@ -426,10 +469,10 @@ def haar_product_state(n: int): threshold=10000 # so it doesn’t summarize large arrays ) -print(U_vec_diag) +print(Uvec_diag) ###################################################################### -# But `U0_diag` does not look block diagonal. What happened here? +# But `Uvec_diag` does not look block diagonal. What happened here? # Well, it is block-diagonal, but we have to reorder the columns and rows of the final matrix to make this visible. # This takes a bit of pain, encapsulated in the following function: # @@ -508,21 +551,19 @@ def key_and_zeros(vec): return P_row, P_col -P_row, P_col = group_rows_cols_by_sparsity(U_vec_diag) - -Q = Q @ P_col -Qinv = P_row @ Qinv +P_row, P_col = group_rows_cols_by_sparsity(Uvec_diag) -U_vec_diag = Qinv @ U_vec @ Q +Uvec_diag = P_row @ Uvec_diag @ P_col -print(U_vec_diag) +print("\n\n ---------------") +print(Uvec_diag) ###################################################################### -# The reordering made the block structure visible. You can check that any vectorised non-entangling matrix `U_vec` -# has the same block structure if we change the basis via `Qinv @ U_vec @ Q`. +# The reordering made the block structure visible. You can check that any vectorised non-entangling matrix `Uvec` +# has the same block structure if we change the basis and reorder via `P_row @ Qinv @ Uvec @ Q @ P_col`. # # The next step is to -# 5. **Identify basis for invariant subspaces**. +# 5. **Find the basis that block-diagonalises the representation**. # # [TRY: Rotate a vector into this basis and only summarise the entries] # @@ -573,7 +614,6 @@ def key_and_zeros(vec): ax2.legend(loc='upper left') plt.tight_layout() -plt.savefig("/home/maria/Desktop/purities1.png") plt.show() @@ -619,14 +659,14 @@ def pauli_basis(n): # compute the basis change matrix basis, B = pauli_basis(n) # apply basis change -U_vec_rot = B.conj().T @ U_vec @ B +Uvec_rot = B.conj().T @ Uvec @ B # make sure the imaginary part is zero, so we don't have to print it # (this is a property of the Pauli basis) -assert np.isclose(np.sum(U_vec_rot.imag), 0) +assert np.isclose(np.sum(Uvec_rot.imag), 0) # print the real part -print(np.round(U_vec_rot.real, 2)) +print(np.round(Uvec_rot.real, 2)) From 9809b156417f9be0933aa7bfb7547651d02f1840 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Tue, 12 Aug 2025 10:38:17 +0200 Subject: [PATCH 12/33] backup --- demonstrations_v2/tutorial_resourcefulness/demo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 9e77c2012f..861672fc5b 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -88,6 +88,9 @@ def f_hat(k): projection = [ f(x) * np.exp(-2 * np.pi * 1j * k * x / N)/np.sqrt(N) for x in range(N)] return np.sum(projection) + + + f_vals = [f(x) for x in range(N)] f_hat_vals = [f_hat(k) for k in range(N)] From 539b4facf5e83eac27e048aece5c7a0a121bafdc Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 13 Aug 2025 10:04:31 +0200 Subject: [PATCH 13/33] backup --- demonstrations/tutorial_resourcefulness.py | 31 +++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/demonstrations/tutorial_resourcefulness.py b/demonstrations/tutorial_resourcefulness.py index b718132855..d02240fe70 100644 --- a/demonstrations/tutorial_resourcefulness.py +++ b/demonstrations/tutorial_resourcefulness.py @@ -77,20 +77,18 @@ def f_hat(k): projection = [ f(x) * np.exp(-2 * np.pi * 1j * k * x / N)/np.sqrt(N) for x in range(N)] return np.sum(projection) -def plot(f, f_hat): - fig, (ax1, ax2) = plt.subplots(2, 1) - ax1.bar(range(N), [np.real(f(x)) for x in range(N)], color='dimgray') # casting to real is needed in case we perform an inverse FT - ax1.set_title(f"function f") +fig, (ax1, ax2) = plt.subplots(2, 1) +ax1.bar(range(N), [np.real(f(x)) for x in range(N)], color='dimgray') # casting to real is needed in case we perform an inverse FT +ax1.set_title(f"function f") - ax2.bar(np.array(range(N))+0.05, [np.imag(f_hat(x)) for x in range(N)], color='lightpink', label="imaginary part") - ax2.bar(range(N), [np.real(f_hat(k)) for k in range(N)], color='dimgray', label="real part") - ax2.set_title("Fourier coefficients") - plt.legend() - plt.tight_layout() - plt.show() +ax2.bar(np.array(range(N))+0.05, [np.imag(f_hat(x)) for x in range(N)], color='lightpink', label="imaginary part") +ax2.bar(range(N), [np.real(f_hat(k)) for k in range(N)], color='dimgray', label="real part") +ax2.set_title("Fourier coefficients") +plt.legend() +plt.tight_layout() +plt.show() -plot(f, f_hat) ###################################################################### # Now, what kind of resource are we dealing with here? In other words, what @@ -114,7 +112,16 @@ def g(x): projection = [g_hat(k) * np.exp(2 * np.pi * 1j * k * x / N)/np.sqrt(N) for k in range(N)] return np.sum(projection) -plot(g, g_hat) +fig, (ax1, ax2) = plt.subplots(2, 1) +ax1.bar(range(N), [np.real(g(x)) for x in range(N)], color='dimgray') +ax1.set_title(f"function g") + +ax2.bar(range(N), [g_hat(k) for k in range(N)], color='dimgray', label="real part") +ax2.set_title("Fourier coefficients") + +plt.legend() +plt.tight_layout() +plt.show() ###################################################################### # Well, the function is constant. This makes sense, because we know that the decay of the Fourier coefficient From 509d04f37284a4509aeda6d713cd8d4f2da6f02b Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 13 Aug 2025 11:09:16 +0200 Subject: [PATCH 14/33] backup --- demonstrations/tutorial_resourcefulness.py | 16 +++- .../tutorial_resourcefulness/demo.py | 88 ++++++++----------- 2 files changed, 49 insertions(+), 55 deletions(-) diff --git a/demonstrations/tutorial_resourcefulness.py b/demonstrations/tutorial_resourcefulness.py index d02240fe70..07473aa0ab 100644 --- a/demonstrations/tutorial_resourcefulness.py +++ b/demonstrations/tutorial_resourcefulness.py @@ -351,11 +351,12 @@ def g(x): # and this time there is a basis transformation that block-diagonalises all matrices in the representation. # This can be done by computing the eigendecomposition of an arbitrary linear combination of a set of matrices in the representation. # +rng = np.random.default_rng(42) matrices = [] for i in range(10): # create n haar random single-qubit unitaries - Us = [unitary_group.rvs(dim=2) for _ in range(n)] + Us = [unitary_group.rvs(dim=2, random_state=rng) for _ in range(n)] # compose them into a non-entangling n-qubit unitary U = reduce(lambda A, B: np.kron(A, B), Us) # Vectorise U @@ -363,7 +364,7 @@ def g(x): matrices.append(U_vec) # Create a random linear combination of the matrices -alphas = np.random.randn(len(matrices)) + 1j * np.random.randn(len(matrices)) +alphas = rng.random(len(matrices)) + 1j * rng.random(len(matrices)) M_combo = sum(a * M for a, M in zip(alphas, matrices)) # Eigendecompose the linear combination @@ -379,7 +380,7 @@ def g(x): ###################################################################### # B does not look block diagonal. What happened here? -# Well, it is block-diagonal, but we have to reorder the columns and rows of the final matrix. +# Well, it *is* block-diagonal, but we have to reorder the columns and rows of the final matrix. # This takes a bit of pain, encapsulated in the following function: # @@ -461,6 +462,15 @@ def key_and_zeros(vec): P_row, P_col = group_rows_cols_by_sparsity(B) Q = Q @ P_col +np.set_printoptions( + formatter={'float': lambda x: f"{x:5.2g}"}, + linewidth=200, # default is 75; increase to fit your array + threshold=10000 # so it doesn’t summarize large arrays +) + +Qinv = np.linalg.inv(Q) +B = Qinv @ matrices[0] @ Q +print("blockdiag", B) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 861672fc5b..d25e162f6d 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -437,13 +437,16 @@ def haar_product_state(n: int): # 3. **Identify a group representation**. This "adjoint action" is indeed a valid representation of # :math:`G = SU(2) x SU(2) ... x SU(2)`, called the *defining representation*. However, it is a different one from before, # and this time there is a basis transformation that properly block-diagonalises all matrices in the representation. +# +# 4. **Find the basis that block-diagonalises the representation**. # To find this transformation we compute the eigendecomposition of an arbitrary linear combination of a set of matrices in the representation. # +rng = np.random.default_rng(42) Uvecs = [] for i in range(10): # create n haar random single-qubit unitaries - Ujs = [unitary_group.rvs(dim=2) for _ in range(n)] + Ujs = [unitary_group.rvs(dim=2, random_state=rng) for _ in range(n)] # compose them into a non-entangling n-qubit unitary U = reduce(lambda A, B: np.kron(A, B), Ujs) # Vectorise U @@ -451,7 +454,7 @@ def haar_product_state(n: int): Uvecs.append(Uvec) # Create a random linear combination of the matrices -alphas = np.random.randn(len(Uvecs)) + 1j * np.random.randn(len(Uvecs)) +alphas = rng.random(len(Uvecs)) + 1j * rng.random(len(Uvecs)) M_combo = sum(a * M for a, M in zip(alphas, Uvecs)) # Eigendecompose the linear combination @@ -475,16 +478,16 @@ def haar_product_state(n: int): print(Uvec_diag) ###################################################################### -# But `Uvec_diag` does not look block diagonal. What happened here? -# Well, it is block-diagonal, but we have to reorder the columns and rows of the final matrix to make this visible. +# But ``Uvec_diag`` does not look block diagonal. What happened here? +# Well, it *is* block-diagonal, but we have to reorder the columns and rows of the final matrix to make this visible. # This takes a bit of pain, encapsulated in the following function: # from collections import OrderedDict -def group_rows_cols_by_sparsity(mat, tol=0): +def group_rows_cols_by_sparsity(B, tol=0): """ - Given a matrix B, this function: + Given a binary or general matrix C, this function: 1. Groups identical rows and columns. 2. Orders these groups by sparsity (most zeros first). 3. Returns the permuted matrix C2, and the row & column permutation @@ -492,7 +495,7 @@ def group_rows_cols_by_sparsity(mat, tol=0): Parameters ---------- - mat : ndarray, shape (n, m) + B : ndarray, shape (n, m) Input matrix. Returns @@ -503,9 +506,11 @@ def group_rows_cols_by_sparsity(mat, tol=0): Column permutation matrix. """ # Compute boolean mask where |B| >= tol - mask = np.abs(mat) >= tol + mask = np.abs(B) >= 1e-8 # Convert boolean mask to integer (False→0, True→1) C = mask.astype(int) + # order by sparsity + n, m = C.shape # Helper to get a key tuple and zero count for a vector @@ -556,68 +561,46 @@ def key_and_zeros(vec): P_row, P_col = group_rows_cols_by_sparsity(Uvec_diag) -Uvec_diag = P_row @ Uvec_diag @ P_col +# reorder the block-diagonalising matrices +Q = Q @ P_col +Qinv = P_row @ Qinv + +Uvec_diag = Qinv @ Uvec @ Q print("\n\n ---------------") print(Uvec_diag) ###################################################################### -# The reordering made the block structure visible. You can check that any vectorised non-entangling matrix `Uvec` -# has the same block structure if we change the basis and reorder via `P_row @ Qinv @ Uvec @ Q @ P_col`. +# The reordering made the block structure visible. You can check that now any vectorised non-entangling matrix ``Uvec`` +# has the same block structure if we change the basis and reorder via ``Qinv @ Uvec @ Q``. # # The next step is to -# 5. **Find the basis that block-diagonalises the representation**. +# 5. **Find a basis for each subspace**. +# We have everything ready to do this: the basis of a subspace are just the rows of ``Q`` that correspond to the row/column +# indices of a given block. To compute the inner product of a given vectorised state ``v`` with +# these basis vectors, we can just compute ``v_diag = Q v``, and hence move into the basis that exposes the subspaces. +# The subsystem purities are just the sums of absolute squares of ... # -# [TRY: Rotate a vector into this basis and only summarise the entries] # -_pauli_map = { - 'I': np.array([[1,0],[0,1]], dtype=complex), - 'X': np.array([[0,1],[1,0]], dtype=complex), - 'Y': np.array([[0,-1j],[1j,0]],dtype=complex), - 'Z': np.array([[1,0],[0,-1]], dtype=complex), -} -factors = [_pauli_map[ch] for ch in 'IX'] -rho_P = functools.reduce(lambda A, B: np.kron(A, B), factors) -rho_P = rho_P.flatten(order="F") -rho_P = Q @ rho_P -print("HERE", rho_P) - # vectorise the states we defined earlier states_vec = [state.flatten(order='F') for state in states] - - # change into the block-diagonal basis -states_vec = [Q @ state_vec for state_vec in states_vec] -purities = [[np.vdot(state_vec[0], state_vec[0]), - np.vdot(state_vec[1:4], state_vec[1:4]), - np.vdot(state_vec[4:8], state_vec[4:8]), - np.vdot(state_vec[8:16], state_vec[8:16]), - ] for state_vec in states_vec ] - - - -# Grab default color cycle -colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] - -# Create two vertically aligned subplots sharing the x-axis -fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) +states_vec_diag = [Q @ state_vec for state_vec in states_vec] +purities = [] +for v in states_vec_diag: + purity = [np.vdot(v[0], v[0].conj()), np.vdot(v[1:4], v[1:4].conj()), np.vdot(v[4:7], v[4:7].conj()), np.vdot(v[7:16], v[7:16].conj())] + purities.append(purity) +print(purities) for i, data in enumerate(purities): - color = colors[i % len(colors)] - ax1.plot(data, label=f'{i}', color=color) - ax2.plot(np.cumsum(data), label=f'{i}', color=color) - -ax1.set_ylabel('Purity') -ax2.set_ylabel('Cumulative Purity') -ax2.set_xlabel('Module weight') - -ax1.legend(loc='upper left') -ax2.legend(loc='upper left') + plt.plot(data, label=f'{labels[i]}') +plt.ylabel('Purity') +plt.xlabel('Module weight') +plt.legend(loc='upper left') plt.tight_layout() - plt.show() @@ -738,6 +721,7 @@ def compute_me_purities(op): purities = [compute_me_purities(op) for op in states] +print(purities) # Grab default color cycle colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] From 3f484d1bc84fd8749a09ea5182b3f980f85da44b Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 13 Aug 2025 12:15:06 +0200 Subject: [PATCH 15/33] first draft, plot not working --- .../tutorial_resourcefulness/demo.py | 325 +++++++----------- 1 file changed, 130 insertions(+), 195 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index d25e162f6d..40184dc4b8 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -19,7 +19,7 @@ It turns out [#Bermejo_Braccia]_ that the resourcefulness of quantum states can be investigated with tools from *generalised Fourier analysis*. "Fourier analysis" here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case is not a function over :mathbb:`R` or :mathbb:`Z`, but a quantum state. "Generalised" indicates that we don't use the -standard Fourier transform, but its group-theoretic generalisations [LINK TO RELATED DEMO]. +standard Fourier transform, but a generalisation of its group theoretic definition. [#Bermejo_Braccia]_ suggest to compute a quantity that they call the **Generalised Fourier Decomposition (GFD) Purity**, and use it as a "footprint" of a state's resource profile. When using the standard Fourier transform, the GFD purities are just the absolute squares of the normal Fourier coefficients, which is also known as the *power spectrum*. @@ -159,7 +159,7 @@ def g(x): plt.show() ###################################################################### -# Well, the function is constant. This makes sense, because we know that the decay of the Fourier coefficient +# The function is constant. This makes sense, because we know that the decay of the Fourier coefficient # is related to the number of times a function is differentiable, which in turn is the technical definition of "smoothness". A # constant function is maximally smooth -- it does not wiggle at all. In other words, # the resource of the standard Fourier transform is smoothness, and a resource-free function is constant! @@ -172,8 +172,8 @@ def g(x): # Computing the GFD Purities for the resource of smoothness was very simple. We will now make it more complicated, # in order to set up the machinery that calculates the Purities for more general cases like quantum entanglement. # -# Find free vectors -# ***************** +# 1. Find free vectors +# ******************** # The first step is to find "free vectors". So far we dealt with a discrete function :math:`f(0), ..., f(N-1)`, # but we can easily write it as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. As argued above, # the set of resource-free vectors corresponds to constant functions, :math:`f(0) = ... = f(N-1)`. @@ -181,14 +181,15 @@ def g(x): f_vec = np.array([f(x) for x in range(N)]) -# Find free unitary transformations -# ********************************* +###################################################################### +# 2. Find free unitary transformations +# ************************************ # Next, we need to identify a set of unitary matrices that does not change the # "constantness" of the vector. In the case above this is given by the permutation matrices, which # swap the entries of the vector but do not change any of them. # -# Identify a group representation -# ******************************* +# 3. Identify a group representation +# ********************************** # This is the most difficult step, which sometimes requires a lot of group-theoretic knowledge. Essentially, we have to associate # the unitary transformations, or a subset of them, with a *unitary group representation*. A representation is a # matrix-valued function :math:`R(g), g \in G` on a group with the property that :math:`R(g) R(g') = R(gg')`, @@ -218,12 +219,12 @@ def g(x): ###################################################################### -# Find the basis that block-diagonalises the representation -# ********************************************************* +# 4. Find the basis that block-diagonalises the representation +# ************************************************************ # This was quite a bit of group jargon, but for a good reason: We are now guaranteed that there is # a basis change that block-diagonalises *all* circulant permutation matrices. If you know group theory, # then you will recognise that the blocks reveal the *irreducible representations* of the group. -# What is important is that the blocks form different "irreducible" subspaces which we need to compute +# In more general linear algebra language, the blocks form different *irreducible subspaces* which we need to compute # the GFD Purities in the next step. # # In our toy case, and for all *Abelian* groups, the blocks are 1-dimensional, and the basis @@ -251,7 +252,7 @@ def g(x): # change the circulant matrix into the Fourier basis using F np.set_printoptions(precision=2, suppress=True) -P_F = F @ P @ np.linalg.inv(F) +P_F = np.linalg.inv(F) @ P @ F # check if the resulting matrix is diagonal # trick: remove diagonal and make sure the remainder is zero @@ -261,41 +262,33 @@ def g(x): # In fact, the Fourier transform is sometimes *defined* as the basis change that diagonalises the regular representation! # For other representations, however, we need other transforms. # -# Project into the basis -# ********************** -# We now have a very different perspective on the power spectrum. We wrote a function as a vector, -# and changed into the basis that diagonalises circulant matrices. For every 1-d block, we took the corresponding -# coordinate in the new vector (which we know are the Fourier coefficients) and computed its absolute square. -# -# .. math:: \mathcal{P}(\vec{f}) = |\hat{f}|^2. +# 5. Find a basis for each subspace +# ********************************* +# The rows of the basis transformation matrix, here ``F``, with the row/column indices of a block in ``P_F`` form a basis +# for the subspace associated with each block. In [#Bermejo_Braccia]_ these basis vectors where +# called :math:`w^{(i)}_{\alpha, j}`, where :math:`\alpha` marks the subspace and :math:`j` the copy or multiplicity +# of a block on the diagonal (a technical detail we will not explore further here). # -# More generally, if -# we have different blocks labeled by :math:`\alpha` and their copies by the multiplicity factor :math:`j`, -# then each block marks a subspace spanned by a basis :math:`\{w^{(i)}_{\alpha, j}\} -# The GFD purity is the length of a projection of a new vector :math:`v \in V` onto one of these subspaces, +# 6. Compute the GFD Purities +# *************************** +# We can now compute the GFD Purities according to Eq. (5) in [#Bermejo_Braccia]_. # # .. math:: # \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. # # - -###################################################################### -# To recap, we saw that by mobilising group theory and linear algebra, -# the standard power spectrum of a function over integers can be interpreted as the "GFD Purities" -# for the resource of "smoothness". Armed with this recipe, we can now try to analyse entanglement as a resource. -# We will first try to work with Dirac vectors as our vectors, and unitary matrices as the representation, -# but see very quickly that this does not lead to rich enough "footprints". -# Instead, we switch to density matrices and their unitary evolution (which need to be vectorised in order to -# use numerical linear algebra tools). This will allow us to find "power spectra" or GFD Purities of states, -# indicating how entangled they are. +# We now have a very different perspective on the power spectrum :math:`|\hat{f}|^2`: It is a projection of the function +# :math:`f` into irreducible subspaces revealed by moving into the Fourier basis. The power spectrum, as +# is widely known, is a footprint for the smoothness of the function. But, having chosen a rather general +# group-theoretic angle, can we do the same for other groups and vector spaces -- for example those +# relevant to investigate resources of quantum states? # ###################################################################### # Fourier analysis of entanglement # -------------------------------- # -# Now that we have a grasp of how standard Fourier transforms can measure the resource of "smoothness" of a classical function, -# let's apply the same kind of reasoning to the most popular resource of quantum states: multipartite entanglement. +# Let us try to apply the same kind of reasoning to the most popular resource of quantum states: multipartite entanglement. # We can think of multipartite entanglement as a resource of the state of a quantum system that measures how "wiggly" # the correlations between its constituents are. # While this is a general statement, we will restrict our attention to systems made of our favourite quantum objects, qubits. @@ -303,26 +296,22 @@ def g(x): # quantum states with simpler entanglement structures, or no entanglement at all like in the case of product states, # have simpler generalized Fourier spectra, where most of their purity resides in the lower-order "irreps" # (that we recall is short for irreducible representations, but you can think of them as generalized frequencies). -# Here are a few examples that we will use below. +# Here are a few examples that we will use below: +# * **Product state**: +# * **GHZ state**: A highly structured, maximally entangled state. It behaves like a quantum analog of a +# high-frequency oscillation, having purity concentrated mostly in the highest-order irreps, creating a clear quantum fingerprint. +# * **W state**: Moderately entangled, somewhat between the GHZ and product states, with a broader yet smoother distribution across the irreps. +# * **Random (Haar) state**: Highly complex but without structured entanglement patterns, its purity is distributed more evenly across the spectrum.# In the code examples we'll show shortly, you'll see precisely these patterns emerge. # import math import functools -def ghz_state(n: int): - """The |GHZ_n⟩ state vector for *n* qubits is famous for having maximal entanglement.""" - psi = np.zeros(2 ** n) - psi[0] = 1 / math.sqrt(2) - psi[-1] = 1 / math.sqrt(2) - return psi - - -def w_state(n: int): - """The |W_n⟩ state vector for *n* qubits ....""" - psi = np.zeros(2 ** n) - for q in range(n): - psi[2 ** q] = 1 / math.sqrt(n) - return psi +def haar_product_state(n: int): + """A "smooth" state with no entanglement. It shows high purity only in the lowest-order GFD Purity coefficients, + similar to a constant function in classical Fourier analysis..""" + states = [haar_state(1) for _ in range(n)] + return functools.reduce(lambda A, B: np.kron(A, B), states) def haar_state(n: int): @@ -337,17 +326,28 @@ def haar_state(n: int): return (Q[:, 0] * phase) -def haar_product_state(n: int): - """A Haar random tensor product state of *n* qubits is maximally unentangled.""" - states = [haar_state(1) for _ in range(n)] - return functools.reduce(lambda A, B: np.kron(A, B), states) +def w_state(n: int): + """Moderately entangled, somewhat between the GHZ and product states, + with a broader yet smoother distribution across the irreps""" + psi = np.zeros(2 ** n) + for q in range(n): + psi[2 ** q] = 1 / math.sqrt(n) + return psi +def ghz_state(n: int): + """A highly structured, maximally entangled state. It behaves like a quantum analog of a + high-frequency oscillation, having purity concentrated mostly in the highest-order coefficients.""" + psi = np.zeros(2 ** n) + psi[0] = 1 / math.sqrt(2) + psi[-1] = 1 / math.sqrt(2) + return psi + n = 2 -states = [haar_product_state(n), haar_state(n), ghz_state(n), w_state(n)] +states = [haar_product_state(n), haar_state(n), w_state(n), ghz_state(n)] # move to density matrices states = [np.outer(state.conj(), state) for state in states] -labels = [ "Product", "Haar", "GHZ", "W"] +labels = [ "Product", "Haar", "W", "GHZ"] ##################################################################### # On the flip side, highly entangled states spread their purity across more and higher-order (i.e., larger dimensional) irreps, @@ -364,6 +364,7 @@ def haar_product_state(n: int): # 3. **Identify a group representation**. It turns out that non-entangling unitaries are the standard representation # of the group :math:`G = SU(2) x SU(2) ... x SU(2)` for the vector space :math:`H`, the Hilbert space of the state vectors. # Again, this implies that we can find a basis of the Hilbert space, where any :math:`U` is simultaneously block-diagonal. +# 4. **Find the basis that block-diagonalises the representation**. # # Let's stop here for now, and try to block-diagonalise one of the non-entangling unitaries. We can use an # eigenvalue decomposition for this. @@ -395,7 +396,7 @@ def haar_product_state(n: int): # the (standard) representation of non-entangling unitary matrices. # After all, what's the first symptom of entanglement in a quantum state? # You guessed right again, the reduced density matrix of some subsystem is mixed! -# Wouldn't it make more sense then to study the multipartite entanglement form the point of view of density matrices? +# Wouldn't it make more sense to study the multipartite entanglement form the point of view of density matrices? # It turns out that moving into the space :math:`B(H)` of bounded linear operators in which the density matrices live leads to # a much more nuanced Fourier spectrum. # @@ -414,8 +415,9 @@ def haar_product_state(n: int): rho_vec = rho.flatten(order='F') ###################################################################### -# The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` -# where each :math:`\rho_j` is a single-qubit state. We only have to write them in flattened form. +# The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` +# where each :math:`\rho_j` is a single-qubit state. We only have to write them in flattened form. +# # 2. **Find free unitary transformations**. The free operations are still given by non-entangling unitaries, but # of course, they act on density matrices via :math:`\rho' = U \rho U^{\dagger}`. # We can also vectorise this operation by defining the matrix :math:`U_{\mathrm{vec}}(g) = U \otimes U^*`. @@ -439,7 +441,8 @@ def haar_product_state(n: int): # and this time there is a basis transformation that properly block-diagonalises all matrices in the representation. # # 4. **Find the basis that block-diagonalises the representation**. -# To find this transformation we compute the eigendecomposition of an arbitrary linear combination of a set of matrices in the representation. +# To find this basis we compute the eigendecomposition of an arbitrary linear combination +# of a set of matrices in the representation. # rng = np.random.default_rng(42) @@ -567,7 +570,6 @@ def key_and_zeros(vec): Uvec_diag = Qinv @ Uvec @ Q -print("\n\n ---------------") print(Uvec_diag) ###################################################################### @@ -577,21 +579,21 @@ def key_and_zeros(vec): # The next step is to # 5. **Find a basis for each subspace**. # We have everything ready to do this: the basis of a subspace are just the rows of ``Q`` that correspond to the row/column -# indices of a given block. To compute the inner product of a given vectorised state ``v`` with -# these basis vectors, we can just compute ``v_diag = Q v``, and hence move into the basis that exposes the subspaces. -# The subsystem purities are just the sums of absolute squares of ... -# +# indices of a given block. Hence, the GFD Purity calculation can be performed by changing a new vector ``v`` into the +# basis we just identified by applying ``v_diag = Q v``, and taking the sum of absolute squares of those entries in ``v_diag`` +# that correspond to one block or subspace. # + # vectorise the states we defined earlier states_vec = [state.flatten(order='F') for state in states] - -# change into the block-diagonal basis states_vec_diag = [Q @ state_vec for state_vec in states_vec] + purities = [] -for v in states_vec_diag: - purity = [np.vdot(v[0], v[0].conj()), np.vdot(v[1:4], v[1:4].conj()), np.vdot(v[4:7], v[4:7].conj()), np.vdot(v[7:16], v[7:16].conj())] - purities.append(purity) +for v in states_vec: + v = np.abs(v)**2 + purity_spectrum = [np.sum(v[0:1]), np.sum(v[1:4]), np.sum(v[4:7]), np.sum(v[7:16])] + purities.append(purity_spectrum) print(purities) for i, data in enumerate(purities): @@ -603,20 +605,28 @@ def key_and_zeros(vec): plt.tight_layout() plt.show() +########################################################################### +# The fascinating aspect here is that while multipartite entanglement can get complex very quickly as we +# add qubits (due to the exponential increase in possible correlations), +# our generalized Fourier analysis still provides a clear, intuitive fingerprint of a state's entanglement structure. +# Even more intriguingly, just as smoothness guides how we compress classical +# signals (by discarding higher-order frequencies), the entanglement fingerprint suggests ways we might compress quantum states, +# discarding information in higher-order purities to simplify quantum simulations and measurements. +# In short, generalized Fourier transforms allow us to understand quantum complexity, much like classical Fourier transforms give insight into smoothness. +# By reading a state's quantum Fourier fingerprint, we gain a clear window into the subtle quantum world of multipartite entanglement. +# ########################################################################### # Bonus section # -------------- # -# But what basis have we actually changed into? It turns out that `Q` changes into the Pauli basis. We could have constructed -# `Q` from first principles: +# If your head is not spinning yet, let's ask a final, and rather instructive, question. +# What basis have we actually changed into in the above example of multi-partite entanglement? +# It turns out that `Q` changes into the Pauli basis! # - -import functools import itertools -# single‑qubit Paulis _pauli_map = { 'I': np.array([[1,0],[0,1]], dtype=complex), 'X': np.array([[0,1],[1,0]], dtype=complex), @@ -624,10 +634,8 @@ def key_and_zeros(vec): 'Z': np.array([[1,0],[0,-1]], dtype=complex), } + def pauli_basis(n): - """ - Generates the basis of Pauli operators, and orders it by appearence in the isotypical decomp of Times_i SU(2). - """ all_strs = [''.join(s) for s in itertools.product('IXYZ', repeat=n)] sorted_strs = sorted(all_strs, key=lambda s: (n-s.count('I'), s)) norm = np.sqrt(2**n) @@ -640,136 +648,63 @@ def pauli_basis(n): return sorted_strs, B +# let's create the vectorized basis of Pauli matrices +basis,B = pauli_basis(n) +# now let's take the first basis vector, the one corresponding to the smallest block +# and project it onto paulis -# compute the basis change matrix -basis, B = pauli_basis(n) -# apply basis change -Uvec_rot = B.conj().T @ Uvec @ B - -# make sure the imaginary part is zero, so we don't have to print it -# (this is a property of the Pauli basis) -assert np.isclose(np.sum(Uvec_rot.imag), 0) - -# print the real part -print(np.round(Uvec_rot.real, 2)) +v = Q[:,0] +v_pauli_coeffs = [np.dot(v, pv).real for pv in B.T] +print("The basis corresponding to the 1x1 block has nonzero overlap with the following Pauli basis elements:") +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) - -###################################################################### -# The different blocks correspond to subspaces spanned by Pauli words with different structures: -# -# * The first block of size 1x1 corresponds to a subspace spanned by the Pauli word operator :math:`\mathbm{1} \otimes \mathbm{1}`. -# * The second two blocks of size 4x4 corresponds to a subspace spanned by to Pauli word operators :math:`\mathbm{1} \otimes P_2` and :math:`P_1 \otimes \mathbm{1}`, where -# :math:`P_1, P_2 \in \{X, Y, Z\}`. -# * The third block of size XxX corresponds to a subspace spanned by Pauli word operators of the form :math:`P_1 \otimes P_2`. -# -# In other words, we didn't need to vectorise everything and use linear algebra tools to compute the GFD purities. We could have -# simply computed the inner product with the rigth set of Pauli operators in :math:`B(H)`: +########################################################################### +# It's just the Identity! This is not a surprise, since any unitary maps the identity to itself. +# Let's keep going, what about a vector in the 3x3 blocks? # -def generate_pauli_strings(n: int): - """ - Generate all length‑n strings over the Pauli alphabet ['I','X','Y','Z']. - Returns a list of 4**n strings, e.g. ['IIX', 'IXZ', …]. - """ - return [''.join(p) for p in itertools.product('IXYZ', repeat=n)] - - -def pauli_string_to_matrix(pauli_str: str): - """ - Convert a Pauli string (e.g. 'XIY') to its full 2^n × 2^n matrix. - """ - mats = [_pauli_map[s] for s in pauli_str] - return functools.reduce(lambda A, B: np.kron(A, B), mats) - - -# function to project into the modules -def compute_me_purities(op): - """ - Computes GFD purities of op (assumed to be np.matrix) - by explicitly computing overlaps with Paulis - """ - - if op.ndim == 1: - # state vector - is_vector = True - elif op.ndim == 2 and op.shape[0] == op.shape[1]: - # density/operator - is_vector = False - else: - raise ValueError("`op` must be either a 1D state vector or a square matrix") - - d = op.shape[0] - n = int(np.log2(d)) - - basis = generate_pauli_strings(n) - purities = np.zeros(n + 1) - for belem in basis: - k = n - belem.count('I') - P = pauli_string_to_matrix(belem) - - if is_vector: - ovp = op.conj() @ (P @ op) - else: - ovp = np.trace(op @ P) - - #assert ovp.imag < 1e-10 - purities[k] += (ovp.real) ** 2 - - return purities / (2 ** n) +for idx in range(1, 4): + v = Q[:, idx] + v_pauli_coeffs = [np.dot(v, pv).real for pv in B.T] +print("The basis corresponding to the first 3x3 block has nonzero overlap with the following Pauli basis elements:") -purities = [compute_me_purities(op) for op in states] -print(purities) - -# Grab default color cycle -colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] - -# Create two vertically aligned subplots sharing the x-axis -fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) - -for i, data in enumerate(purities): - color = colors[i % len(colors)] - ax1.plot(data, label=f'{labels[i]}', color=color) - ax2.plot(np.cumsum(data), label=f'{labels[i]}', color=color) +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) -ax1.set_ylabel('Purity') -ax2.set_ylabel('Cumulative Purity') -ax2.set_xlabel('Module weight') - -ax1.legend(loc='upper left') -ax2.legend(loc='upper left') - -plt.tight_layout() -plt.savefig("/home/maria/Desktop/purities2.png") -plt.show() - -""" -# To illustrate this, we'll consider a few key examples: -# * **Product state**: A "smooth" state with no entanglement. It shows high purity only in the lowest-order irreps, similar to a constant function in classical Fourier analysis. -# * **GHZ state**: A highly structured, maximally entangled state. It behaves like a quantum analog of a high-frequency oscillation, having purity concentrated mostly in the highest-order irreps, creating a clear quantum fingerprint. -# * **W state**: Moderately entangled, somewhat between the GHZ and product states, with a broader yet smoother distribution across the irreps. -# * **Random (Haar) state**: Highly complex but without structured entanglement patterns, its purity is distributed more evenly across the spectrum.# In the code examples we'll show shortly, you'll see precisely these patterns emerge. -# For each state, we compute the purity distribution across generalized Fourier modes (irreps), illustrating exactly how entanglement complexity relates to spectral structure. - -# # Here, show purity plots for these states as computed by the provided code - -# The fascinating aspect here is that while multipartite entanglement can get complex very quickly as we add qubits (due to the exponential increase in possible correlations), -# our generalized Fourier analysis still provides a clear, intuitive fingerprint of a state's entanglement structure. -# Even more intriguingly, just as smoothness guides how we compress classical signals (by discarding higher-order frequencies), the entanglement fingerprint suggests ways we might compress quantum states, -# discarding information in higher-order irreps to simplify quantum simulations and measurements. -# In short, generalized Fourier transforms allow us to understand quantum complexity, much like classical Fourier transforms give insight into smoothness. -# By reading a state's quantum Fourier fingerprint, we gain a clear window into the subtle quantum world of multipartite entanglement. - - -""" +for idx in range(4, 7): + v = Q[:, idx] + v_pauli_coeffs = [np.dot(v, pv).real for pv in B.T] +print("The basis corresponding to the second 3x3 block has nonzero overlap with the following Pauli basis elements:") +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) +########################################################################### +# This block corresponds to operators acting non trivially on a single qubit! +# We can verify that the last block corresponds to those fully supported over our two qubits +for idx in range(7, 16): + v = Q[:, idx] + v_pauli_coeffs = [np.dot(v, pv).real for pv in B.T] +# we can ask over which paulis v is supported +print("The basis corresponding to the last block has nonzero overlap with the following Pauli basis elements:") +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) +########################################################################### +# Now that we realized that the blocks correspond to all the possible supports of operators over the Hilbert spaces of the different qubits, +# we can also see why this is the case. Remember that the representation :math:`U = R(g)` conjugates an operator :math:`\rho` +# by a tensor product of single-qubit unitary operators. [TODO: finish this thought with a bit more rigour] # # References # ---------- @@ -782,6 +717,6 @@ def compute_me_purities(op): ###################################################################### # -# About the author -# ---------------- +# About the authors +# ----------------- # From 58335c070c4f0142171972f88171c178a0fb1d70 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 13 Aug 2025 15:34:39 +0200 Subject: [PATCH 16/33] trigger ci From 12fc9fb54db6299f581641a75cb76216dd63d4b1 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Tue, 19 Aug 2025 14:26:30 +0200 Subject: [PATCH 17/33] fixes from Paolo --- .../tutorial_resourcefulness/demo.py | 392 +++++++++--------- 1 file changed, 188 insertions(+), 204 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index d25e162f6d..ad91215a27 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -19,7 +19,7 @@ It turns out [#Bermejo_Braccia]_ that the resourcefulness of quantum states can be investigated with tools from *generalised Fourier analysis*. "Fourier analysis" here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case is not a function over :mathbb:`R` or :mathbb:`Z`, but a quantum state. "Generalised" indicates that we don't use the -standard Fourier transform, but its group-theoretic generalisations [LINK TO RELATED DEMO]. +standard Fourier transform, but a generalisation of its group theoretic definition. [#Bermejo_Braccia]_ suggest to compute a quantity that they call the **Generalised Fourier Decomposition (GFD) Purity**, and use it as a "footprint" of a state's resource profile. When using the standard Fourier transform, the GFD purities are just the absolute squares of the normal Fourier coefficients, which is also known as the *power spectrum*. @@ -159,7 +159,7 @@ def g(x): plt.show() ###################################################################### -# Well, the function is constant. This makes sense, because we know that the decay of the Fourier coefficient +# The function is constant. This makes sense, because we know that the decay of the Fourier coefficient # is related to the number of times a function is differentiable, which in turn is the technical definition of "smoothness". A # constant function is maximally smooth -- it does not wiggle at all. In other words, # the resource of the standard Fourier transform is smoothness, and a resource-free function is constant! @@ -172,8 +172,8 @@ def g(x): # Computing the GFD Purities for the resource of smoothness was very simple. We will now make it more complicated, # in order to set up the machinery that calculates the Purities for more general cases like quantum entanglement. # -# Find free vectors -# ***************** +# 1. Find free vectors +# ******************** # The first step is to find "free vectors". So far we dealt with a discrete function :math:`f(0), ..., f(N-1)`, # but we can easily write it as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. As argued above, # the set of resource-free vectors corresponds to constant functions, :math:`f(0) = ... = f(N-1)`. @@ -181,14 +181,15 @@ def g(x): f_vec = np.array([f(x) for x in range(N)]) -# Find free unitary transformations -# ********************************* +###################################################################### +# 2. Find free unitary transformations +# ************************************ # Next, we need to identify a set of unitary matrices that does not change the # "constantness" of the vector. In the case above this is given by the permutation matrices, which # swap the entries of the vector but do not change any of them. # -# Identify a group representation -# ******************************* +# 3. Identify a group representation +# ********************************** # This is the most difficult step, which sometimes requires a lot of group-theoretic knowledge. Essentially, we have to associate # the unitary transformations, or a subset of them, with a *unitary group representation*. A representation is a # matrix-valued function :math:`R(g), g \in G` on a group with the property that :math:`R(g) R(g') = R(gg')`, @@ -218,12 +219,12 @@ def g(x): ###################################################################### -# Find the basis that block-diagonalises the representation -# ********************************************************* +# 4. Find the basis that block-diagonalises the representation +# ************************************************************ # This was quite a bit of group jargon, but for a good reason: We are now guaranteed that there is # a basis change that block-diagonalises *all* circulant permutation matrices. If you know group theory, # then you will recognise that the blocks reveal the *irreducible representations* of the group. -# What is important is that the blocks form different "irreducible" subspaces which we need to compute +# In more general linear algebra language, the blocks form different *irreducible subspaces* which we need to compute # the GFD Purities in the next step. # # In our toy case, and for all *Abelian* groups, the blocks are 1-dimensional, and the basis @@ -251,7 +252,7 @@ def g(x): # change the circulant matrix into the Fourier basis using F np.set_printoptions(precision=2, suppress=True) -P_F = F @ P @ np.linalg.inv(F) +P_F = np.linalg.inv(F) @ P @ F # check if the resulting matrix is diagonal # trick: remove diagonal and make sure the remainder is zero @@ -261,41 +262,33 @@ def g(x): # In fact, the Fourier transform is sometimes *defined* as the basis change that diagonalises the regular representation! # For other representations, however, we need other transforms. # -# Project into the basis -# ********************** -# We now have a very different perspective on the power spectrum. We wrote a function as a vector, -# and changed into the basis that diagonalises circulant matrices. For every 1-d block, we took the corresponding -# coordinate in the new vector (which we know are the Fourier coefficients) and computed its absolute square. -# -# .. math:: \mathcal{P}(\vec{f}) = |\hat{f}|^2. +# 5. Find a basis for each subspace +# ********************************* +# The rows of the basis transformation matrix, here ``F``, with the row/column indices of a block in ``P_F`` form a basis +# for the subspace associated with each block. In [#Bermejo_Braccia]_ these basis vectors where +# called :math:`w^{(i)}_{\alpha, j}`, where :math:`\alpha` marks the subspace and :math:`j` the copy or multiplicity +# of a block on the diagonal (a technical detail we will not explore further here). # -# More generally, if -# we have different blocks labeled by :math:`\alpha` and their copies by the multiplicity factor :math:`j`, -# then each block marks a subspace spanned by a basis :math:`\{w^{(i)}_{\alpha, j}\} -# The GFD purity is the length of a projection of a new vector :math:`v \in V` onto one of these subspaces, +# 6. Compute the GFD Purities +# *************************** +# We can now compute the GFD Purities according to Eq. (5) in [#Bermejo_Braccia]_. # # .. math:: # \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. # # - -###################################################################### -# To recap, we saw that by mobilising group theory and linear algebra, -# the standard power spectrum of a function over integers can be interpreted as the "GFD Purities" -# for the resource of "smoothness". Armed with this recipe, we can now try to analyse entanglement as a resource. -# We will first try to work with Dirac vectors as our vectors, and unitary matrices as the representation, -# but see very quickly that this does not lead to rich enough "footprints". -# Instead, we switch to density matrices and their unitary evolution (which need to be vectorised in order to -# use numerical linear algebra tools). This will allow us to find "power spectra" or GFD Purities of states, -# indicating how entangled they are. +# We now have a very different perspective on the power spectrum :math:`|\hat{f}|^2`: It is a projection of the function +# :math:`f` into irreducible subspaces revealed by moving into the Fourier basis. The power spectrum, as +# is widely known, is a footprint for the smoothness of the function. But, having chosen a rather general +# group-theoretic angle, can we do the same for other groups and vector spaces -- for example those +# relevant to investigate resources of quantum states? # ###################################################################### # Fourier analysis of entanglement # -------------------------------- # -# Now that we have a grasp of how standard Fourier transforms can measure the resource of "smoothness" of a classical function, -# let's apply the same kind of reasoning to the most popular resource of quantum states: multipartite entanglement. +# Let us try to apply the same kind of reasoning to the most popular resource of quantum states: multipartite entanglement. # We can think of multipartite entanglement as a resource of the state of a quantum system that measures how "wiggly" # the correlations between its constituents are. # While this is a general statement, we will restrict our attention to systems made of our favourite quantum objects, qubits. @@ -303,26 +296,22 @@ def g(x): # quantum states with simpler entanglement structures, or no entanglement at all like in the case of product states, # have simpler generalized Fourier spectra, where most of their purity resides in the lower-order "irreps" # (that we recall is short for irreducible representations, but you can think of them as generalized frequencies). -# Here are a few examples that we will use below. +# Here are a few examples that we will use below: +# * **Product state**: +# * **GHZ state**: A highly structured, maximally entangled state. It behaves like a quantum analog of a +# high-frequency oscillation, having purity concentrated mostly in the highest-order irreps, creating a clear quantum fingerprint. +# * **W state**: Moderately entangled, somewhat between the GHZ and product states, with a broader yet smoother distribution across the irreps. +# * **Random (Haar) state**: Highly complex but without structured entanglement patterns, its purity is distributed more evenly across the spectrum.# In the code examples we'll show shortly, you'll see precisely these patterns emerge. # import math import functools -def ghz_state(n: int): - """The |GHZ_n⟩ state vector for *n* qubits is famous for having maximal entanglement.""" - psi = np.zeros(2 ** n) - psi[0] = 1 / math.sqrt(2) - psi[-1] = 1 / math.sqrt(2) - return psi - - -def w_state(n: int): - """The |W_n⟩ state vector for *n* qubits ....""" - psi = np.zeros(2 ** n) - for q in range(n): - psi[2 ** q] = 1 / math.sqrt(n) - return psi +def haar_product_state(n: int): + """A "smooth" state with no entanglement. It shows high purity only in the lowest-order GFD Purity coefficients, + similar to a constant function in classical Fourier analysis..""" + states = [haar_state(1) for _ in range(n)] + return functools.reduce(lambda A, B: np.kron(A, B), states) def haar_state(n: int): @@ -337,17 +326,28 @@ def haar_state(n: int): return (Q[:, 0] * phase) -def haar_product_state(n: int): - """A Haar random tensor product state of *n* qubits is maximally unentangled.""" - states = [haar_state(1) for _ in range(n)] - return functools.reduce(lambda A, B: np.kron(A, B), states) +def w_state(n: int): + """Moderately entangled, somewhat between the GHZ and product states, + with a broader yet smoother distribution across the irreps""" + psi = np.zeros(2 ** n) + for q in range(n): + psi[2 ** q] = 1 / math.sqrt(n) + return psi +def ghz_state(n: int): + """A highly structured, maximally entangled state. It behaves like a quantum analog of a + high-frequency oscillation, having purity concentrated mostly in the highest-order coefficients.""" + psi = np.zeros(2 ** n) + psi[0] = 1 / math.sqrt(2) + psi[-1] = 1 / math.sqrt(2) + return psi + n = 2 -states = [haar_product_state(n), haar_state(n), ghz_state(n), w_state(n)] +states = [haar_product_state(n), haar_state(n), w_state(n), ghz_state(n)] # move to density matrices -states = [np.outer(state.conj(), state) for state in states] -labels = [ "Product", "Haar", "GHZ", "W"] +states = [np.outer(state, state.conj()) for state in states] +labels = [ "Product", "Haar", "W", "GHZ"] ##################################################################### # On the flip side, highly entangled states spread their purity across more and higher-order (i.e., larger dimensional) irreps, @@ -364,6 +364,7 @@ def haar_product_state(n: int): # 3. **Identify a group representation**. It turns out that non-entangling unitaries are the standard representation # of the group :math:`G = SU(2) x SU(2) ... x SU(2)` for the vector space :math:`H`, the Hilbert space of the state vectors. # Again, this implies that we can find a basis of the Hilbert space, where any :math:`U` is simultaneously block-diagonal. +# 4. **Find the basis that block-diagonalises the representation**. # # Let's stop here for now, and try to block-diagonalise one of the non-entangling unitaries. We can use an # eigenvalue decomposition for this. @@ -395,7 +396,7 @@ def haar_product_state(n: int): # the (standard) representation of non-entangling unitary matrices. # After all, what's the first symptom of entanglement in a quantum state? # You guessed right again, the reduced density matrix of some subsystem is mixed! -# Wouldn't it make more sense then to study the multipartite entanglement form the point of view of density matrices? +# Wouldn't it make more sense to study the multipartite entanglement from the point of view of density matrices? # It turns out that moving into the space :math:`B(H)` of bounded linear operators in which the density matrices live leads to # a much more nuanced Fourier spectrum. # @@ -407,15 +408,17 @@ def haar_product_state(n: int): # # create a random quantum state -psi = np.random.rand(2**n) +psi = np.random.rand(2**n) + 1j*np.random.rand(2**n) +psi /= np.linalg.norm(psi) # construct the corresponding density matrix rho = np.outer(psi, psi.conj()) # vectorise it rho_vec = rho.flatten(order='F') ###################################################################### -# The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` -# where each :math:`\rho_j` is a single-qubit state. We only have to write them in flattened form. +# The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` +# where each :math:`\rho_j` is a single-qubit state. We only have to write them in flattened form. +# # 2. **Find free unitary transformations**. The free operations are still given by non-entangling unitaries, but # of course, they act on density matrices via :math:`\rho' = U \rho U^{\dagger}`. # We can also vectorise this operation by defining the matrix :math:`U_{\mathrm{vec}}(g) = U \otimes U^*`. @@ -429,7 +432,7 @@ def haar_product_state(n: int): # evolve the state above by U, using the vectorised objects rho_out_vec = Uvec @ rho_vec # reshape the result back into a density matrix -rho_out = np.reshape(rho_out_vec, newshape=(2**2, 2**2)) +rho_out = np.reshape(rho_out_vec, shape=(2**n, 2**n), order='F') # this is the same as the usual adjoint application of U print(np.allclose(rho_out, U @ rho @ U.conj().T )) @@ -439,7 +442,8 @@ def haar_product_state(n: int): # and this time there is a basis transformation that properly block-diagonalises all matrices in the representation. # # 4. **Find the basis that block-diagonalises the representation**. -# To find this transformation we compute the eigendecomposition of an arbitrary linear combination of a set of matrices in the representation. +# To find this basis we compute the eigendecomposition of an arbitrary linear combination +# of a set of matrices in the representation. # rng = np.random.default_rng(42) @@ -567,8 +571,7 @@ def key_and_zeros(vec): Uvec_diag = Qinv @ Uvec @ Q -print("\n\n ---------------") -print(Uvec_diag) +print(np.round(np.abs(Uvec_diag), 4)) ###################################################################### # The reordering made the block structure visible. You can check that now any vectorised non-entangling matrix ``Uvec`` @@ -577,46 +580,99 @@ def key_and_zeros(vec): # The next step is to # 5. **Find a basis for each subspace**. # We have everything ready to do this: the basis of a subspace are just the rows of ``Q`` that correspond to the row/column -# indices of a given block. To compute the inner product of a given vectorised state ``v`` with -# these basis vectors, we can just compute ``v_diag = Q v``, and hence move into the basis that exposes the subspaces. -# The subsystem purities are just the sums of absolute squares of ... -# +# indices of a given block. Hence, the GFD Purity calculation can be performed by changing a new vector ``v`` into the +# basis we just identified by applying ``v_diag = Q v``, and taking the sum of absolute squares of those entries in ``v_diag`` +# that correspond to one block or subspace. # + +# Notice that the similarity transform 'Qinv @ Uvec @ Q' needs not be implemented by a unitary change of basis! +# Indeed in genear Q will not be unitary, as we can quickly check by checking if any two of its columns are orthogonal +print("The inner product between the second and third column of Q is: ", np.dot(Q[:1],Q[:2]).item()) +# This is just becuase the eigenvecotrs of the random linear combiniation that we implemented before are only guaranteed to +# 'span' the blocks, not to be an orthonormal basis of them. +# However, rep-theory actually guarantees that we can indeed find such an orthonormal basis, or, in other words, that we can +# unitarize Q. One way is to polar decompose it + +def unitary_polar(Q): + U, s, Vh = np.linalg.svd(Q, full_matrices=False) + W = U @ Vh + return W + +W = unitary_polar(Q) +# It is indeed unitary +print(np.allclose(W @ W.conj().T, np.eye(len(W)))) +print(np.allclose(W.conj().T @ W, np.eye(len(W)))) + +# And yields the same block-decomposition +print(np.round(np.abs(W.conj().T @ Uvec @ W), 4)) + +# We can now proceed without the risk of messing up the normalization of our states + # vectorise the states we defined earlier states_vec = [state.flatten(order='F') for state in states] +states_vec_diag = [W.conj().T @ state_vec for state_vec in states_vec] -# change into the block-diagonal basis -states_vec_diag = [Q @ state_vec for state_vec in states_vec] purities = [] for v in states_vec_diag: - purity = [np.vdot(v[0], v[0].conj()), np.vdot(v[1:4], v[1:4].conj()), np.vdot(v[4:7], v[4:7].conj()), np.vdot(v[7:16], v[7:16].conj())] - purities.append(purity) + v = np.abs(v)**2 + purity_spectrum = [np.sum(v[0:1]), np.sum(v[1:4]), np.sum(v[4:7]), np.sum(v[7:16])] + purities.append(purity_spectrum) + +for data, label in zip(purities, labels): + print(f"{label} state purities: ") + for k, val in enumerate(data): + print(f" - Block {k+1}: ", val) + +# Notice that, as it should given the normalization of the states considered, the purities are a probability distribution. +# Indeed they are positive and we can check that they add up to 1, reconstructing the states' total purity. +for data, label in zip(purities, labels): + print(f"{label} state has total purity: ", np.sum(data).item()) + -print(purities) -for i, data in enumerate(purities): - plt.plot(data, label=f'{labels[i]}') +## We can further visualize the purities as done in [#Bermejo_Braccia]_, by aggregating them +# by the corresponding block's size + +agg_purities = [[p[0], p[1]+p[2], p[3]] for p in purities] + + +# Plot the purities and their comulative +fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) +for i, data in enumerate(agg_purities): + ax1.plot(data, label=f'{labels[i]}') + ax2.plot(np.cumsum(data), label=f'{labels[i]}') -plt.ylabel('Purity') -plt.xlabel('Module weight') -plt.legend(loc='upper left') +ax1.set_ylabel('Purity') +ax2.set_ylabel('Cumulative Purity') +ax2.set_xlabel('Module weight') +ax2.set_xticks(list(range(n+1))) +ax1.legend(loc='upper left') +ax2.legend(loc='upper left') plt.tight_layout() plt.show() +########################################################################### +# The fascinating aspect here is that while multipartite entanglement can get complex very quickly as we +# add qubits (due to the exponential increase in possible correlations), +# our generalized Fourier analysis still provides a clear, intuitive fingerprint of a state's entanglement structure. +# Even more intriguingly, just as smoothness guides how we compress classical +# signals (by discarding higher-order frequencies), the entanglement fingerprint suggests ways we might compress quantum states, +# discarding information in higher-order purities to simplify quantum simulations and measurements. +# In short, generalized Fourier transforms allow us to understand quantum complexity, much like classical Fourier transforms give insight into smoothness. +# By reading a state's quantum Fourier fingerprint, we gain a clear window into the subtle quantum world of multipartite entanglement. +# ########################################################################### # Bonus section # -------------- # -# But what basis have we actually changed into? It turns out that `Q` changes into the Pauli basis. We could have constructed -# `Q` from first principles: +# If your head is not spinning yet, let's ask a final, and rather instructive, question. +# What basis have we actually changed into in the above example of multi-partite entanglement? +# It turns out that `W` changes into the Pauli basis! # - -import functools import itertools -# single‑qubit Paulis _pauli_map = { 'I': np.array([[1,0],[0,1]], dtype=complex), 'X': np.array([[0,1],[1,0]], dtype=complex), @@ -624,10 +680,8 @@ def key_and_zeros(vec): 'Z': np.array([[1,0],[0,-1]], dtype=complex), } + def pauli_basis(n): - """ - Generates the basis of Pauli operators, and orders it by appearence in the isotypical decomp of Times_i SU(2). - """ all_strs = [''.join(s) for s in itertools.product('IXYZ', repeat=n)] sorted_strs = sorted(all_strs, key=lambda s: (n-s.count('I'), s)) norm = np.sqrt(2**n) @@ -640,136 +694,66 @@ def pauli_basis(n): return sorted_strs, B +# let's create the vectorized basis of Pauli matrices +basis,B = pauli_basis(n) +# now let's take the first basis vector, the one corresponding to the smallest block +# and project it onto paulis -# compute the basis change matrix -basis, B = pauli_basis(n) -# apply basis change -Uvec_rot = B.conj().T @ Uvec @ B +v = W[:,0] +v_pauli_coeffs = [np.abs(np.dot(v, pv)) for pv in B.T] -# make sure the imaginary part is zero, so we don't have to print it -# (this is a property of the Pauli basis) -assert np.isclose(np.sum(Uvec_rot.imag), 0) +print("The basis corresponding to the 1x1 block has nonzero overlap with the following Pauli basis elements:") +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) -# print the real part -print(np.round(Uvec_rot.real, 2)) - - - -###################################################################### -# The different blocks correspond to subspaces spanned by Pauli words with different structures: -# -# * The first block of size 1x1 corresponds to a subspace spanned by the Pauli word operator :math:`\mathbm{1} \otimes \mathbm{1}`. -# * The second two blocks of size 4x4 corresponds to a subspace spanned by to Pauli word operators :math:`\mathbm{1} \otimes P_2` and :math:`P_1 \otimes \mathbm{1}`, where -# :math:`P_1, P_2 \in \{X, Y, Z\}`. -# * The third block of size XxX corresponds to a subspace spanned by Pauli word operators of the form :math:`P_1 \otimes P_2`. -# -# In other words, we didn't need to vectorise everything and use linear algebra tools to compute the GFD purities. We could have -# simply computed the inner product with the rigth set of Pauli operators in :math:`B(H)`: +########################################################################### +# It's just the Identity! This is not a surprise, since any unitary maps the identity to itself. +# Let's keep going, what about a vector in the 3x3 blocks? # -def generate_pauli_strings(n: int): - """ - Generate all length‑n strings over the Pauli alphabet ['I','X','Y','Z']. - Returns a list of 4**n strings, e.g. ['IIX', 'IXZ', …]. - """ - return [''.join(p) for p in itertools.product('IXYZ', repeat=n)] - - -def pauli_string_to_matrix(pauli_str: str): - """ - Convert a Pauli string (e.g. 'XIY') to its full 2^n × 2^n matrix. - """ - mats = [_pauli_map[s] for s in pauli_str] - return functools.reduce(lambda A, B: np.kron(A, B), mats) - - -# function to project into the modules -def compute_me_purities(op): - """ - Computes GFD purities of op (assumed to be np.matrix) - by explicitly computing overlaps with Paulis - """ - - if op.ndim == 1: - # state vector - is_vector = True - elif op.ndim == 2 and op.shape[0] == op.shape[1]: - # density/operator - is_vector = False - else: - raise ValueError("`op` must be either a 1D state vector or a square matrix") - - d = op.shape[0] - n = int(np.log2(d)) - - basis = generate_pauli_strings(n) - purities = np.zeros(n + 1) - for belem in basis: - k = n - belem.count('I') - P = pauli_string_to_matrix(belem) - - if is_vector: - ovp = op.conj() @ (P @ op) - else: - ovp = np.trace(op @ P) - - #assert ovp.imag < 1e-10 - purities[k] += (ovp.real) ** 2 - - return purities / (2 ** n) - - -purities = [compute_me_purities(op) for op in states] -print(purities) - -# Grab default color cycle -colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] - -# Create two vertically aligned subplots sharing the x-axis -fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) - -for i, data in enumerate(purities): - color = colors[i % len(colors)] - ax1.plot(data, label=f'{labels[i]}', color=color) - ax2.plot(np.cumsum(data), label=f'{labels[i]}', color=color) - -ax1.set_ylabel('Purity') -ax2.set_ylabel('Cumulative Purity') -ax2.set_xlabel('Module weight') - -ax1.legend(loc='upper left') -ax2.legend(loc='upper left') +v_pauli_coeffs = np.zeros(len(B)) +for idx in range(1, 4): + v = W[:, idx] + v_pauli_coeffs += [np.abs(np.dot(v, pv)) for pv in B.T] -plt.tight_layout() -plt.savefig("/home/maria/Desktop/purities2.png") -plt.show() +print("The basis corresponding to the first 3x3 block has nonzero overlap with the following Pauli basis elements:") -""" -# To illustrate this, we'll consider a few key examples: -# * **Product state**: A "smooth" state with no entanglement. It shows high purity only in the lowest-order irreps, similar to a constant function in classical Fourier analysis. -# * **GHZ state**: A highly structured, maximally entangled state. It behaves like a quantum analog of a high-frequency oscillation, having purity concentrated mostly in the highest-order irreps, creating a clear quantum fingerprint. -# * **W state**: Moderately entangled, somewhat between the GHZ and product states, with a broader yet smoother distribution across the irreps. -# * **Random (Haar) state**: Highly complex but without structured entanglement patterns, its purity is distributed more evenly across the spectrum.# In the code examples we'll show shortly, you'll see precisely these patterns emerge. -# For each state, we compute the purity distribution across generalized Fourier modes (irreps), illustrating exactly how entanglement complexity relates to spectral structure. +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) -# # Here, show purity plots for these states as computed by the provided code - -# The fascinating aspect here is that while multipartite entanglement can get complex very quickly as we add qubits (due to the exponential increase in possible correlations), -# our generalized Fourier analysis still provides a clear, intuitive fingerprint of a state's entanglement structure. -# Even more intriguingly, just as smoothness guides how we compress classical signals (by discarding higher-order frequencies), the entanglement fingerprint suggests ways we might compress quantum states, -# discarding information in higher-order irreps to simplify quantum simulations and measurements. -# In short, generalized Fourier transforms allow us to understand quantum complexity, much like classical Fourier transforms give insight into smoothness. -# By reading a state's quantum Fourier fingerprint, we gain a clear window into the subtle quantum world of multipartite entanglement. - - -""" +v_pauli_coeffs = np.zeros(len(B)) +for idx in range(4, 7): + v = W[:, idx] + v_pauli_coeffs += [np.abs(np.dot(v, pv)) for pv in B.T] +print("The basis corresponding to the second 3x3 block has nonzero overlap with the following Pauli basis elements:") +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) +########################################################################### +# This block corresponds to operators acting non trivially on a single qubit! +# We can verify that the last block corresponds to those fully supported over our two qubits +v_pauli_coeffs = np.zeros(len(B)) +for idx in range(7, 16): + v = W[:, idx] + v_pauli_coeffs += [np.abs(np.dot(v, pv)) for pv in B.T] +# we can ask over which paulis v is supported +print("The basis corresponding to the last block has nonzero overlap with the following Pauli basis elements:") +for c, ps in zip(v_pauli_coeffs, basis): + if c > 1e-12: + print(" - ", ps) +########################################################################### +# Now that we realized that the blocks correspond to all the possible supports of operators over the Hilbert spaces of the different qubits, +# we can also see why this is the case. Remember that the representation :math:`U = R(g)` conjugates an operator :math:`\rho` +# by a tensor product of single-qubit unitary operators. [TODO: finish this thought with a bit more rigour] # # References # ---------- @@ -782,6 +766,6 @@ def compute_me_purities(op): ###################################################################### # -# About the author -# ---------------- +# About the authors +# ----------------- # From 902b18f44f44a38c52945ead1d203abf8976011c Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Tue, 19 Aug 2025 14:48:28 +0200 Subject: [PATCH 18/33] fix argument --- demonstrations_v2/tutorial_resourcefulness/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index bf7fbef186..1f773322c1 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -432,7 +432,7 @@ def ghz_state(n: int): # evolve the state above by U, using the vectorised objects rho_out_vec = Uvec @ rho_vec # reshape the result back into a density matrix -rho_out = np.reshape(rho_out_vec, shape=(2**n, 2**n), order='F') +rho_out = np.reshape(rho_out_vec, (2**n, 2**n), order='F') # this is the same as the usual adjoint application of U print(np.allclose(rho_out, U @ rho @ U.conj().T )) From 37b3c4d159b6f03854520ec8ae47525c6f878510 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Tue, 19 Aug 2025 14:50:36 +0200 Subject: [PATCH 19/33] fix argument --- demonstrations_v2/tutorial_resourcefulness/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 1f773322c1..22fa5191a5 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -589,7 +589,7 @@ def key_and_zeros(vec): # Indeed in genear Q will not be unitary, as we can quickly check by checking if any two of its columns are orthogonal # -print("The inner product between the second and third column of Q is: ", np.dot(Q[:1],Q[:2]).item()) +print("The inner product between the second and third column of Q is: ", np.dot(Q[:,1],Q[:,2]).item()) ###################################################################### # This is just becuase the eigenvecotrs of the random linear combiniation that we implemented before are only guaranteed to From 973dbae6becedaf4176c1f2d99b3738e415db0fe Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Tue, 19 Aug 2025 17:00:23 +0200 Subject: [PATCH 20/33] fix more issues --- .../tutorial_resourcefulness/demo.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 22fa5191a5..860b878417 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -1,34 +1,32 @@ r""" -Analysing quantum resourcefulness with the generalized Fourier transform -======================================================================== - -.. figure:: ../_static/demo_thumbnails/opengraph_demo_thumbnails/OGthumbnail_resourcefulness.png - :align: center - :width: 70% - :alt: DESCRIPTION. - :target: javascript:void(0) +Resourcefulness of quantum states with Fourier analysis +======================================================= Resource theories in quantum information ask how "complex" a given quantum state is with respect to a certain measure of complexity. For example, using the resource of *entanglement*, we can ask how entangled a quantum state is. Other well-known resources are *Clifford stabilizerness*, which measures how close a state is from being preparable by a -classically simulatable "Clifford circuit", or *Gaussianity*, which measures how far away a state is from "Gaussian states" +classically simulatable *Clifford circuit*, or *Gaussianity*, which measures how far away a state is from *Gaussian states* considered simple in quantum optics. As the name "resourceful" suggests, these measures of complexity often relate to how much "effort" states are, for example with respect to classical simulation or preparation in the lab. -It turns out [#Bermejo_Braccia]_ that the resourcefulness of quantum states can be investigated with tools from *generalised Fourier analysis*. -"Fourier analysis" here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case -is not a function over :mathbb:`R` or :mathbb:`Z`, but a quantum state. "Generalised" indicates that we don't use the +It turns out that the resourcefulness of quantum states can be investigated with tools from *generalised Fourier analysis*. +*Fourier analysis* here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case +is not a function over :math:`\mathbb{R}` or :math:`\mathbb{Z}`, but a quantum state. *Generalised* indicates that we don't use the standard Fourier transform, but a generalisation of its group theoretic definition. -[#Bermejo_Braccia]_ suggest to compute a quantity that they call the **Generalised Fourier Decomposition (GFD) Purity**, -and use it as a "footprint" of a state's resource profile. When using the standard Fourier transform, -the GFD purities are just the absolute squares of the normal Fourier coefficients, which is also known as the *power spectrum*. +Bermejo, Braccia et al. (20025) [#Bermejo_Braccia]_ suggest to compute a quantity that they call the **Generalised Fourier Decomposition (GFD) Purity**, +and use it as a "footprint" of a state's resource profile. -The basic idea is to identify the set of unitaries that maps resource-free +The basic idea is to identify the set of unitary transformations that maps resource-free states to resource-free states with a *linear representation* of a group. The basis in which this representation, and hence the free unitaries, are (block-)diagonal, reveals so-called *irreducible subspaces*. -The GFD Purities are then the "weight" a state has in these subspaces. As in standard Fourier analysis, -higher-order Purities indicate a less resourceful function. +The GFD Purities are then the "weight" a state has when being projected into these subspaces. + +When using the standard Fourier transform, +the GFD purities are just the absolute squares of the normal Fourier coefficients, which is also known as the *power spectrum*. + +As in standard Fourier analysis, +higher-order GFD Purities indicate a less resourceful function. This intuition carries over to the generalised case, where more resourceful states have higher weights in higher-order subspaces. In this tutorial we will illustrate with two simple examples how to compute the GFD Purities to analyse resource. From 06e10637821b79b1c50b6d0ebf246acb5f2c8418 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 20 Aug 2025 20:08:51 +0200 Subject: [PATCH 21/33] polish --- .../tutorial_resourcefulness/demo.py | 385 +++++++++--------- 1 file changed, 198 insertions(+), 187 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 860b878417..a511bc1cda 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -14,23 +14,20 @@ *Fourier analysis* here refers to the well-known technique of computing Fourier coefficients of a mathematical object, which in our case is not a function over :math:`\mathbb{R}` or :math:`\mathbb{Z}`, but a quantum state. *Generalised* indicates that we don't use the standard Fourier transform, but a generalisation of its group theoretic definition. -Bermejo, Braccia et al. (20025) [#Bermejo_Braccia]_ suggest to compute a quantity that they call the **Generalised Fourier Decomposition (GFD) Purity**, +Bermejo, Braccia et al. (2025) [#Bermejo_Braccia]_ suggest to use generalised Fourier analysis to +compute a quantity that they call the **Generalised Fourier Decomposition (GFD) Purity**, and use it as a "footprint" of a state's resource profile. The basic idea is to identify the set of unitary transformations that maps resource-free -states to resource-free states with a *linear representation* of a group. -The basis in which this representation, and hence the free unitaries, are (block-)diagonal, reveals so-called *irreducible subspaces*. -The GFD Purities are then the "weight" a state has when being projected into these subspaces. - -When using the standard Fourier transform, -the GFD purities are just the absolute squares of the normal Fourier coefficients, which is also known as the *power spectrum*. - -As in standard Fourier analysis, -higher-order GFD Purities indicate a less resourceful function. -This intuition carries over to the generalised case, where more resourceful states have higher weights in higher-order subspaces. - -In this tutorial we will illustrate with two simple examples how to compute the GFD Purities to analyse resource. -To introduce the basic recipe in a familiar setting, we will first look at the power spectrum of a discrete function. +states to resource-free states with a *unitary representation of a group*. +The basis in which this representation, and hence the free unitaries, are (block-)diagonal, reveals so-called *irreducible subspaces* +of different "order". +The GFD Purities are then a measure for how much of a state "lives" in each of these subspaces. +More resourceful states have large projections into higher-order subspaces and vice versa. + +We will see that the standard Fourier transform is a special case of this framework, and here +the GFD purities are just the absolute squares of the Fourier coefficients, +which is also known as the *power spectrum*. This will be our first example to develop the more general framework. We will then use the same concepts to analyse the *entanglement* of quantum states as a resource, reproducing Figure 2 in [#Bermejo_Braccia]_. @@ -42,14 +39,8 @@ a product state with no entanglement has contributions in lower-order Purities. The interpolation between the two extremes, exemplified by a Haar random state, has a spectrum in between. -While the theory rests in group theory, the tutorial is aimed at readers who don't know much about groups at all, -since everything can be understood by applying standard linear algebra. - -.. note:: - Of course, even in this simple case the numerical analysis scales exponentially with the number of qubits, and - everything we present here is only possible to compute for small system sizes. It is a fascinating question in - which situations the GFD Purities of a physical state could be read out - on a quantum computer, which is known to sometimes perform the block-diagonalisation efficiently. +While the theory rests in group theory, the tutorial is aimed at readers who don't know much about groups at all. +Instead, we will make heavy use of linear algebra! Standard Fourier analysis through the lense of resources @@ -63,8 +54,7 @@ of a function :math:`f(0), ..., f(N-1)` over the integers :math:`x \in {0,...,N-1}`, the Fourier transform computes the Fourier coefficients -.. math:: - \hat{f}(k) = \frac{1}{\sqrt{N}\sum_{x=0}^{N-1} f(x) e^{\frac{2 \pi i}{N} k x}, k = 0,...,N-1 +.. math:: \hat{f}(k) = \frac{1}{\sqrt{N}} \sum_{x=0}^{N-1} f(x) e^{\frac{2 \pi i}{N} k x}, \;\; k = 0,...,N-1 In words, the Fourier coefficients are projections of the function :math:`f` onto the "Fourier" basis functions :math:`e^{\frac{2 \pi i}{N} k x}`. Note that we use a normalisation here that is consistent with a unitary transform. @@ -109,9 +99,9 @@ def f_hat(k): # GFD Purities, so for this case, we can easily compute our quantity of interest! # -purities = [np.abs(f_hat(k))**2 for k in range(N)] -plt.plot(purities) -plt.ylabel("GFD purities") +power_spectrum = [np.abs(f_hat(k))**2 for k in range(N)] +plt.plot(power_spectrum) +plt.ylabel("GFD purity") plt.xlabel("k") plt.tight_layout() plt.show() @@ -167,41 +157,56 @@ def g(x): # The general recipe # ++++++++++++++++++ # -# Computing the GFD Purities for the resource of smoothness was very simple. We will now make it more complicated, -# in order to set up the machinery that calculates the Purities for more general cases like quantum entanglement. +# Computing the GFD Purities for the resource of smoothness was very simple. We will now view it from a much more complicated +# perspective, but one whose machinery will in the second part allow us to calculate the Purities for +# of concepts like quantum entanglement. # -# 1. Find free vectors -# ******************** -# The first step is to find "free vectors". So far we dealt with a discrete function :math:`f(0), ..., f(N-1)`, -# but we can easily write it as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. As argued above, -# the set of resource-free vectors corresponds to constant functions, :math:`f(0) = ... = f(N-1)`. +# 1. Identify free vectors +# ************************ +# The first step to compute GFD Purities is to define the resource by identifying a set of "resource-free vectors". +# (We need vectors because representation theory deals with vector spaces.) +# So far we only had a discrete *function* :math:`f(x)`, +# but we can easily write it as a vector :math:`[f(0), ..., f(N-1)]^T \in V = \mathbb{R}^N`. # +###################################################################### f_vec = np.array([f(x) for x in range(N)]) +# As argued above, +# the set of resource-free vectors corresponds to constant functions, :math:`f(0) = ... = f(N-1)`. +# + + ###################################################################### -# 2. Find free unitary transformations -# ************************************ -# Next, we need to identify a set of unitary matrices that does not change the -# "constantness" of the vector. In the case above this is given by the permutation matrices, which -# swap the entries of the vector but do not change any of them. +# 2. Identify free unitary transformations +# **************************************** +# Next, we need to identify a set of unitary matrices that, when multiplied by the resource-free or constant vectors +# maps to another resource-free or constant vector. In the case above this is given by the permutation matrices, which +# swap the entries of the vector but do not change their magnitude. # # 3. Identify a group representation # ********************************** # This is the most difficult step, which sometimes requires a lot of group-theoretic knowledge. Essentially, we have to associate -# the unitary transformations, or a subset of them, with a *unitary group representation*. A representation is a -# matrix-valued function :math:`R(g), g \in G` on a group with the property that :math:`R(g) R(g') = R(gg')`, -# where :math:`gg'` is the composition of elements according to the group. Without dwelling further, we simply -# recognise that the *circulant* permutation matrices -- those that shift vector +# the unitary transformations, or at least a subset of them, with a *unitary representation of a group*. +# +# .. admonition:: Unitary representation +# :class: note +# +# A representation is a matrix-valued function :math:`R(g), g \in G` on a group with the +# property that :math:`R(g) R(g') = R(gg')`, where :math:`gg'` is the composition of elements according +# to the group. +# +# Without dwelling further, we simply recognise that the *circulant* permutation matrices -- those that shift vector # entries by :math:`s` positions -- can be shown to # form a unitary representation for the group :math:`g \in G = Z_N`, called the *regular representation*. The group :math:`Z_N` # are the integers from 0 to N-1 under addition modulo N, which can be seen as the x-domain of our function. # Every circulant matrix hence gets associated with one x-value. # +# Here is the circulant matrix associated with the x-value 2: +# s = 2 -# create a circulant permutation matrix first_row = np.zeros(N) first_row[s] = 1 @@ -220,7 +225,8 @@ def g(x): # 4. Find the basis that block-diagonalises the representation # ************************************************************ # This was quite a bit of group jargon, but for a good reason: We are now guaranteed that there is -# a basis change that block-diagonalises *all* circulant permutation matrices. If you know group theory, +# a basis change that block-diagonalises *all* circulant permutation matrices, or more generally, +# all unitary matrices :math:`R(g)` that maintain resource-freeness. If you know group theory, # then you will recognise that the blocks reveal the *irreducible representations* of the group. # In more general linear algebra language, the blocks form different *irreducible subspaces* which we need to compute # the GFD Purities in the next step. @@ -244,9 +250,10 @@ def g(x): # get the Fourier matrix from scipy from scipy.linalg import dft F = dft(N)/np.sqrt(N) +f_hat_vals2 = F @ f_vec # make sure this is what we previously computed -assert np.allclose(F @ f_vec, f_hat_vals) +assert np.allclose(f_hat_vals2, f_hat_vals) # change the circulant matrix into the Fourier basis using F np.set_printoptions(precision=2, suppress=True) @@ -262,10 +269,16 @@ def g(x): # # 5. Find a basis for each subspace # ********************************* -# The rows of the basis transformation matrix, here ``F``, with the row/column indices of a block in ``P_F`` form a basis -# for the subspace associated with each block. In [#Bermejo_Braccia]_ these basis vectors where +# What we actually wanted to accomplish with the (block-)diagonalisation is to find the subspaces associated with each block. +# According to standard linear algebra, a basis for such a subspace can be found by selecting the rows of ``F`` whose indices +# correspond to the indices of a block in ``P_F``. In [#Bermejo_Braccia]_ these basis vectors where # called :math:`w^{(i)}_{\alpha, j}`, where :math:`\alpha` marks the subspace and :math:`j` the copy or multiplicity -# of a block on the diagonal (a technical detail we will not explore further here). +# of a block on the diagonal (a technical detail we will not worry about any further here). +# For example, the basis for our third 1-dimensional block would be +# + +basis3 = F[3:4] + # # 6. Compute the GFD Purities # *************************** @@ -274,12 +287,22 @@ def g(x): # .. math:: # \mathcal{P}(v) = \sum_i | \langle w^{(i)}_{\alpha, j}, v \rangle |^2. # +# For example, the GFD Purity for the third block (or "frequency") would be calculated as follows: # + +purity3 = np.abs(np.vdot(basis3, f_vec))**2 + +# confirm that this is the third entry in the power spectrum +print("purity and power spectrum entry coincide", np.isclose(power_spectrum[3], purity3)) + +###################################################################### # We now have a very different perspective on the power spectrum :math:`|\hat{f}|^2`: It is a projection of the function -# :math:`f` into irreducible subspaces revealed by moving into the Fourier basis. The power spectrum, as +# :math:`f` into irreducible subspaces revealed by moving into the Fourier basis. +# +# The power spectrum, as # is widely known, is a footprint for the smoothness of the function. But, having chosen a rather general # group-theoretic angle, can we do the same for other groups and vector spaces -- for example those -# relevant to investigate resources of quantum states? +# relevant when investigating resources of quantum states? # ###################################################################### @@ -290,16 +313,23 @@ def g(x): # We can think of multipartite entanglement as a resource of the state of a quantum system that measures how "wiggly" # the correlations between its constituents are. # While this is a general statement, we will restrict our attention to systems made of our favourite quantum objects, qubits. -# Just like smoother functions have simpler Fourier spectra (mostly low-momentum components), +# Just like smoother functions have Fourier spectra concentrated in the low-order frequencies, # quantum states with simpler entanglement structures, or no entanglement at all like in the case of product states, -# have simpler generalized Fourier spectra, where most of their purity resides in the lower-order "irreps" -# (that we recall is short for irreducible representations, but you can think of them as generalized frequencies). +# have generalized Fourier spectra where most of their purity resides in the lower-order invariant subspaces. +# # Here are a few examples that we will use below: -# * **Product state**: +# +# * **Product state**: A resource-free state with no entanglement. The Purity is concentrated in the lower-order +# subspaces # * **GHZ state**: A highly structured, maximally entangled state. It behaves like a quantum analog of a -# high-frequency oscillation, having purity concentrated mostly in the highest-order irreps, creating a clear quantum fingerprint. -# * **W state**: Moderately entangled, somewhat between the GHZ and product states, with a broader yet smoother distribution across the irreps. -# * **Random (Haar) state**: Highly complex but without structured entanglement patterns, its purity is distributed more evenly across the spectrum.# In the code examples we'll show shortly, you'll see precisely these patterns emerge. +# high-frequency oscillation, having Purity concentrated mostly in the highest-order subspaces. +# * **W state**: Moderately entangled, somewhat between the GHZ and product states, with a broader +# yet smoother distribution across the subspaces. +# * **Random (Haar) state**: Highly complex but without structured +# entanglement patterns, its Purity is distributed more evenly across the spectrum. +# +# In the code examples we'll show shortly, you'll see precisely these patterns emerge. But for now, let us create +# example states for these categories: # import math @@ -348,20 +378,18 @@ def ghz_state(n: int): labels = [ "Product", "Haar", "W", "GHZ"] ##################################################################### -# On the flip side, highly entangled states spread their purity across more and higher-order (i.e., larger dimensional) irreps, -# similar to how more complex ("wiggly") functions have larger Fourier coefficients at higher frequencies. -# This means that analyzing a state's Fourier spectrum can tell us how "resourceful" or entangled it is. +# To compute the GFD Purities, we can walk the same steps we took when studying the resource of "smoothness". # -# Let us walk the same steps we took when studying the resource of "smoothness". -# 1. **Find free vectors**. Luckily for us, our objects of interest, the quantum states :math:`\psi \in \mathbb{C}^{2n}`, +# 1. **Identify free vectors**. Luckily for us, our objects of interest, the quantum states :math:`|\psi\rangle \in \mathbb{C}^{2n}`, # are already in the form of vectors. It's easy to define the set of free states for multipartite entanglement: -# tensor products of single-qubit quantum states. -# 2. **Find free unitary transformations**. Now, what unitary transformation of a quantum state does not generate +# product states, or tensor products of single-qubit quantum states. +# 2. **Identify free unitary transformations**. Now, what unitary transformation of a quantum state does not generate # entanglement? You guessed it right, "non-entangling" circuits that consist only of single-qubit -# gates :math:`U=\Bigotimes U_j` for :math:`U_j \in SU(2)`. +# gates :math:`U=\bigotimes U_j` for :math:`U_j \in SU(2)`. # 3. **Identify a group representation**. It turns out that non-entangling unitaries are the standard representation -# of the group :math:`G = SU(2) x SU(2) ... x SU(2)` for the vector space :math:`H`, the Hilbert space of the state vectors. -# Again, this implies that we can find a basis of the Hilbert space, where any :math:`U` is simultaneously block-diagonal. +# of the group :math:`G = SU(2) \times SU(2) \times \dots \times SU(2)` for the vector space :math:`H`, +# the Hilbert space of the state vectors. +# Again, this implies that we can find a basis of the Hilbert space where any :math:`U` is simultaneously block-diagonal. # 4. **Find the basis that block-diagonalises the representation**. # # Let's stop here for now, and try to block-diagonalise one of the non-entangling unitaries. We can use an @@ -371,13 +399,12 @@ def ghz_state(n: int): from scipy.stats import unitary_group from functools import reduce - # create n haar random single-qubit unitaries Us = [unitary_group.rvs(dim=2) for _ in range(n)] # compose them into a non-entangling n-qubit unitary U = reduce(lambda A, B: np.kron(A, B), Us) -# Block-diagonalise +# block-diagonalise vals, U_bdiag = np.linalg.eig(U) print(U_bdiag) @@ -387,40 +414,43 @@ def ghz_state(n: int): # It turns out that the non-entangling unitary matrices only have a single block of size :math:`2^n \times 2^n`. # Technically speaking, the representation is *irreducible* over :math:`H`. # As a consequence, the invariant subspace is :math:`H` itself, and the single GFD purity is simply the purity of -# the state :math:`\psi`, which is 1. +# the state :math:`|\psi\rangle`, which is 1. # This, of course, is not a very informative footprint -- one that is much too coarse! # # However, nobody forces us to restrict our attention to :math:`H` and to # the (standard) representation of non-entangling unitary matrices. # After all, what's the first symptom of entanglement in a quantum state? -# You guessed right again, the reduced density matrix of some subsystem is mixed! +# The reduced density matrix of some subsystem is mixed! # Wouldn't it make more sense to study multipartite entanglement from the point of view of density matrices? # It turns out that moving into the space :math:`B(H)` of bounded linear operators in which the density matrices live leads to -# a much more nuanced Fourier spectrum. +# a much more nuanced Fourier spectrum. Although this space contains matrices, it is a vector space. # -# Let us walk the steps above again -# 1. **Find free vectors**. :math:`L(H)` is a vector space in the technical sense, but one of matrices. To use the linear algebra -# tricks from before we have to "vectorize" density matrices :math:`rho=\sum_i,j c_i,j |i\rangle \langle j|` -# into vectors :math:`|rho\rangle \rangle = \sum_i,j c_i,j |i\rangle |j\rangle \in H \otimes H^*`. +# Let us walk the steps above once more: +# +# 1. **Identify free vectors**. +# Our free vectors are still product states, only that now we represent them as density matrices :math:`\rho = \bigotimes \rho_j`. +# But to use the linear algebra tricks from before we have +# to "vectorize" density matrices :math:`\rho=\sum_{i,j} c_{i,j} |i\rangle \langle j|` +# to have the form :math:`|\rho\rangle \rangle = \sum_{i,j} c_{i,j} |i\rangle |j\rangle \in H \otimes H^*` (something +# you might have encountered before in the *Choi* formalism). # For example: # # create a random quantum state psi = np.random.rand(2**n) + 1j*np.random.rand(2**n) -psi /= np.linalg.norm(psi) +psi = psi/np.linalg.norm(psi) # construct the corresponding density matrix rho = np.outer(psi, psi.conj()) # vectorise it rho_vec = rho.flatten(order='F') ###################################################################### -# The set of free states is again that of product states :math:`\rho = \bigotimes \rho_j` -# where each :math:`\rho_j` is a single-qubit state. We only have to write them in flattened form. # -# 2. **Find free unitary transformations**. The free operations are still given by non-entangling unitaries, but +# 2. **Identify free unitary transformations**. +# The free operations are still given by non-entangling unitaries, but # of course, they act on density matrices via :math:`\rho' = U \rho U^{\dagger}`. -# We can also vectorise this operation by defining the matrix :math:`U_{\mathrm{vec}}(g) = U \otimes U^*`. -# We then have that :math:`|\rho'\rangle \rangle = U_{\mathrm{vec}} \rho`. +# We can also vectorise this operation by defining the matrix :math:`U_{\mathrm{vec}} = U \otimes U^*`. +# We then have that :math:`|\rho'\rangle \rangle = U_{\mathrm{vec}} |\rho\rangle \rangle`. # To demonstrate: # @@ -435,13 +465,15 @@ def ghz_state(n: int): print(np.allclose(rho_out, U @ rho @ U.conj().T )) ###################################################################### -# 3. **Identify a group representation**. This "adjoint action" is indeed a valid representation of -# :math:`G = SU(2) x SU(2) ... x SU(2)`, called the *defining representation*. However, it is a different one from before, -# and this time there is a basis transformation that properly block-diagonalises all matrices in the representation. +# 3. **Identify a group representation**. +# This "adjoint action" is indeed a valid representation of +# :math:`G = SU(2) \times \dots \times SU(2)`, called the *defining representation*. However, it is a different one from before, +# and this time there is a basis transformation that properly block-diagonalises all matrices in the representation. # # 4. **Find the basis that block-diagonalises the representation**. -# To find this basis we compute the eigendecomposition of an arbitrary linear combination -# of a set of matrices in the representation. +# To find this basis we compute the eigendecomposition of an arbitrary linear combination +# of a set of matrices in the representation. Using a such a linear combination makes it more likely that we didn't +# find a subblock-structure that is only present in one of the matrices. # rng = np.random.default_rng(42) @@ -463,12 +495,11 @@ def ghz_state(n: int): vals, Q = np.linalg.eig(M_combo) ###################################################################### -# Let's test this basis change +# Let's test this basis change with one of the vectorized unitaries: # -Qinv = np.linalg.inv(Q) -# take one of the vectorised unitaries Uvec = Uvecs[0] +Qinv = np.linalg.inv(Q) Uvec_diag = Qinv @ Uvec @ Q np.set_printoptions( @@ -477,7 +508,8 @@ def ghz_state(n: int): threshold=10000 # so it doesn’t summarize large arrays ) -print(Uvec_diag) +# print the absolute values for better illustration +print(np.round(np.abs(Uvec_diag), 4)) ###################################################################### # But ``Uvec_diag`` does not look block diagonal. What happened here? @@ -489,33 +521,19 @@ def ghz_state(n: int): def group_rows_cols_by_sparsity(B, tol=0): """ - Given a binary or general matrix C, this function: - 1. Groups identical rows and columns. - 2. Orders these groups by sparsity (most zeros first). - 3. Returns the permuted matrix C2, and the row & column permutation - matrices P_row, P_col such that C2 = P_row @ C @ P_col. - - Parameters - ---------- - B : ndarray, shape (n, m) - Input matrix. - - Returns - ------- - P_row : ndarray, shape (n, n) - Row permutation matrix. - P_col : ndarray, shape (m, m) - Column permutation matrix. + Given matrix B, this function groups identical rows and columns, orders these groups + by sparsity (most zeros first) and returns the row & column permutation + matrices P_row, P_col such that B2 = P_row @ B @ P_col is block-diagonal. """ - # Compute boolean mask where |B| >= tol + # compute boolean mask where |B| >= tol mask = np.abs(B) >= 1e-8 - # Convert boolean mask to integer (False→0, True→1) + # convert boolean mask to integer (False→0, True→1) C = mask.astype(int) - # order by sparsity + # order by sparsity n, m = C.shape - # Helper to get a key tuple and zero count for a vector + # helper to get a key tuple and zero count for a vector def key_and_zeros(vec): if tol > 0: bin_vec = (np.abs(vec) < tol).astype(int) @@ -526,7 +544,7 @@ def key_and_zeros(vec): zero_count = int(np.sum(np.array(vec) == 0)) return key, zero_count - # Group rows by key + # group rows by key row_groups = OrderedDict() row_zero_counts = {} for i in range(n): @@ -534,14 +552,14 @@ def key_and_zeros(vec): row_groups.setdefault(key, []).append(i) row_zero_counts[key] = zc - # Sort row groups by zero_count descending + # sort row groups by zero_count descending sorted_row_keys = sorted(row_groups.keys(), key=lambda k: row_zero_counts[k], reverse=True) - # Flatten row permutation + # flatten row permutation row_perm = [i for key in sorted_row_keys for i in row_groups[key]] - # Group columns by key + # group columns by key col_groups = OrderedDict() col_zero_counts = {} for j in range(m): @@ -549,13 +567,13 @@ def key_and_zeros(vec): col_groups.setdefault(key, []).append(j) col_zero_counts[key] = zc - # Sort column groups by zero_count descending + # sort column groups by zero_count descending sorted_col_keys = sorted(col_groups.keys(), key=lambda k: col_zero_counts[k], reverse=True) col_perm = [j for key in sorted_col_keys for j in col_groups[key]] - # Build permutation matrices + # build permutation matrices P_row = np.eye(n)[row_perm, :] P_col = np.eye(m)[:, col_perm] @@ -573,46 +591,29 @@ def key_and_zeros(vec): ###################################################################### -# The reordering made the block structure visible. You can check that now any vectorised non-entangling matrix ``Uvec`` -# has the same block structure if we change the basis and reorder via ``Qinv @ Uvec @ Q``. +# The reordering made the block structure visible. You can check that now any vectorized non-entangling matrix ``Uvec`` +# has the same block structure if we change the basis via ``Qinv @ Uvec @ Q``. # -# The next step is to -# 5. **Find a basis for each subspace**. -# We have everything ready to do this: the basis of a subspace are just the rows of ``Q`` that correspond to the row/column -# indices of a given block. Hence, the GFD Purity calculation can be performed by changing a new vector ``v`` into the -# basis we just identified by applying ``v_diag = Q v``, and taking the sum of absolute squares of those entries in ``v_diag`` -# that correspond to one block or subspace. -# -# Notice that the similarity transform 'Qinv @ Uvec @ Q' needs not be implemented by a unitary change of basis! -# Indeed in genear Q will not be unitary, as we can quickly check by checking if any two of its columns are orthogonal +# But we need one last cosmetic modification that will help us below: We want to turn ``Q`` into a unitary transformation. # -print("The inner product between the second and third column of Q is: ", np.dot(Q[:,1],Q[:,2]).item()) +U, s, Vh = np.linalg.svd(Q, full_matrices=False) +Q = U @ Vh +Qinv = np.linalg.inv(Q) ###################################################################### -# This is just becuase the eigenvecotrs of the random linear combiniation that we implemented before are only guaranteed to -# 'span' the blocks, not to be an orthonormal basis of them. -# However, rep-theory actually guarantees that we can indeed find such an orthonormal basis, or, in other words, that we can -# unitarize Q. One way is to polar decompose it - -def unitary_polar(Q): - U, s, Vh = np.linalg.svd(Q, full_matrices=False) - W = U @ Vh - return W - -W = unitary_polar(Q) -# It is indeed unitary -print(np.allclose(W @ W.conj().T, np.eye(len(W)))) -print(np.allclose(W.conj().T @ W, np.eye(len(W)))) - -# And yields the same block-decomposition -print(np.round(np.abs(W.conj().T @ Uvec @ W), 4)) - -# We can now proceed without the risk of messing up the normalization of our states +# 5. **Find a basis for each subspace**. +# As mentioned before, the basis of a subspace corresponding to a block are just the rows of the unitary basis change +# matrix ``Q`` that correspond to the row/column +# indices of the block. Hence, the GFD Purity calculation can be performed by changing a new vector ``v`` into the +# basis we just identified by applying ``v_diag = Q v``, and taking the sum of absolute squares of those +# entries in ``v_diag`` +# that correspond to the block. +# # vectorise the states we defined earlier states_vec = [state.flatten(order='F') for state in states] -states_vec_diag = [W.conj().T @ state_vec for state_vec in states_vec] +states_vec_diag = [Q.conj().T @ state_vec for state_vec in states_vec] purities = [] for v in states_vec_diag: @@ -626,45 +627,53 @@ def unitary_polar(Q): print(f" - Block {k+1}: ", val) ###################################################################### -# Notice that, as it should given the normalization of the states considered, the purities are a probability distribution. -# Indeed they are positive and we can check that they add up to 1, reconstructing the states' total purity. +# Note that the GFD Purities of quantum states are normalised, and can hence be interpreted as probabilities. # for data, label in zip(purities, labels): print(f"{label} state has total purity: ", np.sum(data).item()) ###################################################################### -# We can further visualize the purities as done in [#Bermejo_Braccia]_, by aggregating them +# We can now reproduce Figure 2 in [#Bermejo_Braccia]_, by aggregating the GFD Purities # by the corresponding block's size # agg_purities = [[p[0], p[1]+p[2], p[3]] for p in purities] -# Plot the purities and their comulative fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) for i, data in enumerate(agg_purities): ax1.plot(data, label=f'{labels[i]}') - ax2.plot(np.cumsum(data), label=f'{labels[i]}') ax1.set_ylabel('Purity') -ax2.set_ylabel('Cumulative Purity') -ax2.set_xlabel('Module weight') -ax2.set_xticks(list(range(n+1))) +ax1.set_xlabel('Module weight') +ax1.set_xticks(list(range(n+1))) ax1.legend(loc='upper left') -ax2.legend(loc='upper left') plt.tight_layout() plt.show() ########################################################################### -# The fascinating aspect here is that while multipartite entanglement can get complex very quickly as we +# The point here is that while multipartite entanglement can get complex very quickly as we # add qubits (due to the exponential increase in possible correlations), # our generalized Fourier analysis still provides a clear, intuitive fingerprint of a state's entanglement structure. # Even more intriguingly, just as smoothness guides how we compress classical -# signals (by discarding higher-order frequencies), the entanglement fingerprint suggests ways we might compress quantum states, +# signals (by discarding higher-order frequencies), the entanglement fingerprint suggests ways of how we might compress quantum states, # discarding information in higher-order purities to simplify quantum simulations and measurements. -# In short, generalized Fourier transforms allow us to understand quantum complexity, much like classical Fourier transforms give insight into smoothness. -# By reading a state's quantum Fourier fingerprint, we gain a clear window into the subtle quantum world of multipartite entanglement. +# + +########################################################################### +# In short, generalized Fourier transforms allow us to understand quantum complexity, +# much like classical Fourier transforms give insight into smoothness. +# By reading a state's "quantum Fourier" fingerprint, the GFD Purities, we gain a clear window into the +# subtle quantum world of multipartite entanglement. We now know that +# computing these purities, once a problem has been defined in terms of representations, is just a matter of +# standard linear algebra. +# +# Of course, we can only hope to compute the GFD Purities for small system sizes, as the matrices involved scale +# rapidly with the number of qubits. It is a fascinating question in which +# situations they could be estimated by a quantum algorithm, since many efficient quantum algorithms +# for the block-diagonalisation of representations are known, such as the Quantum Fourier Transform or +# the quantum Schur transform. # ########################################################################### @@ -673,7 +682,9 @@ def unitary_polar(Q): # # If your head is not spinning yet, let's ask a final, and rather instructive, question. # What basis have we actually changed into in the above example of multi-partite entanglement? -# It turns out that `W` changes into the Pauli basis! +# It turns out that ``Q`` changes into the Pauli basis! +# To see this, we take the Pauli basis matrices, vectorise them and compare each one with the +# subspace basis vectors in ``Q``. # import itertools @@ -699,67 +710,67 @@ def pauli_basis(n): return sorted_strs, B -# let's create the vectorized basis of Pauli matrices basis,B = pauli_basis(n) -# now let's take the first basis vector, the one corresponding to the smallest block -# and project it onto paulis +########################################################################### +# The basis vector for the first 1-dimensional subspace corresponding to the 1x1 block +# is orthogonal to all Pauli basis vectors but "II": +# -v = W[:,0] +v = Q[:,0] v_pauli_coeffs = [np.abs(np.dot(v, pv)) for pv in B.T] -print("The basis corresponding to the 1x1 block has nonzero overlap with the following Pauli basis elements:") +print("Pauli basis vectors that live in 1x1 subspace:") for c, ps in zip(v_pauli_coeffs, basis): if c > 1e-12: print(" - ", ps) ########################################################################### -# It's just the Identity! This is not a surprise, since any unitary maps the identity to itself. -# Let's keep going, what about a vector in the 3x3 blocks? +# What about a vector in the 3x3 blocks? # v_pauli_coeffs = np.zeros(len(B)) for idx in range(1, 4): - v = W[:, idx] + v = Q[:, idx] v_pauli_coeffs += [np.abs(np.dot(v, pv)) for pv in B.T] -print("The basis corresponding to the first 3x3 block has nonzero overlap with the following Pauli basis elements:") - +print("Pauli basis vectors that live in the first 3x3 subspace:") for c, ps in zip(v_pauli_coeffs, basis): if c > 1e-12: print(" - ", ps) v_pauli_coeffs = np.zeros(len(B)) for idx in range(4, 7): - v = W[:, idx] + v = Q[:, idx] v_pauli_coeffs += [np.abs(np.dot(v, pv)) for pv in B.T] - -print("The basis corresponding to the second 3x3 block has nonzero overlap with the following Pauli basis elements:") - +print("Pauli basis vectors that live in the second 3x3 subspace:") for c, ps in zip(v_pauli_coeffs, basis): if c > 1e-12: print(" - ", ps) ########################################################################### -# This block corresponds to operators acting non trivially on a single qubit! +# These subspaces correspond to operators acting non-trivially on a single qubit only! +# +# We can verify that the last block corresponds to those fully supported over two qubits. +# -# We can verify that the last block corresponds to those fully supported over our two qubits v_pauli_coeffs = np.zeros(len(B)) for idx in range(7, 16): - v = W[:, idx] + v = Q[:, idx] v_pauli_coeffs += [np.abs(np.dot(v, pv)) for pv in B.T] -# we can ask over which paulis v is supported -print("The basis corresponding to the last block has nonzero overlap with the following Pauli basis elements:") +print("Pauli basis vectors that live in the 9x9 subspace:") for c, ps in zip(v_pauli_coeffs, basis): if c > 1e-12: print(" - ", ps) ########################################################################### -# Now that we realized that the blocks correspond to all the possible supports of operators over the Hilbert spaces of the different qubits, -# we can also see why this is the case. Remember that the representation :math:`U = R(g)` conjugates an operator :math:`\rho` -# by a tensor product of single-qubit unitary operators. [TODO: finish this thought with a bit more rigour] +# When one thinks about this a little more, it is probably not surprising. The Pauli basis has a structure +# that considers the qubits separately, and entanglement is likewise related to subsystems of qubits that interact. However, +# recognising the generalised Fourier basis as the Pauli basis provides an intuition for the subspaces and their order: +# GFD purities measure how much of a state lives in a given subsets of qubits! +# # # References # ---------- From 713eb8a987e452649002cb89a18514b55b842cc4 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 20 Aug 2025 20:12:20 +0200 Subject: [PATCH 22/33] add second author --- demonstrations_v2/tutorial_resourcefulness/metadata.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/metadata.json b/demonstrations_v2/tutorial_resourcefulness/metadata.json index 1e9d2306bb..1a47de7af6 100644 --- a/demonstrations_v2/tutorial_resourcefulness/metadata.json +++ b/demonstrations_v2/tutorial_resourcefulness/metadata.json @@ -5,10 +5,7 @@ "username": "mariaschuld" }, { - "username": "paolobraccia" - }, - { - "username": "pablobermejo" + "username": "impolster" } ], "executable_stable": true, From 5b56b9e49222dafacc63e8f2e1338a8dbba99f87 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Wed, 20 Aug 2025 20:14:29 +0200 Subject: [PATCH 23/33] fix plot --- demonstrations_v2/tutorial_resourcefulness/demo.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index a511bc1cda..3d6985940e 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -641,14 +641,14 @@ def key_and_zeros(vec): agg_purities = [[p[0], p[1]+p[2], p[3]] for p in purities] -fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 6)) +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) for i, data in enumerate(agg_purities): - ax1.plot(data, label=f'{labels[i]}') + plt.plot(data, label=f'{labels[i]}') -ax1.set_ylabel('Purity') -ax1.set_xlabel('Module weight') -ax1.set_xticks(list(range(n+1))) -ax1.legend(loc='upper left') +ax.set_ylabel('Purity') +ax.set_xlabel('Module weight') +ax.set_xticks(list(range(n+1))) +ax.legend(loc='upper left') plt.tight_layout() plt.show() From 5b78c3cc425375bbe486bf7bb00234ba3b5bde6a Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Thu, 21 Aug 2025 09:06:16 +0200 Subject: [PATCH 24/33] merge master --- demonstrations_v2/tutorial_resourcefulness/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/metadata.json b/demonstrations_v2/tutorial_resourcefulness/metadata.json index 1a47de7af6..7e3d2cade0 100644 --- a/demonstrations_v2/tutorial_resourcefulness/metadata.json +++ b/demonstrations_v2/tutorial_resourcefulness/metadata.json @@ -26,7 +26,7 @@ "uri": "/_static/demo_thumbnails/large_demo_thumbnails/thumbnail_resourcefulness.png" } ], - "seoDescription": "Find out how much quantum resource a state has by looking at its generalised Fourier coefficients.", + "seoDescription": "Find out how entangled a state is by looking at its generalised Fourier spectrum.", "doi": "", "references": [ { From 6d8d21c2222d2d2a7c70da42da6acbf719782f48 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Thu, 21 Aug 2025 10:21:32 +0200 Subject: [PATCH 25/33] minor changes --- demonstrations_v2/tutorial_resourcefulness/demo.py | 2 +- demonstrations_v2/tutorial_resourcefulness/metadata.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demonstrations_v2/tutorial_resourcefulness/demo.py b/demonstrations_v2/tutorial_resourcefulness/demo.py index 3d6985940e..4e4c5b0a62 100644 --- a/demonstrations_v2/tutorial_resourcefulness/demo.py +++ b/demonstrations_v2/tutorial_resourcefulness/demo.py @@ -279,7 +279,7 @@ def g(x): basis3 = F[3:4] -# +####################################################################### # 6. Compute the GFD Purities # *************************** # We can now compute the GFD Purities according to Eq. (5) in [#Bermejo_Braccia]_. diff --git a/demonstrations_v2/tutorial_resourcefulness/metadata.json b/demonstrations_v2/tutorial_resourcefulness/metadata.json index 7e3d2cade0..3b7d0ea7fa 100644 --- a/demonstrations_v2/tutorial_resourcefulness/metadata.json +++ b/demonstrations_v2/tutorial_resourcefulness/metadata.json @@ -2,10 +2,10 @@ "title": "Resourcefulness of quantum states with Fourier analysis", "authors": [ { - "username": "mariaschuld" + "username": "impolster" }, { - "username": "impolster" + "username": "mariaschuld" } ], "executable_stable": true, @@ -19,11 +19,11 @@ "previewImages": [ { "type": "thumbnail", - "uri": "/_static/demo_thumbnails/regular_demo_thumbnails/thumbnail_resourcefulness.png" + "uri": "/_static/demo_thumbnails/regular_demo_thumbnails/thumbnail-resourcefulness.png" }, { "type": "large_thumbnail", - "uri": "/_static/demo_thumbnails/large_demo_thumbnails/thumbnail_resourcefulness.png" + "uri": "/_static/demo_thumbnails/large_demo_thumbnails/thumbnail-resourcefulness.png" } ], "seoDescription": "Find out how entangled a state is by looking at its generalised Fourier spectrum.", From 76f3dcfbf85e3c12eb5b537f8a5df4b730d410d1 Mon Sep 17 00:00:00 2001 From: mariaschuld Date: Thu, 28 Aug 2025 20:06:24 +0200 Subject: [PATCH 26/33] polishing --- .../resourcefulness/figure2_paper.png | Bin 30159 -> 233298 bytes .../tutorial_resourcefulness/demo.py | 153 +++++++++--------- 2 files changed, 81 insertions(+), 72 deletions(-) diff --git a/_static/demonstration_assets/resourcefulness/figure2_paper.png b/_static/demonstration_assets/resourcefulness/figure2_paper.png index 07486e216bee01f2d374a292bf90a0ffbacd609b..dc037c913e6d2aaf6f0e6a481be694bc71f44605 100644 GIT binary patch literal 233298 zcmce;c{J30{5C!!WSyi?_NJ1AkR>@7!Cn>&s!M)sx%Giw{P>3IiZds9B-LD>%?w&T(lc|rw7n;syt*N9=-$Z4V}Aig&XmAZj6?+I;)`6O zEcY8L@}l=RWz_FwYe-S1pHC*`^cp6MMokzVIF*i?yhkE$jv%m;5PAC$EhXZ7T4}7n zEV;uYael3#64yTSVWzoIf`_xGJ)yz;-FmC9uFSmyWRNBMLrBmECgioA$&`!S8~_qTxzbk%qDrv)eTj7 z@O%vW>b1L^+(aV2URP$By!knEp?a-q{`U(XNnR%s+h|s9*V_u;Tc@sE$+fAHxB~a) zZn{|{zQsS_HkIgrWp*98cb;6-d2DxUzDe=Ypn%J4;0bG?@tyPa-Gcx3d+>j<$q&3! zE=jyOQnS-v6UyVW(dvL5+UzdTe?Evje7MTqYlM)c)(NY3$z`_XTz;iQ1sh!Xn+(Iqor2*TN5=j}EAHgo; zp_j(q!NPy+7eZU1PyV{E{|B-7f3gn$n_#Dt$UHI9x|YvM8cALi&C~@^{X6#c(f3*F zW?guUS3Xm8h>F)4Z0BD})h^)vfzdzahtmFL%(6wlv{AX;$4fC_*Th6;iw{BkBeq)L zXz5z@(N{_F1PBe~wuR}vDc;>}<@|SFY&a@s*7Ec6#^(2S=cn?R#J#tl&8)njM|Db3 zi>;3gU}H=#{LW(G|Ase`)kW-HUUESTD=zGL@5Zjz&d=A`RHhZou2B;c&h(ee>ss5f zwg3JLc46ftp`ON)nu_vCQ8rr7orDf!UW$(8Jm;`KY2WE)J2pYPwY%ISc#dcn%Q7!p zV3<@Qn#ldP08I6`(u`s%!dQdp^2%PdleFPt;QzBp zeW$2J3xm&4tsEQDj6QU2NRmE~3h~j2DbUDJh;gjXGGEl7U&Q#Im##ZPQnB9MkkkL_ zfr>p#kOe}D6fQ!s>p4z%X+wmHMa*+()ji{Hp-y~pVb>E4L<052wM*Z9){H1h_RG%xC_tH=Ek#PuTKG8QEL@&d z|GTB7CAWc%xU%&$?%y>bdJEyr52OZO3%zKKbPnP0hC&*|Ygzhq3837kOGI zX@*5TW_Edn{@=@{O3$jTGSI5~p4D4_vAj>JPA@Q(141RZP`}tj>sURnzp2&@^`l1g z^>&PK@!Gk8%_jH#NT`$Y4 zW}MB5M7+OtN>=6dn>UX7N#p;diaUccU`@Wc75>_MYVfjTo+yMZBS}&3 zbz(V-cy>kAXCiJiv3kx7+%?-v4%@V5;eXd&P(xX%U|u{NT_p86W|!VGP~z)-pL2-1-7BajQ1>a zODfSKDT-L9w{}q7CYr6{Gtgua*v7OCH(eRipGL?guf>4T-ND|SHSv*&>bZ%<0teyA|OiDfP zIg9HU|Hhvduy)bZH*$;o<#%@uqF0*irfiYpLh_MBQltKBSe6cosk_dzi6FGCspTG&?W( z`>{YaT3N|r?Z%cCGNnXeC;g<^fA*$tM1zkNYuSN z9NbIwe1nr-x&;}VKVv9pG zJ`hXnW^s^lwbok+`r*nEz5Ai@qWepC zH*p;eT}e!T3eAt~Vt4ysue~z8Q;#KvybolIN~^0gP?^qJWmrh5(4rI$G}$2`LgLab zj_=pV^1Oy+5ei~Lgha7+0P=vPr6t^O>1g1I5ux@Cw{JzYe^Nj{`dt)hat)3&Rh~|==)zO5;m7hWtf)^UPCe%c7I8bj#cyf6?8TCYgW5pK zW=Ky=D|>Ihn*^tR2#^nuBEX>2zT721d|xTw-UM_$wOiK_iHE9m`3ctLhcM{Ye8sM} zYc+d2Xr*#jHcps91mN^{3H0ND3OzSPJmOgx_u_vT24*cR9Bdib8}oOWi{83Sdj(ttGa1 zdit*ZdY}B`a>)#%)g(!ScVvT89T4Trl2|8Fwuyxq0madBZb({-cqsak$oKUjv4QeX zbTv4mTTS0+pMPuj=w_C`REn=srXRp8R?-`}8XY?g^9TNks*ke=OO1#5{BIn~Yt{-f zij%&~tAlkij}H`3G}1Z-VV|A-n(IH}YbVUu349?18jvcqb-Wa6IqI5zcx<2w0qwZK++eG|t++$( zy`Qu%q&Z7^fbp_Dk&Vw*zrm`OfTDC-=hKR>Tf1(DQt|sHzB8>{;4(i%UPbvUQi|C9 zYpd3B4Ob?tMvv>EPtG&mORc15HA0hndse)|vT=Ejz3b}M$2A!_BCjp!#h9q;q&eiOB4>)!yW^Ub8I-%45i!XdT zI(uc$fkv5e=6q#7=aDCsXHqP(tt`3(&^t-l^#Y2}R63R+fvwzH51HH;#mQWtUw9H*oRl7^gyF5+HAg|J^=O&H$3Ghtumu*L37@TU>EH@c7ApdNN# ze=B(I_LG68l&pso;f3NyW$&44EqJj2o>8EItiyw;8?>@{C1{;M`7d{mMl}<4P19yr z+_tbdG`>i`SMBYJaz z<8T>)@}XA=<1*KxXXpO(cw^@Yu5_!p2;_l%oOCPZ)YM7S zdo&2Qn+fAWux?PDZgD|vd35z8gz}3Qr~Hi|Mo&tgQdjS^F1e7ju@guzHUTJkar4liHJwF# z>J1Lo*j6Jnno=S1J%cEzJ#l%9R82jau(&7+%hf7HA87y43D^T#hxl4A1%P*x5S=pJ zt2M)Nktise(0Fd?=)mH0#J0kZ_>x9J4Bxy|<3w~zr#GAM(XpGJceL?dPEg^XZJ*5P zJNg%XMXn_7U4D3mYIUEZlC>ZC_+~|dBZupN{xg>+ZBk?n$X}g(f_Ki!8E`X4^fh<~ zNDhc2HO`b?EB0V^fQhtZ2ceq57r^Yh#bkzJGKE~^2w=~h(~gE(F(Qq#x10W?CAWey z^Bho(af zJZlLEwo0EGs0(C{3hfVfQMkxihZEB2T(|BDCNN?Ye|7mc{Q$yB2EB-kP{;D@9+?W= zamgzrb+!Ad5}yI-4uu(mmR6zd{Yv~HNWM&$N)pLRdf;1mn0r+i6tTdkYdG+}IEpneVN43rsE^&}Y6}rg*N?I0K zucq!20IE>W`-JT#h8O`5FcJlkAD;n<_k4KX7!W%K{Tb(U{qzw>VJ1l2t7#YL*wu5? z8IX#nkNj}2DmzwUW#F+oQgrQc z=~fR`HP|ANQu-CNQYha)G*geAc}L-hc;B$DtAV61M318$0q<~oOk(AoehlMXbiW`p zcO!cHz?;sufqy`f!ioo{KG)AT7GZ|31_4E`^XHAA>b~WuVMT10Irv#D+U+ZN)2~a|@@T)^i+A}~b zAb5|5ew<%Z$@yXu5QUV|xNwy0AT&JbAS&P#N#iB1tAD6vr)+LNxowkZD^g0U^c_ES z17FZb0Yzfz?CcheVlLjFnH$XB+Y)nF3aC;K=Dc+JwHNm}f_dpFpgGeWl6s?85^3n>w!v6cH~7ygXan@at7UQfY8jcx77B!U|*%Nt*>L;_Z$vT#KnZw!qC9>3{;xnK+@d*G|Y`b#=wx!d& zmiJK^@+^4}CR?UO3e}I9lG>LXu(}Ryc{MeIe^nOlYf_}1)73|x30>AHy{g&&zE{q~ z{7K@+m;m?(p)kK`N0=BzWSj*~Y!UB}oW?;0^wC`4KJq~0?Zfa^!}F~lQ$|GtCaoIeKGvWE^NHeMs400m8jRz`)WwUE{SvG^It)obax+$4I~PF${!bCo^` zLLfnrlv;nn0NE!TLu<&e=^INd$(2hwD2!$0*pDD`=nRevN@w4i`TGD?I z0g4x3r~MvmD)D5Wl|f^w4PpEA&Kbicvm|1p`apo5;&I26-Pqehuk!NdJ|$c!`6tp$ zS3c8jReS!#hluOO#Ih>Sape>dfc_#;aO1(~bxPfSC-7_5k^?@%PBH$sK9B+=Q+LiMiUjEw4GljA30S>@T|o>o|My(gWga z;mn?#G!;l_b65~`q5Fe}%gwK!zF=K}Bdy;jxXKL|Dwfm)5x^W0d}y04fX;{RhtjrR zVACx)r6HVvxosx_w^7O@WFu;s1!O*kQyV`(qbf~PD}tOC#XY^?-2JT{j%{H&^&GRGy~8F z7MdyG2*^K3IU&YWUG7#d6KH=!KQb=OuAUoo+WcoAh!9x_owEB8*IB<6&nHnQMx>|; zd8626VwsSg{UDVTYDx7xlsB!cCjpnBIw`7xgr>T=_CH;dzV-R-8J@}XP#$8V}T!4d;!vaOGyS>}_}WuV<=4LGINcc~w$$P|{Z_#a6*z9`Tog9k_zwtJh@F z?Y$#g?k$yY*yea%;z9lxQ#R5A06>Jo*!+P?%SW2z*@J$|zz)n6`t^QwR^g0(jll$R z1mDf?H~~f2cdPywzhZUuXuq;Fj{5+dVq2l| z0dN8Yp`DxhU@?ACm#w0mA>5&2NHNMkbCZor74DOjFKZ%AK-hxyhWjhza)e3J8xCyYM z?d*(41F!!+hy&USVG?5sun7<1NDoXbyY2VDUQ8%3QdyOcxD^a#MB#Uo;SQ>Vl~gc5 z-iUr*MlH#bnhv1T7b3B7bFsVnd&->X@;{67!mE=vo~#6?hDL+h4_YUv;d!{i7JN-~ zgWnJ#pe93O1_?qy5wHgkk=|pPxf=V@B?@mnmIM%w?OfQZ%@r;E1WbEIf$5P~! z#%k;KWpUZM%LqM9Q%mhtakjSuNOWrJ_>4}NH8Ti6$U-s;oV*0+4Qa+gXELSZb3n&WeEO3lh zGT)sm5~Kk5-`CF%(#|FB$4u(GS0z2GdDCga2jX0U6pD?=cZJm=la1yz^OM}%+*#Kh z>#;|_O#;F;8p?`x9kF;JpVf@X*J6Ds{UXBQmyYkoyia6f{N7v&lvgMe^JsR`bGsIw zVF8>On8A|A3H}|G>4DBdAOQt|;&GV}Ztjz<&|5=Z&;Fv;LQvRUPaYFd95uSjfXJWf}Wgf3c&)DA*xvhlMfY*f#~9KO6bD- z`dTHd0aegTEYAMNfAJaSCH)|)1V~AW$sJ?w#fYwXIIW7FAh~CDpS89#O>Qx#W;2Ip zi<_yb?#j?&SeYe=1FfyCJX=*}A(Axb9rt$DjOT}{=-f~IEKWbj7HTggHFcUD(;P5w zK^)vdsyS69o>%NDE0ffEFd;-WCPa+r!Gz+axbkOdPuiG3a0oPdkIVuF#TN(4I-ph% zs6hF5uAtPz762gE!GCmK-A&I;DXEw#TfkpH*g<=eB%e>zC;{T&DT zt3>L0;wk6J13|wwqK7gaXauMkv^=a69Wg&NGYlH|5dz=J=>tKpgo|iELNTBk>J~sl zH{*QdUNw|~1hE}T8N5Gq(J*76skCGPiflKLMC5oZviR;&^p+0pcy);%|llCAq$zXOa9-fmF z4MjeqQ-8I_KBs>`{jEMSU0nJrsROKgACv?LyK|f&>tV~|Ghk_8PC-f_t3{m!YxP^C zyTS231)H6n3R+H>F)YEx3ZA?jm~}n@RUW1)kR0c9%X9;A<@Pp!DSqOcDneXc_dC@| z!Ir)iKmm~`SXfs`#)-!ZAq;;&U!VH_Y%O@)KiT=NOdb{qc_3`uaDU{4z zq3BaPJjenFl%0JWMll~b6lfUO&!0aJYIkqD26rrg?`cOMW6) zYc>?sp9C8BXHE{IH!+$GyvRV9E<|ImBvOSji@G1P+1Va>js7V=v-hb}i`&zX{8UfQ zlC=|<_l2}V&xYj*EZb~e!8eZ-q{weQx$iE?g7y(U9m1Rj#7TL^1D1T+ro zGik4-!BgCi1}7CAyJDs)fE9RxRw5AsR6qa(H?!V*A#j8xMPAmGNwb<*Cm9t1I)4J( zXs-tU6K+AeJ*r>Bxo?qn3%1n5p8qzI#xzKCJ9wi!94IUFfdmb_U$Y|%i)-B5QAed&Nk zpEwr*?1VlLcs2~o_9Y1D1SkcQil?SpuxOxP7IN*AR*;iGDY#n<5bc@E=exzcp!sq} z-{7D%DZWW7>wwos>TQLdjH+e~&r6fuyY_-_%>N<8CACep0rW4D4Df#J}JaE;dDG2DKz>5aB~D$T`Mx{78lGw7}&qu zy$F;XI42NM>2#C=9(1D=zGjDku$9p@Jy0y52>QKI{MQVmg$OfDKp~u_<4VZFeOCla zfUfP=H;@b>Q9||;q=5~i3{`u}5C5n@i3L02=+U5X!Tq_q;T~i5=9uqH=y`Ol`%7rR z`?Dp8A`nAb(zqO3zMeHbZw*qNR0;xLJyBSvPAftD@PIoOlqTTtr|@dg{1#xCr5|rl z3b46gSOC+P=V>+~m@fi)MfW>&B+SmgXuuRreVAH?7cc6yn$xEb6mqtz*tkhY0tUvP zk4^@BZ}Fmjh3H|7fAwEPi8shZB^2R&;6uRklQYT}i@5#^ls};}3Q|ROHVC*-rIAvs zuzny=+`C&W42o^Eyd9%42+9Z$#8Ap1mL>i6N;fez9KnF{Ao#y>h8138$2v^~+7jZ} zZpLRUnOUW0bAsYw!q;p64KwA|Q{ggCHMAMf_(!}_()rC=LZnc&{M2=PL3~6l_YWqaXfj=FBM@kS#Lx<})cSxy zD_ON>M-_Sn;wLkH_GKcPb^sAz)c~bhdG)*Ucwk~bIWb|-cqVzgJePwEUL~(;wNLSj zBHhWqtx)@yiH2`bS1YRE%~7GRoN6nLy;o02173wF~71j$73foN>LF}Ul z`~_xbm(CQn;(=JylY{?h?Gql{3QTt)ELlkbltQU{Z8I#kJubeRMp04Et4q(W6?0Hs z9fB7q8BEzO=DSgP4mSZ%EU|K_p#NJ^m^{`aqJ(T;mn>9gUymqZ3Cd{e5N!EMd=k$U(c^NBMbWw^k* zf^*0o_$~l-P@7-9diAVvxMkz7w&*-DGRj=N@$n&A<1I&zxL@9j!c(&^9Ks4QeEVax zjIJ!kJ4{Ul6n2Hs>%EXGFno+ez2>sy#+PEqCHvC=AhUX{q`Cw^p+9dOXaw4Ct0=(2 zM?a?py>dX_Fz*N5ak)JuHp!K1J;vdq;9q_pGkX`Ll3;eN;x`~%!+02WDC$hfA+`m) z4|$?FDs$tK{qEltjxBb-8|+bCxwH7=GVai~J~d*w-efJqs5TKAsaiWm2KKYT_jxHy zuV`V2Z&B+5YXy#}cm$+3Kten1tbRaSyg zMNxdvQQ*0{+~ac<^Y8G!WyKB;aA*v8F1PXV_R+IfIldG`(1yr@?xC8g)Z8`Aw=dt$ z|HvP|Kl*yM2<2a!A{}HqQcD1u=iTDq;qA8JXSf$4*f19Hu;iT?GemdL{n^0^x=TZ@ zY1`rVe=#1be}pz;oN|MpHXm1uc5k70kzfh9xqh+0!0hb8;`32IRlR23M`(PB^|dd1 z)N2(8(`G%;IcbRqApBweR;(SJ3}hZ$J%GEc>W)Z(APWi==ney5Om>Kt=ZP*Z8?kjO2*6yXBkTb6l3Nc9I3`(_Eg*3BzwMVs-uS#u3-Gis4Z4 zbw6>5n&Bp}a@)0@FR5MIaaiq@A|3FUuUcO**mLrnJZDh1HMAyHxwo@#&GrX=Ozrhe zdVOF@tYn&n=U45XmXD-$8S!Z{@53p5pP6%IW&k;h1*M=bRZ6lF|T0+-vDM27KyR0m4?TGDu3*RLT z7g-|pUhrcXVVZXhsx*r~Jp}79COYp%z57FXj}~yk-U9XB>zriepF}dBnc8#Qjf>?3 z_#Dm>PUK2|Tv@>nv>Dj(?$T_lwU)lm;C(gKdfIjeoF(njEeDud3z3kT&?Z1mgYy4M zX5VB5C>BB<$Z}K<4AOx-f+hexz<1^K%P-pb&tSG!m~cjc2G#*YU=YH_k!Op1_PjmE zjwIKf5nDXO@Z3~gZO7Y3^_d1a1bmlbeCSyR@OxSmzzTV>zP<-!<$>jt!E)s3f`*;>6SX}~>%~o2E zZ_^Wl#PDk%tPm)lG?lVH8U?i*Bs{ot=s3u;6)_!CpztO3+@XIr@GSjzBX8%BiJZsn zQAdpl1&#~7pIQdb1YmC%9_Rb);0nsigAKOF=ydYRptK7st_}N$i-8TMHGg=mdJV0e zW^sIICC`Nb0q)>O;(N>D!^OvnKrg7Nr}kbT1Vml49#YfexYs1UdjWsf<8fw5b#Of3 zzYlkc&Xe5#g;>y2JuRebqOS)dew@by(geJrQHuR z{z#O=neszyNLieC5rjMz*NoSZTVa6n0A^d@l;CQOMBjz!_n@^V)i7~)L+YKPL;V=j z*F*?O!G^ifCy z5J$lhWYS77NOyu*Arp}5c>dmI{#4#hU;a*CjO8T_GST%BzjgdCi*~o?z1J$1Se7e> zcdT{2HzpIh@&3Ssyn~Xq-O0ZOwE4_~`rrvae^QZ-Wo+U&P;Npu&i;1<{Qwr@yuG>kU2aI}qUlZeg|z34F-yOiDLDP| zO2^1vW(SWqg?9|v2qw6W2GC(PbjpNy<%KDew+5yJ6*YZYb%qKj`_AQ+-k!thSeH${ z%8mrGDkr|FvatepJa-;DXxl*PV1O&<&Pn@O#~{Rlutq`>1Y z=uU>b5-)B>u-$YH))9Puh5vXcjQGIe0q$`O+o#-;p2Dq3Yc7rO;PW`0jeSo7qYRc~) z$>|EPbS$RqaK9EPe&SqkVSZ7!U~3`ltw@J7tt?Dv@ni)}kqYAwHBJCEpkDL_c3TMo z{(_D5uHlhJAs{0`E{FLCWG8}^#5Q){@327u6%)w0e!jH&msrY(yz0&G0cbPOweekb zG4N8QQCHQ@*4lVf@-_0;0fiS;b4n+U`p#hGR3PV z=f8)GZF1mii?OMxX=^7;VMfbwc@5BrO+ALer|^kN^v6f(ggo_}F9EocF3fdEx8FjfY(Xn3+^ShcxfB-cNgfUQC7euHL zCXJuoS?(hw!rCzD^eT@v7;0#tS8n!hXM*Vi(pbl)%Bhui+GBW&*}8h;t74o)!L7G| zP|mEmo;7^@Mw(iDdm+QJSQ|aMnY()r+ymP)X5v?7JH%{95cRyCy}#S1<7uU&_|YUD zrhaF|c40)<(>BU?-A+vS!OYnlVSvprvJFlJ7gK58IlSF@vg3Vr6bu_qM>T`Ss?I)X ze;K@)c%&QzPYD4|&qmbU-E9)0-OcbBJVngmcux`D`ldhwHx1@~Wh;*|){EoV>m_tDSNzjc3F+6fR%iUTWoO!s)LGGhT6~2%=^g z`r)zH7z`T*((mNtq$0K?M_&6!_{>P1U*^t+IGlbvHl(Ac*I2W+RWnY=vX0mX5NqD^ zYqkKn-am={S#!*01vNs$-q1?=Zh5HPxc*h57btfYv7t2={6StS`Ie3l78SE5Q4CT z2&C*`kgdR)xBRGPDLUWk1p91nk(^kQa{!|bQ}#+cwvrg%LFob&@Q3>F%&yK!Ph{su z7f8tv7q|5F^@W0_Z`ZS0kic2Mvh(>in(mlyEPZgmz`|!&*=Muse7?`T$8ZB4Ighm2 zc=L-b$90TQ16W%8C`~>3HNoZ`&S1}ddvNlK({n46O0E*hDMFijaTbxo^iv%7$!O)FszF_7ngRxc2t3Bg|m;&piMe#9JMi z!vQ^@4nYV&B7<87wjNLiEEAIGmbP}}@*_X|rkZuNFXrC~z>=V13Vd(6G)ZSz)zZ?LNZkIPQZ@;Vi>T zo2l6+4ecKPm?`Q=TAi{{Z*=_r`Rw+Z&)%i#A1`>C_vrVQc=l*Opao$0)={{zW*1j8 z2175`g*uWB^9&fE0y^x^Q&CsXsQd{l9WDNql~`>=55eqO=@udD6!8lSPUYLJ-g3{{ z?i7CE3TEH%Fvc+7;%Q&-G6=lRLy0%?0ha&O`x{Y4vNzXW(yNT^U3!;P+qd zs$)tC9ye58g0cXu@<4E^mD}rXXTR6#>azP21Q=2w^sOAWzR8J~pC!MgrBw%|xNLd~ z0QX*a)fMWPnLOOPJ+ZCsE%&uJ6Vm1zy>E#Qt1dj3k|Aj1ye4zwjep_wqX%V;qRDTV zS{!#3zt1K$Ik7?I*gQnNE z_)B&=S#}PKE!?C2KqNEoIdx@GZ+Jy-dy2<{$+NAefrV?Gm za;Cr&EJe!Zk6aqq{EoF;Z>do?o~IjOeIZV)aKOOASXE)htlNz?kPT`DoPMAn=B4mk zJE=&;@>-p@fpsXf;^s8xg(kP8LqQ^sB9|!=d z$<+W~0a!->_yEI4iFLv0O@doF9z>F zU}g_6dhg?NM^%PPrsYl%RMrKrGH}9kmCH)dyE@H@x&%(*EKh4Lrpib9 z!zk%;?h~a3sFpLdK=4Lq5AxK4AtM(FS!ayQ3_rx46de zXW=m=C2qV&hzP)Ehf;{&@bVNm4(Fm!G!NsD4|l44dgga#E>r1)fBNg!ukhx!)1OZD zU__UNFfGn)7qNz*3YoP(bJI9+y(3^sd18ez_^wt>WGU-(uZX5voYUtE_dg5_417QX z>)C5LGMuxriLFU0>1(j*!tXNcjg{>c!hTYXVT|IGi2wMb~T zRB2^_a4fn~z+eNyzZsKZ;mPz!6wZQ@-uDfX@3lIXs}T|;Z>gDk8K=!4&^eL*-OgpOydk;mI^ zV!3*w&8B)RYv#m>6Rzu{LCIgXyieov0t^_cO-J~GPDR{ zApUG$gi>byrjs}n8z$im8GJQ_Xqng;XW28Q3-_sD@TlRSc>J-tPT8Dr^$b{lM%ZW) z@b(0llH^qkrp&;jqP<#^Oxj41qo=hniFciBMYId=E^HG=?{pt|lmj)fX_}#A;A0PA-@1^i5wys=!7uQdfz!hB4;WTKr z-S?|ANG0th3H8_0?RsxLJ|}`ImSg)*S$X`r5<9_`rO*VtkcPK0aFNgZ>}?UutQ`U*4yqT(;Ej#4Vn#nM z`7B?8+)_1)nBM|A_0dU`PbS`Tn1mtlaBs@n>R5OpaOAeOwjOm1RCgmgyTds-IX%6- zalIyR>b9byB53;r6D2K5?*+E<>qY(&CXLdgM^Rvp9RL}(W4+j>ViptEpZ6w{;nZD| z@SmMe>OFb?+z@h2{I&D9i!qyu980VKz6`~=B1{?ba7)e4nj_I#F>I}T)Q?i7(jA3E z!6UeTmVqzsgW7{9t&=bY`eSo;au17Q4@R=d|C4Z^jw4Zr*X2^lQVzGg=+>FJRxhzcPR6_yMT?t5$5l;r)A;B6Ztc+(EAGP97A{ zS@a_fenYNW^}e;3I#401O96}pDzx6&@#8|CgP{TPQQ;xv5j`jXrk<%U_YLrPr*nub z*sQ^}1#)q04Ys6aXBJ>02vWfKE0u*v4}cSOZ~cQ<4cf(p4i*e%P*Kevwv|3ey?zNR zc8fBo;oR!Urrbhs8NivoXW~9$_DESIkqI9|h&%Ngp=Ti%_)DrrDQ$#) zT>MqV{3~B?x6ujOdZV2~gCqQ@Xug-Q1=oK@bxz0}-ikU1_Bn9(EjAxl>>6&|s+G5U`WeMLwBG zQwH!U;SZ{U_uY1GpiCO)5Jp25nRG9CBQ7~kxRGm)qlT4q0md(j$p7Bmv@46l^ znPZSYYQPdEMnr}n5|Y0gE)`iH_wn0Ea;>+a!0}Kx&#~|1`{K0h);}EyceA=jHDLd%clcG7Z2!bwH+9?4S0On!R5q0

^FZ*XC?crX!=}ST9^2h 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