Skip to content

Operations

Operations are common PrimitiveBlocks, these are often called gates elsewhere.

Constant blocks

X(target, noise=None)

Bases: PrimitiveBlock

The X gate.

Source code in qadence/operations/primitive.py
def __init__(self, target: int, noise: NoiseHandler | None = None) -> None:
    super().__init__((target,), noise=noise)

Y(target, noise=None)

Bases: PrimitiveBlock

The Y gate.

Source code in qadence/operations/primitive.py
def __init__(self, target: int, noise: NoiseHandler | None = None) -> None:
    super().__init__((target,), noise=noise)

Z(target, noise=None)

Bases: PrimitiveBlock

The Z gate.

Source code in qadence/operations/primitive.py
def __init__(self, target: int, noise: NoiseHandler | None = None) -> None:
    super().__init__((target,), noise=noise)

I(target, noise=None)

Bases: PrimitiveBlock

The identity gate.

Source code in qadence/operations/primitive.py
def __init__(self, target: int, noise: NoiseHandler | None = None) -> None:
    super().__init__((target,), noise=noise)

H(target, noise=None)

Bases: PrimitiveBlock

The Hadamard or H gate.

Source code in qadence/operations/primitive.py
def __init__(
    self,
    target: int,
    noise: NoiseHandler | None = None,
) -> None:
    self.generator = (1 / np.sqrt(2)) * (X(target) + Z(target) - np.sqrt(2) * I(target))
    super().__init__((target,), noise=noise)

S(target, noise=None)

Bases: PrimitiveBlock

The S / Phase gate.

Source code in qadence/operations/primitive.py
def __init__(
    self,
    target: int,
    noise: NoiseHandler | None = None,
) -> None:
    self.generator = I(target) - Z(target)
    super().__init__((target,), noise=noise)

SDagger(target, noise=None)

Bases: PrimitiveBlock

The Hermitian adjoint/conjugate transpose of the S / Phase gate.

Source code in qadence/operations/primitive.py
def __init__(
    self,
    target: int,
    noise: NoiseHandler | None = None,
) -> None:
    self.generator = I(target) - Z(target)
    super().__init__((target,), noise=noise)

SWAP(control, target, noise=None)

Bases: PrimitiveBlock

The SWAP gate.

Source code in qadence/operations/primitive.py
def __init__(
    self,
    control: int,
    target: int,
    noise: NoiseHandler | None = None,
) -> None:
    a11 = 0.5 * (Z(control) - I(control))
    a22 = -0.5 * (Z(target) + I(target))
    a12 = 0.5 * (chain(X(control), Z(control)) + X(control))
    a21 = 0.5 * (chain(Z(target), X(target)) + X(target))
    self.generator = (
        kron(-1.0 * a22, a11) + kron(-1.0 * a11, a22) + kron(a12, a21) + kron(a21, a12)
    )
    super().__init__((control, target), noise=noise)

T(target, noise=None)

Bases: PrimitiveBlock

The T gate.

Source code in qadence/operations/primitive.py
def __init__(
    self,
    target: int,
    noise: NoiseHandler | None = None,
) -> None:
    self.generator = I(target) - Z(target)
    super().__init__((target,), noise)

TDagger(target, noise=None)

Bases: PrimitiveBlock

The Hermitian adjoint/conjugate transpose of the T gate.

Source code in qadence/operations/primitive.py
def __init__(
    self,
    target: int,
    noise: NoiseHandler | None = None,
) -> None:
    self.generator = I(target) - Z(target)
    super().__init__((target,), noise)

CNOT(control, target, noise=None)

Bases: ControlBlock

The CNot, or CX, gate.

Source code in qadence/operations/control_ops.py
def __init__(self, control: int, target: int, noise: NoiseHandler | None = None) -> None:
    self.generator = kron(N(control), X(target) - I(target))
    super().__init__((control,), X(target), noise=noise)

CY gate not implemented

CZ(control, target, noise=None)

Bases: MCZ

The CZ gate.

