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​