Skip to content

Constructors for common quantum circuits

chebyshev_feature_map(n_qubits, support=None, param='phi', op=RX)

Construct a Chebyshev feature map.

PARAMETER DESCRIPTION
n_qubits

number of qubits across which the FM is created

TYPE: int

support

The qubit support

TYPE: Iterable[int] DEFAULT: None

param

The base name for the feature Parameter

TYPE: str DEFAULT: 'phi'

Source code in qadence/constructors/feature_maps.py
def chebyshev_feature_map(
    n_qubits: int, support: tuple[int, ...] = None, param: str = "phi", op: RotationTypes = 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`
    """
    warnings.warn(
        "Function 'chebyshev_feature_map' is deprecated. Please use 'feature_map' directly.",
        FutureWarning,
    )
    fm = feature_map(n_qubits, support=support, param=param, op=op, fm_type=BasisSet.CHEBYSHEV)
    return fm

exp_fourier_feature_map(n_qubits, support=None, param='x', feature_range=None)

Exponential fourier feature map.

PARAMETER DESCRIPTION
n_qubits

number of qubits in the feature

TYPE: int

support

qubit support

TYPE: tuple[int, ...] DEFAULT: None

param

name of feature Parameter

TYPE: str DEFAULT: 'x'

feature_range

min and max value of the feature, as floats in a Tuple

TYPE: tuple[float, float] DEFAULT: None

Source code in qadence/constructors/feature_maps.py
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.

    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)

    support = tuple(range(n_qubits)) if support is None else support
    hlayer = kron(H(qubit) for qubit in support)
    rlayer = feature_map(
        n_qubits,
        support=support,
        param=param,
        op=RZ,
        fm_type=BasisSet.FOURIER,
        reupload_scaling=ReuploadScaling.EXP,
        feature_range=feature_range,
        target_range=(0.0, 2 * PI),
    )
    rlayer.tag = None
    return tag(chain(hlayer, rlayer), f"ExpFourierFM({param})")

feature_map(n_qubits, support=None, param='phi', op=RX, fm_type=BasisSet.FOURIER, reupload_scaling=ReuploadScaling.CONSTANT, feature_range=None, target_range=None, multiplier=None, param_prefix=None)

Construct a feature map of a given type.

PARAMETER DESCRIPTION
n_qubits

Number of qubits the feature map covers. Results in support=range(n_qubits).

TYPE: int

support

Puts one feature-encoding rotation gate on every qubit in support. n_qubits in this case specifies the total overall qubits of the circuit, which may be wider than the support itself, but not narrower.

TYPE: tuple[int, ...] | None DEFAULT: None

param

Parameter of the feature map; you can pass a string or Parameter; it will be set as non-trainable (FeatureParameter) regardless.

TYPE: Parameter | str DEFAULT: 'phi'

op

Rotation operation of the feature map; choose from RX, RY, RZ or PHASE.

TYPE: RotationTypes DEFAULT: RX

fm_type

Basis set for data encoding; choose from BasisSet.FOURIER for Fourier encoding, or BasisSet.CHEBYSHEV for Chebyshev polynomials of the first kind.

TYPE: BasisSet | Callable | str DEFAULT: FOURIER

reupload_scaling

how the feature map scales the data that is re-uploaded for each qubit. choose from ReuploadScaling enumeration or provide your own function with a single int as input and int or float as output.

TYPE: ReuploadScaling | Callable | str DEFAULT: CONSTANT

feature_range

range of data that the input data provided comes from. Used to map input data to the correct domain of the feature-encoding function.

TYPE: tuple[float, float] | None DEFAULT: None

target_range

range of data the data encoder assumes as the natural range. For example, in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*PI). Used to map data to the correct domain of the feature-encoding function.

TYPE: tuple[float, float] | None DEFAULT: None

multiplier

overall multiplier; this is useful for reuploading the feature map serially with different scalings; can be a number or parameter/expression.

TYPE: Parameter | TParameter | None DEFAULT: None

param_prefix

string prefix to create trainable parameters multiplying the feature parameter inside the feature-encoding function. Note that currently this does not take into account the domain of the feature-encoding function.

TYPE: str | None DEFAULT: None

Example:

from qadence import feature_map, BasisSet, ReuploadScaling

fm = feature_map(3, fm_type=BasisSet.FOURIER)
print(f"{fm = }")

fm = feature_map(3, fm_type=BasisSet.CHEBYSHEV)
print(f"{fm = }")

fm = feature_map(3, fm_type=BasisSet.FOURIER, reupload_scaling = ReuploadScaling.TOWER)
print(f"{fm = }")
fm = KronBlock(0,1,2) [tag: Constant Fourier FM]
├── RX(0) [params: ['phi']]
├── RX(1) [params: ['phi']]
└── RX(2) [params: ['phi']]
fm = KronBlock(0,1,2) [tag: Constant Chebyshev FM]
├── RX(0) [params: ['acos(phi)']]
├── RX(1) [params: ['acos(phi)']]
└── RX(2) [params: ['acos(phi)']]
fm = KronBlock(0,1,2) [tag: Tower Fourier FM]
├── RX(0) [params: ['1_0*phi']]
├── RX(1) [params: ['2_0*phi']]
└── RX(2) [params: ['3_0*phi']]

Source code in qadence/constructors/feature_maps.py
def feature_map(
    n_qubits: int,
    support: tuple[int, ...] | None = None,
    param: Parameter | str = "phi",
    op: RotationTypes = RX,
    fm_type: BasisSet | Callable | str = BasisSet.FOURIER,
    reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT,
    feature_range: tuple[float, float] | None = None,
    target_range: tuple[float, float] | None = None,
    multiplier: Parameter | TParameter | None = None,
    param_prefix: str | None = None,
) -> 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: Puts one feature-encoding rotation gate on every qubit in `support`. n_qubits in
            this case specifies the total overall qubits of the circuit, which may be wider than the
            support itself, but not narrower.
        param: Parameter of the feature map; you can pass a string or Parameter;
            it will be set as non-trainable (FeatureParameter) regardless.
        op: Rotation operation of the feature map; choose from RX, RY, RZ or PHASE.
        fm_type: Basis set for data encoding; choose from `BasisSet.FOURIER` for Fourier
            encoding, or `BasisSet.CHEBYSHEV` for Chebyshev polynomials of the first kind.
        reupload_scaling: how the feature map scales the data that is re-uploaded for each qubit.
            choose from `ReuploadScaling` enumeration or provide your own function with a single
            int as input and int or float as output.
        feature_range: range of data that the input data provided comes from. Used to map input data
            to the correct domain of the feature-encoding function.
        target_range: range of data the data encoder assumes as the natural range. For example,
            in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*PI).
            Used to map data to the correct domain of the feature-encoding function.
        multiplier: overall multiplier; this is useful for reuploading the feature map serially with
            different scalings; can be a number or parameter/expression.
        param_prefix: string prefix to create trainable parameters multiplying the feature parameter
            inside the feature-encoding function. Note that currently this does not take into
            account the domain of the feature-encoding function.

    Example:
    ```python exec="on" source="material-block" result="json"
    from qadence import feature_map, BasisSet, ReuploadScaling

    fm = feature_map(3, fm_type=BasisSet.FOURIER)
    print(f"{fm = }")

    fm = feature_map(3, fm_type=BasisSet.CHEBYSHEV)
    print(f"{fm = }")

    fm = feature_map(3, fm_type=BasisSet.FOURIER, reupload_scaling = ReuploadScaling.TOWER)
    print(f"{fm = }")
    ```
    """

    # Process input
    if support is None:
        support = tuple(range(n_qubits))
    elif len(support) != n_qubits:
        raise ValueError("Wrong qubit support supplied")

    if op not in ROTATIONS:
        raise ValueError(
            f"Operation {op} not supported. "
            f"Please provide one from {[rot.__name__ for rot in ROTATIONS]}."
        )

    # Backwards compatibility
    fm_type, reupload_scaling = backwards_compatibility(fm_type, reupload_scaling)

    scaled_fparam = fm_parameter_scaling(
        fm_type, param, feature_range=feature_range, target_range=target_range
    )

    transform_func = fm_parameter_func(fm_type)

    basis_tag = fm_type.value if isinstance(fm_type, BasisSet) else str(fm_type)
    rs_func, rs_tag = fm_reupload_scaling_fn(reupload_scaling)

    # Set overall multiplier
    multiplier = 1 if multiplier is None else Parameter(multiplier)

    # Build feature map
    op_list = []
    fparam = scaled_fparam
    for i, qubit in enumerate(support):
        if param_prefix is not None:
            train_param = VariationalParameter(param_prefix + f"_{i}")
            fparam = train_param * scaled_fparam
        op_list.append(op(qubit, multiplier * rs_func(i) * transform_func(fparam)))
    fm = kron(*op_list)

    fm.tag = rs_tag + " " + basis_tag + " FM"

    return fm

fourier_feature_map(n_qubits, support=None, param='phi', op=RX)

Construct a Fourier feature map.

PARAMETER DESCRIPTION
n_qubits

number of qubits across which the FM is created

TYPE: int

param

The base name for the feature Parameter

TYPE: str DEFAULT: 'phi'

Source code in qadence/constructors/feature_maps.py
def fourier_feature_map(
    n_qubits: int, support: tuple[int, ...] = None, param: str = "phi", op: RotationTypes = 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`
    """
    warnings.warn(
        "Function 'fourier_feature_map' is deprecated. Please use 'feature_map' directly.",
        FutureWarning,
    )
    fm = feature_map(n_qubits, support=support, param=param, op=op, fm_type=BasisSet.FOURIER)
    return fm

tower_feature_map(n_qubits, support=None, param='phi', op=RX)

Construct a Chebyshev tower feature map.

PARAMETER DESCRIPTION
n_qubits

number of qubits across which the FM is created

TYPE: int

param

The base name for the feature Parameter

TYPE: str DEFAULT: 'phi'

Source code in qadence/constructors/feature_maps.py
def tower_feature_map(
    n_qubits: int, support: tuple[int, ...] = None, param: str = "phi", op: RotationTypes = 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`
    """
    warnings.warn(
        "Function 'tower_feature_map' is deprecated. Please use feature_map directly.",
        FutureWarning,
    )
    fm = feature_map(
        n_qubits,
        support=support,
        param=param,
        op=op,
        fm_type=BasisSet.CHEBYSHEV,
        reupload_scaling=ReuploadScaling.TOWER,
    )
    return fm

build_qnn(n_qubits, n_features, depth=None, ansatz=None, fm_pauli=RY, spectrum='simple', basis='fourier', fm_strategy='parallel')

Helper function to build a qadence QNN quantum circuit.

PARAMETER DESCRIPTION
n_qubits

The number of qubits.

TYPE: int

n_features

The number of input dimensions.

TYPE: int

depth

The depth of the ansatz.

TYPE: int DEFAULT: None

ansatz

An optional argument to pass a custom qadence ansatz.

TYPE: Optional[AbstractBlock] DEFAULT: None

fm_pauli

The type of Pauli gate for the feature map. Must be one of 'RX', 'RY', or 'RZ'.

TYPE: str DEFAULT: RY

spectrum

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.

TYPE: str DEFAULT: 'simple'

basis

The encoding function. The options fourier and chebyshev correspond to Φ(x)=x and arcos(x) respectively.

TYPE: str DEFAULT: 'fourier'

fm_strategy

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.

TYPE: str DEFAULT: 'parallel'

RETURNS DESCRIPTION
list[AbstractBlock]

A list of Abstract blocks to be used for constructing a quantum circuit

Source code in qadence/constructors/ansatze.py
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
    """

    warnings.warn("Function build_qnn is deprecated and will be removed in v1.5.0.", FutureWarning)

    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]

hea(n_qubits, depth=1, param_prefix='theta', support=None, strategy=Strategy.DIGITAL, **strategy_args)

Factory function for the Hardware Efficient Ansatz (HEA).

PARAMETER DESCRIPTION
n_qubits

number of qubits in the block

TYPE: int

depth

number of layers of the HEA

TYPE: int DEFAULT: 1

param_prefix

the base name of the variational parameters

TYPE: str DEFAULT: 'theta'

support

qubit indexes where the HEA is applied

TYPE: tuple[int, ...] DEFAULT: None

strategy

Strategy.Digital or Strategy.DigitalAnalog

TYPE: Strategy DEFAULT: DIGITAL

**strategy_args

see below

TYPE: Any DEFAULT: {}

PARAMETER DESCRIPTION
operations

list of operations to cycle through in the digital single-qubit rotations of each layer. Valid for Digital and DigitalAnalog HEA.

TYPE: list

periodic

if the qubits should be linked periodically. periodic=False is not supported in emu-c. Valid for only for Digital HEA.

TYPE: bool

entangler
  • 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.

TYPE: AbstractBlock

Examples:

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]
)

Source code in qadence/constructors/ansatze.py
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

hea_digital(n_qubits, depth=1, param_prefix='theta', periodic=False, operations=[RX, RY, RX], support=None, entangler=CNOT)

Construct the Digital Hardware Efficient Ansatz (HEA).

PARAMETER DESCRIPTION
n_qubits

number of qubits in the block.

TYPE: int

depth

number of layers of the HEA.

TYPE: int DEFAULT: 1

param_prefix

the base name of the variational parameters

TYPE: str DEFAULT: 'theta'

periodic

if the qubits should be linked periodically. periodic=False is not supported in emu-c.

TYPE: bool DEFAULT: False

operations

list of operations to cycle through in the digital single-qubit rotations of each layer.

TYPE: list DEFAULT: [RX, RY, RX]

support

qubit indexes where the HEA is applied.

TYPE: tuple DEFAULT: None

entangler

2-qubit entangling operation. Supports CNOT, CZ, CRX, CRY, CRZ. Controlld rotations will have variational parameters on the rotation angles.

TYPE: AbstractBlock DEFAULT: CNOT

Source code in qadence/constructors/ansatze.py
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")

hea_sDAQC(n_qubits, depth=1, param_prefix='theta', operations=[RX, RY, RX], support=None, entangler=None)

Construct the Hardware Efficient Ansatz (HEA) with analog entangling layers.

It uses step-wise digital-analog computation.

PARAMETER DESCRIPTION
n_qubits

number of qubits in the block.

TYPE: int

depth

number of layers of the HEA.

TYPE: int DEFAULT: 1

param_prefix

the base name of the variational parameters

TYPE: str DEFAULT: 'theta'

operations

list of operations to cycle through in the digital single-qubit rotations of each layer.

TYPE: list DEFAULT: [RX, RY, RX]

support

qubit indexes where the HEA is applied.

TYPE: tuple DEFAULT: None

entangler

Hamiltonian generator for the analog entangling layer. Defaults to global ZZ Hamiltonian. Time parameter is considered variational.

TYPE: AbstractBlock DEFAULT: None

Source code in qadence/constructors/ansatze.py
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.

    It uses 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")

hamiltonian_factory(register, interaction=None, detuning=None, interaction_strength=None, detuning_strength=None, random_strength=False, use_all_node_pairs=False)

General Hamiltonian creation function.

Can be used to create Hamiltonians with 2-qubit interactions and single-qubit detunings, both with arbitrary strength or parameterized.

PARAMETER DESCRIPTION
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.

TYPE: Register | int

interaction

Interaction.ZZ, Interaction.NN, Interaction.XY, or Interacton.XYZ.

TYPE: Interaction | Callable | None DEFAULT: None

detuning

single-qubit operator N, X, Y, or Z.

TYPE: TDetuning | None DEFAULT: None

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".

TYPE: TArray | str | None DEFAULT: None

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".

TYPE: TArray | str | None DEFAULT: None

random_strength

set random interaction and detuning strengths between -1 and 1.

TYPE: bool DEFAULT: False

use_all_node_pairs

computes an interaction term for every pair of nodes in the graph, independent of the edge topology in the register. Useful for defining Hamiltonians where the interaction strength decays with the distance.

TYPE: bool DEFAULT: False

Examples:

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"
    )

Source code in qadence/constructors/hamiltonians.py
def hamiltonian_factory(
    register: Register | int,
    interaction: Interaction | Callable | None = None,
    detuning: TDetuning | None = None,
    interaction_strength: TArray | str | None = None,
    detuning_strength: TArray | str | None = None,
    random_strength: bool = False,
    use_all_node_pairs: 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.
        use_all_node_pairs: computes an interaction term for every pair of nodes in the graph,
            independent of the edge topology in the register. Useful for defining Hamiltonians
            where the interaction strength decays with the distance.

    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
    if interaction is not None:
        if callable(interaction):
            int_fn = interaction
            try:
                if not block_is_qubit_hamiltonian(interaction(0, 1)):
                    raise ValueError("Custom interactions must be composed of Pauli operators.")
            except TypeError:
                raise TypeError(
                    "Please use a custom interaction function signed with two integer parameters."
                )
        else:
            int_fn = INTERACTION_DICT.get(interaction, None)  # type: ignore [arg-type]
            if int_fn is None:
                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
    detuning_strength_array = _preprocess_strengths(
        register, detuning_strength, "nodes", random_strength
    )

    edge_str = "all_node_pairs" if use_all_node_pairs else "edges"
    interaction_strength_array = _preprocess_strengths(
        register, interaction_strength, edge_str, random_strength
    )

    # Create single-qubit detunings:
    single_qubit_terms: List[AbstractBlock] = []
    if detuning is not None:
        for strength, node in zip(detuning_strength_array, register.nodes):
            single_qubit_terms.append(strength * detuning(node))

    # Create two-qubit interactions:
    two_qubit_terms: List[AbstractBlock] = []
    edge_data = register.all_node_pairs if use_all_node_pairs else register.edges
    if interaction is not None and int_fn is not None:
        for strength, edge in zip(interaction_strength_array, edge_data):
            two_qubit_terms.append(strength * int_fn(*edge))

    return add(*single_qubit_terms, *two_qubit_terms)

interaction_nn(i, j)

Ising NN interaction.

Source code in qadence/constructors/hamiltonians.py
def interaction_nn(i: int, j: int) -> AbstractBlock:
    """Ising NN interaction."""
    return N(i) @ N(j)

interaction_xy(i, j)

XY interaction.

Source code in qadence/constructors/hamiltonians.py
def interaction_xy(i: int, j: int) -> AbstractBlock:
    """XY interaction."""
    return X(i) @ X(j) + Y(i) @ Y(j)

interaction_xyz(i, j)

Heisenberg XYZ interaction.

Source code in qadence/constructors/hamiltonians.py
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_zz(i, j)

Ising ZZ interaction.

Source code in qadence/constructors/hamiltonians.py
def interaction_zz(i: int, j: int) -> AbstractBlock:
    """Ising ZZ interaction."""
    return Z(i) @ Z(j)

qft(n_qubits, support=None, inverse=False, reverse_in=False, swaps_out=False, strategy=Strategy.DIGITAL, gen_build=None)

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.

PARAMETER DESCRIPTION
n_qubits

number of qubits in the QFT

TYPE: int

support

qubit support to use

TYPE: tuple[int, ...] DEFAULT: None

inverse

True performs the inverse QFT

TYPE: bool DEFAULT: False

reverse_in

Reverses the input qubits to account for endianness

TYPE: bool DEFAULT: False

swaps_out

Performs swaps on the output qubits to match the "textbook" QFT.

TYPE: bool DEFAULT: False

strategy

Strategy.Digital or Strategy.sDAQC

TYPE: Strategy DEFAULT: DIGITAL

gen_build

building block Ising Hamiltonian for the DAQC transform. Defaults to constant all-to-all Ising.

TYPE: AbstractBlock | None DEFAULT: None

Examples:

from qadence import qft

n_qubits = 3

qft_circuit = qft(n_qubits, strategy = "sDAQC")

Source code in qadence/constructors/qft.py
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")

Hardware efficient ansatz for Rydberg atom arrays

rydberg_hea(register, n_layers=1, addressable_detuning=True, addressable_drive=False, tunable_phase=False, additional_prefix=None)

Hardware efficient ansatz for neutral atom (Rydberg) platforms.

This constructor implements a variational ansatz which is very close to what is implementable on 2nd generation PASQAL quantum devices. In particular, it implements evolution over a specific Hamiltonian which can be realized on the device. This Hamiltonian contains:

  • an interaction term given by the standard NN interaction and determined starting from the positions in the input register: Hᵢₙₜ = ∑ᵢⱼ C₆/rᵢⱼ⁶ nᵢnⱼ

  • a detuning term which corresponding to a n_i = (1+sigma_i^z)/2 applied to all the qubits. If the addressable_detuning flag is set to True, the routine effectively a local n_i = (1+sigma_i^z)/2 term in the evolved Hamiltonian with a different coefficient for each atom. These coefficients determine a local addressing pattern for the detuning on a subset of the qubits. In this routine, the coefficients are variational parameters and they will therefore be optimized at each optimizer step

  • a drive term which corresponding to a sigma^x evolution operation applied to all the qubits. If the addressable_drive flag is set to True, the routine effectively a local sigma_i^x term in the evolved Hamiltonian with a different coefficient for each atom. These coefficients determine a local addressing pattern for the drive on a subset of the qubits. In this routine, the coefficients are variational parameters and they will therefore be optimized at each optimizer step

  • if the tunable_phase flag is set to True, the drive term is modified in the following way: drive = cos(phi) * sigma^x - sin(phi) * sigma^y The addressable pattern above is maintained and the phase is considered just as an additional variational parameter which is optimized with the rest

Notice that, on real devices, the coefficients assigned to each qubit in both the detuning and drive patterns should be non-negative and they should always sum to 1. This is not the case for the implementation in this routine since the coefficients (weights) do not have any constraint. Therefore, this HEA is not completely realizable on neutral atom devices.

PARAMETER DESCRIPTION
register

the input atomic register with Cartesian coordinates.

TYPE: Register

n_layers

number layers in the HEA, each layer includes a drive, detuning and pure interaction pulses whose is a variational parameter

TYPE: int DEFAULT: 1

addressable_detuning

whether to turn on the trainable semi-local addressing pattern on the detuning (n_i terms in the Hamiltonian)

TYPE: bool DEFAULT: True

addressable_drive

whether to turn on the trainable semi-local addressing pattern on the drive (sigma_i^x terms in the Hamiltonian)

TYPE: bool DEFAULT: False

tunable_phase

whether to have a tunable phase to get both sigma^x and sigma^y rotations in the drive term. If False, only a sigma^x term will be included in the drive part of the Hamiltonian generator

TYPE: bool DEFAULT: False

additional_prefix

an additional prefix to attach to the parameter names

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
ChainBlock

The Rydberg HEA block

Source code in qadence/constructors/rydberg_hea.py
def rydberg_hea(
    register: qd.Register,
    n_layers: int = 1,
    addressable_detuning: bool = True,
    addressable_drive: bool = False,
    tunable_phase: bool = False,
    additional_prefix: str = None,
) -> qd.blocks.ChainBlock:
    """Hardware efficient ansatz for neutral atom (Rydberg) platforms.

    This constructor implements a variational ansatz which is very close to
    what is implementable on 2nd generation PASQAL quantum devices. In particular,
    it implements evolution over a specific Hamiltonian which can be realized on
    the device. This Hamiltonian contains:

    * an interaction term given by the standard NN interaction and determined starting
        from the positions in the input register: Hᵢₙₜ = ∑ᵢⱼ C₆/rᵢⱼ⁶ nᵢnⱼ

    * a detuning term which corresponding to a n_i = (1+sigma_i^z)/2 applied to
        all the qubits. If the `addressable_detuning` flag is set to True, the routine
        effectively a local n_i = (1+sigma_i^z)/2 term in the
        evolved Hamiltonian with a different coefficient for each atom. These
        coefficients determine a local addressing pattern for the detuning on a subset
        of the qubits. In this routine, the coefficients are variational parameters
        and they will therefore be optimized at each optimizer step

    * a drive term which corresponding to a sigma^x evolution operation applied to
        all the qubits. If the `addressable_drive` flag is set to True, the routine
        effectively a local sigma_i^x term in the evolved Hamiltonian with a different
        coefficient for each atom. These coefficients determine a local addressing pattern
        for the drive on a subset of the qubits. In this routine, the coefficients are
        variational parameters and they will therefore be optimized at each optimizer step

    * if the `tunable_phase` flag is set to True, the drive term is modified in the following
        way: drive = cos(phi) * sigma^x - sin(phi) * sigma^y
        The addressable pattern above is maintained and the phase is considered just as an
        additional variational parameter which is optimized with the rest

    Notice that, on real devices, the coefficients assigned to each qubit in both the detuning
    and drive patterns should be non-negative and they should always sum to 1. This is not the
    case for the implementation in this routine since the coefficients (weights) do not have any
    constraint. Therefore, this HEA is not completely realizable on neutral atom devices.

    Args:
        register: the input atomic register with Cartesian coordinates.
        n_layers: number layers in the HEA, each layer includes a drive, detuning and
            pure interaction pulses whose is a variational parameter
        addressable_detuning: whether to turn on the trainable semi-local addressing pattern
            on the detuning (n_i terms in the Hamiltonian)
        addressable_drive: whether to turn on the trainable semi-local addressing pattern
            on the drive (sigma_i^x terms in the Hamiltonian)
        tunable_phase: whether to have a tunable phase to get both sigma^x and sigma^y rotations
            in the drive term. If False, only a sigma^x term will be included in the drive part
            of the Hamiltonian generator
        additional_prefix: an additional prefix to attach to the parameter names

    Returns:
        The Rydberg HEA block
    """
    n_qubits = register.n_qubits
    prefix = "" if additional_prefix is None else "_" + additional_prefix

    detunings = None
    # add a detuning pattern locally addressing the atoms
    if addressable_detuning:
        detunings = [qd.VariationalParameter(f"detmap_{j}") for j in range(n_qubits)]

    drives = None
    # add a drive pattern locally addressing the atoms
    if addressable_drive:
        drives = [qd.VariationalParameter(f"drivemap_{j}") for j in range(n_qubits)]

    phase = None
    if tunable_phase:
        phase = qd.VariationalParameter("phase")

    return chain(
        rydberg_hea_layer(
            register,
            VariationalParameter(f"At{prefix}_{layer}"),
            VariationalParameter(f"Omega{prefix}_{layer}"),
            VariationalParameter(f"wait{prefix}_{layer}"),
            detunings=detunings,
            drives=drives,
            phase=phase,
        )
        for layer in range(n_layers)
    )

rydberg_hea_layer(register, tevo_drive, tevo_det, tevo_wait, phase=None, detunings=None, drives=None, drive_scaling=1.0)

A single layer of the Rydberg hardware efficient ansatz.

PARAMETER DESCRIPTION
register

the input register with atomic coordinates needed to build the interaction.

TYPE: Register

tevo_drive

a variational parameter for the duration of the drive term of the Hamiltonian generator, including optional semi-local addressing

TYPE: Parameter | float

tevo_det

a variational parameter for the duration of the detuning term of the Hamiltonian generator, including optional semi-local addressing

TYPE: Parameter | float

tevo_wait

a variational parameter for the duration of the waiting time with interaction only

TYPE: Parameter | float

phase

a variational parameter representing the global phase. If None, the global phase is set to 0 which results in a drive term in sigma^x only. Otherwise both sigma^x and sigma^y terms will be present

TYPE: Parameter | float | None DEFAULT: None

detunings

a list of parameters with the weights of the locally addressed detuning terms. These are variational parameters which are tuned by the optimizer

TYPE: list[Parameter] | list[float] | None DEFAULT: None

drives

a list of parameters with the weights of the locally addressed drive terms. These are variational parameters which are tuned by the optimizer

TYPE: list[Parameter] | list[float] | None DEFAULT: None

drive_scaling

a scaling term to be added to the drive Hamiltonian generator

TYPE: float DEFAULT: 1.0

RETURNS DESCRIPTION
ChainBlock

A block with a single layer of Rydberg HEA

Source code in qadence/constructors/rydberg_hea.py
def rydberg_hea_layer(
    register: qd.Register,
    tevo_drive: Parameter | float,
    tevo_det: Parameter | float,
    tevo_wait: Parameter | float,
    phase: Parameter | float | None = None,
    detunings: list[Parameter] | list[float] | None = None,
    drives: list[Parameter] | list[float] | None = None,
    drive_scaling: float = 1.0,
) -> ChainBlock:
    """A single layer of the Rydberg hardware efficient ansatz.

    Args:
        register: the input register with atomic coordinates needed to build the interaction.
        tevo_drive: a variational parameter for the duration of the drive term of
            the Hamiltonian generator, including optional semi-local addressing
        tevo_det: a variational parameter for the duration of the detuning term of the
            Hamiltonian generator, including optional semi-local addressing
        tevo_wait: a variational parameter for the duration of the waiting
            time with interaction only
        phase: a variational parameter representing the global phase. If None, the
            global phase is set to 0 which results in a drive term in sigma^x only. Otherwise
            both sigma^x and sigma^y terms will be present
        detunings: a list of parameters with the weights of the locally addressed
            detuning terms. These are variational parameters which are tuned by the optimizer
        drives: a list of parameters with the weights of the locally addressed
            drive terms. These are variational parameters which are tuned by the optimizer
        drive_scaling: a scaling term to be added to the drive Hamiltonian generator

    Returns:
        A block with a single layer of Rydberg HEA
    """
    n_qubits = register.n_qubits

    drive_x = _amplitude_map(n_qubits, qd.X, weights=drives)
    drive_y = _amplitude_map(n_qubits, qd.Y, weights=drives)
    detuning = _amplitude_map(n_qubits, qd.N, weights=detunings)
    interaction = hamiltonian_factory(register, qd.Interaction.NN)

    # drive and interaction are not commuting thus they need to be
    # added directly into the final Hamiltonian generator
    if phase is not None:
        generator = (
            drive_scaling * sympy.cos(phase) * drive_x
            - drive_scaling * sympy.sin(phase) * drive_y
            + interaction
        )
    else:
        generator = drive_scaling * drive_x + interaction

    return chain(
        qd.HamEvo(generator, tevo_drive),
        # detuning and interaction are commuting, so they
        # can be ordered arbitrarily and treated separately
        qd.HamEvo(interaction, tevo_wait),
        qd.HamEvo(detuning, tevo_det),
    )

The DAQC Transform

daqc_transform(n_qubits, gen_target, t_f, gen_build=None, zero_tol=1e-08, strategy=Strategy.SDAQC, ignore_global_phases=False)

Implements the DAQC transform for representing an arbitrary 2-body Hamiltonian.

The result is 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.

PARAMETER DESCRIPTION
n_qubits

total number of qubits to use.

TYPE: int

gen_target

target generator built with the structure above. The type of the generator will be automatically evaluated when parsing.

TYPE: AbstractBlock

t_f

total time for the gen_target evolution.

TYPE: float

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.

TYPE: AbstractBlock | None DEFAULT: None

zero_tol

default "zero" for a missing interaction. Included for numerical reasons, see notes below.

TYPE: float DEFAULT: 1e-08

strategy

sDAQC or bDAQC, following definitions in the reference paper.

TYPE: Strategy DEFAULT: SDAQC

ignore_global_phases

if True the transform does not correct the global phases coming from the mapping between ZZ and NN interactions.

TYPE: bool DEFAULT: False

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:

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,
)

Source code in qadence/constructors/daqc/daqc.py
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.

    The result is 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

Some utility functions

build_idx_fms(basis, fm_pauli, fm_strategy, n_features, n_qubits, spectrum)

Builds the index feature maps based on the given parameters.

PARAMETER DESCRIPTION
basis

Type of basis chosen for the feature map.

TYPE: str

fm_pauli

The chosen Pauli rotation type.

TYPE: PrimitiveBlock type

fm_strategy

The feature map strategy to be used. Possible values are 'parallel' or 'serial'.

TYPE: str

n_features

The number of features.

TYPE: int

n_qubits

The number of qubits.

TYPE: int

spectrum

The chosen spectrum.

TYPE: str

RETURNS DESCRIPTION
list[KronBlock]

List[KronBlock]: The list of index feature maps.

Source code in qadence/constructors/utils.py
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

generator_prefactor(spectrum, qubit_index)

Converts a spectrum string, e.g. tower or exponential.

The result is the correct generator prefactor.

Source code in qadence/constructors/utils.py
def generator_prefactor(spectrum: str, qubit_index: int) -> float | int:
    """Converts a spectrum string, e.g. tower or exponential.

    The result is the correct generator prefactor.
    """
    spectrum = spectrum.lower()
    conversion_dict: dict[str, float | int] = {
        "simple": 1,
        "tower": qubit_index + 1,
        "exponential": 2 * PI / (2 ** (qubit_index + 1)),
    }
    return conversion_dict[spectrum]

get_fm_qubits(fm_strategy, i, n_qubits, n_features)

Returns the list of target qubits for the given feature map strategy and feature index.

PARAMETER DESCRIPTION
fm_strategy

The feature map strategy to be used. Possible values are 'parallel' or 'serial'.

TYPE: str

i

The feature index.

TYPE: int

n_qubits

The number of qubits.

TYPE: int

n_features

The number of features.

TYPE: int

RETURNS DESCRIPTION
Iterable

List[int]: The list of target qubits.

RAISES DESCRIPTION
ValueError

If the feature map strategy is not implemented.

Source code in qadence/constructors/utils.py
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