Skip to content

Commit

Permalink
Added state preparation compiler feature (#28)
Browse files Browse the repository at this point in the history
__Overview:__
This PR adds a new feature to the compiler, enabling it to create a
sequence of (multi-controlled) operations that will prepare the circuit
to a specific state input by the user.

__Description:__

- __Support Data Structure:__ Added a support data structure called
"micro_dd", which resembles Decision Diagrams. This structure is used to
keep track of specific elements within the state and to aid in
synthesizing the operations.
- __Synthesis Routine:__ Implemented a routine for synthesizing state
preparation circuits.
- __State Functions:__ Added functions to call famous states for
preparation.
- __Testing:__ Included tests to verify the functionality of the new
feature.


__Checklist:__

 - [x] I have added tests that prove my feature works.
 - [x] All new and existing tests passed.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
KevinMTO and pre-commit-ci[bot] authored Jun 5, 2024
1 parent de01837 commit 457f39c
Show file tree
Hide file tree
Showing 12 changed files with 873 additions and 0 deletions.
Empty file added src/mqt/__init__.py
Empty file.
Empty file.
119 changes: 119 additions & 0 deletions src/mqt/qudits/compiler/state_compilation/retrieve_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from __future__ import annotations

import operator
from functools import reduce

import numpy as np


def verify_normalized_state(quantum_state):
squared_magnitudes = np.abs(quantum_state) ** 2
sum_squared_magnitudes = np.sum(squared_magnitudes)

# Check if the sum is approximately equal to one
tolerance = 1e-6
return np.isclose(sum_squared_magnitudes, 1.0, atol=tolerance)


def generate_random_quantum_state(cardinalities):
length = reduce(operator.mul, cardinalities)
# Generate random complex numbers with real and imaginary parts
real_parts = np.random.randn(length)
imag_parts = np.random.randn(length)
complex_nums = real_parts + 1j * imag_parts

# Normalize the array
return complex_nums / np.linalg.norm(complex_nums)


def generate_all_combinations(dimensions: list[int]) -> list[list[int]]:
if len(dimensions) == 0:
return [[]]

all_combinations = []

for i in range(dimensions[0]):
sub_combinations = generate_all_combinations(dimensions[1:])
for sub_combination in sub_combinations:
all_combinations.append([i, *sub_combination])

return all_combinations


def generate_ghz_entries(dimensions: list[int]) -> list[list[int]]:
min_d = min(dimensions)
entries = []

for i in range(min_d):
entry = [i] * len(dimensions)
entries.append(entry)

return entries


def generate_qudit_w_entries(dimensions: list[int]) -> list[list[int]]:
num_positions = len(dimensions)
entries = []

for j, d in enumerate(dimensions):
for i in range(1, d):
entry = [0] * num_positions
entry[j] = i
entries.append(entry)

return entries


def generate_embedded_w_entries(dimensions: list[int]) -> list[list[int]]:
num_positions = len(dimensions)
entries = []

for j in range(len(dimensions)):
entry = [0] * num_positions
entry[j] = 1
entries.append(entry)

return entries


def find_entries_indices(input_list: list[list[int]], sublist: list[list[int]]) -> list[int]:
indices = []

for state in sublist:
id = True
for i in range(len(input_list)):
for j in range(len(input_list[i])):
if input_list[i][j] != state[j]:
id = False
break
if id:
indices.append(i)
id = True

indices.sort()
return indices


def generate_uniform_state(dimensions: list[int], state: str) -> np.array:
all_entries = generate_all_combinations(dimensions)

if state == "qudit-w-state":
# print("qudit-w-state")
state_entries = generate_qudit_w_entries(dimensions)
elif state == "embedded-w-state":
# print("embedded-w-state")
state_entries = generate_embedded_w_entries(dimensions)
elif state == "ghz":
state_entries = generate_ghz_entries(dimensions)
# print("GHZ")
else:
print("Input chose is wrong")
raise Exception

complex_ = np.sqrt(1.0 / len(state_entries))
state_vector = np.array([0.0] * len(all_entries), dtype=complex)

for i in find_entries_indices(all_entries, state_entries):
state_vector[i] = complex_

return state_vector
175 changes: 175 additions & 0 deletions src/mqt/qudits/compiler/state_compilation/state_preparation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from __future__ import annotations

import copy

import numpy as np

from mqt.qudits.core.micro_dd import (
create_decision_tree,
cut_branches,
dd_reduction_aggregation,
dd_reduction_hashing,
getNodeContributions,
normalize_all,
)
from mqt.qudits.quantum_circuit.gates import R


def find_complex_number(x, c):
a = x.real # Real part of x
b = x.imag # Imaginary part of x

# Calculate z
real_part = (c.real - b * c.imag) / (a**2 + b**2)
imag_part = (c.imag + b * c.real) / (a**2 + b**2)
return complex(real_part, imag_part)


def getAngles(from_, to_):
theta = 2 * np.arctan2(abs(from_), abs(to_))
phi = -(np.pi / 2 + np.angle(to_) - np.angle(from_))

return theta, phi


class Operation:
def __init__(self, controls, qudit, levels, angles) -> None:
self._controls = controls
self._qudit = qudit
self._levels = levels
self._angles = angles

def is_z(self):
return self._levels == (-1, 0)

@property
def controls(self):
return self._controls

@controls.setter
def controls(self, value) -> None:
self._controls = value

def get_control_nodes(self):
return [c[0] for c in self._controls]

def get_control_levels(self):
return [c[1] for c in self._controls]

@property
def qudit(self):
return self._qudit

@qudit.setter
def qudit(self, value) -> None:
self._qudit = value

@property
def levels(self):
return self._levels

@levels.setter
def levels(self, value) -> None:
self._levels = value

def get_angles(self):
return self._angles

@property
def theta(self):
return self._angles[0]

@property
def phi(self):
return self._angles[1]

def __str__(self) -> str:
return (
f"QuantumOperation(controls={self._controls}, qudit={self._qudit},"
f" levels={self._levels}, angles={self._angles})"
)


class StatePrep:
def __init__(self, quantum_circuit, state, approx=False) -> None:
self.circuit = quantum_circuit
self.state = state
self.approximation = approx

def retrieve_local_sequence(self, fweight, children):
size = len(children)
qudit = children[0].value
aplog = {}

coef = np.array([c.weight for c in children])

for i in reversed(range(size - 1)):
a, p = getAngles(coef[i + 1], coef[i])
gate = R(self.circuit, "R", qudit, [i, i + 1, a, p], self.circuit.dimensions[qudit], None).to_matrix()
coef = np.dot(gate, coef)
aplog[(i, i + 1)] = (-a, p)

phase_2 = np.angle(find_complex_number(fweight, coef[0]))
aplog[(-1, 0)] = (-phase_2 * 2, 0)

return aplog

def synthesis(self, labels, cardinalities, node, circuit_meta, controls=None, depth=0) -> None:
if controls is None:
controls = []
if node.terminal:
return

rotations = self.retrieve_local_sequence(node.weight, node.children)

for key in sorted(rotations.keys()):
circuit_meta.append(Operation(controls, labels[depth], key, rotations[key]))

if not node.reduced:
for i in range(cardinalities[depth]):
controls_track = copy.deepcopy(controls)
controls_track.append((labels[depth], i))
if len(node.children_index) == 0:
self.synthesis(labels, cardinalities, node.children[i], circuit_meta, controls_track, depth + 1)
else:
self.synthesis(
labels,
cardinalities,
node.children[node.children_index[i]],
circuit_meta,
controls_track,
depth + 1,
)
else:
controls_track = copy.deepcopy(controls)
self.synthesis(
labels, cardinalities, node.children[node.children_index[0]], circuit_meta, controls_track, depth + 1
)

def compile_state(self):
final_state = self.state
cardinalities = self.circuit.dimensions
labels = list(range(len(self.circuit.dimensions)))
ops = []
decision_tree, _number_of_nodes = create_decision_tree(labels, cardinalities, final_state)

if self.approximation:
contributions = getNodeContributions(decision_tree, labels)
cut_branches(contributions, 0.01)
normalize_all(decision_tree, cardinalities)

dd_reduction_hashing(decision_tree, cardinalities)
dd_reduction_aggregation(decision_tree, cardinalities)
self.synthesis(labels, cardinalities, decision_tree, ops, [], 0)

new_circuit = copy.deepcopy(self.circuit)
for op in ops:
if abs(op.theta) > 1e-5:
nodes = op.get_control_nodes()
levels = op.get_control_levels()
if op.is_z():
new_circuit.rz(op.qudit, [0, 1, op.theta]).control(nodes, levels)
else:
new_circuit.r(op.qudit, [op.levels[0], op.levels[1], op.theta, op.phi]).control(nodes, levels)

return new_circuit
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,19 @@ def density_operator(state_vector) -> np.ndarray:
def frobenius_dist(x, y):
a = x - y
return np.sqrt(np.trace(np.abs(a.T.conj() @ a)))


def naive_state_fidelity(state1, state2):
"""
Calculates fidelity between two state vectors.
"""
# Ensure both states have the same dimension
if state1.shape != state2.shape:
msg = "State vectors must have the same dimension."
raise ValueError(msg)

# Inner product of the two states
inner_product = np.conj(state1).dot(state2)

# Fidelity calculation
return np.abs(inner_product) ** 2
Loading

0 comments on commit 457f39c

Please sign in to comment.