Skip to content

The expression module

The expression module defines an Expression type as a symbolic representation for mathematical expressions together with arithmetic rules. It is syntactically constructed using S-Expressions in prefix notation where the ordered parameters are:

  • A tag as a Python Enum: a token identifier for variables, functions or operations.
  • Arguments: a set of arguments to define or be passed to the expression.
  • Attributes: keyword arguments for symbolic evaluation and compilation directives.

The Expression type defines constructors for four identifiers and operations variants:

  • VALUE: A value type to hold numerical complex, float or int primitive types.
  • SYMBOL: A symbol type for names definition.
  • FN: A variadic function type defined as a symbolic name and arguments.
  • QUANTUM_OP: A quantum operator type defined as an expression, a support of qubit resources and a collection of property attributes.
  • ADD: A variadic addition type as the sum of arguments.
  • MUL: A variadic multiplication type as the product of arguments.
  • KRON: A variadic multiplication type as the product of arguments with commutative rules.
  • POW: A power type as the exponentiation of a base and power arguments.

Expression(head, *args, **attributes)

A symbolic representation of mathematical expressions.

Besides arithmetic operations, the expression can contain classical functions such as sine, cosine, and logarithm, and abstract representations of quantum operators.

Multiplication between quantum operators is stored as Kronecker products. Operators are ordered by qubit index, preserving the order when the qubit indices of two operators overlap. This ensures that operators acting on the same subspace are kept together, enhancing optimisation.

Source code in qadence2_expressions/core/expression.py
def __init__(self, head: Expression.Tag, *args: Any, **attributes: Any) -> None:
    self.head = head
    self.args = args
    self.attrs = attributes

dag: Expression property

Returns the conjugated/dagger version of and expression.

max_index: int cached property

Returns the maximum qubit index present in the expression. An expression without quantum operators or covering all the qubits will return -1.

Example:

>>> value(2).max_index
-1
>>> (X() * Y(1)).max_index
-1
>>> (X(0, 2) * Y(1)).max_index
2

subspace: Support | None cached property

Returns the total subspace coverage of an expression with quantum operators. If there are no quantum operators, the subspace is None. If controlled operators are present, it returns a controlled support if there is no overlap between targets and controls; otherwise, all indices are treated as targets.

Example:

>>> value(1).subspace
None
>>> (X(1) + Y(2)).subspace
[1 2]
>>> (X(target=(1,), control=(0,)) + Y(2)).subspace
[1 2|0]
>>> (X(target=(1,), control=(2,)) + Y(2)).subspace
[1 2]

Tag

Bases: Enum

This auxiliar class allows the Expression to be represented as a tagged union.

__getitem__(index)

Makes the arguments of the expression directly accessible through expression[i].

Source code in qadence2_expressions/core/expression.py
def __getitem__(self, index: int | slice) -> Any:
    """Makes the arguments of the expression directly accessible through `expression[i]`."""
    return self.args[index]

__pow__(other)

Power involving quantum operators always promote expression to quantum operators.

Source code in qadence2_expressions/core/expression.py
def __pow__(self, other: object) -> Expression:
    """Power involving quantum operators always promote expression to quantum operators."""

    if not isinstance(other, Expression | Numeric):
        return NotImplemented

    if isinstance(other, Numeric):
        return self ** Expression.value(other)

    # Numerical values are computed right away.
    if self.is_value and other.is_value:
        return Expression.value(self[0] ** other[0])

    # Null power shortcut.
    if other.is_zero:
        return Expression.one()

    # Identity power shortcut.
    if other.is_one:
        return self

    # Power of power is a simple operation and can be evaluated here.
    if (
        self.is_quantum_operator
        and self.get("is_hermitian")
        and self.get("is_unitary")
        and isinstance(other, Expression)
        and other.is_value
        and other[0] == int(other[0])
    ):
        power = int(other[0]) % 2
        return self if power == 1 else Expression.one()

    # Power of power is an simple operation and can be evaluated here.
    # Whenever a quantum operator is present, the expression is promoted to
    # a quantum operator.
    if self.is_power:
        return Expression.pow(self[0], self[1] * other).as_quantum_operator()

    return Expression.pow(self, other).as_quantum_operator()

add(*args) classmethod

In Expressions, addition is a variadic operation representing the sum of its arguments.

Expression.add(a, b, c) == a + b + c

Source code in qadence2_expressions/core/expression.py
@classmethod
def add(cls, *args: Expression) -> Expression:
    """In Expressions, addition is a variadic operation representing the sum of its arguments.

    Expression.add(a, b, c) == a + b + c
    """
    return cls(cls.Tag.ADD, *args)

