"""
Mathematical Expression Module for OptiX Optimization Framework
================================================================
This module provides classes and utilities for representing and manipulating
mathematical expressions in optimization problems. It implements linear
combinations of variables with precise arithmetic handling for coefficients.
The module serves as a foundation for constraint and objective function
definitions, providing robust handling of variable coefficients through
fraction-based arithmetic to avoid floating-point precision issues.
Classes:
OXpression: Mathematical expression representing linear combinations of variables
Functions:
get_integer_numerator_and_denominators: Utility for converting floating-point
coefficients to integer values with common denominators
Key Features:
- UUID-based variable referencing for serialization compatibility
- Fraction-based arithmetic for precise coefficient handling
- Automatic conversion between floating-point and integer representations
- Support for iterating over variable-coefficient pairs
- Integration with the OptiX constraint and objective systems
Module Dependencies:
- math: For mathematical operations (LCM calculation)
- dataclasses: For structured expression definitions
- decimal: For precise decimal arithmetic
- fractions: For rational number arithmetic
- uuid: For variable identification
- base: For core OptiX object system integration
Example:
Creating and manipulating mathematical expressions:
.. code-block:: python
from constraints import OXpression
from variables import OXVariable
import uuid
# Create variables
x = OXVariable(name="x", lower_bound=0)
y = OXVariable(name="y", lower_bound=0)
z = OXVariable(name="z", lower_bound=0)
# Create expression: 2.5x + 1.5y + 3z
expr = OXpression(
variables=[x.id, y.id, z.id],
weights=[2.5, 1.5, 3.0]
)
# Access expression properties
print(f"Number of variables: {expr.number_of_variables}") # 3
print(f"Integer weights: {expr.integer_weights}") # [5, 3, 6]
print(f"Common denominator: {expr.integer_denominator}") # 2
# Iterate over variable-weight pairs
for var_id, weight in expr:
print(f"Variable {var_id}: coefficient {weight}")
"""
import functools
import math
from dataclasses import dataclass, field
from decimal import Decimal
from fractions import Fraction
from uuid import UUID
from base import OXObject
@functools.lru_cache(maxsize=1024)
def calculate_fraction(value: float | Decimal | int) -> Fraction:
"""
Convert a numeric value to its fractional representation using the mediant method.
This function converts floating-point numbers to exact fractional representations
using a binary search approach with mediants. The mediant of two fractions a/b and c/d
is (a+c)/(b+d), which provides an efficient way to find rational approximations.
The function handles edge cases where the value is already an integer and uses
caching to improve performance for repeated calculations.
Args:
value (float | Decimal | int): The numeric value to convert to a fraction.
Can be a floating-point number, Decimal, or integer.
Returns:
Fraction: The exact fractional representation of the input value.
For integers, returns Fraction(value, 1).
For floating-point numbers, returns the closest rational approximation.
Note:
- Uses LRU cache with maxsize=1024 for performance optimization
- The mediant method ensures finding exact representations for most decimal values
- Integer values are handled as a special case for efficiency
- The algorithm converges when math.isclose() indicates sufficient precision
Example:
.. code-block:: python
# Convert decimal to fraction
frac1 = calculate_fraction(0.5) # Returns Fraction(1, 2)
frac2 = calculate_fraction(0.25) # Returns Fraction(1, 4)
frac3 = calculate_fraction(2.0) # Returns Fraction(2, 1)
# Works with Decimal and int types
from decimal import Decimal
frac4 = calculate_fraction(Decimal('0.125')) # Returns Fraction(1, 8)
frac5 = calculate_fraction(5) # Returns Fraction(5, 1)
"""
if int(math.ceil(value)) == int(math.floor(value)):
return Fraction(math.ceil(value), 1)
value = float(value)
ub = Fraction(math.ceil(value), 1)
lb = Fraction(math.floor(value), 1)
mediant = Fraction(ub.numerator + lb.numerator, ub.denominator + lb.denominator)
while not math.isclose(mediant, value):
if mediant < value:
lb = mediant
else:
ub = mediant
mediant = Fraction(ub.numerator + lb.numerator, ub.denominator + lb.denominator)
return mediant
[docs]
def get_integer_numerator_and_denominators(numbers: list[float | int]) -> tuple[int, list[int]]:
"""
Convert a list of floating-point or integer weights to integer representations.
This function takes a collection of numeric values (which may include floating-point
numbers and integers) and converts them to exact integer representations by finding
a common denominator. This is essential for optimization solvers that require
integer coefficients while maintaining mathematical precision.
The function works by:
1. Converting each number to its fractional representation using calculate_fraction()
2. Finding the least common multiple (LCM) of all denominators
3. Scaling all numerators by appropriate factors to use the common denominator
4. Returning both the common denominator and the scaled integer numerators
Args:
numbers (list[float | int]): A list of numeric values to convert to integer
representations. Can contain floating-point numbers,
integers, or a mix of both types.
Returns:
tuple[int, list[int]]: A tuple containing:
- int: The common denominator for all converted values
- list[int]: List of integer numerators corresponding to each input number
when expressed with the common denominator
Raises:
ValueError: If the input list is empty or contains non-numeric values.
ZeroDivisionError: If any input number results in a zero denominator.
Note:
- All calculations maintain exact precision through Fraction arithmetic
- The LCM approach ensures the smallest possible common denominator
- Integer inputs are handled efficiently as Fraction(value, 1)
- Useful for preparing coefficients for linear programming solvers
Example:
.. code-block:: python
# Convert mixed numeric types
numbers = [0.5, 1.5, 2, 0.25]
denominator, numerators = get_integer_numerator_and_denominators(numbers)
print(f"Common denominator: {denominator}") # 4
print(f"Integer numerators: {numerators}") # [2, 6, 8, 1]
# Verify the conversion
for i, num in enumerate(numbers):
converted = numerators[i] / denominator
print(f"{num} = {numerators[i]}/{denominator} = {converted}")
# Example with simple fractions
simple_fractions = [0.5, 1.5, 2.0]
denom, nums = get_integer_numerator_and_denominators(simple_fractions)
# Returns: (2, [1, 3, 4]) representing [1/2, 3/2, 4/2]
See Also:
calculate_fraction: Used internally to convert individual numbers to fractions
math.lcm: Used to find the least common multiple of denominators
"""
fractional_weights = [calculate_fraction(value=w) if not isinstance(w, Fraction) else w for w in numbers]
denominators = [fw.denominator for fw in fractional_weights]
numerator = [fw.numerator for fw in fractional_weights]
common_multiple = math.lcm(*denominators)
factors = [common_multiple // n for n in denominators]
numerator = [n * f for n, f in zip(numerator, factors)]
return common_multiple, numerator
[docs]
@dataclass
class OXpression(OXObject):
"""
Mathematical expression representing linear combinations of optimization variables.
OXpression is a fundamental component of the OptiX optimization framework that
represents linear mathematical expressions in the form: c₁x₁ + c₂x₂ + ... + cₙxₙ,
where cᵢ are coefficients (weights) and xᵢ are decision variables.
This class is designed to handle expressions used in both constraint definitions
and objective functions within optimization problems. It provides precise arithmetic
handling through fraction-based calculations to avoid floating-point precision errors
that can occur in mathematical optimization.
The class maintains variable references using UUIDs rather than direct object references,
enabling serialization, persistence, and cross-system compatibility. This design
pattern supports distributed optimization scenarios and model persistence.
Key Features:
- UUID-based variable referencing for serialization safety
- Automatic conversion between floating-point and integer coefficient representations
- Fraction-based arithmetic for mathematical precision
- Iterator support for easy traversal of variable-coefficient pairs
- Integration with OptiX constraint and objective function systems
- Support for multiple numeric types (int, float, Fraction, Decimal)
Attributes:
variables (list[UUID]): Ordered list of variable UUIDs that participate in this expression.
The order corresponds to the order of coefficients in the weights list.
weights (list[float | int | Fraction]): Ordered list of coefficients (weights) for each variable.
Supports mixed numeric types with automatic conversion.
Type Parameters:
The class inherits from OXObject, providing UUID-based identity and serialization capabilities.
Example:
Basic usage of OXpression for creating mathematical expressions:
.. code-block:: python
from uuid import UUID
from constraints import OXpression
from variables import OXVariable
# Create some optimization variables
x = OXVariable(name="production_x", lower_bound=0, upper_bound=100)
y = OXVariable(name="production_y", lower_bound=0, upper_bound=50)
z = OXVariable(name="production_z", lower_bound=0)
# Create expression: 2.5x + 1.75y + 3z (production cost function)
cost_expr = OXpression(
variables=[x.id, y.id, z.id],
weights=[2.5, 1.75, 3.0]
)
# Access expression properties
print(f"Variables in expression: {cost_expr.number_of_variables}") # 3
print(f"Integer weights: {cost_expr.integer_weights}") # [10, 7, 12]
print(f"Common denominator: {cost_expr.integer_denominator}") # 4
# Iterate through variable-coefficient pairs
for var_uuid, coefficient in cost_expr:
print(f"Variable {var_uuid}: coefficient = {coefficient}")
# Example with mixed coefficient types
mixed_expr = OXpression(
variables=[x.id, y.id],
weights=[Fraction(1, 3), 0.75] # Mixed Fraction and float
)
Note:
- Variables are referenced by UUID to support serialization and persistence
- The weights list must have the same length as the variables list
- Automatic fraction conversion ensures mathematical precision for optimization solvers
- The class supports empty expressions (no variables/weights) for initialization
- All weight types are converted to fractions internally for consistent arithmetic
Warning:
Ensure that the variables and weights lists maintain corresponding order and equal length.
Mismatched lengths will result in undefined behavior during iteration and calculations.
See Also:
OXVariable: Decision variables used in expressions
OXConstraint: Constraints that use OXpression for left-hand sides
calculate_fraction: Internal function for precise fraction conversion
get_integer_numerator_and_denominators: Utility for solver-compatible representations
"""
variables: list[UUID] = field(default_factory=list)
weights: list[float | int | Fraction] = field(default_factory=list)
@property
def number_of_variables(self) -> int:
"""
Get the total count of variables participating in this mathematical expression.
This property provides a convenient way to determine the dimensionality of the
linear expression, which is useful for validation, debugging, and solver setup.
The count represents the number of decision variables that have non-zero
coefficients in this expression.
Returns:
int: The total number of variables in the expression. Returns 0 for empty
expressions (expressions with no variables or coefficients).
Note:
- The count is based on the length of the variables list
- Empty expressions return 0, which is valid for initialization scenarios
- The count should match the length of the weights list for consistency
Example:
.. code-block:: python
# Create expression with three variables
expr = OXpression(
variables=[var1_id, var2_id, var3_id],
weights=[1.0, 2.5, 0.75]
)
print(expr.number_of_variables) # Output: 3
# Empty expression
empty_expr = OXpression()
print(empty_expr.number_of_variables) # Output: 0
"""
return len(self.variables)
@property
def integer_weights(self) -> list[int]:
"""
Convert expression coefficients to integer representations with common denominator.
This property transforms all variable coefficients from their original numeric types
(float, int, Fraction, Decimal) into integer values by finding a common denominator
and scaling appropriately. This conversion is essential for optimization solvers
that require integer coefficients while maintaining mathematical precision.
The conversion process:
1. Converts each weight to its exact fractional representation
2. Finds the least common multiple (LCM) of all denominators
3. Scales all numerators to use the common denominator
4. Returns the scaled integer numerators
Returns:
list[int]: Integer representations of all coefficients, scaled by the common
denominator. The order corresponds to the variables list order.
Returns empty list if no weights are present.
Note:
- Maintains exact mathematical precision through fraction arithmetic
- The integer values represent numerators when using the common denominator
- Use integer_denominator property to get the corresponding denominator
- Essential for solvers like CPLEX or Gurobi that prefer integer coefficients
Example:
.. code-block:: python
# Expression with decimal coefficients
expr = OXpression(
variables=[x_id, y_id, z_id],
weights=[0.5, 1.25, 2.0]
)
print(expr.integer_weights) # [2, 5, 8]
print(expr.integer_denominator) # 4
# Verification: 2/4 = 0.5, 5/4 = 1.25, 8/4 = 2.0
See Also:
integer_denominator: Get the common denominator for these integer weights
get_integer_numerator_and_denominators: The underlying conversion function
"""
return get_integer_numerator_and_denominators(self.weights)[1]
@property
def integer_denominator(self) -> int:
"""
Get the common denominator used for integer weight representation.
This property returns the least common multiple (LCM) of all denominators
in the fractional representations of the expression coefficients. When combined
with the integer_weights property, it allows for exact reconstruction of the
original coefficient values while providing integer representations suitable
for optimization solvers.
The denominator represents the scaling factor applied to convert floating-point
or fractional coefficients into integers. This approach maintains mathematical
precision and avoids floating-point arithmetic errors in optimization calculations.
Returns:
int: The common denominator for all coefficients in the expression.
Returns 1 if all weights are integers, or the LCM of all fractional
denominators if floating-point weights are present. Returns 1 for
empty expressions.
Note:
- Always returns a positive integer value
- The LCM approach ensures the smallest possible common denominator
- Combined with integer_weights, provides exact coefficient representation
- Essential for maintaining precision in constraint and objective definitions
Example:
.. code-block:: python
# Expression with fractional coefficients
expr = OXpression(
variables=[x_id, y_id, z_id],
weights=[0.5, 0.25, 1.75] # 1/2, 1/4, 7/4
)
print(expr.integer_denominator) # 4 (LCM of 2, 4, 4)
print(expr.integer_weights) # [2, 1, 7]
# Verification: 2/4 = 0.5, 1/4 = 0.25, 7/4 = 1.75
# Expression with integer coefficients
int_expr = OXpression(
variables=[x_id, y_id],
weights=[2, 3]
)
print(int_expr.integer_denominator) # 1
See Also:
integer_weights: Get the integer numerators for these coefficients
get_integer_numerator_and_denominators: The underlying conversion function
"""
return get_integer_numerator_and_denominators(self.weights)[0]
[docs]
def __iter__(self):
"""
Enable iteration over variable-coefficient pairs in the mathematical expression.
This method implements the iterator protocol, allowing the OXpression object
to be used in for-loops and other iteration contexts. It yields tuples of
(variable_uuid, coefficient) pairs, maintaining the order defined in the
variables and weights lists.
The iterator is particularly useful for:
- Traversing expression terms for solver setup
- Debugging and validation of expression contents
- Serialization and persistence operations
- Constructing string representations of expressions
Yields:
tuple[UUID, float | int | Fraction]: Each iteration yields a tuple containing:
- UUID: The unique identifier of the variable
- float | int | Fraction: The coefficient (weight) for that variable
Note:
- Iteration order matches the order of variables and weights lists
- Empty expressions will not yield any items
- The yielded coefficients maintain their original numeric types
- Supports standard Python iteration protocols (for loops, list comprehension, etc.)
Example:
.. code-block:: python
from uuid import uuid4
from fractions import Fraction
# Create expression with mixed coefficient types
expr = OXpression(
variables=[uuid4(), uuid4(), uuid4()],
weights=[2.5, 3, Fraction(1, 2)]
)
# Iterate over variable-coefficient pairs
for var_uuid, coefficient in expr:
print(f"Variable {var_uuid}: coefficient = {coefficient}")
# Use in list comprehension
terms = [(str(var_id)[:8], coef) for var_id, coef in expr]
print(f"Expression terms: {terms}")
# Convert to dictionary
expr_dict = dict(expr)
# Count terms
term_count = len(list(expr))
Raises:
ValueError: If variables and weights lists have different lengths
(this would indicate a malformed expression)
"""
return iter(zip(self.variables, self.weights))