Source code in qadence/operations/control_ops.py
def __init__(self, control: int, target: int, noise: NoiseHandler | None = None) -> None:
    super().__init__((control,), target, noise=noise)

CPHASE(control, target, parameter, noise=None)

Bases: MCPHASE

The CPHASE gate.

Source code in qadence/operations/control_ops.py
def __init__(
    self,
    control: int,
    target: int,
    parameter: Parameter | TNumber | sympy.Expr | str,
    noise: NoiseHandler | None = None,
):
    super().__init__((control,), target, parameter, noise=noise)

Parametrized blocks

RX(target, parameter, noise=None)

Bases: ParametricBlock

The Rx gate.

Source code in qadence/operations/parametric.py
def __init__(
    self,
    target: int,
    parameter: Parameter | TParameter | ParamMap,
    noise: NoiseHandler | None = None,
) -> None:
    # TODO: should we give them more meaningful names? like 'angle'?
    self.parameters = (
        parameter if isinstance(parameter, ParamMap) else ParamMap(parameter=parameter)
    )
    self.generator = X(target)
    super().__init__((target,), noise=noise)

RY(target, parameter, noise=None)

Bases: ParametricBlock

The Ry gate.

Source code in qadence/operations/parametric.py
def __init__(
    self,
    target: int,
    parameter: Parameter | TParameter | ParamMap,
    noise: NoiseHandler | None = None,
) -> None:
    self.parameters = (
        parameter if isinstance(parameter, ParamMap) else ParamMap(parameter=parameter)
    )
    self.generator = Y(target)
    super().__init__((target,), noise=noise)

RZ(target, parameter, noise=None)

Bases: ParametricBlock

The Rz gate.

Source code in qadence/operations/parametric.py
def __init__(
    self,
    target: int,
    parameter: Parameter | TParameter | ParamMap,
    noise: NoiseHandler | None = None,
) -> None:
    self.parameters = (
        parameter if isinstance(parameter, ParamMap) else ParamMap(parameter=parameter)
    )
    self.generator = Z(target)
    super().__init__((target,), noise=noise)

CRX(control, target, parameter, noise=None)

Bases: MCRX

The CRX gate.

Source code in qadence/operations/control_ops.py
def __init__(
    self,
    control: int,
    target: int,
    parameter: Parameter | TNumber | sympy.Expr | str,
    noise: NoiseHandler | None = None,
):
    super().__init__((control,), target, parameter, noise=noise)

CRY(control, target, parameter, noise=None)

Bases: MCRY

The CRY gate.

Source code in qadence/operations/control_ops.py
def __init__(
    self, control: int, target: int, parameter: TParameter, noise: NoiseHandler | None = None
):
    super().__init__((control,), target, parameter, noise=noise)

CRZ(control, target, parameter, noise=None)

Bases: MCRZ

The CRZ gate.

Source code in qadence/operations/control_ops.py
def __init__(
    self,
    control: int,
    target: int,
    parameter: Parameter | TNumber | sympy.Expr | str,
    noise: NoiseHandler | None = None,
):
    super().__init__((control,), target, parameter, noise=noise)

PHASE(target, parameter, noise=None)

Bases: ParametricBlock

The Parametric Phase / S gate.

Source code in qadence/operations/parametric.py
def __init__(
    self,
    target: int,
    parameter: Parameter | TNumber | sympy.Expr | str,
    noise: NoiseHandler | None = None,
) -> None:
    self.parameters = ParamMap(parameter=parameter)
    self.generator = I(target) - Z(target)
    super().__init__((target,), noise=noise)

Hamiltonian Evolution

HamEvo(generator, parameter, qubit_support=None, duration=None, noise_operators=list())

Bases: TimeEvolutionBlock

The Hamiltonian evolution operator U(t).

For time-independent Hamiltonians the solution is exact:

U(t) = exp(-iGt)

where G represents an Hermitian generator, or Hamiltonian and t represents the time parameter. For time-dependent Hamiltonians, the solution is obtained by numerical integration of the Schrodinger equation.

PARAMETER DESCRIPTION
generator