as_quantum_operator()

Promotes and expression to a quantum operator.

When a function takes a quantum operator as input, the function itself must be transformed into a quantum operator to preserve commutation properties. For instance,

Example:

>>> exp(2) * exp(3)
exp(5)
>>> exp(X(1)) * exp(Y(1))
exp(X(1)) exp(Y(1))

Source code in qadence2_expressions/core/expression.py
def as_quantum_operator(self) -> Expression:
    """Promotes and expression to a quantum operator.

    When a function takes a quantum operator as input, the function itself must be transformed
    into a quantum operator to preserve commutation properties. For instance,

    Example:
    ```
    >>> exp(2) * exp(3)
    exp(5)
    >>> exp(X(1)) * exp(Y(1))
    exp(X(1)) exp(Y(1))
    ```
    """

    subspace = self.subspace
    if subspace:
        return Expression.quantum_operator(self, subspace)

    return self

function(name, *args) classmethod

Symbolic representation of a function. The name indicates the function identifier, the remaining arguments are used as the function arguments.

Expression.function("sin", 1.57) => sin(1.57)
PARAMETER DESCRIPTION
name

The function name.

TYPE: str

args

The arguments to be passed to the function.

TYPE: Any DEFAULT: ()

RETURNS DESCRIPTION
Expression

A Function(Symbol('name'), args...) expression.

Source code in qadence2_expressions/core/expression.py
@classmethod
def function(cls, name: str, *args: Any) -> Expression:
    """
    Symbolic representation of a function. The `name` indicates the function identifier, the
    remaining arguments are used as the function arguments.

        Expression.function("sin", 1.57) => sin(1.57)

    Args:
        name: The function name.
        args: The arguments to be passed to the function.

    Returns:
        A `Function(Symbol('name'), args...)` expression.
    """
    return cls(cls.Tag.FN, cls.symbol(name), *args)

get(attribute, default=None)

Retrieve the value of the chosen attribute if it exists, or return the default value if it doesn't.

Source code in qadence2_expressions/core/expression.py
def get(self, attribute: str, default: Any | None = None) -> Any:
    """Retrieve the value of the chosen `attribute` if it exists, or return the `default` value
    if it doesn't.
    """
    return self.attrs.get(attribute, default)

kron(*args) classmethod

In Expressions, the Kronecker product is a variadic operation representing the multiplication of its arguments applying commutative rules. When the qubit indices of two operators overlap, the order is preserved. Otherwise, the operators are ordered by index, with operators acting on the same qubits placed next to each other.

Expression.kron(X(1), X(2), Y(1)) == X(1)Y(1) ⊗  X(2)
Source code in qadence2_expressions/core/expression.py
@classmethod
def kron(cls, *args: Expression) -> Expression:
    """In Expressions, the Kronecker product is a variadic operation representing the
    multiplication of its arguments applying commutative rules. When the qubit indices of two
    operators overlap, the order is preserved. Otherwise, the operators are ordered by index,
    with operators acting on the same qubits placed next to each other.

        Expression.kron(X(1), X(2), Y(1)) == X(1)Y(1) ⊗  X(2)
    """

    return cls(cls.Tag.KRON, *args)

mul(*args) classmethod

In Expressions, multiplication is a variadic operation representing the product of its arguments.

Expression.mul(a, b, c) == a * b * c
Source code in qadence2_expressions/core/expression.py
@classmethod
def mul(cls, *args: Expression) -> Expression:
    """In Expressions, multiplication is a variadic operation representing the product of its
    arguments.

        Expression.mul(a, b, c) == a * b * c
    """

    return cls(cls.Tag.MUL, *args)

one() classmethod

Used to represent both, the numerical value 1 and the identity operator.

RETURNS DESCRIPTION
Expression

An Value(1) expression.

Source code in qadence2_expressions/core/expression.py
@classmethod
def one(cls) -> Expression:
    """Used to represent both, the numerical value `1` and the identity operator.

    Returns:
        An `Value(1)` expression.
    """
    return cls.value(1)

pow(base, power) classmethod

Define a power expression.

Expression.power(a, b) == a**b

Source code in qadence2_expressions/core/expression.py
@classmethod
def pow(cls, base: Expression, power: Expression) -> Expression:
    """Define a power expression.

    Expression.power(a, b) == a**b
    """

    return cls(cls.Tag.POW, base, power)

quantum_operator(expr, support, **attributes) classmethod

