-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added state preparation compiler feature (#28)
__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
1 parent
de01837
commit 457f39c
Showing
12 changed files
with
873 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
Empty file.
119 changes: 119 additions & 0 deletions
119
src/mqt/qudits/compiler/state_compilation/retrieve_state.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
175
src/mqt/qudits/compiler/state_compilation/state_preparation.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.