Skip to main content

Expressions

IntExpr and BoolExpr represent computed values derived from variables. They are used to build constraints, objectives, and derived quantities.

Overview

TypeRepresentsSources
IntExprInteger valueIntVar, IntervalVar.start/end/length(), arithmetic on expressions
BoolExprBoolean valueBoolVar, IntervalVar.presence(), comparisons, boolean operations
import optalcp as cp

model = cp.Model()
task = model.interval_var(length=10, name="task")
x = model.int_var(min=0, max=100, name="x")
b = model.bool_var(name="b")

# IntExpr sources
start = task.start() # From interval
end = task.end()
length = task.length()
expr = x + 10 # From arithmetic

# BoolExpr sources
present = task.presence() # From interval
cond = x > 50 # From comparison
flag = b | (x < 10) # From boolean ops

Operators

Arithmetic (IntExpr)

# Binary operators
x + y # Addition
x - y # Subtraction
x * 2 # Multiplication (by constant)
x // 2 # Integer division (by constant)

# Unary operators
-x # Negation
abs(x - y) # Absolute value

Comparison (IntExpr → BoolExpr)

x < y          # Less than
x <= y # Less than or equal
x == y # Equal
x != y # Not equal
x >= y # Greater than or equal
x > y # Greater than

# Use in constraints
model.enforce(task.end() <= 100)
model.enforce(x + y == 50)

# IMPORTANT: Python's operator precedence requires parentheses here
b = model.bool_var(name="b")
model.enforce(b == (x > 10)) # Parentheses required!
# Without: b == x > 10 parses as (b == x) > 10
# This is standard Python behavior, not specific to OptalCP

Boolean (BoolExpr)

~b             # NOT
b1 | b2 # OR
b1 & b2 # AND
b1.implies(b2) # Implication (equivalent to ~b1 | b2)

# Use in constraints
model.enforce(b1 | b2) # At least one
model.enforce(~(b1 & b2)) # Not both
model.enforce(b1.implies(b2)) # If b1 then b2

Absent Value Propagation

When an optional variable is absent, most expressions involving it become absent—but there are important exceptions.

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

# If task is absent:
task.end() # absent
task.end() + 10 # absent (arithmetic propagates)
task.end() + 10 <= 100 # absent

# Exceptions - these are NOT absent:
task.presence() # false (not absent!)
task.end().guard(0) # 0 (guard provides default)
model.sum([task.end(), 5]) # 5 (aggregations skip absent)
model.max([task.end(), 5]) # 5 (aggregations skip absent)
model.identity(task.end(), other) # true or false (never absent)
Pitfall: Arithmetic vs Aggregation

Arithmetic and aggregation handle absent values differently:

# x is absent
x + 5 # absent (arithmetic propagates)
model.sum([x, 5]) # 5 (aggregation skips absent)
Constraint Semantics

See Enforcing BoolExpr Constraints for how absent values affect constraint satisfaction.

Guard: Default for Absent

guard() provides a default value when an expression is absent:

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

# If task is absent, use 0 instead of absent
guarded_end = task.end().guard(0)

# Useful in objectives: absent tasks contribute 0
model.minimize(task.end().guard(0))

# Useful in arithmetic (where absent would propagate)
total = task1.end().guard(0) + task2.end().guard(0)

Identity: Equality Including Presence

identity() constrains two expressions to have the same value and the same presence status:

# Equality vs identity:
# - x == 0 is absent when x is absent (constraint satisfied)
# - identity(x, 0) is false when x is absent (constraint violated)

opt = model.int_var(min=0, max=10, optional=True, name="opt")
model.identity(opt, other) # Same value AND same presence

Aggregation

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

# Sum of present values (absent excluded)
total_length = model.sum([t.length() for t in tasks])

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

# Minimum start time
earliest = model.min([t.start() for t in tasks])

Absent handling in aggregations: Absent values are skipped. Edge cases:

  • sum([]) or all absent → 0
  • min([]) or all absent → absent
  • max([]) or all absent → absent

BoolExpr as IntExpr

BoolExpr is also an IntExpr with value 1 for true and 0 for false. This enables counting and arithmetic with booleans:

b1 = model.bool_var(name="b1")
b2 = model.bool_var(name="b2")
b3 = model.bool_var(name="b3")

# Count true values
count = b1 + b2 + b3 # 0 to 3

# Weighted sum
cost = b1 * 100 + b2 * 50 + b3 * 25

# At-least-k constraints
model.enforce(b1 + b2 + b3 >= 2) # At least 2 true

# At-most-k constraints
model.enforce(b1 + b2 + b3 <= 1) # At most 1 true

# Count present optional tasks
tasks = [model.interval_var(length=10, optional=True) for _ in range(5)]
num_present = model.sum([t.presence() for t in tasks])
model.enforce(num_present >= 3) # At least 3 tasks scheduled

Objectives

Any IntExpr can be used as an objective:

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

# Two equivalent ways to set objective
model.minimize(task.end())
task.end().minimize() # Fluent style

# Maximize
model.maximize(task.start())
task.start().maximize() # Fluent style

# Complex objectives
makespan = model.max([t.end() for t in tasks])
total_cost = model.sum([t.length() * cost[i] for i, t in enumerate(tasks)])
model.minimize(makespan * 1000 + total_cost) # Weighted combination

Complete Example

import optalcp as cp

model = cp.Model()

# Optional tasks with different lengths and values
tasks = [
model.interval_var(length=10 + i * 5, optional=True, name=f"task_{i}")
for i in range(5)
]
values = [50, 30, 40, 20, 60] # Value of each task

# Tasks cannot overlap (those that are present)
model.no_overlap([t for t in tasks])

# At least 3 tasks must be scheduled
num_present = model.sum([t.presence() for t in tasks])
model.enforce(num_present >= 3)

# Deadline: all tasks must finish by time 50
for task in tasks:
model.enforce(task.end() <= 50)

# Maximize total value of scheduled tasks
total_value = model.sum([t.presence() * values[i] for i, t in enumerate(tasks)])
model.maximize(total_value)

result = model.solve()
if result.solution:
for task in tasks:
if result.solution.is_present(task):
start = result.solution.get_start(task)
end = result.solution.get_end(task)
print(f"{task.name}: {start}-{end}")
else:
print(f"{task.name}: absent")
print(f"Total value: {result.objective}")

See Also

  • Variables - IntVar and BoolVar basics
  • Intervals - IntervalVar expressions (start, end, length, presence)
  • Constraints - Using expressions in constraints