Interval Variables¶
In addition to Boolean, integers and floats, Hexaly Optimizer offers higher-level decision variables: intervals and optional intervals, presented here, as well as sets and lists.
Mathematical principle and creation operator¶
Interval decisions are used to model the time range of an event. They are
declared using the interval operator:
x <- interval(minStart, maxEnd);
x = model.interval(min_start, max_end)
HxExpression x = model.intervalVar(minStart, maxEnd);
HxExpression x = model.Interval(minStart, maxEnd);
HxExpression x = model.intervalVar(minStart, maxEnd);
with two integer operands representing the minimum start and maximum end of the
decision respectively. The value of an interval decision is an integer interval
[a, b), whose start a is included and whose end b is excluded,
verifying a >= minStart, b <= maxEnd, and a <= b.
Based on this definition, the following examples represent valid values for an interval decision variable of minimum start 0 and maximum end 5:
[0, 1) // Singleton (0 is included, but 1 is excluded)
[1, 4)
[2, 2) // Degenerate interval (zero length)
[0, 5) // Full interval (maximum length)
On the contrary, the following examples are invalid values:
[3, 6) // Out of bounds for an interval of maximum end 5
[2, 0) // An interval's start is lower or equal to its end
Optional intervals¶
Similarly to regular intervals, optional intervals are declared using the
optionalInterval operator, with two operands representing the minimum
start and maximum end:
x <- optionalInterval(minStart, maxEnd);
x = model.optional_interval(min_start, max_end)
HxExpression x = model.optionalIntervalVar(minStart, maxEnd);
HxExpression x = model.OptionalInterval(minStart, maxEnd);
HxExpression x = model.optionalIntervalVar(minStart, maxEnd);
Contrary to classical interval decision variables, optional intervals can also
be void: the value of an optional interval decision variable can either be
“absent” (void) or included in [minStart, maxEnd). When present, start is
inclusive and end is exclusive. When absent, start and end are undefined.
Setting and retrieving values¶
As mentioned above, the value of an interval decision is an integer interval.
This value is obtained with the syntax x.value in Hexaly Modeler, with
x.value or x.get_interval_value() in Python, with
getIntervalValue() in C++ and Java, and with GetIntervalValue() in C#.
It returns an object of type HxInterval, that can be read through the
methods: start, end, count, isVoid:
println(x.value.start()); // Current start of the interval
println(x.value.end()); // Current end of the interval
println(x.value.count()); // Current size of the interval
println(x.value.isVoid()); // 1 if the interval is absent (void), or 0 if it is present
println(x.value); // Print the content of the interval
interval = x.value
print(interval.start()) # Current start of the interval
print(interval.end()) # Current end of the interval
print(interval.count()) # Current size of the interval
print(interval.is_void()) # True if the interval is absent (void), or False if it is present
print(interval) # Print the content of the interval
HxInterval interval = x.getIntervalValue();
std::cout << interval.getStart() << std::endl; // Current start of the interval
std::cout << interval.getEnd() << std::endl; // Current end of the interval
std::cout << interval.count() << std::endl; // Current size of the interval
std::cout << interval.isVoid() << std::endl; // 1 if the interval is absent (void), or 0 if it is present
std::cout << interval.toString() << std::endl; // Print the content of the interval
HxInterval interval = x.GetIntervalValue();
Console.WriteLine(interval.Start()); // Current start of the interval
Console.WriteLine(interval.End()); // Current end of the interval
Console.WriteLine(interval.Count()); // Current size of the interval
Console.WriteLine(interval.IsVoid()); // True if the interval is absent (void), or False if it is present
Console.WriteLine(interval); // Print the content of the interval
HxInterval interval = x.getIntervalValue();
System.out.println(interval.getStart()); // Current start of the interval
System.out.println(interval.getEnd()); // Current end of the interval
System.out.println(interval.count()); // Current size of the interval
System.out.println(interval.isVoid()); // true if the interval is absent (void), or false if it is present
System.out.println(interval); // Print the content of the interval
Setting the value of an interval variable can be done by creating a new
HxInterval object. When using Hexaly Modeler, this is done by defining an
integer range using the a...b syntax. When a <= b, this creates a
regular interval [a, b), and when a > b, this creates a void interval:
x.value = 0...4; // Interval [0, 4)
x.value = 1...1; // Interval [1, 1)
x.value = 0...-1; // Void interval
x.set_interval_value(HxInterval(0, 4)) # Interval [0, 4)
x.set_interval_value(HxInterval(1)) # Interval [1, 1)
x.set_interval_value(HxInterval()) # Void interval
x.setIntervalValue(HxInterval(0, 4)); // Interval [0, 4)
x.setIntervalValue(HxInterval(1)); // Interval [1, 1)
x.setIntervalValue(HxInterval()); // Void interval
x.SetIntervalValue(new HxInterval(0, 4)); // Interval [0, 4)
x.SetIntervalValue(new HxInterval(1)); // Interval [1, 1)
x.SetIntervalValue(new HxInterval()); // Void interval
x.setIntervalValue(new HxInterval(0, 4)); // Interval [0, 4)
x.setIntervalValue(new HxInterval(1)); // Interval [1, 1)
x.setIntervalValue(new HxInterval()); // Void interval
With Hexaly Optimizer APIs, this is done by using HxInterval’s constructor.
It can be used with zero operand to create a void interval, with a single
operand a to create a degenerate interval [a, a), or with two operands
a <= b to create an interval [a, b).
Operators on intervals and optional intervals¶
Unary operators¶
The start operator returns the start of a non-void interval (included), or
NaN if applied to a void interval. For example, the following lines compute the
earliest start time of a given set of tasks:
tasks[i in 0...nbTasks] <- interval(0, horizon);
earliestStart <- min[i in 0...nbTasks](start(tasks[i]));
tasks = [model.interval(0, horizon) for i in range(nb_tasks)]
earliest_start = model.min(model.start(tasks[i]) for i in range(nb_tasks))
std::vector<HxExpression> tasks(nbTasks);
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression earliestStart = model.min();
for (int i = 0; i < nbTasks; ++i) {
earliestStart.addOperand(model.start(tasks[i]));
}
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.Interval(0, horizon);
}
HxExpression earliestStart = model.Min();
for (int i = 0; i < nbTasks; ++i) {
earliestStart.AddOperand(model.Start(tasks[i]));
}
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression earliestStart = model.min();
for (int i = 0; i < nbTasks; ++i) {
earliestStart.addOperand(model.start(tasks[i]));
}
The end operator returns the end of a non-void interval (excluded), or NaN
if applied to a void interval. For example, the following lines define a set of
tasks as interval decisions and sets the objective function as the makespan
(latest end time):
tasks[i in 0...nbTasks] <- interval(0, horizon);
makespan <- max[i in 0...nbTasks](end(tasks[i]));
minimize makespan;
tasks = [model.interval(0, horizon) for i in range(nb_tasks)]
makespan = model.max(model.end(tasks[i]) for i in range(nb_tasks))
model.minimize(makespan)
std::vector<HxExpression> tasks(nbTasks);
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression makespan = model.max();
for (int i = 0; i < nbTasks; ++i) {
makespan.addOperand(model.end(tasks[i]));
}
model.minimize(makespan);
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.Interval(0, horizon);
}
HxExpression makespan = model.Max();
for (int i = 0; i < nbTasks; ++i) {
makespan.AddOperand(model.End(tasks[i]));
}
model.Minimize(makespan);
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression makespan = model.max();
for (int i = 0; i < nbTasks; ++i) {
makespan.addOperand(model.end(tasks[i]));
}
model.minimize(makespan);
The length operator returns the length of a non-void interval, or NaN if
applied to a void interval. If x is an interval decision, length(x) is
equivalent to end(x) - start(x). For example, the following constraint
ensures that the length of the interval is equal to the duration of the task it
represents:
task <- interval(0, horizon);
constraint length(task) == duration;
task = model.interval(0, horizon)
model.constraint(model.length(task) == duration)
HxExpression task = model.intervalVar(0, horizon);
model.constraint(model.length(task) == duration);
HxExpression task = model.Interval(0, horizon);
model.Constraint(model.Length(task) == duration);
HxExpression task = model.intervalVar(0, horizon);
model.constraint(model.eq(model.length(task), duration));
The count operator returns the number of elements inside the interval. It is
the set-based counterpart of the length operator: both return the same value
for non-void intervals, but the count operator returns 0 when applied to a
void interval (instead of NaN). Indeed, on the one hand, the length operator
regards intervals as time intervals, and considers that the length of a void
interval is undefined, and on the other hand, the count operator regards
intervals as collections of consecutive integers, and considers that the number
of elements inside a void interval is zero. For example, the following lines
constraint the total size of subtasks in a preemptive scheduling problem to be
equal to the whole duration of the task:
subtasks[k in 0...nbSubtasks] <- optionalInterval(0, horizon);
constraint sum[k in 0...nbSubtasks](count(subtasks[k])) == duration;
subtasks = [model.optional_interval(0, horizon) for k in range(nb_subtasks)]
total_size = model.sum(model.count(subtasks[k]) for k in range(nb_subtasks))
model.constraint(total_size == duration)
std::vector<HxExpression> subtasks(nbSubtasks);
for (int k = 0; k < nbSubtasks; ++k) {
subtasks[k] = model.optionalIntervalVar(0, horizon);
}
HxExpression totalSize = model.sum();
for (int k = 0; k < nbSubtasks; ++k) {
totalSize.addOperand(model.count(subtasks[k]));
}
model.constraint(totalSize == duration);
HxExpression[] subtasks = new HxExpression[nbSubtasks];
for (int k = 0; k < nbSubtasks; ++k) {
subtasks[k] = model.OptionalInterval(0, horizon);
}
HxExpression totalSize = model.Sum();
for (int k = 0; k < nbSubtasks; ++k) {
totalSize.AddOperand(model.Count(subtasks[k]));
}
model.Constraint(totalSize == duration);
HxExpression[] subtasks = new HxExpression[nbSubtasks];
for (int k = 0; k < nbSubtasks; ++k) {
subtasks[k] = model.optionalIntervalVar(0, horizon);
}
HxExpression totalSize = model.sum();
for (int k = 0; k < nbSubtasks; ++k) {
totalSize.addOperand(model.count(subtasks[k]));
}
model.constraint(model.eq(totalSize, duration));
The presence operator returns 1 if the interval is present (non-void), and
0 if it is void. For example, the following lines ensure that we constrain the
size of an interval to be equal to the corresponding task’s duration only if it
is present:
task <- optionalInterval(0, horizon);
constraint presence(task) ? length(task) == duration : true;
task = model.optional_interval(0, horizon)
model.constraint(model.iif(model.presence(task), model.length(task) == duration, True))
HxExpression task = model.optionalIntervalVar(0, horizon);
model.constraint(model.iif(model.presence(task), model.length(task) == duration, true));
HxExpression task = model.OptionalInterval(0, horizon);
model.Constraint(model.Or(model.Presence(task) == 0, model.Length(task) == duration));
HxExpression task = model.optionalIntervalVar(0, horizon);
model.constraint(model.iif(model.presence(task), model.eq(model.length(task), duration), 1));
Binary operators¶
The lt (resp. gt) operator, with shortcut < (resp. >) returns
1 if the first interval is strictly before (resp. after) the second one: it
returns 1 if the end of the first interval is lower of equal to the start of the
second one, and 0 if not. The expression x < y is equivalent to
end(x) <= start(y). It returns NaN if at least one operand is void. For
example, the following lines express a precedence constraint between two
intervals:
x <- interval(0, horizon);
y <- interval(0, horizon);
constraint x < y;
x = model.interval(0, horizon)
y = model.interval(0, horizon)
model.constraint(x < y)
HxExpression x = model.intervalVar(0, horizon);
HxExpression y = model.intervalVar(0, horizon);
model.constraint(x < y);
HxExpression x = model.Interval(0, horizon);
HxExpression y = model.Interval(0, horizon);
model.Constraint(x < y);
HxExpression x = model.intervalVar(0, horizon);
HxExpression y = model.intervalVar(0, horizon);
model.constraint(model.lt(x, y));
The contains operator takes an interval as a first operand, and an interval
or an integer as a second operand. It returns 1 if the second operand if fully
contained within the first one. A non-void interval b is considered fully
contained within a non-void interval a if b’s start is greater or equal
to a’s start and if b’s end is lower of equal to a’s end. If the
first operand is a void interval, the contains operator returns 0. If the
second operand is a void interval, the contains operator returns NaN. For
example, the following lines count the number of tasks that are active at a
given time t:
tasks[i in 0...nbTasks] <- interval(0, horizon);
nbActiveTasks <- sum[i in 0...nbTasks](contains(tasks[i], t));
tasks = [model.interval(0, horizon) for i in range(nb_tasks)]
nb_active_tasks = model.sum(model.contains(tasks[i], t) for i in range(nb_tasks))
std::vector<HxExpression> tasks(nbTasks);
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression nbActiveTasks = model.sum();
for (int i = 0; i < nbTasks; ++i) {
nbActiveTasks.addOperand(model.contains(tasks[i], t));
}
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.Interval(0, horizon);
}
HxExpression nbActiveTasks = model.Sum();
for (int i = 0; i < nbTasks; ++i) {
nbActiveTasks.AddOperand(model.Contains(tasks[i], t));
}
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression nbActiveTasks = model.sum();
for (int i = 0; i < nbTasks; ++i) {
nbActiveTasks.addOperand(model.contains(tasks[i], t));
}
The distinct operator takes as input an interval and a lambda function,
and returns the unordered set of distinct values among all the values returned
by the function. For example, the following constraint forces a machine to
produce at most two distinct types of product within the time span of a task:
task <- interval(0, 10);
productTypes = { 0, 1, 2, 1, 2, 0, 0, 1, 2, 1 };
constraint count(distinct(task, i => productTypes[i])) <= 2;
task = model.interval(0, 10)
product_types = model.array([0, 1, 2, 1, 2, 0, 0, 1, 2, 1])
product_type = model.lambda_function(lambda i: product_types[i])
model.constraint(model.count(model.distinct(task, product_type)) <= 2)
HxExpression task = model.intervalVar(0, 10);
std::vector<hxint> productTypesData = {0, 1, 2, 1, 2, 0, 0, 1, 2, 1};
HxExpression productTypes = model.array(productTypesData.begin(), productTypesData.end());
HxExpression productType = model.createLambdaFunction(
[&productTypes](HxExpression i) { return productTypes[i]; });
model.constraint(model.count(model.distinct(task, productType)) <= 2);
HxExpression task = model.Interval(0, 10);
HxExpression productTypes = model.Array(new long[] { 0, 1, 2, 1, 2, 0, 0, 1, 2, 1 });
HxExpression productType = model.LambdaFunction(i => productTypes[i]);
model.Constraint(model.Count(model.Distinct(task, productType)) <= 2);
HxExpression task = model.intervalVar(0, 10);
HxExpression productTypes = model.array(new long[] { 0, 1, 2, 1, 2, 0, 0, 1, 2, 1 });
HxExpression productType = model.lambdaFunction(i -> model.at(productTypes, i));
model.constraint(model.leq(model.count(model.distinct(task, productType)), 2));
N-ary operators¶
The hull operator returns the smallest interval containing all the intervals
given as arguments. If all operands are void, it returns a void interval. If at
least of operand is non-void, it returns an interval whose start is equal to the
smallest start among all non-void operands, and whose end is equal to the
largest end among all non-void operands. For example, the following lines define
a set of tasks as interval decisions and defines the objective function as the
makespan (latest end time):
tasks[i in 0...nbTasks] <- interval(0, horizon);
makespan <- end(hull[i in 0...nbTasks](tasks[i]));
minimize makespan;
tasks = [model.interval(0, horizon) for i in range(nb_tasks)]
makespan = model.end(model.hull(tasks))
model.minimize(makespan)
std::vector<HxExpression> tasks(nbTasks);
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression taskArray = model.array(tasks.begin(), tasks.end());
HxExpression makespan = model.end(model.hull(taskArray));
model.minimize(makespan);
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.Interval(0, horizon);
}
HxExpression makespan = model.End(model.Hull(tasks));
model.Minimize(makespan);
HxExpression[] tasks = new HxExpression[nbTasks];
for (int i = 0; i < nbTasks; ++i) {
tasks[i] = model.intervalVar(0, horizon);
}
HxExpression makespan = model.end(model.hull(tasks));
model.minimize(makespan);
The intersection operator returns the biggest interval fully present in all
operands. If at least one operand if void, it returns a void interval. For
example, the following lines maximize the intersection between the time span of
a task and a preferred production period:
task <- interval(0, horizon);
preferredRange <- prefStart...prefEnd;
maximize intersection(task, preferredRange);
task = model.interval(0, horizon)
preferred_range = model.range(pref_start, pref_end)
model.maximize(model.intersection(task, preferred_range))
HxExpression task = model.intervalVar(0, horizon);
HxExpression preferredRange = model.range(prefStart, prefEnd);
model.maximize(model.intersection(task, preferredRange));
HxExpression task = model.Interval(0, horizon);
HxExpression preferredRange = model.Range(prefStart, prefEnd);
model.Maximize(model.Intersection(task, preferredRange));
HxExpression task = model.intervalVar(0, horizon);
HxExpression preferredRange = model.range(prefStart, prefEnd);
model.maximize(model.intersection(task, preferredRange));
Modeling with intervals¶
Intervals are used to model the time span of tasks in scheduling problems. Optional intervals model optional tasks, which are often encountered in multi-mode, prize-collecting, or preemptive scheduling problems.
When modeling disjunctive scheduling problems, interval decisions are often paired with list decisions. Indeed, it is often useful to know the order in which the tasks are processed on a disjunctive resource, to access the task in position i, or the neighboring tasks of a given task. In this case, the list decisions represent the order in which the tasks are processed on each machine.
Detailed scheduling examples using interval (and list) decision variables are available in our code templates.