Extending Nonlinear Module

Guide for adding custom nonlinear term types to LumiX.

Overview

You can extend the nonlinear module by:

  1. Adding New Term Types: Define new dataclasses for additional nonlinear operations

  2. Custom Linearization: Implement linearization methods for your terms

  3. Integration: Register terms with the linearization engine

Adding New Term Types

Step-by-Step Guide

Example: Adding Cubic Terms

Let’s add support for cubic terms (x³):

Step 1: Define the dataclass in src/lumix/nonlinear/terms.py:

@dataclass
class LXCubicTerm:
    """Cubic term: coefficient * x³.

    Represents the cube of a variable, linearized using piecewise-linear
    approximation or auxiliary variables.

    Attributes:
        var: The variable to cube.
        coefficient: Coefficient to multiply the cubic term by (default: 1.0).

    Example:
        Cubic cost function::

            volume = LXVariable[Container, float]("volume").continuous().bounds(0, 10)
            cubic_cost = LXCubicTerm(var=volume, coefficient=2.5)
    """

    var: LXVariable
    coefficient: float = 1.0

Step 2: Export from __init__.py:

from .terms import (
    LXAbsoluteTerm,
    LXBilinearTerm,
    LXCubicTerm,  # Add new term
    LXIndicatorTerm,
    LXMinMaxTerm,
    LXPiecewiseLinearTerm,
)

__all__ = [
    "LXAbsoluteTerm",
    "LXBilinearTerm",
    "LXCubicTerm",  # Add to exports
    "LXIndicatorTerm",
    "LXMinMaxTerm",
    "LXPiecewiseLinearTerm",
]

Step 3: Add linearization in src/lumix/linearization/engine.py:

def linearize_cubic(self, term: LXCubicTerm) -> ...:
    """Linearize x³ using piecewise-linear approximation."""
    # Get variable bounds
    x_min = term.var.lower_bound or 0
    x_max = term.var.upper_bound or 1

    # Create piecewise-linear approximation
    def cubic_func(x):
        return term.coefficient * (x ** 3)

    # Use existing piecewise linearization
    piecewise_term = LXPiecewiseLinearTerm(
        var=term.var,
        func=cubic_func,
        num_segments=20,
        x_min=x_min,
        x_max=x_max,
        adaptive=True
    )

    return self.linearize_piecewise(piecewise_term)

Step 4: Add tests:

def test_cubic_term():
    var = LXVariable[Item, float]("x").continuous().bounds(0, 10)
    term = LXCubicTerm(var=var, coefficient=2.0)

    assert term.var == var
    assert term.coefficient == 2.0

def test_cubic_linearization():
    var = LXVariable[Item, float]("x").continuous().bounds(0, 10)
    term = LXCubicTerm(var=var, coefficient=1.0)

    linearizer = LXLinearizer()
    result = linearizer.linearize_cubic(term)
    # Verify linearization constraints

More Examples

Logical AND Term

@dataclass
class LXAndTerm:
    """Logical AND of multiple binary variables.

    z = x₁ AND x₂ AND ... AND xₙ

    Attributes:
        vars: List of binary variables to AND together.
    """
    vars: List[LXVariable]

# Linearization (in engine):
def linearize_and(self, term: LXAndTerm):
    """z = 1 iff all vars = 1."""
    z = self._create_aux_var("and", var_type=BINARY)

    # z <= xᵢ for all i
    for var in term.vars:
        self._add_constraint(z <= var)

    # z >= sum(xᵢ) - n + 1
    self._add_constraint(z >= sum(term.vars) - len(term.vars) + 1)

    return z

Absolute Difference

@dataclass
class LXAbsDifferenceTerm:
    """Absolute difference between two variables: |x - y|.

    Attributes:
        var1: First variable.
        var2: Second variable.
        coefficient: Coefficient (default: 1.0).
    """
    var1: LXVariable
    var2: LXVariable
    coefficient: float = 1.0

# Linearization:
def linearize_abs_difference(self, term: LXAbsDifferenceTerm):
    """Linearize |x - y|."""
    # Create auxiliary for difference
    diff = self._create_aux_var("diff")
    self._add_constraint(diff == term.var1 - term.var2)

    # Use absolute value linearization
    abs_term = LXAbsoluteTerm(var=diff, coefficient=term.coefficient)
    return self.linearize_absolute(abs_term)

