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:
Sinfor sineCosfor cosineTanfor tangentExpfor exponentialLogfor logarithmSqrtfor square rootAbsfor 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 |
||
Not Equal |
||
Less Than |
||
Less Than or Equal |
||
Greater Than |
||
Greater Than or Equal |
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.