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 xix<xi+1ynif xxnf(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=se1f(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