ommx.Function#

In mathematical optimization, functions are used to express objective functions and constraints. OMMX stores polynomials compactly and can compose them with scalar operations such as absolute value, minimum, division, and integer power.

Data Structure

Description

Linear

Linear function. Holds pairs of variable IDs and their coefficients

Quadratic

Quadratic function. Holds pairs of variable ID pairs and their coefficients

Polynomial

Polynomial. Holds pairs of variable ID combinations and their coefficients

Function

A polynomial or a composed scalar expression

Creating ommx.Function#

In the Python SDK, there are two main approaches to create these data structures. The first approach is to directly call the constructors of each data structure. For example, you can create ommx.Linear as follows.

from ommx import Linear

linear = Linear(terms={1: 1.0, 2: 2.0}, constant=3.0)
print(linear)

In this way, decision variables are identified by IDs and coefficients are represented by real numbers. To access coefficients and constant values, use the terms, linear_terms and constant_term properties.

print(f"{linear.terms=}")
print(f"{linear.linear_terms=}")
print(f"{linear.constant_term=}")

Another approach is to create from ommx.DecisionVariable. ommx.DecisionVariable is a data structure that only holds the ID of the decision variable. When creating polynomials such as ommx.Linear, you can first create decision variables using ommx.DecisionVariable and then use them to create polynomials.

from ommx import DecisionVariable

x = DecisionVariable.binary(1, name="x")
y = DecisionVariable.binary(2, name="y")

linear = x + 2.0 * y + 3.0
print(linear)

Note that the polynomial data type retains only the ID of the decision variable and does not store additional information. In the above example, information passed to DecisionVariable.binary such as x and y is not carried over to Linear. This second method can create polynomials of any degree.

q = x * x + x * y + y * y
print(q)
p = x * x * x + y * y
print(p)

Linear, Quadratic, and Polynomial each have their own unique data storage methods, so they are separate Messages. Since any of them can be used as objective functions or constraints, Function provides one common representation and also owns composed scalar expressions.

from ommx import Function

# Constant
print(Function(1.0))
# Linear
print(Function(linear))
# Quadratic
print(Function(q))
# Polynomial
print(Function(p))

Composing Scalar Functions#

Convert an arithmetic expression to Function before applying the composed operations. Python’s abs and arithmetic operators build composed expressions; pointwise minimum and maximum use the named minimum and maximum methods.

fx = Function(x)
fy = Function(y)

absolute = abs(fx - 3)
sign = (fx - 1).signum()
minimum = fx.minimum(fy)
maximum = fx.maximum(fy)
quotient = fx / (fy + 1)
power = fx**2
same_power = fx.powi(2)

print(absolute)
print(minimum)
print(quotient)
print(power)
print(same_power)

The built-in Python functions min(fx, fy) and max(fx, fy) perform comparisons and therefore do not construct pointwise expressions. Use fx.minimum(fy) and fx.maximum(fy) instead.

fx**n and fx.powi(n) are equivalent, and n must fit in a signed 32-bit integer. Floating-point or function-valued exponents and reverse exponentiation such as 2**fx are not supported.

Within a composed expression, operands are encountered from left to right and every associative application still combines exactly two values. Both (abs(a) + abs(b)) + abs(c) and abs(a) + (abs(b) + abs(c)) therefore retain their grouping and operation order. Overflow and undefined-operation errors occur in that represented order. The protobuf wire format stores the composition as a flat reverse Polish notation (RPN) instruction sequence rather than a recursively nested tree. Exact addition, subtraction, multiplication, and negation containing only polynomials continue to normalize to the compact polynomial representation. Division of a Function remains a composed expression even when the divisor is a constant or coefficient, because whether that divisor is treated as zero depends on the evaluation tolerance.

Composed functions follow real-valued evaluation semantics. At an explicit evaluation entry point, the atol argument defines zero as \(|v| \leq \mathtt{atol}\) (including the boundary):

  • signum(v) is 0 when \(|v| \leq \mathtt{atol}\), -1 below that interval, and 1 above it.

  • Division is undefined when the absolute value of the denominator is at most atol.

  • Raising a value with absolute value at most atol to a negative integer power is undefined; 0**0 is defined as 1.

  • Negative bases are supported because every exponent is an integer.

  • Undefined operations and non-finite intermediate results raise ValueError in Python.

If atol is omitted, that evaluation call uses the configured default tolerance. Expression construction, substitution, and partial evaluation do not use a default tolerance to decide these zero-sensitive operations. Their Signum, Div, and negative Powi nodes remain in the RPN expression so a later evaluation call can apply its own atol.

evaluate_bound(bounds, atol=...) uses the same zero classification for interval evaluation. Transformations that derive constraints or coefficients from this bound accept the tolerance so their Function-body semantics are explicit; this is separate from canonicalizing approximate discrete solver values.

Division and negative integer powers can make a Function partial. Algebraic simplification preserves that domain: for example, 0 * (1 / fx) is still undefined wherever \(|\mathtt{fx}| \leq \mathtt{atol}\).

try:
    (1 / fx).evaluate({1: 0}, atol=1e-6)
except ValueError as e:
    print(f"Error: {e}")

Polynomial metadata is available only when the Function uses the compact polynomial representation. degree() and num_terms() return None for composed expressions, while coefficient properties such as terms raise TypeError. Apart from identity and constant folding, an integer-power expression remains composed even when its exponent is non-negative; use explicit multiplication when you need a compact expanded polynomial. Every current InstanceClassClause requires polynomial objective and constraint-body functions. Consequently, all bundled solver adapters reject composed objectives and constraint bodies; reformulate those solver-input functions as supported polynomials before invoking an adapter.

Compact polynomial normalization is exact: it removes an actual zero produced by cancellation or floating-point underflow, but retains every finite nonzero coefficient regardless of magnitude. It does not use atol to prune small terms. OMMX does not currently perform approximate cleanup implicitly; such cleanup would require a separate, explicit API rather than being part of expression construction or evaluation.

Substitution and Partial Evaluation of Decision Variables#

Function and the polynomial types have an evaluate method that substitutes values for decision variables. For example, substituting \(x_1 = 1\) and \(x_2 = 0\) into the linear function \(x_1 + 2x_2 + 3\) created above results in \(1 + 2 \times 0 + 3 = 4\).

value= linear.evaluate({1: 1, 2: 0})
print(f"{value=}")

The argument supports the format dict[int, float] and ommx.State. evaluate returns an error if the necessary decision variable IDs are missing.

try:
    linear.evaluate({1: 1})
except ValueError as e:
    print(f"Error: {e}")

If you want to substitute values for only some of the decision variables, use the partial_evaluate method.

linear2= linear.partial_evaluate({1: 1})
print(f"{linear2=}")

Linear, Quadratic, and Polynomial partial evaluation keeps the original Python type. For a composed Function, the expression structure that still depends on unassigned variables is preserved. A closed subexpression may be folded only when its result is independent of atol. Zero-sensitive Signum, Div, and negative Powi subexpressions remain composed even after all their variables have been assigned, so the atol of a later evaluate call still determines their result or domain error.

Comparison of Coefficients#

Function and the polynomial types have an almost_equal function. For polynomial functions, it determines whether the coefficients match within a specified error. For composed functions, it compares matching expression structures; it is not a proof of global mathematical equivalence. For example, to confirm that \( (x + 1)^2 = x^2 + 2x + 1 \), write as follows

xx = (x + 1) * (x + 1)
xx.almost_equal(x * x + 2 * x + 1)