Hamiltonian generator, either symbolic as an AbstractBlock, or as a torch.Tensor or numpy.ndarray.

TYPE: Union[TGenerator, AbstractBlock]

parameter

The time parameter for evolution operator. For the time-independent case, it represents the actual value for which the evolution will be evaluated. For the time-dependent case, it should be an instance of TimeParameter to signal the solver the variable that will be integrated over.

TYPE: TParameter

qubit_support

The qubits on which the evolution will be performed on. Only required for generators that are not a composition of blocks.

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

duration

(optional) duration of the evolution in case of time-dependent generator. By default, a FeatureParameter with tag "duration" will be initialized, and the value will then be required in the values dict.

TYPE: TParameter | None DEFAULT: None

noise_operators

(optional) the list of jump operators to use when using a shrodinger solver, allowing to perform noisy simulations.

TYPE: list[AbstractBlock] DEFAULT: list()

Examples:

from qadence import X, HamEvo, PI, add, run
from qadence import FeatureParameter, TimeParameter
import torch

n_qubits = 3

# Hamiltonian as a block composition
hamiltonian = add(X(i) for i in range(n_qubits))
hevo = HamEvo(hamiltonian, parameter=torch.rand(2))
state = run(hevo)

# Hamiltonian as a random matrix
hamiltonian = torch.rand(2, 2, dtype=torch.complex128)
hevo = HamEvo(hamiltonian, parameter=torch.rand(2), qubit_support=(0,))
state = run(hevo)

# Time-dependent Hamiltonian
t = TimeParameter("t")
hamiltonian = t * add(X(i) for i in range(n_qubits))
hevo = HamEvo(hamiltonian, parameter=t)
state = run(hevo, values = {"duration": torch.tensor(1.0)})

# Adding noise operators
noise_ops = [X(0)]
hevo = HamEvo(hamiltonian, parameter=t, noise_operators=noise_ops)

Source code in qadence/operations/ham_evo.py
def __init__(
    self,
    generator: Union[TGenerator, AbstractBlock],
    parameter: TParameter,
    qubit_support: tuple[int, ...] = None,
    duration: TParameter | None = None,
    noise_operators: list[AbstractBlock] = list(),
):
    params = {}
    if qubit_support is None and not isinstance(generator, AbstractBlock):
        raise ValueError("You have to supply a qubit support for non-block generators.")
    super().__init__(qubit_support if qubit_support else generator.qubit_support)
    if isinstance(generator, AbstractBlock):
        qubit_support = generator.qubit_support
        if generator.is_parametric:
            params = {str(e): e for e in expressions(generator)}
        if generator.is_time_dependent:
            if isinstance(duration, str):
                duration = Parameter(duration, trainable=False)
            elif duration is None:
                duration = Parameter("duration", trainable=False)
        if not generator.is_time_dependent and duration is not None:
            raise TypeError(
                "Duration argument is only supported for time-dependent generators."
            )
    elif isinstance(generator, torch.Tensor):
        if duration is not None:
            raise TypeError(
                "Duration argument is only supported for time-dependent generators."
            )
        msg = "Please provide a square generator."
        if len(generator.shape) == 2:
            assert generator.shape[0] == generator.shape[1], msg
        elif len(generator.shape) == 3:
            assert generator.shape[1] == generator.shape[2], msg
            assert generator.shape[0] == 1, "Qadence doesnt support batched generators."
        else:
            raise TypeError(
                "Only 2D or 3D generators are supported.\
                            In case of a 3D generator, the batch dim\
                            is expected to be at dim 0."
            )
        params = {str(generator.__hash__()): generator}
    elif isinstance(generator, (sympy.Basic, sympy.Array)):
        if duration is not None:
            raise TypeError(
                "Duration argument is only supported for time-dependent generators."
            )
        params = {str(generator): generator}
    else:
        raise TypeError(
            f"Generator of type {type(generator)} not supported.\
                        If you're using a numpy.ndarray, please cast it to a torch tensor."
        )
    if duration is not None:
        params = {"duration": Parameter(duration), **params}
    params = {"parameter": Parameter(parameter), **params}
    self.parameters = ParamMap(**params)
    self.time_param = parameter
    self.generator = generator
    self.duration = duration

    if len(noise_operators) > 0:
        if not all(
            [
                len(set(op.qubit_support + self.qubit_support) - set(self.qubit_support)) == 0
                for op in noise_operators
            ]
        ):
            raise ValueError(
                "Noise operators should be defined"
                " over the same or a subset of the qubit support"
            )
        if True in [op.is_parametric for op in noise_operators]:
            raise ValueError("Parametric operators are not supported")
    self.noise_operators = noise_operators