To turn an expression into a quantum operator, specify the support it acts on. Attributes like is_projector [bool], is_hermitian [bool], is_unitary [bool], is_dagger [bool], and join [callable] indicate how the operator behaves during evaluation.

A parametric quantum operator is a function wrapped in a quantum operator. The join attribute is used to combine the arguments of two parametric operators.

PARAMETER DESCRIPTION
expr

An expression that describes the operator. If expr is a symbol, it represents a generic gate like Pauli and Clifford gates. If expr is a function, it represents a parametric operator. Power expressions can be used to represent unitary evolution operators.

TYPE: Expression

support

The qubit indices to what the operator is applied.

TYPE: Support

Kwargs

Keyword arguments are used primarily to symbolic evaluation. Examples of keywords are: - is_projector [bool] - is_hermitian [bool] - is_unitary [bool] - is_dagger [bool] - join [callable]

The keyword, join is used with parametric operators. Whe two parametric operator of the same kind action on the same subspace are muliplied, the join is used to combine their arguments.

RETURNS DESCRIPTION
Expression

An expression of type QuantumOperator.

Source code in qadence2_expressions/core/expression.py
@classmethod
def quantum_operator(cls, expr: Expression, support: Support, **attributes: Any) -> Expression:
    """To turn an expression into a quantum operator, specify the support it acts on. Attributes
    like `is_projector` [bool], `is_hermitian` [bool], `is_unitary` [bool], `is_dagger` [bool],
    and `join` [callable] indicate how the operator behaves during evaluation.

    A parametric quantum operator is a function wrapped in a quantum operator. The `join`
    attribute is used to combine the arguments of two parametric operators.

    Args:
        expr: An expression that describes the operator. If `expr` is a symbol, it represents
            a generic gate like Pauli and Clifford gates. If `expr` is a function, it represents
            a parametric operator. Power expressions can be used to represent unitary evolution
            operators.
        support: The qubit indices to what the operator is applied.

    Kwargs:
        Keyword arguments are used primarily to symbolic evaluation. Examples of keywords are:
            - `is_projector` [bool]
            - `is_hermitian` [bool]
            - `is_unitary` [bool]
            - `is_dagger` [bool]
            - `join` [callable]

        The keyword, `join` is used with parametric operators. Whe two parametric operator of
        the same kind action on the same subspace are muliplied, the `join` is used to combine
        their arguments.

    Returns:
        An expression of type `QuantumOperator`.
    """

    return cls(cls.Tag.QUANTUM_OP, expr, support, **attributes)

symbol(identifier, **attributes) classmethod

Create a symbol from the identifier.

PARAMETER DESCRIPTION
identifier

A string used as the symbol name.

TYPE: str

Kwargs

Keyword arguments are used as flags for compilation steps. The valid flags are defined in Qadence2-IR.

RETURNS DESCRIPTION
Expression

A Symbol('identifier') expression.

Source code in qadence2_expressions/core/expression.py
@classmethod
def symbol(cls, identifier: str, **attributes: Any) -> Expression:
    """Create a symbol from the identifier.

    Args:
        identifier: A string used as the symbol name.

    Kwargs:
        Keyword arguments are used as flags for compilation steps.
        The valid flags are defined in Qadence2-IR.

    Returns:
        A `Symbol('identifier')` expression.
    """
    return cls(cls.Tag.SYMBOL, identifier, **attributes)

value(x) classmethod

Promote a numerical value (complex, float, int) to an expression.

PARAMETER DESCRIPTION
x

A numerical value.

TYPE: Numeric

RETURNS DESCRIPTION
Expression

A Value(x) expression.

Source code in qadence2_expressions/core/expression.py
@classmethod
def value(cls, x: Numeric) -> Expression:
    """Promote a numerical value (complex, float, int) to an expression.

    Args:
        x: A numerical value.

    Returns:
        A `Value(x)` expression.
    """

    return cls(cls.Tag.VALUE, float(x)) if isinstance(x, int) else cls(cls.Tag.VALUE, x)

zero() classmethod

Used to represent both, the numerical value 0 and the null operator.

RETURNS DESCRIPTION
Expression

An Value(0) expression.

Source code in qadence2_expressions/core/expression.py
@classmethod
def zero(cls) -> Expression:
    """Used to represent both, the numerical value `0` and the null operator.

    Returns:
        An `Value(0)` expression.
    """
    return cls.value(0)

evaluate_kron(expr)

Evaluate Kronecker product expressions.

