Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/make_utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ function documentation_structure()
"tutorials/linear/multi_objective_project_planning.md",
"tutorials/linear/sudoku.md",
"tutorials/linear/n-queens.md",
"tutorials/linear/sports_scheduling.md",
"tutorials/linear/constraint_programming.md",
"tutorials/linear/callbacks.md",
"tutorials/linear/lp_sensitivity.md",
Expand Down
174 changes: 174 additions & 0 deletions docs/src/tutorials/linear/sports_scheduling.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Copyright (c) 2026 Oscar Dowson and contributors #src
# #src
# Permission is hereby granted, free of charge, to any person obtaining a copy #src
# of this software and associated documentation files (the "Software"), to deal #src
# in the Software without restriction, including without limitation the rights #src
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #src
# copies of the Software, and to permit persons to whom the Software is #src
# furnished to do so, subject to the following conditions: #src
# #src
# The above copyright notice and this permission notice shall be included in all #src
# copies or substantial portions of the Software. #src
# #src
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #src
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #src
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #src
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #src
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #src
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #src
# SOFTWARE. #src

# # Sport scheduling

# **This tutorial was adapted from an [example written by Eli Towle](https://publicsectororcourse.wordpress.com/2016/05/09/optimization-in-julia/)
# in 2016 as part of an operations research course at the University of
# Wisconsin-Madison.**

# The purpose of this tutorial is to demonstrate a simple model for scheduling
# round-robin tournaments. As teams, it uses the [Big 10](https://en.wikipedia.org/wiki/Big_Ten_Conference).
# (You might notice that there are more than 10 teams. Our example was also
# written before the expansion of the Conference in 2024.)

# ## Required packages

# This tutorial uses the following packages:

using JuMP
import HiGHS

# ## Basic model

# Here are the teams in our tournament:

M = [
#!format:off
"Ind", "UMD", "UMich", "MSU", "OSU", "Penn", "Rtgrs", "Ill", "Iowa", "UMN",
"UNL", "NU", "Purd", "UW",
#!format: on
];

# For each team to play each other exactly once, we need the number of teams - 1
# weeks:

T = length(M) - 1;

# Now we create a JuMP model to build our optimzation problem:

model = Model(HiGHS.Optimizer);

# Variable: a binary which is true for `x[m,n,t]` if team `m` is playing `n` at
# home in week `t`.

@variable(model, x[M, M, 1:T], Bin);

# Constraint: each team `m` can never play themselves.

@constraint(model, [m in M, t in 1:T], x[m, m, t] == 0);

# Constraint: each team `m` can play at most once per day

@constraint(model, [m in M, t in 1:T], sum(x[m, :, t]) + sum(x[:, m, t]) <= 1);

# Constraint: every team `m` plays at least half home games

@constraint(model, [m in M], div(T, 2) <= sum(x[m, :, :]) <= div(T, 2) + 1);

# Constraint: no more than two away games in any three-game window

@constraint(model, [m in M, t in 1:(T-2)], sum(x[:, m, t:(t+2)]) <= 2);

# Constraint: every team must play every other team exactly once

@constraint(
model,
[m in M, n in M; m != n],
sum(x[m, n, :]) + sum(x[n, m, :]) == 1,
);

# Now we can solve our model:

set_silent(model)
optimize!(model)
assert_is_solved_and_feasible(model)

# and print the schedule:

function print_schedule(M::Vector{String}, T::Int, Y::AbstractArray{Bool})
println("Week ", join(rpad.(M, 6), ' '))
for t in 1:T
print(rpad(t, 5))
for m in M, n in M
if Y[m, n, t]
print(rpad(n, 7))
elseif Y[n, m, t]
print("@", rpad(n, 6))
end
end
println()
end
return
end

Y = round.(Bool, value.(x))
print_schedule(M, T, Y)

# This schedule is okay, but it features a large number of back-to-back away
# games. Let's count them:

number_of_back_to_back_away_games =
sum(round(Int, value(sum(x[:, m, (t-1):t]))) == 2 for m in M, t in 2:T)

# A better schedule would minimize this quantity.

# ## Minimizing the number of back-to-back away games

# To minimize the number of back-to-back away games, we modify our model.

# Variable: a binary which is true for `y[m,t]` if team `m` is playing
# back-to-back away games in week `t`.

@variable(model, y[M, 2:T], Bin);

# Objective: minimize the number of back-to-back away games

@objective(model, Min, sum(y));

# Constraint: count back-to-back away games

@constraint(model, [m in M, t in 2:T], y[m, t] >= sum(x[:, m, (t-1):t]) - 1);

# Now we can solve our model. However, this problem is actually very difficult
# to solve to optimality. Rather than wait a very long time, we set a time limit
# so that this documentation doesn't take too long to build:

set_time_limit_sec(model, 30.0)

# We're also going to set a start value based on the previous solution:

set_start_value.(x, Y)
for m in M, t in 2:T
set_start_value(y[m, t], sum(Y[:, m, (t-1):t]) - 1)
end

# Now we can optimize:

optimize!(model)

# Because we hit a time limit, we can't use [`assert_is_solved_and_feasible`](@ref),
# but we still check that we found a feasible primal solution:

@assert termination_status(model) == TIME_LIMIT
@assert primal_status(model) == FEASIBLE_POINT

# This solution has fewer back-to-back away games:

number_of_back_to_back_away_games =
sum(round(Int, value(sum(x[:, m, (t-1):t]))) == 2 for m in M, t in 2:T)

# And the final schedule is:

Y = round.(Bool, value.(x))
print_schedule(M, T, Y)

# Try running the model for longer. What is the smallest number of back-to-back
# away games you can find?
1 change: 1 addition & 0 deletions docs/styles/config/vocabularies/JuMP/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ Tanneau
Teghem
Tejada
Tillmann
Towle
Ulungu
Vanaret
Vandenberghe
Expand Down
Loading