Skip to main content

Step Functions

IntStepFunction is a piecewise-constant function defined at modeling time. Step functions provide constant data that the solver uses to model calendars, availability windows, time-dependent costs, energy pricing, efficiency rates, and other time-varying quantities. The function itself doesn't change during solving—it's static input data.

Creating Step Functions​

Create a step function using model.step_function():

import optalcp as cp

model = cp.Model()

func = model.step_function([
# Implicitly: value is 0 before time 0
(0, 10), # From time 0: value is 10
(100, 20), # From time 100: value is 20
(200, 15), # From time 200 onwards: value is 15
])

Parameters​

  • values: Iterable of (time, value) pairs defining the function. Must be sorted by time in ascending order.

Semantics​

A step function ff defined by points (x0,y0),(x1,y1),…,(xn,yn)(x_0, y_0), (x_1, y_1), \ldots, (x_n, y_n):

f(x)={0if x<x0yiif xi≤x<xi+1ynif x≥xnf(x) = \begin{cases} 0 & \text{if } x < x_0 \\ y_i & \text{if } x_i \le x < x_{i+1} \\ y_n & \text{if } x \ge x_n \end{cases}

The function value at any point is the value from the most recent step at or before that point. Before the first step, the value is 0.

Evaluating Step Functions​

Point Evaluation​

Evaluate the function at a specific point in time:

# Cost varies by time of day
cost_func = model.step_function([
(0, 10), # 0h-8h: low cost ($10)
(8, 20), # 8h-17h: high cost ($20)
(17, 10), # 17h onwards: low cost ($10)
])

task = model.interval_var(length=5, name="task")

# Evaluate cost at task start
start_cost = model.eval(cost_func, task.start())

model.minimize(start_cost)

Integral Over Interval​

Compute the sum of function values at each integer point in the interval:

integral(f,[s,e))=∑t=se−1f(t)\mathtt{integral}(f, [s, e)) = \sum_{t=s}^{e-1} f(t)

The interval is half-open: includes start, excludes end. If the interval has zero length (s=es = e), the result is 0. If the interval is absent, the result is absent.

Requirement: The step function must be non-negative.

# Energy rate per time unit
energy_rate = model.step_function([
(0, 5), # 0h-8h: off-peak ($5/hour)
(8, 15), # 8h-18h: peak ($15/hour)
(18, 5), # 18h onwards: off-peak ($5/hour)
])

task = model.interval_var(length=10, name="task")

# Total energy cost = sum of rate over task duration
total_cost = model.integral(energy_rate, task)

model.minimize(total_cost)

Forbid Constraints​

Step functions can forbid task placement in time windows where the function value is zero.

Forbid Start / End​

Require the function to be non-zero at task start or end:

# Availability: 1 = available, 0 = unavailable
availability = model.step_function([
(0, 1), # 0h-8h: available
(8, 0), # 8h-12h: break
(12, 1), # 12h onwards: available
])

task = model.interval_var(length=5, name="task")

# Task cannot start when availability is 0
task.forbid_start(availability)
# Equivalent to:
model.enforce(availability.eval(task.start()) != 0)

# Task cannot end when availability is 0
task.forbid_end(availability)
# Equivalent to:
model.enforce(availability.eval(task.end()) != 0)

Forbid Extent​

Require the function to be non-zero throughout the entire task:

# Machine availability calendar
availability = model.step_function([
(0, 1), # 0h-8h: available
(8, 0), # 8h-12h: maintenance
(12, 1), # 12h-20h: available
(20, 0), # 20h onwards: closed
])

task = model.interval_var(length=5, name="task")

# Task cannot overlap any period where availability is 0
task.forbid_extent(availability)

Use Cases​

Time-Dependent Costs​

Model costs that vary throughout the day:

electricity_cost = model.step_function([
(0, 5), # 0h-6h: night ($5/hour)
(6, 8), # 6h-9h: morning ($8/hour)
(9, 15), # 9h-17h: peak ($15/hour)
(17, 8), # 17h-22h: evening ($8/hour)
(22, 5), # 22h onwards: night ($5/hour)
])

tasks = [model.interval_var(length=10) for _ in range(5)]

# Minimize total electricity cost
total_cost = model.sum([
model.integral(electricity_cost, task) for task in tasks
])
model.minimize(total_cost)

Availability Windows​

Model resource availability over time:

# Machine available only during working hours
working_hours = model.step_function([
(0, 0), # 0h-8h: closed
(8, 1), # 8h-17h: open
(17, 0), # 17h onwards: closed
])

tasks = [model.interval_var(length=5) for _ in range(10)]

# All tasks must execute during working hours
for task in tasks:
task.forbid_extent(working_hours)

model.minimize(model.max([t.end() for t in tasks]))

Minimum Collected Value​

Require a task to accumulate a minimum total from a step function:

# Sunlight intensity throughout the day
sunlight = model.step_function([
(0, 0), # 0h-6h: dark
(6, 3), # 6h-9h: morning
(9, 10), # 9h-15h: peak sunlight
(15, 5), # 15h-18h: afternoon
(18, 0), # 18h onwards: dark
])

# Solar charging task with variable length
charging = model.interval_var(length=(10, 50), name="charging")

# Must collect at least 100 units of sunlight
model.enforce(model.integral(sunlight, charging) >= 100)

Maintenance Windows​

Forbid task execution during scheduled maintenance:

# Machine status: 1 = available, 0 = maintenance
machine_status = model.step_function([
(0, 1), # 0-100: available
(100, 0), # 100-120: maintenance
(120, 1), # 120-200: available
(200, 0), # 200-220: maintenance
(220, 1), # 220 onwards: available
])

tasks = [model.interval_var(length=15) for _ in range(10)]

# Tasks cannot run during maintenance
for task in tasks:
task.forbid_extent(machine_status)

model.no_overlap(tasks)

Complete Example​

import optalcp as cp

model = cp.Model()

# Energy pricing (24-hour cycle)
energy_price = model.step_function([
(0, 5), # 0h-6h: low ($5/hour)
(6, 10), # 6h-9h: medium ($10/hour)
(9, 20), # 9h-17h: peak ($20/hour)
(17, 10), # 17h-22h: medium ($10/hour)
(22, 5), # 22h onwards: low ($5/hour)
])

# Machine availability
machine_hours = model.step_function([
(0, 0), # 0h-6h: closed
(6, 1), # 6h-22h: open
(22, 0), # 22h onwards: closed
])

tasks = [
model.interval_var(length=8, name=f"task_{i}")
for i in range(5)
]

# Tasks must run during machine hours
for task in tasks:
task.forbid_extent(machine_hours)

# Sequential execution
for i in range(len(tasks) - 1):
tasks[i].end_before_start(tasks[i + 1])

# Minimize total energy cost
total_cost = model.sum([
model.integral(energy_price, task) for task in tasks
])
model.minimize(total_cost)

result = model.solve()
if result.solution:
print(f"Total energy cost: ${result.objective}")
for task in tasks:
start = result.solution.get_start(task)
end = result.solution.get_end(task)
print(f"{task.name}: {start}-{end}")

Edge Cases​

Value Before First Step​

Before the first step, the function value is 0:

func = model.step_function([
# Implicitly: value is 0 before time 10
(10, 5), # From time 10: value is 5
(20, 10), # From time 20 onwards: value is 10
])

Tip: For forbid start, forbid end, and forbid extent, the constraint forbids times where the function is zero. If you don't define a step at time 0, tasks cannot start/end/run before the first defined step.

Empty Steps​

An empty step function is valid—the function is 0 everywhere:

func = model.step_function([]) # func(x) = 0 for all x

See Also​