diff --git a/qadence/constructors/__init__.py b/qadence/constructors/__init__.py new file mode 100644 index 000000000..13b76d852 --- /dev/null +++ b/qadence/constructors/__init__.py @@ -0,0 +1,41 @@ +# flake8: noqa + +from .feature_maps import ( + feature_map, + chebyshev_feature_map, + fourier_feature_map, + tower_feature_map, + exp_fourier_feature_map, +) + +from .ansatze import hea, build_qnn + +from .daqc import daqc_transform + +from .hamiltonians import ( + hamiltonian_factory, + ising_hamiltonian, + single_z, + total_magnetization, + zz_hamiltonian, +) + +from .qft import qft + +# Modules to be automatically added to the qadence namespace +__all__ = [ + "feature_map", + "chebyshev_feature_map", + "fourier_feature_map", + "tower_feature_map", + "exp_fourier_feature_map", + "hea", + "build_qnn", + "hamiltonian_factory", + "ising_hamiltonian", + "single_z", + "total_magnetization", + "zz_hamiltonian", + "qft", + "daqc_transform", +] diff --git a/qadence/constructors/ansatze.py b/qadence/constructors/ansatze.py new file mode 100644 index 000000000..e96b7d21f --- /dev/null +++ b/qadence/constructors/ansatze.py @@ -0,0 +1,385 @@ +from __future__ import annotations + +import itertools +from typing import Any, Optional, Type, Union + +from qadence.blocks import AbstractBlock, block_is_qubit_hamiltonian, chain, kron, tag +from qadence.operations import CNOT, CPHASE, CRX, CRY, CRZ, CZ, RX, RY, HamEvo +from qadence.types import Interaction, Strategy + +from .hamiltonians import hamiltonian_factory +from .utils import build_idx_fms + +DigitalEntanglers = Union[CNOT, CZ, CRZ, CRY, CRX] + + +def hea( + n_qubits: int, + depth: int = 1, + param_prefix: str = "theta", + support: tuple[int, ...] = None, + strategy: Strategy = Strategy.DIGITAL, + **strategy_args: Any, +) -> AbstractBlock: + """ + Factory function for the Hardware Efficient Ansatz (HEA). + + Args: + n_qubits: number of qubits in the block + depth: number of layers of the HEA + param_prefix: the base name of the variational parameters + support: qubit indexes where the HEA is applied + strategy: Strategy.Digital or Strategy.DigitalAnalog + **strategy_args: see below + + Keyword Arguments: + operations (list): list of operations to cycle through in the + digital single-qubit rotations of each layer. Valid for + Digital and DigitalAnalog HEA. + periodic (bool): if the qubits should be linked periodically. + periodic=False is not supported in emu-c. Valid for only + for Digital HEA. + entangler (AbstractBlock): + - Digital: 2-qubit entangling operation. Supports CNOT, CZ, + CRX, CRY, CRZ, CPHASE. Controlled rotations will have variational + parameters on the rotation angles. + - DigitaAnalog | Analog: Hamiltonian generator for the + analog entangling layer. Defaults to global ZZ Hamiltonian. + Time parameter is considered variational. + + + Examples: + ```python exec="on" source="material-block" result="json" + from qadence import RZ, RX + from qadence import hea + + # create the circuit + n_qubits, depth = 2, 4 + ansatz = hea( + n_qubits=n_qubits, + depth=depth, + strategy="sDAQC", + operations=[RZ,RX,RZ] + ) + ``` + """ + + if support is None: + support = tuple(range(n_qubits)) + + hea_func_dict = { + Strategy.DIGITAL: hea_digital, + Strategy.SDAQC: hea_sDAQC, + Strategy.BDAQC: hea_bDAQC, + Strategy.ANALOG: hea_analog, + } + + try: + hea_func = hea_func_dict[strategy] + except KeyError: + raise KeyError(f"Strategy {strategy} not recognized.") + + hea_block: AbstractBlock = hea_func( + n_qubits=n_qubits, + depth=depth, + param_prefix=param_prefix, + support=support, + **strategy_args, + ) # type: ignore + + return hea_block + + +############# +## DIGITAL ## +############# + + +def _rotations_digital( + n_qubits: int, + depth: int, + param_prefix: str = "theta", + support: tuple[int, ...] = None, + operations: list[Type[AbstractBlock]] = [RX, RY, RX], +) -> list[AbstractBlock]: + """ + Creates the layers of single qubit rotations in an HEA. + """ + if support is None: + support = tuple(range(n_qubits)) + iterator = itertools.count() + rot_list: list[AbstractBlock] = [] + for d in range(depth): + rots = [ + kron( + gate(support[n], param_prefix + f"_{next(iterator)}") # type: ignore [arg-type] + for n in range(n_qubits) + ) + for gate in operations + ] + rot_list.append(chain(*rots)) + return rot_list + + +def _entangler( + control: int, + target: int, + param_str: str, + op: Type[DigitalEntanglers] = CNOT, +) -> AbstractBlock: + if op in [CNOT, CZ]: + return op(control, target) # type: ignore + elif op in [CRZ, CRY, CRX, CPHASE]: + return op(control, target, param_str) # type: ignore + else: + raise ValueError("Provided entangler not accepted for digital HEA.") + + +def _entanglers_digital( + n_qubits: int, + depth: int, + param_prefix: str = "theta", + support: tuple[int, ...] = None, + periodic: bool = False, + entangler: Type[DigitalEntanglers] = CNOT, +) -> list[AbstractBlock]: + """ + Creates the layers of digital entangling operations in an HEA. + """ + if support is None: + support = tuple(range(n_qubits)) + iterator = itertools.count() + ent_list: list[AbstractBlock] = [] + for d in range(depth): + ents = [] + ents.append( + kron( + _entangler( + control=support[n], + target=support[n + 1], + param_str=param_prefix + f"_ent_{next(iterator)}", + op=entangler, + ) + for n in range(n_qubits) + if not n % 2 and n < n_qubits - 1 + ) + ) + if n_qubits > 2: + ents.append( + kron( + _entangler( + control=support[n], + target=support[(n + 1) % n_qubits], + param_str=param_prefix + f"_ent_{next(iterator)}", + op=entangler, + ) + for n in range(n_qubits - (not periodic)) + if n % 2 + ) + ) + ent_list.append(chain(*ents)) + return ent_list + + +def hea_digital( + n_qubits: int, + depth: int = 1, + param_prefix: str = "theta", + periodic: bool = False, + operations: list[type[AbstractBlock]] = [RX, RY, RX], + support: tuple[int, ...] = None, + entangler: Type[DigitalEntanglers] = CNOT, +) -> AbstractBlock: + """ + Construct the Digital Hardware Efficient Ansatz (HEA). + + Args: + n_qubits (int): number of qubits in the block. + depth (int): number of layers of the HEA. + param_prefix (str): the base name of the variational parameters + periodic (bool): if the qubits should be linked periodically. + periodic=False is not supported in emu-c. + operations (list): list of operations to cycle through in the + digital single-qubit rotations of each layer. + support (tuple): qubit indexes where the HEA is applied. + entangler (AbstractBlock): 2-qubit entangling operation. + Supports CNOT, CZ, CRX, CRY, CRZ. Controlld rotations + will have variational parameters on the rotation angles. + """ + try: + if entangler not in [CNOT, CZ, CRX, CRY, CRZ, CPHASE]: + raise ValueError( + "Please provide a valid two-qubit entangler operation for digital HEA." + ) + except TypeError: + raise ValueError("Please provide a valid two-qubit entangler operation for digital HEA.") + + rot_list = _rotations_digital( + n_qubits=n_qubits, + depth=depth, + param_prefix=param_prefix, + support=support, + operations=operations, + ) + + ent_list = _entanglers_digital( + n_qubits=n_qubits, + depth=depth, + param_prefix=param_prefix, + support=support, + periodic=periodic, + entangler=entangler, + ) + + layers = [] + for d in range(depth): + layers.append(rot_list[d]) + layers.append(ent_list[d]) + return tag(chain(*layers), "HEA") + + +########### +## sDAQC ## +########### + + +def _entanglers_analog( + depth: int, + param_prefix: str = "theta", + entangler: AbstractBlock | None = None, +) -> list[AbstractBlock]: + return [HamEvo(entangler, param_prefix + f"_t_{d}") for d in range(depth)] + + +def hea_sDAQC( + n_qubits: int, + depth: int = 1, + param_prefix: str = "theta", + operations: list[type[AbstractBlock]] = [RX, RY, RX], + support: tuple[int, ...] = None, + entangler: AbstractBlock | None = None, +) -> AbstractBlock: + """ + Construct the Hardware Efficient Ansatz (HEA) with analog entangling layers + using step-wise digital-analog computation. + + Args: + n_qubits (int): number of qubits in the block. + depth (int): number of layers of the HEA. + param_prefix (str): the base name of the variational parameters + operations (list): list of operations to cycle through in the + digital single-qubit rotations of each layer. + support (tuple): qubit indexes where the HEA is applied. + entangler (AbstractBlock): Hamiltonian generator for the + analog entangling layer. Defaults to global ZZ Hamiltonian. + Time parameter is considered variational. + """ + + # TODO: Add qubit support + if entangler is None: + entangler = hamiltonian_factory(n_qubits, interaction=Interaction.NN) + try: + if not block_is_qubit_hamiltonian(entangler): + raise ValueError( + "Please provide a valid Pauli Hamiltonian generator for digital-analog HEA." + ) + except NotImplementedError: + raise ValueError( + "Please provide a valid Pauli Hamiltonian generator for digital-analog HEA." + ) + + rot_list = _rotations_digital( + n_qubits=n_qubits, + depth=depth, + param_prefix=param_prefix, + support=support, + operations=operations, + ) + + ent_list = _entanglers_analog( + depth=depth, + param_prefix=param_prefix, + entangler=entangler, + ) + + layers = [] + for d in range(depth): + layers.append(rot_list[d]) + layers.append(ent_list[d]) + return tag(chain(*layers), "HEA-sDA") + + +########### +## bDAQC ## +########### + + +def hea_bDAQC(*args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + +############ +## ANALOG ## +############ + + +def hea_analog(*args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + +######### +## QNN ## +######### + + +def build_qnn( + n_qubits: int, + n_features: int, + depth: int = None, + ansatz: Optional[AbstractBlock] = None, + fm_pauli: Type[RY] = RY, + spectrum: str = "simple", + basis: str = "fourier", + fm_strategy: str = "parallel", +) -> list[AbstractBlock]: + """Helper function to build a qadence QNN quantum circuit + + Args: + n_qubits (int): The number of qubits. + n_features (int): The number of input dimensions. + depth (int): The depth of the ansatz. + ansatz (Optional[AbstractBlock]): An optional argument to pass a custom qadence ansatz. + fm_pauli (str): The type of Pauli gate for the feature map. Must be one of 'RX', + 'RY', or 'RZ'. + spectrum (str): The desired spectrum of the feature map generator. The options simple, + tower and exponential produce a spectrum with linear, quadratic and exponential + eigenvalues with respect to the number of qubits. + basis (str): The encoding function. The options fourier and chebyshev correspond to Φ(x)=x + and arcos(x) respectively. + fm_strategy (str): The feature map encoding strategy. If "parallel", the features + are encoded in one block of rotation gates, with each feature given + an equal number of qubits. If "serial", the features are encoded + sequentially, with a HEA block between. + + Returns: + A list of Abstract blocks to be used for constructing a quantum circuit + """ + depth = n_qubits if depth is None else depth + + idx_fms = build_idx_fms(basis, fm_pauli, fm_strategy, n_features, n_qubits, spectrum) + + if fm_strategy == "parallel": + _fm = kron(*idx_fms) + fm = tag(_fm, tag="FM") + + elif fm_strategy == "serial": + fm_components: list[AbstractBlock] = [] + for j, fm_idx in enumerate(idx_fms[:-1]): + fm_idx = tag(fm_idx, tag=f"FM{j}") # type: ignore[assignment] + fm_component = (fm_idx, hea(n_qubits, 1, f"theta_{j}")) + fm_components.extend(fm_component) + fm_components.append(tag(idx_fms[-1], tag=f"FM{len(idx_fms) - 1}")) + fm = chain(*fm_components) # type: ignore[assignment] + + ansatz = hea(n_qubits, depth=depth) if ansatz is None else ansatz + return [fm, ansatz] diff --git a/qadence/constructors/daqc/__init__.py b/qadence/constructors/daqc/__init__.py new file mode 100644 index 000000000..11961266c --- /dev/null +++ b/qadence/constructors/daqc/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa + +from .daqc import daqc_transform + +# Modules to be automatically added to the qucint namespace +__all__ = [] # type: ignore diff --git a/qadence/constructors/daqc/daqc.py b/qadence/constructors/daqc/daqc.py new file mode 100644 index 000000000..44b3addfe --- /dev/null +++ b/qadence/constructors/daqc/daqc.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +import torch + +from qadence.blocks import AbstractBlock, add, chain, kron +from qadence.blocks.utils import block_is_qubit_hamiltonian +from qadence.constructors.hamiltonians import hamiltonian_factory +from qadence.logger import get_logger +from qadence.operations import HamEvo, I, N, X +from qadence.types import GenDAQC, Interaction, Strategy + +from .gen_parser import _check_compatibility, _parse_generator +from .utils import _build_matrix_M, _ix_map + +logger = get_logger(__name__) + + +def daqc_transform( + n_qubits: int, + gen_target: AbstractBlock, + t_f: float, + gen_build: AbstractBlock | None = None, + zero_tol: float = 1e-08, + strategy: Strategy = Strategy.SDAQC, + ignore_global_phases: bool = False, +) -> AbstractBlock: + """ + Implements the DAQC transform for representing an arbitrary 2-body Hamiltonian + with another fixed 2-body Hamiltonian. + + Reference for universality of 2-body Hamiltonians: + + -- https://arxiv.org/abs/quant-ph/0106064 + + Based on the transformation for Ising (ZZ) interactions, as described in the paper + + -- https://arxiv.org/abs/1812.03637 + + The transform translates a target weighted generator of the type: + + `gen_target = add(g_jk * kron(op(j), op(k)) for j < k)` + + To a circuit using analog evolutions with a fixed building block generator: + + `gen_build = add(f_jk * kron(op(j), op(k)) for j < k)` + + where `op = Z` or `op = N`. + + Args: + n_qubits: total number of qubits to use. + gen_target: target generator built with the structure above. The type + of the generator will be automatically evaluated when parsing. + t_f (float): total time for the gen_target evolution. + gen_build: fixed generator to act as a building block. Defaults to + constant NN: add(1.0 * kron(N(j), N(k)) for j < k). The type + of the generator will be automatically evaluated when parsing. + zero_tol: default "zero" for a missing interaction. Included for + numerical reasons, see notes below. + strategy: sDAQC or bDAQC, following definitions in the reference paper. + ignore_global_phases: if `True` the transform does not correct the global + phases coming from the mapping between ZZ and NN interactions. + + Notes: + + The paper follows an index convention of running from 1 to N. A few functions + here also use that convention to be consistent with the paper. However, for qadence + related things the indices are converted to [0, N-1]. + + The case for `n_qubits = 4` is an edge case where the sign matrix is not invertible. + There is a workaround for this described in the paper, but it is currently not implemented. + + The current implementation may result in evolution times that are both positive or + negative. In practice, both can be represented by simply changing the signs of the + interactions. However, for a real implementation where the interactions should remain + fixed, the paper discusses a workaround that is not currently implemented. + + The transformation works by representing each interaction in the target hamiltonian by + a set of evolutions using the build hamiltonian. As a consequence, some care must be + taken when choosing the build hamiltonian. Some cases: + + - The target hamiltonian can have any interaction, as long as it is sufficiently + represented in the build hamiltonian. E.g., if the interaction `g_01 * kron(Z(0), Z(1))` + is in the target hamiltonian, the corresponding interaction `f_01 * kron(Z(0), Z(1))` + needs to be in the build hamiltonian. This is checked when the generators are parsed. + + - The build hamiltonian can have any interaction, irrespectively of it being needed + for the target hamiltonian. This is especially useful for designing local operations + through the repeated evolution of a "global" hamiltonian. + + - The parameter `zero_tol` controls what it means for an interaction to be "missing". + Any interaction strength smaller than `zero_tol` in the build hamiltonian will not be + considered, and thus that interaction is missing. + + - The various ratios `g_jk / f_jk` will influence the time parameter for the various + evolution slices, meaning that if there is a big discrepancy in the interaction strength + for a given qubit pair (j, k), the output circuit may require the usage of hamiltonian + evolutions with very large times. + + - A warning will be issued for evolution times larger than `1/sqrt(zero_tol)`. Evolution + times smaller than `zero_tol` will not be represented. + + + Examples: + ```python exec="on" source="material-block" result="json" + from qadence import Z, N, daqc_transform + + n_qubits = 3 + + gen_build = 0.5 * (N(0)@N(1)) + 0.7 * (N(1)@N(2)) + 0.2 * (N(0)@N(2)) + + gen_target = 0.1 * (Z(1)@Z(2)) + + t_f = 2.0 + + transformed_circuit = daqc_transform( + n_qubits = n_qubits, + gen_target = gen_target, + t_f = t_f, + gen_build = gen_build, + ) + ``` + """ + + ################## + # Input controls # + ################## + + if strategy != Strategy.SDAQC: + raise NotImplementedError("Currently only the sDAQC transform is implemented.") + + if n_qubits == 4: + raise NotImplementedError("DAQC transform 4-qubit edge case not implemented.") + + if gen_build is None: + gen_build = hamiltonian_factory(n_qubits, interaction=Interaction.NN) + + try: + if (not block_is_qubit_hamiltonian(gen_target)) or ( + not block_is_qubit_hamiltonian(gen_build) + ): + raise ValueError( + "Generator block is not a qubit Hamiltonian. Only ZZ or NN interactions allowed." + ) + except NotImplementedError: + # Happens when block_is_qubit_hamiltonian is called on something that is not a block. + raise TypeError( + "Generator block is not a qubit Hamiltonian. Only ZZ or NN interactions allowed." + ) + + ##################### + # Generator parsing # + ##################### + + g_jk_target, mat_jk_target, target_type = _parse_generator(n_qubits, gen_target, 0.0) + g_jk_build, mat_jk_build, build_type = _parse_generator(n_qubits, gen_build, zero_tol) + + # Get the global phase hamiltonian and single-qubit detuning hamiltonian + if build_type == GenDAQC.NN: + h_phase_build, h_sq_build = _nn_phase_and_detunings(n_qubits, mat_jk_build) + + if target_type == GenDAQC.NN: + h_phase_target, h_sq_target = _nn_phase_and_detunings(n_qubits, mat_jk_target) + + # Time re-scalings + if build_type == GenDAQC.ZZ and target_type == GenDAQC.NN: + t_star = t_f / 4.0 + elif build_type == GenDAQC.NN and target_type == GenDAQC.ZZ: + t_star = 4.0 * t_f + else: + t_star = t_f + + # Check if target Hamiltonian can be mapped with the build Hamiltonian + assert _check_compatibility(g_jk_target, g_jk_build, zero_tol) + + ################## + # DAQC Transform # + ################## + + # Section III A of https://arxiv.org/abs/1812.03637: + + # Matrix M for the linear system, exemplified in Table I: + matrix_M = _build_matrix_M(n_qubits) + + # Linear system mapping interaction ratios -> evolution times. + t_slices = torch.linalg.solve(matrix_M, g_jk_target / g_jk_build) * t_star + + # ZZ-DAQC with ZZ or NN build Hamiltonian + daqc_slices = [] + for m in range(2, n_qubits + 1): + for n in range(1, m): + alpha = _ix_map(n_qubits, n, m) + t = t_slices[alpha - 1] + if abs(t) > zero_tol: + if abs(t) > (1 / (zero_tol**0.5)): + logger.warning( + """ +Transformed circuit with very long evolution time. +Make sure your target interactions are sufficiently +represented in the build Hamiltonian.""" + ) + x_gates = kron(X(n - 1), X(m - 1)) + analog_evo = HamEvo(gen_build, t) + # TODO: Fix repeated X-gates + if build_type == GenDAQC.NN: + # Local detuning at each DAQC layer for NN build Hamiltonian + sq_detuning_build = HamEvo(h_sq_build, t) + daqc_slices.append(chain(x_gates, sq_detuning_build, analog_evo, x_gates)) + elif build_type == GenDAQC.ZZ: + daqc_slices.append(chain(x_gates, analog_evo, x_gates)) + + daqc_circuit = chain(*daqc_slices) + + ######################## + # Phases and Detunings # + ######################## + + if target_type == GenDAQC.NN: + # Local detuning given a NN target Hamiltonian + sq_detuning_target = HamEvo(h_sq_target, t_f).dagger() + daqc_circuit = chain(sq_detuning_target, daqc_circuit) + + if not ignore_global_phases: + if build_type == GenDAQC.NN: + # Constant global phase given a NN build Hamiltonian + global_phase_build = HamEvo(h_phase_build, t_slices.sum()) + daqc_circuit = chain(global_phase_build, daqc_circuit) + + if target_type == GenDAQC.NN: + # Constant global phase and given a NN target Hamiltonian + global_phase_target = HamEvo(h_phase_target, t_f).dagger() + daqc_circuit = chain(global_phase_target, daqc_circuit) + + return daqc_circuit + + +def _nn_phase_and_detunings( + n_qubits: int, + mat_jk: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + # Constant global shift, leads to a global phase + global_shift = mat_jk.sum() / 8 + + # Strength of the local detunings + g_sq = mat_jk.sum(0) / 2 + + h_phase = global_shift * kron(I(i) for i in range(n_qubits)) + h_sq = add(-1.0 * g_sq[i] * N(i) for i in range(n_qubits)) + + return h_phase, h_sq diff --git a/qadence/constructors/daqc/gen_parser.py b/qadence/constructors/daqc/gen_parser.py new file mode 100644 index 000000000..d0258e212 --- /dev/null +++ b/qadence/constructors/daqc/gen_parser.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import torch + +from qadence.blocks import AbstractBlock, KronBlock +from qadence.blocks.utils import unroll_block_with_scaling +from qadence.logger import get_logger +from qadence.operations import N, Z +from qadence.parameters import Parameter, evaluate +from qadence.types import GenDAQC + +from .utils import _ix_map + +logger = get_logger(__name__) + + +def _parse_generator( + n_qubits: int, + generator: AbstractBlock, + zero_tol: float, +) -> torch.Tensor: + """ + Parses the input generator to extract the `g_jk` weights + of the Ising model and the respective target qubits `(j, k)`. + """ + + flat_size = int(0.5 * n_qubits * (n_qubits - 1)) + g_jk_list = torch.zeros(flat_size) + g_jk_mat = torch.zeros(n_qubits, n_qubits) + + # This parser is heavily dependent on unroll_block_with_scaling + gen_list = unroll_block_with_scaling(generator) + + # Now we wish to check if generator is of the form: + # `add(g_jk * kron(op(j), op(k)) for j < k)` + # and determine if `op = Z` or `op = N` + + gen_type_Z = [] + gen_type_N = [] + + for block, scale in gen_list: + if isinstance(scale, Parameter): + raise TypeError("DAQC transform does not support parameterized Hamiltonians.") + + # First we check if all relevant blocks (with non-negligible scaling) + # are of type(KronBlock), since we only admit kron(Z, Z) or kron(N, N). + if not isinstance(block, KronBlock): + if abs(scale) < zero_tol: + continue + else: + raise TypeError( + "DAQC transform only supports ZZ or NN interaction Hamiltonians." + "Error found on block: {block}." + ) + + # Next we check and keep track of the contents of each KronBlock + for pauli in block.blocks: + if isinstance(pauli, Z): + gen_type_Z.append(True) + gen_type_N.append(False) + elif isinstance(pauli, N): + gen_type_N.append(True) + gen_type_Z.append(False) + else: + raise ValueError( + "DAQC transform only supports ZZ or NN interaction Hamiltonians." + "Error found on block: {block}." + ) + + # We save the qubit support and interaction + # strength of each KronBlock to be used in DAQC + j, k = block.qubit_support + g_jk = torch.tensor(evaluate(scale), dtype=torch.get_default_dtype()) + + beta = _ix_map(n_qubits, j + 1, k + 1) + + # Flat list of interaction strength + g_jk_list[beta - 1] += g_jk + + # Symmetric matrix of interaction strength + g_jk_mat[j, k] += g_jk + g_jk_mat[k, j] += g_jk + + # Finally we check if all individual interaction terms were + # either ZZ or NN to determine the generator type. + if torch.tensor(gen_type_Z).prod() == 1 and len(gen_type_Z) > 0: + gen_type = GenDAQC.ZZ + elif torch.tensor(gen_type_N).prod() == 1 and len(gen_type_N) > 0: + gen_type = GenDAQC.NN + else: + raise ValueError( + "Wrong Hamiltonian structure provided. " + "Possible mixture of Z and N terms in the Hamiltonian." + ) + + g_jk_list[g_jk_list == 0.0] = zero_tol + + return g_jk_list, g_jk_mat, gen_type + + +def _check_compatibility( + g_jk_target: torch.Tensor, + g_jk_build: torch.Tensor, + zero_tol: float, +) -> bool: + """ + Checks if the build Hamiltonian is missing any interactions needed + for the transformation into the requested target Hamiltonian. + """ + for g_t, g_b in zip(g_jk_target, g_jk_build): + if abs(g_t) > zero_tol and abs(g_b) <= zero_tol: + raise ValueError("Incompatible interactions between target and build Hamiltonians.") + return True diff --git a/qadence/constructors/daqc/utils.py b/qadence/constructors/daqc/utils.py new file mode 100644 index 000000000..44e4f13d8 --- /dev/null +++ b/qadence/constructors/daqc/utils.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import torch + +from qadence.logger import get_logger + +logger = get_logger(__name__) + + +def _k_d(a: int, b: int) -> int: + """Kronecker delta""" + return int(a == b) + + +def _ix_map(n: int, a: int, b: int) -> int: + """Maps `(a, b)` with `b` in [1, n] and `a < b` to range [1, n(n-1)/2]""" + return int(n * (a - 1) - 0.5 * a * (a + 1) + b) + + +def _build_matrix_M(n_qubits: int) -> torch.Tensor: + """Sign matrix used by the DAQC technique for the Ising model.""" + flat_size = int(0.5 * n_qubits * (n_qubits - 1)) + + def matrix_M_ix(j: int, k: int, n: int, m: int) -> float: + return (-1.0) ** (_k_d(n, j) + _k_d(n, k) + _k_d(m, j) + _k_d(m, k)) + + M = torch.zeros(flat_size, flat_size) + for k in range(2, n_qubits + 1): + for j in range(1, k): + for m in range(2, n_qubits + 1): + for n in range(1, m): + alpha = _ix_map(n_qubits, n, m) + beta = _ix_map(n_qubits, j, k) + M[alpha - 1, beta - 1] = matrix_M_ix(j, k, n, m) + return M diff --git a/qadence/constructors/feature_maps.py b/qadence/constructors/feature_maps.py new file mode 100644 index 000000000..648cdb9a9 --- /dev/null +++ b/qadence/constructors/feature_maps.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from typing import Type, Union + +import numpy as np +import sympy + +from qadence.blocks import AbstractBlock, KronBlock, chain, kron, tag +from qadence.operations import RX, RY, RZ, H +from qadence.parameters import FeatureParameter, Parameter + +Rotation = Union[RX, RY, RZ] + + +def feature_map( + n_qubits: int, + support: tuple[int, ...] = None, + param: str = "phi", + op: Type[Rotation] = RX, + fm_type: str = "fourier", +) -> KronBlock: + """Construct a feature map of a given type. + + Arguments: + n_qubits: Number of qubits the feature map covers. Results in `support=range(n_qubits)`. + support: Overrides `n_qubits`. Puts one rotation gate on every qubit in `support`. + param: Parameter of the feature map. + op: Rotation operation of the feature map. + fm_type: Determines the additional expression the final feature parameter (the addtional + term in front of `param`). `"fourier": param` (nothing is done to `param`) + `"chebyshev": 2*acos(param)`, `"tower": (i+1)*2*acos(param)` (where `i` is the qubit + index). + + Example: + ```python exec="on" source="material-block" result="json" + from qadence import feature_map + + fm = feature_map(3, fm_type="fourier") + print(f"{fm = }") + + fm = feature_map(3, fm_type="chebyshev") + print(f"{fm = }") + + fm = feature_map(3, fm_type="tower") + print(f"{fm = }") + ``` + """ + fparam = FeatureParameter(param) + if support is None: + support = tuple(range(n_qubits)) + + assert len(support) <= n_qubits, "Wrong qubit support supplied" + + if fm_type == "fourier": + fm = kron(*[op(qubit, fparam) for qubit in support]) + elif fm_type == "chebyshev": + fm = kron(*[op(qubit, 2 * sympy.acos(fparam)) for qubit in support]) + elif fm_type == "tower": + fm = kron(*[op(qubit, (i + 1) * 2 * sympy.acos(fparam)) for i, qubit in enumerate(support)]) + else: + raise NotImplementedError(f"Feature map {fm_type} not implemented") + fm.tag = "FM" + return fm + + +def fourier_feature_map( + n_qubits: int, support: tuple[int, ...] = None, param: str = "phi", op: Type[Rotation] = RX +) -> AbstractBlock: + """Construct a Fourier feature map + + Args: + n_qubits: number of qubits across which the FM is created + param: The base name for the feature `Parameter` + """ + fm = feature_map(n_qubits, support=support, param=param, op=op, fm_type="fourier") + return tag(fm, tag="FourierFM") + + +def chebyshev_feature_map( + n_qubits: int, support: tuple[int, ...] = None, param: str = "phi", op: Type[Rotation] = RX +) -> AbstractBlock: + """Construct a Chebyshev feature map + + Args: + n_qubits: number of qubits across which the FM is created + support (Iterable[int]): The qubit support + param: The base name for the feature `Parameter` + """ + fm = feature_map(n_qubits, support=support, param=param, op=op, fm_type="chebyshev") + return tag(fm, tag="ChebyshevFM") + + +def tower_feature_map( + n_qubits: int, support: tuple[int, ...] = None, param: str = "phi", op: Type[Rotation] = RX +) -> AbstractBlock: + """Construct a Chebyshev tower feature map + + Args: + n_qubits: number of qubits across which the FM is created + param: The base name for the feature `Parameter` + """ + fm = feature_map(n_qubits, support=support, param=param, op=op, fm_type="tower") + return tag(fm, tag="TowerFM") + + +def exp_fourier_feature_map( + n_qubits: int, + support: tuple[int, ...] = None, + param: str = "x", + feature_range: tuple[float, float] = None, +) -> AbstractBlock: + """ + Exponential fourier feature map, compatible with the DQGM algorithm. + + Args: + n_qubits: number of qubits in the feature + support: qubit support + param: name of feature `Parameter` + feature_range: min and max value of the feature, as floats in a Tuple + """ + + if feature_range is None: + feature_range = (0.0, 2.0**n_qubits) + + if support is None: + support = tuple(range(n_qubits)) + + xmax = max(feature_range) + xmin = min(feature_range) + + x = Parameter(param, trainable=False) + + # The feature map works on the range of 0 to 2**n + x_rescaled = 2 * np.pi * (x - xmin) / (xmax - xmin) + + hlayer = kron(H(qubit) for qubit in support) + rlayer = kron(RZ(support[i], x_rescaled * (2**i)) for i in range(n_qubits)) + + return tag(chain(hlayer, rlayer), f"ExpFourierFM({param})") diff --git a/qadence/constructors/hamiltonians.py b/qadence/constructors/hamiltonians.py new file mode 100644 index 000000000..8577affb0 --- /dev/null +++ b/qadence/constructors/hamiltonians.py @@ -0,0 +1,286 @@ +from __future__ import annotations + +import warnings +from typing import List, Tuple, Type, Union + +import numpy as np +import torch + +from qadence.blocks import AbstractBlock, add +from qadence.logger import get_logger +from qadence.operations import N, X, Y, Z +from qadence.register import Register +from qadence.types import Interaction, TArray + +logger = get_logger(__name__) + + +def interaction_zz(i: int, j: int) -> AbstractBlock: + """Ising ZZ interaction.""" + return Z(i) @ Z(j) + + +def interaction_nn(i: int, j: int) -> AbstractBlock: + """Ising NN interaction.""" + return N(i) @ N(j) + + +def interaction_xy(i: int, j: int) -> AbstractBlock: + """XY interaction.""" + return X(i) @ X(j) + Y(i) @ Y(j) + + +def interaction_xyz(i: int, j: int) -> AbstractBlock: + """Heisenberg XYZ interaction.""" + return X(i) @ X(j) + Y(i) @ Y(j) + Z(i) @ Z(j) + + +INTERACTION_DICT = { + Interaction.ZZ: interaction_zz, + Interaction.NN: interaction_nn, + Interaction.XY: interaction_xy, + Interaction.XYZ: interaction_xyz, +} + + +ARRAYS = (list, np.ndarray, torch.Tensor) + +DETUNINGS = (N, X, Y, Z) + +TDetuning = Union[Type[N], Type[X], Type[Y], Type[Z]] + + +def hamiltonian_factory( + register: Register | int, + interaction: Interaction | None = None, + detuning: TDetuning | None = None, + interaction_strength: TArray | str | None = None, + detuning_strength: TArray | str | None = None, + random_strength: bool = False, + force_update: bool = False, +) -> AbstractBlock: + """ + General Hamiltonian creation function. Can be used to create Hamiltonians with 2-qubit + interactions and single-qubit detunings, both with arbitrary strength or parameterized. + + Arguments: + register: register of qubits with a specific graph topology, or number of qubits. + When passing a number of qubits a register with all-to-all connectivity + is created. + interaction: Interaction.ZZ, Interaction.NN, Interaction.XY, or Interacton.XYZ. + detuning: single-qubit operator N, X, Y, or Z. + interaction_strength: list of values to be used as the interaction strength for each + pair of qubits. Should be ordered following the order of `Register(n_qubits).edges`. + Alternatively, some string "x" can be passed, which will create a parameterized + interactions for each pair of qubits, each labelled as `"x_ij"`. + detuning_strength: list of values to be used as the detuning strength for each qubit. + Alternatively, some string "x" can be passed, which will create a parameterized + detuning for each qubit, each labelled as `"x_i"`. + random_strength: set random interaction and detuning strengths between -1 and 1. + force_update: force override register detuning and interaction strengths. + + Examples: + ```python exec="on" source="material-block" result="json" + from qadence import hamiltonian_factory, Interaction, Register, Z + + n_qubits = 3 + + # Constant total magnetization observable: + observable = hamiltonian_factory(n_qubits, detuning = Z) + + # Parameterized total magnetization observable: + observable = hamiltonian_factory(n_qubits, detuning = Z, detuning_strength = "z") + + # Random all-to-all XY Hamiltonian generator: + generator = hamiltonian_factory( + n_qubits, + interaction = Interaction.XY, + random_strength = True, + ) + + # Parameterized NN Hamiltonian generator with a square grid interaction topology: + register = Register.square(qubits_side = n_qubits) + generator = hamiltonian_factory( + register, + interaction = Interaction.NN, + interaction_strength = "theta" + ) + ``` + """ + + if interaction is None and detuning is None: + raise ValueError("Please provide an interaction and/or detuning for the Hamiltonian.") + + # If number of qubits is given, creates all-to-all register + register = Register(register) if isinstance(register, int) else register + + # Get interaction function + try: + int_fn = INTERACTION_DICT[interaction] # type: ignore [index] + except (KeyError, ValueError) as error: + if interaction is None: + pass + else: + raise KeyError(f"Interaction {interaction} not supported.") + + # Check single-qubit detuning + if (detuning is not None) and (detuning not in DETUNINGS): + raise TypeError(f"Detuning of type {type(detuning)} not supported.") + + # Pre-process detuning and interaction strengths and update register + has_detuning_strength, detuning_strength = _preprocess_strengths( + register, detuning_strength, "nodes", force_update, random_strength + ) + has_interaction_strength, interaction_strength = _preprocess_strengths( + register, interaction_strength, "edges", force_update, random_strength + ) + + if (not has_detuning_strength) or force_update: + register = _update_detuning_strength(register, detuning_strength) + + if (not has_interaction_strength) or force_update: + register = _update_interaction_strength(register, interaction_strength) + + # Create single-qubit detunings: + single_qubit_terms: List[AbstractBlock] = [] + if detuning is not None: + for node in register.nodes: + block_sq = detuning(node) # type: ignore [operator] + strength_sq = register.nodes[node]["strength"] + single_qubit_terms.append(strength_sq * block_sq) + + # Create two-qubit interactions: + two_qubit_terms: List[AbstractBlock] = [] + if interaction is not None: + for edge in register.edges: + block_tq = int_fn(*edge) # type: ignore [operator] + strength_tq = register.edges[edge]["strength"] + two_qubit_terms.append(strength_tq * block_tq) + + return add(*single_qubit_terms, *two_qubit_terms) + + +def _preprocess_strengths( + register: Register, + strength: TArray | str | None, + nodes_or_edges: str, + force_update: bool, + random_strength: bool, +) -> Tuple[bool, Union[TArray | str]]: + data = getattr(register, nodes_or_edges) + + # Useful for error messages: + strength_target = "detuning" if nodes_or_edges == "nodes" else "interaction" + + # First we check if strength values already exist in the register + has_strength = any(["strength" in data[i] for i in data]) + if has_strength and not force_update: + if strength is not None: + logger.warning( + "Register already includes " + strength_target + " strengths. " + "Skipping update. Use `force_update = True` to override them." + ) + # Next we process the strength given in the input arguments + if strength is None: + if random_strength: + strength = 2 * torch.rand(len(data), dtype=torch.double) - 1 + else: + # None defaults to constant = 1.0 + strength = torch.ones(len(data), dtype=torch.double) + elif isinstance(strength, ARRAYS): + # If array is given, checks it has the correct length + if len(strength) != len(data): + message = "Array of " + strength_target + " strengths has incorrect size." + raise ValueError(message) + elif isinstance(strength, str): + # Any string will be used as a prefix to variational parameters + pass + else: + # If not of the accepted types ARRAYS or str, we error out + raise TypeError( + "Incorrect " + strength_target + f" strength type {type(strength)}. " + "Please provide an array of strength values, or a string for " + "parameterized " + strength_target + "s." + ) + + return has_strength, strength + + +def _update_detuning_strength(register: Register, detuning_strength: TArray | str) -> Register: + for node in register.nodes: + if isinstance(detuning_strength, str): + register.nodes[node]["strength"] = detuning_strength + f"_{node}" + elif isinstance(detuning_strength, ARRAYS): + register.nodes[node]["strength"] = detuning_strength[node] + return register + + +def _update_interaction_strength( + register: Register, interaction_strength: TArray | str +) -> Register: + for idx, edge in enumerate(register.edges): + if isinstance(interaction_strength, str): + register.edges[edge]["strength"] = interaction_strength + f"_{edge[0]}{edge[1]}" + elif isinstance(interaction_strength, ARRAYS): + register.edges[edge]["strength"] = interaction_strength[idx] + return register + + +# FIXME: Previous hamiltonian / observable functions, now refactored, to be deprecated: + +DEPRECATION_MESSAGE = "This function will be removed in the future. " + + +def single_z(qubit: int = 0, z_coefficient: float = 1.0) -> AbstractBlock: + message = DEPRECATION_MESSAGE + "Please use `z_coefficient * Z(qubit)` directly." + warnings.warn(message, FutureWarning) + return Z(qubit) * z_coefficient + + +def total_magnetization(n_qubits: int, z_terms: np.ndarray | list | None = None) -> AbstractBlock: + message = ( + DEPRECATION_MESSAGE + + "Please use `hamiltonian_factory(n_qubits, detuning=Z, node_coeff=z_terms)`." + ) + warnings.warn(message, FutureWarning) + return hamiltonian_factory(n_qubits, detuning=Z, detuning_strength=z_terms) + + +def zz_hamiltonian( + n_qubits: int, + z_terms: np.ndarray | None = None, + zz_terms: np.ndarray | None = None, +) -> AbstractBlock: + message = ( + DEPRECATION_MESSAGE + + """ +Please use `hamiltonian_factory(n_qubits, Interaction.ZZ, Z, interaction_strength, z_terms)`. \ +Note that the argument `zz_terms` in this function is a 2D array of size `(n_qubits, n_qubits)`, \ +while `interaction_strength` is expected as a 1D array of size `0.5 * n_qubits * (n_qubits - 1)`.""" + ) + warnings.warn(message, FutureWarning) + if zz_terms is not None: + register = Register(n_qubits) + interaction_strength = [zz_terms[edge[0], edge[1]] for edge in register.edges] + else: + interaction_strength = None + + return hamiltonian_factory(n_qubits, Interaction.ZZ, Z, interaction_strength, z_terms) + + +def ising_hamiltonian( + n_qubits: int, + x_terms: np.ndarray | None = None, + z_terms: np.ndarray | None = None, + zz_terms: np.ndarray | None = None, +) -> AbstractBlock: + message = ( + DEPRECATION_MESSAGE + + """ +You can build a general transverse field ising model with the `hamiltonian_factory` function. \ +Check the hamiltonian construction tutorial in the documentation for more information.""" + ) + warnings.warn(message, FutureWarning) + zz_ham = zz_hamiltonian(n_qubits, z_terms=z_terms, zz_terms=zz_terms) + x_ham = hamiltonian_factory(n_qubits, detuning=X, detuning_strength=x_terms) + return zz_ham + x_ham diff --git a/qadence/constructors/qft.py b/qadence/constructors/qft.py new file mode 100644 index 000000000..5604e223c --- /dev/null +++ b/qadence/constructors/qft.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +from typing import Any + +import torch + +from qadence.blocks import AbstractBlock, add, chain, kron, tag +from qadence.operations import CPHASE, SWAP, H, HamEvo, I, Z +from qadence.types import Strategy + +from .daqc import daqc_transform + + +def qft( + n_qubits: int, + support: tuple[int, ...] = None, + inverse: bool = False, + reverse_in: bool = False, + swaps_out: bool = False, + strategy: Strategy = Strategy.DIGITAL, + gen_build: AbstractBlock | None = None, +) -> AbstractBlock: + """ + The Quantum Fourier Transform + + Depending on the application, user should be careful with qubit ordering + in the input and output. This can be controlled with reverse_in and swaps_out + arguments. + + Args: + n_qubits: number of qubits in the QFT + support: qubit support to use + inverse: True performs the inverse QFT + reverse_in: Reverses the input qubits to account for endianness + swaps_out: Performs swaps on the output qubits to match the "textbook" QFT. + strategy: Strategy.Digital or Strategy.sDAQC + gen_build: building block Ising Hamiltonian for the DAQC transform. + Defaults to constant all-to-all Ising. + + Examples: + ```python exec="on" source="material-block" result="json" + from qadence import qft + + n_qubits = 3 + + qft_circuit = qft(n_qubits, strategy = "sDAQC") + ``` + """ + + if support is None: + support = tuple(range(n_qubits)) + + assert len(support) <= n_qubits, "Wrong qubit support supplied" + + if reverse_in: + support = support[::-1] + + qft_layer_dict = { + Strategy.DIGITAL: _qft_layer_digital, + Strategy.SDAQC: _qft_layer_sDAQC, + Strategy.BDAQC: _qft_layer_bDAQC, + Strategy.ANALOG: _qft_layer_analog, + } + + try: + layer_func = qft_layer_dict[strategy] + except KeyError: + raise KeyError(f"Strategy {strategy} not recognized.") + + qft_layers = reversed(range(n_qubits)) if inverse else range(n_qubits) + + qft_circ = chain( + layer_func( + n_qubits=n_qubits, support=support, layer=layer, inverse=inverse, gen_build=gen_build + ) # type: ignore + for layer in qft_layers + ) + + if swaps_out: + swap_ops = [SWAP(support[i], support[n_qubits - i - 1]) for i in range(n_qubits // 2)] + qft_circ = chain(*swap_ops, qft_circ) if inverse else chain(qft_circ, *swap_ops) + + return tag(qft_circ, tag="iQFT") if inverse else tag(qft_circ, tag="QFT") + + +######################## +# STANDARD DIGITAL QFT # +######################## + + +def _qft_layer_digital( + n_qubits: int, + support: tuple[int, ...], + layer: int, + inverse: bool, + gen_build: AbstractBlock | None = None, +) -> AbstractBlock: + """ + Applies the Hadamard gate followed by CPHASE gates + corresponding to one layer of the QFT. + """ + qubit_range_layer = ( + reversed(range(layer + 1, n_qubits)) if inverse else range(layer + 1, n_qubits) + ) + rots = [] + for j in qubit_range_layer: + angle = torch.tensor( + ((-1) ** inverse) * 2 * torch.pi / (2 ** (j - layer + 1)), dtype=torch.cdouble + ) + rots.append(CPHASE(support[j], support[layer], angle)) # type: ignore + if inverse: + return chain(*rots, H(support[layer])) # type: ignore + return chain(H(support[layer]), *rots) # type: ignore + + +######################################## +# DIGITAL-ANALOG QFT (with sDAQC) # +# [1] https://arxiv.org/abs/1906.07635 # +######################################## + + +def _theta(k: int) -> float: + """Eq. (16) from [1]""" + return float(torch.pi / (2 ** (k + 1))) + + +def _alpha(c: int, m: int, k: int) -> float: + """Eq. (16) from [1]""" + if c == m: + return float(torch.pi / (2 ** (k - m + 2))) + else: + return 0.0 + + +def _sqg_gen(n_qubits: int, support: tuple[int, ...], m: int, inverse: bool) -> list[AbstractBlock]: + """ + Eq. (13) from [1] + + Creates the generator corresponding to single-qubit rotations coming + out of the CPHASE decomposition. The paper also includes the generator + for the Hadamard of each layer here, but we left it explicit at + the start of each layer. + """ + k_sqg_list = reversed(range(2, n_qubits - m + 2)) if inverse else range(2, n_qubits - m + 2) + + sqg_gen_list = [] + for k in k_sqg_list: + sqg_gen = ( + kron(I(support[j]) for j in range(n_qubits)) - Z(support[k + m - 2]) - Z(support[m - 1]) + ) + sqg_gen_list.append(_theta(k) * sqg_gen) + + return sqg_gen_list + + +def _tqg_gen(n_qubits: int, support: tuple[int, ...], m: int, inverse: bool) -> list[AbstractBlock]: + """ + Eq. (14) from [1] + + Creates the generator corresponding to the two-qubit ZZ + interactions coming out of the CPHASE decomposition. + """ + k_tqg_list = reversed(range(2, n_qubits + 1)) if inverse else range(2, n_qubits + 1) + + tqg_gen_list = [] + for k in k_tqg_list: + for c in range(1, k): + tqg_gen = kron(Z(support[c - 1]), Z(support[k - 1])) + tqg_gen_list.append(_alpha(c, m, k) * tqg_gen) + + return tqg_gen_list + + +def _qft_layer_sDAQC( + n_qubits: int, + support: tuple[int, ...], + layer: int, + inverse: bool, + gen_build: AbstractBlock, +) -> AbstractBlock: + """ + QFT Layer using the sDAQC technique following the paper: + + -- [1] https://arxiv.org/abs/1906.07635 + + 4 - qubit edge case is not implemented. + + Note: the paper follows an index convention of running from 1 to N. A few functions + here also use that convention to be consistent with the paper. However, for qadence + related things the indices are converted to [0, N-1]. + """ + + # TODO: Properly check and include support for changing qubit support + allowed_support = tuple(range(n_qubits)) + if support != allowed_support and support != allowed_support[::-1]: + raise NotImplementedError("Changing support for DigitalAnalog QFT not yet supported.") + + m = layer + 1 # Paper index convention + + # Generator for the single-qubit rotations contributing to the CPHASE gate + sqg_gen_list = _sqg_gen(n_qubits=n_qubits, support=support, m=m, inverse=inverse) + + # Ising model representing the CPHASE gates two-qubit interactions + tqg_gen_list = _tqg_gen(n_qubits=n_qubits, support=support, m=m, inverse=inverse) + + if len(sqg_gen_list) > 0: + # Single-qubit rotations (leaving the Hadamard explicit) + sq_gate = chain(H(support[m - 1]), HamEvo(add(*sqg_gen_list), -1.0)) + + # Two-qubit interaction in the CPHASE converted with sDAQC + gen_cphases = add(*tqg_gen_list) + transformed_daqc_circuit = daqc_transform( + n_qubits=n_qubits, + gen_target=gen_cphases, + t_f=-1.0, + gen_build=gen_build, + ) + + layer_circ = chain( + sq_gate, + transformed_daqc_circuit, + ) + if inverse: + return layer_circ.dagger() + return layer_circ # type: ignore + else: + return chain(H(support[m - 1])) # type: ignore + + +######################################## +# DIGITAL-ANALOG QFT (with bDAQC) # +# [1] https://arxiv.org/abs/1906.07635 # +######################################## + + +def _qft_layer_bDAQC(*args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + +############ +## ANALOG ## +############ + + +def _qft_layer_analog(*args: Any, **kwargs: Any) -> Any: + raise NotImplementedError diff --git a/qadence/constructors/utils.py b/qadence/constructors/utils.py new file mode 100644 index 000000000..e1a011961 --- /dev/null +++ b/qadence/constructors/utils.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from typing import Iterable, Type + +import numpy as np +import sympy + +from qadence.blocks import KronBlock, kron +from qadence.operations import RY +from qadence.parameters import FeatureParameter, Parameter + + +def generator_prefactor(spectrum: str, qubit_index: int) -> float | int: + """ + Converts a spectrum string (e.g., tower or exponential) to the correct generator prefactor. + """ + spectrum = spectrum.lower() + conversion_dict: dict[str, float | int] = { + "simple": 1, + "tower": qubit_index + 1, + "exponential": 2 * np.pi / (2 ** (qubit_index + 1)), + } + return conversion_dict[spectrum] + + +def basis_func(basis: str, x: Parameter) -> Parameter | sympy.Expr: + basis = basis.lower() + conversion_dict: dict[str, Parameter | sympy.Expr] = { + "fourier": x, + "chebyshev": 2 * sympy.acos(x), + } + return conversion_dict[basis] + + +def build_idx_fms( + basis: str, + fm_pauli: Type[RY], + fm_strategy: str, + n_features: int, + n_qubits: int, + spectrum: str, +) -> list[KronBlock]: + """Builds the index feature maps based on the given parameters. + + Args: + basis (str): Type of basis chosen for the feature map. + fm_pauli (PrimitiveBlock type): The chosen Pauli rotation type. + fm_strategy (str): The feature map strategy to be used. Possible values are + 'parallel' or 'serial'. + n_features (int): The number of features. + n_qubits (int): The number of qubits. + spectrum (str): The chosen spectrum. + + Returns: + List[KronBlock]: The list of index feature maps. + """ + idx_fms = [] + for i in range(n_features): + target_qubits = get_fm_qubits(fm_strategy, i, n_qubits, n_features) + param = FeatureParameter(f"x{i}") + block = kron( + *[ + fm_pauli(qubit, generator_prefactor(spectrum, j) * basis_func(basis, param)) + for j, qubit in enumerate(target_qubits) + ] + ) + idx_fm = block + idx_fms.append(idx_fm) + return idx_fms + + +def get_fm_qubits(fm_strategy: str, i: int, n_qubits: int, n_features: int) -> Iterable: + """Returns the list of target qubits for the given feature map strategy and feature index + + Args: + fm_strategy (str): The feature map strategy to be used. Possible values + are 'parallel' or 'serial'. + i (int): The feature index. + n_qubits (int): The number of qubits. + n_features (int): The number of features. + + Returns: + List[int]: The list of target qubits. + + Raises: + ValueError: If the feature map strategy is not implemented. + """ + if fm_strategy == "parallel": + n_qubits_per_feature = int(n_qubits / n_features) + target_qubits = range(i * n_qubits_per_feature, (i + 1) * n_qubits_per_feature) + elif fm_strategy == "serial": + target_qubits = range(0, n_qubits) + else: + raise ValueError(f"Feature map strategy {fm_strategy} not implemented.") + return target_qubits