digital_decomposition(approximation=LTSOrder.ST4)

Decompose the Hamiltonian evolution into digital gates.

PARAMETER DESCRIPTION
approximation

Choose the type of decomposition. Defaults to "st4". Available types are: * 'basic' = apply first-order Trotter formula and decompose each term of the exponential into digital gates. It is exact only if applied to an operator whose terms are mutually commuting. * 'st2' = Trotter-Suzuki 2nd order formula for approximating non-commuting Hamiltonians. * 'st4' = Trotter-Suzuki 4th order formula for approximating non-commuting Hamiltonians.

TYPE: str DEFAULT: ST4

RETURNS DESCRIPTION
AbstractBlock

a block with the digital decomposition

TYPE: AbstractBlock

Source code in qadence/operations/ham_evo.py
def digital_decomposition(self, approximation: LTSOrder = LTSOrder.ST4) -> AbstractBlock:
    """Decompose the Hamiltonian evolution into digital gates.

    Args:
        approximation (str, optional): Choose the type of decomposition. Defaults to "st4".
            Available types are:
            * 'basic' = apply first-order Trotter formula and decompose each term of
                the exponential into digital gates. It is exact only if applied to an
                operator whose terms are mutually commuting.
            * 'st2' = Trotter-Suzuki 2nd order formula for approximating non-commuting
                Hamiltonians.
            * 'st4' = Trotter-Suzuki 4th order formula for approximating non-commuting
                Hamiltonians.

    Returns:
        AbstractBlock: a block with the digital decomposition
    """

    # psi(t) = exp(-i * H * t * psi0)
    # psi(t) = exp(-i * lambda * t * psi0)
    # H = sum(Paulin) + sum(Pauli1*Pauli2)
    logger.info("Quantum simulation of the time-independent Schrödinger equation.")

    blocks = []

    # how to change the type/dict to enum effectively

    # when there is a term including non-commuting matrices use st2 or st4

    # 1) should check that the given generator respects the constraints
    # single-qubit gates

    assert isinstance(
        self.generator, AbstractBlock
    ), "Only a generator represented as a block can be decomposed"

    if block_is_qubit_hamiltonian(self.generator):
        try:
            block_is_commuting_hamiltonian(self.generator)
            approximation = LTSOrder.BASIC  # use the simpler approach if the H is commuting
        except TypeError:
            logger.warning(
                """Non-commuting terms in the Pauli operator.
                The Suzuki-Trotter approximation is applied."""
            )

        blocks.extend(
            lie_trotter_suzuki(
                block=self.generator,
                parameter=self.parameters.parameter,
                order=LTSOrder[approximation],
            )
        )

        # 2) return an AbstractBlock instance with the set of gates
        # resulting from the decomposition

        return chain(*blocks)
    else:
        raise NotImplementedError(
            "The current digital decomposition can be applied only to Pauli Hamiltonians."
        )

AnalogSWAP(control, target, parameter=3 * PI / 4)

Bases: HamEvo

Single time-independent Hamiltonian evolution over a Rydberg Ising.

hamiltonian yielding a SWAP (up to global phase).

Derived from Bapat et al. where it is applied to XX-type Hamiltonian

