Source code for mqt.qudits.compiler.state_compilation.state_preparation

# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM
# Copyright (c) 2025 Munich Quantum Software Company GmbH
# All rights reserved.
#
# SPDX-License-Identifier: MIT
#
# Licensed under the MIT License

from __future__ import annotations

import copy
import typing

import numpy as np

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

if typing.TYPE_CHECKING:
    from numpy.typing import NDArray

    from mqt.qudits.core.micro_dd import MicroDDNode, NodeContribution
    from mqt.qudits.quantum_circuit import QuantumCircuit


[docs] def find_complex_number(x: complex, c: complex) -> complex: 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)
[docs] def get_angles(from_: complex, to_: complex) -> tuple[float, float]: theta = 2 * np.arctan2(abs(from_), abs(to_)) phi = typing.cast("float", -(np.pi / 2 + np.angle(to_) - np.angle(from_))) return theta, phi
[docs] class Operation: def __init__( self, controls: list[tuple[int, int]], qudit: int, levels: tuple[int, int], angles: tuple[float, float] ) -> None: self._controls = controls self._qudit = qudit self._levels = levels self._angles = angles
[docs] def is_z(self) -> bool: return self._levels == (-1, 0)
@property def controls(self) -> list[tuple[int, int]]: return self._controls @controls.setter def controls(self, value: list[tuple[int, int]]) -> None: self._controls = value
[docs] def get_control_nodes(self) -> list[int]: return [c[0] for c in self._controls]
[docs] def get_control_levels(self) -> list[int]: return [c[1] for c in self._controls]
@property def qudit(self) -> int: return self._qudit @qudit.setter def qudit(self, value: int) -> None: self._qudit = value @property def levels(self) -> tuple[int, int]: return self._levels @levels.setter def levels(self, value: tuple[int, int]) -> None: self._levels = value
[docs] def get_angles(self) -> tuple[float, float]: return self._angles
@property def theta(self) -> float: return self._angles[0] @property def phi(self) -> float: return self._angles[1]
[docs] def __str__(self) -> str: return ( f"QuantumOperation(controls={self._controls}, qudit={self._qudit}," f" levels={self._levels}, angles={self._angles})" )
[docs] class StatePrep: def __init__(self, quantum_circuit: QuantumCircuit, state: NDArray[np.complex128], approx: bool = False) -> None: self.circuit = quantum_circuit self.state = state self.approximation = approx
[docs] def retrieve_local_sequence( self, fweight: complex, children: list[MicroDDNode] ) -> dict[tuple[int, int], tuple[float, float]]: size = len(children) qudit = int(children[0].value) aplog = {} coef = np.array([c.weight for c in children]) for i in reversed(range(size - 1)): a, p = get_angles(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 = float(np.angle(find_complex_number(fweight, coef[0]))) aplog[-1, 0] = (-phase_2 * 2.0, 0.0) return aplog
[docs] def synthesis( self, labels: list[int], cardinalities: list[int], node: MicroDDNode, circuit_meta: list[Operation], controls: list[tuple[int, int]] | None = None, depth: int = 0, ) -> None: if controls is None: controls = [] if node.terminal: return if node.weight is not None: rotations = self.retrieve_local_sequence(node.weight, node.children) circuit_meta.extend([ Operation(controls, labels[depth], key, rotations[key]) for key in sorted(rotations.keys()) ]) 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 )
[docs] def compile_state(self) -> QuantumCircuit: final_state = self.state cardinalities = self.circuit.dimensions labels = list(range(len(self.circuit.dimensions))) ops: list[Operation] = [] decision_tree, _number_of_nodes = create_decision_tree(labels, cardinalities, final_state) if self.approximation: contributions: NodeContribution = get_node_contributions(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