From 804cd76454bd585cfadfb93588bc3fc9b80bd533 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 17:14:30 +0900 Subject: [PATCH 01/26] concurrence, mutual info, graph init., and min-cut --- qdna/entanglement.py | 169 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 qdna/entanglement.py diff --git a/qdna/entanglement.py b/qdna/entanglement.py new file mode 100644 index 0000000..46b453a --- /dev/null +++ b/qdna/entanglement.py @@ -0,0 +1,169 @@ +# Copyright 2023 qdna-lib project. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import random +import networkx as nx +from qiskit.quantum_info import partial_trace, Statevector +import numpy as np + +def concurrence(rho_ab): + ''' + Concurrence specifically quantifies the quantum entanglement between the + two qubits. It does not consider classical correlations. + + https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.80.2245 + ''' + # Compute the spin-flipped state + sigma_y = np.array([[0, -1j], [1j, 0]]) + rho_star = np.conj(rho_ab) + rho_tilde = np.kron(sigma_y, sigma_y) @ rho_star @ np.kron(sigma_y, sigma_y) + + # Calculate the eigenvalues of the product matrix + eigenvalues = np.linalg.eigvals(rho_ab @ rho_tilde) + # Sort in decreasing order + eigenvalues = np.sort(np.sqrt(np.abs(eigenvalues)))[::-1] + + # Compute the concurrence + return max(0, eigenvalues[0] - sum(eigenvalues[1:])) + +def mutual_information(rho_ab): + ''' + Mutual information quantifies the total amount of correlation between two + qubits. It includes both classical and quantum correlations. + ''' + # Compute the reduced density matrices for each qubit + rho_a = partial_trace(rho_ab, [0]) + rho_b = partial_trace(rho_ab, [1]) + + # Compute the Von Neumann entropy for each density matrix + s_a = -np.trace(rho_a @ rho_a.log2()).real + s_b = -np.trace(rho_b @ rho_b.log2()).real + s_ab = -np.trace(rho_ab @ rho_ab.log2()).real + + # Calculate the mutual information + return s_a + s_b - s_ab + +def initialize_entanglement_graph(state_vector, n_qubits, quantify_shared_info=concurrence): + ''' + Initialize a graph where nodes represent qubits and the weights represent + the entanglement between pairs of qubits in a register of `n` qubits for a + pure state. + ''' + # Create a graph + graph = nx.Graph() + + # Add nodes for each qubit + for i in range(n_qubits): + graph.add_node(i) + + # Add edges with weights representing entanglement + for i in range(n_qubits): + for j in range(i + 1, n_qubits): + # Compute the reduced density matrix for qubits i and j + psi = Statevector(state_vector) + rho_ij = partial_trace(psi, list(set(range(n_qubits)).difference([i,j]))) + + # Compute the Von Neumann entropy (entanglement measure) + shared_info = quantify_shared_info(rho_ij) + + # Add an edge with this entanglement measure as weight + graph.add_edge(i, j, weight=shared_info) + + return graph + +def min_cut_fixed_size_optimal(graph, size_a, size_b): + ''' + Optimal solution to the minimum cut problem with fixed sizes for the sets. + ''' + nodes = list(graph.nodes()) + min_cut_weight = float('inf') + min_cut_partition = (set(), set()) + + # Iterate over all combinations for set A + for nodes_a in itertools.combinations(nodes, size_a): + set_a = set(nodes_a) + set_b = set(nodes) - set_a + + # Ensure the size of set B is as required + if len(set_b) != size_b: + continue + + # Calculate the sum of weights of edges between the two sets + cut_weight = sum( + graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) + ) + + # Update min cut if a lower weight is found + if cut_weight < min_cut_weight: + min_cut_weight = cut_weight + min_cut_partition = (set_a, set_b) + + return min_cut_partition, min_cut_weight + +def min_cut_fixed_size_heuristic(graph, size_a, size_b): + ''' + Heuristic approach for the Min-Cut problem with a fixed number of nodes in + each partition. + + O(k * n_a * n_b * m): + k: number of iterations (vary significantly based on the graph's + structure and the initial partitioning). + n_a: subsystem A number of qubits. + n_b: subsystem B number of qubits. + m: number of edges between a node and subsystem B (typically equal to n_b). + + Example (n_a=n_b=n/2): + O(k * n^3) + ''' + nodes = list(graph.nodes()) + random.shuffle(nodes) # Shuffle nodes to randomize initial selection + + # Initialize sets A and B with random nodes + set_a = set(nodes[:size_a]) + set_b = set(nodes[size_a:size_a + size_b]) + + def calculate_cut_weight(node, set_b): + return sum(graph[node][neighbor]['weight'] for neighbor in graph[node] if neighbor in set_b) + + # Iteratively try to improve the cut by swapping nodes between A and B + improved = True + while improved: + improved = False + for node in set_a: + for other_node in set_b: + set_c = set_b.copy() + set_c.remove(other_node) + set_c.add(node) + if calculate_cut_weight(node, set_b) > calculate_cut_weight(other_node, set_c): + # Swap nodes + set_a.remove(node) + set_b.add(node) + set_b.remove(other_node) + set_a.add(other_node) + improved = True + break + if improved: + break + + # Sorts the sets + if sorted(set_a)[0] > sorted(set_b)[0]: + set_a, set_b = set_b, set_a + + # Calculate the sum of weights of edges between the two sets + cut_weight = sum( + graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) + ) + + return (set_a, set_b), cut_weight From a175ef81b7ac34c9e9bc868b5a80570961c20d78 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 17:14:57 +0900 Subject: [PATCH 02/26] Test product state --- test/test_entanglement.py | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/test_entanglement.py diff --git a/test/test_entanglement.py b/test/test_entanglement.py new file mode 100644 index 0000000..e1c6984 --- /dev/null +++ b/test/test_entanglement.py @@ -0,0 +1,67 @@ +# Copyright 2023 qdna-lib project. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for the entanglement.py module. +""" + +from unittest import TestCase +from math import isclose +import numpy as np +from qdna.entanglement import min_cut_fixed_size_optimal, \ + initialize_entanglement_graph, \ + min_cut_fixed_size_heuristic + + +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring + + +class TestEntanglement(TestCase): + + def test_min_cut_optimal_product_state(self): + n_qubits = 3 + + state_vector1 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector1 = state_vector1 / np.linalg.norm(state_vector1) + + state_vector2 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector2 = state_vector2 / np.linalg.norm(state_vector2) + + state_vector = np.kron(state_vector1, state_vector2) + + graph = initialize_entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + + def test_min_cut_heuristic_product_state(self): + n_qubits = 3 + + state_vector1 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector1 = state_vector1 / np.linalg.norm(state_vector1) + + state_vector2 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector2 = state_vector2 / np.linalg.norm(state_vector2) + + state_vector = np.kron(state_vector1, state_vector2) + + graph = initialize_entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) From 613fe3de3348358f718000433bf8e27489e316bc Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 18:43:01 +0900 Subject: [PATCH 03/26] Use scipy's `logm` function to calculate the log of the density matrices --- qdna/entanglement.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 46b453a..b638bac 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -17,6 +17,7 @@ import networkx as nx from qiskit.quantum_info import partial_trace, Statevector import numpy as np +from scipy.linalg import logm def concurrence(rho_ab): ''' @@ -48,9 +49,9 @@ def mutual_information(rho_ab): rho_b = partial_trace(rho_ab, [1]) # Compute the Von Neumann entropy for each density matrix - s_a = -np.trace(rho_a @ rho_a.log2()).real - s_b = -np.trace(rho_b @ rho_b.log2()).real - s_ab = -np.trace(rho_ab @ rho_ab.log2()).real + s_a = -np.trace(rho_a @ logm(rho_a)).real + s_b = -np.trace(rho_b @ logm(rho_b)).real + s_ab = -np.trace(rho_ab @ logm(rho_ab)).real # Calculate the mutual information return s_a + s_b - s_ab From 584223f4052a64ad113e4906df8569179acdc3d5 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 18:43:59 +0900 Subject: [PATCH 04/26] Inverts the qubits that are being traced over --- qdna/entanglement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index b638bac..b6c6b1a 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -45,8 +45,8 @@ def mutual_information(rho_ab): qubits. It includes both classical and quantum correlations. ''' # Compute the reduced density matrices for each qubit - rho_a = partial_trace(rho_ab, [0]) - rho_b = partial_trace(rho_ab, [1]) + rho_a = partial_trace(rho_ab, [1]) + rho_b = partial_trace(rho_ab, [0]) # Compute the Von Neumann entropy for each density matrix s_a = -np.trace(rho_a @ logm(rho_a)).real From 38aee459def861c58d586a825c4d69276d1a95d3 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 18:45:08 +0900 Subject: [PATCH 05/26] Comment on using the definition to calculate Von Neumann entropy --- qdna/entanglement.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index b6c6b1a..816748b 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -49,6 +49,11 @@ def mutual_information(rho_ab): rho_b = partial_trace(rho_ab, [0]) # Compute the Von Neumann entropy for each density matrix + # To calculate entropies, it is convenient to calculate the + # eigendecomposition of \rho. + # S(\rho) = -sum_i( \lambda_i * ln(\lambda_i) ) + # But as the matrices are small, I'll use the definition to + # make it easier to read. s_a = -np.trace(rho_a @ logm(rho_a)).real s_b = -np.trace(rho_b @ logm(rho_b)).real s_ab = -np.trace(rho_ab @ logm(rho_ab)).real From 8173b4a30ef50697f0aa224725fa290bfac818a3 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 18:48:09 +0900 Subject: [PATCH 06/26] Changes the parameter name `quantify_shared_info` to `entanglement_measure` --- qdna/entanglement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 816748b..9bbe867 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -61,7 +61,7 @@ def mutual_information(rho_ab): # Calculate the mutual information return s_a + s_b - s_ab -def initialize_entanglement_graph(state_vector, n_qubits, quantify_shared_info=concurrence): +def initialize_entanglement_graph(state_vector, n_qubits, entanglement_measure=concurrence): ''' Initialize a graph where nodes represent qubits and the weights represent the entanglement between pairs of qubits in a register of `n` qubits for a @@ -82,7 +82,7 @@ def initialize_entanglement_graph(state_vector, n_qubits, quantify_shared_info=c rho_ij = partial_trace(psi, list(set(range(n_qubits)).difference([i,j]))) # Compute the Von Neumann entropy (entanglement measure) - shared_info = quantify_shared_info(rho_ij) + shared_info = entanglement_measure(rho_ij) # Add an edge with this entanglement measure as weight graph.add_edge(i, j, weight=shared_info) From 7b9aa42237b7da41ae47ad029bcd6a19346a2a4b Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 18:48:34 +0900 Subject: [PATCH 07/26] Sorts the sets when the subsystem sizes are equal --- qdna/entanglement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 9bbe867..1fe497e 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -163,8 +163,8 @@ def calculate_cut_weight(node, set_b): if improved: break - # Sorts the sets - if sorted(set_a)[0] > sorted(set_b)[0]: + # Sorts the sets when the subsystem sizes are equal + if size_a == size_b and sorted(set_a)[0] > sorted(set_b)[0]: set_a, set_b = set_b, set_a # Calculate the sum of weights of edges between the two sets From 59a872ae82564ff5a4b6b4d74c3dc75081668873 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Fri, 29 Dec 2023 19:15:08 +0900 Subject: [PATCH 08/26] Includes the complexities of the functions in the respective comments --- qdna/entanglement.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 1fe497e..d2a7a67 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -50,7 +50,7 @@ def mutual_information(rho_ab): # Compute the Von Neumann entropy for each density matrix # To calculate entropies, it is convenient to calculate the - # eigendecomposition of \rho. + # eigendecomposition of \rho: # S(\rho) = -sum_i( \lambda_i * ln(\lambda_i) ) # But as the matrices are small, I'll use the definition to # make it easier to read. @@ -66,6 +66,8 @@ def initialize_entanglement_graph(state_vector, n_qubits, entanglement_measure=c Initialize a graph where nodes represent qubits and the weights represent the entanglement between pairs of qubits in a register of `n` qubits for a pure state. + + O(n^2) ''' # Create a graph graph = nx.Graph() @@ -92,6 +94,10 @@ def initialize_entanglement_graph(state_vector, n_qubits, entanglement_measure=c def min_cut_fixed_size_optimal(graph, size_a, size_b): ''' Optimal solution to the minimum cut problem with fixed sizes for the sets. + + O(n! / k!(n-k)!) x O(m), where m is the number of edges in the graph. + The total number of edges m in a complete graph with n nodes is given by + m = n(n-1) / 2 ''' nodes = list(graph.nodes()) min_cut_weight = float('inf') @@ -130,7 +136,7 @@ def min_cut_fixed_size_heuristic(graph, size_a, size_b): n_b: subsystem B number of qubits. m: number of edges between a node and subsystem B (typically equal to n_b). - Example (n_a=n_b=n/2): + Example worst-case scenario (n_a=n_b=n/2): O(k * n^3) ''' nodes = list(graph.nodes()) From 6b8ebf8693a202219d3fa79bc45d003e4807f2f7 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sun, 31 Dec 2023 00:04:23 +0900 Subject: [PATCH 09/26] Comments --- qdna/entanglement.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index d2a7a67..0874c49 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -67,7 +67,7 @@ def initialize_entanglement_graph(state_vector, n_qubits, entanglement_measure=c the entanglement between pairs of qubits in a register of `n` qubits for a pure state. - O(n^2) + O(n^2) x O(2^n) ''' # Create a graph graph = nx.Graph() @@ -128,6 +128,14 @@ def min_cut_fixed_size_heuristic(graph, size_a, size_b): ''' Heuristic approach for the Min-Cut problem with a fixed number of nodes in each partition. + + This approach aims to find a partition of the graph where the total edge + weight crossing the cut is as low as possible, given the size constraints. + The algorithm iteratively attempts to reduce the total weight of the cut by + swapping nodes between sets A and B, provided the swap decreases the total + weight of the cut. + This is a heuristic approach and may not always find the globally optimal + solution, especially for complex or large graphs. O(k * n_a * n_b * m): k: number of iterations (vary significantly based on the graph's From 592beba324fa762c72ed6b71aa0c1698c670bfc8 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sun, 31 Dec 2023 00:15:31 +0900 Subject: [PATCH 10/26] Optimization --- qdna/entanglement.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 0874c49..6226e79 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -163,17 +163,20 @@ def calculate_cut_weight(node, set_b): improved = False for node in set_a: for other_node in set_b: - set_c = set_b.copy() - set_c.remove(other_node) - set_c.add(node) - if calculate_cut_weight(node, set_b) > calculate_cut_weight(other_node, set_c): + weight_node = calculate_cut_weight(node, set_b) + set_b.remove(other_node) + set_b.add(node) + weight_other_node = calculate_cut_weight(other_node, set_b) + if weight_node > weight_other_node: # Swap nodes set_a.remove(node) - set_b.add(node) - set_b.remove(other_node) set_a.add(other_node) improved = True break + else: + set_b.remove(node) + set_b.add(other_node) + if improved: break From 63dbc60ccdfe0a23363d07d3b3ff721b85c93dfc Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sun, 31 Dec 2023 03:10:57 +0900 Subject: [PATCH 11/26] Improved heuristics --- qdna/entanglement.py | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 6226e79..2c59647 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -128,7 +128,7 @@ def min_cut_fixed_size_heuristic(graph, size_a, size_b): ''' Heuristic approach for the Min-Cut problem with a fixed number of nodes in each partition. - + This approach aims to find a partition of the graph where the total edge weight crossing the cut is as low as possible, given the size constraints. The algorithm iteratively attempts to reduce the total weight of the cut by @@ -154,28 +154,39 @@ def min_cut_fixed_size_heuristic(graph, size_a, size_b): set_a = set(nodes[:size_a]) set_b = set(nodes[size_a:size_a + size_b]) - def calculate_cut_weight(node, set_b): - return sum(graph[node][neighbor]['weight'] for neighbor in graph[node] if neighbor in set_b) + def swapping_weight(node, other_node, set_node, set_other_node): + # Entanglement (without - with) swapping. + # A positive result indicates that a swap should be made + # to reduce entanglement. That is, the entanglement with + # a swap is smaller than without. + return \ + sum(graph[node][neighbor]['weight'] + for neighbor in set_other_node if neighbor is not other_node) - \ + sum(graph[node][neighbor]['weight'] + for neighbor in set_node if neighbor is not node) # Iteratively try to improve the cut by swapping nodes between A and B improved = True while improved: improved = False - for node in set_a: - for other_node in set_b: - weight_node = calculate_cut_weight(node, set_b) - set_b.remove(other_node) - set_b.add(node) - weight_other_node = calculate_cut_weight(other_node, set_b) - if weight_node > weight_other_node: - # Swap nodes - set_a.remove(node) - set_a.add(other_node) + for node_a in set_a: + for node_b in set_b: + weight_a = swapping_weight(node_a, node_b, set_a, set_b) + weight_b = swapping_weight(node_b, node_a, set_b, set_a) + total_weight = weight_a + weight_b + # Here, the entanglements of both nodes are + # considered simultaneously. + if total_weight > 0: + # Swap the nodes if the total entanglement with the swap is + # smaller. In other words, the entanglement without + # swapping is greater, which means that `total_weight` is + # positive. + set_a.remove(node_a) + set_b.remove(node_b) + set_a.add(node_b) + set_b.add(node_a) improved = True break - else: - set_b.remove(node) - set_b.add(other_node) if improved: break From a81200b862ed2ce09b1a0cf068f3eefc561f8a85 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sun, 31 Dec 2023 03:16:02 +0900 Subject: [PATCH 12/26] Fixed state vector test --- test/test_entanglement.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/test_entanglement.py b/test/test_entanglement.py index e1c6984..c6c7189 100644 --- a/test/test_entanglement.py +++ b/test/test_entanglement.py @@ -65,3 +65,28 @@ def test_min_cut_heuristic_product_state(self): self.assertTrue(isclose(cut_weight, 0.0)) self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + + def test_min_cut_heuristic_fixed(self): + state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, + 2.05658826e-01, 8.15141431e-02, 8.40762648e-03, 0.00000000e+00, + 0.00000000e+00, 9.96282923e-03, 1.59787371e-01, 2.46020212e-01, + 2.40154124e-01, 1.88551405e-01, 1.90979862e-02, 0.00000000e+00, + 9.31053205e-04, 4.54430541e-02, 2.03018262e-01, 1.86283916e-01, + 1.52698965e-01, 1.86716850e-01, 3.98788754e-02, 0.00000000e+00, + 9.31053205e-04, 7.52123527e-02, 2.05815792e-01, 1.50703564e-01, + 1.29600833e-01, 1.44311684e-01, 7.06191583e-02, 0.00000000e+00, + 0.00000000e+00, 7.69075128e-02, 1.73599511e-01, 1.17960532e-01, + 1.25934109e-01, 1.33062442e-01, 8.39199837e-02, 0.00000000e+00, + 0.00000000e+00, 3.84563398e-02, 1.76000915e-01, 1.11207757e-01, + 1.36568022e-01, 1.59108898e-01, 6.19303017e-02, 0.00000000e+00, + 0.00000000e+00, 9.01723392e-03, 1.70193955e-01, 1.96598638e-01, + 2.21354420e-01, 1.95836976e-01, 4.14572396e-02, 6.39589243e-03, + 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, + 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] + + graph = initialize_entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) From bbb2957f316941f75723faf37614d470d6879df1 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Mon, 1 Jan 2024 07:22:33 +0900 Subject: [PATCH 13/26] `mutual_information` comment --- qdna/entanglement.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 2c59647..8ceebd2 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -43,17 +43,20 @@ def mutual_information(rho_ab): ''' Mutual information quantifies the total amount of correlation between two qubits. It includes both classical and quantum correlations. + + To calculate the entropies, it is convenient to calculate the + eigendecomposition of `rho` and use the eigenvalues `lambda_i` to determine + the entropy: + `S(rho) = -sum_i( lambda_i * ln(lambda_i) )` + But as the matrices are small, I'll use the definition to make the function + easier to read: + `S(rho) = -Tr( rho @ ln(rho) )` ''' # Compute the reduced density matrices for each qubit rho_a = partial_trace(rho_ab, [1]) rho_b = partial_trace(rho_ab, [0]) # Compute the Von Neumann entropy for each density matrix - # To calculate entropies, it is convenient to calculate the - # eigendecomposition of \rho: - # S(\rho) = -sum_i( \lambda_i * ln(\lambda_i) ) - # But as the matrices are small, I'll use the definition to - # make it easier to read. s_a = -np.trace(rho_a @ logm(rho_a)).real s_b = -np.trace(rho_b @ logm(rho_b)).real s_ab = -np.trace(rho_ab @ logm(rho_ab)).real From 2de83a8d7a1fcf256a1bb0ce943ea58b9a4c6d92 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Mon, 1 Jan 2024 07:22:55 +0900 Subject: [PATCH 14/26] new test --- test/test_entanglement.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/test_entanglement.py b/test/test_entanglement.py index c6c7189..75b9480 100644 --- a/test/test_entanglement.py +++ b/test/test_entanglement.py @@ -66,6 +66,31 @@ def test_min_cut_heuristic_product_state(self): self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + def test_min_cut_optimal_fixed(self): + state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, + 2.05658826e-01, 8.15141431e-02, 8.40762648e-03, 0.00000000e+00, + 0.00000000e+00, 9.96282923e-03, 1.59787371e-01, 2.46020212e-01, + 2.40154124e-01, 1.88551405e-01, 1.90979862e-02, 0.00000000e+00, + 9.31053205e-04, 4.54430541e-02, 2.03018262e-01, 1.86283916e-01, + 1.52698965e-01, 1.86716850e-01, 3.98788754e-02, 0.00000000e+00, + 9.31053205e-04, 7.52123527e-02, 2.05815792e-01, 1.50703564e-01, + 1.29600833e-01, 1.44311684e-01, 7.06191583e-02, 0.00000000e+00, + 0.00000000e+00, 7.69075128e-02, 1.73599511e-01, 1.17960532e-01, + 1.25934109e-01, 1.33062442e-01, 8.39199837e-02, 0.00000000e+00, + 0.00000000e+00, 3.84563398e-02, 1.76000915e-01, 1.11207757e-01, + 1.36568022e-01, 1.59108898e-01, 6.19303017e-02, 0.00000000e+00, + 0.00000000e+00, 9.01723392e-03, 1.70193955e-01, 1.96598638e-01, + 2.21354420e-01, 1.95836976e-01, 4.14572396e-02, 6.39589243e-03, + 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, + 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] + + graph = initialize_entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + def test_min_cut_heuristic_fixed(self): state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, 2.05658826e-01, 8.15141431e-02, 8.40762648e-03, 0.00000000e+00, From 322b178daac874c8fb9d95b8383252ae7fbe7935 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Mon, 8 Jan 2024 00:34:30 +0900 Subject: [PATCH 15/26] Map a graph to a QUBO model --- qdna/entanglement.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 8ceebd2..d0931c5 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -204,3 +204,24 @@ def swapping_weight(node, other_node, set_node, set_other_node): ) return (set_a, set_b), cut_weight + + +# D-Wave functions + +def graph_to_qubo(graph): + ''' + Map the graph to a QUBO model. + ''' + qubo = {} + + max_weight = max(weight for _, _, weight in graph.edges(data='weight')) + + for i, j, weight in graph.edges(data='weight'): + # Set qubo_{ij} to be `max_weight - weight` for min-cut. + weight = max_weight - weight + qubo[(i, i)] = qubo.get((i, i), 0) - weight + qubo[(j, j)] = qubo.get((j, j), 0) - weight + qubo[(i, j)] = qubo.get((i, j), 0) + 2 * weight + + return qubo + From 114f36fa88f941c0b14d88d4dfa6997ee899898f Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Mon, 8 Jan 2024 00:34:49 +0900 Subject: [PATCH 16/26] Map the graph to an Ising model. --- qdna/entanglement.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index d0931c5..6b4bc5f 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -225,3 +225,24 @@ def graph_to_qubo(graph): return qubo +def graph_to_ising(graph): + ''' + Map the graph to an Ising model. + ''' + ising = {} + local_field = {} + + max_weight = max(weight for _, _, weight in graph.edges(data='weight')) + + # Add interaction strengths and local fields. + for i in graph.nodes(): + local_field[i] = 0 # local fields + for j in graph.nodes(): + if i < j: + # For a fully connected graph, define the interaction + # between every pair of nodes (i, j). + # Set ising_{ij} to be `max_weight - weight` for min-cut. + ising[(i, j)] = max_weight - graph[i][j]['weight'] + + return ising, local_field + From 5a8ae5c6878d76ab00c8b9c0f90dfe0283623cd9 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Mon, 8 Jan 2024 00:35:50 +0900 Subject: [PATCH 17/26] D-Wave min-cut --- qdna/entanglement.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/qdna/entanglement.py b/qdna/entanglement.py index 6b4bc5f..27b4048 100644 --- a/qdna/entanglement.py +++ b/qdna/entanglement.py @@ -18,6 +18,7 @@ from qiskit.quantum_info import partial_trace, Statevector import numpy as np from scipy.linalg import logm +from dwave.samplers import SimulatedAnnealingSampler def concurrence(rho_ab): ''' @@ -246,3 +247,43 @@ def graph_to_ising(graph): return ising, local_field +def min_cut_dwave(graph, size_a=None, sample_method='ising', sampler=SimulatedAnnealingSampler(), num_reads=100): + + if sample_method == 'qubo': + # Map the graph to a QUBO model. + qubo = graph_to_qubo(graph) + response = sampler.sample_qubo(qubo, num_reads=num_reads) + else: + # Map the graph to an Ising model. + ising, local_field = graph_to_ising(graph) + response = sampler.sample_ising(local_field, ising, num_reads=num_reads) + + # Find the sample with the lowest energy (best result), respecting the size of the block. + min_cut_weight = float('inf') + set_a = None + if size_a is not None: + for sample, energy in response.data(['sample', 'energy']): + if energy < min_cut_weight and sum(v for v in sample.values() if v==1) == size_a: + min_cut_weight = energy + set_a = sample + else: + set_a = response.first.sample + min_cut_weight = response.first.energy + + # Subsystem B is the complement of subsystem A. + nodes = set(graph.nodes()) + set_a = {k for k, v in set_a.items() if v == 1} + size_a = len(set_a) + set_b = nodes - set_a + size_b = len(set_b) + + # Sorts the sets when the subsystem sizes are equal. + if size_a == size_b and sorted(set_a)[0] > sorted(set_b)[0]: + set_a, set_b = set_b, set_a + + # Calculate the sum of weights of edges between the two sets. + cut_weight = sum( + graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) + ) + + return (set_a, set_b), cut_weight From 42ffdee2b303bb8aa4ca4ae1111022d0bc3eb38a Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Mon, 8 Jan 2024 00:42:16 +0900 Subject: [PATCH 18/26] Includes d-wave sdk --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index cefb2de..af847b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ qiskit-aer qiskit-algorithms torch qclib +dwave-ocean-sdk From d3016ffcc4e1c5f49c261c638c75d34241c556d5 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sat, 3 Feb 2024 19:21:06 +0900 Subject: [PATCH 19/26] Divides the functions into different modules --- qdna/entanglement.py | 289 ------------------------------------------- 1 file changed, 289 deletions(-) delete mode 100644 qdna/entanglement.py diff --git a/qdna/entanglement.py b/qdna/entanglement.py deleted file mode 100644 index 27b4048..0000000 --- a/qdna/entanglement.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright 2023 qdna-lib project. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import itertools -import random -import networkx as nx -from qiskit.quantum_info import partial_trace, Statevector -import numpy as np -from scipy.linalg import logm -from dwave.samplers import SimulatedAnnealingSampler - -def concurrence(rho_ab): - ''' - Concurrence specifically quantifies the quantum entanglement between the - two qubits. It does not consider classical correlations. - - https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.80.2245 - ''' - # Compute the spin-flipped state - sigma_y = np.array([[0, -1j], [1j, 0]]) - rho_star = np.conj(rho_ab) - rho_tilde = np.kron(sigma_y, sigma_y) @ rho_star @ np.kron(sigma_y, sigma_y) - - # Calculate the eigenvalues of the product matrix - eigenvalues = np.linalg.eigvals(rho_ab @ rho_tilde) - # Sort in decreasing order - eigenvalues = np.sort(np.sqrt(np.abs(eigenvalues)))[::-1] - - # Compute the concurrence - return max(0, eigenvalues[0] - sum(eigenvalues[1:])) - -def mutual_information(rho_ab): - ''' - Mutual information quantifies the total amount of correlation between two - qubits. It includes both classical and quantum correlations. - - To calculate the entropies, it is convenient to calculate the - eigendecomposition of `rho` and use the eigenvalues `lambda_i` to determine - the entropy: - `S(rho) = -sum_i( lambda_i * ln(lambda_i) )` - But as the matrices are small, I'll use the definition to make the function - easier to read: - `S(rho) = -Tr( rho @ ln(rho) )` - ''' - # Compute the reduced density matrices for each qubit - rho_a = partial_trace(rho_ab, [1]) - rho_b = partial_trace(rho_ab, [0]) - - # Compute the Von Neumann entropy for each density matrix - s_a = -np.trace(rho_a @ logm(rho_a)).real - s_b = -np.trace(rho_b @ logm(rho_b)).real - s_ab = -np.trace(rho_ab @ logm(rho_ab)).real - - # Calculate the mutual information - return s_a + s_b - s_ab - -def initialize_entanglement_graph(state_vector, n_qubits, entanglement_measure=concurrence): - ''' - Initialize a graph where nodes represent qubits and the weights represent - the entanglement between pairs of qubits in a register of `n` qubits for a - pure state. - - O(n^2) x O(2^n) - ''' - # Create a graph - graph = nx.Graph() - - # Add nodes for each qubit - for i in range(n_qubits): - graph.add_node(i) - - # Add edges with weights representing entanglement - for i in range(n_qubits): - for j in range(i + 1, n_qubits): - # Compute the reduced density matrix for qubits i and j - psi = Statevector(state_vector) - rho_ij = partial_trace(psi, list(set(range(n_qubits)).difference([i,j]))) - - # Compute the Von Neumann entropy (entanglement measure) - shared_info = entanglement_measure(rho_ij) - - # Add an edge with this entanglement measure as weight - graph.add_edge(i, j, weight=shared_info) - - return graph - -def min_cut_fixed_size_optimal(graph, size_a, size_b): - ''' - Optimal solution to the minimum cut problem with fixed sizes for the sets. - - O(n! / k!(n-k)!) x O(m), where m is the number of edges in the graph. - The total number of edges m in a complete graph with n nodes is given by - m = n(n-1) / 2 - ''' - nodes = list(graph.nodes()) - min_cut_weight = float('inf') - min_cut_partition = (set(), set()) - - # Iterate over all combinations for set A - for nodes_a in itertools.combinations(nodes, size_a): - set_a = set(nodes_a) - set_b = set(nodes) - set_a - - # Ensure the size of set B is as required - if len(set_b) != size_b: - continue - - # Calculate the sum of weights of edges between the two sets - cut_weight = sum( - graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) - ) - - # Update min cut if a lower weight is found - if cut_weight < min_cut_weight: - min_cut_weight = cut_weight - min_cut_partition = (set_a, set_b) - - return min_cut_partition, min_cut_weight - -def min_cut_fixed_size_heuristic(graph, size_a, size_b): - ''' - Heuristic approach for the Min-Cut problem with a fixed number of nodes in - each partition. - - This approach aims to find a partition of the graph where the total edge - weight crossing the cut is as low as possible, given the size constraints. - The algorithm iteratively attempts to reduce the total weight of the cut by - swapping nodes between sets A and B, provided the swap decreases the total - weight of the cut. - This is a heuristic approach and may not always find the globally optimal - solution, especially for complex or large graphs. - - O(k * n_a * n_b * m): - k: number of iterations (vary significantly based on the graph's - structure and the initial partitioning). - n_a: subsystem A number of qubits. - n_b: subsystem B number of qubits. - m: number of edges between a node and subsystem B (typically equal to n_b). - - Example worst-case scenario (n_a=n_b=n/2): - O(k * n^3) - ''' - nodes = list(graph.nodes()) - random.shuffle(nodes) # Shuffle nodes to randomize initial selection - - # Initialize sets A and B with random nodes - set_a = set(nodes[:size_a]) - set_b = set(nodes[size_a:size_a + size_b]) - - def swapping_weight(node, other_node, set_node, set_other_node): - # Entanglement (without - with) swapping. - # A positive result indicates that a swap should be made - # to reduce entanglement. That is, the entanglement with - # a swap is smaller than without. - return \ - sum(graph[node][neighbor]['weight'] - for neighbor in set_other_node if neighbor is not other_node) - \ - sum(graph[node][neighbor]['weight'] - for neighbor in set_node if neighbor is not node) - - # Iteratively try to improve the cut by swapping nodes between A and B - improved = True - while improved: - improved = False - for node_a in set_a: - for node_b in set_b: - weight_a = swapping_weight(node_a, node_b, set_a, set_b) - weight_b = swapping_weight(node_b, node_a, set_b, set_a) - total_weight = weight_a + weight_b - # Here, the entanglements of both nodes are - # considered simultaneously. - if total_weight > 0: - # Swap the nodes if the total entanglement with the swap is - # smaller. In other words, the entanglement without - # swapping is greater, which means that `total_weight` is - # positive. - set_a.remove(node_a) - set_b.remove(node_b) - set_a.add(node_b) - set_b.add(node_a) - improved = True - break - - if improved: - break - - # Sorts the sets when the subsystem sizes are equal - if size_a == size_b and sorted(set_a)[0] > sorted(set_b)[0]: - set_a, set_b = set_b, set_a - - # Calculate the sum of weights of edges between the two sets - cut_weight = sum( - graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) - ) - - return (set_a, set_b), cut_weight - - -# D-Wave functions - -def graph_to_qubo(graph): - ''' - Map the graph to a QUBO model. - ''' - qubo = {} - - max_weight = max(weight for _, _, weight in graph.edges(data='weight')) - - for i, j, weight in graph.edges(data='weight'): - # Set qubo_{ij} to be `max_weight - weight` for min-cut. - weight = max_weight - weight - qubo[(i, i)] = qubo.get((i, i), 0) - weight - qubo[(j, j)] = qubo.get((j, j), 0) - weight - qubo[(i, j)] = qubo.get((i, j), 0) + 2 * weight - - return qubo - -def graph_to_ising(graph): - ''' - Map the graph to an Ising model. - ''' - ising = {} - local_field = {} - - max_weight = max(weight for _, _, weight in graph.edges(data='weight')) - - # Add interaction strengths and local fields. - for i in graph.nodes(): - local_field[i] = 0 # local fields - for j in graph.nodes(): - if i < j: - # For a fully connected graph, define the interaction - # between every pair of nodes (i, j). - # Set ising_{ij} to be `max_weight - weight` for min-cut. - ising[(i, j)] = max_weight - graph[i][j]['weight'] - - return ising, local_field - -def min_cut_dwave(graph, size_a=None, sample_method='ising', sampler=SimulatedAnnealingSampler(), num_reads=100): - - if sample_method == 'qubo': - # Map the graph to a QUBO model. - qubo = graph_to_qubo(graph) - response = sampler.sample_qubo(qubo, num_reads=num_reads) - else: - # Map the graph to an Ising model. - ising, local_field = graph_to_ising(graph) - response = sampler.sample_ising(local_field, ising, num_reads=num_reads) - - # Find the sample with the lowest energy (best result), respecting the size of the block. - min_cut_weight = float('inf') - set_a = None - if size_a is not None: - for sample, energy in response.data(['sample', 'energy']): - if energy < min_cut_weight and sum(v for v in sample.values() if v==1) == size_a: - min_cut_weight = energy - set_a = sample - else: - set_a = response.first.sample - min_cut_weight = response.first.energy - - # Subsystem B is the complement of subsystem A. - nodes = set(graph.nodes()) - set_a = {k for k, v in set_a.items() if v == 1} - size_a = len(set_a) - set_b = nodes - set_a - size_b = len(set_b) - - # Sorts the sets when the subsystem sizes are equal. - if size_a == size_b and sorted(set_a)[0] > sorted(set_b)[0]: - set_a, set_b = set_b, set_a - - # Calculate the sum of weights of edges between the two sets. - cut_weight = sum( - graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) - ) - - return (set_a, set_b), cut_weight From 39b12517125477f4462b26d3628683abb047c134 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sat, 3 Feb 2024 19:21:51 +0900 Subject: [PATCH 20/26] Changes the file name according to the new function module --- test/test_entanglement.py | 117 -------------------------------------- 1 file changed, 117 deletions(-) delete mode 100644 test/test_entanglement.py diff --git a/test/test_entanglement.py b/test/test_entanglement.py deleted file mode 100644 index 75b9480..0000000 --- a/test/test_entanglement.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2023 qdna-lib project. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Tests for the entanglement.py module. -""" - -from unittest import TestCase -from math import isclose -import numpy as np -from qdna.entanglement import min_cut_fixed_size_optimal, \ - initialize_entanglement_graph, \ - min_cut_fixed_size_heuristic - - -# pylint: disable=missing-function-docstring -# pylint: disable=missing-class-docstring - - -class TestEntanglement(TestCase): - - def test_min_cut_optimal_product_state(self): - n_qubits = 3 - - state_vector1 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j - state_vector1 = state_vector1 / np.linalg.norm(state_vector1) - - state_vector2 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j - state_vector2 = state_vector2 / np.linalg.norm(state_vector2) - - state_vector = np.kron(state_vector1, state_vector2) - - graph = initialize_entanglement_graph(state_vector, 6) - (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) - - self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) - - def test_min_cut_heuristic_product_state(self): - n_qubits = 3 - - state_vector1 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j - state_vector1 = state_vector1 / np.linalg.norm(state_vector1) - - state_vector2 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j - state_vector2 = state_vector2 / np.linalg.norm(state_vector2) - - state_vector = np.kron(state_vector1, state_vector2) - - graph = initialize_entanglement_graph(state_vector, 6) - (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) - - self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) - - def test_min_cut_optimal_fixed(self): - state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, - 2.05658826e-01, 8.15141431e-02, 8.40762648e-03, 0.00000000e+00, - 0.00000000e+00, 9.96282923e-03, 1.59787371e-01, 2.46020212e-01, - 2.40154124e-01, 1.88551405e-01, 1.90979862e-02, 0.00000000e+00, - 9.31053205e-04, 4.54430541e-02, 2.03018262e-01, 1.86283916e-01, - 1.52698965e-01, 1.86716850e-01, 3.98788754e-02, 0.00000000e+00, - 9.31053205e-04, 7.52123527e-02, 2.05815792e-01, 1.50703564e-01, - 1.29600833e-01, 1.44311684e-01, 7.06191583e-02, 0.00000000e+00, - 0.00000000e+00, 7.69075128e-02, 1.73599511e-01, 1.17960532e-01, - 1.25934109e-01, 1.33062442e-01, 8.39199837e-02, 0.00000000e+00, - 0.00000000e+00, 3.84563398e-02, 1.76000915e-01, 1.11207757e-01, - 1.36568022e-01, 1.59108898e-01, 6.19303017e-02, 0.00000000e+00, - 0.00000000e+00, 9.01723392e-03, 1.70193955e-01, 1.96598638e-01, - 2.21354420e-01, 1.95836976e-01, 4.14572396e-02, 6.39589243e-03, - 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, - 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] - - graph = initialize_entanglement_graph(state_vector, 6) - (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) - - self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) - - def test_min_cut_heuristic_fixed(self): - state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, - 2.05658826e-01, 8.15141431e-02, 8.40762648e-03, 0.00000000e+00, - 0.00000000e+00, 9.96282923e-03, 1.59787371e-01, 2.46020212e-01, - 2.40154124e-01, 1.88551405e-01, 1.90979862e-02, 0.00000000e+00, - 9.31053205e-04, 4.54430541e-02, 2.03018262e-01, 1.86283916e-01, - 1.52698965e-01, 1.86716850e-01, 3.98788754e-02, 0.00000000e+00, - 9.31053205e-04, 7.52123527e-02, 2.05815792e-01, 1.50703564e-01, - 1.29600833e-01, 1.44311684e-01, 7.06191583e-02, 0.00000000e+00, - 0.00000000e+00, 7.69075128e-02, 1.73599511e-01, 1.17960532e-01, - 1.25934109e-01, 1.33062442e-01, 8.39199837e-02, 0.00000000e+00, - 0.00000000e+00, 3.84563398e-02, 1.76000915e-01, 1.11207757e-01, - 1.36568022e-01, 1.59108898e-01, 6.19303017e-02, 0.00000000e+00, - 0.00000000e+00, 9.01723392e-03, 1.70193955e-01, 1.96598638e-01, - 2.21354420e-01, 1.95836976e-01, 4.14572396e-02, 6.39589243e-03, - 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, - 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] - - graph = initialize_entanglement_graph(state_vector, 6) - (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) - - self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) From 8de396814d930e1fa0c9511ace416cb4acdf3cdd Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sat, 3 Feb 2024 19:22:42 +0900 Subject: [PATCH 21/26] Refactoring --- qdna/compression/schmidt.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/qdna/compression/schmidt.py b/qdna/compression/schmidt.py index 26833d3..2b31647 100644 --- a/qdna/compression/schmidt.py +++ b/qdna/compression/schmidt.py @@ -94,15 +94,11 @@ def __init__(self, params, label=None, opt_params=None): self.svd = "auto" if opt_params.get("svd") is None else \ opt_params.get("svd") - # The trash and latent qubits must take into account that the qiskit qubits are reversed. - complement = sorted( - set(range(self.num_qubits)).difference(set(self.partition)) - )[::-1] - self.latent_qubits = [ - self.num_qubits-i-1 for i in complement - ] - self.trash_qubits = sorted( - set(range(self.num_qubits)).difference(set(self.latent_qubits)) + # The trash and latent qubits must take into account that the qiskit + # qubits are reversed. See line `return circuit.reverse_bits()`. + self.trash_qubits = sorted([self.num_qubits-i-1 for i in self.partition]) + self.latent_qubits = sorted( + set(range(self.num_qubits)).difference(set(self.trash_qubits)) ) if label is None: @@ -120,17 +116,18 @@ def _define_initialize(self): circuit.initialize(self.params) return circuit.inverse() + # reg_a = trash register, reg_b = latent register. circuit, reg_a, reg_b = self._create_quantum_circuit() - # Schmidt decomposition + # Schmidt decomposition. rank, svd_u, _, svd_v = schmidt_decomposition( self.params, reg_a, rank=self.low_rank, svd=self.svd ) - # Schmidt measure of entanglement + # Schmidt measure of entanglement. e_bits = _to_qubits(rank) - # Phase 3 and 4 encode gates U and V.T + # Phase 3 and 4 encode gates U and V.T. self._encode(svd_u, circuit, reg_b) self._encode(svd_v.T, circuit, reg_a) From 5974353f7f6e4dde81b84d09be2ef745ec3990c0 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sat, 3 Feb 2024 19:23:39 +0900 Subject: [PATCH 22/26] Module for graph functions --- qdna/graph.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 qdna/graph.py diff --git a/qdna/graph.py b/qdna/graph.py new file mode 100644 index 0000000..6bb5c7b --- /dev/null +++ b/qdna/graph.py @@ -0,0 +1,209 @@ +# Copyright 2023 qdna-lib project. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import random +from dwave.samplers import SimulatedAnnealingSampler + +def min_cut_fixed_size_optimal(graph, size_a, size_b): + ''' + Optimal solution to the minimum cut problem with fixed sizes for the sets. + + O(n! / k!(n-k)!) x O(m), where m is the number of edges in the graph. + The total number of edges m in a complete graph with n nodes is given by + m = n(n-1) / 2 + ''' + nodes = list(graph.nodes()) + min_cut_weight = float('inf') + min_cut_partition = (set(), set()) + + # Iterate over all combinations for set A + for nodes_a in itertools.combinations(nodes, size_a): + set_a = set(nodes_a) + set_b = set(nodes) - set_a + + # Ensure the size of set B is as required + if len(set_b) != size_b: + continue + + # Calculate the sum of weights of edges between the two sets + cut_weight = sum( + graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) + ) + + # Update min cut if a lower weight is found + if cut_weight < min_cut_weight: + min_cut_weight = cut_weight + min_cut_partition = (set_a, set_b) + + return min_cut_partition, min_cut_weight + +def min_cut_fixed_size_heuristic(graph, size_a, size_b): + ''' + Heuristic approach for the Min-Cut problem with a fixed number of nodes in + each partition. + + This approach aims to find a partition of the graph where the total edge + weight crossing the cut is as low as possible, given the size constraints. + The algorithm iteratively attempts to reduce the total weight of the cut by + swapping nodes between sets A and B, provided the swap decreases the total + weight of the cut. + This is a heuristic approach and may not always find the globally optimal + solution, especially for complex or large graphs. + + O(k * n_a * n_b * m): + k: number of iterations (vary significantly based on the graph's + structure and the initial partitioning). + n_a: subsystem A number of qubits. + n_b: subsystem B number of qubits. + m: number of edges between a node and subsystem B (typically equal to n_b). + + Example worst-case scenario (n_a=n_b=n/2): + O(k * n^3) + ''' + nodes = list(graph.nodes()) + random.shuffle(nodes) # Shuffle nodes to randomize initial selection + + # Initialize sets A and B with random nodes + set_a = set(nodes[:size_a]) + set_b = set(nodes[size_a:size_a + size_b]) + + def swapping_weight(node, other_node, set_node, set_other_node): + # Entanglement (without - with) swapping. + # A positive result indicates that a swap should be made + # to reduce entanglement. That is, the entanglement with + # a swap is smaller than without. + return \ + sum(graph[node][neighbor]['weight'] + for neighbor in set_other_node if neighbor is not other_node) - \ + sum(graph[node][neighbor]['weight'] + for neighbor in set_node if neighbor is not node) + + # Iteratively try to improve the cut by swapping nodes between A and B + improved = True + while improved: + improved = False + for node_a in set_a: + for node_b in set_b: + weight_a = swapping_weight(node_a, node_b, set_a, set_b) + weight_b = swapping_weight(node_b, node_a, set_b, set_a) + total_weight = weight_a + weight_b + # Here, the entanglements of both nodes are + # considered simultaneously. + if total_weight > 0: + # Swap the nodes if the total entanglement with the swap is + # smaller. In other words, the entanglement without + # swapping is greater, which means that `total_weight` is + # positive. + set_a.remove(node_a) + set_b.remove(node_b) + set_a.add(node_b) + set_b.add(node_a) + improved = True + break + + if improved: + break + + # Sorts the sets when the subsystem sizes are equal + if size_a == size_b and sorted(set_a)[0] > sorted(set_b)[0]: + set_a, set_b = set_b, set_a + + # Calculate the sum of weights of edges between the two sets + cut_weight = sum( + graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) + ) + + return (set_a, set_b), cut_weight + + +# D-Wave functions + +def graph_to_qubo(graph): + ''' + Map the graph to a QUBO model. + ''' + qubo = {} + + max_weight = max(weight for _, _, weight in graph.edges(data='weight')) + + for i, j, weight in graph.edges(data='weight'): + # Set qubo_{ij} to be `max_weight - weight` for min-cut. + weight = max_weight - weight + qubo[(i, i)] = qubo.get((i, i), 0) - weight + qubo[(j, j)] = qubo.get((j, j), 0) - weight + qubo[(i, j)] = qubo.get((i, j), 0) + 2 * weight + + return qubo + +def graph_to_ising(graph): + ''' + Map the graph to an Ising model. + ''' + ising = {} + local_field = {} + + max_weight = max(weight for _, _, weight in graph.edges(data='weight')) + + for i, j, weight in graph.edges(data='weight'): + # Set qubo_{ij} to be `max_weight - weight` for min-cut. + weight = max_weight - weight + ising[(i, j)] = weight + + # Add interaction strengths and local fields. + for i in graph.nodes(): + local_field[i] = 0 # local fields + + return ising, local_field + +def min_cut_dwave(graph, size_a=None, sample_method='ising', sampler=SimulatedAnnealingSampler(), num_reads=100): + + if sample_method == 'qubo': + # Map the graph to a QUBO model. + qubo = graph_to_qubo(graph) + response = sampler.sample_qubo(qubo, num_reads=num_reads) + else: + # Map the graph to an Ising model. + ising, local_field = graph_to_ising(graph) + response = sampler.sample_ising(local_field, ising, num_reads=num_reads) + print(response) + # Find the sample with the lowest energy (best result), respecting the size of the block. + min_cut_weight = float('inf') + set_a = None + if size_a is not None: + for sample, energy in response.data(['sample', 'energy']): + if energy < min_cut_weight and sum(v for v in sample.values() if v==1) == size_a: + min_cut_weight = energy + set_a = sample + else: + set_a = response.first.sample + min_cut_weight = response.first.energy + + # Subsystem B is the complement of subsystem A. + nodes = set(graph.nodes()) + set_a = {k for k, v in set_a.items() if v == 1} + size_a = len(set_a) + set_b = nodes - set_a + size_b = len(set_b) + + # Sorts the sets when the subsystem sizes are equal. + if size_a == size_b and sorted(set_a)[0] > sorted(set_b)[0]: + set_a, set_b = set_b, set_a + + # Calculate the sum of weights of edges between the two sets. + cut_weight = sum( + graph[u][v]['weight'] for u in set_a for v in set_b if graph.has_edge(u, v) + ) + + return (set_a, set_b), cut_weight From 044f6fdcbe53c9e486a33ee3faf5e0a7ca6c1482 Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sat, 3 Feb 2024 19:23:52 +0900 Subject: [PATCH 23/26] Test for graph functions --- test/test_graph.py | 117 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 test/test_graph.py diff --git a/test/test_graph.py b/test/test_graph.py new file mode 100644 index 0000000..e0fd35c --- /dev/null +++ b/test/test_graph.py @@ -0,0 +1,117 @@ +# Copyright 2023 qdna-lib project. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for the entanglement.py module. +""" + +from unittest import TestCase +from math import isclose +import numpy as np +from qdna.quantum_info import entanglement_graph +from qdna.graph import min_cut_fixed_size_optimal, \ + min_cut_fixed_size_heuristic + + +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring + + +class TestEntanglement(TestCase): + + def test_min_cut_optimal_product_state(self): + n_qubits = 3 + + state_vector1 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector1 = state_vector1 / np.linalg.norm(state_vector1) + + state_vector2 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector2 = state_vector2 / np.linalg.norm(state_vector2) + + state_vector = np.kron(state_vector1, state_vector2) + + graph = entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + + def test_min_cut_heuristic_product_state(self): + n_qubits = 3 + + state_vector1 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector1 = state_vector1 / np.linalg.norm(state_vector1) + + state_vector2 = np.random.rand(2**n_qubits) + np.random.rand(2**n_qubits) * 1j + state_vector2 = state_vector2 / np.linalg.norm(state_vector2) + + state_vector = np.kron(state_vector1, state_vector2) + + graph = entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + + def test_min_cut_optimal_fixed(self): + state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, + 2.05658826e-01, 8.15141431e-02, 8.40762648e-03, 0.00000000e+00, + 0.00000000e+00, 9.96282923e-03, 1.59787371e-01, 2.46020212e-01, + 2.40154124e-01, 1.88551405e-01, 1.90979862e-02, 0.00000000e+00, + 9.31053205e-04, 4.54430541e-02, 2.03018262e-01, 1.86283916e-01, + 1.52698965e-01, 1.86716850e-01, 3.98788754e-02, 0.00000000e+00, + 9.31053205e-04, 7.52123527e-02, 2.05815792e-01, 1.50703564e-01, + 1.29600833e-01, 1.44311684e-01, 7.06191583e-02, 0.00000000e+00, + 0.00000000e+00, 7.69075128e-02, 1.73599511e-01, 1.17960532e-01, + 1.25934109e-01, 1.33062442e-01, 8.39199837e-02, 0.00000000e+00, + 0.00000000e+00, 3.84563398e-02, 1.76000915e-01, 1.11207757e-01, + 1.36568022e-01, 1.59108898e-01, 6.19303017e-02, 0.00000000e+00, + 0.00000000e+00, 9.01723392e-03, 1.70193955e-01, 1.96598638e-01, + 2.21354420e-01, 1.95836976e-01, 4.14572396e-02, 6.39589243e-03, + 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, + 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] + + graph = entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + + def test_min_cut_heuristic_fixed(self): + state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, + 2.05658826e-01, 8.15141431e-02, 8.40762648e-03, 0.00000000e+00, + 0.00000000e+00, 9.96282923e-03, 1.59787371e-01, 2.46020212e-01, + 2.40154124e-01, 1.88551405e-01, 1.90979862e-02, 0.00000000e+00, + 9.31053205e-04, 4.54430541e-02, 2.03018262e-01, 1.86283916e-01, + 1.52698965e-01, 1.86716850e-01, 3.98788754e-02, 0.00000000e+00, + 9.31053205e-04, 7.52123527e-02, 2.05815792e-01, 1.50703564e-01, + 1.29600833e-01, 1.44311684e-01, 7.06191583e-02, 0.00000000e+00, + 0.00000000e+00, 7.69075128e-02, 1.73599511e-01, 1.17960532e-01, + 1.25934109e-01, 1.33062442e-01, 8.39199837e-02, 0.00000000e+00, + 0.00000000e+00, 3.84563398e-02, 1.76000915e-01, 1.11207757e-01, + 1.36568022e-01, 1.59108898e-01, 6.19303017e-02, 0.00000000e+00, + 0.00000000e+00, 9.01723392e-03, 1.70193955e-01, 1.96598638e-01, + 2.21354420e-01, 1.95836976e-01, 4.14572396e-02, 6.39589243e-03, + 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, + 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] + + graph = entanglement_graph(state_vector, 6) + (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) + + self.assertTrue(isclose(cut_weight, 0.0)) + self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) From 6f81ea78965a61417a1a2cde6977e767f4ec0aeb Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sat, 3 Feb 2024 19:26:47 +0900 Subject: [PATCH 24/26] Measurements of the correlation between subsystems of quantum states --- qdna/quantum_info.py | 137 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 qdna/quantum_info.py diff --git a/qdna/quantum_info.py b/qdna/quantum_info.py new file mode 100644 index 0000000..900bc99 --- /dev/null +++ b/qdna/quantum_info.py @@ -0,0 +1,137 @@ +# Copyright 2023 qdna-lib project. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import networkx as nx +from qiskit.quantum_info import Statevector, partial_trace +import numpy as np + +def von_neumann_entropy(rho): + ''' + Compute the Von Neumann entropy (entanglement measure). + + To calculate the entropies, it is convenient to calculate the + eigendecomposition of `rho` and use the eigenvalues `lambda_i` to determine + the entropy: + `S(rho) = -sum_i( lambda_i * ln(lambda_i) )` + ''' + evals = np.real(np.linalg.eigvals(rho.data)) + return -np.sum([e * np.log2(e) for e in evals if 0 < e < 1]) + +def concurrence(rho): + ''' + Concurrence specifically quantifies the quantum entanglement between the + two qubits. It does not consider classical correlations. + + https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.80.2245 + ''' + # Compute the spin-flipped state + sigma_y = np.array([[0, -1j], [1j, 0]]) + rho_star = np.conj(rho) + rho_tilde = np.kron(sigma_y, sigma_y) @ rho_star @ np.kron(sigma_y, sigma_y) + + # Calculate the eigenvalues of the product matrix + eigenvalues = np.linalg.eigvals(rho @ rho_tilde) + # Sort in decreasing order + eigenvalues = np.sort(np.sqrt(np.abs(eigenvalues)))[::-1] + + # Compute the concurrence + return max(0, eigenvalues[0] - sum(eigenvalues[1:])) + +def mutual_information(rho_a, rho_b, rho_ab): + ''' + Mutual information quantifies the total amount of correlation between two + qubits. It includes both classical and quantum correlations. + ''' + + # Compute the Von Neumann entropy for each density matrix + s_a = von_neumann_entropy(rho_a) + s_b = von_neumann_entropy(rho_b) + s_ab = von_neumann_entropy(rho_ab) + + # Calculate the mutual information + return s_a + s_b - s_ab + +def correlation(state_vector, set_a, set_b, correlation_measure=mutual_information): + ''' + Compute the correlation between subsystems A and B. + ''' + + if (len(set_a) > 1 or len(set_b) > 1) and correlation_measure is concurrence: + raise ValueError( + "The value of `correlation_measure` cannot be `concurrence` when " + "`len(set_a) > 1` or `len(set_b) > 1`. Choose, for example, " + "`mutual_information` instead." + ) + + psi = Statevector(state_vector) + + # Compute the reduced density matrix for the union of the two sets. + set_ab = set_a.union(set_b) + rho_ab = partial_trace(psi, list(set(range(psi.num_qubits)).difference(set_ab))) + + # Maintains the relative position between the qubits of the two subsystems. + new_set_a = [sum(i < item for i in set_ab) for item in set_a] + new_set_b = [sum(i < item for i in set_ab) for item in set_b] + + # Calculate the reduced density matrice for each set. + rho_a = partial_trace(rho_ab, new_set_b) + rho_b = partial_trace(rho_ab, new_set_a) + + if correlation_measure is mutual_information: + return correlation_measure(rho_a, rho_b, rho_ab) + + return correlation_measure(rho_ab) + +def correlation_graph(state_vector, n_qubits, max_set_size=1, correlation_measure=mutual_information): + ''' + Initialize a graph where nodes represent qubits and the weights represent + the entanglement between pairs of qubits in a register of `n` qubits for a + pure state. + + O(n^2) x O(2^n) + ''' + if n_qubits <= max_set_size <= 0: + raise ValueError( + "The value of `max_set_size` must be greater than zero and less " + "than `n_qubits`." + ) + + # Create a graph. + graph = nx.Graph() + + # Add nodes for each set of qubits up to the size `max_set_size`. + for set_size in range(1, max_set_size + 1): + for qubit_set in itertools.combinations(range(n_qubits), set_size): + graph.add_node(qubit_set) + + # Add edges with weights representing entanglement. + for node_a in graph.nodes(): + set_a = set(node_a) + for node_b in graph.nodes(): + set_b = set(node_b) + # Ensure non-overlapping sets. + if not set_a.intersection(set_b): + # Compute the correlation betweem subsystems. + weight = correlation( + state_vector, + set_a, + set_b, + correlation_measure=correlation_measure + ) + + # Add an edge with the shared info as weight. + graph.add_edge(node_a, node_b, weight=weight) + + return graph From b9c3c22b835d6d134be845bebd8f911d203fb82f Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sat, 3 Feb 2024 20:26:35 +0900 Subject: [PATCH 25/26] Adjust to the new modules --- test/test_graph.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/test_graph.py b/test/test_graph.py index e0fd35c..851cbcb 100644 --- a/test/test_graph.py +++ b/test/test_graph.py @@ -19,7 +19,7 @@ from unittest import TestCase from math import isclose import numpy as np -from qdna.quantum_info import entanglement_graph +from qdna.quantum_info import correlation_graph, concurrence from qdna.graph import min_cut_fixed_size_optimal, \ min_cut_fixed_size_heuristic @@ -41,12 +41,12 @@ def test_min_cut_optimal_product_state(self): state_vector = np.kron(state_vector1, state_vector2) - graph = entanglement_graph(state_vector, 6) + graph = correlation_graph(state_vector, 6, correlation_measure=concurrence) (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + self.assertTrue(np.allclose(sorted(sum(set_a, ())), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(sum(set_b, ())), [3, 4, 5])) def test_min_cut_heuristic_product_state(self): n_qubits = 3 @@ -59,12 +59,12 @@ def test_min_cut_heuristic_product_state(self): state_vector = np.kron(state_vector1, state_vector2) - graph = entanglement_graph(state_vector, 6) + graph = correlation_graph(state_vector, 6, correlation_measure=concurrence) (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + self.assertTrue(np.allclose(sorted(sum(set_a, ())), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(sum(set_b, ())), [3, 4, 5])) def test_min_cut_optimal_fixed(self): state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, @@ -84,12 +84,12 @@ def test_min_cut_optimal_fixed(self): 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] - graph = entanglement_graph(state_vector, 6) + graph = correlation_graph(state_vector, 6, correlation_measure=concurrence) (set_a, set_b), cut_weight = min_cut_fixed_size_optimal(graph, 3, 3) self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + self.assertTrue(np.allclose(sorted(sum(set_a, ())), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(sum(set_b, ())), [3, 4, 5])) def test_min_cut_heuristic_fixed(self): state_vector = [0.00000000e+00, 6.94991284e-04, 6.34061054e-02, 2.13286776e-01, @@ -109,9 +109,9 @@ def test_min_cut_heuristic_fixed(self): 0.00000000e+00, 2.11959665e-04, 6.15966561e-02, 2.17282223e-01, 2.49816212e-01, 1.27759885e-01, 2.73632167e-02, 1.20487999e-02] - graph = entanglement_graph(state_vector, 6) + graph = correlation_graph(state_vector, 6, correlation_measure=concurrence) (set_a, set_b), cut_weight = min_cut_fixed_size_heuristic(graph, 3, 3) self.assertTrue(isclose(cut_weight, 0.0)) - self.assertTrue(np.allclose(sorted(set_a), [0, 1, 2])) - self.assertTrue(np.allclose(sorted(set_b), [3, 4, 5])) + self.assertTrue(np.allclose(sorted(sum(set_a, ())), [0, 1, 2])) + self.assertTrue(np.allclose(sorted(sum(set_b, ())), [3, 4, 5])) From e1f4c293820747c98f3a38a7dc06dc8e0d5eb70f Mon Sep 17 00:00:00 2001 From: israelferrazaraujo Date: Sun, 24 Mar 2024 18:40:26 +0900 Subject: [PATCH 26/26] Includes the `min_set_size` parameter --- qdna/quantum_info.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/qdna/quantum_info.py b/qdna/quantum_info.py index 900bc99..9eed232 100644 --- a/qdna/quantum_info.py +++ b/qdna/quantum_info.py @@ -94,7 +94,12 @@ def correlation(state_vector, set_a, set_b, correlation_measure=mutual_informati return correlation_measure(rho_ab) -def correlation_graph(state_vector, n_qubits, max_set_size=1, correlation_measure=mutual_information): +def correlation_graph(state_vector, + n_qubits, + min_set_size=1, + max_set_size=1, + correlation_measure=mutual_information +): ''' Initialize a graph where nodes represent qubits and the weights represent the entanglement between pairs of qubits in a register of `n` qubits for a @@ -104,15 +109,22 @@ def correlation_graph(state_vector, n_qubits, max_set_size=1, correlation_measur ''' if n_qubits <= max_set_size <= 0: raise ValueError( - "The value of `max_set_size` must be greater than zero and less " - "than `n_qubits`." + f"The value of `max_set_size` [{max_set_size}] must be greater " + f"than zero and less than `n_qubits` [{n_qubits}]." + ) + + if max_set_size < min_set_size <= 0: + raise ValueError( + f"The value of `min_set_size` [{min_set_size}] must be greater " + f"than zero and less or equal than `max_set_size` " + f"[{max_set_size}]." ) # Create a graph. graph = nx.Graph() # Add nodes for each set of qubits up to the size `max_set_size`. - for set_size in range(1, max_set_size + 1): + for set_size in range(min_set_size, max_set_size + 1): for qubit_set in itertools.combinations(range(n_qubits), set_size): graph.add_node(qubit_set)