Source code in qadence2_expressions/core/expression.py
def evaluate_kron(expr: Expression) -> Expression:
    """Evaluate Kronecker product expressions."""

    lhs = expr[0]
    for rhs in expr[1:]:
        # Single operators multiplication, A ⊗ B
        if lhs.is_quantum_operator and rhs.is_quantum_operator:
            lhs = evaluate_kronop(lhs, rhs)

        # Left associativity, A ⊗ (B ⊗ C ⊗ D) = (A ⊗ B ⊗ C ⊗ D)
        elif lhs.is_quantum_operator and rhs.is_kronecker_product:
            lhs = evaluate_kronleft(lhs, rhs)

        # Right associativity, (A ⊗ B ⊗ C) ⊗ D = (A ⊗ B ⊗ C ⊗ D)
        elif lhs.is_kronecker_product and rhs.is_quantum_operator:
            lhs = evaluate_kronright(lhs, rhs)

        # Combine two Kronecker products, (A ⊗ B) ⊗ (C ⊗ D) = (A ⊗ B ⊗ C ⊗ D)
        elif lhs.is_kronecker_product and rhs.is_kronecker_product:
            lhs = evaluate_kronjoin(lhs, rhs)

        else:
            raise NotImplementedError

    return lhs  # type: ignore

evaluate_kronjoin(lhs, rhs)

Evaluate the Kronecker product between a LHS=quantum operators and a RHS=Kronecker product.

Source code in qadence2_expressions/core/expression.py
def evaluate_kronjoin(lhs: Expression, rhs: Expression) -> Expression:
    """Evaluate the Kronecker product between a LHS=quantum operators and a RHS=Kronecker
    product.
    """

    if not (lhs.is_kronecker_product or rhs.is_kronecker_product):
        raise SyntaxError("Only defined for LHS and RHS both Kronecker product.")

    result = lhs
    for term in rhs.args:
        result = evaluate_kron(Expression.kron(result, term))

    return result

evaluate_kronleft(lhs, rhs)

Left associativity of the Kronecker product.

Evaluate the Kronecker product between a LHS=quantum operators and a RHS=Kronecker product.

Source code in qadence2_expressions/core/expression.py
def evaluate_kronleft(lhs: Expression, rhs: Expression) -> Expression:
    """Left associativity of the Kronecker product.

    Evaluate the Kronecker product between a LHS=quantum operators and a RHS=Kronecker product.
    """

    if not (lhs.is_quantum_operator or rhs.is_kronecker_product):
        raise SyntaxError("Only defined for a quantum operator and a Kronecker product.")

    args = rhs.args

    # Using a insertion-sort-like to add the LHS term in the the RHS product.
    for i, rhs_arg in enumerate(args):
        if rhs_arg.subspace == lhs.subspace:  # type: ignore
            ii = i + 1

            result = evaluate_kronop(lhs, rhs_arg)

            if result.is_one:
                args = (*args[:i], *args[ii:])

            elif result.is_kronecker_product:
                args = (*args[:i], *result.args, *args[ii:])

            else:
                args = (*args[:i], result, *args[ii:])

            break

        if rhs_arg.subspace > lhs.subspace or rhs_arg.subspace.overlap_with(lhs.subspace):
            args = (*args[:i], lhs, *args[i:])
            break

        if i == len(args) - 1:
            args = (lhs, *args)

    if not args:
        return Expression.one()

    return args[0] if len(args) == 1 else Expression.kron(*args)  # type: ignore

evaluate_kronop(lhs, rhs)

Evaluate the Kronecker product between two quantum operators.

Source code in qadence2_expressions/core/expression.py
def evaluate_kronop(lhs: Expression, rhs: Expression) -> Expression:
    """Evaluate the Kronecker product between two quantum operators."""

    if not (lhs.is_quantum_operator or rhs.is_quantum_operator):
        raise SyntaxError("Operation only valid for LHS and RHS both quantum operators.")

    # Multiplication of unitary Hermitian operators acting on the the same subspace.
    if lhs == rhs and (lhs.get("is_hermitian") and lhs.get("is_unitary")):
        return Expression.one()

    # General multiplications of operators acting on the same subspace.
    if lhs.subspace == rhs.subspace:
        if lhs.get("is_projector") and rhs.get("is_projector"):
            return lhs if lhs[0] == rhs[0] else Expression.zero()

        if lhs[0].is_function and rhs[0].is_function and lhs[0][0] == rhs[0][0] and lhs.get("join"):
            res = lhs.get("join")(
                lhs[0], rhs[0], lhs.get("is_dagger", False), rhs.get("is_dagger", False)
            )
            return (  # type: ignore
                res
                if res.is_zero or res.is_one
                else Expression.quantum_operator(res, lhs[1], **lhs.attrs)
            )

        # Simplify the multiplication of unitary Hermitian operators with fractional
        # power, e.g., `√X() * √X() == X()`.
        if (
            lhs[0].is_power
            and rhs[0].is_power
            and lhs[0][0] == rhs[0][0]  # both are the same operator
            and (lhs[0][0].get("is_hermitian") and lhs[0][0].get("is_unitary"))
        ):
            return lhs[0][0] ** (lhs[0][1] + rhs[0][1])  # type: ignore

    # Order the operators by subspace.
    if lhs.subspace < rhs.subspace or lhs.subspace.overlap_with(  # type: ignore
        rhs.subspace  # type: ignore
    ):
        return Expression.kron(lhs, rhs)

    return Expression.kron(rhs, lhs)

