Skip to main content

Objectives

An objective function defines what the solver should optimize. OptalCP supports minimization and maximization of integer expressions.

Creating Objectives

Create an objective using model.minimize() or model.maximize():

import optalcp as cp

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

# Minimize completion time
model.minimize(task.end())

# Maximize start time
model.maximize(task.start())

Shortcut Syntax

Integer expressions provide shortcut functions:

# These are equivalent:
task.end().minimize()
model.minimize(task.end())

# These are equivalent:
task.start().maximize()
model.maximize(task.start())

Single Objective

Only one objective is allowed per model. Setting a new objective replaces the previous one:

model.minimize(task1.end())  # First objective
model.minimize(task2.end()) # Replaces first objective
Pitfall: Don't Create Variables for Objectives

Minimize or maximize expressions directly—don't create intermediate variables.

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

# DON'T: Create a variable and constrain it to equal the expression
makespan = model.int_var(min=0, max=1000, name="makespan")
model.enforce(makespan == model.max([t.end() for t in tasks]))
model.minimize(makespan)

# DO: Minimize the expression directly
model.minimize(model.max([t.end() for t in tasks]))

# ESPECIALLY DON'T: Use upper-bound constraints
makespan = model.int_var(min=0, max=1000, name="makespan")
model.minimize(makespan)
for t in tasks:
model.enforce(t.end() <= makespan) # Hides the max() structure!

The problems with explicit variables:

  • Needless branching: The solver will branch on user-created variables, wasting search effort on a derived value.
  • Hidden structure: Using t.end() <= makespan constraints hides the fact that the objective is the maximum of task ends. The solver cannot apply specialized propagation for max.
  • Weaker bounds: The solver computes tighter bounds when it knows the objective structure directly.

See also Don't Create Variables for Expressions for the general principle.

Common Objective Patterns

Makespan

Minimize the completion time of all tasks:

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

# Makespan: latest end time
makespan = model.max([t.end() for t in tasks])
model.minimize(makespan)

Total Flow Time

Sum of completion times:

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

# Total flow time
total_flow = model.sum([t.end() for t in tasks])
model.minimize(total_flow)

Weighted Sum

Combine multiple criteria with weights:

tasks = [model.interval_var(length=10) for _ in range(5)]
weights = [5, 3, 2, 4, 1]

# Weighted completion time
weighted_sum = model.sum([
t.end() * w for t, w in zip(tasks, weights)
])
model.minimize(weighted_sum)

Tardiness

Minimize lateness with respect to deadlines:

tasks = [model.interval_var(length=10) for _ in range(5)]
deadlines = [100, 150, 120, 180, 200]

# Tardiness: max(0, completion - deadline)
tardiness = [
model.max2(0, task.end() - deadline)
for task, deadline in zip(tasks, deadlines)
]

total_tardiness = model.sum(tardiness)
model.minimize(total_tardiness)

Number of Tasks

Maximize the number of selected optional tasks:

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

# Count present tasks
num_selected = model.sum([t.presence() for t in tasks])
model.maximize(num_selected)

Multi-Objective Optimization

OptalCP does not support native multi-objective optimization, but you can combine objectives using weighted sums or lexicographic optimization:

Weighted Sum Approach

Combine objectives with weights:

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

# Makespan
makespan = model.max([t.end() for t in tasks])

# Total flow time
total_flow = model.sum([t.end() for t in tasks])

# Weighted combination
objective = makespan * 10 + total_flow
model.minimize(objective)

Lexicographic Optimization

Optimize objectives in order of priority:

# Step 1: Optimize primary objective
model.minimize(makespan)
result1 = model.solve()

if result1.solution:
# Step 2: Fix primary objective, optimize secondary
optimal_makespan = result1.objective
model.enforce(makespan <= optimal_makespan)

model.minimize(total_flow)
result2 = model.solve()

Absent Expressions in Objectives

Aggregations like sum, max, and min skip absent values. For example, max([absent, 5, absent]) equals 5. However, if the entire objective expression is absent, it is treated as the worst possible value: +∞ for minimization, −∞ for maximization.

This can happen when:

  • Minimizing a single optional interval's end: minimize(task.end()) where task is absent
  • Taking max or min of an empty set (all values are absent)

Use guard() to provide an explicit default:

task = model.interval_var(length=10, optional=True)

# If task is absent, end() is absent → worst value for minimization
model.minimize(task.end())

# With guard: if task is absent, use 0 instead
model.minimize(task.end().guard(0))

Satisfaction Problems

For satisfaction problems (finding any feasible solution without optimization), simply omit the objective. The solver automatically stops after finding the first feasible solution.

model = cp.Model()

# Add variables and constraints
# ...

# No objective - solver automatically stops after first solution
result = model.solve()

To find multiple solutions, explicitly set solutionLimit:

# Find up to 5 different solutions
result = model.solve({'solutionLimit': 5})

Reading Objective Values

result = model.solve()

if result.solution:
# Objective value
obj_value = result.objective
print(f"Objective: {obj_value}")

# Objective sense
print(f"Sense: {result.objective_sense}") # 'minimize' or 'maximize'

# Best bound (for optimization)
if result.objective_bound is not None:
print(f"Bound: {result.objective_bound}")
gap = abs(result.objective - result.objective_bound)
print(f"Gap: {gap}")

Complete Example

import optalcp as cp

model = cp.Model()

# Job shop scheduling: 3 jobs, 2 machines
jobs = []
for job_id in range(3):
job = []
for op_id in range(2):
task = model.interval_var(
length=10 + job_id * 5 + op_id * 3,
name=f"job{job_id}_op{op_id}"
)
job.append(task)
jobs.append(job)

# Operations in sequence
job[0].end_before_start(job[1])

# Machine constraints
machine1_tasks = [jobs[i][0] for i in range(3)]
machine2_tasks = [jobs[i][1] for i in range(3)]
model.no_overlap(machine1_tasks)
model.no_overlap(machine2_tasks)

# Objective: minimize makespan
makespan = model.max([jobs[i][1].end() for i in range(3)])
model.minimize(makespan)

# Solve
result = model.solve()
if result.solution:
print(f"Makespan: {result.objective}")
for i, job in enumerate(jobs):
for j, task in enumerate(job):
start = result.solution.get_start(task)
end = result.solution.get_end(task)
print(f"Job {i} Op {j}: {start}-{end}")

See Also