Expressions, Functions and Comparisons

Expressions

Variables can be combined algebraically with the usual Python operators (+, -, *, /, **) to build a symbolic Expression. Every leaf (a variable, parameter or numeric constant) and every operator node (Add, Mul, Pow) is itself an Expression, so expressions compose freely. For example:

from qilisdk.core.variables import BinaryVariable, Bitwise, Domain, SpinVariable, Variable
x = Variable("x", domain=Domain.REAL, bounds=(1, 2), encoding=Bitwise, precision=1e-1)
s = SpinVariable("s")
b = BinaryVariable("b")

e1 = 2 * x + 3
print("e1:", e1)
e2 = 3 * x**2 + 2 * x + 4
print("e2:", e2)
e3 = 2 * x + b - 1
print("e3:", e3)
e4 = e1 - e2
print("e4:", e4)

Output:

e1: 3 + 2 * x
e2: 4 + 2 * x + 3 * x**2
e3: -1 + b + 2 * x
e4: 3 - (4 + 2 * x + 3 * x**2) + 2 * x

Upon construction we canonicalize the expression (flattening nested sums/products, combining like terms, ordering things etc.), ensuring that x + y + 1 equals y + 1 + x. Products are not distributed over sums, so e4 keeps the factored - (4 + 2 * x + 3 * x**2) sub-expression. Use expand() to distribute:

print(e4.expand())          # distribute the product over the sum

Output:

-1 - 3 * x**2

Expressions can be evaluated by providing values for the involved variables via evaluate():

e3.evaluate({
    x: 1.5,
    b: 0
})

Output:

2.0

Warning

To evaluate an expression, all participating variables must be assigned valid values within their respective domains and bounds.

Inspecting and differentiating expressions

An Expression exposes a number of helper functions. You can list the named leaves it depends on, isolate just the free Parameter leaves, read its polynomial degree, or take a symbolic derivative with derivative():

from qilisdk.core.variables import Parameter, Variable, Domain

a = Parameter("a", value=2.0)
y = Variable("y", domain=Domain.REAL, bounds=(0, 5))

expr = a * y**2 + 3 * y

print(expr.variables())          # named leaves, sorted by label
print(expr.free_parameters())    # only the Parameter leaves
print(expr.degree)               # highest polynomial degree (a and y both count)
print(expr.derivative(y))              # symbolic d/dy

Output:

[a, y]
{a}
3
3 + 2 * a * y

Mathematical Functions

Non-polynomial operations are represented by Function, the abstract base for the unary maths functions. Each of its concrete subclasses (listed below) wraps a single Expression operand (a Parameter, any other variable, or a compound expression) and defers numeric evaluation until values are provided.

from qilisdk.core.expression import Cos, Sin
from qilisdk.core.variables import Parameter

theta = Parameter("theta", 0.5)
expr = Sin(theta) + Cos(2 * theta)

print(expr)                # Cos(2 * theta) + Sin(theta)
print(expr.evaluate({}))   # uses theta.value automatically

# You can also supply a different value at evaluation time:
print(expr.evaluate({theta: 1.0}))

Output:

Cos(2 * theta) + Sin(theta)
1.0197278444723428
0.4253241482607541

Because every function is a regular Expression node, it participates in the same algebra: it can be added to or multiplied with other expressions, differentiated symbolically (the chain rule is applied automatically), and evaluated. Wrapping a numeric constant folds eagerly to a Constant:

from qilisdk.core.expression import Cos, Exp, Sin
from qilisdk.core.variables import Parameter

theta = Parameter("theta", 0.5)

print(Sin(theta).derivative(theta))   # d/dtheta Sin(theta) == Cos(theta)
print(Exp(theta).derivative(theta))   # d/dtheta Exp(theta) == Exp(theta)
print(Cos(0))                   # folds to a numeric constant

Output:

Cos(theta)
Exp(theta)
1.0

These functions compose naturally with the rest of the expression tree, so you can include them in constraints, objectives, or schedule coefficients and rely on the same evaluation and encoding rules as any other symbolic expression.

The available function nodes are:

  • Sin for sine

  • Cos for cosine

  • Tan for tangent

  • Exp for exponential

  • Log for logarithm

  • Sqrt for square root

  • Abs for absolute value

Abs is the one function with no derivative: it is not differentiable at zero and there is no sign node to write its derivative with, so derivative() raises on it.

For powers, use the ** operator, which builds a Pow node and accepts a fractional or symbolic exponent. Inv() is a shorthand for x ** -1, so Inv(x), 1 / x and x ** -1 are all the same expression:

from qilisdk.core.expression import Inv
from qilisdk.core.variables import Parameter

x, y = Parameter("x", 4.0), Parameter("y", 0.5)

print(x**y)                       # symbolic exponent
print((x**y).evaluate({}))
print(Inv(x) == 1 / x == x**-1)

Output:

x**y
2.0
True

To write your own function, subclass Function with a NAME, a numeric kernel and a derivative. Everything else (canonicalization, equality, derivative, expand, substitute, serialization) comes from the base class.

Comparisons

A Comparison relates two expressions and is what a Constraint is built from. It is not an Expression itself. Use the following operators to construct one:

Comparison Operation

QiliSDK Method

Alias

Equality

Equal(lhs, rhs)

EQ(lhs, rhs)

Not Equal

NotEqual(lhs, rhs)

NEQ(lhs, rhs)

Less Than

LessThan(lhs, rhs)

LT(lhs, rhs)

Less Than or Equal

LessThanOrEqual(lhs, rhs)

LEQ(lhs, rhs)

Greater Than

GreaterThan(lhs, rhs)

GT(lhs, rhs)

Greater Than or Equal

GreaterThanOrEqual(lhs, rhs)

GEQ(lhs, rhs)

Note: lhs and rhs refer to the left-hand side and right-hand side expressions, respectively.

Example:

from qilisdk.core.comparison import LT
from qilisdk.core.variables import BinaryVariable
x = BinaryVariable("x")
print(LT(2 * x - 1, 1))

Output:

2 * x < 2

When a comparison term is created, constants are automatically moved to the right-hand side, and variable terms to the left-hand side.