Custom Linearization Methods

Overriding Default Linearization

Create custom linearizer subclass:

from lumix.linearization import LXLinearizer, LXLinearizerConfig
from lumix.nonlinear import LXBilinearTerm

class TightBoundsLinearizer(LXLinearizer):
    """Linearizer with improved bound computation."""

    def linearize_bilinear(self, term: LXBilinearTerm):
        """Custom McCormick with tighter bounds."""
        # Compute tighter bounds using problem structure
        tight_bounds = self._compute_tight_bounds(term)

        # Apply custom McCormick
        return self._mccormick_with_bounds(term, tight_bounds)

    def _compute_tight_bounds(self, term):
        # Custom logic to compute tighter bounds
        pass

Alternative Formulations

Provide multiple linearization strategies:

class FlexibleLinearizer(LXLinearizer):
    def __init__(self, config: LXLinearizerConfig):
        super().__init__(config)
        self.bilinear_method = config.bilinear_method

    def linearize_bilinear(self, term: LXBilinearTerm):
        if self.bilinear_method == "mccormick":
            return self._mccormick(term)
        elif self.bilinear_method == "logarithmic":
            return self._logarithmic_formulation(term)
        else:
            return super().linearize_bilinear(term)

Integration with Model Builder

Automatic Detection

Register term types for automatic linearization:

# In linearization engine
TERM_HANDLERS = {
    LXAbsoluteTerm: linearize_absolute,
    LXBilinearTerm: linearize_bilinear,
    LXCubicTerm: linearize_cubic,  # New handler
    # ...
}

def linearize_term(self, term):
    """Dispatch to appropriate handler."""
    handler = TERM_HANDLERS.get(type(term))
    if handler is None:
        raise ValueError(f"No handler for {type(term)}")
    return handler(self, term)

Testing Custom Terms

Unit Tests

def test_custom_term_creation():
    var = LXVariable[Data, float]("x").continuous()
    term = LXCubicTerm(var=var, coefficient=2.0)
    assert isinstance(term, LXCubicTerm)
    assert term.coefficient == 2.0

Integration Tests

def test_custom_term_in_model():
    var = LXVariable[Data, float]("x").continuous().bounds(0, 10)
    term = LXCubicTerm(var=var)

    model = LXModel("test")
    model.add_variable(var)
    # Add term to objective or constraint

    linearizer = CustomLinearizer()
    linear_model = linearizer.linearize(model)

    # Verify linearization
    assert len(linear_model.constraints) > 0

Best Practices

Term Design

  1. Immutable dataclasses: Use @dataclass with no methods

  2. Type annotations: Full typing for all attributes

  3. Default values: Provide sensible defaults where appropriate

  4. Documentation: Comprehensive docstrings with examples

Linearization Implementation

  1. Validate inputs: Check variable bounds before linearization

  2. Efficient formulations: Minimize auxiliary variables and constraints

  3. Numerical stability: Avoid large M values

  4. Error handling: Raise informative errors for invalid inputs

Example: Validation

def linearize_cubic(self, term: LXCubicTerm):
    """Linearize x³."""
    # Validate bounds
    if term.var.lower_bound is None or term.var.upper_bound is None:
        raise ValueError(
            f"Variable '{term.var.name}' must have finite bounds "
            f"for cubic linearization"
        )

    # Validate coefficient
    if term.coefficient == 0:
        raise ValueError("Coefficient cannot be zero")

    # Proceed with linearization
    ...

Documentation

Document your custom terms:

@dataclass
class LXCustomTerm:
    """One-line summary.

    Detailed description of the term, including:
    - Mathematical formulation
    - Use cases
    - Linearization approach

    Attributes:
        var: Description of variable.
        param: Description of parameter.

    Example:
        Basic usage::

            var = LXVariable[Data, float]("x").continuous()
            term = LXCustomTerm(var=var, param=1.0)

    Note:
        Important notes about usage, requirements, or limitations.
    """
    var: LXVariable
    param: float = 1.0

Contributing to LumiX

If you develop useful custom terms, consider contributing them to LumiX:

  1. Fork the repository

  2. Add your term following the guidelines above

  3. Include comprehensive tests

  4. Update documentation

  5. Submit a pull request

See Also