Source code in qadence/operations/analog.py
def __init__(self, control: int, target: int, parameter: TParameter = 3 * PI / 4):
    rydberg_ising_hamiltonian_generator = (
        4.0 * kron((I(control) - Z(control)) / 2.0, (I(target) - Z(target)) / 2.0)
        + (2.0 / 3.0) * np.sqrt(2.0) * X(control)
        + (2.0 / 3.0) * np.sqrt(2.0) * X(target)
        + (1.0 + np.sqrt(5.0) / 3) * Z(control)
        + (1.0 + np.sqrt(5.0) / 3) * Z(target)
    )
    super().__init__(rydberg_ising_hamiltonian_generator, parameter, (control, target))

AnalogSWAP should be turned into a proper analog block


Analog blocks

AnalogRX(angle, qubit_support='global', add_pattern=True)

Analog X rotation.

Shorthand for AnalogRot:

φ=2.4; Ω=π; t = φ/Ω * 1000
AnalogRot(duration=t, omega=Ω)
PARAMETER DESCRIPTION
angle

Rotation angle [rad]

TYPE: float | str | Parameter

qubit_support

Defines the (local/global) qubit support

TYPE: str | QubitSupport | Tuple DEFAULT: 'global'

RETURNS DESCRIPTION
ConstantAnalogRotation

ConstantAnalogRotation

Source code in qadence/operations/analog.py
def AnalogRX(
    angle: float | str | Parameter,
    qubit_support: str | QubitSupport | Tuple = "global",
    add_pattern: bool = True,
) -> ConstantAnalogRotation:
    """Analog X rotation.

    Shorthand for [`AnalogRot`][qadence.operations.AnalogRot]:

    ```python
    φ=2.4; Ω=π; t = φ/Ω * 1000
    AnalogRot(duration=t, omega=Ω)
    ```

    Arguments:
        angle: Rotation angle [rad]
        qubit_support: Defines the (local/global) qubit support

    Returns:
        ConstantAnalogRotation
    """
    return _analog_rot(angle, qubit_support, phase=0, add_pattern=add_pattern)

AnalogRY(angle, qubit_support='global', add_pattern=True)

Analog Y rotation.

Shorthand for AnalogRot:

φ=2.4; Ω=π; t = φ/Ω * 1000
AnalogRot(duration=t, omega=Ω, phase=-π/2)
Arguments: angle: Rotation angle [rad] qubit_support: Defines the (local/global) qubit support

RETURNS DESCRIPTION
ConstantAnalogRotation

ConstantAnalogRotation

Source code in qadence/operations/analog.py
def AnalogRY(
    angle: float | str | Parameter,
    qubit_support: str | QubitSupport | Tuple = "global",
    add_pattern: bool = True,
) -> ConstantAnalogRotation:
    """Analog Y rotation.

    Shorthand for [`AnalogRot`][qadence.operations.AnalogRot]:

    ```python
    φ=2.4; Ω=π; t = φ/Ω * 1000
    AnalogRot(duration=t, omega=Ω, phase=-π/2)
    ```
    Arguments:
        angle: Rotation angle [rad]
        qubit_support: Defines the (local/global) qubit support

    Returns:
        ConstantAnalogRotation
    """
    return _analog_rot(angle, qubit_support, phase=-PI / 2, add_pattern=add_pattern)

AnalogRZ(angle, qubit_support='global', add_pattern=True)

Analog Z rotation. Shorthand for AnalogRot:

φ=2.4; δ=π; t = φ/δ * 100)
AnalogRot(duration=t, delta=δ, phase=π/2)

Source code in qadence/operations/analog.py
def AnalogRZ(
    angle: float | str | Parameter,
    qubit_support: str | QubitSupport | Tuple = "global",
    add_pattern: bool = True,
) -> ConstantAnalogRotation:
    """Analog Z rotation. Shorthand for [`AnalogRot`][qadence.operations.AnalogRot]:
    ```
    φ=2.4; δ=π; t = φ/δ * 100)
    AnalogRot(duration=t, delta=δ, phase=π/2)
    ```
    """
    q = _cast(QubitSupport, qubit_support)
    alpha = _cast(Parameter, angle)
    delta = PI
    omega = 0
    duration = alpha / delta * 1000
    h_norm = sympy.sqrt(omega**2 + delta**2)
    ps = ParamMap(
        alpha=alpha, duration=duration, omega=omega, delta=delta, phase=0.0, h_norm=h_norm
    )
    return ConstantAnalogRotation(qubit_support=q, parameters=ps, add_pattern=add_pattern)

