lumix.nonlinear.terms.LXPiecewiseLinearTerm

class lumix.nonlinear.terms.LXPiecewiseLinearTerm(var, func, num_segments=20, x_min=None, x_max=None, adaptive=True, method='sos2')[source]

Piecewise-linear approximation for arbitrary nonlinear functions.

Approximates any univariate nonlinear function using a piecewise-linear function. The domain is divided into segments, and the function is approximated by linear interpolation between breakpoints.

Three formulation methods are supported:
  • SOS2: Special Ordered Set type 2 (best when solver supports SOS2)

  • Incremental: Binary selection variables for each segment

  • Logarithmic: Gray code encoding (best for many segments, uses fewer binaries)

Parameters:
var

The input variable to the nonlinear function.

func

The nonlinear function to approximate, taking a float and returning a float.

num_segments

Number of linear segments to use (default: 20).

x_min

Minimum value of the input domain (default: use variable lower bound).

x_max

Maximum value of the input domain (default: use variable upper bound).

adaptive

If True, use adaptive segmentation based on function curvature (default: True).

method

Formulation method - “sos2”, “incremental”, or “logarithmic” (default: “sos2”).

Example

Exponential growth function:

import math
from lumix.nonlinear import LXPiecewiseLinearTerm
from lumix.core import LXVariable

# Approximate exp(t) for t in [0, 5]
time = LXVariable[Task, float]("time").continuous().bounds(0, 5)

exp_term = LXPiecewiseLinearTerm(
    var=time,
    func=lambda t: math.exp(t),
    num_segments=30,
    x_min=0.0,
    x_max=5.0,
    adaptive=True,
    method="sos2"
)

Custom discount curve:

# Tiered discount: 100% up to 100 units, 90% up to 1000, then 80%
quantity = LXVariable[Order, float]("qty").continuous().bounds(0, 2000)

def discount_func(q):
    if q < 100:
        return 1.0
    elif q < 1000:
        return 0.9
    else:
        return 0.8

discount = LXPiecewiseLinearTerm(
    var=quantity,
    func=discount_func,
    num_segments=50,
    adaptive=False,  # Uniform segments for step function
    method="incremental"
)

Logarithmic cost function:

# Cost grows logarithmically with size
size = LXVariable[Component, float]("size").continuous().bounds(1, 1000)

log_cost = LXPiecewiseLinearTerm(
    var=size,
    func=lambda s: 10 * math.log(s),
    num_segments=25,
    method="logarithmic"  # Efficient for many segments
)

Sigmoid activation:

# Approximate sigmoid function
def sigmoid(x):
    return 1.0 / (1.0 + math.exp(-x))

activation = LXVariable[Node, float]("activation").continuous()

sigmoid_approx = LXPiecewiseLinearTerm(
    var=activation,
    func=sigmoid,
    num_segments=40,
    x_min=-6.0,
    x_max=6.0,
    adaptive=True
)

Note

Adaptive Segmentation: When adaptive=True, the algorithm places more breakpoints in regions where the function has higher curvature, improving approximation accuracy with fewer segments.

Method Selection:
  • Use “sos2” if your solver has native SOS2 support (most efficient)

  • Use “incremental” for better solver performance with few segments

  • Use “logarithmic” when you need many segments (uses O(log n) binaries)

Domain Bounds: If x_min and x_max are not specified, they default to the variable’s lower and upper bounds. The variable MUST have finite bounds for piecewise-linear approximation.

Approximation Error: More segments generally provide better approximation but increase model complexity. The num_segments parameter allows you to trade off accuracy for solving speed.

__init__(var, func, num_segments=20, x_min=None, x_max=None, adaptive=True, method='sos2')
Parameters:
Return type:

None

Methods

__init__(var, func[, num_segments, x_min, ...])

Attributes

var: LXVariable
func: Callable[[float], float]
num_segments: int = 20
x_min: Optional[float] = None
x_max: Optional[float] = None
adaptive: bool = True
method: Literal['sos2', 'incremental', 'logarithmic'] = 'sos2'
__init__(var, func, num_segments=20, x_min=None, x_max=None, adaptive=True, method='sos2')
Parameters:
Return type:

None