From 7da70f74cf2a4d9b13e341caca3e59c3a833ca17 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Wed, 21 Aug 2024 17:03:10 +0200 Subject: [PATCH] Added contributors and chores. --- 00_intro.md | 12 ++++- 04B_advanced_modelling.md | 86 ++++++++++++++++++-------------- CITATION.cff | 7 ++- README.md | 96 +++++++++++++++++++++--------------- examples/add_reservoir.ipynb | 15 +++--- 5 files changed, 130 insertions(+), 86 deletions(-) diff --git a/00_intro.md b/00_intro.md index 8ef77f2..a2d6136 100644 --- a/00_intro.md +++ b/00_intro.md @@ -8,7 +8,8 @@ -_By [Dominik Krupke](https://krupke.cc), TU Braunschweig_ +_By [Dominik Krupke](https://krupke.cc), TU Braunschweig, with contributions +from Leon Lan, Michael Perk, and others._ @@ -149,7 +150,7 @@ to coding; rather, it originates from an earlier usage of the word "program", which denoted a plan of action or a schedule. If this distinction is new to you, it is a strong indication that you would benefit from reading this article. -> **About the Main Author:** [Dr. Dominik Krupke](https://krupke.cc) is a +> **About the Lead Author:** [Dr. Dominik Krupke](https://krupke.cc) is a > postdoctoral researcher with the > [Algorithms Division](https://www.ibr.cs.tu-bs.de/alg) at TU Braunschweig. He > specializes in practical solutions to NP-hard problems. Initially focused on @@ -157,6 +158,13 @@ it is a strong indication that you would benefit from reading this article. > once deemed impossible, frequently with the help of CP-SAT. This primer on > CP-SAT, first developed as course material for his students, has been extended > in his spare time to cater to a wider audience. +> +> **Contributors:** This primer has been enriched by the contributions of +> several individuals. Notably, Leon Lan played a key role in restructuring the +> content and offering critical feedback, while Michael Perk significantly +> enhanced the section on the reservoir constraint. I also extend my gratitude +> to all other contributors who identified and corrected errors, improved the +> text, and offered valuable insights. > **Found a mistake?** Please open an issue or a pull request. You can also just > write me a quick mail to `krupked@gmail.com`. diff --git a/04B_advanced_modelling.md b/04B_advanced_modelling.md index d36a438..a03b05f 100644 --- a/04B_advanced_modelling.md +++ b/04B_advanced_modelling.md @@ -300,18 +300,20 @@ infeasible as it does not end in a final state. Sometimes, we need to keep the balance between inflows and outflows of a reservoir. The name giving example is a water reservoir, where we need to keep -the water level between a minimum and a maximum level. -The reservoir constraint takes a list of time variables, -a list of integer level changes, and the minimum and maximum level of the reservoir. -If the affine expression `times[i]` is assigned a value `t`, then the current -level changes by `level_changes[i]`. Note that at the moment, variable level changes are not supported, which means -level changes are constant at time `t`. The constraint ensures that the level stays between the minimum and maximum -level at all time, i.e. `sum(level_changes[i] if times[i] <= t) in [min_level, max_level]`. - -There are many other examples apart from water reservoirs, where you need to balance demands and supplies, -such as maintaining a certain stock level in a warehouse, or -ensuring a certain staffing level in a clinic. The `add_reservoir_constraint` -constraint in CP-SAT allows you to model such problems easily. +the water level between a minimum and a maximum level. The reservoir constraint +takes a list of time variables, a list of integer level changes, and the minimum +and maximum level of the reservoir. If the affine expression `times[i]` is +assigned a value `t`, then the current level changes by `level_changes[i]`. Note +that at the moment, variable level changes are not supported, which means level +changes are constant at time `t`. The constraint ensures that the level stays +between the minimum and maximum level at all time, i.e. +`sum(level_changes[i] if times[i] <= t) in [min_level, max_level]`. + +There are many other examples apart from water reservoirs, where you need to +balance demands and supplies, such as maintaining a certain stock level in a +warehouse, or ensuring a certain staffing level in a clinic. The +`add_reservoir_constraint` constraint in CP-SAT allows you to model such +problems easily. In the following example, `times[i]` represents the time at which the change `level_changes[i]` will be applied, thus both lists needs to be of the same @@ -332,11 +334,11 @@ model.add_reservoir_constraint( Additionally, the `add_reservoir_constraint_with_active` constraint allows you to model a reservoir with _optional_ changes. Here, we additionally have a list -of Boolean variables `actives`, where `actives[i]` indicates if the -change `level_changes[i]` takes place, i.e. if +of Boolean variables `actives`, where `actives[i]` indicates if the change +`level_changes[i]` takes place, i.e. if `sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, max_level]` -If a change is not active, it is as if it does not exist, and the reservoir level remains the same, independent of the -time and change values. +If a change is not active, it is as if it does not exist, and the reservoir +level remains the same, independent of the time and change values. ```python times = [model.new_int_var(0, 10, f"time_{i}") for i in range(10)] @@ -352,12 +354,13 @@ model.add_reservoir_constraint_with_active( ) ``` -To illustrate the usage of the reservoir constraint, we look at an example for scheduling nurses in a clinic. -For the full example, take a look at the +To illustrate the usage of the reservoir constraint, we look at an example for +scheduling nurses in a clinic. For the full example, take a look at the [notebook](https://github.com/d-krupke/cpsat-primer/blob/main/examples/add_reservoir.ipynb). -The clinic needs to ensure that there are always enough nurses available without over-staffing too much. -For a 12-hour work day, we model the demands for nurses as integers for each hour of the day. +The clinic needs to ensure that there are always enough nurses available without +over-staffing too much. For a 12-hour work day, we model the demands for nurses +as integers for each hour of the day. ```python # a positive number means we need more nurses, a negative number means we need fewer nurses. @@ -365,7 +368,8 @@ demand_change_at_t = [3, 0, 0, 0, 2, 0, 0, 0, -1, 0, -1, 0, -3] demand_change_times = list(range(len(demand_change_at_t))) # [0, 1, ..., 12] ``` -We have a list of nurses, each with an individual availability as well as a maximum shift length. +We have a list of nurses, each with an individual availability as well as a +maximum shift length. ```python max_shift_length = 5 @@ -380,18 +384,18 @@ nurse_availabilities = 2 * [ (5, 12), (7, 12), (0, 12), - (4, 12) + (4, 12), ] ``` -We now initialize all relevant variables of the model. Each nurse is assigned a start and end time of their shift -as well as a Boolean variable indicating if they are working at all. +We now initialize all relevant variables of the model. Each nurse is assigned a +start and end time of their shift as well as a Boolean variable indicating if +they are working at all. ```python # boolean variable to indicate if a nurse is scheduled nurse_scheduled = [ - model.new_bool_var(f"nurse_{i}_scheduled") - for i in range(len(nurse_availabilities)) + model.new_bool_var(f"nurse_{i}_scheduled") for i in range(len(nurse_availabilities)) ] # model the begin and end of each shift @@ -414,21 +418,27 @@ for begin, end in zip(shifts_begin, shifts_end): model.add(end - begin <= max_shift_length) # make sure, the shifts are not too long ``` -Our reservoir level is the number of nurses scheduled at any time minus the demand for nurses up until that point. -We can now add the reservoir constraint to ensure that we have enough nurses available at all times while not having -too many nurses scheduled (i.e., the reservoir level is between 0 and 2). We have three types of changes in the -reservoir: - -1. The demand for nurses changes at the beginning of each hour. For these we use fixed integer times and activate all - changes. Note that the demand changes are negated, as an increase in demand lowers the reservoir level. -2. If a nurse begins a shift, we increase the reservoir level by 1. We use the `shifts_begin` variables as times and - change the reservoir level only if the nurse is scheduled. -3. Once a nurse ends a shift, we decrease the reservoir level by 1. We use the `shifts_end` variables as times and - change the reservoir level only if the nurse is scheduled. +Our reservoir level is the number of nurses scheduled at any time minus the +demand for nurses up until that point. We can now add the reservoir constraint +to ensure that we have enough nurses available at all times while not having too +many nurses scheduled (i.e., the reservoir level is between 0 and 2). We have +three types of changes in the reservoir: + +1. The demand for nurses changes at the beginning of each hour. For these we use + fixed integer times and activate all changes. Note that the demand changes + are negated, as an increase in demand lowers the reservoir level. +2. If a nurse begins a shift, we increase the reservoir level by 1. We use the + `shifts_begin` variables as times and change the reservoir level only if the + nurse is scheduled. +3. Once a nurse ends a shift, we decrease the reservoir level by 1. We use the + `shifts_end` variables as times and change the reservoir level only if the + nurse is scheduled. ```python times = demand_change_times -demands = [-demand for demand in demand_change_at_t] # an increase in demand lowers the reservoir +demands = [ + -demand for demand in demand_change_at_t +] # an increase in demand lowers the reservoir actives = [1] * len(demand_change_times) times += list(shifts_begin) diff --git a/CITATION.cff b/CITATION.cff index 907425d..baaf248 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,7 +1,7 @@ # YAML 1.2 --- cff-version: "1.1.0" -message: "You are free to use material from this primer. If so, please cite it." +message: "You are free to use material from this primer. If so, please cite or refert to it." title: "The CP-SAT Primer: Using and Understanding Google OR-Tools' CP-SAT Solver" abstract: "The CP-SAT Primer: Using and Understanding Google OR-Tools' CP-SAT Solver" authors: @@ -9,5 +9,10 @@ authors: family-names: Krupke given-names: Dominik orcid: "https://orcid.org/0000-0003-1573-3496" + - family-names: Lan + given-names: Leon + - family-names: Perk + given-names: Michael + - family-names: and others license: CC-BY-4.0 repository-code: "https://github.com/d-krupke/cpsat-primer" diff --git a/README.md b/README.md index 185ce55..dfd2db5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ # The CP-SAT Primer: Using and Understanding Google OR-Tools' CP-SAT Solver -_By [Dominik Krupke](https://krupke.cc), TU Braunschweig_ +_By [Dominik Krupke](https://krupke.cc), TU Braunschweig, with contributions +from Leon Lan, Michael Perk, and others._ @@ -148,7 +149,7 @@ to coding; rather, it originates from an earlier usage of the word "program", which denoted a plan of action or a schedule. If this distinction is new to you, it is a strong indication that you would benefit from reading this article. -> **About the Main Author:** [Dr. Dominik Krupke](https://krupke.cc) is a +> **About the Lead Author:** [Dr. Dominik Krupke](https://krupke.cc) is a > postdoctoral researcher with the > [Algorithms Division](https://www.ibr.cs.tu-bs.de/alg) at TU Braunschweig. He > specializes in practical solutions to NP-hard problems. Initially focused on @@ -156,6 +157,13 @@ it is a strong indication that you would benefit from reading this article. > once deemed impossible, frequently with the help of CP-SAT. This primer on > CP-SAT, first developed as course material for his students, has been extended > in his spare time to cater to a wider audience. +> +> **Contributors:** This primer has been enriched by the contributions of +> several individuals. Notably, Leon Lan played a key role in restructuring the +> content and offering critical feedback, while Michael Perk significantly +> enhanced the section on the reservoir constraint. I also extend my gratitude +> to all other contributors who identified and corrected errors, improved the +> text, and offered valuable insights. > **Found a mistake?** Please open an issue or a pull request. You can also just > write me a quick mail to `krupked@gmail.com`. @@ -1812,18 +1820,20 @@ infeasible as it does not end in a final state. Sometimes, we need to keep the balance between inflows and outflows of a reservoir. The name giving example is a water reservoir, where we need to keep -the water level between a minimum and a maximum level. -The reservoir constraint takes a list of time variables, -a list of integer level changes, and the minimum and maximum level of the reservoir. -If the affine expression `times[i]` is assigned a value `t`, then the current -level changes by `level_changes[i]`. Note that at the moment, variable level changes are not supported, which means -level changes are constant at time `t`. The constraint ensures that the level stays between the minimum and maximum -level at all time, i.e. `sum(level_changes[i] if times[i] <= t) in [min_level, max_level]`. - -There are many other examples apart from water reservoirs, where you need to balance demands and supplies, -such as maintaining a certain stock level in a warehouse, or -ensuring a certain staffing level in a clinic. The `add_reservoir_constraint` -constraint in CP-SAT allows you to model such problems easily. +the water level between a minimum and a maximum level. The reservoir constraint +takes a list of time variables, a list of integer level changes, and the minimum +and maximum level of the reservoir. If the affine expression `times[i]` is +assigned a value `t`, then the current level changes by `level_changes[i]`. Note +that at the moment, variable level changes are not supported, which means level +changes are constant at time `t`. The constraint ensures that the level stays +between the minimum and maximum level at all time, i.e. +`sum(level_changes[i] if times[i] <= t) in [min_level, max_level]`. + +There are many other examples apart from water reservoirs, where you need to +balance demands and supplies, such as maintaining a certain stock level in a +warehouse, or ensuring a certain staffing level in a clinic. The +`add_reservoir_constraint` constraint in CP-SAT allows you to model such +problems easily. In the following example, `times[i]` represents the time at which the change `level_changes[i]` will be applied, thus both lists needs to be of the same @@ -1844,11 +1854,11 @@ model.add_reservoir_constraint( Additionally, the `add_reservoir_constraint_with_active` constraint allows you to model a reservoir with _optional_ changes. Here, we additionally have a list -of Boolean variables `actives`, where `actives[i]` indicates if the -change `level_changes[i]` takes place, i.e. if +of Boolean variables `actives`, where `actives[i]` indicates if the change +`level_changes[i]` takes place, i.e. if `sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, max_level]` -If a change is not active, it is as if it does not exist, and the reservoir level remains the same, independent of the -time and change values. +If a change is not active, it is as if it does not exist, and the reservoir +level remains the same, independent of the time and change values. ```python times = [model.new_int_var(0, 10, f"time_{i}") for i in range(10)] @@ -1864,12 +1874,13 @@ model.add_reservoir_constraint_with_active( ) ``` -To illustrate the usage of the reservoir constraint, we look at an example for scheduling nurses in a clinic. -For the full example, take a look at the +To illustrate the usage of the reservoir constraint, we look at an example for +scheduling nurses in a clinic. For the full example, take a look at the [notebook](https://github.com/d-krupke/cpsat-primer/blob/main/examples/add_reservoir.ipynb). -The clinic needs to ensure that there are always enough nurses available without over-staffing too much. -For a 12-hour work day, we model the demands for nurses as integers for each hour of the day. +The clinic needs to ensure that there are always enough nurses available without +over-staffing too much. For a 12-hour work day, we model the demands for nurses +as integers for each hour of the day. ```python # a positive number means we need more nurses, a negative number means we need fewer nurses. @@ -1877,7 +1888,8 @@ demand_change_at_t = [3, 0, 0, 0, 2, 0, 0, 0, -1, 0, -1, 0, -3] demand_change_times = list(range(len(demand_change_at_t))) # [0, 1, ..., 12] ``` -We have a list of nurses, each with an individual availability as well as a maximum shift length. +We have a list of nurses, each with an individual availability as well as a +maximum shift length. ```python max_shift_length = 5 @@ -1892,18 +1904,18 @@ nurse_availabilities = 2 * [ (5, 12), (7, 12), (0, 12), - (4, 12) + (4, 12), ] ``` -We now initialize all relevant variables of the model. Each nurse is assigned a start and end time of their shift -as well as a Boolean variable indicating if they are working at all. +We now initialize all relevant variables of the model. Each nurse is assigned a +start and end time of their shift as well as a Boolean variable indicating if +they are working at all. ```python # boolean variable to indicate if a nurse is scheduled nurse_scheduled = [ - model.new_bool_var(f"nurse_{i}_scheduled") - for i in range(len(nurse_availabilities)) + model.new_bool_var(f"nurse_{i}_scheduled") for i in range(len(nurse_availabilities)) ] # model the begin and end of each shift @@ -1926,21 +1938,27 @@ for begin, end in zip(shifts_begin, shifts_end): model.add(end - begin <= max_shift_length) # make sure, the shifts are not too long ``` -Our reservoir level is the number of nurses scheduled at any time minus the demand for nurses up until that point. -We can now add the reservoir constraint to ensure that we have enough nurses available at all times while not having -too many nurses scheduled (i.e., the reservoir level is between 0 and 2). We have three types of changes in the -reservoir: +Our reservoir level is the number of nurses scheduled at any time minus the +demand for nurses up until that point. We can now add the reservoir constraint +to ensure that we have enough nurses available at all times while not having too +many nurses scheduled (i.e., the reservoir level is between 0 and 2). We have +three types of changes in the reservoir: -1. The demand for nurses changes at the beginning of each hour. For these we use fixed integer times and activate all - changes. Note that the demand changes are negated, as an increase in demand lowers the reservoir level. -2. If a nurse begins a shift, we increase the reservoir level by 1. We use the `shifts_begin` variables as times and - change the reservoir level only if the nurse is scheduled. -3. Once a nurse ends a shift, we decrease the reservoir level by 1. We use the `shifts_end` variables as times and - change the reservoir level only if the nurse is scheduled. +1. The demand for nurses changes at the beginning of each hour. For these we use + fixed integer times and activate all changes. Note that the demand changes + are negated, as an increase in demand lowers the reservoir level. +2. If a nurse begins a shift, we increase the reservoir level by 1. We use the + `shifts_begin` variables as times and change the reservoir level only if the + nurse is scheduled. +3. Once a nurse ends a shift, we decrease the reservoir level by 1. We use the + `shifts_end` variables as times and change the reservoir level only if the + nurse is scheduled. ```python times = demand_change_times -demands = [-demand for demand in demand_change_at_t] # an increase in demand lowers the reservoir +demands = [ + -demand for demand in demand_change_at_t +] # an increase in demand lowers the reservoir actives = [1] * len(demand_change_times) times += list(shifts_begin) diff --git a/examples/add_reservoir.ipynb b/examples/add_reservoir.ipynb index d25242c..aa591fe 100644 --- a/examples/add_reservoir.ipynb +++ b/examples/add_reservoir.ipynb @@ -19,6 +19,7 @@ "cell_type": "code", "source": [ "from ortools.sat.python import cp_model\n", + "\n", "model = cp_model.CpModel()" ], "id": "457bc28276122b9b", @@ -58,7 +59,7 @@ " (5, 12),\n", " (7, 12),\n", " (0, 12),\n", - " (4, 12)\n", + " (4, 12),\n", "]\n", "\n", "max_shift_length = 5" @@ -83,8 +84,7 @@ "source": [ "# boolean variable to indicate if a nurse is scheduled\n", "nurse_scheduled = [\n", - " model.new_bool_var(f\"nurse_{i}_scheduled\")\n", - " for i in range(len(nurse_availabilities))\n", + " model.new_bool_var(f\"nurse_{i}_scheduled\") for i in range(len(nurse_availabilities))\n", "]\n", "\n", "# model the begin and end of each shift\n", @@ -130,9 +130,10 @@ }, "cell_type": "code", "source": [ - "\n", "times = demand_change_times\n", - "demands = [-demand for demand in demand_change_at_t] # an increase in demand lowers the reservoir\n", + "demands = [\n", + " -demand for demand in demand_change_at_t\n", + "] # an increase in demand lowers the reservoir\n", "actives = [1] * len(demand_change_times)\n", "\n", "times += list(shifts_begin)\n", @@ -188,7 +189,9 @@ "\n", "for i in range(len(nurse_availabilities)):\n", " if solver.Value(nurse_scheduled[i]):\n", - " print(f\"Nurse {i} is scheduled from {solver.Value(shifts_begin[i])} to {solver.Value(shifts_end[i])}\")" + " print(\n", + " f\"Nurse {i} is scheduled from {solver.Value(shifts_begin[i])} to {solver.Value(shifts_end[i])}\"\n", + " )" ], "id": "initial_id", "outputs": [