AnalogRot(duration, omega=0, delta=0, phase=0, qubit_support='global', add_pattern=True)

General analog rotation operation.

PARAMETER DESCRIPTION
duration

Duration of the rotation [ns].

TYPE: float | str | Parameter

omega

Rotation frequency [rad/μs]

TYPE: float | str | Parameter DEFAULT: 0

delta

Rotation frequency [rad/μs]

TYPE: float | str | Parameter DEFAULT: 0

phase

Phase angle [rad]

TYPE: float | str | Parameter DEFAULT: 0

qubit_support

Defines the (local/global) qubit support

TYPE: str | QubitSupport | Tuple DEFAULT: 'global'

add_pattern

False disables the semi-local addressing pattern for the execution of this specific block.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
ConstantAnalogRotation

ConstantAnalogRotation

Source code in qadence/operations/analog.py
def AnalogRot(
    duration: float | str | Parameter,
    omega: float | str | Parameter = 0,
    delta: float | str | Parameter = 0,
    phase: float | str | Parameter = 0,
    qubit_support: str | QubitSupport | Tuple = "global",
    add_pattern: bool = True,
) -> ConstantAnalogRotation:
    """General analog rotation operation.

    Arguments:
        duration: Duration of the rotation [ns].
        omega: Rotation frequency [rad/μs]
        delta: Rotation frequency [rad/μs]
        phase: Phase angle [rad]
        qubit_support: Defines the (local/global) qubit support
        add_pattern: False disables the semi-local addressing pattern
            for the execution of this specific block.

    Returns:
        ConstantAnalogRotation
    """

    if omega == 0 and delta == 0:
        raise ValueError("Parameters omega and delta cannot both be 0.")

    q = _cast(QubitSupport, qubit_support)
    duration = Parameter(duration)
    omega = Parameter(omega)
    delta = Parameter(delta)
    phase = Parameter(phase)
    h_norm = sympy.sqrt(omega**2 + delta**2)
    alpha = duration * h_norm / 1000
    ps = ParamMap(
        alpha=alpha, duration=duration, omega=omega, delta=delta, phase=phase, h_norm=h_norm
    )
    return ConstantAnalogRotation(parameters=ps, qubit_support=q, add_pattern=add_pattern)

AnalogInteraction(duration, qubit_support='global', add_pattern=True)

Evolution of the interaction term for a register of qubits.

Constructs a InteractionBlock.

PARAMETER DESCRIPTION
duration

Time to evolve the interaction for in nanoseconds.

TYPE: TNumber | Basic

qubit_support

Qubits the InteractionBlock is applied to. Can be either "global" to evolve the interaction block to all qubits or a tuple of integers.

TYPE: str | QubitSupport | tuple DEFAULT: 'global'

add_pattern

False disables the semi-local addressing pattern for the execution of this specific block.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
InteractionBlock

a InteractionBlock

Source code in qadence/operations/analog.py
def AnalogInteraction(
    duration: TNumber | sympy.Basic,
    qubit_support: str | QubitSupport | tuple = "global",
    add_pattern: bool = True,
) -> InteractionBlock:
    """Evolution of the interaction term for a register of qubits.

    Constructs a [`InteractionBlock`][qadence.blocks.analog.InteractionBlock].

    Arguments:
        duration: Time to evolve the interaction for in nanoseconds.
        qubit_support: Qubits the `InteractionBlock` is applied to. Can be either
            `"global"` to evolve the interaction block to all qubits or a tuple of integers.
        add_pattern: False disables the semi-local addressing pattern
            for the execution of this specific block.

    Returns:
        a `InteractionBlock`
    """
    q = _cast(QubitSupport, qubit_support)
    ps = ParamMap(duration=duration)
    return InteractionBlock(parameters=ps, qubit_support=q, add_pattern=add_pattern)