evaluate_kronright(lhs, rhs)

Right associativity of the Kronecker product.

Evaluate the Kronecker product between a LHS=quantum operators and a RHS=Kronecker product.

Source code in qadence2_expressions/core/expression.py
def evaluate_kronright(lhs: Expression, rhs: Expression) -> Expression:
    """Right associativity of the Kronecker product.

    Evaluate the Kronecker product between a LHS=quantum operators and a RHS=Kronecker product.
    """

    if not (lhs.is_kronecker_product or rhs.is_quantum_operator):
        raise SyntaxError("Only defined for a Kronecker product and a quantum operator.")

    args = lhs.args

    # Using a insertion-sort-like to add the RHS term in the the LHS product.
    for i in range(len(args) - 1, -1, -1):
        ii = i + 1

        if args[i].subspace == rhs.subspace:  # type: ignore
            result = evaluate_kronop(args[i], rhs)

            if result.is_one:
                args = (*args[:i], *args[ii:])

            elif result.is_kronecker_product:
                args = (*args[:i], *result.args, *args[ii:])

            else:
                args = (*args[:i], result, *args[ii:])

            break

        if args[i].subspace < rhs.subspace or args[i].subspace.overlap_with(rhs.subspace):
            args = (*args[:ii], rhs, *args[ii:])
            break

        if i == 0:
            args = (rhs, *args)

    if not args:
        return Expression.one()

    return args[0] if len(args) == 1 else Expression.kron(*args)  # type: ignore

visualize_expression(expr)

Stringfy expressions.

Source code in qadence2_expressions/core/expression.py
def visualize_expression(expr: Expression) -> str:
    """Stringfy expressions."""

    if expr.is_value or expr.is_symbol:
        return str(expr[0])

    if expr.is_quantum_operator:
        dag = "\u2020" if expr.get("is_dagger") else ""
        if expr[0].is_symbol or expr[0].is_function:
            return f"{expr[0]}{dag}{expr[1]}"
        return f"{expr[0]}"

    if expr.is_function:
        args = ",\u2009".join(map(str, expr[1:]))
        return f"{expr[0]}({args})"

    if expr.is_multiplication:
        result = visualize_sequence(expr, "\u2009*\u2009")
        return sub(r"-1\.0(\s\*)?\s", "-", result)

    if expr.is_kronecker_product:
        return visualize_sequence(expr, "\u2009*\u2009")

    if expr.is_addition:
        result = visualize_sequence(expr, " + ", with_brackets=False)
        return sub(r"\s\+\s-(1\.0(\s\*)?\s)?", " - ", result)

    if expr.is_power:
        return visualize_sequence(expr, "\u2009^\u2009")

    return repr(expr)

visualize_sequence(expr, operator, with_brackets=True)

Stringfy the arguments of an expression expr with the designed operator.

The with_brackets option wrap any argument that is either a multiplication or a sum.

Source code in qadence2_expressions/core/expression.py
def visualize_sequence(expr: Expression, operator: str, with_brackets: bool = True) -> str:
    """Stringfy the arguments of an expression `expr` with the designed `operator`.

    The `with_brackets` option wrap any argument that is either a multiplication or a sum.
    """

    if expr.is_value or expr.is_symbol or (expr.is_quantum_operator and expr[0].is_symbol):
        raise SyntaxError("Only a sequence of expressions is allowed.")

    if with_brackets:
        return operator.join(map(visualize_with_brackets, expr.args))

    return operator.join(map(str, expr.args))

visualize_with_brackets(expr)

Stringfy addition and multiplication expression surrounded by brackets.

Source code in qadence2_expressions/core/expression.py
def visualize_with_brackets(expr: Expression) -> str:
    """Stringfy addition and multiplication expression surrounded by brackets."""

    if expr.is_multiplication or expr.is_addition:
        return f"({str(expr)})"

    return str(expr)