From 6c5dd63a090456d08db16096cb1daedeb1685412 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 16:00:52 +1200 Subject: [PATCH 1/9] [docs] add sports scheduling tutorial --- docs/make_utilities.jl | 1 + .../src/tutorials/linear/sports_scheduling.jl | 162 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 docs/src/tutorials/linear/sports_scheduling.jl diff --git a/docs/make_utilities.jl b/docs/make_utilities.jl index 80961c53bbd..1c9de8f2f5d 100644 --- a/docs/make_utilities.jl +++ b/docs/make_utilities.jl @@ -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", diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl new file mode 100644 index 00000000000..d01ee5a9bc4 --- /dev/null +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -0,0 +1,162 @@ +# Copyright (c) 2019 Arpit Bhatia 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 = ["Ind", "UMD", "UMich", "MSU", "OSU", "Penn", "Rtgrs", "Ill", "Iowa", "UMN", "UNL", "NU", "Purd", "UW"] + +# 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, + # [(i, m) in enumerate(M), n in M[i+1:end]], + [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, T, x) + Y = round.(Bool, value.(x)) + 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 + +print_schedule(M, T, x) + +# 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 bette 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: minimze 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, 20.0) +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: + +print_schedule(M, T, x) + +# Try running the model for longer. What is the smallest number of back-to-back +# away games you can find? From 1d3447767501f70cf8bf9a38dd87bb0d6de318a7 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 16:05:03 +1200 Subject: [PATCH 2/9] Fix formatting --- .../src/tutorials/linear/sports_scheduling.jl | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index d01ee5a9bc4..08ce45f456f 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -1,4 +1,4 @@ -# Copyright (c) 2019 Arpit Bhatia and contributors #src +# 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 @@ -40,7 +40,12 @@ import HiGHS # Here are the teams in our tournament: -M = ["Ind", "UMD", "UMich", "MSU", "OSU", "Penn", "Rtgrs", "Ill", "Iowa", "UMN", "UNL", "NU", "Purd", "UW"] +#!format:off +M = [ + "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: @@ -57,19 +62,19 @@ model = Model(HiGHS.Optimizer) # Constraint: each team `m` can never play themselves. -@constraint(model, [m in M, t in 1:T], x[m,m,t] == 0) +@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(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(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(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 @@ -77,7 +82,7 @@ model = Model(HiGHS.Optimizer) model, # [(i, m) in enumerate(M), n in M[i+1:end]], [m in M, n in M; m != n], - sum(x[m,n,:]) + sum(x[n,m,:]) == 1 + sum(x[m, n, :]) + sum(x[n, m, :]) == 1 ) # Now we can solve our model: @@ -94,9 +99,9 @@ function print_schedule(M, T, x) for t in 1:T print(rpad(t, 5)) for m in M, n in M - if Y[m,n,t] + if Y[m, n, t] print(rpad(n, 7)) - elseif Y[n,m,t] + elseif Y[n, m, t] print("@", rpad(n, 6)) end end @@ -110,10 +115,8 @@ print_schedule(M, T, x) # 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 -) +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 bette schedule would minimize this quantity. @@ -132,7 +135,7 @@ number_of_back_to_back_away_games = sum( # 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) +@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 @@ -149,10 +152,8 @@ optimize!(model) # 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 -) +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: From 9a9f0ca32d51cbc52be54f3c6d8b15ebfc8e650e Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 16:39:20 +1200 Subject: [PATCH 3/9] Update --- docs/src/tutorials/linear/sports_scheduling.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index 08ce45f456f..d0ca3065329 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -49,6 +49,7 @@ M = [ # 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: @@ -80,9 +81,8 @@ model = Model(HiGHS.Optimizer) @constraint( model, - # [(i, m) in enumerate(M), n in M[i+1:end]], [m in M, n in M; m != n], - sum(x[m, n, :]) + sum(x[n, m, :]) == 1 + sum(x[m, n, :]) + sum(x[n, m, :]) == 1, ) # Now we can solve our model: From 8750e4ddf607ef7d96cd9ae119d05cf21a071a61 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 19:55:52 +1200 Subject: [PATCH 4/9] Update --- docs/src/tutorials/linear/sports_scheduling.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index d0ca3065329..7da836ffe8e 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -118,7 +118,7 @@ print_schedule(M, T, x) 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 bette schedule would minimize this quantity. +# A better schedule would minimize this quantity. # ## Minimizing the number of back-to-back away games From e1bf147a13f13dbc521863de59c708a6944b67cb Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 14 May 2026 08:50:16 +1200 Subject: [PATCH 5/9] Update --- .../src/tutorials/linear/sports_scheduling.jl | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index 7da836ffe8e..c8b3dd50b5d 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -40,42 +40,42 @@ import HiGHS # Here are the teams in our tournament: -#!format:off M = [ + #!format:off "Ind", "UMD", "UMich", "MSU", "OSU", "Penn", "Rtgrs", "Ill", "Iowa", "UMN", "UNL", "NU", "Purd", "UW", -] -#!format: on + #!format: on +]; # For each team to play each other exactly once, we need the number of teams - 1 # weeks: -T = length(M) - 1 +T = length(M) - 1; # Now we create a JuMP model to build our optimzation problem: -model = Model(HiGHS.Optimizer) +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) +@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(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(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(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(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 @@ -83,7 +83,7 @@ model = Model(HiGHS.Optimizer) model, [m in M, n in M; m != n], sum(x[m, n, :]) + sum(x[n, m, :]) == 1, -) +); # Now we can solve our model: @@ -127,15 +127,15 @@ number_of_back_to_back_away_games = # 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) +@variable(model, y[M, 2:T], Bin); # Objective: minimze the number of back-to-back away games -@objective(model, Min, sum(y)) +@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) +@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 From 9261486edbad90967dad45955bf3171507c2cca5 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 14 May 2026 10:15:38 +1200 Subject: [PATCH 6/9] Update --- docs/src/tutorials/linear/sports_scheduling.jl | 2 +- docs/styles/config/vocabularies/JuMP/accept.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index c8b3dd50b5d..e2d0c2cf9eb 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -129,7 +129,7 @@ number_of_back_to_back_away_games = @variable(model, y[M, 2:T], Bin); -# Objective: minimze the number of back-to-back away games +# Objective: minimize the number of back-to-back away games @objective(model, Min, sum(y)); diff --git a/docs/styles/config/vocabularies/JuMP/accept.txt b/docs/styles/config/vocabularies/JuMP/accept.txt index 9c2a50e2c5c..f07befe5272 100644 --- a/docs/styles/config/vocabularies/JuMP/accept.txt +++ b/docs/styles/config/vocabularies/JuMP/accept.txt @@ -300,6 +300,7 @@ Tanneau Teghem Tejada Tillmann +Towle Ulungu Vanaret Vandenberghe From c0069c66956b126482fc149afe738f810f8b682b Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 14 May 2026 11:02:50 +1200 Subject: [PATCH 7/9] Update --- .../src/tutorials/linear/sports_scheduling.jl | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index e2d0c2cf9eb..f25244b3808 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -87,14 +87,13 @@ model = Model(HiGHS.Optimizer); # Now we can solve our model: -set_silent(model) +# set_silent(model) optimize!(model) assert_is_solved_and_feasible(model) # and print the schedule: -function print_schedule(M, T, x) - Y = round.(Bool, value.(x)) +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)) @@ -110,7 +109,8 @@ function print_schedule(M, T, x) return end -print_schedule(M, T, x) +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: @@ -141,7 +141,17 @@ number_of_back_to_back_away_games = # 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, 20.0) +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), From 7bfab597296df27a225a739c16ee8cbaa3e3b84c Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 14 May 2026 12:20:34 +1200 Subject: [PATCH 8/9] Update --- docs/src/tutorials/linear/sports_scheduling.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index f25244b3808..4b7093c2963 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -167,7 +167,8 @@ number_of_back_to_back_away_games = # And the final schedule is: -print_schedule(M, T, x) +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? From dae7ceda5de129add52e6f74277737e042a6f42e Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 14 May 2026 15:04:04 +1200 Subject: [PATCH 9/9] Apply suggestion from @odow --- docs/src/tutorials/linear/sports_scheduling.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/linear/sports_scheduling.jl b/docs/src/tutorials/linear/sports_scheduling.jl index 4b7093c2963..fa58b6611df 100644 --- a/docs/src/tutorials/linear/sports_scheduling.jl +++ b/docs/src/tutorials/linear/sports_scheduling.jl @@ -87,7 +87,7 @@ model = Model(HiGHS.Optimizer); # Now we can solve our model: -# set_silent(model) +set_silent(model) optimize!(model) assert_is_solved_and_feasible(model)