diff --git a/README.md b/README.md index e798442..c77e52a 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,61 @@ -# IBM® Decision Optimization Modeling for Python (DOcplex) - -Welcome to the IBM® Decision Optimization Modeling for Python. -Licensed under the Apache License v2.0. - -With this library, you can quickly and easily add the power of optimization to -your application. You need IBM ILOG CPLEX Optimization Studio to solve the models. - -This library is composed of 2 modules: - -* IBM® Decision Optimization CPLEX Optimizer Modeling for Python - with namespace docplex.mp -* IBM® Decision Optimization CP Optimizer Modeling for Python - with namespace docplex.cp - -Solving with CPLEX locally requires that IBM® ILOG CPLEX Optimization Studio V12.8 or later -is installed on your machine. - -This library is numpy friendly. - -## Install the library - -``` - pip install docplex -``` - -## Get the documentation and examples - -* [Latest documentation](http://ibmdecisionoptimization.github.io/docplex-doc/) -* Documentation archives: - * [2.23.217](http://ibmdecisionoptimization.github.io/docplex-doc/2.23.217) - * [2.22.213](http://ibmdecisionoptimization.github.io/docplex-doc/2.22.213) - * [2.21.207](http://ibmdecisionoptimization.github.io/docplex-doc/2.21.207) - * [2.20.204](http://ibmdecisionoptimization.github.io/docplex-doc/2.20.204) - * [2.19.202](http://ibmdecisionoptimization.github.io/docplex-doc/2.19.202) - * [2.18.200](http://ibmdecisionoptimization.github.io/docplex-doc/2.18.200) - * [2.16.195](http://ibmdecisionoptimization.github.io/docplex-doc/2.16.195) -* [Examples](https://github.com/IBMDecisionOptimization/docplex-examples) - -## Get your IBM® ILOG CPLEX Optimization Studio edition - -- You can get a free [Community Edition](https://www.ibm.com/account/reg/us-en/signup?formid=urx-20028) - of CPLEX Optimization Studio, with limited solving capabilities in term of problem size. - -- Faculty members, research professionals at accredited institutions can get access to an unlimited version of CPLEX through the - [IBM® Academic Initiative](http://ibm.biz/cplex-free-for-students). - -## Dependencies - -These third-party dependencies are automatically installed with ``pip`` - -- [enum34](https://pypi.python.org/pypi/enum34) -- [futures](https://pypi.python.org/pypi/futures) -- [requests](https://pypi.python.org/pypi/requests) -- [six](https://pypi.python.org/pypi/six) -- [certifi](https://pypi.python.org/pypi/certifi) -- [chardet](https://pypi.python.org/pypi/chardet) -- [idna](https://pypi.python.org/pypi/idna) -- [urllib3](https://pypi.python.org/pypi/urllib3) - - -## License - -This library is delivered under the Apache License Version 2.0, January 2004 (see LICENSE.txt). +# IBM® Decision Optimization Modeling for Python (DOcplex) + +Welcome to the IBM® Decision Optimization Modeling for Python. +Licensed under the Apache License v2.0. + +With this library, you can quickly and easily add the power of optimization to +your application. You need IBM ILOG CPLEX Optimization Studio to solve the models. + +This library is composed of 2 modules: + +* IBM® Decision Optimization CPLEX Optimizer Modeling for Python - with namespace docplex.mp +* IBM® Decision Optimization CP Optimizer Modeling for Python - with namespace docplex.cp + +Solving with CPLEX requires that IBM® ILOG CPLEX Optimization Studio V12.10 or later +is installed on your machine. + +This library is numpy friendly. + +## Install the library + +``` + pip install docplex +``` + +## Get the documentation and examples + +* [Latest documentation](http://ibmdecisionoptimization.github.io/docplex-doc/) +* Documentation archives: + * [2.23.222](http://ibmdecisionoptimization.github.io/docplex-doc/2.23.222) + * [2.22.213](http://ibmdecisionoptimization.github.io/docplex-doc/2.22.213) + * [2.21.207](http://ibmdecisionoptimization.github.io/docplex-doc/2.21.207) + * [2.20.204](http://ibmdecisionoptimization.github.io/docplex-doc/2.20.204) + * [2.19.202](http://ibmdecisionoptimization.github.io/docplex-doc/2.19.202) + * [2.18.200](http://ibmdecisionoptimization.github.io/docplex-doc/2.18.200) + * [2.16.195](http://ibmdecisionoptimization.github.io/docplex-doc/2.16.195) +* [Examples](https://github.com/IBMDecisionOptimization/docplex-examples) + +## Get your IBM® ILOG CPLEX Optimization Studio edition + +- You can get a free [Community Edition](https://www.ibm.com/account/reg/us-en/signup?formid=urx-20028) + of CPLEX Optimization Studio, with limited solving capabilities in term of problem size. + +- Faculty members, research professionals at accredited institutions can get access to an unlimited version of CPLEX through the + [IBM® Academic Initiative](http://ibm.biz/cplex-free-for-students). + +## Dependencies + +These third-party dependencies are automatically installed with ``pip`` + +- [futures](https://pypi.python.org/pypi/futures) +- [requests](https://pypi.python.org/pypi/requests) +- [six](https://pypi.python.org/pypi/six) +- [certifi](https://pypi.python.org/pypi/certifi) +- [chardet](https://pypi.python.org/pypi/chardet) +- [idna](https://pypi.python.org/pypi/idna) +- [urllib3](https://pypi.python.org/pypi/urllib3) + + +## License + +This library is delivered under the Apache License Version 2.0, January 2004 (see LICENSE.txt). diff --git a/examples/cp/basic/color.py b/examples/cp/basic/color.py index 5076cab..9f99e43 100644 --- a/examples/cp/basic/color.py +++ b/examples/cp/basic/color.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/cvrptw.py b/examples/cp/basic/cvrptw.py new file mode 100644 index 0000000..3114669 --- /dev/null +++ b/examples/cp/basic/cvrptw.py @@ -0,0 +1,325 @@ +import sys +import os +import math +from collections import namedtuple +from docplex.cp.model import CpoModel, CpoParameters + +import docplex.cp.solver.solver as solver +from docplex.cp.utils import compare_natural + +solver_version = solver.get_version_info()['SolverVersion'] +if compare_natural(solver_version, '22.1.1.0') < 0: + print('Warning solver version', solver_version, 'is too old for', __file__) + exit(0) + +TIME_FACTOR = 10 + + +class CVRPTWProblem: + def __init__(self): + self.nb_trucks = -1 + self.truck_capacity = -1 + self.max_horizon = -1 + self.nb_customers = -1 + self.depot_xy = None + self.customers_xy = [] + self.demands = [] + self.earliest_start = [] + self.latest_start = [] + self.service_time = [] + self._xy = None + + def read_elem(self, filename): + with open(filename) as f: + return [str(elem) for elem in f.read().split()] + + # The input files follow the "Solomon" format. + def read(self, filename): + + def skip_elems(n): + for _ in range(n): + next(file_it) + + file_it = iter(self.read_elem(filename)) + + skip_elems(4) + + self.nb_trucks = int(next(file_it)) + self.truck_capacity = int(next(file_it)) + + skip_elems(13) + + self.depot_xy = (int(next(file_it)), int(next(file_it))) + + skip_elems(2) + + self.max_horizon = int(next(file_it)) + + skip_elems(1) + + idx = 0 + while True: + val = next(file_it, None) + if val is None: break + idx = int(val) - 1 + self.customers_xy.append((int(next(file_it)), int(next(file_it)))) + self.demands.append(int(next(file_it))) + ready = int(next(file_it)) + due = int(next(file_it)) + stime = int(next(file_it)) + self.earliest_start.append(ready) + self.latest_start.append(due) + self.service_time.append(stime) + + self.nb_customers = idx + 1 + self._xy = [self.depot_xy] + self.customers_xy + + def get_num_nodes(self): return self.nb_customers + 1 + + def get_nb_trucks(self): return self.nb_trucks + + def get_capacity(self): return self.truck_capacity + + def get_max_horizon(self): return TIME_FACTOR * self.max_horizon + + def get_demand(self, i): + assert i >= 0 + assert i < self.get_num_nodes() + if i == 0: + return 0 + return self.demands[i - 1] + + def get_service_time(self, i): + assert i >= 0 + assert i < self.get_num_nodes() + if i == 0: + return 0 + return TIME_FACTOR * self.service_time[i - 1] + + def get_earliest_start(self, i): + assert i >= 0 + assert i < self.get_num_nodes() + if i == 0: + return 0 + return TIME_FACTOR * self.earliest_start[i - 1] + + def get_latest_start(self, i): + assert i >= 0 + assert i < self.get_num_nodes() + if i == 0: + return 0 + return TIME_FACTOR * self.latest_start[i - 1] + + def _get_distance(self, from_, to_): + c1, c2 = self._xy[from_], self._xy[to_] + dx, dy, d = c2[0] - c1[0], c2[1] - c1[1], 0.0 + d = math.sqrt(dx * dx + dy * dy) + return int(math.floor(d * TIME_FACTOR)) + + def get_distance(self, from_, to_): + assert from_ >= 0 + assert from_ < self.get_num_nodes() + assert to_ >= 0 + assert to_ < self.get_num_nodes() + return self._get_distance(from_, to_) + + +class VRP: + VisitData = namedtuple("CustomerData", "demand service_time earliest, latest") + def __init__(self, pb): + # Sizes + self._num_veh = pb.get_nb_trucks() + self._num_cust = pb.get_num_nodes() - 1 + self._n = self._num_cust + self._num_veh * 2 + + # First, last, customer groups + self._first = tuple(self._num_cust + i for i in range(self._num_veh)) + self._last = tuple(self._num_cust + self._num_veh + i for i in range(self._num_veh)) + self._cust = tuple(range(self._num_cust)) + + # Time and load limits + self._max_horizon = pb.get_max_horizon() + self._capacity = pb.get_capacity() + + # Node mapping + pnode = [i + 1 for i in range(self._num_cust)] + [0] * (2 * self._num_veh) + + # Visit data + self._visit_data = \ + tuple(VRP.VisitData(pb.get_demand(pnode[c]), pb.get_service_time(pnode[c]), pb.get_earliest_start(pnode[c]), pb.get_latest_start(pnode[c])) for c in self._cust) + \ + tuple(VRP.VisitData(0, 0, 0, self._max_horizon) for _ in self._first + self._last) + + # Distance + self._distance = [ + [ pb.get_distance(pnode[i], pnode[j]) for j in range(self._n) ] + for i in range(self._n) + ] + + def first(self) : return self._first + def last(self) : return self._last + def vehicles(self) : return zip(range(self._num_veh), self._first, self._last) + def customers(self) : return self._cust + def all(self) : return range(self._n) + def get_num_customers(self): return self._num_cust + def get_num_visits(self): return self._n + def get_num_vehicles(self): return self._num_veh + def get_first(self, veh): return self._first[veh] + def get_last(self, veh): return self._last[veh] + def get_capacity(self): return self._capacity + def get_max_horizon(self): return self._max_horizon + def get_demand(self, i): return self._visit_data[i].demand + def get_service_time(self, i): return self._visit_data[i].service_time + def get_earliest_start(self, i): return self._visit_data[i].earliest + def get_latest_start(self, i): return self._visit_data[i].latest + def get_distance(self, i, j): return self._distance[i][j] + + +class DataModel: + vrp = None + prev = None + veh = None + load = None + start_time = None + params = None + + +def build_model(cvrp_prob, tlim): + data = DataModel() + vrp = VRP(cvrp_prob) + num_cust = vrp.get_num_customers() + num_vehicles = vrp.get_num_vehicles() + n = vrp.get_num_visits() + + mdl = CpoModel() + + # Prev variables, circuit, first/last + prev = [mdl.integer_var(0, n - 1, "P{}".format(i)) for i in range(n)] + for v,fv,lv in vrp.vehicles(): + mdl.add(prev[fv] == vrp.get_last((v - 1) % num_vehicles)) + + before = vrp.customers() + vrp.first() + for c in vrp.customers(): + mdl.add(mdl.allowed_assignments(prev[c], before)) + mdl.add(prev[c] != c) + + for _,fv,lv in vrp.vehicles(): + mdl.add(mdl.allowed_assignments(prev[lv], vrp.customers() + (fv,))) + + mdl.add(mdl.sub_circuit(prev)) + + # Vehicle + veh = [ mdl.integer_var(0, num_vehicles - 1, "V{}".format(i)) for i in range(n) ] + for v,fv,lv in vrp.vehicles(): + mdl.add(veh[fv] == v) + mdl.add(veh[lv] == v) + mdl.add(mdl.element(veh, prev[lv]) == v) + for c in vrp.customers(): + mdl.add(veh[c] == mdl.element(veh, prev[c])) + + # Demand + load = [ mdl.integer_var(0, vrp.get_capacity(), "L{}".format(i)) for i in range(num_vehicles) ] + used = mdl.integer_var(0, num_vehicles, 'U') + cust_veh = [ veh[c] for c in vrp.customers() ] + demand = [ vrp.get_demand(c) for c in vrp.customers() ] + mdl.add(mdl.pack(load, cust_veh, demand, used)) + + # Time + start_time = [ mdl.integer_var(vrp.get_earliest_start(i), vrp.get_latest_start(i), "T{}".format(i)) for i in range(n) ] + for fv in vrp.first(): + mdl.add(start_time[fv] == 0) + for i in vrp.customers() + vrp.last(): + arrive = mdl.element([start_time[j] + vrp.get_service_time(j) + vrp.get_distance(j, i) for j in range(n)], prev[i]) + mdl.add(start_time[i] == mdl.max(arrive, vrp.get_earliest_start(i))) + + # Distance + all_dist = [] + for i in vrp.customers() + vrp.last(): + ldist = [ vrp.get_distance(j, i) for j in range(n) ] + all_dist.append(mdl.element(ldist, prev[i])) + total_distance = mdl.sum(all_dist) / TIME_FACTOR + + # Variables with inferred values + mdl.add(mdl.inferred(cust_veh + load + [used] + start_time)) + + # Objective + mdl.add(mdl.minimize(total_distance)) + + # KPIs + mdl.add_kpi(used, 'Used') + + # Solver params setting + params = CpoParameters() + params.SearchType = 'Restart' + params.LogPeriod = 10000 + if tlim != None: + params.TimeLimit = tlim + + mdl.set_parameters(params=params) + + data.vrp = vrp + data.prev = prev + data.veh = veh + data.load = load + data.start_time = start_time + data.params = params + + return mdl, data + + +def display_solution(sol, data): + vrp = data.vrp + sprev = tuple(sol.solution[p] for p in data.prev) + + for v,fv,lv in vrp.vehicles(): + route = [] + nd = lv + while nd != fv: + route.append(nd) + nd = sprev[nd] + route.append(fv) + route.reverse() + print('Veh {} --->'.format(v, route), end="") + if len(route) > 2: + arrive = 0 + total_distance = 0 + total_load = 0 + line = "" + for idx, nd in enumerate(route): + early = vrp.get_earliest_start(nd) + late = vrp.get_latest_start(nd) + start = max(arrive, early) + assert(start == sol.solution[data.start_time[nd]]) + line += " {} (a = {}, t = {} <= {} <= {})".format(nd, arrive, early, start, late) + if nd != route[-1]: + nxt = route[idx + 1] + locald = vrp.get_distance(nd, nxt) + serv = vrp.get_service_time(nd) + line += " -- {} + {} -->".format(serv, locald) + arrive = start + serv + locald + total_distance += locald + if nd != route[0]: + total_load += data.vrp.get_demand(nd) + line += " --- D = {:.1f}, L = {}".format(total_distance, total_load) + print(line) + else: + print(" empty") + + +if __name__ == "__main__": + fname = os.path.dirname(os.path.abspath(__file__)) + "/data/cvrptw_C101_25.data" + if len(sys.argv) != 1: + if len(sys.argv) != 2: + print(f'Usage: {sys.argv[0]} OR {sys.argv[0]} ') + exit(1) + else: + fname = sys.argv[1] + + tlim = 15 + if len(sys.argv) == 3: + tlim = float(sys.argv[2]) + cvrptw_prob = CVRPTWProblem() + cvrptw_prob.read(fname) + model, data_model = build_model(cvrptw_prob, tlim) + solution = model.solve() + if solution: + display_solution(solution, data_model) diff --git a/examples/cp/basic/data/cvrptw_C101_25.data b/examples/cp/basic/data/cvrptw_C101_25.data new file mode 100644 index 0000000..ac8dd51 --- /dev/null +++ b/examples/cp/basic/data/cvrptw_C101_25.data @@ -0,0 +1,36 @@ +C101 + +VEHICLE +NUMBER CAPACITY + 25 200 + +CUSTOMER +CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME + + 0 40 50 0 0 1236 0 + 1 45 68 10 912 967 90 + 2 45 70 30 825 870 90 + 3 42 66 10 65 146 90 + 4 42 68 10 727 782 90 + 5 42 65 10 15 67 90 + 6 40 69 20 621 702 90 + 7 40 66 20 170 225 90 + 8 38 68 20 255 324 90 + 9 38 70 10 534 605 90 + 10 35 66 10 357 410 90 + 11 35 69 10 448 505 90 + 12 25 85 20 652 721 90 + 13 22 75 30 30 92 90 + 14 22 85 10 567 620 90 + 15 20 80 40 384 429 90 + 16 20 85 40 475 528 90 + 17 18 75 20 99 148 90 + 18 15 75 20 179 254 90 + 19 15 80 10 278 345 90 + 20 30 50 10 10 73 90 + 21 30 52 20 914 965 90 + 22 28 52 20 812 883 90 + 23 28 55 10 732 777 90 + 24 25 50 10 65 144 90 + 25 25 52 40 169 224 90 + \ No newline at end of file diff --git a/examples/cp/basic/facility.py b/examples/cp/basic/facility.py index d6896c2..966de11 100644 --- a/examples/cp/basic/facility.py +++ b/examples/cp/basic/facility.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/golomb_ruler.py b/examples/cp/basic/golomb_ruler.py index c63257e..50414c6 100644 --- a/examples/cp/basic/golomb_ruler.py +++ b/examples/cp/basic/golomb_ruler.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/golomb_ruler_all_solutions.py b/examples/cp/basic/golomb_ruler_all_solutions.py index c0351da..56a3883 100644 --- a/examples/cp/basic/golomb_ruler_all_solutions.py +++ b/examples/cp/basic/golomb_ruler_all_solutions.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/golomb_ruler_with_propagate.py b/examples/cp/basic/golomb_ruler_with_propagate.py index 5474d7e..a134269 100644 --- a/examples/cp/basic/golomb_ruler_with_propagate.py +++ b/examples/cp/basic/golomb_ruler_with_propagate.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/golomb_ruler_with_refine_conflicts.py b/examples/cp/basic/golomb_ruler_with_refine_conflicts.py index f0ff9c3..c19e139 100644 --- a/examples/cp/basic/golomb_ruler_with_refine_conflicts.py +++ b/examples/cp/basic/golomb_ruler_with_refine_conflicts.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/hitori.py b/examples/cp/basic/hitori.py index 7321a93..9626a42 100644 --- a/examples/cp/basic/hitori.py +++ b/examples/cp/basic/hitori.py @@ -1,215 +1,215 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -Hitori is played with a grid of squares or cells, and each cell contains a -number. The objective is to eliminate numbers by filling in the squares such -that remaining cells do not contain numbers that appear more than once in -either a given row or column. - -Filled-in cells cannot be horizontally or vertically adjacent, although they -can be diagonally adjacent. The remaining un-filled cells must form a single -component connected horizontally and vertically. - -See https://en.wikipedia.org/wiki/Hitori - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from sys import stdout - - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Problem 0 (for test). A solution is: -# * 2 * -# 2 3 1 -# * 1 * -HITORI_PROBLEM_0 = ( (2, 2, 1), - (2, 3, 1), - (1, 1, 1), - ) - -# Problem 1. A solution is: -# * 2 * 5 3 -# 2 3 1 4 * -# * 1 * 3 5 -# 1 * 5 * 2 -# 5 4 3 2 1 -HITORI_PROBLEM_1 = ( (2, 2, 1, 5, 3), - (2, 3, 1, 4, 5), - (1, 1, 1, 3, 5), - (1, 3, 5, 4, 2), - (5, 4, 3, 2, 1), - ) - -# Problem 2. A solution is: -# * 8 * 6 3 2 * 7 -# 3 6 7 2 1 * 5 4 -# * 3 4 * 2 8 6 1 -# 4 1 * 5 7 * 3 * -# 7 * 3 * 8 5 1 2 -# * 5 6 7 * 1 8 * -# 6 * 2 3 5 4 7 8 -# 8 7 1 4 * 3 * 6 -HITORI_PROBLEM_2 = ( (4, 8, 1, 6, 3, 2, 5, 7), - (3, 6, 7, 2, 1, 6, 5, 4), - (2, 3, 4, 8, 2, 8, 6, 1), - (4, 1, 6, 5, 7, 7, 3, 5), - (7, 2, 3, 1, 8, 5, 1, 2), - (3, 5, 6, 7, 3, 1, 8, 4), - (6, 4, 2, 3, 5, 4, 7, 8), - (8, 7, 1, 4, 2, 3, 5, 6), - ) - -# Problem 3, solution to discover ! -HITORI_PROBLEM_3 = ( ( 2, 5, 6, 3, 8, 10, 7, 4, 13, 6, 14, 15, 9, 4, 1), - ( 3, 1, 7, 12, 8, 4, 10, 4, 4, 11, 5, 13, 4, 9, 2), - ( 4, 14, 10, 10, 14, 5, 11, 1, 6, 2, 7, 11, 13, 15, 12), - ( 5, 10, 2, 5, 13, 3, 8, 5, 9, 7, 4, 10, 6, 10, 2), - ( 1, 6, 8, 15, 10, 7, 4, 2, 15, 14, 9, 3, 3, 11, 4), - ( 6, 14, 3, 11, 2, 4, 9, 5, 7, 13, 12, 8, 10, 14, 1), - (12, 8, 14, 11, 3, 7, 15, 13, 10, 7, 12, 13, 5, 2, 13), - (11, 4, 12, 15, 5, 6, 5, 3, 15, 10, 7, 9, 5, 13, 14), - ( 8, 15, 4, 6, 15, 3, 13, 14, 6, 12, 10, 1, 11, 3, 5), - (15, 15, 9, 12, 1, 8, 11, 10, 2, 2, 11, 9, 4, 12, 2), - ( 7, 1, 9, 9, 10, 5, 3, 11, 13, 6, 7, 4, 12, 5, 8), - (14, 10, 13, 4, 12, 15, 11, 10, 5, 7, 8, 12, 5, 3, 6), - ( 5, 10, 11, 5, 11, 14, 14, 15, 8, 13, 13, 2, 7, 9, 9), - ( 9, 7, 15, 10, 12, 11, 8, 6, 1, 5, 7, 14, 13, 1, 3), - ( 6, 9, 1, 13, 6, 4, 12, 7, 14, 4, 2, 1, 3, 8, 12) - ) - - -#----------------------------------------------------------------------------- -# Prepare the data for modeling -#----------------------------------------------------------------------------- - -PUZZLE = HITORI_PROBLEM_3 -SIZE = len(PUZZLE) - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -def get_neighbors(l, c): - """ Build the list of neighbors of a given cell """ - res = [] - if c > 0: res.append((l, c-1)) - if c < SIZE - 1: res.append((l, c+1)) - if l > 0: res.append((l-1, c)) - if l < SIZE - 1: res.append((l+1, c)) - return res - -# Create model -mdl = CpoModel() - -# Create one binary variable for each colored cell -color = [[mdl.integer_var(min=0, max=1, name="C" + str(l) + "_" + str(c)) for c in range(SIZE)] for l in range(SIZE)] - -# Forbid adjacent colored cells -for l in range(SIZE): - for c in range(SIZE - 1): - mdl.add((color[l][c] + color[l][c + 1]) < 2) -for c in range(SIZE): - for l in range(SIZE - 1): - mdl.add((color[l][c] + color[l + 1][c]) < 2) - -# Color cells for digits occurring more than once -for l in range(SIZE): - lvals = [] # List of values already processed - for c in range(SIZE): - v = PUZZLE[l][c] - if v not in lvals: - lvals.append(v) - lvars = [color[l][c]] - for c2 in range(c + 1, SIZE): - if PUZZLE[l][c2] == v: - lvars.append(color[l][c2]) - # Add constraint if more than one occurrence of the value - nbocc = len(lvars) - if nbocc > 1: - mdl.add(mdl.sum(lvars) >= nbocc - 1) -for c in range(SIZE): - lvals = [] # List of values already processed - for l in range(SIZE): - v = PUZZLE[l][c] - if v not in lvals: - lvals.append(v) - lvars = [color[l][c]] - for l2 in range(l + 1, SIZE): - if PUZZLE[l2][c] == v: - lvars.append(color[l2][c]) - # Add constraint if more than one occurrence of the value - nbocc = len(lvars) - if nbocc > 1: - mdl.add(mdl.sum(lvars) >= nbocc - 1) - -# Each cell (blank or not) must be adjacent to at least another -for l in range(SIZE): - for c in range(SIZE): - lvars = [color[l2][c2] for l2, c2 in get_neighbors(l, c)] - mdl.add(mdl.sum(lvars) < len(lvars)) - -# At least cell 0,0 or cell 0,1 is blank. -# Build table of distance to one of these cells -# Black cells are associated to a max distance SIZE*SIZE -MAX_DIST = SIZE * SIZE -distance = [[mdl.integer_var(min=0, max=MAX_DIST, name="D" + str(l) + "_" + str(c)) for c in range(SIZE)] for l in range(SIZE)] -mdl.add(distance[0][0] == mdl.conditional(color[0][0], MAX_DIST, 0)) -mdl.add(distance[0][1] == mdl.conditional(color[0][1], MAX_DIST, 0)) -for c in range(2, SIZE): - mdl.add( distance[0][c] == mdl.conditional(color[0][c], MAX_DIST, 1 + mdl.min(distance[l2][c2] for l2, c2 in get_neighbors(0, c))) ) -for l in range(1, SIZE): - for c in range(SIZE): - mdl.add( distance[l][c] == mdl.conditional(color[l][c], MAX_DIST, 1 + mdl.min(distance[l2][c2] for l2, c2 in get_neighbors(l, c))) ) - -# Force distance of blank cells to be less than max -for l in range(SIZE): - for c in range(SIZE): - mdl.add((color[l][c] > 0) | (distance[l][c] < MAX_DIST)) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -def print_grid(grid): - """ Print Hitori grid """ - mxlen = max([len(str(grid[l][c])) for l in range(SIZE) for c in range(SIZE)]) - frmt = " {:>" + str(mxlen) + "}" - for l in grid: - for v in l: - stdout.write(frmt.format(v)) - stdout.write('\n') - -# Solve model -print("\nSolving model....") -msol = mdl.solve(TimeLimit=100) - -# Print solution -stdout.write("Initial problem:\n") -print_grid(PUZZLE) -stdout.write("Solution:\n") -if msol: - # Print solution grig - psol = [] - for l in range(SIZE): - nl = [] - for c in range(SIZE): - nl.append('.' if msol[color[l][c]] > 0 else PUZZLE[l][c]) - psol.append(nl) - print_grid(psol) - # Print distance grid - print("Distances:") - psol = [['.' if msol[distance[l][c]] == MAX_DIST else msol[distance[l][c]] for c in range(SIZE)] for l in range(SIZE)] - print_grid(psol) -else: - stdout.write("No solution found\n") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +Hitori is played with a grid of squares or cells, and each cell contains a +number. The objective is to eliminate numbers by filling in the squares such +that remaining cells do not contain numbers that appear more than once in +either a given row or column. + +Filled-in cells cannot be horizontally or vertically adjacent, although they +can be diagonally adjacent. The remaining un-filled cells must form a single +component connected horizontally and vertically. + +See https://en.wikipedia.org/wiki/Hitori + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from sys import stdout + + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Problem 0 (for test). A solution is: +# * 2 * +# 2 3 1 +# * 1 * +HITORI_PROBLEM_0 = ( (2, 2, 1), + (2, 3, 1), + (1, 1, 1), + ) + +# Problem 1. A solution is: +# * 2 * 5 3 +# 2 3 1 4 * +# * 1 * 3 5 +# 1 * 5 * 2 +# 5 4 3 2 1 +HITORI_PROBLEM_1 = ( (2, 2, 1, 5, 3), + (2, 3, 1, 4, 5), + (1, 1, 1, 3, 5), + (1, 3, 5, 4, 2), + (5, 4, 3, 2, 1), + ) + +# Problem 2. A solution is: +# * 8 * 6 3 2 * 7 +# 3 6 7 2 1 * 5 4 +# * 3 4 * 2 8 6 1 +# 4 1 * 5 7 * 3 * +# 7 * 3 * 8 5 1 2 +# * 5 6 7 * 1 8 * +# 6 * 2 3 5 4 7 8 +# 8 7 1 4 * 3 * 6 +HITORI_PROBLEM_2 = ( (4, 8, 1, 6, 3, 2, 5, 7), + (3, 6, 7, 2, 1, 6, 5, 4), + (2, 3, 4, 8, 2, 8, 6, 1), + (4, 1, 6, 5, 7, 7, 3, 5), + (7, 2, 3, 1, 8, 5, 1, 2), + (3, 5, 6, 7, 3, 1, 8, 4), + (6, 4, 2, 3, 5, 4, 7, 8), + (8, 7, 1, 4, 2, 3, 5, 6), + ) + +# Problem 3, solution to discover ! +HITORI_PROBLEM_3 = ( ( 2, 5, 6, 3, 8, 10, 7, 4, 13, 6, 14, 15, 9, 4, 1), + ( 3, 1, 7, 12, 8, 4, 10, 4, 4, 11, 5, 13, 4, 9, 2), + ( 4, 14, 10, 10, 14, 5, 11, 1, 6, 2, 7, 11, 13, 15, 12), + ( 5, 10, 2, 5, 13, 3, 8, 5, 9, 7, 4, 10, 6, 10, 2), + ( 1, 6, 8, 15, 10, 7, 4, 2, 15, 14, 9, 3, 3, 11, 4), + ( 6, 14, 3, 11, 2, 4, 9, 5, 7, 13, 12, 8, 10, 14, 1), + (12, 8, 14, 11, 3, 7, 15, 13, 10, 7, 12, 13, 5, 2, 13), + (11, 4, 12, 15, 5, 6, 5, 3, 15, 10, 7, 9, 5, 13, 14), + ( 8, 15, 4, 6, 15, 3, 13, 14, 6, 12, 10, 1, 11, 3, 5), + (15, 15, 9, 12, 1, 8, 11, 10, 2, 2, 11, 9, 4, 12, 2), + ( 7, 1, 9, 9, 10, 5, 3, 11, 13, 6, 7, 4, 12, 5, 8), + (14, 10, 13, 4, 12, 15, 11, 10, 5, 7, 8, 12, 5, 3, 6), + ( 5, 10, 11, 5, 11, 14, 14, 15, 8, 13, 13, 2, 7, 9, 9), + ( 9, 7, 15, 10, 12, 11, 8, 6, 1, 5, 7, 14, 13, 1, 3), + ( 6, 9, 1, 13, 6, 4, 12, 7, 14, 4, 2, 1, 3, 8, 12) + ) + + +#----------------------------------------------------------------------------- +# Prepare the data for modeling +#----------------------------------------------------------------------------- + +PUZZLE = HITORI_PROBLEM_3 +SIZE = len(PUZZLE) + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +def get_neighbors(l, c): + """ Build the list of neighbors of a given cell """ + res = [] + if c > 0: res.append((l, c-1)) + if c < SIZE - 1: res.append((l, c+1)) + if l > 0: res.append((l-1, c)) + if l < SIZE - 1: res.append((l+1, c)) + return res + +# Create model +mdl = CpoModel() + +# Create one binary variable for each colored cell +color = [[mdl.integer_var(min=0, max=1, name="C" + str(l) + "_" + str(c)) for c in range(SIZE)] for l in range(SIZE)] + +# Forbid adjacent colored cells +for l in range(SIZE): + for c in range(SIZE - 1): + mdl.add((color[l][c] + color[l][c + 1]) < 2) +for c in range(SIZE): + for l in range(SIZE - 1): + mdl.add((color[l][c] + color[l + 1][c]) < 2) + +# Color cells for digits occurring more than once +for l in range(SIZE): + lvals = [] # List of values already processed + for c in range(SIZE): + v = PUZZLE[l][c] + if v not in lvals: + lvals.append(v) + lvars = [color[l][c]] + for c2 in range(c + 1, SIZE): + if PUZZLE[l][c2] == v: + lvars.append(color[l][c2]) + # Add constraint if more than one occurrence of the value + nbocc = len(lvars) + if nbocc > 1: + mdl.add(mdl.sum(lvars) >= nbocc - 1) +for c in range(SIZE): + lvals = [] # List of values already processed + for l in range(SIZE): + v = PUZZLE[l][c] + if v not in lvals: + lvals.append(v) + lvars = [color[l][c]] + for l2 in range(l + 1, SIZE): + if PUZZLE[l2][c] == v: + lvars.append(color[l2][c]) + # Add constraint if more than one occurrence of the value + nbocc = len(lvars) + if nbocc > 1: + mdl.add(mdl.sum(lvars) >= nbocc - 1) + +# Each cell (blank or not) must be adjacent to at least another +for l in range(SIZE): + for c in range(SIZE): + lvars = [color[l2][c2] for l2, c2 in get_neighbors(l, c)] + mdl.add(mdl.sum(lvars) < len(lvars)) + +# At least cell 0,0 or cell 0,1 is blank. +# Build table of distance to one of these cells +# Black cells are associated to a max distance SIZE*SIZE +MAX_DIST = SIZE * SIZE +distance = [[mdl.integer_var(min=0, max=MAX_DIST, name="D" + str(l) + "_" + str(c)) for c in range(SIZE)] for l in range(SIZE)] +mdl.add(distance[0][0] == mdl.conditional(color[0][0], MAX_DIST, 0)) +mdl.add(distance[0][1] == mdl.conditional(color[0][1], MAX_DIST, 0)) +for c in range(2, SIZE): + mdl.add( distance[0][c] == mdl.conditional(color[0][c], MAX_DIST, 1 + mdl.min(distance[l2][c2] for l2, c2 in get_neighbors(0, c))) ) +for l in range(1, SIZE): + for c in range(SIZE): + mdl.add( distance[l][c] == mdl.conditional(color[l][c], MAX_DIST, 1 + mdl.min(distance[l2][c2] for l2, c2 in get_neighbors(l, c))) ) + +# Force distance of blank cells to be less than max +for l in range(SIZE): + for c in range(SIZE): + mdl.add((color[l][c] > 0) | (distance[l][c] < MAX_DIST)) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +def print_grid(grid): + """ Print Hitori grid """ + mxlen = max([len(str(grid[l][c])) for l in range(SIZE) for c in range(SIZE)]) + frmt = " {:>" + str(mxlen) + "}" + for l in grid: + for v in l: + stdout.write(frmt.format(v)) + stdout.write('\n') + +# Solve model +print("\nSolving model....") +msol = mdl.solve(TimeLimit=100) + +# Print solution +stdout.write("Initial problem:\n") +print_grid(PUZZLE) +stdout.write("Solution:\n") +if msol: + # Print solution grig + psol = [] + for l in range(SIZE): + nl = [] + for c in range(SIZE): + nl.append('.' if msol[color[l][c]] > 0 else PUZZLE[l][c]) + psol.append(nl) + print_grid(psol) + # Print distance grid + print("Distances:") + psol = [['.' if msol[distance[l][c]] == MAX_DIST else msol[distance[l][c]] for c in range(SIZE)] for l in range(SIZE)] + print_grid(psol) +else: + stdout.write("No solution found\n") diff --git a/examples/cp/basic/house_building.py b/examples/cp/basic/house_building.py index 3ba1b4f..e41fc34 100644 --- a/examples/cp/basic/house_building.py +++ b/examples/cp/basic/house_building.py @@ -1,176 +1,176 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -This problem schedule a series of tasks of varying durations where some tasks must finish -before others start. And assign workers to each of the tasks such that each worker is assigned -to only one task to a given time. The objective of the problem is to maximize the matching worker -skill level to the tasks. - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from collections import namedtuple - - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Number of Houses to build -NB_HOUSES = 5 - -# Max number of periods for the schedule -MAX_SCHEDULE = 318 -MAX_SCHEDULE = 200000 - -# House construction tasks -Task = (namedtuple("Task", ["name", "duration"])) -TASKS = {Task("masonry", 35), - Task("carpentry", 15), - Task("plumbing", 40), - Task("ceiling", 15), - Task("roofing", 5), - Task("painting", 10), - Task("windows", 5), - Task("facade", 10), - Task("garden", 5), - Task("moving", 5), - } - -# The tasks precedences -TaskPrecedence = (namedtuple("TaskPrecedence", ["beforeTask", "afterTask"])) -TASK_PRECEDENCES = {TaskPrecedence("masonry", "carpentry"), - TaskPrecedence("masonry", "plumbing"), - TaskPrecedence("masonry", "ceiling"), - TaskPrecedence("carpentry", "roofing"), - TaskPrecedence("ceiling", "painting"), - TaskPrecedence("roofing", "windows"), - TaskPrecedence("roofing", "facade"), - TaskPrecedence("plumbing", "facade"), - TaskPrecedence("roofing", "garden"), - TaskPrecedence("plumbing", "garden"), - TaskPrecedence("windows", "moving"), - TaskPrecedence("facade", "moving"), - TaskPrecedence("garden", "moving"), - TaskPrecedence("painting", "moving"), - } - - -# Workers Name and level for each of there skill -Skill = (namedtuple("Skill", ["worker", "task", "level"])) -SKILLS = {Skill("Joe", "masonry", 9), - Skill("Joe", "carpentry", 7), - Skill("Joe", "ceiling", 5), - Skill("Joe", "roofing", 6), - Skill("Joe", "windows", 8), - Skill("Joe", "facade", 5), - Skill("Joe", "garden", 5), - Skill("Joe", "moving", 6), - Skill("Jack", "masonry", 5), - Skill("Jack", "plumbing", 7), - Skill("Jack", "ceiling", 8), - Skill("Jack", "roofing", 7), - Skill("Jack", "painting", 9), - Skill("Jack", "facade", 5), - Skill("Jack", "garden", 5), - Skill("Jim", "carpentry", 5), - Skill("Jim", "painting", 6), - Skill("Jim", "windows", 5), - Skill("Jim", "garden", 9), - Skill("Jim", "moving", 8) - } - -# Worker and continuity requirements: if the Task 1 is done on the house, he must do the task 2 in this house -Continuity = (namedtuple("Continuity", ["worker", "task1", "task2"])) -CONTINUITIES = {Continuity("Joe", "masonry", "carpentry"), - Continuity("Jack", "roofing", "facade"), - Continuity("Joe", "carpentry", "roofing"), - Continuity("Jim", "garden", "moving") - } - - -#----------------------------------------------------------------------------- -# Prepare the data for modeling -#----------------------------------------------------------------------------- - -# Find_tasks: return the task it refers to in the Tasks vector -def find_tasks(name): - return next(t for t in TASKS if t.name == name) - -# Find_skills: return the skill it refers to in the Skills vector -def find_skills(worker, task): - return next(s for s in SKILLS if (s.worker == worker) and (s.task == task)) - -# Iterator on houses numbers -HOUSES = range(1, NB_HOUSES + 1) - -# Build the list of all worker names -WORKERS = set(sk.worker for sk in SKILLS) - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -# Create model -mdl = CpoModel() - -# Variables of the model -tasks = {} # dict of interval variable for each house and task -wtasks = {} # dict of interval variable for each house and skill -for house in HOUSES: - for task in TASKS: - v = (0, MAX_SCHEDULE) - tasks[(house, task)] = mdl.interval_var(v, v, size=task.duration, name="house {} task {}".format(house, task)) - for task in SKILLS: - wtasks[(house, task)] = mdl.interval_var(optional=True, name="house {} skill {}".format(house, task)) - -# Maximization objective of the model -obj2 = mdl.sum([s.level * mdl.presence_of(wtasks[(h, s)]) for s in SKILLS for h in HOUSES]) -mdl.add(mdl.maximize(obj2)) - -# Constraints of the model -for h in HOUSES: - # Temporal constraints - for p in TASK_PRECEDENCES: - mdl.add(mdl.end_before_start(tasks[(h, find_tasks(p.beforeTask))], tasks[(h, find_tasks(p.afterTask))])) - # Alternative workers - for t in TASKS: - mdl.add(mdl.alternative(tasks[(h, t)], [wtasks[(h, s)] for s in SKILLS if (s.task == t.name)], 1)) - # Continuity constraints - for c in CONTINUITIES: - mdl.add(mdl.presence_of(wtasks[(h, find_skills(c.worker, c.task1))]) == - mdl.presence_of(wtasks[(h, find_skills(c.worker, c.task2))])) - -# No overlap constraint -for w in WORKERS: - mdl.add(mdl.no_overlap([wtasks[(h, s)] for h in HOUSES for s in SKILLS if s.worker == w])) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -print("\nSolving model....") -msol = mdl.solve(TimeLimit=20, trace_log=False) - -# Print solution -print("Solve status: " + msol.get_solve_status()) -if msol.is_solution(): - # Sort tasks in increasing begin order - ltasks = [] - for hs in HOUSES: - for tsk in TASKS: - (beg, end, dur) = msol[tasks[(hs, tsk)]] - ltasks.append((hs, tsk, beg, end, dur)) - ltasks = sorted(ltasks, key = lambda x : x[2]) - # Print solution - print("\nList of tasks in increasing start order:") - for tsk in ltasks: - print("From " + str(tsk[2]) + " to " + str(tsk[3]) + ", " + tsk[1].name + " in house " + str(tsk[0])) - +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +This problem schedule a series of tasks of varying durations where some tasks must finish +before others start. And assign workers to each of the tasks such that each worker is assigned +to only one task to a given time. The objective of the problem is to maximize the matching worker +skill level to the tasks. + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from collections import namedtuple + + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Number of Houses to build +NB_HOUSES = 5 + +# Max number of periods for the schedule +MAX_SCHEDULE = 318 +MAX_SCHEDULE = 200000 + +# House construction tasks +Task = (namedtuple("Task", ["name", "duration"])) +TASKS = {Task("masonry", 35), + Task("carpentry", 15), + Task("plumbing", 40), + Task("ceiling", 15), + Task("roofing", 5), + Task("painting", 10), + Task("windows", 5), + Task("facade", 10), + Task("garden", 5), + Task("moving", 5), + } + +# The tasks precedences +TaskPrecedence = (namedtuple("TaskPrecedence", ["beforeTask", "afterTask"])) +TASK_PRECEDENCES = {TaskPrecedence("masonry", "carpentry"), + TaskPrecedence("masonry", "plumbing"), + TaskPrecedence("masonry", "ceiling"), + TaskPrecedence("carpentry", "roofing"), + TaskPrecedence("ceiling", "painting"), + TaskPrecedence("roofing", "windows"), + TaskPrecedence("roofing", "facade"), + TaskPrecedence("plumbing", "facade"), + TaskPrecedence("roofing", "garden"), + TaskPrecedence("plumbing", "garden"), + TaskPrecedence("windows", "moving"), + TaskPrecedence("facade", "moving"), + TaskPrecedence("garden", "moving"), + TaskPrecedence("painting", "moving"), + } + + +# Workers Name and level for each of there skill +Skill = (namedtuple("Skill", ["worker", "task", "level"])) +SKILLS = {Skill("Joe", "masonry", 9), + Skill("Joe", "carpentry", 7), + Skill("Joe", "ceiling", 5), + Skill("Joe", "roofing", 6), + Skill("Joe", "windows", 8), + Skill("Joe", "facade", 5), + Skill("Joe", "garden", 5), + Skill("Joe", "moving", 6), + Skill("Jack", "masonry", 5), + Skill("Jack", "plumbing", 7), + Skill("Jack", "ceiling", 8), + Skill("Jack", "roofing", 7), + Skill("Jack", "painting", 9), + Skill("Jack", "facade", 5), + Skill("Jack", "garden", 5), + Skill("Jim", "carpentry", 5), + Skill("Jim", "painting", 6), + Skill("Jim", "windows", 5), + Skill("Jim", "garden", 9), + Skill("Jim", "moving", 8) + } + +# Worker and continuity requirements: if the Task 1 is done on the house, he must do the task 2 in this house +Continuity = (namedtuple("Continuity", ["worker", "task1", "task2"])) +CONTINUITIES = {Continuity("Joe", "masonry", "carpentry"), + Continuity("Jack", "roofing", "facade"), + Continuity("Joe", "carpentry", "roofing"), + Continuity("Jim", "garden", "moving") + } + + +#----------------------------------------------------------------------------- +# Prepare the data for modeling +#----------------------------------------------------------------------------- + +# Find_tasks: return the task it refers to in the Tasks vector +def find_tasks(name): + return next(t for t in TASKS if t.name == name) + +# Find_skills: return the skill it refers to in the Skills vector +def find_skills(worker, task): + return next(s for s in SKILLS if (s.worker == worker) and (s.task == task)) + +# Iterator on houses numbers +HOUSES = range(1, NB_HOUSES + 1) + +# Build the list of all worker names +WORKERS = set(sk.worker for sk in SKILLS) + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +# Create model +mdl = CpoModel() + +# Variables of the model +tasks = {} # dict of interval variable for each house and task +wtasks = {} # dict of interval variable for each house and skill +for house in HOUSES: + for task in TASKS: + v = (0, MAX_SCHEDULE) + tasks[(house, task)] = mdl.interval_var(v, v, size=task.duration, name="house {} task {}".format(house, task)) + for task in SKILLS: + wtasks[(house, task)] = mdl.interval_var(optional=True, name="house {} skill {}".format(house, task)) + +# Maximization objective of the model +obj2 = mdl.sum([s.level * mdl.presence_of(wtasks[(h, s)]) for s in SKILLS for h in HOUSES]) +mdl.add(mdl.maximize(obj2)) + +# Constraints of the model +for h in HOUSES: + # Temporal constraints + for p in TASK_PRECEDENCES: + mdl.add(mdl.end_before_start(tasks[(h, find_tasks(p.beforeTask))], tasks[(h, find_tasks(p.afterTask))])) + # Alternative workers + for t in TASKS: + mdl.add(mdl.alternative(tasks[(h, t)], [wtasks[(h, s)] for s in SKILLS if (s.task == t.name)], 1)) + # Continuity constraints + for c in CONTINUITIES: + mdl.add(mdl.presence_of(wtasks[(h, find_skills(c.worker, c.task1))]) == + mdl.presence_of(wtasks[(h, find_skills(c.worker, c.task2))])) + +# No overlap constraint +for w in WORKERS: + mdl.add(mdl.no_overlap([wtasks[(h, s)] for h in HOUSES for s in SKILLS if s.worker == w])) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +print("\nSolving model....") +msol = mdl.solve(TimeLimit=20, trace_log=False) + +# Print solution +print("Solve status: " + msol.get_solve_status()) +if msol.is_solution(): + # Sort tasks in increasing begin order + ltasks = [] + for hs in HOUSES: + for tsk in TASKS: + (beg, end, dur) = msol[tasks[(hs, tsk)]] + ltasks.append((hs, tsk, beg, end, dur)) + ltasks = sorted(ltasks, key = lambda x : x[2]) + # Print solution + print("\nList of tasks in increasing start order:") + for tsk in ltasks: + print("From " + str(tsk[2]) + " to " + str(tsk[3]) + ", " + tsk[1].name + " in house " + str(tsk[0])) + diff --git a/examples/cp/basic/latin_cube.py b/examples/cp/basic/latin_cube.py index f0985ce..1361137 100644 --- a/examples/cp/basic/latin_cube.py +++ b/examples/cp/basic/latin_cube.py @@ -1,110 +1,110 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -In combinatorics and in experimental design, a Latin cube is a 3 dimensions extension of the Latin square. - -The latin cube is a n x n x n array filled with n different symbols, -each occurring exactly once in each row and exactly once in each column. - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from sys import stdout - - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Size of the cube -CUBE_SIZE = 4 - -# Indicate to constrain each square diagonal with all different symbols -CONSTRAIN_DIAGONALS = True - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -# Create CPO model -mdl = CpoModel() - -# Create grid of variables -GRNG = range(CUBE_SIZE) -grid = [[[mdl.integer_var(min=0, max=CUBE_SIZE - 1, name="C_{}_{}_{}".format(x, y, z)) for x in GRNG] for y in GRNG] for z in GRNG] - -# Add constraints for each slice on direction x -for x in GRNG: - # Add alldiff constraints for lines - for l in GRNG: - mdl.add(mdl.all_diff([grid[x][l][c] for c in GRNG])) - - # Add alldiff constraints for columns - for c in GRNG: - mdl.add(mdl.all_diff([grid[x][l][c] for l in GRNG])) - - # Add alldiff constraints for diagonals - if CONSTRAIN_DIAGONALS: - mdl.add(mdl.all_diff([grid[x][l][l] for l in GRNG])) - mdl.add(mdl.all_diff([grid[x][l][CUBE_SIZE - l - 1] for l in GRNG])) - -# Add constraints for each slice on direction y -for y in GRNG: - # Add alldiff constraints for lines - for l in GRNG: - mdl.add(mdl.all_diff([grid[l][y][c] for c in GRNG])) - - # Add alldiff constraints for columns - for c in GRNG: - mdl.add(mdl.all_diff([grid[l][y][c] for l in GRNG])) - - # Add alldiff constraints for diagonals - if CONSTRAIN_DIAGONALS: - mdl.add(mdl.all_diff([grid[l][y][l] for l in GRNG])) - mdl.add(mdl.all_diff([grid[l][y][CUBE_SIZE - l - 1] for l in GRNG])) - -# Add constraints for each slice on direction z -for z in GRNG: - # Add alldiff constraints for lines - for l in GRNG: - mdl.add(mdl.all_diff([grid[l][c][z] for c in GRNG])) - - # Add alldiff constraints for columns - for c in GRNG: - mdl.add(mdl.all_diff([grid[l][c][z] for l in GRNG])) - - # Add alldiff constraints for diagonals - if CONSTRAIN_DIAGONALS: - mdl.add(mdl.all_diff([grid[l][l][z] for l in GRNG])) - mdl.add(mdl.all_diff([grid[l][CUBE_SIZE - l - 1][z] for l in GRNG])) - -# Force first line to natural sequence -for c in GRNG: - mdl.add(grid[0][0][c] == c) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -# Solve model -print("\nSolving model....") -msol = mdl.solve(TimeLimit=10) - -# Print solution -stdout.write("Solution:\n") -if msol: - for x in GRNG: - for l in GRNG: - for c in GRNG: - stdout.write(" " + chr(ord('A') + msol[grid[x][l][c]])) - stdout.write('\n') - stdout.write('\n') -else: - stdout.write("No solution found\n") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +In combinatorics and in experimental design, a Latin cube is a 3 dimensions extension of the Latin square. + +The latin cube is a n x n x n array filled with n different symbols, +each occurring exactly once in each row and exactly once in each column. + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from sys import stdout + + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Size of the cube +CUBE_SIZE = 4 + +# Indicate to constrain each square diagonal with all different symbols +CONSTRAIN_DIAGONALS = True + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +# Create CPO model +mdl = CpoModel() + +# Create grid of variables +GRNG = range(CUBE_SIZE) +grid = [[[mdl.integer_var(min=0, max=CUBE_SIZE - 1, name="C_{}_{}_{}".format(x, y, z)) for x in GRNG] for y in GRNG] for z in GRNG] + +# Add constraints for each slice on direction x +for x in GRNG: + # Add alldiff constraints for lines + for l in GRNG: + mdl.add(mdl.all_diff([grid[x][l][c] for c in GRNG])) + + # Add alldiff constraints for columns + for c in GRNG: + mdl.add(mdl.all_diff([grid[x][l][c] for l in GRNG])) + + # Add alldiff constraints for diagonals + if CONSTRAIN_DIAGONALS: + mdl.add(mdl.all_diff([grid[x][l][l] for l in GRNG])) + mdl.add(mdl.all_diff([grid[x][l][CUBE_SIZE - l - 1] for l in GRNG])) + +# Add constraints for each slice on direction y +for y in GRNG: + # Add alldiff constraints for lines + for l in GRNG: + mdl.add(mdl.all_diff([grid[l][y][c] for c in GRNG])) + + # Add alldiff constraints for columns + for c in GRNG: + mdl.add(mdl.all_diff([grid[l][y][c] for l in GRNG])) + + # Add alldiff constraints for diagonals + if CONSTRAIN_DIAGONALS: + mdl.add(mdl.all_diff([grid[l][y][l] for l in GRNG])) + mdl.add(mdl.all_diff([grid[l][y][CUBE_SIZE - l - 1] for l in GRNG])) + +# Add constraints for each slice on direction z +for z in GRNG: + # Add alldiff constraints for lines + for l in GRNG: + mdl.add(mdl.all_diff([grid[l][c][z] for c in GRNG])) + + # Add alldiff constraints for columns + for c in GRNG: + mdl.add(mdl.all_diff([grid[l][c][z] for l in GRNG])) + + # Add alldiff constraints for diagonals + if CONSTRAIN_DIAGONALS: + mdl.add(mdl.all_diff([grid[l][l][z] for l in GRNG])) + mdl.add(mdl.all_diff([grid[l][CUBE_SIZE - l - 1][z] for l in GRNG])) + +# Force first line to natural sequence +for c in GRNG: + mdl.add(grid[0][0][c] == c) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +# Solve model +print("\nSolving model....") +msol = mdl.solve(TimeLimit=10) + +# Print solution +stdout.write("Solution:\n") +if msol: + for x in GRNG: + for l in GRNG: + for c in GRNG: + stdout.write(" " + chr(ord('A') + msol[grid[x][l][c]])) + stdout.write('\n') + stdout.write('\n') +else: + stdout.write("No solution found\n") diff --git a/examples/cp/basic/latin_square.py b/examples/cp/basic/latin_square.py index 2b72837..b21d8d7 100644 --- a/examples/cp/basic/latin_square.py +++ b/examples/cp/basic/latin_square.py @@ -1,78 +1,78 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -In combinatorics and in experimental design, a Latin square is an n x n array filled with n different symbols, -each occurring exactly once in each row and exactly once in each column. -Here is an example: - - A B C D - D C B A - B A D C - C D A B - -More information is available on https://en.wikipedia.org/wiki/Latin_square - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from sys import stdout - - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Size of the square -SQUARE_SIZE = 16 - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -# Create CPO model -mdl = CpoModel() - -# Create grid of variables -GRNG = range(SQUARE_SIZE) -grid = [[mdl.integer_var(min=0, max=SQUARE_SIZE - 1, name="C_{}_{}".format(l, c)) for l in GRNG] for c in GRNG] - -# Add alldiff constraints for lines -for l in GRNG: - mdl.add(mdl.all_diff([grid[l][c] for c in GRNG])) - -# Add alldiff constraints for columns -for c in GRNG: - mdl.add(mdl.all_diff([grid[l][c] for l in GRNG])) - -# Add alldiff constraints for diagonals -mdl.add(mdl.all_diff([grid[l][l] for l in GRNG])) -mdl.add(mdl.all_diff([grid[l][SQUARE_SIZE - l - 1] for l in GRNG])) - -# Force first line to natural sequence -for c in GRNG: - mdl.add(grid[0][c] == c) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -# Solve model -print("\nSolving model....") -msol = mdl.solve(TimeLimit=10) - -# Print solution -stdout.write("Solution:\n") -if msol: - for l in GRNG: - for c in GRNG: - stdout.write(" " + chr(ord('A') + msol[grid[l][c]])) - stdout.write('\n') -else: - stdout.write("No solution found\n") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +In combinatorics and in experimental design, a Latin square is an n x n array filled with n different symbols, +each occurring exactly once in each row and exactly once in each column. +Here is an example: + + A B C D + D C B A + B A D C + C D A B + +More information is available on https://en.wikipedia.org/wiki/Latin_square + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from sys import stdout + + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Size of the square +SQUARE_SIZE = 16 + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +# Create CPO model +mdl = CpoModel() + +# Create grid of variables +GRNG = range(SQUARE_SIZE) +grid = [[mdl.integer_var(min=0, max=SQUARE_SIZE - 1, name="C_{}_{}".format(l, c)) for l in GRNG] for c in GRNG] + +# Add alldiff constraints for lines +for l in GRNG: + mdl.add(mdl.all_diff([grid[l][c] for c in GRNG])) + +# Add alldiff constraints for columns +for c in GRNG: + mdl.add(mdl.all_diff([grid[l][c] for l in GRNG])) + +# Add alldiff constraints for diagonals +mdl.add(mdl.all_diff([grid[l][l] for l in GRNG])) +mdl.add(mdl.all_diff([grid[l][SQUARE_SIZE - l - 1] for l in GRNG])) + +# Force first line to natural sequence +for c in GRNG: + mdl.add(grid[0][c] == c) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +# Solve model +print("\nSolving model....") +msol = mdl.solve(TimeLimit=10) + +# Print solution +stdout.write("Solution:\n") +if msol: + for l in GRNG: + for c in GRNG: + stdout.write(" " + chr(ord('A') + msol[grid[l][c]])) + stdout.write('\n') +else: + stdout.write("No solution found\n") diff --git a/examples/cp/basic/light_up.py b/examples/cp/basic/light_up.py index 467aae9..c54ed60 100644 --- a/examples/cp/basic/light_up.py +++ b/examples/cp/basic/light_up.py @@ -1,232 +1,232 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -Light Up, also called Akari, is a binary-determination logic puzzle published by Nikoli. - -Light Up is played on a rectangular grid of white and black cells. -The player places light bulbs in white cells such that no two bulbs shine on each other, -until the entire grid is lit up. A bulb sends rays of light horizontally and vertically, -illuminating its entire row and column unless its light is blocked by a black cell. -A black cell may have a number on it from 0 to 4, indicating how many bulbs must be placed -adjacent to its four sides; for example, a cell with a 4 must have four bulbs around it, -one on each side, and a cell with a 0 cannot have a bulb next to any of its sides. -An unnumbered black cell may have any number of light bulbs adjacent to it, or none. -Bulbs placed diagonally adjacent to a numbered cell do not contribute to the bulb count. - -See https://en.wikipedia.org/wiki/Light_Up_(puzzle). - -Examples taken from https://www.brainbashers.com and https://en.wikipedia.org. - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from sys import stdout - - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Each problem is expressed as a list of strings, each one representing a line of the puzzle. -# Character may be: -# - Blank for an empty cell -# - A digit (in 0..4) for black cell that force a number of neighbor bulbs, -# - Any character to represent a black cell - -# Problem 1. Solution: -LIGHT_UP_PROBLEM_1 = (" 2 ", - " ", - " X0 ", - " 1 ", - " 1 ") - -# Problem 2 -LIGHT_UP_PROBLEM_2 = ("X X X", - " X ", - " 3 0 ", - " 2 X 1", - " 10X ", - " 1XX ", - "X 2 2 ", - " X X ", - " 1 ", - "0 1 0") - -# Problem 2 -LIGHT_UP_PROBLEM_3 = (" X X 1 ", - "3 X 2 X 2 ", - " XX 1 XX ", - " X 1X1 X1 3 X X2XX ", - "X 1X X 0X", - " X XX XXXX ", - "X 1 3 ", - " X 3 X X", - " 1X 1X X X 2 X ", - " X X X XXX XX", - " X X 0X0XX 1 X ", - " 0 0 0 X X 1 ", - " 1 1 X ", - " X X X 1 X XX", - "X 2 2 X1X1 ", - "X X 0 1 X X X ", - " 2 X X 2 ", - "X X XX X ", - " 0 2X X ", - "XX 1 2 X 2X ") - -PUZZLE = LIGHT_UP_PROBLEM_3 - - -#----------------------------------------------------------------------------- -# Prepare the data for modeling -#----------------------------------------------------------------------------- - -# Retrieve size of the grid -WIDTH = len(PUZZLE[0]) -HEIGHT = len(PUZZLE) - -def get_neighbors(l, c): - """ Build the list of neighbors of a given cell """ - res = [] - if c > 0: res.append((l, c-1)) - if c < WIDTH - 1: res.append((l, c+1)) - if l > 0: res.append((l-1, c)) - if l < HEIGHT - 1: res.append((l+1, c)) - return res - -def get_all_visible(l, c): - """ Build the list of cells that are visible from a given one """ - res = [(l, c)] - c2 = c - 1 - while c2 >= 0 and PUZZLE[l][c2] == ' ': - res.append((l, c2)) - c2 -= 1 - c2 = c + 1 - while c2 < WIDTH and PUZZLE[l][c2] == ' ': - res.append((l, c2)) - c2 += 1 - - l2 = l - 1 - while l2 >= 0 and PUZZLE[l2][c] == ' ': - res.append((l2, c)) - l2 -= 1 - l2 = l + 1 - while l2 < HEIGHT and PUZZLE[l2][c] == ' ': - res.append((l2, c)) - l2 += 1 - return res - -def get_right_empty_count(l, c): - """ Get the number of empty cells at the right of the given one, including it """ - if PUZZLE[l][c] != ' ': - return 1 - res = 1 - c += 1 - while c < WIDTH and PUZZLE[l][c] == ' ': - c += 1 - res += 1 - return res - -def get_down_empty_count(l, c): - """ Get the number of empty cells at the down of the given one, including it """ - if PUZZLE[l][c] != ' ': - return 1 - res = 1 - l += 1 - while l < HEIGHT and PUZZLE[l][c] == ' ': - l += 1 - res += 1 - return res - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -# Create CPO model -mdl = CpoModel() - -# Create one binary variable for presence of bulbs in cells -bulbs = [[mdl.integer_var(min=0, max=1, name="C{}_{}".format(l, c)) for c in range(WIDTH)] for l in range(HEIGHT)] - -# Force number of bulbs in black cells to zero -for l in range(HEIGHT): - for c in range(WIDTH): - if PUZZLE[l][c] != ' ': - mdl.add(bulbs[l][c] == 0) - -# Force number of bulbs around numbered cells -for l in range(HEIGHT): - for c in range(WIDTH): - v = PUZZLE[l][c] - if v.isdigit(): - mdl.add(mdl.sum(bulbs[l2][c2] for l2, c2 in get_neighbors(l, c)) == int(v)) - -# Avoid multiple bulbs on adjacent empty cells -for l in range(HEIGHT): - c = 0 - while c < WIDTH: - nbc = get_right_empty_count(l, c) - if nbc > 1: - mdl.add(mdl.sum(bulbs[l][c2] for c2 in range(c, c + nbc)) <= 1) - c += nbc -for c in range(WIDTH): - l = 0 - while l < HEIGHT: - nbc = get_down_empty_count(l, c) - if nbc > 1: - mdl.add(mdl.sum(bulbs[l2][c] for l2 in range(l, l + nbc)) <= 1) - l += nbc - -# Each empty cell must be lighten by at least one bulb -for l in range(HEIGHT): - for c in range(WIDTH): - if PUZZLE[l][c] == ' ': - mdl.add(mdl.sum(bulbs[l2][c2] for l2, c2 in get_all_visible(l, c)) > 0) - -# Minimize the total number of bulbs -nbbulbs = mdl.integer_var(0, HEIGHT * WIDTH, name="NbBulbs") -mdl.add(nbbulbs == sum(bulbs[l][c] for c in range(WIDTH) for l in range(HEIGHT))) -mdl.add(mdl.minimize(nbbulbs)) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -def print_grid(grid): - """ Print grid """ - for l in grid: - stdout.write('|') - for v in l: - stdout.write(" " + str(v)) - stdout.write(' |\n') - -# Solve model -print("\nSolving model....") -msol = mdl.solve(TimeLimit=10) - -# Print solution -stdout.write("Initial problem:\n") -print_grid(PUZZLE) -if msol: - # Print solution grig - psol = [] - stdout.write("Solution: (bulbs represented by *):\n") - for l in range(HEIGHT): - nl = [] - for c in range(WIDTH): - if PUZZLE[l][c] == ' ': - nl.append('*' if msol[bulbs[l][c]] > 0 else '.') - else: - nl.append(PUZZLE[l][c]) - psol.append(nl) - print_grid(psol) - print("Total bulbs: " + str(msol[nbbulbs])) -else: - stdout.write("No solution found\n") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +Light Up, also called Akari, is a binary-determination logic puzzle published by Nikoli. + +Light Up is played on a rectangular grid of white and black cells. +The player places light bulbs in white cells such that no two bulbs shine on each other, +until the entire grid is lit up. A bulb sends rays of light horizontally and vertically, +illuminating its entire row and column unless its light is blocked by a black cell. +A black cell may have a number on it from 0 to 4, indicating how many bulbs must be placed +adjacent to its four sides; for example, a cell with a 4 must have four bulbs around it, +one on each side, and a cell with a 0 cannot have a bulb next to any of its sides. +An unnumbered black cell may have any number of light bulbs adjacent to it, or none. +Bulbs placed diagonally adjacent to a numbered cell do not contribute to the bulb count. + +See https://en.wikipedia.org/wiki/Light_Up_(puzzle). + +Examples taken from https://www.brainbashers.com and https://en.wikipedia.org. + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from sys import stdout + + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Each problem is expressed as a list of strings, each one representing a line of the puzzle. +# Character may be: +# - Blank for an empty cell +# - A digit (in 0..4) for black cell that force a number of neighbor bulbs, +# - Any character to represent a black cell + +# Problem 1. Solution: +LIGHT_UP_PROBLEM_1 = (" 2 ", + " ", + " X0 ", + " 1 ", + " 1 ") + +# Problem 2 +LIGHT_UP_PROBLEM_2 = ("X X X", + " X ", + " 3 0 ", + " 2 X 1", + " 10X ", + " 1XX ", + "X 2 2 ", + " X X ", + " 1 ", + "0 1 0") + +# Problem 2 +LIGHT_UP_PROBLEM_3 = (" X X 1 ", + "3 X 2 X 2 ", + " XX 1 XX ", + " X 1X1 X1 3 X X2XX ", + "X 1X X 0X", + " X XX XXXX ", + "X 1 3 ", + " X 3 X X", + " 1X 1X X X 2 X ", + " X X X XXX XX", + " X X 0X0XX 1 X ", + " 0 0 0 X X 1 ", + " 1 1 X ", + " X X X 1 X XX", + "X 2 2 X1X1 ", + "X X 0 1 X X X ", + " 2 X X 2 ", + "X X XX X ", + " 0 2X X ", + "XX 1 2 X 2X ") + +PUZZLE = LIGHT_UP_PROBLEM_3 + + +#----------------------------------------------------------------------------- +# Prepare the data for modeling +#----------------------------------------------------------------------------- + +# Retrieve size of the grid +WIDTH = len(PUZZLE[0]) +HEIGHT = len(PUZZLE) + +def get_neighbors(l, c): + """ Build the list of neighbors of a given cell """ + res = [] + if c > 0: res.append((l, c-1)) + if c < WIDTH - 1: res.append((l, c+1)) + if l > 0: res.append((l-1, c)) + if l < HEIGHT - 1: res.append((l+1, c)) + return res + +def get_all_visible(l, c): + """ Build the list of cells that are visible from a given one """ + res = [(l, c)] + c2 = c - 1 + while c2 >= 0 and PUZZLE[l][c2] == ' ': + res.append((l, c2)) + c2 -= 1 + c2 = c + 1 + while c2 < WIDTH and PUZZLE[l][c2] == ' ': + res.append((l, c2)) + c2 += 1 + + l2 = l - 1 + while l2 >= 0 and PUZZLE[l2][c] == ' ': + res.append((l2, c)) + l2 -= 1 + l2 = l + 1 + while l2 < HEIGHT and PUZZLE[l2][c] == ' ': + res.append((l2, c)) + l2 += 1 + return res + +def get_right_empty_count(l, c): + """ Get the number of empty cells at the right of the given one, including it """ + if PUZZLE[l][c] != ' ': + return 1 + res = 1 + c += 1 + while c < WIDTH and PUZZLE[l][c] == ' ': + c += 1 + res += 1 + return res + +def get_down_empty_count(l, c): + """ Get the number of empty cells at the down of the given one, including it """ + if PUZZLE[l][c] != ' ': + return 1 + res = 1 + l += 1 + while l < HEIGHT and PUZZLE[l][c] == ' ': + l += 1 + res += 1 + return res + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +# Create CPO model +mdl = CpoModel() + +# Create one binary variable for presence of bulbs in cells +bulbs = [[mdl.integer_var(min=0, max=1, name="C{}_{}".format(l, c)) for c in range(WIDTH)] for l in range(HEIGHT)] + +# Force number of bulbs in black cells to zero +for l in range(HEIGHT): + for c in range(WIDTH): + if PUZZLE[l][c] != ' ': + mdl.add(bulbs[l][c] == 0) + +# Force number of bulbs around numbered cells +for l in range(HEIGHT): + for c in range(WIDTH): + v = PUZZLE[l][c] + if v.isdigit(): + mdl.add(mdl.sum(bulbs[l2][c2] for l2, c2 in get_neighbors(l, c)) == int(v)) + +# Avoid multiple bulbs on adjacent empty cells +for l in range(HEIGHT): + c = 0 + while c < WIDTH: + nbc = get_right_empty_count(l, c) + if nbc > 1: + mdl.add(mdl.sum(bulbs[l][c2] for c2 in range(c, c + nbc)) <= 1) + c += nbc +for c in range(WIDTH): + l = 0 + while l < HEIGHT: + nbc = get_down_empty_count(l, c) + if nbc > 1: + mdl.add(mdl.sum(bulbs[l2][c] for l2 in range(l, l + nbc)) <= 1) + l += nbc + +# Each empty cell must be lighten by at least one bulb +for l in range(HEIGHT): + for c in range(WIDTH): + if PUZZLE[l][c] == ' ': + mdl.add(mdl.sum(bulbs[l2][c2] for l2, c2 in get_all_visible(l, c)) > 0) + +# Minimize the total number of bulbs +nbbulbs = mdl.integer_var(0, HEIGHT * WIDTH, name="NbBulbs") +mdl.add(nbbulbs == sum(bulbs[l][c] for c in range(WIDTH) for l in range(HEIGHT))) +mdl.add(mdl.minimize(nbbulbs)) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +def print_grid(grid): + """ Print grid """ + for l in grid: + stdout.write('|') + for v in l: + stdout.write(" " + str(v)) + stdout.write(' |\n') + +# Solve model +print("\nSolving model....") +msol = mdl.solve(TimeLimit=10) + +# Print solution +stdout.write("Initial problem:\n") +print_grid(PUZZLE) +if msol: + # Print solution grig + psol = [] + stdout.write("Solution: (bulbs represented by *):\n") + for l in range(HEIGHT): + nl = [] + for c in range(WIDTH): + if PUZZLE[l][c] == ' ': + nl.append('*' if msol[bulbs[l][c]] > 0 else '.') + else: + nl.append(PUZZLE[l][c]) + psol.append(nl) + print_grid(psol) + print("Total bulbs: " + str(msol[nbbulbs])) +else: + stdout.write("No solution found\n") diff --git a/examples/cp/basic/linear_peg_solitaire.py b/examples/cp/basic/linear_peg_solitaire.py index 3d82fdf..be3ae09 100644 --- a/examples/cp/basic/linear_peg_solitaire.py +++ b/examples/cp/basic/linear_peg_solitaire.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/n_queen.py b/examples/cp/basic/n_queen.py index f4e0196..718e4bb 100644 --- a/examples/cp/basic/n_queen.py +++ b/examples/cp/basic/n_queen.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/plant_location_with_cpo_callback.py b/examples/cp/basic/plant_location_with_cpo_callback.py index 1e790c6..5391646 100644 --- a/examples/cp/basic/plant_location_with_cpo_callback.py +++ b/examples/cp/basic/plant_location_with_cpo_callback.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016, 2018, 2020 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/plant_location_with_kpis.py b/examples/cp/basic/plant_location_with_kpis.py index 9862570..504e09a 100644 --- a/examples/cp/basic/plant_location_with_kpis.py +++ b/examples/cp/basic/plant_location_with_kpis.py @@ -1,121 +1,120 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016, 2018 -# -------------------------------------------------------------------------- - -""" -A ship-building company has a certain number of customers. Each customer is supplied -by exactly one plant. In turn, a plant can supply several customers. The problem is -to decide where to set up the plants in order to supply every customer while minimizing -the cost of building each plant and the transportation cost of supplying the customers. - -For each possible plant location there is a fixed cost and a production capacity. -Both take into account the country and the geographical conditions. - -For every customer, there is a demand and a transportation cost with respect to -each plant location. - -While a first solution of this problem can be found easily by CP Optimizer, it can take -quite some time to improve it to a very good one. We illustrate the warm start capabilities -of CP Optimizer by giving a good starting point solution that CP Optimizer will try to improve. -This solution could be one from an expert or the result of another optimization engine -applied to the problem. - -In the solution we only give a value to the variables that determine which plant delivers -a customer. This is sufficient to define a complete solution on all model variables. -CP Optimizer first extends the solution to all variables and then starts to improve it. - -The model has been enriched by the addition of KPIs (key performance indicators), operational with a -version of COS greater or equal to 12.9.0.0. -These are named expressions which are of interest to help get an idea of the performance of the model. -Here, we are interested in two indicators: - - the first is the `occupancy'' defined as the total demand divided by the total plant capacity. - - the second indicator is the occupancy which is the lowest of all the plants. - -The KPIs are displayed in the log whenever an improving solution is found and at the end of the search. -""" - -from docplex.cp.model import CpoModel -import docplex.cp.solver.solver as solver -from docplex.cp.utils import compare_natural -from collections import deque -import os - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Read problem data from a file and convert it as a list of integers -filename = os.path.dirname(os.path.abspath(__file__)) + "/data/plant_location.data" -data = deque() -with open(filename, "r") as file: - for val in file.read().split(): - data.append(int(val)) - -# Read number of customers and locations -nbCustomer = data.popleft() -nbLocation = data.popleft() - -# Initialize cost. cost[c][p] = cost to deliver customer c from plant p -cost = list([list([data.popleft() for l in range(nbLocation)]) for c in range(nbCustomer)]) - -# Initialize demand of each customer -demand = list([data.popleft() for c in range(nbCustomer)]) - -# Initialize fixed cost of each location -fixedCost = list([data.popleft() for p in range(nbLocation)]) - -# Initialize capacity of each location -capacity = list([data.popleft() for p in range(nbLocation)]) - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -mdl = CpoModel() - -# Create variables identifying which location serves each customer -cust = mdl.integer_var_list(nbCustomer, 0, nbLocation - 1, "CustomerLocation") - -# Create variables indicating which plant location is open -open = mdl.integer_var_list(nbLocation, 0, 1, "OpenLocation") - -# Create variables indicating load of each plant -load = [mdl.integer_var(0, capacity[p], "PlantLoad_" + str(p)) for p in range(nbLocation)] - -# Associate plant openness to its load -for p in range(nbLocation): - mdl.add(open[p] == (load[p] > 0)) - -# Add constraints -mdl.add(mdl.pack(load, cust, demand)) - -# Add objective -obj = mdl.scal_prod(fixedCost, open) -for c in range(nbCustomer): - obj += mdl.element(cust[c], cost[c]) -mdl.add(mdl.minimize(obj)) - -# Add KPIs -sol_version = solver.get_solver_version() -if compare_natural(sol_version, '12.9') >= 0: - mdl.add_kpi(mdl.sum(demand) / mdl.scal_prod(open, capacity), "Occupancy") - mdl.add_kpi(mdl.min([load[l] / capacity[l] + (1 - open[l]) for l in range(nbLocation)]), "Min occupancy") - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -# Solve the model -print("Solve the model") -msol = mdl.solve(TimeLimit=10, trace_log=False) # Set trace_log=True to have a real-time view of the KPIs -if msol: - print(" Objective value: {}".format(msol.get_objective_values()[0])) - if compare_natural(sol_version, '12.9') >= 0: - print(" KPIs: {}".format(msol.get_kpis())) -else: - print(" No solution") - +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +A ship-building company has a certain number of customers. Each customer is supplied +by exactly one plant. In turn, a plant can supply several customers. The problem is +to decide where to set up the plants in order to supply every customer while minimizing +the cost of building each plant and the transportation cost of supplying the customers. + +For each possible plant location there is a fixed cost and a production capacity. +Both take into account the country and the geographical conditions. + +For every customer, there is a demand and a transportation cost with respect to +each plant location. + +While a first solution of this problem can be found easily by CP Optimizer, it can take +quite some time to improve it to a very good one. We illustrate the warm start capabilities +of CP Optimizer by giving a good starting point solution that CP Optimizer will try to improve. +This solution could be one from an expert or the result of another optimization engine +applied to the problem. + +In the solution we only give a value to the variables that determine which plant delivers +a customer. This is sufficient to define a complete solution on all model variables. +CP Optimizer first extends the solution to all variables and then starts to improve it. + +The model has been enriched by the addition of KPIs (key performance indicators), operational with a +version of COS greater or equal to 12.9.0.0. +These are named expressions which are of interest to help get an idea of the performance of the model. +Here, we are interested in two indicators: + - the first is the `occupancy'' defined as the total demand divided by the total plant capacity. + - the second indicator is the occupancy which is the lowest of all the plants. + +The KPIs are displayed in the log whenever an improving solution is found and at the end of the search. +""" + +from docplex.cp.model import CpoModel +import docplex.cp.solver.solver as solver +from docplex.cp.utils import compare_natural +from collections import deque +import os + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Read problem data from a file and convert it as a list of integers +filename = os.path.dirname(os.path.abspath(__file__)) + "/data/plant_location.data" +data = deque() +with open(filename, "r") as file: + for val in file.read().split(): + data.append(int(val)) + +# Read number of customers and locations +nbCustomer = data.popleft() +nbLocation = data.popleft() + +# Initialize cost. cost[c][p] = cost to deliver customer c from plant p +cost = list([list([data.popleft() for l in range(nbLocation)]) for c in range(nbCustomer)]) + +# Initialize demand of each customer +demand = list([data.popleft() for c in range(nbCustomer)]) + +# Initialize fixed cost of each location +fixedCost = list([data.popleft() for p in range(nbLocation)]) + +# Initialize capacity of each location +capacity = list([data.popleft() for p in range(nbLocation)]) + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +mdl = CpoModel() + +# Create variables identifying which location serves each customer +cust = mdl.integer_var_list(nbCustomer, 0, nbLocation - 1, "CustomerLocation") + +# Create variables indicating which plant location is open +open = mdl.integer_var_list(nbLocation, 0, 1, "OpenLocation") + +# Create variables indicating load of each plant +load = [mdl.integer_var(0, capacity[p], "PlantLoad_" + str(p)) for p in range(nbLocation)] + +# Associate plant openness to its load +for p in range(nbLocation): + mdl.add(open[p] == (load[p] > 0)) + +# Add constraints +mdl.add(mdl.pack(load, cust, demand)) + +# Add objective +obj = mdl.scal_prod(fixedCost, open) +for c in range(nbCustomer): + obj += mdl.element(cust[c], cost[c]) +mdl.add(mdl.minimize(obj)) + +# Add KPIs +sol_version = solver.get_solver_version() +if compare_natural(sol_version, '12.9') >= 0: + mdl.add_kpi(mdl.sum(demand) / mdl.scal_prod(open, capacity), "Occupancy") + mdl.add_kpi(mdl.min([load[l] / capacity[l] + (1 - open[l]) for l in range(nbLocation)]), "Min occupancy") + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +# Solve the model +print("Solve the model") +msol = mdl.solve(TimeLimit=10, trace_log=False) # Set trace_log=True to have a real-time view of the KPIs +if msol: + print(" Objective value: {}".format(msol.get_objective_values()[0])) + if compare_natural(sol_version, '12.9') >= 0: + print(" KPIs: {}".format(msol.get_kpis())) +else: + print(" No solution") diff --git a/examples/cp/basic/plant_location_with_starting_point.py b/examples/cp/basic/plant_location_with_starting_point.py index fde13ef..bbfef23 100644 --- a/examples/cp/basic/plant_location_with_starting_point.py +++ b/examples/cp/basic/plant_location_with_starting_point.py @@ -1,125 +1,125 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -A ship-building company has a certain number of customers. Each customer is supplied -by exactly one plant. In turn, a plant can supply several customers. The problem is -to decide where to set up the plants in order to supply every customer while minimizing -the cost of building each plant and the transportation cost of supplying the customers. - -For each possible plant location there is a fixed cost and a production capacity. -Both take into account the country and the geographical conditions. - -For every customer, there is a demand and a transportation cost with respect to -each plant location. - -While a first solution of this problem can be found easily by CP Optimizer, it can take -quite some time to improve it to a very good one. We illustrate the warm start capabilities -of CP Optimizer by giving a good starting point solution that CP Optimizer will try to improve. -This solution could be one from an expert or the result of another optimization engine -applied to the problem. - -In the solution we only give a value to the variables that determine which plant delivers -a customer. This is sufficient to define a complete solution on all model variables. -CP Optimizer first extends the solution to all variables and then starts to improve it. - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from docplex.cp.solution import CpoModelSolution -from collections import deque -import os - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Read problem data from a file and convert it as a list of integers -filename = os.path.dirname(os.path.abspath(__file__)) + "/data/plant_location.data" -data = deque() -with open(filename, "r") as file: - for val in file.read().split(): - data.append(int(val)) - -# Read number of customers and locations -nbCustomer = data.popleft() -nbLocation = data.popleft() - -# Initialize cost. cost[c][p] = cost to deliver customer c from plant p -cost = list([list([data.popleft() for l in range(nbLocation)]) for c in range(nbCustomer)]) - -# Initialize demand of each customer -demand = list([data.popleft() for c in range(nbCustomer)]) - -# Initialize fixed cost of each location -fixedCost = list([data.popleft() for p in range(nbLocation)]) - -# Initialize capacity of each location -capacity = list([data.popleft() for p in range(nbLocation)]) - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -mdl = CpoModel() - -# Create variables identifying which location serves each customer -cust = mdl.integer_var_list(nbCustomer, 0, nbLocation - 1, "CustomerLocation") - -# Create variables indicating which plant location is open -open = mdl.integer_var_list(nbLocation, 0, 1, "OpenLocation") - -# Create variables indicating load of each plant -load = [mdl.integer_var(0, capacity[p], "PlantLoad_" + str(p)) for p in range(nbLocation)] - -# Associate plant openness to its load -for p in range(nbLocation): - mdl.add(open[p] == (load[p] > 0)) - -# Add constraints -mdl.add(mdl.pack(load, cust, demand)) - -# Add objective -obj = mdl.scal_prod(fixedCost, open) -for c in range(nbCustomer): - obj += mdl.element(cust[c], cost[c]) -mdl.add(mdl.minimize(obj)) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -# Solve without starting point -print("Solve the model with no starting point") -msol = mdl.solve(TimeLimit=10) -if msol: - print(" Objective value: " + str(msol.get_objective_values()[0])) -else: - print(" No solution") - -# Solve with starting point -print("Solve the model with starting point") -custValues = [19, 0, 11, 8, 29, 9, 29, 28, 17, 15, 7, 9, 18, 15, 1, 17, 25, 18, 17, 27, - 22, 1, 26, 3, 22, 2, 20, 27, 2, 16, 1, 16, 12, 28, 19, 2, 20, 14, 13, 27, - 3, 9, 18, 0, 13, 19, 27, 14, 12, 1, 15, 14, 17, 0, 7, 12, 11, 0, 25, 16, - 22, 13, 16, 8, 18, 27, 19, 23, 26, 13, 11, 11, 19, 22, 28, 26, 23, 3, 18, 23, - 26, 14, 29, 18, 9, 7, 12, 27, 8, 20] -sp = CpoModelSolution() -for c in range(nbCustomer): - sp.add_integer_var_solution(cust[c], custValues[c]) -mdl.set_starting_point(sp) - -try: - msol = mdl.solve(TimeLimit=10) - if msol: - print(" Objective value: " + str(msol.get_objective_values()[0])) - else: - print(" No solution") -except: - print(" Starting point seems not available with your solver version.") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +A ship-building company has a certain number of customers. Each customer is supplied +by exactly one plant. In turn, a plant can supply several customers. The problem is +to decide where to set up the plants in order to supply every customer while minimizing +the cost of building each plant and the transportation cost of supplying the customers. + +For each possible plant location there is a fixed cost and a production capacity. +Both take into account the country and the geographical conditions. + +For every customer, there is a demand and a transportation cost with respect to +each plant location. + +While a first solution of this problem can be found easily by CP Optimizer, it can take +quite some time to improve it to a very good one. We illustrate the warm start capabilities +of CP Optimizer by giving a good starting point solution that CP Optimizer will try to improve. +This solution could be one from an expert or the result of another optimization engine +applied to the problem. + +In the solution we only give a value to the variables that determine which plant delivers +a customer. This is sufficient to define a complete solution on all model variables. +CP Optimizer first extends the solution to all variables and then starts to improve it. + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from docplex.cp.solution import CpoModelSolution +from collections import deque +import os + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Read problem data from a file and convert it as a list of integers +filename = os.path.dirname(os.path.abspath(__file__)) + "/data/plant_location.data" +data = deque() +with open(filename, "r") as file: + for val in file.read().split(): + data.append(int(val)) + +# Read number of customers and locations +nbCustomer = data.popleft() +nbLocation = data.popleft() + +# Initialize cost. cost[c][p] = cost to deliver customer c from plant p +cost = list([list([data.popleft() for l in range(nbLocation)]) for c in range(nbCustomer)]) + +# Initialize demand of each customer +demand = list([data.popleft() for c in range(nbCustomer)]) + +# Initialize fixed cost of each location +fixedCost = list([data.popleft() for p in range(nbLocation)]) + +# Initialize capacity of each location +capacity = list([data.popleft() for p in range(nbLocation)]) + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +mdl = CpoModel() + +# Create variables identifying which location serves each customer +cust = mdl.integer_var_list(nbCustomer, 0, nbLocation - 1, "CustomerLocation") + +# Create variables indicating which plant location is open +open = mdl.integer_var_list(nbLocation, 0, 1, "OpenLocation") + +# Create variables indicating load of each plant +load = [mdl.integer_var(0, capacity[p], "PlantLoad_" + str(p)) for p in range(nbLocation)] + +# Associate plant openness to its load +for p in range(nbLocation): + mdl.add(open[p] == (load[p] > 0)) + +# Add constraints +mdl.add(mdl.pack(load, cust, demand)) + +# Add objective +obj = mdl.scal_prod(fixedCost, open) +for c in range(nbCustomer): + obj += mdl.element(cust[c], cost[c]) +mdl.add(mdl.minimize(obj)) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +# Solve without starting point +print("Solve the model with no starting point") +msol = mdl.solve(TimeLimit=10) +if msol: + print(" Objective value: " + str(msol.get_objective_values()[0])) +else: + print(" No solution") + +# Solve with starting point +print("Solve the model with starting point") +custValues = [19, 0, 11, 8, 29, 9, 29, 28, 17, 15, 7, 9, 18, 15, 1, 17, 25, 18, 17, 27, + 22, 1, 26, 3, 22, 2, 20, 27, 2, 16, 1, 16, 12, 28, 19, 2, 20, 14, 13, 27, + 3, 9, 18, 0, 13, 19, 27, 14, 12, 1, 15, 14, 17, 0, 7, 12, 11, 0, 25, 16, + 22, 13, 16, 8, 18, 27, 19, 23, 26, 13, 11, 11, 19, 22, 28, 26, 23, 3, 18, 23, + 26, 14, 29, 18, 9, 7, 12, 27, 8, 20] +sp = CpoModelSolution() +for c in range(nbCustomer): + sp.add_integer_var_solution(cust[c], custValues[c]) +mdl.set_starting_point(sp) + +try: + msol = mdl.solve(TimeLimit=10) + if msol: + print(" Objective value: " + str(msol.get_objective_values()[0])) + else: + print(" No solution") +except: + print(" Starting point seems not available with your solver version.") diff --git a/examples/cp/basic/sched_jobshop_blackbox.py b/examples/cp/basic/sched_jobshop_blackbox.py index a255f28..097cec4 100644 --- a/examples/cp/basic/sched_jobshop_blackbox.py +++ b/examples/cp/basic/sched_jobshop_blackbox.py @@ -92,12 +92,21 @@ import sys try: import numpy as np +except ImportError: + print("Please ensure you have installed module 'numpy'.") + sys.exit(0) + +try: import functools - import numba except ImportError: - print("Please ensure you have installed modules 'numpy', 'functools' and 'numba'.") + print("Please ensure you have installed module 'functools'.") sys.exit(0) +try: + import numba +except ImportError: + print("Please ensure you have installed module 'numba'.") + from docplex.cp.utils import compare_natural import docplex.cp.solver.solver as solver # check Solver version diff --git a/examples/cp/basic/shapes_with_blackboxes.py b/examples/cp/basic/shapes_with_blackboxes.py index b4735a5..d83861a 100644 --- a/examples/cp/basic/shapes_with_blackboxes.py +++ b/examples/cp/basic/shapes_with_blackboxes.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2021 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/steelmill.py b/examples/cp/basic/steelmill.py index 54e6e33..0172910 100644 --- a/examples/cp/basic/steelmill.py +++ b/examples/cp/basic/steelmill.py @@ -1,153 +1,153 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -The problem is to build steel coils from slabs that are available in a -work-in-process inventory of semi-finished products. -There is no limitation in the number of slabs that can be requested, -but only a finite number of slab sizes is available -(sizes 11, 13, 16, 17, 19, 20, 23, 24, 25, 26, 27, 28, 29, 30, 33, 34, 40, 43, 45). -The problem is to select a number of slabs to build the coil orders, -and to satisfy the following constraints: - - * A coil order can be built from only one slab. - * Each coil order requires a specific process to build it from a - slab. This process is encoded by a "color". - * Several coil orders can be built from the same slab, but a slab can - be used to produce at most two different "colors" of coils. - * The sum of the sizes of each coil order built from a slab must not - exceed the slab size. - -Finally, the production plan should minimize the unused capacity of the -selected slabs. - -This problem is based on "prob038: Steel mill slab design problem" from -CSPLib (www.csplib.org). It is a simplification of an industrial problem -described in J. R. Kalagnanam, M. W. Dawande, M. Trumbo, H. S. Lee. -"Inventory Matching Problems in the Steel Industry," IBM Research -Report RC 21171, 1998. - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from collections import namedtuple - - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# List of coils to produce (orders) -Order = namedtuple("Order", ['id', 'weight', 'color']) -ORDERS = ( - Order( 1, 22, 5), - Order( 2, 9, 3), - Order( 3, 9, 4), - Order( 4, 8, 5), - Order( 5, 8, 7), - Order( 6, 6, 3), - Order( 7, 5, 6), - Order( 8, 3, 0), - Order( 9, 3, 2), - Order(10, 3, 3), - Order(11, 2, 1), - Order(12, 2, 5) - ) - -# Max number of different colors of coils produced by a single slab -MAX_COLOR_PER_SLAB = 2 - -# List of available slab weights. -AVAILABLE_SLAB_WEIGHTS = [11, 13, 16, 17, 19, 20, 23, 24, 25, - 26, 27, 28, 29, 30, 33, 34, 40, 43, 45] - - -#----------------------------------------------------------------------------- -# Prepare the data for modeling -#----------------------------------------------------------------------------- - -# Upper bound for the number of slabs to use -MAX_SLABS = len(ORDERS) - -# Build a set of all colors -allcolors = set(o.color for o in ORDERS) - -# The heaviest slab -max_slab_weight = max(AVAILABLE_SLAB_WEIGHTS) - -# Minimum loss incurred for a given slab usage. -# loss[v] = loss when smallest slab is used to produce a total weight of v -loss = [0] + [min([sw - use for sw in AVAILABLE_SLAB_WEIGHTS if sw >= use]) for use in range(1, max_slab_weight + 1)] - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -# Create model -mdl = CpoModel() - -# Index of the slab used to produce each coil order -production_slab = mdl.integer_var_list(len(ORDERS), 0, MAX_SLABS - 1, "production_slab") - -# Usage of each slab -slab_use = mdl.integer_var_list(MAX_SLABS, 0, max_slab_weight, "slab_use") - -# The orders are allocated to the slabs with capacity -mdl.add(mdl.pack(slab_use, production_slab, [o.weight for o in ORDERS])) - -# Constrain max number of colors produced by each slab -for s in range(MAX_SLABS): - su = 0 - for c in allcolors: - lo = False - for i, o in enumerate(ORDERS): - if o.color == c: - lo |= (production_slab[i] == s) - su += lo - mdl.add(su <= MAX_COLOR_PER_SLAB) - -# Minimize the total loss -total_loss = sum([mdl.element(slab_use[s], loss) for s in range(MAX_SLABS)]) -mdl.add(mdl.minimize(total_loss)) - -# Set search strategy -mdl.set_search_phases([mdl.search_phase(production_slab)]) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -# Solve model -print("Solving model....") -msol = mdl.solve(FailLimit=100000, TimeLimit=10) - -# Print solution -if msol: - print("Solution: ") - for s in set(msol[ps] for ps in production_slab): - # Determine orders using this slab - lordrs = [o for i, o in enumerate(ORDERS) if msol[production_slab[i]] == s] - # Compute display attributes - used_weight = msol[slab_use[s]] # Weight used in the slab - loss_weight = loss[used_weight] # Loss weight - colors = set(o.color for o in lordrs) # List of colors - loids = [o.id for o in lordrs] # List of order irs - print("Slab weight={}, used={}, loss={}, colors={}, orders={}" - .format(used_weight + loss_weight, used_weight, loss_weight, colors, loids)) -else: - print("No solution found") - - - - - - - - - +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +The problem is to build steel coils from slabs that are available in a +work-in-process inventory of semi-finished products. +There is no limitation in the number of slabs that can be requested, +but only a finite number of slab sizes is available +(sizes 11, 13, 16, 17, 19, 20, 23, 24, 25, 26, 27, 28, 29, 30, 33, 34, 40, 43, 45). +The problem is to select a number of slabs to build the coil orders, +and to satisfy the following constraints: + + * A coil order can be built from only one slab. + * Each coil order requires a specific process to build it from a + slab. This process is encoded by a "color". + * Several coil orders can be built from the same slab, but a slab can + be used to produce at most two different "colors" of coils. + * The sum of the sizes of each coil order built from a slab must not + exceed the slab size. + +Finally, the production plan should minimize the unused capacity of the +selected slabs. + +This problem is based on "prob038: Steel mill slab design problem" from +CSPLib (www.csplib.org). It is a simplification of an industrial problem +described in J. R. Kalagnanam, M. W. Dawande, M. Trumbo, H. S. Lee. +"Inventory Matching Problems in the Steel Industry," IBM Research +Report RC 21171, 1998. + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from collections import namedtuple + + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# List of coils to produce (orders) +Order = namedtuple("Order", ['id', 'weight', 'color']) +ORDERS = ( + Order( 1, 22, 5), + Order( 2, 9, 3), + Order( 3, 9, 4), + Order( 4, 8, 5), + Order( 5, 8, 7), + Order( 6, 6, 3), + Order( 7, 5, 6), + Order( 8, 3, 0), + Order( 9, 3, 2), + Order(10, 3, 3), + Order(11, 2, 1), + Order(12, 2, 5) + ) + +# Max number of different colors of coils produced by a single slab +MAX_COLOR_PER_SLAB = 2 + +# List of available slab weights. +AVAILABLE_SLAB_WEIGHTS = [11, 13, 16, 17, 19, 20, 23, 24, 25, + 26, 27, 28, 29, 30, 33, 34, 40, 43, 45] + + +#----------------------------------------------------------------------------- +# Prepare the data for modeling +#----------------------------------------------------------------------------- + +# Upper bound for the number of slabs to use +MAX_SLABS = len(ORDERS) + +# Build a set of all colors +allcolors = set(o.color for o in ORDERS) + +# The heaviest slab +max_slab_weight = max(AVAILABLE_SLAB_WEIGHTS) + +# Minimum loss incurred for a given slab usage. +# loss[v] = loss when smallest slab is used to produce a total weight of v +loss = [0] + [min([sw - use for sw in AVAILABLE_SLAB_WEIGHTS if sw >= use]) for use in range(1, max_slab_weight + 1)] + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +# Create model +mdl = CpoModel() + +# Index of the slab used to produce each coil order +production_slab = mdl.integer_var_list(len(ORDERS), 0, MAX_SLABS - 1, "production_slab") + +# Usage of each slab +slab_use = mdl.integer_var_list(MAX_SLABS, 0, max_slab_weight, "slab_use") + +# The orders are allocated to the slabs with capacity +mdl.add(mdl.pack(slab_use, production_slab, [o.weight for o in ORDERS])) + +# Constrain max number of colors produced by each slab +for s in range(MAX_SLABS): + su = 0 + for c in allcolors: + lo = False + for i, o in enumerate(ORDERS): + if o.color == c: + lo |= (production_slab[i] == s) + su += lo + mdl.add(su <= MAX_COLOR_PER_SLAB) + +# Minimize the total loss +total_loss = sum([mdl.element(slab_use[s], loss) for s in range(MAX_SLABS)]) +mdl.add(mdl.minimize(total_loss)) + +# Set search strategy +mdl.set_search_phases([mdl.search_phase(production_slab)]) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +# Solve model +print("Solving model....") +msol = mdl.solve(FailLimit=100000, TimeLimit=10) + +# Print solution +if msol: + print("Solution: ") + for s in set(msol[ps] for ps in production_slab): + # Determine orders using this slab + lordrs = [o for i, o in enumerate(ORDERS) if msol[production_slab[i]] == s] + # Compute display attributes + used_weight = msol[slab_use[s]] # Weight used in the slab + loss_weight = loss[used_weight] # Loss weight + colors = set(o.color for o in lordrs) # List of colors + loids = [o.id for o in lordrs] # List of order irs + print("Slab weight={}, used={}, loss={}, colors={}, orders={}" + .format(used_weight + loss_weight, used_weight, loss_weight, colors, loids)) +else: + print("No solution found") + + + + + + + + + diff --git a/examples/cp/basic/sudoku.py b/examples/cp/basic/sudoku.py index 84e584a..ae794fb 100644 --- a/examples/cp/basic/sudoku.py +++ b/examples/cp/basic/sudoku.py @@ -1,118 +1,118 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -Sudoku is a logic-based, combinatorial number-placement puzzle. -The objective is to fill a 9x9 grid with digits so that each column, each row, -and each of the nine 3x3 sub-grids that compose the grid contains all of the digits from 1 to 9. -The puzzle setter provides a partially completed grid, which for a well-posed puzzle has a unique solution. - -See https://en.wikipedia.org/wiki/Sudoku for details - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import CpoModel -from sys import stdout - - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Problem 1 (zero means cell to be filled with appropriate value) -SUDOKU_PROBLEM_1 = ( (0, 0, 0, 0, 9, 0, 1, 0, 0), - (2, 8, 0, 0, 0, 5, 0, 0, 0), - (7, 0, 0, 0, 0, 6, 4, 0, 0), - - (8, 0, 5, 0, 0, 3, 0, 0, 6), - (0, 0, 1, 0, 0, 4, 0, 0, 0), - (0, 7, 0, 2, 0, 0, 0, 0, 0), - - (3, 0, 0, 0, 0, 1, 0, 8, 0), - (0, 0, 0, 0, 0, 0, 0, 5, 0), - (0, 9, 0, 0, 0, 0, 0, 7, 0), - ) - -# Problem 2 -SUDOKU_PROBLEM_2 = ( (0, 7, 0, 0, 0, 0, 0, 4, 9), - (0, 0, 0, 4, 0, 0, 0, 0, 0), - (4, 0, 3, 5, 0, 7, 0, 0, 8), - - (0, 0, 7, 2, 5, 0, 4, 0, 0), - (0, 0, 0, 0, 0, 0, 8, 0, 0), - (0, 0, 4, 0, 3, 0, 5, 9, 2), - - (6, 1, 8, 0, 0, 0, 0, 0, 5), - (0, 9, 0, 1, 0, 0, 0, 3, 0), - (0, 0, 5, 0, 0, 0, 0, 0, 7), - ) - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -# Create CPO model -mdl = CpoModel() - -# Grid range -GRNG = range(9) - -# Create grid of variables -grid = [[mdl.integer_var(min=1, max=9, name="C" + str(l) + str(c)) for l in GRNG] for c in GRNG] - -# Add alldiff constraints for lines -for l in GRNG: - mdl.add(mdl.all_diff([grid[l][c] for c in GRNG])) - -# Add alldiff constraints for columns -for c in GRNG: - mdl.add(mdl.all_diff([grid[l][c] for l in GRNG])) - -# Add alldiff constraints for sub-squares -ssrng = range(0, 9, 3) -for sl in ssrng: - for sc in ssrng: - mdl.add(mdl.all_diff([grid[l][c] for l in range(sl, sl + 3) for c in range(sc, sc + 3)])) - -# Initialize known cells -problem = SUDOKU_PROBLEM_2 -for l in GRNG: - for c in GRNG: - v = problem[l][c] - if v > 0: - grid[l][c].set_domain((v, v)) - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -def print_grid(grid): - """ Print Sudoku grid """ - for l in GRNG: - if (l > 0) and (l % 3 == 0): - stdout.write('\n') - for c in GRNG: - v = grid[l][c] - stdout.write(' ' if (c % 3 == 0) else ' ') - stdout.write(str(v) if v > 0 else '.') - stdout.write('\n') - -# Solve model -print("\nSolving model....") -msol = mdl.solve(TimeLimit=10) - -# Print solution -stdout.write("Initial problem:\n") -print_grid(problem) -stdout.write("Solution:\n") -if msol: - sol = [[msol[grid[l][c]] for c in GRNG] for l in GRNG] - print_grid(sol) -else: - stdout.write("No solution found\n") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +Sudoku is a logic-based, combinatorial number-placement puzzle. +The objective is to fill a 9x9 grid with digits so that each column, each row, +and each of the nine 3x3 sub-grids that compose the grid contains all of the digits from 1 to 9. +The puzzle setter provides a partially completed grid, which for a well-posed puzzle has a unique solution. + +See https://en.wikipedia.org/wiki/Sudoku for details + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import CpoModel +from sys import stdout + + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Problem 1 (zero means cell to be filled with appropriate value) +SUDOKU_PROBLEM_1 = ( (0, 0, 0, 0, 9, 0, 1, 0, 0), + (2, 8, 0, 0, 0, 5, 0, 0, 0), + (7, 0, 0, 0, 0, 6, 4, 0, 0), + + (8, 0, 5, 0, 0, 3, 0, 0, 6), + (0, 0, 1, 0, 0, 4, 0, 0, 0), + (0, 7, 0, 2, 0, 0, 0, 0, 0), + + (3, 0, 0, 0, 0, 1, 0, 8, 0), + (0, 0, 0, 0, 0, 0, 0, 5, 0), + (0, 9, 0, 0, 0, 0, 0, 7, 0), + ) + +# Problem 2 +SUDOKU_PROBLEM_2 = ( (0, 7, 0, 0, 0, 0, 0, 4, 9), + (0, 0, 0, 4, 0, 0, 0, 0, 0), + (4, 0, 3, 5, 0, 7, 0, 0, 8), + + (0, 0, 7, 2, 5, 0, 4, 0, 0), + (0, 0, 0, 0, 0, 0, 8, 0, 0), + (0, 0, 4, 0, 3, 0, 5, 9, 2), + + (6, 1, 8, 0, 0, 0, 0, 0, 5), + (0, 9, 0, 1, 0, 0, 0, 3, 0), + (0, 0, 5, 0, 0, 0, 0, 0, 7), + ) + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +# Create CPO model +mdl = CpoModel() + +# Grid range +GRNG = range(9) + +# Create grid of variables +grid = [[mdl.integer_var(min=1, max=9, name="C" + str(l) + str(c)) for l in GRNG] for c in GRNG] + +# Add alldiff constraints for lines +for l in GRNG: + mdl.add(mdl.all_diff([grid[l][c] for c in GRNG])) + +# Add alldiff constraints for columns +for c in GRNG: + mdl.add(mdl.all_diff([grid[l][c] for l in GRNG])) + +# Add alldiff constraints for sub-squares +ssrng = range(0, 9, 3) +for sl in ssrng: + for sc in ssrng: + mdl.add(mdl.all_diff([grid[l][c] for l in range(sl, sl + 3) for c in range(sc, sc + 3)])) + +# Initialize known cells +problem = SUDOKU_PROBLEM_2 +for l in GRNG: + for c in GRNG: + v = problem[l][c] + if v > 0: + grid[l][c].set_domain((v, v)) + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +def print_grid(grid): + """ Print Sudoku grid """ + for l in GRNG: + if (l > 0) and (l % 3 == 0): + stdout.write('\n') + for c in GRNG: + v = grid[l][c] + stdout.write(' ' if (c % 3 == 0) else ' ') + stdout.write(str(v) if v > 0 else '.') + stdout.write('\n') + +# Solve model +print("\nSolving model....") +msol = mdl.solve(TimeLimit=10) + +# Print solution +stdout.write("Initial problem:\n") +print_grid(problem) +stdout.write("Solution:\n") +if msol: + sol = [[msol[grid[l][c]] for c in GRNG] for l in GRNG] + print_grid(sol) +else: + stdout.write("No solution found\n") diff --git a/examples/cp/basic/truck_fleet.py b/examples/cp/basic/truck_fleet.py index b96ccbe..14f112b 100644 --- a/examples/cp/basic/truck_fleet.py +++ b/examples/cp/basic/truck_fleet.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/basic/vietnamese_problem.py b/examples/cp/basic/vietnamese_problem.py index c619024..8368c61 100644 --- a/examples/cp/basic/vietnamese_problem.py +++ b/examples/cp/basic/vietnamese_problem.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/jupyter/SteelMill.ipynb b/examples/cp/jupyter/SteelMill.ipynb index 84c2ec7..3fa9fb0 100644 --- a/examples/cp/jupyter/SteelMill.ipynb +++ b/examples/cp/jupyter/SteelMill.ipynb @@ -385,7 +385,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/jupyter/golomb_ruler.ipynb b/examples/cp/jupyter/golomb_ruler.ipynb index 7b262b3..936adf5 100644 --- a/examples/cp/jupyter/golomb_ruler.ipynb +++ b/examples/cp/jupyter/golomb_ruler.ipynb @@ -519,7 +519,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/jupyter/house_building.ipynb b/examples/cp/jupyter/house_building.ipynb index 50cade0..ed45455 100644 --- a/examples/cp/jupyter/house_building.ipynb +++ b/examples/cp/jupyter/house_building.ipynb @@ -991,7 +991,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/jupyter/n_queen.ipynb b/examples/cp/jupyter/n_queen.ipynb index a457fcc..052647f 100644 --- a/examples/cp/jupyter/n_queen.ipynb +++ b/examples/cp/jupyter/n_queen.ipynb @@ -362,7 +362,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/jupyter/sched_square.ipynb b/examples/cp/jupyter/sched_square.ipynb index 0f85a3e..4c38239 100644 --- a/examples/cp/jupyter/sched_square.ipynb +++ b/examples/cp/jupyter/sched_square.ipynb @@ -407,7 +407,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/jupyter/scheduling_tuto.ipynb b/examples/cp/jupyter/scheduling_tuto.ipynb index 513dfe8..3e35971 100644 --- a/examples/cp/jupyter/scheduling_tuto.ipynb +++ b/examples/cp/jupyter/scheduling_tuto.ipynb @@ -3335,7 +3335,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/jupyter/sudoku.ipynb b/examples/cp/jupyter/sudoku.ipynb index e0b980b..cdfc24b 100644 --- a/examples/cp/jupyter/sudoku.ipynb +++ b/examples/cp/jupyter/sudoku.ipynb @@ -512,7 +512,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/jupyter/truck_fleet.ipynb b/examples/cp/jupyter/truck_fleet.ipynb index 029872c..a0c15c3 100644 --- a/examples/cp/jupyter/truck_fleet.ipynb +++ b/examples/cp/jupyter/truck_fleet.ipynb @@ -429,7 +429,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017, 2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017, 2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/cp/visu/data/rcpspmm_default.json b/examples/cp/visu/data/rcpspmm_default.json index caf058f..b1c7085 100644 --- a/examples/cp/visu/data/rcpspmm_default.json +++ b/examples/cp/visu/data/rcpspmm_default.json @@ -1,318 +1,318 @@ -{ - "capacityRenewable": [25, 25], - "capacityNonRenewable": [240, 240], - "tasks": - [ - { - "id": 0, "successors": [1, 2, 3], - "modes": - [ - { "duration": 0, "demandRenewable": [0, 0], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 1, "successors": [4, 5, 6], - "modes": - [ - { "duration": 5, "demandRenewable": [10, 13], "demandNonRenewable": [7, 0] }, - { "duration": 1, "demandRenewable": [15, 19], "demandNonRenewable": [0, 17] }, - { "duration": 7, "demandRenewable": [15, 2], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 2, "successors": [10, 11], - "modes": - [ - { "duration": 1, "demandRenewable": [17, 16], "demandNonRenewable": [4, 13] }, - { "duration": 3, "demandRenewable": [7, 2], "demandNonRenewable": [1, 17] }, - { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [19, 5] } - ] - }, - { - "id": 3, "successors": [7, 8, 9], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 2], "demandNonRenewable": [8, 13] }, - { "duration": 1, "demandRenewable": [19, 19], "demandNonRenewable": [18, 7] }, - { "duration": 3, "demandRenewable": [0, 17], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 4, "successors": [10], - "modes": - [ - { "duration": 3, "demandRenewable": [5, 0], "demandNonRenewable": [18, 19] }, - { "duration": 3, "demandRenewable": [17, 8], "demandNonRenewable": [0, 0] }, - { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [0, 11] } - ] - }, - { - "id": 5, "successors": [10], - "modes": - [ - { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [0, 7] }, - { "duration": 7, "demandRenewable": [0, 0], "demandNonRenewable": [0, 8] }, - { "duration": 7, "demandRenewable": [0, 12], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 6, "successors": [15], - "modes": - [ - { "duration": 7, "demandRenewable": [0, 0], "demandNonRenewable": [1, 18] }, - { "duration": 4, "demandRenewable": [8, 7], "demandNonRenewable": [0, 0] }, - { "duration": 4, "demandRenewable": [0, 7], "demandNonRenewable": [2, 13] } - ] - }, - { - "id": 7, "successors": [12], - "modes": - [ - { "duration": 9, "demandRenewable": [0, 0], "demandNonRenewable": [0, 9] }, - { "duration": 6, "demandRenewable": [0, 6], "demandNonRenewable": [11, 4] }, - { "duration": 8, "demandRenewable": [0, 0], "demandNonRenewable": [8, 7] } - ] - }, - { - "id": 8, "successors": [12], - "modes": - [ - { "duration": 1, "demandRenewable": [11, 20], "demandNonRenewable": [20, 12] }, - { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [11, 0] }, - { "duration": 9, "demandRenewable": [0, 0], "demandNonRenewable": [4, 5] } - ] - }, - { - "id": 9, "successors": [12], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 0], "demandNonRenewable": [0, 13] }, - { "duration": 5, "demandRenewable": [0, 19], "demandNonRenewable": [0, 0] }, - { "duration": 2, "demandRenewable": [0, 19], "demandNonRenewable": [0, 18] } - ] - }, - { - "id": 10, "successors": [13, 14, 15, 16], - "modes": - [ - { "duration": 10, "demandRenewable": [10, 0], "demandNonRenewable": [3, 0] }, - { "duration": 7, "demandRenewable": [2, 10], "demandNonRenewable": [0, 0] }, - { "duration": 5, "demandRenewable": [0, 10], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 11, "successors": [13, 14, 15, 16], - "modes": - [ - { "duration": 6, "demandRenewable": [0, 19], "demandNonRenewable": [0, 0] }, - { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [10, 1] }, - { "duration": 1, "demandRenewable": [15, 19], "demandNonRenewable": [13, 16] } - ] - }, - { - "id": 12, "successors": [18], - "modes": - [ - { "duration": 7, "demandRenewable": [0, 4], "demandNonRenewable": [0, 8] }, - { "duration": 6, "demandRenewable": [0, 7], "demandNonRenewable": [3, 0] }, - { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [7, 7] } - ] - }, - { - "id": 13, "successors": [17], - "modes": - [ - { "duration": 2, "demandRenewable": [11, 0], "demandNonRenewable": [7, 17] }, - { "duration": 4, "demandRenewable": [0, 2], "demandNonRenewable": [19, 13] }, - { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [0, 9] } - ] - }, - { - "id": 14, "successors": [17], - "modes": - [ - { "duration": 7, "demandRenewable": [0, 16], "demandNonRenewable": [0, 0] }, - { "duration": 4, "demandRenewable": [5, 0], "demandNonRenewable": [13, 0] }, - { "duration": 1, "demandRenewable": [10, 20], "demandNonRenewable": [19, 18] } - ] - }, - { - "id": 15, "successors": [17, 18], - "modes": - [ - { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [11, 11] }, - { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [13, 20] }, - { "duration": 3, "demandRenewable": [15, 0], "demandNonRenewable": [8, 0] } - ] - }, - { - "id": 16, "successors": [17, 18], - "modes": - [ - { "duration": 5, "demandRenewable": [0, 9], "demandNonRenewable": [12, 5] }, - { "duration": 1, "demandRenewable": [17, 14], "demandNonRenewable": [17, 12] }, - { "duration": 9, "demandRenewable": [0, 13], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 17, "successors": [20, 21], - "modes": - [ - { "duration": 3, "demandRenewable": [1, 11], "demandNonRenewable": [19, 17] }, - { "duration": 4, "demandRenewable": [0, 0], "demandNonRenewable": [13, 11] }, - { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [0, 14] } - ] - }, - { - "id": 18, "successors": [19, 23], - "modes": - [ - { "duration": 5, "demandRenewable": [0, 3], "demandNonRenewable": [0, 9] }, - { "duration": 8, "demandRenewable": [0, 0], "demandNonRenewable": [17, 0] }, - { "duration": 8, "demandRenewable": [4, 0], "demandNonRenewable": [0, 8] } - ] - }, - { - "id": 19, "successors": [21], - "modes": - [ - { "duration": 1, "demandRenewable": [19, 10], "demandNonRenewable": [11, 13] }, - { "duration": 7, "demandRenewable": [0, 6], "demandNonRenewable": [0, 15] }, - { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 8] } - ] - }, - { - "id": 20, "successors": [24], - "modes": - [ - { "duration": 5, "demandRenewable": [2, 16], "demandNonRenewable": [0, 0] }, - { "duration": 8, "demandRenewable": [0, 0], "demandNonRenewable": [0, 12] }, - { "duration": 6, "demandRenewable": [19, 0], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 21, "successors": [24], - "modes": - [ - { "duration": 1, "demandRenewable": [15, 17], "demandNonRenewable": [7, 20] }, - { "duration": 5, "demandRenewable": [14, 0], "demandNonRenewable": [0, 3] }, - { "duration": 10, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 22, "successors": [25], - "modes": - [ - { "duration": 3, "demandRenewable": [14, 0], "demandNonRenewable": [20, 0] }, - { "duration": 3, "demandRenewable": [20, 0], "demandNonRenewable": [14, 7] }, - { "duration": 3, "demandRenewable": [19, 16], "demandNonRenewable": [0, 0] } - ] - }, - { - "id": 23, "successors": [25], - "modes": - [ - { "duration": 7, "demandRenewable": [0, 0], "demandNonRenewable": [2, 17] }, - { "duration": 10, "demandRenewable": [0, 10], "demandNonRenewable": [0, 0] }, - { "duration": 7, "demandRenewable": [14, 0], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 24, "successors": [26, 27], - "modes": - [ - { "duration": 3, "demandRenewable": [9, 6], "demandNonRenewable": [13, 0] }, - { "duration": 4, "demandRenewable": [4, 0], "demandNonRenewable": [0, 10] }, - { "duration": 3, "demandRenewable": [18, 4], "demandNonRenewable": [10, 7] } - ] - }, - { - "id": 25, "successors": [28, 31], - "modes": - [ - { "duration": 7, "demandRenewable": [12, 0], "demandNonRenewable": [0, 0] }, - { "duration": 9, "demandRenewable": [7, 2], "demandNonRenewable": [0, 0] }, - { "duration": 2, "demandRenewable": [8, 9], "demandNonRenewable": [5, 9] } - ] - }, - { - "id": 26, "successors": [30], - "modes": - [ - { "duration": 1, "demandRenewable": [3, 16], "demandNonRenewable": [19, 17] }, - { "duration": 4, "demandRenewable": [7, 14], "demandNonRenewable": [0, 2] }, - { "duration": 6, "demandRenewable": [0, 10], "demandNonRenewable": [0, 7] } - ] - }, - { - "id": 27, "successors": [29], - "modes": - [ - { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [4, 15] }, - { "duration": 9, "demandRenewable": [1, 0], "demandNonRenewable": [12, 0] }, - { "duration": 5, "demandRenewable": [14, 0], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 28, "successors": [29], - "modes": - [ - { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [9, 12] }, - { "duration": 4, "demandRenewable": [15, 1], "demandNonRenewable": [0, 17] }, - { "duration": 5, "demandRenewable": [6, 3], "demandNonRenewable": [15, 0] } - ] - }, - { - "id": 29, "successors": [34], - "modes": - [ - { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [0, 5] }, - { "duration": 9, "demandRenewable": [8, 0], "demandNonRenewable": [3, 4] }, - { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [17, 0] } - ] - }, - { - "id": 30, "successors": [31, 32, 33], - "modes": - [ - { "duration": 1, "demandRenewable": [12, 13], "demandNonRenewable": [10, 15] }, - { "duration": 5, "demandRenewable": [15, 1], "demandNonRenewable": [0, 0] }, - { "duration": 2, "demandRenewable": [0, 6], "demandNonRenewable": [19, 0] } - ] - }, - { - "id": 31, "successors": [34], - "modes": - [ - { "duration": 5, "demandRenewable": [15, 0], "demandNonRenewable": [0, 9] }, - { "duration": 3, "demandRenewable": [0, 13], "demandNonRenewable": [18, 4] }, - { "duration": 10, "demandRenewable": [6, 0], "demandNonRenewable": [2, 2] } - ] - }, - { - "id": 32, "successors": [33], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 0], "demandNonRenewable": [19, 3] }, - { "duration": 2, "demandRenewable": [11, 0], "demandNonRenewable": [0, 20] }, - { "duration": 5, "demandRenewable": [13, 0], "demandNonRenewable": [3, 6] } - ] - }, - { - "id": 33, "successors": [34], - "modes": - [ - { "duration": 8, "demandRenewable": [18, 0], "demandNonRenewable": [0, 0] }, - { "duration": 7, "demandRenewable": [8, 0], "demandNonRenewable": [0, 0] }, - { "duration": 3, "demandRenewable": [8, 13], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 34, "successors": [], - "modes": - [ - { "duration": 0, "demandRenewable": [0, 0], "demandNonRenewable": [0, 0] } - ] - } - ] -} +{ + "capacityRenewable": [25, 25], + "capacityNonRenewable": [240, 240], + "tasks": + [ + { + "id": 0, "successors": [1, 2, 3], + "modes": + [ + { "duration": 0, "demandRenewable": [0, 0], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 1, "successors": [4, 5, 6], + "modes": + [ + { "duration": 5, "demandRenewable": [10, 13], "demandNonRenewable": [7, 0] }, + { "duration": 1, "demandRenewable": [15, 19], "demandNonRenewable": [0, 17] }, + { "duration": 7, "demandRenewable": [15, 2], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 2, "successors": [10, 11], + "modes": + [ + { "duration": 1, "demandRenewable": [17, 16], "demandNonRenewable": [4, 13] }, + { "duration": 3, "demandRenewable": [7, 2], "demandNonRenewable": [1, 17] }, + { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [19, 5] } + ] + }, + { + "id": 3, "successors": [7, 8, 9], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 2], "demandNonRenewable": [8, 13] }, + { "duration": 1, "demandRenewable": [19, 19], "demandNonRenewable": [18, 7] }, + { "duration": 3, "demandRenewable": [0, 17], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 4, "successors": [10], + "modes": + [ + { "duration": 3, "demandRenewable": [5, 0], "demandNonRenewable": [18, 19] }, + { "duration": 3, "demandRenewable": [17, 8], "demandNonRenewable": [0, 0] }, + { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [0, 11] } + ] + }, + { + "id": 5, "successors": [10], + "modes": + [ + { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [0, 7] }, + { "duration": 7, "demandRenewable": [0, 0], "demandNonRenewable": [0, 8] }, + { "duration": 7, "demandRenewable": [0, 12], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 6, "successors": [15], + "modes": + [ + { "duration": 7, "demandRenewable": [0, 0], "demandNonRenewable": [1, 18] }, + { "duration": 4, "demandRenewable": [8, 7], "demandNonRenewable": [0, 0] }, + { "duration": 4, "demandRenewable": [0, 7], "demandNonRenewable": [2, 13] } + ] + }, + { + "id": 7, "successors": [12], + "modes": + [ + { "duration": 9, "demandRenewable": [0, 0], "demandNonRenewable": [0, 9] }, + { "duration": 6, "demandRenewable": [0, 6], "demandNonRenewable": [11, 4] }, + { "duration": 8, "demandRenewable": [0, 0], "demandNonRenewable": [8, 7] } + ] + }, + { + "id": 8, "successors": [12], + "modes": + [ + { "duration": 1, "demandRenewable": [11, 20], "demandNonRenewable": [20, 12] }, + { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [11, 0] }, + { "duration": 9, "demandRenewable": [0, 0], "demandNonRenewable": [4, 5] } + ] + }, + { + "id": 9, "successors": [12], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 0], "demandNonRenewable": [0, 13] }, + { "duration": 5, "demandRenewable": [0, 19], "demandNonRenewable": [0, 0] }, + { "duration": 2, "demandRenewable": [0, 19], "demandNonRenewable": [0, 18] } + ] + }, + { + "id": 10, "successors": [13, 14, 15, 16], + "modes": + [ + { "duration": 10, "demandRenewable": [10, 0], "demandNonRenewable": [3, 0] }, + { "duration": 7, "demandRenewable": [2, 10], "demandNonRenewable": [0, 0] }, + { "duration": 5, "demandRenewable": [0, 10], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 11, "successors": [13, 14, 15, 16], + "modes": + [ + { "duration": 6, "demandRenewable": [0, 19], "demandNonRenewable": [0, 0] }, + { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [10, 1] }, + { "duration": 1, "demandRenewable": [15, 19], "demandNonRenewable": [13, 16] } + ] + }, + { + "id": 12, "successors": [18], + "modes": + [ + { "duration": 7, "demandRenewable": [0, 4], "demandNonRenewable": [0, 8] }, + { "duration": 6, "demandRenewable": [0, 7], "demandNonRenewable": [3, 0] }, + { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [7, 7] } + ] + }, + { + "id": 13, "successors": [17], + "modes": + [ + { "duration": 2, "demandRenewable": [11, 0], "demandNonRenewable": [7, 17] }, + { "duration": 4, "demandRenewable": [0, 2], "demandNonRenewable": [19, 13] }, + { "duration": 10, "demandRenewable": [0, 0], "demandNonRenewable": [0, 9] } + ] + }, + { + "id": 14, "successors": [17], + "modes": + [ + { "duration": 7, "demandRenewable": [0, 16], "demandNonRenewable": [0, 0] }, + { "duration": 4, "demandRenewable": [5, 0], "demandNonRenewable": [13, 0] }, + { "duration": 1, "demandRenewable": [10, 20], "demandNonRenewable": [19, 18] } + ] + }, + { + "id": 15, "successors": [17, 18], + "modes": + [ + { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [11, 11] }, + { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [13, 20] }, + { "duration": 3, "demandRenewable": [15, 0], "demandNonRenewable": [8, 0] } + ] + }, + { + "id": 16, "successors": [17, 18], + "modes": + [ + { "duration": 5, "demandRenewable": [0, 9], "demandNonRenewable": [12, 5] }, + { "duration": 1, "demandRenewable": [17, 14], "demandNonRenewable": [17, 12] }, + { "duration": 9, "demandRenewable": [0, 13], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 17, "successors": [20, 21], + "modes": + [ + { "duration": 3, "demandRenewable": [1, 11], "demandNonRenewable": [19, 17] }, + { "duration": 4, "demandRenewable": [0, 0], "demandNonRenewable": [13, 11] }, + { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [0, 14] } + ] + }, + { + "id": 18, "successors": [19, 23], + "modes": + [ + { "duration": 5, "demandRenewable": [0, 3], "demandNonRenewable": [0, 9] }, + { "duration": 8, "demandRenewable": [0, 0], "demandNonRenewable": [17, 0] }, + { "duration": 8, "demandRenewable": [4, 0], "demandNonRenewable": [0, 8] } + ] + }, + { + "id": 19, "successors": [21], + "modes": + [ + { "duration": 1, "demandRenewable": [19, 10], "demandNonRenewable": [11, 13] }, + { "duration": 7, "demandRenewable": [0, 6], "demandNonRenewable": [0, 15] }, + { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 8] } + ] + }, + { + "id": 20, "successors": [24], + "modes": + [ + { "duration": 5, "demandRenewable": [2, 16], "demandNonRenewable": [0, 0] }, + { "duration": 8, "demandRenewable": [0, 0], "demandNonRenewable": [0, 12] }, + { "duration": 6, "demandRenewable": [19, 0], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 21, "successors": [24], + "modes": + [ + { "duration": 1, "demandRenewable": [15, 17], "demandNonRenewable": [7, 20] }, + { "duration": 5, "demandRenewable": [14, 0], "demandNonRenewable": [0, 3] }, + { "duration": 10, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 22, "successors": [25], + "modes": + [ + { "duration": 3, "demandRenewable": [14, 0], "demandNonRenewable": [20, 0] }, + { "duration": 3, "demandRenewable": [20, 0], "demandNonRenewable": [14, 7] }, + { "duration": 3, "demandRenewable": [19, 16], "demandNonRenewable": [0, 0] } + ] + }, + { + "id": 23, "successors": [25], + "modes": + [ + { "duration": 7, "demandRenewable": [0, 0], "demandNonRenewable": [2, 17] }, + { "duration": 10, "demandRenewable": [0, 10], "demandNonRenewable": [0, 0] }, + { "duration": 7, "demandRenewable": [14, 0], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 24, "successors": [26, 27], + "modes": + [ + { "duration": 3, "demandRenewable": [9, 6], "demandNonRenewable": [13, 0] }, + { "duration": 4, "demandRenewable": [4, 0], "demandNonRenewable": [0, 10] }, + { "duration": 3, "demandRenewable": [18, 4], "demandNonRenewable": [10, 7] } + ] + }, + { + "id": 25, "successors": [28, 31], + "modes": + [ + { "duration": 7, "demandRenewable": [12, 0], "demandNonRenewable": [0, 0] }, + { "duration": 9, "demandRenewable": [7, 2], "demandNonRenewable": [0, 0] }, + { "duration": 2, "demandRenewable": [8, 9], "demandNonRenewable": [5, 9] } + ] + }, + { + "id": 26, "successors": [30], + "modes": + [ + { "duration": 1, "demandRenewable": [3, 16], "demandNonRenewable": [19, 17] }, + { "duration": 4, "demandRenewable": [7, 14], "demandNonRenewable": [0, 2] }, + { "duration": 6, "demandRenewable": [0, 10], "demandNonRenewable": [0, 7] } + ] + }, + { + "id": 27, "successors": [29], + "modes": + [ + { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [4, 15] }, + { "duration": 9, "demandRenewable": [1, 0], "demandNonRenewable": [12, 0] }, + { "duration": 5, "demandRenewable": [14, 0], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 28, "successors": [29], + "modes": + [ + { "duration": 6, "demandRenewable": [0, 0], "demandNonRenewable": [9, 12] }, + { "duration": 4, "demandRenewable": [15, 1], "demandNonRenewable": [0, 17] }, + { "duration": 5, "demandRenewable": [6, 3], "demandNonRenewable": [15, 0] } + ] + }, + { + "id": 29, "successors": [34], + "modes": + [ + { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [0, 5] }, + { "duration": 9, "demandRenewable": [8, 0], "demandNonRenewable": [3, 4] }, + { "duration": 5, "demandRenewable": [0, 0], "demandNonRenewable": [17, 0] } + ] + }, + { + "id": 30, "successors": [31, 32, 33], + "modes": + [ + { "duration": 1, "demandRenewable": [12, 13], "demandNonRenewable": [10, 15] }, + { "duration": 5, "demandRenewable": [15, 1], "demandNonRenewable": [0, 0] }, + { "duration": 2, "demandRenewable": [0, 6], "demandNonRenewable": [19, 0] } + ] + }, + { + "id": 31, "successors": [34], + "modes": + [ + { "duration": 5, "demandRenewable": [15, 0], "demandNonRenewable": [0, 9] }, + { "duration": 3, "demandRenewable": [0, 13], "demandNonRenewable": [18, 4] }, + { "duration": 10, "demandRenewable": [6, 0], "demandNonRenewable": [2, 2] } + ] + }, + { + "id": 32, "successors": [33], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 0], "demandNonRenewable": [19, 3] }, + { "duration": 2, "demandRenewable": [11, 0], "demandNonRenewable": [0, 20] }, + { "duration": 5, "demandRenewable": [13, 0], "demandNonRenewable": [3, 6] } + ] + }, + { + "id": 33, "successors": [34], + "modes": + [ + { "duration": 8, "demandRenewable": [18, 0], "demandNonRenewable": [0, 0] }, + { "duration": 7, "demandRenewable": [8, 0], "demandNonRenewable": [0, 0] }, + { "duration": 3, "demandRenewable": [8, 13], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 34, "successors": [], + "modes": + [ + { "duration": 0, "demandRenewable": [0, 0], "demandNonRenewable": [0, 0] } + ] + } + ] +} diff --git a/examples/cp/visu/data/rcpspmm_j30_12_8.json b/examples/cp/visu/data/rcpspmm_j30_12_8.json index 9eb37b2..f247202 100644 --- a/examples/cp/visu/data/rcpspmm_j30_12_8.json +++ b/examples/cp/visu/data/rcpspmm_j30_12_8.json @@ -1,277 +1,277 @@ -{ - "capacityRenewable": [33, 42], - "capacityNonRenewable": [77, 94], - "tasks": - [ - { - "id": 0, "successors": [3, 5, 14], - "modes": - [ - { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [10, 0] }, - { "duration": 6, "demandRenewable": [0, 5], "demandNonRenewable": [10, 0] }, - { "duration": 7, "demandRenewable": [0, 4], "demandNonRenewable": [0, 2] } - ] - }, - { - "id": 1, "successors": [4, 6, 7], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] }, - { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [6, 0] }, - { "duration": 8, "demandRenewable": [9, 0], "demandNonRenewable": [0, 9] } - ] - }, - { - "id": 2, "successors": [8, 13], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [0, 9] }, - { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [0, 8] }, - { "duration": 7, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] } - ] - }, - { - "id": 3, "successors": [11, 29], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 7], "demandNonRenewable": [0, 6] }, - { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, - { "duration": 8, "demandRenewable": [8, 0], "demandNonRenewable": [0, 1] } - ] - }, - { - "id": 4, "successors": [17], - "modes": - [ - { "duration": 3, "demandRenewable": [8, 0], "demandNonRenewable": [0, 7] }, - { "duration": 8, "demandRenewable": [7, 0], "demandNonRenewable": [0, 7] }, - { "duration": 8, "demandRenewable": [5, 0], "demandNonRenewable": [9, 0] } - ] - }, - { - "id": 5, "successors": [9, 11, 21], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] }, - { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [3, 0] }, - { "duration": 4, "demandRenewable": [4, 0], "demandNonRenewable": [0, 3] } - ] - }, - { - "id": 6, "successors": [10, 13], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [0, 8] }, - { "duration": 7, "demandRenewable": [6, 0], "demandNonRenewable": [10, 0] }, - { "duration": 7, "demandRenewable": [0, 9], "demandNonRenewable": [1, 0] } - ] - }, - { - "id": 7, "successors": [8, 9, 23], - "modes": - [ - { "duration": 7, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] }, - { "duration": 10, "demandRenewable": [5, 0], "demandNonRenewable": [0, 2] }, - { "duration": 10, "demandRenewable": [0, 5], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 8, "successors": [25], - "modes": - [ - { "duration": 3, "demandRenewable": [8, 0], "demandNonRenewable": [7, 0] }, - { "duration": 6, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] }, - { "duration": 10, "demandRenewable": [0, 7], "demandNonRenewable": [0, 3] } - ] - }, - { - "id": 9, "successors": [10, 15, 24], - "modes": - [ - { "duration": 7, "demandRenewable": [1, 0], "demandNonRenewable": [9, 0] }, - { "duration": 7, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] }, - { "duration": 8, "demandRenewable": [0, 7], "demandNonRenewable": [0, 10] } - ] - }, - { - "id": 10, "successors": [16, 20], - "modes": - [ - { "duration": 3, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] }, - { "duration": 3, "demandRenewable": [0, 10], "demandNonRenewable": [2, 0] }, - { "duration": 10, "demandRenewable": [0, 9], "demandNonRenewable": [2, 0] } - ] - }, - { - "id": 11, "successors": [12, 15, 16], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [6, 0] }, - { "duration": 4, "demandRenewable": [8, 0], "demandNonRenewable": [6, 0] }, - { "duration": 8, "demandRenewable": [0, 4], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 12, "successors": [17], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [0, 6] }, - { "duration": 3, "demandRenewable": [3, 0], "demandNonRenewable": [0, 6] }, - { "duration": 8, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] } - ] - }, - { - "id": 13, "successors": [15, 16, 21], - "modes": - [ - { "duration": 4, "demandRenewable": [9, 0], "demandNonRenewable": [9, 0] }, - { "duration": 6, "demandRenewable": [0, 9], "demandNonRenewable": [0, 6] }, - { "duration": 9, "demandRenewable": [0, 5], "demandNonRenewable": [7, 0] } - ] - }, - { - "id": 14, "successors": [27], - "modes": - [ - { "duration": 5, "demandRenewable": [6, 0], "demandNonRenewable": [7, 0] }, - { "duration": 8, "demandRenewable": [5, 0], "demandNonRenewable": [0, 7] }, - { "duration": 8, "demandRenewable": [0, 8], "demandNonRenewable": [0, 8] } - ] - }, - { - "id": 15, "successors": [19], - "modes": - [ - { "duration": 3, "demandRenewable": [10, 0], "demandNonRenewable": [0, 4] }, - { "duration": 5, "demandRenewable": [0, 4], "demandNonRenewable": [0, 4] }, - { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 2] } - ] - }, - { - "id": 16, "successors": [18, 26], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 8], "demandNonRenewable": [0, 8] }, - { "duration": 6, "demandRenewable": [0, 7], "demandNonRenewable": [8, 0] }, - { "duration": 7, "demandRenewable": [0, 7], "demandNonRenewable": [0, 6] } - ] - }, - { - "id": 17, "successors": [20], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [0, 5] }, - { "duration": 3, "demandRenewable": [7, 0], "demandNonRenewable": [1, 0] }, - { "duration": 4, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 18, "successors": [22], - "modes": - [ - { "duration": 3, "demandRenewable": [5, 0], "demandNonRenewable": [3, 0] }, - { "duration": 10, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, - { "duration": 10, "demandRenewable": [3, 0], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 19, "successors": [20, 22], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [0, 6] }, - { "duration": 8, "demandRenewable": [0, 9], "demandNonRenewable": [4, 0] }, - { "duration": 8, "demandRenewable": [0, 9], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 20, "successors": [26, 27], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 10], "demandNonRenewable": [0, 10] }, - { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 9] }, - { "duration": 5, "demandRenewable": [0, 3], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 21, "successors": [23, 24, 25], - "modes": - [ - { "duration": 7, "demandRenewable": [0, 10], "demandNonRenewable": [0, 7] }, - { "duration": 7, "demandRenewable": [0, 9], "demandNonRenewable": [4, 0] }, - { "duration": 7, "demandRenewable": [7, 0], "demandNonRenewable": [4, 0] } - ] - }, - { - "id": 22, "successors": [25], - "modes": - [ - { "duration": 2, "demandRenewable": [8, 0], "demandNonRenewable": [0, 3] }, - { "duration": 3, "demandRenewable": [2, 0], "demandNonRenewable": [5, 0] }, - { "duration": 7, "demandRenewable": [0, 1], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 23, "successors": [27, 28, 29], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [5, 0] }, - { "duration": 1, "demandRenewable": [5, 0], "demandNonRenewable": [0, 7] }, - { "duration": 3, "demandRenewable": [4, 0], "demandNonRenewable": [0, 7] } - ] - }, - { - "id": 24, "successors": [26], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [5, 0] }, - { "duration": 10, "demandRenewable": [5, 0], "demandNonRenewable": [3, 0] }, - { "duration": 10, "demandRenewable": [0, 7], "demandNonRenewable": [4, 0] } - ] - }, - { - "id": 25, "successors": [28], - "modes": - [ - { "duration": 2, "demandRenewable": [2, 0], "demandNonRenewable": [5, 0] }, - { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [0, 5] }, - { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 26, "successors": [28], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [0, 6] }, - { "duration": 4, "demandRenewable": [0, 3], "demandNonRenewable": [0, 6] }, - { "duration": 8, "demandRenewable": [4, 0], "demandNonRenewable": [0, 3] } - ] - }, - { - "id": 27, "successors": [], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 3], "demandNonRenewable": [0, 7] }, - { "duration": 10, "demandRenewable": [2, 0], "demandNonRenewable": [0, 7] }, - { "duration": 10, "demandRenewable": [0, 3], "demandNonRenewable": [0, 6] } - ] - }, - { - "id": 28, "successors": [], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 9], "demandNonRenewable": [0, 7] }, - { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [5, 0] }, - { "duration": 10, "demandRenewable": [4, 0], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 29, "successors": [], - "modes": - [ - { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [0, 6] }, - { "duration": 1, "demandRenewable": [3, 0], "demandNonRenewable": [7, 0] }, - { "duration": 10, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] } - ] - } - ] -} +{ + "capacityRenewable": [33, 42], + "capacityNonRenewable": [77, 94], + "tasks": + [ + { + "id": 0, "successors": [3, 5, 14], + "modes": + [ + { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [10, 0] }, + { "duration": 6, "demandRenewable": [0, 5], "demandNonRenewable": [10, 0] }, + { "duration": 7, "demandRenewable": [0, 4], "demandNonRenewable": [0, 2] } + ] + }, + { + "id": 1, "successors": [4, 6, 7], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] }, + { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [6, 0] }, + { "duration": 8, "demandRenewable": [9, 0], "demandNonRenewable": [0, 9] } + ] + }, + { + "id": 2, "successors": [8, 13], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [0, 9] }, + { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [0, 8] }, + { "duration": 7, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] } + ] + }, + { + "id": 3, "successors": [11, 29], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 7], "demandNonRenewable": [0, 6] }, + { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, + { "duration": 8, "demandRenewable": [8, 0], "demandNonRenewable": [0, 1] } + ] + }, + { + "id": 4, "successors": [17], + "modes": + [ + { "duration": 3, "demandRenewable": [8, 0], "demandNonRenewable": [0, 7] }, + { "duration": 8, "demandRenewable": [7, 0], "demandNonRenewable": [0, 7] }, + { "duration": 8, "demandRenewable": [5, 0], "demandNonRenewable": [9, 0] } + ] + }, + { + "id": 5, "successors": [9, 11, 21], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] }, + { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [3, 0] }, + { "duration": 4, "demandRenewable": [4, 0], "demandNonRenewable": [0, 3] } + ] + }, + { + "id": 6, "successors": [10, 13], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [0, 8] }, + { "duration": 7, "demandRenewable": [6, 0], "demandNonRenewable": [10, 0] }, + { "duration": 7, "demandRenewable": [0, 9], "demandNonRenewable": [1, 0] } + ] + }, + { + "id": 7, "successors": [8, 9, 23], + "modes": + [ + { "duration": 7, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] }, + { "duration": 10, "demandRenewable": [5, 0], "demandNonRenewable": [0, 2] }, + { "duration": 10, "demandRenewable": [0, 5], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 8, "successors": [25], + "modes": + [ + { "duration": 3, "demandRenewable": [8, 0], "demandNonRenewable": [7, 0] }, + { "duration": 6, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] }, + { "duration": 10, "demandRenewable": [0, 7], "demandNonRenewable": [0, 3] } + ] + }, + { + "id": 9, "successors": [10, 15, 24], + "modes": + [ + { "duration": 7, "demandRenewable": [1, 0], "demandNonRenewable": [9, 0] }, + { "duration": 7, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] }, + { "duration": 8, "demandRenewable": [0, 7], "demandNonRenewable": [0, 10] } + ] + }, + { + "id": 10, "successors": [16, 20], + "modes": + [ + { "duration": 3, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] }, + { "duration": 3, "demandRenewable": [0, 10], "demandNonRenewable": [2, 0] }, + { "duration": 10, "demandRenewable": [0, 9], "demandNonRenewable": [2, 0] } + ] + }, + { + "id": 11, "successors": [12, 15, 16], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [6, 0] }, + { "duration": 4, "demandRenewable": [8, 0], "demandNonRenewable": [6, 0] }, + { "duration": 8, "demandRenewable": [0, 4], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 12, "successors": [17], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [0, 6] }, + { "duration": 3, "demandRenewable": [3, 0], "demandNonRenewable": [0, 6] }, + { "duration": 8, "demandRenewable": [0, 8], "demandNonRenewable": [7, 0] } + ] + }, + { + "id": 13, "successors": [15, 16, 21], + "modes": + [ + { "duration": 4, "demandRenewable": [9, 0], "demandNonRenewable": [9, 0] }, + { "duration": 6, "demandRenewable": [0, 9], "demandNonRenewable": [0, 6] }, + { "duration": 9, "demandRenewable": [0, 5], "demandNonRenewable": [7, 0] } + ] + }, + { + "id": 14, "successors": [27], + "modes": + [ + { "duration": 5, "demandRenewable": [6, 0], "demandNonRenewable": [7, 0] }, + { "duration": 8, "demandRenewable": [5, 0], "demandNonRenewable": [0, 7] }, + { "duration": 8, "demandRenewable": [0, 8], "demandNonRenewable": [0, 8] } + ] + }, + { + "id": 15, "successors": [19], + "modes": + [ + { "duration": 3, "demandRenewable": [10, 0], "demandNonRenewable": [0, 4] }, + { "duration": 5, "demandRenewable": [0, 4], "demandNonRenewable": [0, 4] }, + { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 2] } + ] + }, + { + "id": 16, "successors": [18, 26], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 8], "demandNonRenewable": [0, 8] }, + { "duration": 6, "demandRenewable": [0, 7], "demandNonRenewable": [8, 0] }, + { "duration": 7, "demandRenewable": [0, 7], "demandNonRenewable": [0, 6] } + ] + }, + { + "id": 17, "successors": [20], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [0, 5] }, + { "duration": 3, "demandRenewable": [7, 0], "demandNonRenewable": [1, 0] }, + { "duration": 4, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 18, "successors": [22], + "modes": + [ + { "duration": 3, "demandRenewable": [5, 0], "demandNonRenewable": [3, 0] }, + { "duration": 10, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, + { "duration": 10, "demandRenewable": [3, 0], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 19, "successors": [20, 22], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [0, 6] }, + { "duration": 8, "demandRenewable": [0, 9], "demandNonRenewable": [4, 0] }, + { "duration": 8, "demandRenewable": [0, 9], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 20, "successors": [26, 27], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 10], "demandNonRenewable": [0, 10] }, + { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 9] }, + { "duration": 5, "demandRenewable": [0, 3], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 21, "successors": [23, 24, 25], + "modes": + [ + { "duration": 7, "demandRenewable": [0, 10], "demandNonRenewable": [0, 7] }, + { "duration": 7, "demandRenewable": [0, 9], "demandNonRenewable": [4, 0] }, + { "duration": 7, "demandRenewable": [7, 0], "demandNonRenewable": [4, 0] } + ] + }, + { + "id": 22, "successors": [25], + "modes": + [ + { "duration": 2, "demandRenewable": [8, 0], "demandNonRenewable": [0, 3] }, + { "duration": 3, "demandRenewable": [2, 0], "demandNonRenewable": [5, 0] }, + { "duration": 7, "demandRenewable": [0, 1], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 23, "successors": [27, 28, 29], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [5, 0] }, + { "duration": 1, "demandRenewable": [5, 0], "demandNonRenewable": [0, 7] }, + { "duration": 3, "demandRenewable": [4, 0], "demandNonRenewable": [0, 7] } + ] + }, + { + "id": 24, "successors": [26], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [5, 0] }, + { "duration": 10, "demandRenewable": [5, 0], "demandNonRenewable": [3, 0] }, + { "duration": 10, "demandRenewable": [0, 7], "demandNonRenewable": [4, 0] } + ] + }, + { + "id": 25, "successors": [28], + "modes": + [ + { "duration": 2, "demandRenewable": [2, 0], "demandNonRenewable": [5, 0] }, + { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [0, 5] }, + { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 26, "successors": [28], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 7], "demandNonRenewable": [0, 6] }, + { "duration": 4, "demandRenewable": [0, 3], "demandNonRenewable": [0, 6] }, + { "duration": 8, "demandRenewable": [4, 0], "demandNonRenewable": [0, 3] } + ] + }, + { + "id": 27, "successors": [], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 3], "demandNonRenewable": [0, 7] }, + { "duration": 10, "demandRenewable": [2, 0], "demandNonRenewable": [0, 7] }, + { "duration": 10, "demandRenewable": [0, 3], "demandNonRenewable": [0, 6] } + ] + }, + { + "id": 28, "successors": [], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 9], "demandNonRenewable": [0, 7] }, + { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [5, 0] }, + { "duration": 10, "demandRenewable": [4, 0], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 29, "successors": [], + "modes": + [ + { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [0, 6] }, + { "duration": 1, "demandRenewable": [3, 0], "demandNonRenewable": [7, 0] }, + { "duration": 10, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] } + ] + } + ] +} diff --git a/examples/cp/visu/data/rcpspmm_j30_17_7.json b/examples/cp/visu/data/rcpspmm_j30_17_7.json index 781c5ce..6defbaa 100644 --- a/examples/cp/visu/data/rcpspmm_j30_17_7.json +++ b/examples/cp/visu/data/rcpspmm_j30_17_7.json @@ -1,277 +1,277 @@ -{ - "capacityRenewable": [14, 12], - "capacityNonRenewable": [132, 130], - "tasks": - [ - { - "id": 0, "successors": [4, 6, 15], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 2], "demandNonRenewable": [0, 8] }, - { "duration": 5, "demandRenewable": [2, 0], "demandNonRenewable": [0, 4] }, - { "duration": 6, "demandRenewable": [0, 2], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 1, "successors": [3, 5], - "modes": - [ - { "duration": 4, "demandRenewable": [10, 0], "demandNonRenewable": [0, 9] }, - { "duration": 6, "demandRenewable": [10, 0], "demandNonRenewable": [9, 0] }, - { "duration": 9, "demandRenewable": [9, 0], "demandNonRenewable": [0, 8] } - ] - }, - { - "id": 2, "successors": [10, 26, 27], - "modes": - [ - { "duration": 6, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, - { "duration": 9, "demandRenewable": [7, 0], "demandNonRenewable": [0, 3] }, - { "duration": 9, "demandRenewable": [0, 3], "demandNonRenewable": [5, 0] } - ] - }, - { - "id": 3, "successors": [7], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 8], "demandNonRenewable": [3, 0] }, - { "duration": 5, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, - { "duration": 7, "demandRenewable": [2, 0], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 4, "successors": [9], - "modes": - [ - { "duration": 4, "demandRenewable": [2, 0], "demandNonRenewable": [9, 0] }, - { "duration": 7, "demandRenewable": [0, 6], "demandNonRenewable": [9, 0] }, - { "duration": 10, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 5, "successors": [10], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [8, 0] }, - { "duration": 3, "demandRenewable": [5, 0], "demandNonRenewable": [0, 9] }, - { "duration": 5, "demandRenewable": [3, 0], "demandNonRenewable": [0, 3] } - ] - }, - { - "id": 6, "successors": [8, 14, 21], - "modes": - [ - { "duration": 2, "demandRenewable": [4, 0], "demandNonRenewable": [5, 0] }, - { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 5] }, - { "duration": 5, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] } - ] - }, - { - "id": 7, "successors": [11, 14], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [0, 6] }, - { "duration": 7, "demandRenewable": [0, 3], "demandNonRenewable": [0, 5] }, - { "duration": 7, "demandRenewable": [3, 0], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 8, "successors": [12], - "modes": - [ - { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [5, 0] }, - { "duration": 2, "demandRenewable": [0, 5], "demandNonRenewable": [0, 6] }, - { "duration": 4, "demandRenewable": [1, 0], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 9, "successors": [23], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [8, 0] }, - { "duration": 4, "demandRenewable": [0, 9], "demandNonRenewable": [0, 6] }, - { "duration": 5, "demandRenewable": [6, 0], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 10, "successors": [16, 20], - "modes": - [ - { "duration": 1, "demandRenewable": [7, 0], "demandNonRenewable": [0, 5] }, - { "duration": 4, "demandRenewable": [3, 0], "demandNonRenewable": [8, 0] }, - { "duration": 5, "demandRenewable": [2, 0], "demandNonRenewable": [5, 0] } - ] - }, - { - "id": 11, "successors": [12, 13, 20], - "modes": - [ - { "duration": 5, "demandRenewable": [0, 9], "demandNonRenewable": [2, 0] }, - { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [2, 0] }, - { "duration": 7, "demandRenewable": [0, 9], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 12, "successors": [22, 25, 27], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 9], "demandNonRenewable": [0, 10] }, - { "duration": 2, "demandRenewable": [2, 0], "demandNonRenewable": [0, 9] }, - { "duration": 10, "demandRenewable": [2, 0], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 13, "successors": [17, 18, 19], - "modes": - [ - { "duration": 3, "demandRenewable": [10, 0], "demandNonRenewable": [0, 7] }, - { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] }, - { "duration": 10, "demandRenewable": [4, 0], "demandNonRenewable": [7, 0] } - ] - }, - { - "id": 14, "successors": [16, 26, 28], - "modes": - [ - { "duration": 4, "demandRenewable": [7, 0], "demandNonRenewable": [6, 0] }, - { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 9] }, - { "duration": 9, "demandRenewable": [0, 10], "demandNonRenewable": [4, 0] } - ] - }, - { - "id": 15, "successors": [19, 20], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 8], "demandNonRenewable": [3, 0] }, - { "duration": 6, "demandRenewable": [0, 2], "demandNonRenewable": [0, 9] }, - { "duration": 9, "demandRenewable": [6, 0], "demandNonRenewable": [0, 9] } - ] - }, - { - "id": 16, "successors": [29], - "modes": - [ - { "duration": 2, "demandRenewable": [8, 0], "demandNonRenewable": [5, 0] }, - { "duration": 2, "demandRenewable": [0, 7], "demandNonRenewable": [4, 0] }, - { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 17, "successors": [21, 25], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 7], "demandNonRenewable": [6, 0] }, - { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] }, - { "duration": 10, "demandRenewable": [0, 3], "demandNonRenewable": [0, 6] } - ] - }, - { - "id": 18, "successors": [21, 22, 23], - "modes": - [ - { "duration": 5, "demandRenewable": [0, 3], "demandNonRenewable": [5, 0] }, - { "duration": 6, "demandRenewable": [0, 2], "demandNonRenewable": [0, 5] }, - { "duration": 10, "demandRenewable": [0, 2], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 19, "successors": [22, 23, 24], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [5, 0] }, - { "duration": 5, "demandRenewable": [4, 0], "demandNonRenewable": [4, 0] }, - { "duration": 5, "demandRenewable": [5, 0], "demandNonRenewable": [0, 9] } - ] - }, - { - "id": 20, "successors": [24, 29], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [8, 0] }, - { "duration": 5, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, - { "duration": 6, "demandRenewable": [0, 4], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 21, "successors": [24, 27], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 8], "demandNonRenewable": [9, 0] }, - { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [10, 0] }, - { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 22, "successors": [28], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 7], "demandNonRenewable": [8, 0] }, - { "duration": 9, "demandRenewable": [0, 7], "demandNonRenewable": [5, 0] }, - { "duration": 9, "demandRenewable": [8, 0], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 23, "successors": [25], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 5], "demandNonRenewable": [0, 6] }, - { "duration": 2, "demandRenewable": [0, 3], "demandNonRenewable": [9, 0] }, - { "duration": 8, "demandRenewable": [0, 1], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 24, "successors": [28], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, - { "duration": 5, "demandRenewable": [0, 4], "demandNonRenewable": [5, 0] }, - { "duration": 9, "demandRenewable": [0, 3], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 25, "successors": [26], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] }, - { "duration": 3, "demandRenewable": [9, 0], "demandNonRenewable": [2, 0] }, - { "duration": 10, "demandRenewable": [3, 0], "demandNonRenewable": [2, 0] } - ] - }, - { - "id": 26, "successors": [29], - "modes": - [ - { "duration": 2, "demandRenewable": [6, 0], "demandNonRenewable": [6, 0] }, - { "duration": 6, "demandRenewable": [3, 0], "demandNonRenewable": [6, 0] }, - { "duration": 9, "demandRenewable": [1, 0], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 27, "successors": [], - "modes": - [ - { "duration": 2, "demandRenewable": [6, 0], "demandNonRenewable": [0, 6] }, - { "duration": 3, "demandRenewable": [0, 5], "demandNonRenewable": [8, 0] }, - { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [8, 0] } - ] - }, - { - "id": 28, "successors": [], - "modes": - [ - { "duration": 5, "demandRenewable": [10, 0], "demandNonRenewable": [7, 0] }, - { "duration": 5, "demandRenewable": [0, 8], "demandNonRenewable": [8, 0] }, - { "duration": 9, "demandRenewable": [0, 8], "demandNonRenewable": [0, 2] } - ] - }, - { - "id": 29, "successors": [], - "modes": - [ - { "duration": 4, "demandRenewable": [6, 0], "demandNonRenewable": [0, 7] }, - { "duration": 5, "demandRenewable": [0, 4], "demandNonRenewable": [0, 6] }, - { "duration": 8, "demandRenewable": [6, 0], "demandNonRenewable": [4, 0] } - ] - } - ] -} +{ + "capacityRenewable": [14, 12], + "capacityNonRenewable": [132, 130], + "tasks": + [ + { + "id": 0, "successors": [4, 6, 15], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 2], "demandNonRenewable": [0, 8] }, + { "duration": 5, "demandRenewable": [2, 0], "demandNonRenewable": [0, 4] }, + { "duration": 6, "demandRenewable": [0, 2], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 1, "successors": [3, 5], + "modes": + [ + { "duration": 4, "demandRenewable": [10, 0], "demandNonRenewable": [0, 9] }, + { "duration": 6, "demandRenewable": [10, 0], "demandNonRenewable": [9, 0] }, + { "duration": 9, "demandRenewable": [9, 0], "demandNonRenewable": [0, 8] } + ] + }, + { + "id": 2, "successors": [10, 26, 27], + "modes": + [ + { "duration": 6, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, + { "duration": 9, "demandRenewable": [7, 0], "demandNonRenewable": [0, 3] }, + { "duration": 9, "demandRenewable": [0, 3], "demandNonRenewable": [5, 0] } + ] + }, + { + "id": 3, "successors": [7], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 8], "demandNonRenewable": [3, 0] }, + { "duration": 5, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, + { "duration": 7, "demandRenewable": [2, 0], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 4, "successors": [9], + "modes": + [ + { "duration": 4, "demandRenewable": [2, 0], "demandNonRenewable": [9, 0] }, + { "duration": 7, "demandRenewable": [0, 6], "demandNonRenewable": [9, 0] }, + { "duration": 10, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 5, "successors": [10], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [8, 0] }, + { "duration": 3, "demandRenewable": [5, 0], "demandNonRenewable": [0, 9] }, + { "duration": 5, "demandRenewable": [3, 0], "demandNonRenewable": [0, 3] } + ] + }, + { + "id": 6, "successors": [8, 14, 21], + "modes": + [ + { "duration": 2, "demandRenewable": [4, 0], "demandNonRenewable": [5, 0] }, + { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 5] }, + { "duration": 5, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] } + ] + }, + { + "id": 7, "successors": [11, 14], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [0, 6] }, + { "duration": 7, "demandRenewable": [0, 3], "demandNonRenewable": [0, 5] }, + { "duration": 7, "demandRenewable": [3, 0], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 8, "successors": [12], + "modes": + [ + { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [5, 0] }, + { "duration": 2, "demandRenewable": [0, 5], "demandNonRenewable": [0, 6] }, + { "duration": 4, "demandRenewable": [1, 0], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 9, "successors": [23], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [8, 0] }, + { "duration": 4, "demandRenewable": [0, 9], "demandNonRenewable": [0, 6] }, + { "duration": 5, "demandRenewable": [6, 0], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 10, "successors": [16, 20], + "modes": + [ + { "duration": 1, "demandRenewable": [7, 0], "demandNonRenewable": [0, 5] }, + { "duration": 4, "demandRenewable": [3, 0], "demandNonRenewable": [8, 0] }, + { "duration": 5, "demandRenewable": [2, 0], "demandNonRenewable": [5, 0] } + ] + }, + { + "id": 11, "successors": [12, 13, 20], + "modes": + [ + { "duration": 5, "demandRenewable": [0, 9], "demandNonRenewable": [2, 0] }, + { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [2, 0] }, + { "duration": 7, "demandRenewable": [0, 9], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 12, "successors": [22, 25, 27], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 9], "demandNonRenewable": [0, 10] }, + { "duration": 2, "demandRenewable": [2, 0], "demandNonRenewable": [0, 9] }, + { "duration": 10, "demandRenewable": [2, 0], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 13, "successors": [17, 18, 19], + "modes": + [ + { "duration": 3, "demandRenewable": [10, 0], "demandNonRenewable": [0, 7] }, + { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] }, + { "duration": 10, "demandRenewable": [4, 0], "demandNonRenewable": [7, 0] } + ] + }, + { + "id": 14, "successors": [16, 26, 28], + "modes": + [ + { "duration": 4, "demandRenewable": [7, 0], "demandNonRenewable": [6, 0] }, + { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 9] }, + { "duration": 9, "demandRenewable": [0, 10], "demandNonRenewable": [4, 0] } + ] + }, + { + "id": 15, "successors": [19, 20], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 8], "demandNonRenewable": [3, 0] }, + { "duration": 6, "demandRenewable": [0, 2], "demandNonRenewable": [0, 9] }, + { "duration": 9, "demandRenewable": [6, 0], "demandNonRenewable": [0, 9] } + ] + }, + { + "id": 16, "successors": [29], + "modes": + [ + { "duration": 2, "demandRenewable": [8, 0], "demandNonRenewable": [5, 0] }, + { "duration": 2, "demandRenewable": [0, 7], "demandNonRenewable": [4, 0] }, + { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 17, "successors": [21, 25], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 7], "demandNonRenewable": [6, 0] }, + { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] }, + { "duration": 10, "demandRenewable": [0, 3], "demandNonRenewable": [0, 6] } + ] + }, + { + "id": 18, "successors": [21, 22, 23], + "modes": + [ + { "duration": 5, "demandRenewable": [0, 3], "demandNonRenewable": [5, 0] }, + { "duration": 6, "demandRenewable": [0, 2], "demandNonRenewable": [0, 5] }, + { "duration": 10, "demandRenewable": [0, 2], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 19, "successors": [22, 23, 24], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [5, 0] }, + { "duration": 5, "demandRenewable": [4, 0], "demandNonRenewable": [4, 0] }, + { "duration": 5, "demandRenewable": [5, 0], "demandNonRenewable": [0, 9] } + ] + }, + { + "id": 20, "successors": [24, 29], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [8, 0] }, + { "duration": 5, "demandRenewable": [0, 6], "demandNonRenewable": [0, 4] }, + { "duration": 6, "demandRenewable": [0, 4], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 21, "successors": [24, 27], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 8], "demandNonRenewable": [9, 0] }, + { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [10, 0] }, + { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 22, "successors": [28], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 7], "demandNonRenewable": [8, 0] }, + { "duration": 9, "demandRenewable": [0, 7], "demandNonRenewable": [5, 0] }, + { "duration": 9, "demandRenewable": [8, 0], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 23, "successors": [25], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 5], "demandNonRenewable": [0, 6] }, + { "duration": 2, "demandRenewable": [0, 3], "demandNonRenewable": [9, 0] }, + { "duration": 8, "demandRenewable": [0, 1], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 24, "successors": [28], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, + { "duration": 5, "demandRenewable": [0, 4], "demandNonRenewable": [5, 0] }, + { "duration": 9, "demandRenewable": [0, 3], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 25, "successors": [26], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [3, 0] }, + { "duration": 3, "demandRenewable": [9, 0], "demandNonRenewable": [2, 0] }, + { "duration": 10, "demandRenewable": [3, 0], "demandNonRenewable": [2, 0] } + ] + }, + { + "id": 26, "successors": [29], + "modes": + [ + { "duration": 2, "demandRenewable": [6, 0], "demandNonRenewable": [6, 0] }, + { "duration": 6, "demandRenewable": [3, 0], "demandNonRenewable": [6, 0] }, + { "duration": 9, "demandRenewable": [1, 0], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 27, "successors": [], + "modes": + [ + { "duration": 2, "demandRenewable": [6, 0], "demandNonRenewable": [0, 6] }, + { "duration": 3, "demandRenewable": [0, 5], "demandNonRenewable": [8, 0] }, + { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [8, 0] } + ] + }, + { + "id": 28, "successors": [], + "modes": + [ + { "duration": 5, "demandRenewable": [10, 0], "demandNonRenewable": [7, 0] }, + { "duration": 5, "demandRenewable": [0, 8], "demandNonRenewable": [8, 0] }, + { "duration": 9, "demandRenewable": [0, 8], "demandNonRenewable": [0, 2] } + ] + }, + { + "id": 29, "successors": [], + "modes": + [ + { "duration": 4, "demandRenewable": [6, 0], "demandNonRenewable": [0, 7] }, + { "duration": 5, "demandRenewable": [0, 4], "demandNonRenewable": [0, 6] }, + { "duration": 8, "demandRenewable": [6, 0], "demandNonRenewable": [4, 0] } + ] + } + ] +} diff --git a/examples/cp/visu/data/rcpspmm_j30_27_6.json b/examples/cp/visu/data/rcpspmm_j30_27_6.json index 1e0f33d..c57d182 100644 --- a/examples/cp/visu/data/rcpspmm_j30_27_6.json +++ b/examples/cp/visu/data/rcpspmm_j30_27_6.json @@ -1,277 +1,277 @@ -{ - "capacityRenewable": [26, 29], - "capacityNonRenewable": [188, 157], - "tasks": - [ - { - "id": 0, "successors": [4, 5], - "modes": - [ - { "duration": 4, "demandRenewable": [3, 0], "demandNonRenewable": [0, 4] }, - { "duration": 8, "demandRenewable": [0, 6], "demandNonRenewable": [4, 0] }, - { "duration": 10, "demandRenewable": [0, 4], "demandNonRenewable": [4, 0] } - ] - }, - { - "id": 1, "successors": [6, 14], - "modes": - [ - { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] }, - { "duration": 7, "demandRenewable": [6, 0], "demandNonRenewable": [6, 0] }, - { "duration": 7, "demandRenewable": [7, 0], "demandNonRenewable": [5, 0] } - ] - }, - { - "id": 2, "successors": [3, 12, 13], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [0, 6] }, - { "duration": 8, "demandRenewable": [0, 6], "demandNonRenewable": [3, 0] }, - { "duration": 10, "demandRenewable": [0, 4], "demandNonRenewable": [0, 3] } - ] - }, - { - "id": 3, "successors": [7, 20, 24], - "modes": - [ - { "duration": 1, "demandRenewable": [7, 0], "demandNonRenewable": [9, 0] }, - { "duration": 3, "demandRenewable": [4, 0], "demandNonRenewable": [0, 3] }, - { "duration": 9, "demandRenewable": [3, 0], "demandNonRenewable": [0, 3] } - ] - }, - { - "id": 4, "successors": [16], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 5], "demandNonRenewable": [0, 7] }, - { "duration": 6, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] }, - { "duration": 9, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] } - ] - }, - { - "id": 5, "successors": [8, 10, 24], - "modes": - [ - { "duration": 1, "demandRenewable": [0, 4], "demandNonRenewable": [9, 0] }, - { "duration": 5, "demandRenewable": [0, 1], "demandNonRenewable": [0, 4] }, - { "duration": 8, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] } - ] - }, - { - "id": 6, "successors": [18, 25], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 5], "demandNonRenewable": [0, 8] }, - { "duration": 5, "demandRenewable": [7, 0], "demandNonRenewable": [5, 0] }, - { "duration": 10, "demandRenewable": [6, 0], "demandNonRenewable": [0, 8] } - ] - }, - { - "id": 7, "successors": [11, 21, 23], - "modes": - [ - { "duration": 3, "demandRenewable": [3, 0], "demandNonRenewable": [0, 9] }, - { "duration": 4, "demandRenewable": [0, 4], "demandNonRenewable": [5, 0] }, - { "duration": 10, "demandRenewable": [0, 1], "demandNonRenewable": [0, 8] } - ] - }, - { - "id": 8, "successors": [9, 17, 26], - "modes": - [ - { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [3, 0] }, - { "duration": 3, "demandRenewable": [0, 2], "demandNonRenewable": [2, 0] }, - { "duration": 5, "demandRenewable": [3, 0], "demandNonRenewable": [2, 0] } - ] - }, - { - "id": 9, "successors": [13, 14], - "modes": - [ - { "duration": 1, "demandRenewable": [9, 0], "demandNonRenewable": [4, 0] }, - { "duration": 2, "demandRenewable": [0, 6], "demandNonRenewable": [3, 0] }, - { "duration": 8, "demandRenewable": [8, 0], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 10, "successors": [15, 21], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 7], "demandNonRenewable": [0, 9] }, - { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [5, 0] }, - { "duration": 6, "demandRenewable": [0, 6], "demandNonRenewable": [0, 9] } - ] - }, - { - "id": 11, "successors": [26, 29], - "modes": - [ - { "duration": 6, "demandRenewable": [8, 0], "demandNonRenewable": [9, 0] }, - { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 7] }, - { "duration": 8, "demandRenewable": [0, 9], "demandNonRenewable": [10, 0] } - ] - }, - { - "id": 12, "successors": [15, 17, 22], - "modes": - [ - { "duration": 4, "demandRenewable": [3, 0], "demandNonRenewable": [0, 9] }, - { "duration": 7, "demandRenewable": [0, 6], "demandNonRenewable": [0, 7] }, - { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 13, "successors": [19], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 10], "demandNonRenewable": [6, 0] }, - { "duration": 6, "demandRenewable": [0, 9], "demandNonRenewable": [0, 5] }, - { "duration": 10, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] } - ] - }, - { - "id": 14, "successors": [15], - "modes": - [ - { "duration": 1, "demandRenewable": [6, 0], "demandNonRenewable": [7, 0] }, - { "duration": 4, "demandRenewable": [5, 0], "demandNonRenewable": [0, 9] }, - { "duration": 8, "demandRenewable": [0, 7], "demandNonRenewable": [3, 0] } - ] - }, - { - "id": 15, "successors": [29], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [0, 8] }, - { "duration": 3, "demandRenewable": [0, 4], "demandNonRenewable": [0, 2] }, - { "duration": 7, "demandRenewable": [5, 0], "demandNonRenewable": [7, 0] } - ] - }, - { - "id": 16, "successors": [19, 21], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [8, 0] }, - { "duration": 7, "demandRenewable": [9, 0], "demandNonRenewable": [8, 0] }, - { "duration": 8, "demandRenewable": [0, 5], "demandNonRenewable": [8, 0] } - ] - }, - { - "id": 17, "successors": [23], - "modes": - [ - { "duration": 4, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] }, - { "duration": 9, "demandRenewable": [6, 0], "demandNonRenewable": [3, 0] }, - { "duration": 10, "demandRenewable": [3, 0], "demandNonRenewable": [0, 6] } - ] - }, - { - "id": 18, "successors": [20, 22], - "modes": - [ - { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [6, 0] }, - { "duration": 2, "demandRenewable": [8, 0], "demandNonRenewable": [0, 2] }, - { "duration": 4, "demandRenewable": [7, 0], "demandNonRenewable": [6, 0] } - ] - }, - { - "id": 19, "successors": [20, 23], - "modes": - [ - { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [0, 6] }, - { "duration": 7, "demandRenewable": [0, 4], "demandNonRenewable": [9, 0] }, - { "duration": 9, "demandRenewable": [0, 3], "demandNonRenewable": [7, 0] } - ] - }, - { - "id": 20, "successors": [28], - "modes": - [ - { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] }, - { "duration": 7, "demandRenewable": [0, 1], "demandNonRenewable": [0, 3] }, - { "duration": 8, "demandRenewable": [3, 0], "demandNonRenewable": [0, 1] } - ] - }, - { - "id": 21, "successors": [25, 26, 29], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [9, 0] }, - { "duration": 7, "demandRenewable": [0, 2], "demandNonRenewable": [9, 0] }, - { "duration": 8, "demandRenewable": [2, 0], "demandNonRenewable": [8, 0] } - ] - }, - { - "id": 22, "successors": [24, 27], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, - { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [5, 0] }, - { "duration": 10, "demandRenewable": [0, 3], "demandNonRenewable": [1, 0] } - ] - }, - { - "id": 23, "successors": [25, 27], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 8], "demandNonRenewable": [10, 0] }, - { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, - { "duration": 10, "demandRenewable": [0, 2], "demandNonRenewable": [0, 6] } - ] - }, - { - "id": 24, "successors": [28], - "modes": - [ - { "duration": 3, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] }, - { "duration": 7, "demandRenewable": [4, 0], "demandNonRenewable": [9, 0] }, - { "duration": 8, "demandRenewable": [0, 8], "demandNonRenewable": [8, 0] } - ] - }, - { - "id": 25, "successors": [28], - "modes": - [ - { "duration": 3, "demandRenewable": [0, 5], "demandNonRenewable": [0, 7] }, - { "duration": 4, "demandRenewable": [0, 5], "demandNonRenewable": [7, 0] }, - { "duration": 8, "demandRenewable": [3, 0], "demandNonRenewable": [0, 7] } - ] - }, - { - "id": 26, "successors": [27], - "modes": - [ - { "duration": 6, "demandRenewable": [3, 0], "demandNonRenewable": [3, 0] }, - { "duration": 7, "demandRenewable": [0, 3], "demandNonRenewable": [3, 0] }, - { "duration": 9, "demandRenewable": [0, 2], "demandNonRenewable": [0, 8] } - ] - }, - { - "id": 27, "successors": [], - "modes": - [ - { "duration": 4, "demandRenewable": [0, 4], "demandNonRenewable": [0, 7] }, - { "duration": 8, "demandRenewable": [0, 4], "demandNonRenewable": [0, 6] }, - { "duration": 8, "demandRenewable": [3, 0], "demandNonRenewable": [7, 0] } - ] - }, - { - "id": 28, "successors": [], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [10, 0] }, - { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [5, 0] }, - { "duration": 8, "demandRenewable": [0, 2], "demandNonRenewable": [5, 0] } - ] - }, - { - "id": 29, "successors": [], - "modes": - [ - { "duration": 1, "demandRenewable": [6, 0], "demandNonRenewable": [9, 0] }, - { "duration": 5, "demandRenewable": [6, 0], "demandNonRenewable": [0, 7] }, - { "duration": 9, "demandRenewable": [0, 8], "demandNonRenewable": [0, 3] } - ] - } - ] -} +{ + "capacityRenewable": [26, 29], + "capacityNonRenewable": [188, 157], + "tasks": + [ + { + "id": 0, "successors": [4, 5], + "modes": + [ + { "duration": 4, "demandRenewable": [3, 0], "demandNonRenewable": [0, 4] }, + { "duration": 8, "demandRenewable": [0, 6], "demandNonRenewable": [4, 0] }, + { "duration": 10, "demandRenewable": [0, 4], "demandNonRenewable": [4, 0] } + ] + }, + { + "id": 1, "successors": [6, 14], + "modes": + [ + { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] }, + { "duration": 7, "demandRenewable": [6, 0], "demandNonRenewable": [6, 0] }, + { "duration": 7, "demandRenewable": [7, 0], "demandNonRenewable": [5, 0] } + ] + }, + { + "id": 2, "successors": [3, 12, 13], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [0, 6] }, + { "duration": 8, "demandRenewable": [0, 6], "demandNonRenewable": [3, 0] }, + { "duration": 10, "demandRenewable": [0, 4], "demandNonRenewable": [0, 3] } + ] + }, + { + "id": 3, "successors": [7, 20, 24], + "modes": + [ + { "duration": 1, "demandRenewable": [7, 0], "demandNonRenewable": [9, 0] }, + { "duration": 3, "demandRenewable": [4, 0], "demandNonRenewable": [0, 3] }, + { "duration": 9, "demandRenewable": [3, 0], "demandNonRenewable": [0, 3] } + ] + }, + { + "id": 4, "successors": [16], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 5], "demandNonRenewable": [0, 7] }, + { "duration": 6, "demandRenewable": [0, 5], "demandNonRenewable": [5, 0] }, + { "duration": 9, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] } + ] + }, + { + "id": 5, "successors": [8, 10, 24], + "modes": + [ + { "duration": 1, "demandRenewable": [0, 4], "demandNonRenewable": [9, 0] }, + { "duration": 5, "demandRenewable": [0, 1], "demandNonRenewable": [0, 4] }, + { "duration": 8, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] } + ] + }, + { + "id": 6, "successors": [18, 25], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 5], "demandNonRenewable": [0, 8] }, + { "duration": 5, "demandRenewable": [7, 0], "demandNonRenewable": [5, 0] }, + { "duration": 10, "demandRenewable": [6, 0], "demandNonRenewable": [0, 8] } + ] + }, + { + "id": 7, "successors": [11, 21, 23], + "modes": + [ + { "duration": 3, "demandRenewable": [3, 0], "demandNonRenewable": [0, 9] }, + { "duration": 4, "demandRenewable": [0, 4], "demandNonRenewable": [5, 0] }, + { "duration": 10, "demandRenewable": [0, 1], "demandNonRenewable": [0, 8] } + ] + }, + { + "id": 8, "successors": [9, 17, 26], + "modes": + [ + { "duration": 1, "demandRenewable": [4, 0], "demandNonRenewable": [3, 0] }, + { "duration": 3, "demandRenewable": [0, 2], "demandNonRenewable": [2, 0] }, + { "duration": 5, "demandRenewable": [3, 0], "demandNonRenewable": [2, 0] } + ] + }, + { + "id": 9, "successors": [13, 14], + "modes": + [ + { "duration": 1, "demandRenewable": [9, 0], "demandNonRenewable": [4, 0] }, + { "duration": 2, "demandRenewable": [0, 6], "demandNonRenewable": [3, 0] }, + { "duration": 8, "demandRenewable": [8, 0], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 10, "successors": [15, 21], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 7], "demandNonRenewable": [0, 9] }, + { "duration": 6, "demandRenewable": [1, 0], "demandNonRenewable": [5, 0] }, + { "duration": 6, "demandRenewable": [0, 6], "demandNonRenewable": [0, 9] } + ] + }, + { + "id": 11, "successors": [26, 29], + "modes": + [ + { "duration": 6, "demandRenewable": [8, 0], "demandNonRenewable": [9, 0] }, + { "duration": 6, "demandRenewable": [7, 0], "demandNonRenewable": [0, 7] }, + { "duration": 8, "demandRenewable": [0, 9], "demandNonRenewable": [10, 0] } + ] + }, + { + "id": 12, "successors": [15, 17, 22], + "modes": + [ + { "duration": 4, "demandRenewable": [3, 0], "demandNonRenewable": [0, 9] }, + { "duration": 7, "demandRenewable": [0, 6], "demandNonRenewable": [0, 7] }, + { "duration": 9, "demandRenewable": [0, 4], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 13, "successors": [19], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 10], "demandNonRenewable": [6, 0] }, + { "duration": 6, "demandRenewable": [0, 9], "demandNonRenewable": [0, 5] }, + { "duration": 10, "demandRenewable": [9, 0], "demandNonRenewable": [0, 4] } + ] + }, + { + "id": 14, "successors": [15], + "modes": + [ + { "duration": 1, "demandRenewable": [6, 0], "demandNonRenewable": [7, 0] }, + { "duration": 4, "demandRenewable": [5, 0], "demandNonRenewable": [0, 9] }, + { "duration": 8, "demandRenewable": [0, 7], "demandNonRenewable": [3, 0] } + ] + }, + { + "id": 15, "successors": [29], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 0], "demandNonRenewable": [0, 8] }, + { "duration": 3, "demandRenewable": [0, 4], "demandNonRenewable": [0, 2] }, + { "duration": 7, "demandRenewable": [5, 0], "demandNonRenewable": [7, 0] } + ] + }, + { + "id": 16, "successors": [19, 21], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 9], "demandNonRenewable": [8, 0] }, + { "duration": 7, "demandRenewable": [9, 0], "demandNonRenewable": [8, 0] }, + { "duration": 8, "demandRenewable": [0, 5], "demandNonRenewable": [8, 0] } + ] + }, + { + "id": 17, "successors": [23], + "modes": + [ + { "duration": 4, "demandRenewable": [7, 0], "demandNonRenewable": [7, 0] }, + { "duration": 9, "demandRenewable": [6, 0], "demandNonRenewable": [3, 0] }, + { "duration": 10, "demandRenewable": [3, 0], "demandNonRenewable": [0, 6] } + ] + }, + { + "id": 18, "successors": [20, 22], + "modes": + [ + { "duration": 2, "demandRenewable": [0, 4], "demandNonRenewable": [6, 0] }, + { "duration": 2, "demandRenewable": [8, 0], "demandNonRenewable": [0, 2] }, + { "duration": 4, "demandRenewable": [7, 0], "demandNonRenewable": [6, 0] } + ] + }, + { + "id": 19, "successors": [20, 23], + "modes": + [ + { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [0, 6] }, + { "duration": 7, "demandRenewable": [0, 4], "demandNonRenewable": [9, 0] }, + { "duration": 9, "demandRenewable": [0, 3], "demandNonRenewable": [7, 0] } + ] + }, + { + "id": 20, "successors": [28], + "modes": + [ + { "duration": 6, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] }, + { "duration": 7, "demandRenewable": [0, 1], "demandNonRenewable": [0, 3] }, + { "duration": 8, "demandRenewable": [3, 0], "demandNonRenewable": [0, 1] } + ] + }, + { + "id": 21, "successors": [25, 26, 29], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [9, 0] }, + { "duration": 7, "demandRenewable": [0, 2], "demandNonRenewable": [9, 0] }, + { "duration": 8, "demandRenewable": [2, 0], "demandNonRenewable": [8, 0] } + ] + }, + { + "id": 22, "successors": [24, 27], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, + { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [5, 0] }, + { "duration": 10, "demandRenewable": [0, 3], "demandNonRenewable": [1, 0] } + ] + }, + { + "id": 23, "successors": [25, 27], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 8], "demandNonRenewable": [10, 0] }, + { "duration": 3, "demandRenewable": [0, 6], "demandNonRenewable": [0, 6] }, + { "duration": 10, "demandRenewable": [0, 2], "demandNonRenewable": [0, 6] } + ] + }, + { + "id": 24, "successors": [28], + "modes": + [ + { "duration": 3, "demandRenewable": [6, 0], "demandNonRenewable": [0, 5] }, + { "duration": 7, "demandRenewable": [4, 0], "demandNonRenewable": [9, 0] }, + { "duration": 8, "demandRenewable": [0, 8], "demandNonRenewable": [8, 0] } + ] + }, + { + "id": 25, "successors": [28], + "modes": + [ + { "duration": 3, "demandRenewable": [0, 5], "demandNonRenewable": [0, 7] }, + { "duration": 4, "demandRenewable": [0, 5], "demandNonRenewable": [7, 0] }, + { "duration": 8, "demandRenewable": [3, 0], "demandNonRenewable": [0, 7] } + ] + }, + { + "id": 26, "successors": [27], + "modes": + [ + { "duration": 6, "demandRenewable": [3, 0], "demandNonRenewable": [3, 0] }, + { "duration": 7, "demandRenewable": [0, 3], "demandNonRenewable": [3, 0] }, + { "duration": 9, "demandRenewable": [0, 2], "demandNonRenewable": [0, 8] } + ] + }, + { + "id": 27, "successors": [], + "modes": + [ + { "duration": 4, "demandRenewable": [0, 4], "demandNonRenewable": [0, 7] }, + { "duration": 8, "demandRenewable": [0, 4], "demandNonRenewable": [0, 6] }, + { "duration": 8, "demandRenewable": [3, 0], "demandNonRenewable": [7, 0] } + ] + }, + { + "id": 28, "successors": [], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 0], "demandNonRenewable": [10, 0] }, + { "duration": 4, "demandRenewable": [0, 6], "demandNonRenewable": [5, 0] }, + { "duration": 8, "demandRenewable": [0, 2], "demandNonRenewable": [5, 0] } + ] + }, + { + "id": 29, "successors": [], + "modes": + [ + { "duration": 1, "demandRenewable": [6, 0], "demandNonRenewable": [9, 0] }, + { "duration": 5, "demandRenewable": [6, 0], "demandNonRenewable": [0, 7] }, + { "duration": 9, "demandRenewable": [0, 8], "demandNonRenewable": [0, 3] } + ] + } + ] +} diff --git a/examples/cp/visu/data/rcpspmm_j30_46_1.json b/examples/cp/visu/data/rcpspmm_j30_46_1.json index 3fa6b88..8ad5fab 100644 --- a/examples/cp/visu/data/rcpspmm_j30_46_1.json +++ b/examples/cp/visu/data/rcpspmm_j30_46_1.json @@ -1,277 +1,277 @@ -{ - "capacityRenewable": [25, 29], - "capacityNonRenewable": [159, 157], - "tasks": - [ - { - "id": 0, "successors": [6, 9, 10], - "modes": - [ - { "duration": 3, "demandRenewable": [7, 4], "demandNonRenewable": [6, 7] }, - { "duration": 4, "demandRenewable": [7, 4], "demandNonRenewable": [5, 7] }, - { "duration": 6, "demandRenewable": [6, 4], "demandNonRenewable": [4, 5] } - ] - }, - { - "id": 1, "successors": [5, 7, 17], - "modes": - [ - { "duration": 1, "demandRenewable": [6, 7], "demandNonRenewable": [9, 8] }, - { "duration": 2, "demandRenewable": [4, 5], "demandNonRenewable": [9, 7] }, - { "duration": 7, "demandRenewable": [3, 5], "demandNonRenewable": [8, 7] } - ] - }, - { - "id": 2, "successors": [3, 4, 8], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 6], "demandNonRenewable": [6, 5] }, - { "duration": 5, "demandRenewable": [5, 6], "demandNonRenewable": [6, 5] }, - { "duration": 6, "demandRenewable": [1, 5], "demandNonRenewable": [5, 4] } - ] - }, - { - "id": 3, "successors": [9, 11, 19], - "modes": - [ - { "duration": 1, "demandRenewable": [8, 9], "demandNonRenewable": [10, 6] }, - { "duration": 1, "demandRenewable": [9, 8], "demandNonRenewable": [10, 6] }, - { "duration": 9, "demandRenewable": [7, 6], "demandNonRenewable": [8, 6] } - ] - }, - { - "id": 4, "successors": [16, 25], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 6], "demandNonRenewable": [5, 5] }, - { "duration": 6, "demandRenewable": [4, 3], "demandNonRenewable": [5, 5] }, - { "duration": 10, "demandRenewable": [2, 3], "demandNonRenewable": [5, 5] } - ] - }, - { - "id": 5, "successors": [19], - "modes": - [ - { "duration": 1, "demandRenewable": [3, 5], "demandNonRenewable": [9, 3] }, - { "duration": 6, "demandRenewable": [3, 4], "demandNonRenewable": [6, 3] }, - { "duration": 7, "demandRenewable": [1, 3], "demandNonRenewable": [5, 2] } - ] - }, - { - "id": 6, "successors": [19, 26, 27], - "modes": - [ - { "duration": 2, "demandRenewable": [2, 8], "demandNonRenewable": [4, 8] }, - { "duration": 4, "demandRenewable": [2, 6], "demandNonRenewable": [3, 5] }, - { "duration": 9, "demandRenewable": [2, 6], "demandNonRenewable": [2, 3] } - ] - }, - { - "id": 7, "successors": [12], - "modes": - [ - { "duration": 4, "demandRenewable": [3, 6], "demandNonRenewable": [10, 4] }, - { "duration": 5, "demandRenewable": [3, 6], "demandNonRenewable": [9, 4] }, - { "duration": 9, "demandRenewable": [2, 6], "demandNonRenewable": [8, 2] } - ] - }, - { - "id": 8, "successors": [13, 23], - "modes": - [ - { "duration": 3, "demandRenewable": [8, 3], "demandNonRenewable": [8, 6] }, - { "duration": 7, "demandRenewable": [8, 3], "demandNonRenewable": [7, 4] }, - { "duration": 9, "demandRenewable": [8, 3], "demandNonRenewable": [7, 3] } - ] - }, - { - "id": 9, "successors": [14, 23], - "modes": - [ - { "duration": 1, "demandRenewable": [9, 9], "demandNonRenewable": [3, 10] }, - { "duration": 3, "demandRenewable": [9, 9], "demandNonRenewable": [2, 10] }, - { "duration": 7, "demandRenewable": [9, 7], "demandNonRenewable": [2, 10] } - ] - }, - { - "id": 10, "successors": [13, 18], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 9], "demandNonRenewable": [7, 8] }, - { "duration": 4, "demandRenewable": [3, 1], "demandNonRenewable": [7, 7] }, - { "duration": 4, "demandRenewable": [2, 5], "demandNonRenewable": [7, 6] } - ] - }, - { - "id": 11, "successors": [29], - "modes": - [ - { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [5, 8] }, - { "duration": 5, "demandRenewable": [5, 7], "demandNonRenewable": [5, 5] }, - { "duration": 6, "demandRenewable": [2, 7], "demandNonRenewable": [4, 2] } - ] - }, - { - "id": 12, "successors": [14], - "modes": - [ - { "duration": 4, "demandRenewable": [9, 7], "demandNonRenewable": [7, 5] }, - { "duration": 8, "demandRenewable": [7, 6], "demandNonRenewable": [5, 5] }, - { "duration": 9, "demandRenewable": [5, 6], "demandNonRenewable": [3, 5] } - ] - }, - { - "id": 13, "successors": [14, 16, 17], - "modes": - [ - { "duration": 1, "demandRenewable": [9, 8], "demandNonRenewable": [6, 7] }, - { "duration": 2, "demandRenewable": [9, 7], "demandNonRenewable": [5, 4] }, - { "duration": 6, "demandRenewable": [8, 7], "demandNonRenewable": [3, 4] } - ] - }, - { - "id": 14, "successors": [15, 21], - "modes": - [ - { "duration": 3, "demandRenewable": [8, 10], "demandNonRenewable": [10, 10] }, - { "duration": 6, "demandRenewable": [8, 9], "demandNonRenewable": [6, 7] }, - { "duration": 8, "demandRenewable": [6, 9], "demandNonRenewable": [4, 7] } - ] - }, - { - "id": 15, "successors": [20, 26], - "modes": - [ - { "duration": 1, "demandRenewable": [6, 5], "demandNonRenewable": [8, 8] }, - { "duration": 6, "demandRenewable": [4, 4], "demandNonRenewable": [8, 7] }, - { "duration": 10, "demandRenewable": [3, 4], "demandNonRenewable": [7, 6] } - ] - }, - { - "id": 16, "successors": [24, 26], - "modes": - [ - { "duration": 2, "demandRenewable": [10, 6], "demandNonRenewable": [4, 8] }, - { "duration": 7, "demandRenewable": [8, 5], "demandNonRenewable": [4, 7] }, - { "duration": 10, "demandRenewable": [7, 4], "demandNonRenewable": [4, 6] } - ] - }, - { - "id": 17, "successors": [21], - "modes": - [ - { "duration": 2, "demandRenewable": [6, 4], "demandNonRenewable": [6, 3] }, - { "duration": 3, "demandRenewable": [4, 3], "demandNonRenewable": [6, 3] }, - { "duration": 4, "demandRenewable": [2, 3], "demandNonRenewable": [2, 2] } - ] - }, - { - "id": 18, "successors": [23, 24], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 10], "demandNonRenewable": [7, 5] }, - { "duration": 5, "demandRenewable": [9, 9], "demandNonRenewable": [6, 5] }, - { "duration": 6, "demandRenewable": [8, 9], "demandNonRenewable": [4, 5] } - ] - }, - { - "id": 19, "successors": [22], - "modes": - [ - { "duration": 1, "demandRenewable": [4, 9], "demandNonRenewable": [6, 8] }, - { "duration": 3, "demandRenewable": [4, 6], "demandNonRenewable": [5, 5] }, - { "duration": 10, "demandRenewable": [3, 1], "demandNonRenewable": [5, 3] } - ] - }, - { - "id": 20, "successors": [22, 25], - "modes": - [ - { "duration": 2, "demandRenewable": [6, 4], "demandNonRenewable": [5, 9] }, - { "duration": 4, "demandRenewable": [5, 3], "demandNonRenewable": [5, 9] }, - { "duration": 5, "demandRenewable": [2, 2], "demandNonRenewable": [1, 7] } - ] - }, - { - "id": 21, "successors": [22, 25], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 8], "demandNonRenewable": [5, 5] }, - { "duration": 3, "demandRenewable": [7, 8], "demandNonRenewable": [3, 5] }, - { "duration": 5, "demandRenewable": [4, 8], "demandNonRenewable": [2, 4] } - ] - }, - { - "id": 22, "successors": [24], - "modes": - [ - { "duration": 6, "demandRenewable": [4, 7], "demandNonRenewable": [4, 4] }, - { "duration": 8, "demandRenewable": [1, 7], "demandNonRenewable": [4, 2] }, - { "duration": 8, "demandRenewable": [1, 7], "demandNonRenewable": [2, 4] } - ] - }, - { - "id": 23, "successors": [27, 28, 29], - "modes": - [ - { "duration": 3, "demandRenewable": [8, 8], "demandNonRenewable": [5, 2] }, - { "duration": 4, "demandRenewable": [7, 7], "demandNonRenewable": [4, 2] }, - { "duration": 8, "demandRenewable": [5, 4], "demandNonRenewable": [3, 2] } - ] - }, - { - "id": 24, "successors": [28], - "modes": - [ - { "duration": 2, "demandRenewable": [4, 7], "demandNonRenewable": [9, 6] }, - { "duration": 6, "demandRenewable": [3, 6], "demandNonRenewable": [8, 5] }, - { "duration": 8, "demandRenewable": [3, 2], "demandNonRenewable": [8, 4] } - ] - }, - { - "id": 25, "successors": [27, 29], - "modes": - [ - { "duration": 7, "demandRenewable": [7, 3], "demandNonRenewable": [5, 5] }, - { "duration": 10, "demandRenewable": [7, 3], "demandNonRenewable": [2, 5] }, - { "duration": 10, "demandRenewable": [7, 3], "demandNonRenewable": [4, 3] } - ] - }, - { - "id": 26, "successors": [28], - "modes": - [ - { "duration": 5, "demandRenewable": [7, 8], "demandNonRenewable": [7, 9] }, - { "duration": 7, "demandRenewable": [7, 7], "demandNonRenewable": [6, 7] }, - { "duration": 8, "demandRenewable": [6, 6], "demandNonRenewable": [3, 7] } - ] - }, - { - "id": 27, "successors": [], - "modes": - [ - { "duration": 1, "demandRenewable": [7, 9], "demandNonRenewable": [4, 2] }, - { "duration": 5, "demandRenewable": [5, 6], "demandNonRenewable": [3, 2] }, - { "duration": 8, "demandRenewable": [2, 5], "demandNonRenewable": [1, 2] } - ] - }, - { - "id": 28, "successors": [], - "modes": - [ - { "duration": 6, "demandRenewable": [4, 7], "demandNonRenewable": [9, 2] }, - { "duration": 8, "demandRenewable": [3, 5], "demandNonRenewable": [6, 2] }, - { "duration": 9, "demandRenewable": [3, 1], "demandNonRenewable": [3, 1] } - ] - }, - { - "id": 29, "successors": [], - "modes": - [ - { "duration": 3, "demandRenewable": [8, 7], "demandNonRenewable": [4, 8] }, - { "duration": 7, "demandRenewable": [8, 5], "demandNonRenewable": [4, 6] }, - { "duration": 8, "demandRenewable": [6, 3], "demandNonRenewable": [3, 4] } - ] - } - ] -} +{ + "capacityRenewable": [25, 29], + "capacityNonRenewable": [159, 157], + "tasks": + [ + { + "id": 0, "successors": [6, 9, 10], + "modes": + [ + { "duration": 3, "demandRenewable": [7, 4], "demandNonRenewable": [6, 7] }, + { "duration": 4, "demandRenewable": [7, 4], "demandNonRenewable": [5, 7] }, + { "duration": 6, "demandRenewable": [6, 4], "demandNonRenewable": [4, 5] } + ] + }, + { + "id": 1, "successors": [5, 7, 17], + "modes": + [ + { "duration": 1, "demandRenewable": [6, 7], "demandNonRenewable": [9, 8] }, + { "duration": 2, "demandRenewable": [4, 5], "demandNonRenewable": [9, 7] }, + { "duration": 7, "demandRenewable": [3, 5], "demandNonRenewable": [8, 7] } + ] + }, + { + "id": 2, "successors": [3, 4, 8], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 6], "demandNonRenewable": [6, 5] }, + { "duration": 5, "demandRenewable": [5, 6], "demandNonRenewable": [6, 5] }, + { "duration": 6, "demandRenewable": [1, 5], "demandNonRenewable": [5, 4] } + ] + }, + { + "id": 3, "successors": [9, 11, 19], + "modes": + [ + { "duration": 1, "demandRenewable": [8, 9], "demandNonRenewable": [10, 6] }, + { "duration": 1, "demandRenewable": [9, 8], "demandNonRenewable": [10, 6] }, + { "duration": 9, "demandRenewable": [7, 6], "demandNonRenewable": [8, 6] } + ] + }, + { + "id": 4, "successors": [16, 25], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 6], "demandNonRenewable": [5, 5] }, + { "duration": 6, "demandRenewable": [4, 3], "demandNonRenewable": [5, 5] }, + { "duration": 10, "demandRenewable": [2, 3], "demandNonRenewable": [5, 5] } + ] + }, + { + "id": 5, "successors": [19], + "modes": + [ + { "duration": 1, "demandRenewable": [3, 5], "demandNonRenewable": [9, 3] }, + { "duration": 6, "demandRenewable": [3, 4], "demandNonRenewable": [6, 3] }, + { "duration": 7, "demandRenewable": [1, 3], "demandNonRenewable": [5, 2] } + ] + }, + { + "id": 6, "successors": [19, 26, 27], + "modes": + [ + { "duration": 2, "demandRenewable": [2, 8], "demandNonRenewable": [4, 8] }, + { "duration": 4, "demandRenewable": [2, 6], "demandNonRenewable": [3, 5] }, + { "duration": 9, "demandRenewable": [2, 6], "demandNonRenewable": [2, 3] } + ] + }, + { + "id": 7, "successors": [12], + "modes": + [ + { "duration": 4, "demandRenewable": [3, 6], "demandNonRenewable": [10, 4] }, + { "duration": 5, "demandRenewable": [3, 6], "demandNonRenewable": [9, 4] }, + { "duration": 9, "demandRenewable": [2, 6], "demandNonRenewable": [8, 2] } + ] + }, + { + "id": 8, "successors": [13, 23], + "modes": + [ + { "duration": 3, "demandRenewable": [8, 3], "demandNonRenewable": [8, 6] }, + { "duration": 7, "demandRenewable": [8, 3], "demandNonRenewable": [7, 4] }, + { "duration": 9, "demandRenewable": [8, 3], "demandNonRenewable": [7, 3] } + ] + }, + { + "id": 9, "successors": [14, 23], + "modes": + [ + { "duration": 1, "demandRenewable": [9, 9], "demandNonRenewable": [3, 10] }, + { "duration": 3, "demandRenewable": [9, 9], "demandNonRenewable": [2, 10] }, + { "duration": 7, "demandRenewable": [9, 7], "demandNonRenewable": [2, 10] } + ] + }, + { + "id": 10, "successors": [13, 18], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 9], "demandNonRenewable": [7, 8] }, + { "duration": 4, "demandRenewable": [3, 1], "demandNonRenewable": [7, 7] }, + { "duration": 4, "demandRenewable": [2, 5], "demandNonRenewable": [7, 6] } + ] + }, + { + "id": 11, "successors": [29], + "modes": + [ + { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [5, 8] }, + { "duration": 5, "demandRenewable": [5, 7], "demandNonRenewable": [5, 5] }, + { "duration": 6, "demandRenewable": [2, 7], "demandNonRenewable": [4, 2] } + ] + }, + { + "id": 12, "successors": [14], + "modes": + [ + { "duration": 4, "demandRenewable": [9, 7], "demandNonRenewable": [7, 5] }, + { "duration": 8, "demandRenewable": [7, 6], "demandNonRenewable": [5, 5] }, + { "duration": 9, "demandRenewable": [5, 6], "demandNonRenewable": [3, 5] } + ] + }, + { + "id": 13, "successors": [14, 16, 17], + "modes": + [ + { "duration": 1, "demandRenewable": [9, 8], "demandNonRenewable": [6, 7] }, + { "duration": 2, "demandRenewable": [9, 7], "demandNonRenewable": [5, 4] }, + { "duration": 6, "demandRenewable": [8, 7], "demandNonRenewable": [3, 4] } + ] + }, + { + "id": 14, "successors": [15, 21], + "modes": + [ + { "duration": 3, "demandRenewable": [8, 10], "demandNonRenewable": [10, 10] }, + { "duration": 6, "demandRenewable": [8, 9], "demandNonRenewable": [6, 7] }, + { "duration": 8, "demandRenewable": [6, 9], "demandNonRenewable": [4, 7] } + ] + }, + { + "id": 15, "successors": [20, 26], + "modes": + [ + { "duration": 1, "demandRenewable": [6, 5], "demandNonRenewable": [8, 8] }, + { "duration": 6, "demandRenewable": [4, 4], "demandNonRenewable": [8, 7] }, + { "duration": 10, "demandRenewable": [3, 4], "demandNonRenewable": [7, 6] } + ] + }, + { + "id": 16, "successors": [24, 26], + "modes": + [ + { "duration": 2, "demandRenewable": [10, 6], "demandNonRenewable": [4, 8] }, + { "duration": 7, "demandRenewable": [8, 5], "demandNonRenewable": [4, 7] }, + { "duration": 10, "demandRenewable": [7, 4], "demandNonRenewable": [4, 6] } + ] + }, + { + "id": 17, "successors": [21], + "modes": + [ + { "duration": 2, "demandRenewable": [6, 4], "demandNonRenewable": [6, 3] }, + { "duration": 3, "demandRenewable": [4, 3], "demandNonRenewable": [6, 3] }, + { "duration": 4, "demandRenewable": [2, 3], "demandNonRenewable": [2, 2] } + ] + }, + { + "id": 18, "successors": [23, 24], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 10], "demandNonRenewable": [7, 5] }, + { "duration": 5, "demandRenewable": [9, 9], "demandNonRenewable": [6, 5] }, + { "duration": 6, "demandRenewable": [8, 9], "demandNonRenewable": [4, 5] } + ] + }, + { + "id": 19, "successors": [22], + "modes": + [ + { "duration": 1, "demandRenewable": [4, 9], "demandNonRenewable": [6, 8] }, + { "duration": 3, "demandRenewable": [4, 6], "demandNonRenewable": [5, 5] }, + { "duration": 10, "demandRenewable": [3, 1], "demandNonRenewable": [5, 3] } + ] + }, + { + "id": 20, "successors": [22, 25], + "modes": + [ + { "duration": 2, "demandRenewable": [6, 4], "demandNonRenewable": [5, 9] }, + { "duration": 4, "demandRenewable": [5, 3], "demandNonRenewable": [5, 9] }, + { "duration": 5, "demandRenewable": [2, 2], "demandNonRenewable": [1, 7] } + ] + }, + { + "id": 21, "successors": [22, 25], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 8], "demandNonRenewable": [5, 5] }, + { "duration": 3, "demandRenewable": [7, 8], "demandNonRenewable": [3, 5] }, + { "duration": 5, "demandRenewable": [4, 8], "demandNonRenewable": [2, 4] } + ] + }, + { + "id": 22, "successors": [24], + "modes": + [ + { "duration": 6, "demandRenewable": [4, 7], "demandNonRenewable": [4, 4] }, + { "duration": 8, "demandRenewable": [1, 7], "demandNonRenewable": [4, 2] }, + { "duration": 8, "demandRenewable": [1, 7], "demandNonRenewable": [2, 4] } + ] + }, + { + "id": 23, "successors": [27, 28, 29], + "modes": + [ + { "duration": 3, "demandRenewable": [8, 8], "demandNonRenewable": [5, 2] }, + { "duration": 4, "demandRenewable": [7, 7], "demandNonRenewable": [4, 2] }, + { "duration": 8, "demandRenewable": [5, 4], "demandNonRenewable": [3, 2] } + ] + }, + { + "id": 24, "successors": [28], + "modes": + [ + { "duration": 2, "demandRenewable": [4, 7], "demandNonRenewable": [9, 6] }, + { "duration": 6, "demandRenewable": [3, 6], "demandNonRenewable": [8, 5] }, + { "duration": 8, "demandRenewable": [3, 2], "demandNonRenewable": [8, 4] } + ] + }, + { + "id": 25, "successors": [27, 29], + "modes": + [ + { "duration": 7, "demandRenewable": [7, 3], "demandNonRenewable": [5, 5] }, + { "duration": 10, "demandRenewable": [7, 3], "demandNonRenewable": [2, 5] }, + { "duration": 10, "demandRenewable": [7, 3], "demandNonRenewable": [4, 3] } + ] + }, + { + "id": 26, "successors": [28], + "modes": + [ + { "duration": 5, "demandRenewable": [7, 8], "demandNonRenewable": [7, 9] }, + { "duration": 7, "demandRenewable": [7, 7], "demandNonRenewable": [6, 7] }, + { "duration": 8, "demandRenewable": [6, 6], "demandNonRenewable": [3, 7] } + ] + }, + { + "id": 27, "successors": [], + "modes": + [ + { "duration": 1, "demandRenewable": [7, 9], "demandNonRenewable": [4, 2] }, + { "duration": 5, "demandRenewable": [5, 6], "demandNonRenewable": [3, 2] }, + { "duration": 8, "demandRenewable": [2, 5], "demandNonRenewable": [1, 2] } + ] + }, + { + "id": 28, "successors": [], + "modes": + [ + { "duration": 6, "demandRenewable": [4, 7], "demandNonRenewable": [9, 2] }, + { "duration": 8, "demandRenewable": [3, 5], "demandNonRenewable": [6, 2] }, + { "duration": 9, "demandRenewable": [3, 1], "demandNonRenewable": [3, 1] } + ] + }, + { + "id": 29, "successors": [], + "modes": + [ + { "duration": 3, "demandRenewable": [8, 7], "demandNonRenewable": [4, 8] }, + { "duration": 7, "demandRenewable": [8, 5], "demandNonRenewable": [4, 6] }, + { "duration": 8, "demandRenewable": [6, 3], "demandNonRenewable": [3, 4] } + ] + } + ] +} diff --git a/examples/cp/visu/data/rcpspmm_j30_62_10.json b/examples/cp/visu/data/rcpspmm_j30_62_10.json index 9770884..d94903e 100644 --- a/examples/cp/visu/data/rcpspmm_j30_62_10.json +++ b/examples/cp/visu/data/rcpspmm_j30_62_10.json @@ -1,277 +1,277 @@ -{ - "capacityRenewable": [27, 27], - "capacityNonRenewable": [185, 193], - "tasks": - [ - { - "id": 0, "successors": [3, 18], - "modes": - [ - { "duration": 2, "demandRenewable": [6, 4], "demandNonRenewable": [5, 3] }, - { "duration": 5, "demandRenewable": [4, 3], "demandNonRenewable": [4, 2] }, - { "duration": 8, "demandRenewable": [3, 3], "demandNonRenewable": [4, 2] } - ] - }, - { - "id": 1, "successors": [7, 24], - "modes": - [ - { "duration": 1, "demandRenewable": [8, 2], "demandNonRenewable": [9, 7] }, - { "duration": 1, "demandRenewable": [9, 2], "demandNonRenewable": [8, 8] }, - { "duration": 8, "demandRenewable": [8, 2], "demandNonRenewable": [7, 7] } - ] - }, - { - "id": 2, "successors": [4, 5, 8], - "modes": - [ - { "duration": 3, "demandRenewable": [9, 4], "demandNonRenewable": [5, 6] }, - { "duration": 3, "demandRenewable": [7, 4], "demandNonRenewable": [5, 10] }, - { "duration": 8, "demandRenewable": [5, 2], "demandNonRenewable": [2, 3] } - ] - }, - { - "id": 3, "successors": [7, 9, 20], - "modes": - [ - { "duration": 1, "demandRenewable": [6, 4], "demandNonRenewable": [6, 7] }, - { "duration": 5, "demandRenewable": [5, 3], "demandNonRenewable": [5, 6] }, - { "duration": 9, "demandRenewable": [2, 3], "demandNonRenewable": [2, 6] } - ] - }, - { - "id": 4, "successors": [6], - "modes": - [ - { "duration": 3, "demandRenewable": [8, 4], "demandNonRenewable": [10, 6] }, - { "duration": 3, "demandRenewable": [9, 3], "demandNonRenewable": [10, 6] }, - { "duration": 6, "demandRenewable": [5, 2], "demandNonRenewable": [10, 6] } - ] - }, - { - "id": 5, "successors": [10, 12, 17], - "modes": - [ - { "duration": 1, "demandRenewable": [7, 8], "demandNonRenewable": [10, 7] }, - { "duration": 8, "demandRenewable": [6, 6], "demandNonRenewable": [9, 7] }, - { "duration": 9, "demandRenewable": [5, 5], "demandNonRenewable": [9, 4] } - ] - }, - { - "id": 6, "successors": [28], - "modes": - [ - { "duration": 1, "demandRenewable": [4, 10], "demandNonRenewable": [6, 8] }, - { "duration": 3, "demandRenewable": [3, 4], "demandNonRenewable": [4, 6] }, - { "duration": 3, "demandRenewable": [4, 6], "demandNonRenewable": [4, 3] } - ] - }, - { - "id": 7, "successors": [16, 17, 26], - "modes": - [ - { "duration": 5, "demandRenewable": [5, 7], "demandNonRenewable": [9, 9] }, - { "duration": 7, "demandRenewable": [3, 5], "demandNonRenewable": [6, 8] }, - { "duration": 9, "demandRenewable": [1, 4], "demandNonRenewable": [3, 8] } - ] - }, - { - "id": 8, "successors": [9, 10, 15], - "modes": - [ - { "duration": 2, "demandRenewable": [7, 7], "demandNonRenewable": [6, 7] }, - { "duration": 3, "demandRenewable": [6, 6], "demandNonRenewable": [5, 7] }, - { "duration": 5, "demandRenewable": [4, 5], "demandNonRenewable": [3, 6] } - ] - }, - { - "id": 9, "successors": [11, 19, 23], - "modes": - [ - { "duration": 5, "demandRenewable": [10, 6], "demandNonRenewable": [4, 6] }, - { "duration": 9, "demandRenewable": [6, 3], "demandNonRenewable": [4, 5] }, - { "duration": 9, "demandRenewable": [6, 1], "demandNonRenewable": [3, 6] } - ] - }, - { - "id": 10, "successors": [18, 22], - "modes": - [ - { "duration": 2, "demandRenewable": [8, 10], "demandNonRenewable": [6, 3] }, - { "duration": 5, "demandRenewable": [6, 5], "demandNonRenewable": [3, 3] }, - { "duration": 6, "demandRenewable": [5, 5], "demandNonRenewable": [1, 2] } - ] - }, - { - "id": 11, "successors": [13], - "modes": - [ - { "duration": 1, "demandRenewable": [6, 9], "demandNonRenewable": [9, 2] }, - { "duration": 5, "demandRenewable": [6, 4], "demandNonRenewable": [8, 1] }, - { "duration": 9, "demandRenewable": [6, 3], "demandNonRenewable": [8, 1] } - ] - }, - { - "id": 12, "successors": [14, 20, 21], - "modes": - [ - { "duration": 4, "demandRenewable": [8, 7], "demandNonRenewable": [9, 10] }, - { "duration": 4, "demandRenewable": [7, 8], "demandNonRenewable": [9, 10] }, - { "duration": 10, "demandRenewable": [6, 6], "demandNonRenewable": [7, 9] } - ] - }, - { - "id": 13, "successors": [16, 17], - "modes": - [ - { "duration": 3, "demandRenewable": [9, 9], "demandNonRenewable": [4, 5] }, - { "duration": 7, "demandRenewable": [8, 6], "demandNonRenewable": [4, 4] }, - { "duration": 8, "demandRenewable": [8, 5], "demandNonRenewable": [2, 2] } - ] - }, - { - "id": 14, "successors": [22], - "modes": - [ - { "duration": 1, "demandRenewable": [8, 8], "demandNonRenewable": [5, 9] }, - { "duration": 5, "demandRenewable": [6, 7], "demandNonRenewable": [3, 7] }, - { "duration": 7, "demandRenewable": [6, 6], "demandNonRenewable": [3, 4] } - ] - }, - { - "id": 15, "successors": [29], - "modes": - [ - { "duration": 1, "demandRenewable": [4, 9], "demandNonRenewable": [5, 3] }, - { "duration": 2, "demandRenewable": [3, 6], "demandNonRenewable": [5, 3] }, - { "duration": 9, "demandRenewable": [3, 5], "demandNonRenewable": [4, 2] } - ] - }, - { - "id": 16, "successors": [27, 29], - "modes": - [ - { "duration": 1, "demandRenewable": [5, 6], "demandNonRenewable": [3, 4] }, - { "duration": 6, "demandRenewable": [4, 5], "demandNonRenewable": [2, 4] }, - { "duration": 6, "demandRenewable": [2, 4], "demandNonRenewable": [3, 3] } - ] - }, - { - "id": 17, "successors": [27], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 5], "demandNonRenewable": [5, 3] }, - { "duration": 3, "demandRenewable": [8, 5], "demandNonRenewable": [2, 2] }, - { "duration": 3, "demandRenewable": [8, 4], "demandNonRenewable": [4, 1] } - ] - }, - { - "id": 18, "successors": [20, 21], - "modes": - [ - { "duration": 4, "demandRenewable": [8, 7], "demandNonRenewable": [7, 8] }, - { "duration": 10, "demandRenewable": [8, 5], "demandNonRenewable": [6, 7] }, - { "duration": 10, "demandRenewable": [8, 7], "demandNonRenewable": [5, 8] } - ] - }, - { - "id": 19, "successors": [21, 24], - "modes": - [ - { "duration": 1, "demandRenewable": [5, 5], "demandNonRenewable": [7, 10] }, - { "duration": 4, "demandRenewable": [2, 5], "demandNonRenewable": [6, 10] }, - { "duration": 9, "demandRenewable": [1, 4], "demandNonRenewable": [5, 10] } - ] - }, - { - "id": 20, "successors": [23, 26, 28], - "modes": - [ - { "duration": 2, "demandRenewable": [9, 6], "demandNonRenewable": [5, 10] }, - { "duration": 3, "demandRenewable": [9, 3], "demandNonRenewable": [4, 9] }, - { "duration": 5, "demandRenewable": [7, 2], "demandNonRenewable": [3, 8] } - ] - }, - { - "id": 21, "successors": [25], - "modes": - [ - { "duration": 4, "demandRenewable": [8, 4], "demandNonRenewable": [4, 6] }, - { "duration": 8, "demandRenewable": [6, 4], "demandNonRenewable": [3, 4] }, - { "duration": 10, "demandRenewable": [4, 4], "demandNonRenewable": [2, 3] } - ] - }, - { - "id": 22, "successors": [23, 24, 25], - "modes": - [ - { "duration": 3, "demandRenewable": [4, 9], "demandNonRenewable": [9, 6] }, - { "duration": 4, "demandRenewable": [4, 7], "demandNonRenewable": [9, 5] }, - { "duration": 10, "demandRenewable": [2, 6], "demandNonRenewable": [9, 4] } - ] - }, - { - "id": 23, "successors": [27], - "modes": - [ - { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [6, 5] }, - { "duration": 7, "demandRenewable": [5, 8], "demandNonRenewable": [6, 4] }, - { "duration": 10, "demandRenewable": [5, 8], "demandNonRenewable": [5, 4] } - ] - }, - { - "id": 24, "successors": [28], - "modes": - [ - { "duration": 4, "demandRenewable": [4, 9], "demandNonRenewable": [5, 9] }, - { "duration": 9, "demandRenewable": [4, 9], "demandNonRenewable": [3, 7] }, - { "duration": 10, "demandRenewable": [4, 8], "demandNonRenewable": [3, 2] } - ] - }, - { - "id": 25, "successors": [26], - "modes": - [ - { "duration": 6, "demandRenewable": [10, 6], "demandNonRenewable": [3, 8] }, - { "duration": 8, "demandRenewable": [10, 6], "demandNonRenewable": [3, 6] }, - { "duration": 10, "demandRenewable": [9, 4], "demandNonRenewable": [3, 5] } - ] - }, - { - "id": 26, "successors": [29], - "modes": - [ - { "duration": 2, "demandRenewable": [6, 10], "demandNonRenewable": [5, 6] }, - { "duration": 5, "demandRenewable": [6, 10], "demandNonRenewable": [5, 5] }, - { "duration": 9, "demandRenewable": [4, 10], "demandNonRenewable": [2, 4] } - ] - }, - { - "id": 27, "successors": [], - "modes": - [ - { "duration": 2, "demandRenewable": [2, 8], "demandNonRenewable": [9, 2] }, - { "duration": 3, "demandRenewable": [2, 6], "demandNonRenewable": [6, 2] }, - { "duration": 6, "demandRenewable": [1, 1], "demandNonRenewable": [4, 1] } - ] - }, - { - "id": 28, "successors": [], - "modes": - [ - { "duration": 1, "demandRenewable": [9, 10], "demandNonRenewable": [3, 5] }, - { "duration": 2, "demandRenewable": [7, 10], "demandNonRenewable": [2, 4] }, - { "duration": 3, "demandRenewable": [6, 9], "demandNonRenewable": [1, 3] } - ] - }, - { - "id": 29, "successors": [], - "modes": - [ - { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [6, 7] }, - { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [5, 8] }, - { "duration": 6, "demandRenewable": [5, 8], "demandNonRenewable": [3, 3] } - ] - } - ] -} +{ + "capacityRenewable": [27, 27], + "capacityNonRenewable": [185, 193], + "tasks": + [ + { + "id": 0, "successors": [3, 18], + "modes": + [ + { "duration": 2, "demandRenewable": [6, 4], "demandNonRenewable": [5, 3] }, + { "duration": 5, "demandRenewable": [4, 3], "demandNonRenewable": [4, 2] }, + { "duration": 8, "demandRenewable": [3, 3], "demandNonRenewable": [4, 2] } + ] + }, + { + "id": 1, "successors": [7, 24], + "modes": + [ + { "duration": 1, "demandRenewable": [8, 2], "demandNonRenewable": [9, 7] }, + { "duration": 1, "demandRenewable": [9, 2], "demandNonRenewable": [8, 8] }, + { "duration": 8, "demandRenewable": [8, 2], "demandNonRenewable": [7, 7] } + ] + }, + { + "id": 2, "successors": [4, 5, 8], + "modes": + [ + { "duration": 3, "demandRenewable": [9, 4], "demandNonRenewable": [5, 6] }, + { "duration": 3, "demandRenewable": [7, 4], "demandNonRenewable": [5, 10] }, + { "duration": 8, "demandRenewable": [5, 2], "demandNonRenewable": [2, 3] } + ] + }, + { + "id": 3, "successors": [7, 9, 20], + "modes": + [ + { "duration": 1, "demandRenewable": [6, 4], "demandNonRenewable": [6, 7] }, + { "duration": 5, "demandRenewable": [5, 3], "demandNonRenewable": [5, 6] }, + { "duration": 9, "demandRenewable": [2, 3], "demandNonRenewable": [2, 6] } + ] + }, + { + "id": 4, "successors": [6], + "modes": + [ + { "duration": 3, "demandRenewable": [8, 4], "demandNonRenewable": [10, 6] }, + { "duration": 3, "demandRenewable": [9, 3], "demandNonRenewable": [10, 6] }, + { "duration": 6, "demandRenewable": [5, 2], "demandNonRenewable": [10, 6] } + ] + }, + { + "id": 5, "successors": [10, 12, 17], + "modes": + [ + { "duration": 1, "demandRenewable": [7, 8], "demandNonRenewable": [10, 7] }, + { "duration": 8, "demandRenewable": [6, 6], "demandNonRenewable": [9, 7] }, + { "duration": 9, "demandRenewable": [5, 5], "demandNonRenewable": [9, 4] } + ] + }, + { + "id": 6, "successors": [28], + "modes": + [ + { "duration": 1, "demandRenewable": [4, 10], "demandNonRenewable": [6, 8] }, + { "duration": 3, "demandRenewable": [3, 4], "demandNonRenewable": [4, 6] }, + { "duration": 3, "demandRenewable": [4, 6], "demandNonRenewable": [4, 3] } + ] + }, + { + "id": 7, "successors": [16, 17, 26], + "modes": + [ + { "duration": 5, "demandRenewable": [5, 7], "demandNonRenewable": [9, 9] }, + { "duration": 7, "demandRenewable": [3, 5], "demandNonRenewable": [6, 8] }, + { "duration": 9, "demandRenewable": [1, 4], "demandNonRenewable": [3, 8] } + ] + }, + { + "id": 8, "successors": [9, 10, 15], + "modes": + [ + { "duration": 2, "demandRenewable": [7, 7], "demandNonRenewable": [6, 7] }, + { "duration": 3, "demandRenewable": [6, 6], "demandNonRenewable": [5, 7] }, + { "duration": 5, "demandRenewable": [4, 5], "demandNonRenewable": [3, 6] } + ] + }, + { + "id": 9, "successors": [11, 19, 23], + "modes": + [ + { "duration": 5, "demandRenewable": [10, 6], "demandNonRenewable": [4, 6] }, + { "duration": 9, "demandRenewable": [6, 3], "demandNonRenewable": [4, 5] }, + { "duration": 9, "demandRenewable": [6, 1], "demandNonRenewable": [3, 6] } + ] + }, + { + "id": 10, "successors": [18, 22], + "modes": + [ + { "duration": 2, "demandRenewable": [8, 10], "demandNonRenewable": [6, 3] }, + { "duration": 5, "demandRenewable": [6, 5], "demandNonRenewable": [3, 3] }, + { "duration": 6, "demandRenewable": [5, 5], "demandNonRenewable": [1, 2] } + ] + }, + { + "id": 11, "successors": [13], + "modes": + [ + { "duration": 1, "demandRenewable": [6, 9], "demandNonRenewable": [9, 2] }, + { "duration": 5, "demandRenewable": [6, 4], "demandNonRenewable": [8, 1] }, + { "duration": 9, "demandRenewable": [6, 3], "demandNonRenewable": [8, 1] } + ] + }, + { + "id": 12, "successors": [14, 20, 21], + "modes": + [ + { "duration": 4, "demandRenewable": [8, 7], "demandNonRenewable": [9, 10] }, + { "duration": 4, "demandRenewable": [7, 8], "demandNonRenewable": [9, 10] }, + { "duration": 10, "demandRenewable": [6, 6], "demandNonRenewable": [7, 9] } + ] + }, + { + "id": 13, "successors": [16, 17], + "modes": + [ + { "duration": 3, "demandRenewable": [9, 9], "demandNonRenewable": [4, 5] }, + { "duration": 7, "demandRenewable": [8, 6], "demandNonRenewable": [4, 4] }, + { "duration": 8, "demandRenewable": [8, 5], "demandNonRenewable": [2, 2] } + ] + }, + { + "id": 14, "successors": [22], + "modes": + [ + { "duration": 1, "demandRenewable": [8, 8], "demandNonRenewable": [5, 9] }, + { "duration": 5, "demandRenewable": [6, 7], "demandNonRenewable": [3, 7] }, + { "duration": 7, "demandRenewable": [6, 6], "demandNonRenewable": [3, 4] } + ] + }, + { + "id": 15, "successors": [29], + "modes": + [ + { "duration": 1, "demandRenewable": [4, 9], "demandNonRenewable": [5, 3] }, + { "duration": 2, "demandRenewable": [3, 6], "demandNonRenewable": [5, 3] }, + { "duration": 9, "demandRenewable": [3, 5], "demandNonRenewable": [4, 2] } + ] + }, + { + "id": 16, "successors": [27, 29], + "modes": + [ + { "duration": 1, "demandRenewable": [5, 6], "demandNonRenewable": [3, 4] }, + { "duration": 6, "demandRenewable": [4, 5], "demandNonRenewable": [2, 4] }, + { "duration": 6, "demandRenewable": [2, 4], "demandNonRenewable": [3, 3] } + ] + }, + { + "id": 17, "successors": [27], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 5], "demandNonRenewable": [5, 3] }, + { "duration": 3, "demandRenewable": [8, 5], "demandNonRenewable": [2, 2] }, + { "duration": 3, "demandRenewable": [8, 4], "demandNonRenewable": [4, 1] } + ] + }, + { + "id": 18, "successors": [20, 21], + "modes": + [ + { "duration": 4, "demandRenewable": [8, 7], "demandNonRenewable": [7, 8] }, + { "duration": 10, "demandRenewable": [8, 5], "demandNonRenewable": [6, 7] }, + { "duration": 10, "demandRenewable": [8, 7], "demandNonRenewable": [5, 8] } + ] + }, + { + "id": 19, "successors": [21, 24], + "modes": + [ + { "duration": 1, "demandRenewable": [5, 5], "demandNonRenewable": [7, 10] }, + { "duration": 4, "demandRenewable": [2, 5], "demandNonRenewable": [6, 10] }, + { "duration": 9, "demandRenewable": [1, 4], "demandNonRenewable": [5, 10] } + ] + }, + { + "id": 20, "successors": [23, 26, 28], + "modes": + [ + { "duration": 2, "demandRenewable": [9, 6], "demandNonRenewable": [5, 10] }, + { "duration": 3, "demandRenewable": [9, 3], "demandNonRenewable": [4, 9] }, + { "duration": 5, "demandRenewable": [7, 2], "demandNonRenewable": [3, 8] } + ] + }, + { + "id": 21, "successors": [25], + "modes": + [ + { "duration": 4, "demandRenewable": [8, 4], "demandNonRenewable": [4, 6] }, + { "duration": 8, "demandRenewable": [6, 4], "demandNonRenewable": [3, 4] }, + { "duration": 10, "demandRenewable": [4, 4], "demandNonRenewable": [2, 3] } + ] + }, + { + "id": 22, "successors": [23, 24, 25], + "modes": + [ + { "duration": 3, "demandRenewable": [4, 9], "demandNonRenewable": [9, 6] }, + { "duration": 4, "demandRenewable": [4, 7], "demandNonRenewable": [9, 5] }, + { "duration": 10, "demandRenewable": [2, 6], "demandNonRenewable": [9, 4] } + ] + }, + { + "id": 23, "successors": [27], + "modes": + [ + { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [6, 5] }, + { "duration": 7, "demandRenewable": [5, 8], "demandNonRenewable": [6, 4] }, + { "duration": 10, "demandRenewable": [5, 8], "demandNonRenewable": [5, 4] } + ] + }, + { + "id": 24, "successors": [28], + "modes": + [ + { "duration": 4, "demandRenewable": [4, 9], "demandNonRenewable": [5, 9] }, + { "duration": 9, "demandRenewable": [4, 9], "demandNonRenewable": [3, 7] }, + { "duration": 10, "demandRenewable": [4, 8], "demandNonRenewable": [3, 2] } + ] + }, + { + "id": 25, "successors": [26], + "modes": + [ + { "duration": 6, "demandRenewable": [10, 6], "demandNonRenewable": [3, 8] }, + { "duration": 8, "demandRenewable": [10, 6], "demandNonRenewable": [3, 6] }, + { "duration": 10, "demandRenewable": [9, 4], "demandNonRenewable": [3, 5] } + ] + }, + { + "id": 26, "successors": [29], + "modes": + [ + { "duration": 2, "demandRenewable": [6, 10], "demandNonRenewable": [5, 6] }, + { "duration": 5, "demandRenewable": [6, 10], "demandNonRenewable": [5, 5] }, + { "duration": 9, "demandRenewable": [4, 10], "demandNonRenewable": [2, 4] } + ] + }, + { + "id": 27, "successors": [], + "modes": + [ + { "duration": 2, "demandRenewable": [2, 8], "demandNonRenewable": [9, 2] }, + { "duration": 3, "demandRenewable": [2, 6], "demandNonRenewable": [6, 2] }, + { "duration": 6, "demandRenewable": [1, 1], "demandNonRenewable": [4, 1] } + ] + }, + { + "id": 28, "successors": [], + "modes": + [ + { "duration": 1, "demandRenewable": [9, 10], "demandNonRenewable": [3, 5] }, + { "duration": 2, "demandRenewable": [7, 10], "demandNonRenewable": [2, 4] }, + { "duration": 3, "demandRenewable": [6, 9], "demandNonRenewable": [1, 3] } + ] + }, + { + "id": 29, "successors": [], + "modes": + [ + { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [6, 7] }, + { "duration": 3, "demandRenewable": [5, 8], "demandNonRenewable": [5, 8] }, + { "duration": 6, "demandRenewable": [5, 8], "demandNonRenewable": [3, 3] } + ] + } + ] +} diff --git a/examples/cp/visu/flow_shop.py b/examples/cp/visu/flow_shop.py index 4095dfd..99fb881 100644 --- a/examples/cp/visu/flow_shop.py +++ b/examples/cp/visu/flow_shop.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/flow_shop_permutation.py b/examples/cp/visu/flow_shop_permutation.py index f5bbbae..53a3af7 100644 --- a/examples/cp/visu/flow_shop_permutation.py +++ b/examples/cp/visu/flow_shop_permutation.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/house_building_basic.py b/examples/cp/visu/house_building_basic.py index 5d8726b..252ec93 100644 --- a/examples/cp/visu/house_building_basic.py +++ b/examples/cp/visu/house_building_basic.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/house_building_calendar.py b/examples/cp/visu/house_building_calendar.py index c18e951..330e8f3 100644 --- a/examples/cp/visu/house_building_calendar.py +++ b/examples/cp/visu/house_building_calendar.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/house_building_cumul.py b/examples/cp/visu/house_building_cumul.py index c0141e5..54d1e94 100644 --- a/examples/cp/visu/house_building_cumul.py +++ b/examples/cp/visu/house_building_cumul.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/house_building_optional.py b/examples/cp/visu/house_building_optional.py index c36287a..e396d2f 100644 --- a/examples/cp/visu/house_building_optional.py +++ b/examples/cp/visu/house_building_optional.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/house_building_state.py b/examples/cp/visu/house_building_state.py index d6a91d6..e0bf62a 100644 --- a/examples/cp/visu/house_building_state.py +++ b/examples/cp/visu/house_building_state.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/house_building_time.py b/examples/cp/visu/house_building_time.py index 4d926df..5d121cf 100644 --- a/examples/cp/visu/house_building_time.py +++ b/examples/cp/visu/house_building_time.py @@ -1,114 +1,114 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -""" -This is a problem of building a house. The masonry, roofing, painting, -etc. must be scheduled. Some tasks must necessarily take place before -others and these requirements are expressed through precedence -constraints. - -Moreover, there are earliness and tardiness costs associated with some tasks. -The objective is to minimize these costs. - -Please refer to documentation for appropriate setup of solving configuration. -""" - -from docplex.cp.model import * - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Create model -mdl = CpoModel() - -# List of tasks to be executed for the house -TASKS = { - 'masonry' : (35 , 1, {'release_date':25, 'earliness_cost':200.0} ), - 'carpentry' : (15 , 2, {'release_date':75, 'earliness_cost':300.0} ), - 'plumbing' : (40 , 3, {} ), - 'ceiling' : (15 , 4, {'release_date':75, 'earliness_cost':100.0} ), - 'roofing' : ( 5 , 5, {} ), - 'painting' : (10 , 6, {} ), - 'windows' : ( 5 , 7, {} ), - 'facade' : (10 , 8, {} ), - 'garden' : ( 5 , 9, {} ), - 'moving' : ( 5 , 10, {'due_date':100, 'tardiness_cost':400.0} ) -} - -# Tasks precedence constraints (each tuple (X, Y) means X ends before start of Y) -PRECEDENCES = [ - ('masonry', 'carpentry'), - ('masonry', 'plumbing'), - ('masonry', 'ceiling'), - ('carpentry', 'roofing'), - ('ceiling', 'painting'), - ('roofing', 'windows'), - ('roofing', 'facade'), - ('plumbing', 'facade'), - ('roofing', 'garden'), - ('plumbing', 'garden'), - ('windows', 'moving'), - ('facade', 'moving'), - ('garden', 'moving'), - ('painting', 'moving'), -] - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -# Create model -mdl = CpoModel() - -# Create interval variable for each building task -tasks = { t: interval_var(size=TASKS[t][0], name=t) for t in TASKS } - -# Add precedence constraints -mdl.add(end_before_start(tasks[p], tasks[s]) for p,s in PRECEDENCES) - -# Cost function -fearliness = dict() # Task earliness cost function -ftardiness = dict() # Task tardiness cost function - -for t in TASKS: - if 'release_date' in TASKS[t][2]: - fearliness[t] = CpoSegmentedFunction((-TASKS[t][2]['earliness_cost'], 0), [(TASKS[t][2]['release_date'], 0, 0)]) - if 'due_date' in TASKS[t][2]: - ftardiness[t] = CpoSegmentedFunction((0, 0), [(TASKS[t][2]['due_date'], 0, TASKS[t][2]['tardiness_cost'],)]) - -# Minimize cost -mdl.add(minimize( sum( start_eval(tasks[t], fearliness[t]) for t in fearliness) + - sum( end_eval (tasks[t], ftardiness[t]) for t in ftardiness) )) - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -print('Solving model...') -res = mdl.solve(TimeLimit=10) -print('Solution:') -res.print_solution() - -import docplex.cp.utils_visu as visu -if res and visu.is_visu_enabled(): - visu.timeline('Solution house building', origin=10, horizon=120) - visu.panel('Schedule') - for t in TASKS: - visu.interval(res.get_var_solution(tasks[t]), TASKS[t][1], t) - for t in TASKS: - itvsol = res.get_var_solution(tasks[t]) - if 'release_date' in TASKS[t][2]: - visu.panel('Earliness') - cost = fearliness[t].get_value(itvsol.get_start()) - visu.function(segments=[(itvsol, cost, t)], color=TASKS[t][1], style='interval') - visu.function(segments=fearliness[t], color=TASKS[t][1]) - if 'due_date' in TASKS[t][2]: - visu.panel('Tardiness') - cost = ftardiness[t].get_value(itvsol.get_end()) - visu.function(segments=[(itvsol, cost, t)], color=TASKS[t][1], style='interval') - visu.function(segments=ftardiness[t], color=TASKS[t][1]) - visu.show() +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +This is a problem of building a house. The masonry, roofing, painting, +etc. must be scheduled. Some tasks must necessarily take place before +others and these requirements are expressed through precedence +constraints. + +Moreover, there are earliness and tardiness costs associated with some tasks. +The objective is to minimize these costs. + +Please refer to documentation for appropriate setup of solving configuration. +""" + +from docplex.cp.model import * + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Create model +mdl = CpoModel() + +# List of tasks to be executed for the house +TASKS = { + 'masonry' : (35 , 1, {'release_date':25, 'earliness_cost':200.0} ), + 'carpentry' : (15 , 2, {'release_date':75, 'earliness_cost':300.0} ), + 'plumbing' : (40 , 3, {} ), + 'ceiling' : (15 , 4, {'release_date':75, 'earliness_cost':100.0} ), + 'roofing' : ( 5 , 5, {} ), + 'painting' : (10 , 6, {} ), + 'windows' : ( 5 , 7, {} ), + 'facade' : (10 , 8, {} ), + 'garden' : ( 5 , 9, {} ), + 'moving' : ( 5 , 10, {'due_date':100, 'tardiness_cost':400.0} ) +} + +# Tasks precedence constraints (each tuple (X, Y) means X ends before start of Y) +PRECEDENCES = [ + ('masonry', 'carpentry'), + ('masonry', 'plumbing'), + ('masonry', 'ceiling'), + ('carpentry', 'roofing'), + ('ceiling', 'painting'), + ('roofing', 'windows'), + ('roofing', 'facade'), + ('plumbing', 'facade'), + ('roofing', 'garden'), + ('plumbing', 'garden'), + ('windows', 'moving'), + ('facade', 'moving'), + ('garden', 'moving'), + ('painting', 'moving'), +] + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +# Create model +mdl = CpoModel() + +# Create interval variable for each building task +tasks = { t: interval_var(size=TASKS[t][0], name=t) for t in TASKS } + +# Add precedence constraints +mdl.add(end_before_start(tasks[p], tasks[s]) for p,s in PRECEDENCES) + +# Cost function +fearliness = dict() # Task earliness cost function +ftardiness = dict() # Task tardiness cost function + +for t in TASKS: + if 'release_date' in TASKS[t][2]: + fearliness[t] = CpoSegmentedFunction((-TASKS[t][2]['earliness_cost'], 0), [(TASKS[t][2]['release_date'], 0, 0)]) + if 'due_date' in TASKS[t][2]: + ftardiness[t] = CpoSegmentedFunction((0, 0), [(TASKS[t][2]['due_date'], 0, TASKS[t][2]['tardiness_cost'],)]) + +# Minimize cost +mdl.add(minimize( sum( start_eval(tasks[t], fearliness[t]) for t in fearliness) + + sum( end_eval (tasks[t], ftardiness[t]) for t in ftardiness) )) + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +print('Solving model...') +res = mdl.solve(TimeLimit=10) +print('Solution:') +res.print_solution() + +import docplex.cp.utils_visu as visu +if res and visu.is_visu_enabled(): + visu.timeline('Solution house building', origin=10, horizon=120) + visu.panel('Schedule') + for t in TASKS: + visu.interval(res.get_var_solution(tasks[t]), TASKS[t][1], t) + for t in TASKS: + itvsol = res.get_var_solution(tasks[t]) + if 'release_date' in TASKS[t][2]: + visu.panel('Earliness') + cost = fearliness[t].get_value(itvsol.get_start()) + visu.function(segments=[(itvsol, cost, t)], color=TASKS[t][1], style='interval') + visu.function(segments=fearliness[t], color=TASKS[t][1]) + if 'due_date' in TASKS[t][2]: + visu.panel('Tardiness') + cost = ftardiness[t].get_value(itvsol.get_end()) + visu.function(segments=[(itvsol, cost, t)], color=TASKS[t][1], style='interval') + visu.function(segments=ftardiness[t], color=TASKS[t][1]) + visu.show() diff --git a/examples/cp/visu/job_shop_basic.py b/examples/cp/visu/job_shop_basic.py index c02f8ec..66c030a 100644 --- a/examples/cp/visu/job_shop_basic.py +++ b/examples/cp/visu/job_shop_basic.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/job_shop_flexible.py b/examples/cp/visu/job_shop_flexible.py index 32cc181..cc757de 100644 --- a/examples/cp/visu/job_shop_flexible.py +++ b/examples/cp/visu/job_shop_flexible.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/job_shop_stochastic.py b/examples/cp/visu/job_shop_stochastic.py index 029aa7c..65494ac 100644 --- a/examples/cp/visu/job_shop_stochastic.py +++ b/examples/cp/visu/job_shop_stochastic.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/open_shop.py b/examples/cp/visu/open_shop.py index 7dfce29..f4e4ebc 100644 --- a/examples/cp/visu/open_shop.py +++ b/examples/cp/visu/open_shop.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/plant_location_with_solver_listener.py b/examples/cp/visu/plant_location_with_solver_listener.py index fdebf63..1916d6b 100644 --- a/examples/cp/visu/plant_location_with_solver_listener.py +++ b/examples/cp/visu/plant_location_with_solver_listener.py @@ -1,117 +1,117 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016, 2018 -# -------------------------------------------------------------------------- - -""" -A ship-building company has a certain number of customers. Each customer is supplied -by exactly one plant. In turn, a plant can supply several customers. The problem is -to decide where to set up the plants in order to supply every customer while minimizing -the cost of building each plant and the transportation cost of supplying the customers. - -For each possible plant location there is a fixed cost and a production capacity. -Both take into account the country and the geographical conditions. - -For every customer, there is a demand and a transportation cost with respect to -each plant location. - -While a first solution of this problem can be found easily by CP Optimizer, it can take -quite some time to improve it to a very good one. We illustrate the warm start capabilities -of CP Optimizer by giving a good starting point solution that CP Optimizer will try to improve. -This solution could be one from an expert or the result of another optimization engine -applied to the problem. - -In the solution we only give a value to the variables that determine which plant delivers -a customer. This is sufficient to define a complete solution on all model variables. -CP Optimizer first extends the solution to all variables and then starts to improve it. - -This model has been enriched by the addition of KPIs (key performance indicators), operational with a -version of COS greater or equal to 12.9.0.0. -These are named expressions which are of interest to help get an idea of the performance of the model. -Here, we are interested in two indicators: - - the first is the `occupancy'' defined as the total demand divided by the total plant capacity. - - the second indicator is the occupancy which is the lowest of all the plants. - -The KPIs are displayed using a SolverProgressPanelListener that displays solve progress in real time -and allows to stop solve when good enough objective or KPIs are reached. -Log parsing is also activated to retrieve runtime information from it. -""" - -from docplex.cp.model import * -from docplex.cp.solver.solver_listener import * -from docplex.cp.config import context -from docplex.cp.utils import compare_natural -from collections import deque -import os - -#----------------------------------------------------------------------------- -# Initialize the problem data -#----------------------------------------------------------------------------- - -# Read problem data from a file and convert it as a list of integers -filename = os.path.dirname(os.path.abspath(__file__)) + '/data/plant_location.data' -data = deque() -with open(filename, 'r') as file: - for val in file.read().split(): - data.append(int(val)) - -# Read number of customers and locations -nbCustomer = data.popleft() -nbLocation = data.popleft() - -# Initialize cost. cost[c][p] = cost to deliver customer c from plant p -cost = list([list([data.popleft() for l in range(nbLocation)]) for c in range(nbCustomer)]) - -# Initialize demand of each customer -demand = list([data.popleft() for c in range(nbCustomer)]) - -# Initialize fixed cost of each location -fixedCost = list([data.popleft() for p in range(nbLocation)]) - -# Initialize capacity of each location -capacity = list([data.popleft() for p in range(nbLocation)]) - - -#----------------------------------------------------------------------------- -# Build the model -#----------------------------------------------------------------------------- - -mdl = CpoModel() - -# Create variables identifying which location serves each customer -cust = integer_var_list(nbCustomer, 0, nbLocation - 1, 'CustomerLocation') - -# Create variables indicating which plant location is open -open = integer_var_list(nbLocation, 0, 1, 'OpenLocation') - -# Create variables indicating load of each plant -load = [integer_var(0, capacity[p], 'PlantLoad_' + str(p)) for p in range(nbLocation)] - -# Associate plant openness to its load -mdl.add(open[p] == (load[p] > 0) for p in range(nbLocation)) - -# Add constraints -mdl.add(pack(load, cust, demand)) - -# Add objective -obj = scal_prod(fixedCost, open) + sum(element(cust[c], cost[c]) for c in range(nbCustomer)) -mdl.add(minimize(obj)) - -# Add KPIs -if compare_natural(context.model.version, '12.9') >= 0: - mdl.add_kpi(sum(demand) / scal_prod(open, capacity), 'Average Occupancy') - mdl.add_kpi(min([load[l] / capacity[l] + (1 - open[l]) for l in range(nbLocation)]), 'Min occupancy') - - -#----------------------------------------------------------------------------- -# Solve the model and display the result -#----------------------------------------------------------------------------- - -if context.visu_enabled: - mdl.add_solver_listener(SolverProgressPanelListener(parse_log=True)) - -# Solve the model -print('Solve the model') -res = mdl.solve(TimeLimit=20, LogPeriod=1000) -res.write() +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +""" +A ship-building company has a certain number of customers. Each customer is supplied +by exactly one plant. In turn, a plant can supply several customers. The problem is +to decide where to set up the plants in order to supply every customer while minimizing +the cost of building each plant and the transportation cost of supplying the customers. + +For each possible plant location there is a fixed cost and a production capacity. +Both take into account the country and the geographical conditions. + +For every customer, there is a demand and a transportation cost with respect to +each plant location. + +While a first solution of this problem can be found easily by CP Optimizer, it can take +quite some time to improve it to a very good one. We illustrate the warm start capabilities +of CP Optimizer by giving a good starting point solution that CP Optimizer will try to improve. +This solution could be one from an expert or the result of another optimization engine +applied to the problem. + +In the solution we only give a value to the variables that determine which plant delivers +a customer. This is sufficient to define a complete solution on all model variables. +CP Optimizer first extends the solution to all variables and then starts to improve it. + +This model has been enriched by the addition of KPIs (key performance indicators), operational with a +version of COS greater or equal to 12.9.0.0. +These are named expressions which are of interest to help get an idea of the performance of the model. +Here, we are interested in two indicators: + - the first is the `occupancy'' defined as the total demand divided by the total plant capacity. + - the second indicator is the occupancy which is the lowest of all the plants. + +The KPIs are displayed using a SolverProgressPanelListener that displays solve progress in real time +and allows to stop solve when good enough objective or KPIs are reached. +Log parsing is also activated to retrieve runtime information from it. +""" + +from docplex.cp.model import * +from docplex.cp.solver.solver_listener import * +from docplex.cp.config import context +from docplex.cp.utils import compare_natural +from collections import deque +import os + +#----------------------------------------------------------------------------- +# Initialize the problem data +#----------------------------------------------------------------------------- + +# Read problem data from a file and convert it as a list of integers +filename = os.path.dirname(os.path.abspath(__file__)) + '/data/plant_location.data' +data = deque() +with open(filename, 'r') as file: + for val in file.read().split(): + data.append(int(val)) + +# Read number of customers and locations +nbCustomer = data.popleft() +nbLocation = data.popleft() + +# Initialize cost. cost[c][p] = cost to deliver customer c from plant p +cost = list([list([data.popleft() for l in range(nbLocation)]) for c in range(nbCustomer)]) + +# Initialize demand of each customer +demand = list([data.popleft() for c in range(nbCustomer)]) + +# Initialize fixed cost of each location +fixedCost = list([data.popleft() for p in range(nbLocation)]) + +# Initialize capacity of each location +capacity = list([data.popleft() for p in range(nbLocation)]) + + +#----------------------------------------------------------------------------- +# Build the model +#----------------------------------------------------------------------------- + +mdl = CpoModel() + +# Create variables identifying which location serves each customer +cust = integer_var_list(nbCustomer, 0, nbLocation - 1, 'CustomerLocation') + +# Create variables indicating which plant location is open +open = integer_var_list(nbLocation, 0, 1, 'OpenLocation') + +# Create variables indicating load of each plant +load = [integer_var(0, capacity[p], 'PlantLoad_' + str(p)) for p in range(nbLocation)] + +# Associate plant openness to its load +mdl.add(open[p] == (load[p] > 0) for p in range(nbLocation)) + +# Add constraints +mdl.add(pack(load, cust, demand)) + +# Add objective +obj = scal_prod(fixedCost, open) + sum(element(cust[c], cost[c]) for c in range(nbCustomer)) +mdl.add(minimize(obj)) + +# Add KPIs +if compare_natural(context.model.version, '12.9') >= 0: + mdl.add_kpi(sum(demand) / scal_prod(open, capacity), 'Average Occupancy') + mdl.add_kpi(min([load[l] / capacity[l] + (1 - open[l]) for l in range(nbLocation)]), 'Min occupancy') + + +#----------------------------------------------------------------------------- +# Solve the model and display the result +#----------------------------------------------------------------------------- + +if context.visu_enabled: + mdl.add_solver_listener(SolverProgressPanelListener(parse_log=True)) + +# Solve the model +print('Solve the model') +res = mdl.solve(TimeLimit=20, LogPeriod=1000) +res.write() diff --git a/examples/cp/visu/rcpsp.py b/examples/cp/visu/rcpsp.py index 0bba201..825efa6 100644 --- a/examples/cp/visu/rcpsp.py +++ b/examples/cp/visu/rcpsp.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/rcpsp_multi_mode.py b/examples/cp/visu/rcpsp_multi_mode.py index 425b140..69e0180 100644 --- a/examples/cp/visu/rcpsp_multi_mode.py +++ b/examples/cp/visu/rcpsp_multi_mode.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/rcpsp_multi_mode_json.py b/examples/cp/visu/rcpsp_multi_mode_json.py index b027096..a1cc6e4 100644 --- a/examples/cp/visu/rcpsp_multi_mode_json.py +++ b/examples/cp/visu/rcpsp_multi_mode_json.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/setup_costs.py b/examples/cp/visu/setup_costs.py index bb3137c..24bf32c 100644 --- a/examples/cp/visu/setup_costs.py +++ b/examples/cp/visu/setup_costs.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/setup_times.py b/examples/cp/visu/setup_times.py index e0aa5ca..2223b2b 100644 --- a/examples/cp/visu/setup_times.py +++ b/examples/cp/visu/setup_times.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/cp/visu/squaring_square.py b/examples/cp/visu/squaring_square.py index 717017f..ca61431 100644 --- a/examples/cp/visu/squaring_square.py +++ b/examples/cp/visu/squaring_square.py @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Source file provided under Apache License, Version 2.0, January 2004, # http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 +# (c) Copyright IBM Corp. 2015, 2022 # -------------------------------------------------------------------------- """ diff --git a/examples/mp/callbacks/branch_callback.py b/examples/mp/callbacks/branch_callback.py index fc824fc..29da22c 100644 --- a/examples/mp/callbacks/branch_callback.py +++ b/examples/mp/callbacks/branch_callback.py @@ -202,5 +202,6 @@ def lifegame_make_initial_solution(mdl): if __name__ == "__main__": life_m = build_lifegame_model(n=6) add_branch_callback(life_m, logged=False) + life_m.end() diff --git a/examples/mp/callbacks/cut_callback.py b/examples/mp/callbacks/cut_callback.py index 648f8c4..89fde9d 100644 --- a/examples/mp/callbacks/cut_callback.py +++ b/examples/mp/callbacks/cut_callback.py @@ -149,11 +149,9 @@ def print_solution(m, tol=1e-6): [54, 72, 41, 12, 78], [54, 64, 65, 89, 89]] - def build_test_supply_model(use_cuts, **kwargs): return build_supply_model(DEFAULT_FIXED_COSTS, DEFAULT_SUPPLY_COSTS, use_cuts=use_cuts, **kwargs) - if __name__ == "__main__": # parse args args = sys.argv @@ -188,3 +186,4 @@ def build_test_supply_model(use_cuts, **kwargs): # expected value is 843, regardless of cuts if not random: assert abs(m.objective_value - 843) <= 1e-4 + m.end() diff --git a/examples/mp/callbacks/heuristic_callback.py b/examples/mp/callbacks/heuristic_callback.py index f891284..6646302 100644 --- a/examples/mp/callbacks/heuristic_callback.py +++ b/examples/mp/callbacks/heuristic_callback.py @@ -55,7 +55,8 @@ def try_heuristic_cb_on_file(filename): from docplex.mp.model_reader import ModelReader mdl = ModelReader.read(filename) if mdl: - return try_heuristic_cb_on_model(mdl) + return mdl, try_heuristic_cb_on_model(mdl) + if __name__ == "__main__": @@ -71,6 +72,7 @@ def try_heuristic_cb_on_file(filename): print(" extension, and a possible, additional .gz") print(" extension") sys.exit(-1) - s = try_heuristic_cb_on_file(data_file) + mdl, s = try_heuristic_cb_on_file(data_file) if expected: assert abs(s.objective_value - expected) <= 1 + mdl.end() diff --git a/examples/mp/callbacks/incumbent_callback.py b/examples/mp/callbacks/incumbent_callback.py index 73c60a4..5ab4969 100644 --- a/examples/mp/callbacks/incumbent_callback.py +++ b/examples/mp/callbacks/incumbent_callback.py @@ -83,4 +83,5 @@ def build_hearts(r, **kwargs): s10 = love.solve(log_output=False) assert s10 is not None love.report() + love.end() diff --git a/examples/mp/callbacks/lazy_callback.py b/examples/mp/callbacks/lazy_callback.py index 7eef133..695d543 100644 --- a/examples/mp/callbacks/lazy_callback.py +++ b/examples/mp/callbacks/lazy_callback.py @@ -144,7 +144,6 @@ def print_solution(m, tol=1e-6): [54, 72, 41, 12, 78], [54, 64, 65, 89, 89]] - def build_test_supply_model(lazy, **kwargs): return build_supply_model(DEFAULT_FIXED_COSTS, DEFAULT_SUPPLY_COSTS, lazy=lazy, **kwargs) @@ -182,6 +181,7 @@ def build_test_supply_model(lazy, **kwargs): # expected value is 843, regardless of using lazy constraints if not random: assert abs(m.objective_value - 843) <= 1e-4 + m.end() # * model suppy solved with objective = 843.000 diff --git a/examples/mp/jupyter/boxes.ipynb b/examples/mp/jupyter/boxes.ipynb index 92353d5..a80dc8f 100644 --- a/examples/mp/jupyter/boxes.ipynb +++ b/examples/mp/jupyter/boxes.ipynb @@ -587,7 +587,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/efficient.ipynb b/examples/mp/jupyter/efficient.ipynb index 798c804..1323ed2 100644 --- a/examples/mp/jupyter/efficient.ipynb +++ b/examples/mp/jupyter/efficient.ipynb @@ -923,7 +923,7 @@ "collapsed": true }, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/lagrangian_relaxation.ipynb b/examples/mp/jupyter/lagrangian_relaxation.ipynb index 80bb758..efb5da9 100644 --- a/examples/mp/jupyter/lagrangian_relaxation.ipynb +++ b/examples/mp/jupyter/lagrangian_relaxation.ipynb @@ -498,7 +498,7 @@ "metadata": {}, "source": [ "
\n", - "Copyright © IBM Corp. 2017-2021. Released as licensed Sample Materials." + "Copyright © IBM Corp. 2017-2022. Released as licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/lifegame.ipynb b/examples/mp/jupyter/lifegame.ipynb index 0c08560..b191db4 100644 --- a/examples/mp/jupyter/lifegame.ipynb +++ b/examples/mp/jupyter/lifegame.ipynb @@ -458,7 +458,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. Sample Materials." + "Copyright © 2017-2022 IBM. Sample Materials." ] }, { diff --git a/examples/mp/jupyter/load_balancing.ipynb b/examples/mp/jupyter/load_balancing.ipynb index 28c5a1b..f6478f4 100644 --- a/examples/mp/jupyter/load_balancing.ipynb +++ b/examples/mp/jupyter/load_balancing.ipynb @@ -610,7 +610,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/marketing_campaign.ipynb b/examples/mp/jupyter/marketing_campaign.ipynb index 47d8f4e..c1d6b16 100644 --- a/examples/mp/jupyter/marketing_campaign.ipynb +++ b/examples/mp/jupyter/marketing_campaign.ipynb @@ -1310,7 +1310,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/nurses_pandas-multi_objective.ipynb b/examples/mp/jupyter/nurses_pandas-multi_objective.ipynb index 7e6250e..77661da 100644 --- a/examples/mp/jupyter/nurses_pandas-multi_objective.ipynb +++ b/examples/mp/jupyter/nurses_pandas-multi_objective.ipynb @@ -4508,7 +4508,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/nurses_pandas.ipynb b/examples/mp/jupyter/nurses_pandas.ipynb index cac3a76..3996157 100644 --- a/examples/mp/jupyter/nurses_pandas.ipynb +++ b/examples/mp/jupyter/nurses_pandas.ipynb @@ -4773,7 +4773,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/nurses_scheduling.ipynb b/examples/mp/jupyter/nurses_scheduling.ipynb index e072d3a..730f858 100644 --- a/examples/mp/jupyter/nurses_scheduling.ipynb +++ b/examples/mp/jupyter/nurses_scheduling.ipynb @@ -1312,7 +1312,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/oil_blending.ipynb b/examples/mp/jupyter/oil_blending.ipynb index b36b0b5..3410d94 100644 --- a/examples/mp/jupyter/oil_blending.ipynb +++ b/examples/mp/jupyter/oil_blending.ipynb @@ -1010,7 +1010,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/pasta_production.ipynb b/examples/mp/jupyter/pasta_production.ipynb index eed17bc..ada6cd1 100644 --- a/examples/mp/jupyter/pasta_production.ipynb +++ b/examples/mp/jupyter/pasta_production.ipynb @@ -356,7 +356,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/progress.ipynb b/examples/mp/jupyter/progress.ipynb index 937b547..6667053 100644 --- a/examples/mp/jupyter/progress.ipynb +++ b/examples/mp/jupyter/progress.ipynb @@ -849,7 +849,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. Sample Materials." + "Copyright © 2017-2022 IBM. Sample Materials." ] }, { diff --git a/examples/mp/jupyter/sports_scheduling.ipynb b/examples/mp/jupyter/sports_scheduling.ipynb index 844f1de..5a6aecc 100644 --- a/examples/mp/jupyter/sports_scheduling.ipynb +++ b/examples/mp/jupyter/sports_scheduling.ipynb @@ -1106,7 +1106,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/jupyter/ucp_pandas.ipynb b/examples/mp/jupyter/ucp_pandas.ipynb index 0a1d70f..6b10573 100644 --- a/examples/mp/jupyter/ucp_pandas.ipynb +++ b/examples/mp/jupyter/ucp_pandas.ipynb @@ -2439,7 +2439,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Copyright © 2017-2021 IBM. IPLA licensed Sample Materials." + "Copyright © 2017-2022 IBM. IPLA licensed Sample Materials." ] }, { diff --git a/examples/mp/modeling/diet.py b/examples/mp/modeling/diet.py index d60ecff..62ce7ba 100644 --- a/examples/mp/modeling/diet.py +++ b/examples/mp/modeling/diet.py @@ -1,121 +1,119 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - -# The goal of the diet problem is to select a set of foods that satisfies -# a set of daily nutritional requirements at minimal cost. -# Source of data: http://www.neos-guide.org/content/diet-problem-solver - -from collections import namedtuple - -from docplex.mp.model import Model -from docplex.util.environment import get_environment - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- - -FOODS = [ - ("Roasted Chicken", 0.84, 0, 10), - ("Spaghetti W/ Sauce", 0.78, 0, 10), - ("Tomato,Red,Ripe,Raw", 0.27, 0, 10), - ("Apple,Raw,W/Skin", .24, 0, 10), - ("Grapes", 0.32, 0, 10), - ("Chocolate Chip Cookies", 0.03, 0, 10), - ("Lowfat Milk", 0.23, 0, 10), - ("Raisin Brn", 0.34, 0, 10), - ("Hotdog", 0.31, 0, 10) -] - -NUTRIENTS = [ - ("Calories", 2000, 2500), - ("Calcium", 800, 1600), - ("Iron", 10, 30), - ("Vit_A", 5000, 50000), - ("Dietary_Fiber", 25, 100), - ("Carbohydrates", 0, 300), - ("Protein", 50, 100) -] - -FOOD_NUTRIENTS = [ - ("Roasted Chicken", 277.4, 21.9, 1.8, 77.4, 0, 0, 42.2), - ("Spaghetti W/ Sauce", 358.2, 80.2, 2.3, 3055.2, 11.6, 58.3, 8.2), - ("Tomato,Red,Ripe,Raw", 25.8, 6.2, 0.6, 766.3, 1.4, 5.7, 1), - ("Apple,Raw,W/Skin", 81.4, 9.7, 0.2, 73.1, 3.7, 21, 0.3), - ("Grapes", 15.1, 3.4, 0.1, 24, 0.2, 4.1, 0.2), - ("Chocolate Chip Cookies", 78.1, 6.2, 0.4, 101.8, 0, 9.3, 0.9), - ("Lowfat Milk", 121.2, 296.7, 0.1, 500.2, 0, 11.7, 8.1), - ("Raisin Brn", 115.1, 12.9, 16.8, 1250.2, 4, 27.9, 4), - ("Hotdog", 242.1, 23.5, 2.3, 0, 0, 18, 10.4) -] - -Food = namedtuple("Food", ["name", "unit_cost", "qmin", "qmax"]) -Nutrient = namedtuple("Nutrient", ["name", "qmin", "qmax"]) - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- - -def build_diet_model(name='diet', **kwargs): - ints = kwargs.pop('ints', False) - - # Create tuples with named fields for foods and nutrients - foods = [Food(*f) for f in FOODS] - nutrients = [Nutrient(*row) for row in NUTRIENTS] - - food_nutrients = {(fn[0], nutrients[n].name): - fn[1 + n] for fn in FOOD_NUTRIENTS for n in range(len(NUTRIENTS))} - - # Model - mdl = Model(name=name, **kwargs) - - # Decision variables, limited to be >= Food.qmin and <= Food.qmax - ftype = mdl.integer_vartype if ints else mdl.continuous_vartype - qty = mdl.var_dict(foods, ftype, lb=lambda f: f.qmin, ub=lambda f: f.qmax, name=lambda f: "q_%s" % f.name) - - # Limit range of nutrients, and mark them as KPIs - for n in nutrients: - amount = mdl.sum(qty[f] * food_nutrients[f.name, n.name] for f in foods) - mdl.add_range(n.qmin, amount, n.qmax) - mdl.add_kpi(amount, publish_name="Total %s" % n.name) - - # Minimize cost - total_cost = mdl.sum(qty[f] * f.unit_cost for f in foods) - mdl.add_kpi(total_cost, 'Total cost') - - # add a functional KPI , taking a model and a solution as argument - # this KPI counts the number of foods used. - def nb_products(mdl_, s_): - qvs = mdl_.find_matching_vars(pattern="q_") - return sum(1 for qv in qvs if s_[qv] >= 1e-5) - - mdl.add_kpi(nb_products, 'Nb foods') - mdl.minimize(total_cost) - - return mdl - - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- - -if __name__ == '__main__': - mdl = build_diet_model(ints=True, log_output=True, float_precision=6) - mdl.print_information() - - s = mdl.solve() - if s: - qty_vars = mdl.find_matching_vars(pattern="q_") - for fv in qty_vars: - food_name = fv.name[2:] - print("Buy {0:<25} = {1:9.6g}".format(food_name, fv.solution_value)) - - mdl.report_kpis() - # Save the CPLEX solution as "solution.json" program output - with get_environment().get_output_stream("solution.json") as fp: - mdl.solution.export(fp, "json") - else: - print("* model has no solution") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + +# The goal of the diet problem is to select a set of foods that satisfies +# a set of daily nutritional requirements at minimal cost. +# Source of data: http://www.neos-guide.org/content/diet-problem-solver + +from collections import namedtuple + +from docplex.mp.model import Model +from docplex.util.environment import get_environment +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- + +FOODS = [ + ("Roasted Chicken", 0.84, 0, 10), + ("Spaghetti W/ Sauce", 0.78, 0, 10), + ("Tomato,Red,Ripe,Raw", 0.27, 0, 10), + ("Apple,Raw,W/Skin", .24, 0, 10), + ("Grapes", 0.32, 0, 10), + ("Chocolate Chip Cookies", 0.03, 0, 10), + ("Lowfat Milk", 0.23, 0, 10), + ("Raisin Brn", 0.34, 0, 10), + ("Hotdog", 0.31, 0, 10) +] + +NUTRIENTS = [ + ("Calories", 2000, 2500), + ("Calcium", 800, 1600), + ("Iron", 10, 30), + ("Vit_A", 5000, 50000), + ("Dietary_Fiber", 25, 100), + ("Carbohydrates", 0, 300), + ("Protein", 50, 100) +] + +FOOD_NUTRIENTS = [ + ("Roasted Chicken", 277.4, 21.9, 1.8, 77.4, 0, 0, 42.2), + ("Spaghetti W/ Sauce", 358.2, 80.2, 2.3, 3055.2, 11.6, 58.3, 8.2), + ("Tomato,Red,Ripe,Raw", 25.8, 6.2, 0.6, 766.3, 1.4, 5.7, 1), + ("Apple,Raw,W/Skin", 81.4, 9.7, 0.2, 73.1, 3.7, 21, 0.3), + ("Grapes", 15.1, 3.4, 0.1, 24, 0.2, 4.1, 0.2), + ("Chocolate Chip Cookies", 78.1, 6.2, 0.4, 101.8, 0, 9.3, 0.9), + ("Lowfat Milk", 121.2, 296.7, 0.1, 500.2, 0, 11.7, 8.1), + ("Raisin Brn", 115.1, 12.9, 16.8, 1250.2, 4, 27.9, 4), + ("Hotdog", 242.1, 23.5, 2.3, 0, 0, 18, 10.4) +] + +Food = namedtuple("Food", ["name", "unit_cost", "qmin", "qmax"]) +Nutrient = namedtuple("Nutrient", ["name", "qmin", "qmax"]) + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- + + +def build_diet_model(mdl, **kwargs): + ints = kwargs.pop('ints', False) + + # Create tuples with named fields for foods and nutrients + foods = [Food(*f) for f in FOODS] + nutrients = [Nutrient(*row) for row in NUTRIENTS] + + food_nutrients = {(fn[0], nutrients[n].name): + fn[1 + n] for fn in FOOD_NUTRIENTS for n in range(len(NUTRIENTS))} + + # Decision variables, limited to be >= Food.qmin and <= Food.qmax + ftype = mdl.integer_vartype if ints else mdl.continuous_vartype + qty = mdl.var_dict(foods, ftype, lb=lambda f: f.qmin, ub=lambda f: f.qmax, name=lambda f: "q_%s" % f.name) + + # Limit range of nutrients, and mark them as KPIs + for n in nutrients: + amount = mdl.sum(qty[f] * food_nutrients[f.name, n.name] for f in foods) + mdl.add_range(n.qmin, amount, n.qmax) + mdl.add_kpi(amount, publish_name="Total %s" % n.name) + + # Minimize cost + total_cost = mdl.sum(qty[f] * f.unit_cost for f in foods) + mdl.add_kpi(total_cost, 'Total cost') + + # add a functional KPI , taking a model and a solution as argument + # this KPI counts the number of foods used. + def nb_products(mdl_, s_): + qvs = mdl_.find_matching_vars(pattern="q_") + return sum(1 for qv in qvs if s_[qv] >= 1e-5) + + mdl.add_kpi(nb_products, 'Nb foods') + mdl.minimize(total_cost) + + return mdl + + +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- + +if __name__ == '__main__': + with Model(name="diet", log_output=True, float_precision=6) as mdl: + build_diet_model(mdl, ints=True) + mdl.print_information() + + s = mdl.solve() + if s: + qty_vars = mdl.find_matching_vars(pattern="q_") + for fv in qty_vars: + food_name = fv.name[2:] + print("Buy {0:<25} = {1:9.6g}".format(food_name, fv.solution_value)) + + mdl.report_kpis() + # Save the CPLEX solution as "solution.json" program output + with get_environment().get_output_stream("solution.json") as fp: + mdl.solution.export(fp, "json") + else: + print("* model has no solution") diff --git a/examples/mp/modeling/nurses.py b/examples/mp/modeling/nurses.py index 6d4d953..5f5e7b4 100644 --- a/examples/mp/modeling/nurses.py +++ b/examples/mp/modeling/nurses.py @@ -1,530 +1,531 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - -from collections import namedtuple - -from docplex.mp.model import Model -from docplex.util.environment import get_environment - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- - -# utility to convert a weekday string to an index in 0..6 -_all_days = ["monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday"] - - -def day_to_day_week(day): - day_map = {day: d for d, day in enumerate(_all_days)} - return day_map[day.lower()] - - -TWorkRules = namedtuple("TWorkRules", ["work_time_max"]) -TVacation = namedtuple("TVacation", ["nurse", "day"]) -TNursePair = namedtuple("TNursePair", ["firstNurse", "secondNurse"]) -TSkillRequirement = namedtuple("TSkillRequirement", ["department", "skill", "required"]) - - -NURSES = [("Anne", 11, 1, 25), - ("Bethanie", 4, 5, 28), - ("Betsy", 2, 2, 17), - ("Cathy", 2, 2, 17), - ("Cecilia", 9, 5, 38), - ("Chris", 11, 4, 38), - ("Cindy", 5, 2, 21), - ("David", 1, 2, 15), - ("Debbie", 7, 2, 24), - ("Dee", 3, 3, 21), - ("Gloria", 8, 2, 25), - ("Isabelle", 3, 1, 16), - ("Jane", 3, 4, 23), - ("Janelle", 4, 3, 22), - ("Janice", 2, 2, 17), - ("Jemma", 2, 4, 22), - ("Joan", 5, 3, 24), - ("Joyce", 8, 3, 29), - ("Jude", 4, 3, 22), - ("Julie", 6, 2, 22), - ("Juliet", 7, 4, 31), - ("Kate", 5, 3, 24), - ("Nancy", 8, 4, 32), - ("Nathalie", 9, 5, 38), - ("Nicole", 0, 2, 14), - ("Patricia", 1, 1, 13), - ("Patrick", 6, 1, 19), - ("Roberta", 3, 5, 26), - ("Suzanne", 5, 1, 18), - ("Vickie", 7, 1, 20), - ("Wendie", 5, 2, 21), - ("Zoe", 8, 3, 29) - ] - -SHIFTS = [("Emergency", "monday", 2, 8, 3, 5), - ("Emergency", "monday", 8, 12, 4, 7), - ("Emergency", "monday", 12, 18, 2, 5), - ("Emergency", "monday", 18, 2, 3, 7), - ("Consultation", "monday", 8, 12, 10, 13), - ("Consultation", "monday", 12, 18, 8, 12), - ("Cardiac_Care", "monday", 8, 12, 10, 13), - ("Cardiac_Care", "monday", 12, 18, 8, 12), - ("Emergency", "tuesday", 8, 12, 4, 7), - ("Emergency", "tuesday", 12, 18, 2, 5), - ("Emergency", "tuesday", 18, 2, 3, 7), - ("Consultation", "tuesday", 8, 12, 10, 13), - ("Consultation", "tuesday", 12, 18, 8, 12), - ("Cardiac_Care", "tuesday", 8, 12, 4, 7), - ("Cardiac_Care", "tuesday", 12, 18, 2, 5), - ("Cardiac_Care", "tuesday", 18, 2, 3, 7), - ("Emergency", "wednesday", 2, 8, 3, 5), - ("Emergency", "wednesday", 8, 12, 4, 7), - ("Emergency", "wednesday", 12, 18, 2, 5), - ("Emergency", "wednesday", 18, 2, 3, 7), - ("Consultation", "wednesday", 8, 12, 10, 13), - ("Consultation", "wednesday", 12, 18, 8, 12), - ("Emergency", "thursday", 2, 8, 3, 5), - ("Emergency", "thursday", 8, 12, 4, 7), - ("Emergency", "thursday", 12, 18, 2, 5), - ("Emergency", "thursday", 18, 2, 3, 7), - ("Consultation", "thursday", 8, 12, 10, 13), - ("Consultation", "thursday", 12, 18, 8, 12), - ("Emergency", "friday", 2, 8, 3, 5), - ("Emergency", "friday", 8, 12, 4, 7), - ("Emergency", "friday", 12, 18, 2, 5), - ("Emergency", "friday", 18, 2, 3, 7), - ("Consultation", "friday", 8, 12, 10, 13), - ("Consultation", "friday", 12, 18, 8, 12), - ("Emergency", "saturday", 2, 12, 5, 7), - ("Emergency", "saturday", 12, 20, 7, 9), - ("Emergency", "saturday", 20, 2, 12, 12), - ("Emergency", "sunday", 2, 12, 5, 7), - ("Emergency", "sunday", 12, 20, 7, 9), - ("Emergency", "sunday", 20, 2, 12, 12), - ("Geriatrics", "sunday", 8, 10, 2, 5)] - -NURSE_SKILLS = {"Anne": ["Anaesthesiology", "Oncology", "Pediatrics"], - "Betsy": ["Cardiac_Care"], - "Cathy": ["Anaesthesiology"], - "Cecilia": ["Anaesthesiology", "Oncology", "Pediatrics"], - "Chris": ["Cardiac_Care", "Oncology", "Geriatrics"], - "Gloria": ["Pediatrics"], "Jemma": ["Cardiac_Care"], - "Joyce": ["Anaesthesiology", "Pediatrics"], - "Julie": ["Geriatrics"], "Juliet": ["Pediatrics"], - "Kate": ["Pediatrics"], "Nancy": ["Cardiac_Care"], - "Nathalie": ["Anaesthesiology", "Geriatrics"], - "Patrick": ["Oncology"], "Suzanne": ["Pediatrics"], - "Wendie": ["Geriatrics"], - "Zoe": ["Cardiac_Care"] - } - -VACATIONS = [("Anne", "friday"), - ("Anne", "sunday"), - ("Cathy", "thursday"), - ("Cathy", "tuesday"), - ("Joan", "thursday"), - ("Joan", "saturday"), - ("Juliet", "monday"), - ("Juliet", "tuesday"), - ("Juliet", "thursday"), - ("Nathalie", "sunday"), - ("Nathalie", "thursday"), - ("Isabelle", "monday"), - ("Isabelle", "thursday"), - ("Patricia", "saturday"), - ("Patricia", "wednesday"), - ("Nicole", "friday"), - ("Nicole", "wednesday"), - ("Jude", "tuesday"), - ("Jude", "friday"), - ("Debbie", "saturday"), - ("Debbie", "wednesday"), - ("Joyce", "sunday"), - ("Joyce", "thursday"), - ("Chris", "thursday"), - ("Chris", "tuesday"), - ("Cecilia", "friday"), - ("Cecilia", "wednesday"), - ("Patrick", "saturday"), - ("Patrick", "sunday"), - ("Cindy", "sunday"), - ("Dee", "tuesday"), - ("Dee", "friday"), - ("Jemma", "friday"), - ("Jemma", "wednesday"), - ("Bethanie", "wednesday"), - ("Bethanie", "tuesday"), - ("Betsy", "monday"), - ("Betsy", "thursday"), - ("David", "monday"), - ("Gloria", "monday"), - ("Jane", "saturday"), - ("Jane", "sunday"), - ("Janelle", "wednesday"), - ("Janelle", "friday"), - ("Julie", "sunday"), - ("Kate", "tuesday"), - ("Kate", "monday"), - ("Nancy", "sunday"), - ("Roberta", "friday"), - ("Roberta", "saturday"), - ("Janice", "tuesday"), - ("Janice", "friday"), - ("Suzanne", "monday"), - ("Vickie", "wednesday"), - ("Vickie", "friday"), - ("Wendie", "thursday"), - ("Wendie", "saturday"), - ("Zoe", "saturday"), - ("Zoe", "sunday")] - -NURSE_ASSOCIATIONS = [("Isabelle", "Dee"), - ("Anne", "Patrick")] - -NURSE_INCOMPATIBILITIES = [("Patricia", "Patrick"), - ("Janice", "Wendie"), - ("Suzanne", "Betsy"), - ("Janelle", "Jane"), - ("Gloria", "David"), - ("Dee", "Jemma"), - ("Bethanie", "Dee"), - ("Roberta", "Zoe"), - ("Nicole", "Patricia"), - ("Vickie", "Dee"), - ("Joan", "Anne") - ] - -SKILL_REQUIREMENTS = [("Emergency", "Cardiac_Care", 1)] - -DEFAULT_WORK_RULES = TWorkRules(40) - - -# ---------------------------------------------------------------------------- -# Prepare the data for modeling -# ---------------------------------------------------------------------------- -# subclass the namedtuple to refine the str() method as the nurse's name -class TNurse(namedtuple("TNurse1", ["name", "seniority", "qualification", "pay_rate"])): - def __str__(self): - return self.name - - -# specialized namedtuple to redefine its str() method -class TShift(namedtuple("TShift", - ["department", "day", "start_time", "end_time", "min_requirement", "max_requirement"])): - - def __str__(self): - # keep first two characters in department, uppercase - dept2 = self.department[0:4].upper() - # keep 3 days of weekday - dayname = self.day[0:3] - return '{}_{}_{:02d}'.format(dept2, dayname, self.start_time).replace(" ", "_") - - -class ShiftActivity(object): - @staticmethod - def to_abstime(day_index, time_of_day): - """ Convert a pair (day_index, time) into a number of hours since Monday 00:00 - - :param day_index: The index of the day from 1 to 7 (Monday is 1). - :param time_of_day: An integer number of hours. - - :return: - """ - time = 24 * (day_index - 1) - time += time_of_day - return time - - def __init__(self, weekday, start_time_of_day, end_time_of_day): - assert (start_time_of_day >= 0) - assert (start_time_of_day <= 24) - assert (end_time_of_day >= 0) - assert (end_time_of_day <= 24) - - self._weekday = weekday - self._start_time_of_day = start_time_of_day - self._end_time_of_day = end_time_of_day - # conversion to absolute time. - start_day_index = day_to_day_week(self._weekday) - self.start_time = self.to_abstime(start_day_index, start_time_of_day) - end_day_index = start_day_index if end_time_of_day > start_time_of_day else start_day_index + 1 - self.end_time = self.to_abstime(end_day_index, end_time_of_day) - assert self.end_time > self.start_time - - @property - def duration(self): - return self.end_time - self.start_time - - def overlaps(self, other_shift): - if not isinstance(other_shift, ShiftActivity): - return False - else: - return other_shift.end_time > self.start_time and other_shift.start_time < self.end_time - - -def solve(model, **kwargs): - # Here, we set the number of threads for CPLEX to 2 and set the time limit to 2mins. - model.parameters.threads = 2 - model.parameters.timelimit = 120 # nurse should not take more than that ! - sol = model.solve(log_output=True, **kwargs) - if sol is not None: - print("solution for a cost of {}".format(model.objective_value)) - print_information(model) - print_solution(model) - return model.objective_value - else: - print("* model is infeasible") - return None - - -def load_data(model, shifts_, nurses_, nurse_skills, vacations_=None, - nurse_associations_=None, nurse_imcompatibilities_=None, verbose=True): - """ Usage: load_data(shifts, nurses, nurse_skills, vacations) """ - model.number_of_overlaps = 0 - model.work_rules = DEFAULT_WORK_RULES - model.shifts = [TShift(*shift_row) for shift_row in shifts_] - model.nurses = [TNurse(*nurse_row) for nurse_row in nurses_] - model.skill_requirements = SKILL_REQUIREMENTS - model.nurse_skills = nurse_skills - # transactional data - model.vacations = [TVacation(*vacation_row) for vacation_row in vacations_] if vacations_ else [] - model.nurse_associations = [TNursePair(*npr) for npr in nurse_associations_]\ - if nurse_associations_ else [] - model.nurse_incompatibilities = [TNursePair(*npr) for npr in nurse_imcompatibilities_]\ - if nurse_imcompatibilities_ else [] - - # computed - model.departments = set(sh.department for sh in model.shifts) - - if verbose: - print('#nurses: {0}'.format(len(model.nurses))) - print('#shifts: {0}'.format(len(model.shifts))) - print('#vacations: {0}'.format(len(model.vacations))) - print("#associations=%d" % len(model.nurse_associations)) - print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) - - -def setup_data(model): - """ compute internal data """ - # compute shift activities (start, end duration) and stor ethem in a dict indexed by shifts - model.shift_activities = {s: ShiftActivity(s.day, s.start_time, s.end_time) for s in model.shifts} - # map from nurse names to nurse tuples. - model.nurses_by_id = {n.name: n for n in model.nurses} - - -def setup_variables(model): - all_nurses, all_shifts = model.nurses, model.shifts - # one binary variable for each pair (nurse, shift) equal to 1 iff nurse n is assigned to shift s - model.nurse_assignment_vars = model.binary_var_matrix(all_nurses, all_shifts, 'NurseAssigned') - # for each nurse, allocate one variable for work time - model.nurse_work_time_vars = model.continuous_var_dict(all_nurses, lb=0, name='NurseWorkTime') - # and two variables for over_average and under-average work time - model.nurse_over_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, - name='NurseOverAverageWorkTime') - model.nurse_under_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, - name='NurseUnderAverageWorkTime') - # finally the global average work time - model.average_nurse_work_time = model.continuous_var(lb=0, name='AverageWorkTime') - - -def setup_constraints(model): - all_nurses = model.nurses - all_shifts = model.shifts - nurse_assigned = model.nurse_assignment_vars - nurse_work_time = model.nurse_work_time_vars - shift_activities = model.shift_activities - nurses_by_id = model.nurses_by_id - max_work_time = model.work_rules.work_time_max - - # define average - model.add_constraint( - len(all_nurses) * model.average_nurse_work_time == model.sum(nurse_work_time[n] for n in all_nurses), "average") - - # compute nurse work time , average and under, over - for n in all_nurses: - work_time_var = nurse_work_time[n] - model.add_constraint( - work_time_var == model.sum(nurse_assigned[n, s] * shift_activities[s].duration for s in all_shifts), - "work_time_{0!s}".format(n)) - - # relate over/under average worktime variables to the worktime variables - # the trick here is that variables have zero lower bound - # however, thse variables are not completely defined by this constraint, - # only their difference is. - # if these variables are part of the objective, CPLEX wil naturally minimize their value, - # as expected - model.add_constraint( - work_time_var == model.average_nurse_work_time - + model.nurse_over_average_time_vars[n] - - model.nurse_under_average_time_vars[n], - "average_work_time_{0!s}".format(n)) - - # state the maximum work time as a constraint, so that is can be relaxed, - # should the problem become infeasible. - model.add_constraint(work_time_var <= max_work_time, "max_time_{0!s}".format(n)) - - # vacations - v = 0 - for vac_nurse_id, vac_day in model.vacations: - vac_n = nurses_by_id[vac_nurse_id] - for shift in (s for s in all_shifts if s.day == vac_day): - v += 1 - model.add_constraint(nurse_assigned[vac_n, shift] == 0, - "medium_vacations_{0!s}_{1!s}_{2!s}".format(vac_n, vac_day, shift)) - #print('#vacation cts: {0}'.format(v)) - - # a nurse cannot be assigned overlapping shifts - # post only one constraint per couple(s1, s2) - number_of_overlaps = 0 - nb_shifts = len(all_shifts) - for i1 in range(nb_shifts): - for i2 in range(i1 + 1, nb_shifts): - s1 = all_shifts[i1] - s2 = all_shifts[i2] - if shift_activities[s1].overlaps(shift_activities[s2]): - number_of_overlaps += 1 - for n in all_nurses: - model.add_constraint(nurse_assigned[n, s1] + nurse_assigned[n, s2] <= 1, - "high_overlapping_{0!s}_{1!s}_{2!s}".format(s1, s2, n)) - #print('# overlapping cts: {0}'.format(number_of_overlaps)) - - for s in all_shifts: - demand_min = s.min_requirement - demand_max = s.max_requirement - total_assigned = model.sum(nurse_assigned[n, s] for n in model.nurses) - model.add_constraint(total_assigned >= demand_min, - "high_req_min_{0!s}_{1}".format(s, demand_min)) - model.add_constraint(total_assigned <= demand_max, - "medium_req_max_{0!s}_{1}".format(s, demand_max)) - - for (dept, skill, required) in model.skill_requirements: - if required > 0: - for dsh in (s for s in all_shifts if dept == s.department): - model.add_constraint(model.sum(nurse_assigned[skilled_nurse, dsh] for skilled_nurse in - (n for n in all_nurses if - n.name in model.nurse_skills.keys() and skill in model.nurse_skills[ - n.name])) >= required, - "high_required_{0!s}_{1!s}_{2!s}_{3!s}".format(dept, skill, required, dsh)) - - # nurse-nurse associations - # for each pair of associated nurses, their assignment variables are equal - # over all shifts. - c = 0 - for (nurse_id1, nurse_id2) in model.nurse_associations: - if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: - nurse1 = nurses_by_id[nurse_id1] - nurse2 = nurses_by_id[nurse_id2] - for s in all_shifts: - c += 1 - ctname = 'medium_ct_nurse_assoc_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) - model.add_constraint(nurse_assigned[nurse1, s] == nurse_assigned[nurse2, s], ctname) - - # nurse-nurse incompatibilities - # for each pair of incompatible nurses, the sum of assigned variables is less than one - # in other terms, both nurses can never be assigned to the same shift - c = 0 - for (nurse_id1, nurse_id2) in model.nurse_incompatibilities: - if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: - nurse1 = nurses_by_id[nurse_id1] - nurse2 = nurses_by_id[nurse_id2] - for s in all_shifts: - c += 1 - ctname = 'medium_ct_nurse_incompat_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) - model.add_constraint(nurse_assigned[nurse1, s] + nurse_assigned[nurse2, s] <= 1, ctname) - - model.total_number_of_assignments = model.sum(nurse_assigned[n, s] for n in all_nurses for s in all_shifts) - # model.nurse_costs = [model.nurse_assignment_vars[n, s] * n.pay_rate * model.shift_activities[s].duration - # for n in model.nurses for s in model.shifts] - - def assignment_cost_f(ns): - n, s = ns - return n.pay_rate * model.shift_activities[s].duration - - model.nurse_costs = model.scal_prod_f(nurse_assigned, assignment_cost_f) - model.total_salary_cost = model.sum(model.nurse_costs) - - -def setup_objective(model): - model.add_kpi(model.total_salary_cost, "Total salary cost") - model.add_kpi(model.total_number_of_assignments, "Total number of assignments") - model.add_kpi(model.average_nurse_work_time, "average work time") - - total_over_average_worktime = model.sum(model.nurse_over_average_time_vars[n] for n in model.nurses) - total_under_average_worktime = model.sum(model.nurse_under_average_time_vars[n] for n in model.nurses) - model.add_kpi(total_over_average_worktime, "Total over-average worktime") - model.add_kpi(total_under_average_worktime, "Total under-average worktime") - total_fairness = total_over_average_worktime + total_under_average_worktime - model.add_kpi(total_fairness, "Total fairness") - - model.minimize(model.total_salary_cost + total_fairness + model.total_number_of_assignments) - - -def print_information(model): - print("#shifts=%d" % len(model.shifts)) - print("#nurses=%d" % len(model.nurses)) - print("#vacations=%d" % len(model.vacations)) - print("#nurse skills=%d" % len(model.nurse_skills)) - print("#nurse associations=%d" % len(model.nurse_associations)) - print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) - model.print_information() - model.report_kpis() - - -def print_solution(model): - print("*************************** Solution ***************************") - print("Allocation By Department:") - for d in model.departments: - print("\t{}: {}".format(d, sum( - model.nurse_assignment_vars[n, s].solution_value for n in model.nurses for s in model.shifts if - s.department == d))) - print("Cost By Department:") - for d in model.departments: - cost = sum( - model.nurse_assignment_vars[n, s].solution_value * n.pay_rate * model.shift_activities[s].duration for n in - model.nurses for s in model.shifts if s.department == d) - print("\t{}: {}".format(d, cost)) - print("Nurses Assignments") - for n in sorted(model.nurses): - total_hours = sum( - model.nurse_assignment_vars[n, s].solution_value * model.shift_activities[s].duration for s in model.shifts) - print("\t{}: total hours:{}".format(n.name, total_hours)) - for s in model.shifts: - if model.nurse_assignment_vars[n, s].solution_value == 1: - print("\t\t{}: {} {}-{}".format(s.day, s.department, s.start_time, s.end_time)) - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- - -def build(context=None, verbose=False, **kwargs): - mdl = Model("Nurses", context=context, **kwargs) - load_data(mdl, SHIFTS, NURSES, NURSE_SKILLS, VACATIONS, NURSE_ASSOCIATIONS, - NURSE_INCOMPATIBILITIES, verbose=verbose) - setup_data(mdl) - setup_variables(mdl) - setup_constraints(mdl) - setup_objective(mdl) - return mdl - - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- - -if __name__ == '__main__': - # Build model - model = build() - - # Solve the model and print solution - solve(model) - - # Save the CPLEX solution as "solution.json" program output - with get_environment().get_output_stream("solution.json") as fp: - model.solution.export(fp, "json") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + +from collections import namedtuple + +from docplex.mp.model import Model +from docplex.util.environment import get_environment + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- + +# utility to convert a weekday string to an index in 0..6 +_all_days = ["monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday"] + + +def day_to_day_week(day): + day_map = {day: d for d, day in enumerate(_all_days)} + return day_map[day.lower()] + + +TWorkRules = namedtuple("TWorkRules", ["work_time_max"]) +TVacation = namedtuple("TVacation", ["nurse", "day"]) +TNursePair = namedtuple("TNursePair", ["firstNurse", "secondNurse"]) +TSkillRequirement = namedtuple("TSkillRequirement", ["department", "skill", "required"]) + + +NURSES = [("Anne", 11, 1, 25), + ("Bethanie", 4, 5, 28), + ("Betsy", 2, 2, 17), + ("Cathy", 2, 2, 17), + ("Cecilia", 9, 5, 38), + ("Chris", 11, 4, 38), + ("Cindy", 5, 2, 21), + ("David", 1, 2, 15), + ("Debbie", 7, 2, 24), + ("Dee", 3, 3, 21), + ("Gloria", 8, 2, 25), + ("Isabelle", 3, 1, 16), + ("Jane", 3, 4, 23), + ("Janelle", 4, 3, 22), + ("Janice", 2, 2, 17), + ("Jemma", 2, 4, 22), + ("Joan", 5, 3, 24), + ("Joyce", 8, 3, 29), + ("Jude", 4, 3, 22), + ("Julie", 6, 2, 22), + ("Juliet", 7, 4, 31), + ("Kate", 5, 3, 24), + ("Nancy", 8, 4, 32), + ("Nathalie", 9, 5, 38), + ("Nicole", 0, 2, 14), + ("Patricia", 1, 1, 13), + ("Patrick", 6, 1, 19), + ("Roberta", 3, 5, 26), + ("Suzanne", 5, 1, 18), + ("Vickie", 7, 1, 20), + ("Wendie", 5, 2, 21), + ("Zoe", 8, 3, 29) + ] + +SHIFTS = [("Emergency", "monday", 2, 8, 3, 5), + ("Emergency", "monday", 8, 12, 4, 7), + ("Emergency", "monday", 12, 18, 2, 5), + ("Emergency", "monday", 18, 2, 3, 7), + ("Consultation", "monday", 8, 12, 10, 13), + ("Consultation", "monday", 12, 18, 8, 12), + ("Cardiac_Care", "monday", 8, 12, 10, 13), + ("Cardiac_Care", "monday", 12, 18, 8, 12), + ("Emergency", "tuesday", 8, 12, 4, 7), + ("Emergency", "tuesday", 12, 18, 2, 5), + ("Emergency", "tuesday", 18, 2, 3, 7), + ("Consultation", "tuesday", 8, 12, 10, 13), + ("Consultation", "tuesday", 12, 18, 8, 12), + ("Cardiac_Care", "tuesday", 8, 12, 4, 7), + ("Cardiac_Care", "tuesday", 12, 18, 2, 5), + ("Cardiac_Care", "tuesday", 18, 2, 3, 7), + ("Emergency", "wednesday", 2, 8, 3, 5), + ("Emergency", "wednesday", 8, 12, 4, 7), + ("Emergency", "wednesday", 12, 18, 2, 5), + ("Emergency", "wednesday", 18, 2, 3, 7), + ("Consultation", "wednesday", 8, 12, 10, 13), + ("Consultation", "wednesday", 12, 18, 8, 12), + ("Emergency", "thursday", 2, 8, 3, 5), + ("Emergency", "thursday", 8, 12, 4, 7), + ("Emergency", "thursday", 12, 18, 2, 5), + ("Emergency", "thursday", 18, 2, 3, 7), + ("Consultation", "thursday", 8, 12, 10, 13), + ("Consultation", "thursday", 12, 18, 8, 12), + ("Emergency", "friday", 2, 8, 3, 5), + ("Emergency", "friday", 8, 12, 4, 7), + ("Emergency", "friday", 12, 18, 2, 5), + ("Emergency", "friday", 18, 2, 3, 7), + ("Consultation", "friday", 8, 12, 10, 13), + ("Consultation", "friday", 12, 18, 8, 12), + ("Emergency", "saturday", 2, 12, 5, 7), + ("Emergency", "saturday", 12, 20, 7, 9), + ("Emergency", "saturday", 20, 2, 12, 12), + ("Emergency", "sunday", 2, 12, 5, 7), + ("Emergency", "sunday", 12, 20, 7, 9), + ("Emergency", "sunday", 20, 2, 12, 12), + ("Geriatrics", "sunday", 8, 10, 2, 5)] + +NURSE_SKILLS = {"Anne": ["Anaesthesiology", "Oncology", "Pediatrics"], + "Betsy": ["Cardiac_Care"], + "Cathy": ["Anaesthesiology"], + "Cecilia": ["Anaesthesiology", "Oncology", "Pediatrics"], + "Chris": ["Cardiac_Care", "Oncology", "Geriatrics"], + "Gloria": ["Pediatrics"], "Jemma": ["Cardiac_Care"], + "Joyce": ["Anaesthesiology", "Pediatrics"], + "Julie": ["Geriatrics"], "Juliet": ["Pediatrics"], + "Kate": ["Pediatrics"], "Nancy": ["Cardiac_Care"], + "Nathalie": ["Anaesthesiology", "Geriatrics"], + "Patrick": ["Oncology"], "Suzanne": ["Pediatrics"], + "Wendie": ["Geriatrics"], + "Zoe": ["Cardiac_Care"] + } + +VACATIONS = [("Anne", "friday"), + ("Anne", "sunday"), + ("Cathy", "thursday"), + ("Cathy", "tuesday"), + ("Joan", "thursday"), + ("Joan", "saturday"), + ("Juliet", "monday"), + ("Juliet", "tuesday"), + ("Juliet", "thursday"), + ("Nathalie", "sunday"), + ("Nathalie", "thursday"), + ("Isabelle", "monday"), + ("Isabelle", "thursday"), + ("Patricia", "saturday"), + ("Patricia", "wednesday"), + ("Nicole", "friday"), + ("Nicole", "wednesday"), + ("Jude", "tuesday"), + ("Jude", "friday"), + ("Debbie", "saturday"), + ("Debbie", "wednesday"), + ("Joyce", "sunday"), + ("Joyce", "thursday"), + ("Chris", "thursday"), + ("Chris", "tuesday"), + ("Cecilia", "friday"), + ("Cecilia", "wednesday"), + ("Patrick", "saturday"), + ("Patrick", "sunday"), + ("Cindy", "sunday"), + ("Dee", "tuesday"), + ("Dee", "friday"), + ("Jemma", "friday"), + ("Jemma", "wednesday"), + ("Bethanie", "wednesday"), + ("Bethanie", "tuesday"), + ("Betsy", "monday"), + ("Betsy", "thursday"), + ("David", "monday"), + ("Gloria", "monday"), + ("Jane", "saturday"), + ("Jane", "sunday"), + ("Janelle", "wednesday"), + ("Janelle", "friday"), + ("Julie", "sunday"), + ("Kate", "tuesday"), + ("Kate", "monday"), + ("Nancy", "sunday"), + ("Roberta", "friday"), + ("Roberta", "saturday"), + ("Janice", "tuesday"), + ("Janice", "friday"), + ("Suzanne", "monday"), + ("Vickie", "wednesday"), + ("Vickie", "friday"), + ("Wendie", "thursday"), + ("Wendie", "saturday"), + ("Zoe", "saturday"), + ("Zoe", "sunday")] + +NURSE_ASSOCIATIONS = [("Isabelle", "Dee"), + ("Anne", "Patrick")] + +NURSE_INCOMPATIBILITIES = [("Patricia", "Patrick"), + ("Janice", "Wendie"), + ("Suzanne", "Betsy"), + ("Janelle", "Jane"), + ("Gloria", "David"), + ("Dee", "Jemma"), + ("Bethanie", "Dee"), + ("Roberta", "Zoe"), + ("Nicole", "Patricia"), + ("Vickie", "Dee"), + ("Joan", "Anne") + ] + +SKILL_REQUIREMENTS = [("Emergency", "Cardiac_Care", 1)] + +DEFAULT_WORK_RULES = TWorkRules(40) + + +# ---------------------------------------------------------------------------- +# Prepare the data for modeling +# ---------------------------------------------------------------------------- +# subclass the namedtuple to refine the str() method as the nurse's name +class TNurse(namedtuple("TNurse1", ["name", "seniority", "qualification", "pay_rate"])): + def __str__(self): + return self.name + + +# specialized namedtuple to redefine its str() method +class TShift(namedtuple("TShift", + ["department", "day", "start_time", "end_time", "min_requirement", "max_requirement"])): + + def __str__(self): + # keep first two characters in department, uppercase + dept2 = self.department[0:4].upper() + # keep 3 days of weekday + dayname = self.day[0:3] + return '{}_{}_{:02d}'.format(dept2, dayname, self.start_time).replace(" ", "_") + + +class ShiftActivity(object): + @staticmethod + def to_abstime(day_index, time_of_day): + """ Convert a pair (day_index, time) into a number of hours since Monday 00:00 + + :param day_index: The index of the day from 1 to 7 (Monday is 1). + :param time_of_day: An integer number of hours. + + :return: + """ + time = 24 * (day_index - 1) + time += time_of_day + return time + + def __init__(self, weekday, start_time_of_day, end_time_of_day): + assert (start_time_of_day >= 0) + assert (start_time_of_day <= 24) + assert (end_time_of_day >= 0) + assert (end_time_of_day <= 24) + + self._weekday = weekday + self._start_time_of_day = start_time_of_day + self._end_time_of_day = end_time_of_day + # conversion to absolute time. + start_day_index = day_to_day_week(self._weekday) + self.start_time = self.to_abstime(start_day_index, start_time_of_day) + end_day_index = start_day_index if end_time_of_day > start_time_of_day else start_day_index + 1 + self.end_time = self.to_abstime(end_day_index, end_time_of_day) + assert self.end_time > self.start_time + + @property + def duration(self): + return self.end_time - self.start_time + + def overlaps(self, other_shift): + if not isinstance(other_shift, ShiftActivity): + return False + else: + return other_shift.end_time > self.start_time and other_shift.start_time < self.end_time + + +def solve(model, **kwargs): + # Here, we set the number of threads for CPLEX to 2 and set the time limit to 2mins. + model.parameters.threads = 2 + model.parameters.timelimit = 120 # nurse should not take more than that ! + sol = model.solve(log_output=True, **kwargs) + if sol is not None: + print("solution for a cost of {}".format(model.objective_value)) + print_information(model) + print_solution(model) + return model.objective_value + else: + print("* model is infeasible") + return None + + +def load_data(model, shifts_, nurses_, nurse_skills, vacations_=None, + nurse_associations_=None, nurse_imcompatibilities_=None, verbose=True): + """ Usage: load_data(shifts, nurses, nurse_skills, vacations) """ + model.number_of_overlaps = 0 + model.work_rules = DEFAULT_WORK_RULES + model.shifts = [TShift(*shift_row) for shift_row in shifts_] + model.nurses = [TNurse(*nurse_row) for nurse_row in nurses_] + model.skill_requirements = SKILL_REQUIREMENTS + model.nurse_skills = nurse_skills + # transactional data + model.vacations = [TVacation(*vacation_row) for vacation_row in vacations_] if vacations_ else [] + model.nurse_associations = [TNursePair(*npr) for npr in nurse_associations_]\ + if nurse_associations_ else [] + model.nurse_incompatibilities = [TNursePair(*npr) for npr in nurse_imcompatibilities_]\ + if nurse_imcompatibilities_ else [] + + # computed + model.departments = set(sh.department for sh in model.shifts) + + if verbose: + print('#nurses: {0}'.format(len(model.nurses))) + print('#shifts: {0}'.format(len(model.shifts))) + print('#vacations: {0}'.format(len(model.vacations))) + print("#associations=%d" % len(model.nurse_associations)) + print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) + + +def setup_data(model): + """ compute internal data """ + # compute shift activities (start, end duration) and stor ethem in a dict indexed by shifts + model.shift_activities = {s: ShiftActivity(s.day, s.start_time, s.end_time) for s in model.shifts} + # map from nurse names to nurse tuples. + model.nurses_by_id = {n.name: n for n in model.nurses} + + +def setup_variables(model): + all_nurses, all_shifts = model.nurses, model.shifts + # one binary variable for each pair (nurse, shift) equal to 1 iff nurse n is assigned to shift s + model.nurse_assignment_vars = model.binary_var_matrix(all_nurses, all_shifts, 'NurseAssigned') + # for each nurse, allocate one variable for work time + model.nurse_work_time_vars = model.continuous_var_dict(all_nurses, lb=0, name='NurseWorkTime') + # and two variables for over_average and under-average work time + model.nurse_over_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, + name='NurseOverAverageWorkTime') + model.nurse_under_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, + name='NurseUnderAverageWorkTime') + # finally the global average work time + model.average_nurse_work_time = model.continuous_var(lb=0, name='AverageWorkTime') + + +def setup_constraints(model): + all_nurses = model.nurses + all_shifts = model.shifts + nurse_assigned = model.nurse_assignment_vars + nurse_work_time = model.nurse_work_time_vars + shift_activities = model.shift_activities + nurses_by_id = model.nurses_by_id + max_work_time = model.work_rules.work_time_max + + # define average + model.add_constraint( + len(all_nurses) * model.average_nurse_work_time == model.sum(nurse_work_time[n] for n in all_nurses), "average") + + # compute nurse work time , average and under, over + for n in all_nurses: + work_time_var = nurse_work_time[n] + model.add_constraint( + work_time_var == model.sum(nurse_assigned[n, s] * shift_activities[s].duration for s in all_shifts), + "work_time_{0!s}".format(n)) + + # relate over/under average worktime variables to the worktime variables + # the trick here is that variables have zero lower bound + # however, thse variables are not completely defined by this constraint, + # only their difference is. + # if these variables are part of the objective, CPLEX wil naturally minimize their value, + # as expected + model.add_constraint( + work_time_var == model.average_nurse_work_time + + model.nurse_over_average_time_vars[n] + - model.nurse_under_average_time_vars[n], + "average_work_time_{0!s}".format(n)) + + # state the maximum work time as a constraint, so that is can be relaxed, + # should the problem become infeasible. + model.add_constraint(work_time_var <= max_work_time, "max_time_{0!s}".format(n)) + + # vacations + v = 0 + for vac_nurse_id, vac_day in model.vacations: + vac_n = nurses_by_id[vac_nurse_id] + for shift in (s for s in all_shifts if s.day == vac_day): + v += 1 + model.add_constraint(nurse_assigned[vac_n, shift] == 0, + "medium_vacations_{0!s}_{1!s}_{2!s}".format(vac_n, vac_day, shift)) + #print('#vacation cts: {0}'.format(v)) + + # a nurse cannot be assigned overlapping shifts + # post only one constraint per couple(s1, s2) + number_of_overlaps = 0 + nb_shifts = len(all_shifts) + for i1 in range(nb_shifts): + for i2 in range(i1 + 1, nb_shifts): + s1 = all_shifts[i1] + s2 = all_shifts[i2] + if shift_activities[s1].overlaps(shift_activities[s2]): + number_of_overlaps += 1 + for n in all_nurses: + model.add_constraint(nurse_assigned[n, s1] + nurse_assigned[n, s2] <= 1, + "high_overlapping_{0!s}_{1!s}_{2!s}".format(s1, s2, n)) + #print('# overlapping cts: {0}'.format(number_of_overlaps)) + + for s in all_shifts: + demand_min = s.min_requirement + demand_max = s.max_requirement + total_assigned = model.sum(nurse_assigned[n, s] for n in model.nurses) + model.add_constraint(total_assigned >= demand_min, + "high_req_min_{0!s}_{1}".format(s, demand_min)) + model.add_constraint(total_assigned <= demand_max, + "medium_req_max_{0!s}_{1}".format(s, demand_max)) + + for (dept, skill, required) in model.skill_requirements: + if required > 0: + for dsh in (s for s in all_shifts if dept == s.department): + model.add_constraint(model.sum(nurse_assigned[skilled_nurse, dsh] for skilled_nurse in + (n for n in all_nurses if + n.name in model.nurse_skills.keys() and skill in model.nurse_skills[ + n.name])) >= required, + "high_required_{0!s}_{1!s}_{2!s}_{3!s}".format(dept, skill, required, dsh)) + + # nurse-nurse associations + # for each pair of associated nurses, their assignment variables are equal + # over all shifts. + c = 0 + for (nurse_id1, nurse_id2) in model.nurse_associations: + if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: + nurse1 = nurses_by_id[nurse_id1] + nurse2 = nurses_by_id[nurse_id2] + for s in all_shifts: + c += 1 + ctname = 'medium_ct_nurse_assoc_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) + model.add_constraint(nurse_assigned[nurse1, s] == nurse_assigned[nurse2, s], ctname) + + # nurse-nurse incompatibilities + # for each pair of incompatible nurses, the sum of assigned variables is less than one + # in other terms, both nurses can never be assigned to the same shift + c = 0 + for (nurse_id1, nurse_id2) in model.nurse_incompatibilities: + if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: + nurse1 = nurses_by_id[nurse_id1] + nurse2 = nurses_by_id[nurse_id2] + for s in all_shifts: + c += 1 + ctname = 'medium_ct_nurse_incompat_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) + model.add_constraint(nurse_assigned[nurse1, s] + nurse_assigned[nurse2, s] <= 1, ctname) + + model.total_number_of_assignments = model.sum(nurse_assigned[n, s] for n in all_nurses for s in all_shifts) + # model.nurse_costs = [model.nurse_assignment_vars[n, s] * n.pay_rate * model.shift_activities[s].duration + # for n in model.nurses for s in model.shifts] + + def assignment_cost_f(ns): + n, s = ns + return n.pay_rate * model.shift_activities[s].duration + + model.nurse_costs = model.scal_prod_f(nurse_assigned, assignment_cost_f) + model.total_salary_cost = model.sum(model.nurse_costs) + + +def setup_objective(model): + model.add_kpi(model.total_salary_cost, "Total salary cost") + model.add_kpi(model.total_number_of_assignments, "Total number of assignments") + model.add_kpi(model.average_nurse_work_time, "average work time") + + total_over_average_worktime = model.sum(model.nurse_over_average_time_vars[n] for n in model.nurses) + total_under_average_worktime = model.sum(model.nurse_under_average_time_vars[n] for n in model.nurses) + model.add_kpi(total_over_average_worktime, "Total over-average worktime") + model.add_kpi(total_under_average_worktime, "Total under-average worktime") + total_fairness = total_over_average_worktime + total_under_average_worktime + model.add_kpi(total_fairness, "Total fairness") + + model.minimize(model.total_salary_cost + total_fairness + model.total_number_of_assignments) + + +def print_information(model): + print("#shifts=%d" % len(model.shifts)) + print("#nurses=%d" % len(model.nurses)) + print("#vacations=%d" % len(model.vacations)) + print("#nurse skills=%d" % len(model.nurse_skills)) + print("#nurse associations=%d" % len(model.nurse_associations)) + print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) + model.print_information() + model.report_kpis() + + +def print_solution(model): + print("*************************** Solution ***************************") + print("Allocation By Department:") + for d in model.departments: + print("\t{}: {}".format(d, sum( + model.nurse_assignment_vars[n, s].solution_value for n in model.nurses for s in model.shifts if + s.department == d))) + print("Cost By Department:") + for d in model.departments: + cost = sum( + model.nurse_assignment_vars[n, s].solution_value * n.pay_rate * model.shift_activities[s].duration for n in + model.nurses for s in model.shifts if s.department == d) + print("\t{}: {}".format(d, cost)) + print("Nurses Assignments") + for n in sorted(model.nurses): + total_hours = sum( + model.nurse_assignment_vars[n, s].solution_value * model.shift_activities[s].duration for s in model.shifts) + print("\t{}: total hours:{}".format(n.name, total_hours)) + for s in model.shifts: + if model.nurse_assignment_vars[n, s].solution_value == 1: + print("\t\t{}: {} {}-{}".format(s.day, s.department, s.start_time, s.end_time)) + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- + +def build(context=None, verbose=False, **kwargs): + mdl = Model("Nurses", context=context, **kwargs) + load_data(mdl, SHIFTS, NURSES, NURSE_SKILLS, VACATIONS, NURSE_ASSOCIATIONS, + NURSE_INCOMPATIBILITIES, verbose=verbose) + setup_data(mdl) + setup_variables(mdl) + setup_constraints(mdl) + setup_objective(mdl) + return mdl + + +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- + +if __name__ == '__main__': + # Build model + model = build() + + # Solve the model and print solution + solve(model) + + # Save the CPLEX solution as "solution.json" program output + with get_environment().get_output_stream("solution.json") as fp: + model.solution.export(fp, "json") + model.end() diff --git a/examples/mp/modeling/nurses_multiobj.py b/examples/mp/modeling/nurses_multiobj.py index 96736e2..d0d7ac3 100644 --- a/examples/mp/modeling/nurses_multiobj.py +++ b/examples/mp/modeling/nurses_multiobj.py @@ -1,545 +1,548 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - -from collections import namedtuple - -from docplex.mp.model import Model -from docplex.mp.constants import ObjectiveSense -from docplex.util.environment import get_environment - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- - -# utility to convert a weekday string to an index in 0..6 -_all_days = ["monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday"] - - -def day_to_day_week(day): - day_map = {day: d for d, day in enumerate(_all_days)} - return day_map[day.lower()] - - -TWorkRules = namedtuple("TWorkRules", ["work_time_max"]) -TVacation = namedtuple("TVacation", ["nurse", "day"]) -TNursePair = namedtuple("TNursePair", ["firstNurse", "secondNurse"]) -TSkillRequirement = namedtuple("TSkillRequirement", ["department", "skill", "required"]) - - -NURSES = [("Anne", 11, 1, 25), - ("Bethanie", 4, 5, 28), - ("Betsy", 2, 2, 17), - ("Cathy", 2, 2, 17), - ("Cecilia", 9, 5, 38), - ("Chris", 11, 4, 38), - ("Cindy", 5, 2, 21), - ("David", 1, 2, 15), - ("Debbie", 7, 2, 24), - ("Dee", 3, 3, 21), - ("Gloria", 8, 2, 25), - ("Isabelle", 3, 1, 16), - ("Jane", 3, 4, 23), - ("Janelle", 4, 3, 22), - ("Janice", 2, 2, 17), - ("Jemma", 2, 4, 22), - ("Joan", 5, 3, 24), - ("Joyce", 8, 3, 29), - ("Jude", 4, 3, 22), - ("Julie", 6, 2, 22), - ("Juliet", 7, 4, 31), - ("Kate", 5, 3, 24), - ("Nancy", 8, 4, 32), - ("Nathalie", 9, 5, 38), - ("Nicole", 0, 2, 14), - ("Patricia", 1, 1, 13), - ("Patrick", 6, 1, 19), - ("Roberta", 3, 5, 26), - ("Suzanne", 5, 1, 18), - ("Vickie", 7, 1, 20), - ("Wendie", 5, 2, 21), - ("Zoe", 8, 3, 29) - ] - -SHIFTS = [("Emergency", "monday", 2, 8, 3, 5), - ("Emergency", "monday", 8, 12, 4, 7), - ("Emergency", "monday", 12, 18, 2, 5), - ("Emergency", "monday", 18, 2, 3, 7), - ("Consultation", "monday", 8, 12, 10, 13), - ("Consultation", "monday", 12, 18, 8, 12), - ("Cardiac_Care", "monday", 8, 12, 10, 13), - ("Cardiac_Care", "monday", 12, 18, 8, 12), - ("Emergency", "tuesday", 8, 12, 4, 7), - ("Emergency", "tuesday", 12, 18, 2, 5), - ("Emergency", "tuesday", 18, 2, 3, 7), - ("Consultation", "tuesday", 8, 12, 10, 13), - ("Consultation", "tuesday", 12, 18, 8, 12), - ("Cardiac_Care", "tuesday", 8, 12, 4, 7), - ("Cardiac_Care", "tuesday", 12, 18, 2, 5), - ("Cardiac_Care", "tuesday", 18, 2, 3, 7), - ("Emergency", "wednesday", 2, 8, 3, 5), - ("Emergency", "wednesday", 8, 12, 4, 7), - ("Emergency", "wednesday", 12, 18, 2, 5), - ("Emergency", "wednesday", 18, 2, 3, 7), - ("Consultation", "wednesday", 8, 12, 10, 13), - ("Consultation", "wednesday", 12, 18, 8, 12), - ("Emergency", "thursday", 2, 8, 3, 5), - ("Emergency", "thursday", 8, 12, 4, 7), - ("Emergency", "thursday", 12, 18, 2, 5), - ("Emergency", "thursday", 18, 2, 3, 7), - ("Consultation", "thursday", 8, 12, 10, 13), - ("Consultation", "thursday", 12, 18, 8, 12), - ("Emergency", "friday", 2, 8, 3, 5), - ("Emergency", "friday", 8, 12, 4, 7), - ("Emergency", "friday", 12, 18, 2, 5), - ("Emergency", "friday", 18, 2, 3, 7), - ("Consultation", "friday", 8, 12, 10, 13), - ("Consultation", "friday", 12, 18, 8, 12), - ("Emergency", "saturday", 2, 12, 5, 7), - ("Emergency", "saturday", 12, 20, 7, 9), - ("Emergency", "saturday", 20, 2, 12, 12), - ("Emergency", "sunday", 2, 12, 5, 7), - ("Emergency", "sunday", 12, 20, 7, 9), - ("Emergency", "sunday", 20, 2, 12, 12), - ("Geriatrics", "sunday", 8, 10, 2, 5)] - -NURSE_SKILLS = {"Anne": ["Anaesthesiology", "Oncology", "Pediatrics"], - "Betsy": ["Cardiac_Care"], - "Cathy": ["Anaesthesiology"], - "Cecilia": ["Anaesthesiology", "Oncology", "Pediatrics"], - "Chris": ["Cardiac_Care", "Oncology", "Geriatrics"], - "Gloria": ["Pediatrics"], "Jemma": ["Cardiac_Care"], - "Joyce": ["Anaesthesiology", "Pediatrics"], - "Julie": ["Geriatrics"], "Juliet": ["Pediatrics"], - "Kate": ["Pediatrics"], "Nancy": ["Cardiac_Care"], - "Nathalie": ["Anaesthesiology", "Geriatrics"], - "Patrick": ["Oncology"], "Suzanne": ["Pediatrics"], - "Wendie": ["Geriatrics"], - "Zoe": ["Cardiac_Care"] - } - -VACATIONS = [("Anne", "friday"), - ("Anne", "sunday"), - ("Cathy", "thursday"), - ("Cathy", "tuesday"), - ("Joan", "thursday"), - ("Joan", "saturday"), - ("Juliet", "monday"), - ("Juliet", "tuesday"), - ("Juliet", "thursday"), - ("Nathalie", "sunday"), - ("Nathalie", "thursday"), - ("Isabelle", "monday"), - ("Isabelle", "thursday"), - ("Patricia", "saturday"), - ("Patricia", "wednesday"), - ("Nicole", "friday"), - ("Nicole", "wednesday"), - ("Jude", "tuesday"), - ("Jude", "friday"), - ("Debbie", "saturday"), - ("Debbie", "wednesday"), - ("Joyce", "sunday"), - ("Joyce", "thursday"), - ("Chris", "thursday"), - ("Chris", "tuesday"), - ("Cecilia", "friday"), - ("Cecilia", "wednesday"), - ("Patrick", "saturday"), - ("Patrick", "sunday"), - ("Cindy", "sunday"), - ("Dee", "tuesday"), - ("Dee", "friday"), - ("Jemma", "friday"), - ("Jemma", "wednesday"), - ("Bethanie", "wednesday"), - ("Bethanie", "tuesday"), - ("Betsy", "monday"), - ("Betsy", "thursday"), - ("David", "monday"), - ("Gloria", "monday"), - ("Jane", "saturday"), - ("Jane", "sunday"), - ("Janelle", "wednesday"), - ("Janelle", "friday"), - ("Julie", "sunday"), - ("Kate", "tuesday"), - ("Kate", "monday"), - ("Nancy", "sunday"), - ("Roberta", "friday"), - ("Roberta", "saturday"), - ("Janice", "tuesday"), - ("Janice", "friday"), - ("Suzanne", "monday"), - ("Vickie", "wednesday"), - ("Vickie", "friday"), - ("Wendie", "thursday"), - ("Wendie", "saturday"), - ("Zoe", "saturday"), - ("Zoe", "sunday")] - -NURSE_ASSOCIATIONS = [("Isabelle", "Dee"), - ("Anne", "Patrick")] - -NURSE_INCOMPATIBILITIES = [("Patricia", "Patrick"), - ("Janice", "Wendie"), - ("Suzanne", "Betsy"), - ("Janelle", "Jane"), - ("Gloria", "David"), - ("Dee", "Jemma"), - ("Bethanie", "Dee"), - ("Roberta", "Zoe"), - ("Nicole", "Patricia"), - ("Vickie", "Dee"), - ("Joan", "Anne") - ] - -SKILL_REQUIREMENTS = [("Emergency", "Cardiac_Care", 1)] - -DEFAULT_WORK_RULES = TWorkRules(40) - - -# ---------------------------------------------------------------------------- -# Prepare the data for modeling -# ---------------------------------------------------------------------------- -# subclass the namedtuple to refine the str() method as the nurse's name -class TNurse(namedtuple("TNurse1", ["name", "seniority", "qualification", "pay_rate"])): - def __str__(self): - return self.name - - -# specialized namedtuple to redefine its str() method -class TShift(namedtuple("TShift", - ["department", "day", "start_time", "end_time", "min_requirement", "max_requirement"])): - - def __str__(self): - # keep first two characters in department, uppercase - dept2 = self.department[0:4].upper() - # keep 3 days of weekday - dayname = self.day[0:3] - return '{}_{}_{:02d}'.format(dept2, dayname, self.start_time).replace(" ", "_") - - -class ShiftActivity(object): - @staticmethod - def to_abstime(day_index, time_of_day): - """ Convert a pair (day_index, time) into a number of hours since Monday 00:00 - - :param day_index: The index of the day from 1 to 7 (Monday is 1). - :param time_of_day: An integer number of hours. - - :return: - """ - time = 24 * (day_index - 1) - time += time_of_day - return time - - def __init__(self, weekday, start_time_of_day, end_time_of_day): - assert (start_time_of_day >= 0) - assert (start_time_of_day <= 24) - assert (end_time_of_day >= 0) - assert (end_time_of_day <= 24) - - self._weekday = weekday - self._start_time_of_day = start_time_of_day - self._end_time_of_day = end_time_of_day - # conversion to absolute time. - start_day_index = day_to_day_week(self._weekday) - self.start_time = self.to_abstime(start_day_index, start_time_of_day) - end_day_index = start_day_index if end_time_of_day > start_time_of_day else start_day_index + 1 - self.end_time = self.to_abstime(end_day_index, end_time_of_day) - assert self.end_time > self.start_time - - @property - def duration(self): - return self.end_time - self.start_time - - def overlaps(self, other_shift): - if not isinstance(other_shift, ShiftActivity): - return False - else: - return other_shift.end_time > self.start_time and other_shift.start_time < self.end_time - - -def solve(model, **kwargs): - # Here, we set the number of threads for CPLEX to 2 and set the time limit to 2mins. - if kwargs.pop('parameter_sets', None) == None: - model.parameters.threads = 2 - model.parameters.mip.tolerances.mipgap = 0.000001 - model.parameters.timelimit = 120 # nurse should not take more than that ! - sol = model.solve(log_output=True, **kwargs) - if sol is not None: - print("solution for a cost of {}".format(model.objective_value)) - print_information(model) - # print_solution(model) - return model.objective_value - else: - print("* model is infeasible") - return None - - -def load_data(model, shifts_, nurses_, nurse_skills, vacations_=None, - nurse_associations_=None, nurse_imcompatibilities_=None): - """ Usage: load_data(shifts, nurses, nurse_skills, vacations) """ - model.number_of_overlaps = 0 - model.work_rules = DEFAULT_WORK_RULES - model.shifts = [TShift(*shift_row) for shift_row in shifts_] - model.nurses = [TNurse(*nurse_row) for nurse_row in nurses_] - model.skill_requirements = SKILL_REQUIREMENTS - model.nurse_skills = nurse_skills - # transactional data - model.vacations = [TVacation(*vacation_row) for vacation_row in vacations_] if vacations_ else [] - model.nurse_associations = [TNursePair(*npr) for npr in nurse_associations_]\ - if nurse_associations_ else [] - model.nurse_incompatibilities = [TNursePair(*npr) for npr in nurse_imcompatibilities_]\ - if nurse_imcompatibilities_ else [] - - # computed - model.departments = set(sh.department for sh in model.shifts) - - - print('#nurses: {0}'.format(len(model.nurses))) - print('#shifts: {0}'.format(len(model.shifts))) - print('#vacations: {0}'.format(len(model.vacations))) - print("#associations=%d" % len(model.nurse_associations)) - print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) - - -def setup_data(model): - """ compute internal data """ - # compute shift activities (start, end duration) and stor ethem in a dict indexed by shifts - model.shift_activities = {s: ShiftActivity(s.day, s.start_time, s.end_time) for s in model.shifts} - # map from nurse names to nurse tuples. - model.nurses_by_id = {n.name: n for n in model.nurses} - - -def setup_variables(model): - all_nurses, all_shifts = model.nurses, model.shifts - # one binary variable for each pair (nurse, shift) equal to 1 iff nurse n is assigned to shift s - model.nurse_assignment_vars = model.binary_var_matrix(all_nurses, all_shifts, 'NurseAssigned') - # for each nurse, allocate one variable for work time - model.nurse_work_time_vars = model.continuous_var_dict(all_nurses, lb=0, name='NurseWorkTime') - # and two variables for over_average and under-average work time - model.nurse_over_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, - name='NurseOverAverageWorkTime') - model.nurse_under_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, - name='NurseUnderAverageWorkTime') - # finally the global average work time - model.average_nurse_work_time = model.continuous_var(lb=0, name='AverageWorkTime') - - -def setup_constraints(model): - all_nurses = model.nurses - all_shifts = model.shifts - nurse_assigned = model.nurse_assignment_vars - nurse_work_time = model.nurse_work_time_vars - shift_activities = model.shift_activities - nurses_by_id = model.nurses_by_id - max_work_time = model.work_rules.work_time_max - - # define average - model.add_constraint( - len(all_nurses) * model.average_nurse_work_time == model.sum(nurse_work_time[n] for n in all_nurses), "average") - - # compute nurse work time , average and under, over - for n in all_nurses: - work_time_var = nurse_work_time[n] - model.add_constraint( - work_time_var == model.sum(nurse_assigned[n, s] * shift_activities[s].duration for s in all_shifts), - "work_time_{0!s}".format(n)) - - # relate over/under average worktime variables to the worktime variables - # the trick here is that variables have zero lower bound - # however, thse variables are not completely defined by this constraint, - # only their difference is. - # if these variables are part of the objective, CPLEX wil naturally minimize their value, - # as expected - model.add_constraint( - work_time_var == model.average_nurse_work_time - + model.nurse_over_average_time_vars[n] - - model.nurse_under_average_time_vars[n], - "average_work_time_{0!s}".format(n)) - - # state the maximum work time as a constraint, so that is can be relaxed, - # should the problem become infeasible. - model.add_constraint(work_time_var <= max_work_time, "max_time_{0!s}".format(n)) - - # vacations - v = 0 - for vac_nurse_id, vac_day in model.vacations: - vac_n = nurses_by_id[vac_nurse_id] - for shift in (s for s in all_shifts if s.day == vac_day): - v += 1 - model.add_constraint(nurse_assigned[vac_n, shift] == 0, - "medium_vacations_{0!s}_{1!s}_{2!s}".format(vac_n, vac_day, shift)) - #print('#vacation cts: {0}'.format(v)) - - # a nurse cannot be assigned overlapping shifts - # post only one constraint per couple(s1, s2) - number_of_overlaps = 0 - nb_shifts = len(all_shifts) - for i1 in range(nb_shifts): - for i2 in range(i1 + 1, nb_shifts): - s1 = all_shifts[i1] - s2 = all_shifts[i2] - if shift_activities[s1].overlaps(shift_activities[s2]): - number_of_overlaps += 1 - for n in all_nurses: - model.add_constraint(nurse_assigned[n, s1] + nurse_assigned[n, s2] <= 1, - "high_overlapping_{0!s}_{1!s}_{2!s}".format(s1, s2, n)) - #print('# overlapping cts: {0}'.format(number_of_overlaps)) - - for s in all_shifts: - demand_min = s.min_requirement - demand_max = s.max_requirement - total_assigned = model.sum(nurse_assigned[n, s] for n in model.nurses) - model.add_constraint(total_assigned >= demand_min, - "high_req_min_{0!s}_{1}".format(s, demand_min)) - model.add_constraint(total_assigned <= demand_max, - "medium_req_max_{0!s}_{1}".format(s, demand_max)) - - for (dept, skill, required) in model.skill_requirements: - if required > 0: - for dsh in (s for s in all_shifts if dept == s.department): - model.add_constraint(model.sum(nurse_assigned[skilled_nurse, dsh] for skilled_nurse in - (n for n in all_nurses if - n.name in model.nurse_skills.keys() and skill in model.nurse_skills[ - n.name])) >= required, - "high_required_{0!s}_{1!s}_{2!s}_{3!s}".format(dept, skill, required, dsh)) - - # nurse-nurse associations - # for each pair of associated nurses, their assignment variables are equal - # over all shifts. - c = 0 - for (nurse_id1, nurse_id2) in model.nurse_associations: - if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: - nurse1 = nurses_by_id[nurse_id1] - nurse2 = nurses_by_id[nurse_id2] - for s in all_shifts: - c += 1 - ctname = 'medium_ct_nurse_assoc_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) - model.add_constraint(nurse_assigned[nurse1, s] == nurse_assigned[nurse2, s], ctname) - - # nurse-nurse incompatibilities - # for each pair of incompatible nurses, the sum of assigned variables is less than one - # in other terms, both nurses can never be assigned to the same shift - c = 0 - for (nurse_id1, nurse_id2) in model.nurse_incompatibilities: - if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: - nurse1 = nurses_by_id[nurse_id1] - nurse2 = nurses_by_id[nurse_id2] - for s in all_shifts: - c += 1 - ctname = 'medium_ct_nurse_incompat_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) - model.add_constraint(nurse_assigned[nurse1, s] + nurse_assigned[nurse2, s] <= 1, ctname) - - model.total_number_of_assignments = model.sum(nurse_assigned[n, s] for n in all_nurses for s in all_shifts) - model.nurse_costs = [model.nurse_assignment_vars[n, s] * n.pay_rate * model.shift_activities[s].duration for n in - model.nurses - for s in model.shifts] - model.total_salary_cost = model.sum(model.nurse_costs) - - -def setup_objective(model): - model.add_kpi(model.total_salary_cost, "Total salary cost") - model.add_kpi(model.total_number_of_assignments, "Total number of assignments") - model.add_kpi(model.average_nurse_work_time, "average work time") - - total_over_average_worktime = model.sum(model.nurse_over_average_time_vars[n] for n in model.nurses) - total_under_average_worktime = model.sum(model.nurse_under_average_time_vars[n] for n in model.nurses) - model.add_kpi(total_over_average_worktime, "Total over-average worktime") - model.add_kpi(total_under_average_worktime, "Total under-average worktime") - total_fairness = total_over_average_worktime + total_under_average_worktime - model.add_kpi(total_fairness, "Total fairness") - - model.minimize_static_lex([model.total_salary_cost, total_fairness, model.total_number_of_assignments]) - - -def print_information(model): - print("#shifts=%d" % len(model.shifts)) - print("#nurses=%d" % len(model.nurses)) - print("#vacations=%d" % len(model.vacations)) - print("#nurse skills=%d" % len(model.nurse_skills)) - print("#nurse associations=%d" % len(model.nurse_associations)) - print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) - model.print_information() - model.report_kpis() - - -def print_solution(model): - print("*************************** Solution ***************************") - print("Allocation By Department:") - for d in model.departments: - print("\t{}: {}".format(d, sum( - model.nurse_assignment_vars[n, s].solution_value for n in model.nurses for s in model.shifts if - s.department == d))) - print("Cost By Department:") - for d in model.departments: - cost = sum( - model.nurse_assignment_vars[n, s].solution_value * n.pay_rate * model.shift_activities[s].duration for n in - model.nurses for s in model.shifts if s.department == d) - print("\t{}: {}".format(d, cost)) - print("Nurses Assignments") - for n in sorted(model.nurses): - total_hours = sum( - model.nurse_assignment_vars[n, s].solution_value * model.shift_activities[s].duration for s in model.shifts) - print("\t{}: total hours:{}".format(n.name, total_hours)) - for s in model.shifts: - if model.nurse_assignment_vars[n, s].solution_value == 1: - print("\t\t{}: {} {}-{}".format(s.day, s.department, s.start_time, s.end_time)) - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- - -def build(context=None, **kwargs): - mdl = Model("Nurses", context=context, **kwargs) - load_data(mdl, SHIFTS, NURSES, NURSE_SKILLS, VACATIONS, NURSE_ASSOCIATIONS, - NURSE_INCOMPATIBILITIES) - setup_data(mdl) - setup_variables(mdl) - setup_constraints(mdl) - setup_objective(mdl) - return mdl - - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- - -if __name__ == '__main__': - # Build model - model = build() - - # Solve the model and print solution - solve(model) - - print(model.solve_details) - - # Save the CPLEX solution as "solution.json" program output - with get_environment().get_output_stream("solution.json") as fp: - model.solution.export(fp, "json") - - model = build() - paramsets = model.build_multiobj_paramsets(timelimits=[70,60,50] , mipgaps=[0.000003, 0.000002, 0.000001]) - solve(model, clean_before_solve=True, parameter_sets=paramsets) - print(model.solve_details) - - model = build() - paramsets = model.create_parameter_sets() - cplex = model.get_cplex() - for i,p in enumerate(paramsets): - p.add(cplex.parameters.timelimit, 70+i) - p.add(cplex.parameters.mip.tolerances.mipgap, 0.000001*i) - p.add(cplex.parameters.threads, 2+i) - solve(model, clean_before_solve=True, parameter_sets=paramsets) - print(model.solve_details) +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + +from collections import namedtuple + +from docplex.mp.model import Model +from docplex.mp.constants import ObjectiveSense +from docplex.util.environment import get_environment + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- + +# utility to convert a weekday string to an index in 0..6 +_all_days = ["monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday"] + + +def day_to_day_week(day): + day_map = {day: d for d, day in enumerate(_all_days)} + return day_map[day.lower()] + + +TWorkRules = namedtuple("TWorkRules", ["work_time_max"]) +TVacation = namedtuple("TVacation", ["nurse", "day"]) +TNursePair = namedtuple("TNursePair", ["firstNurse", "secondNurse"]) +TSkillRequirement = namedtuple("TSkillRequirement", ["department", "skill", "required"]) + + +NURSES = [("Anne", 11, 1, 25), + ("Bethanie", 4, 5, 28), + ("Betsy", 2, 2, 17), + ("Cathy", 2, 2, 17), + ("Cecilia", 9, 5, 38), + ("Chris", 11, 4, 38), + ("Cindy", 5, 2, 21), + ("David", 1, 2, 15), + ("Debbie", 7, 2, 24), + ("Dee", 3, 3, 21), + ("Gloria", 8, 2, 25), + ("Isabelle", 3, 1, 16), + ("Jane", 3, 4, 23), + ("Janelle", 4, 3, 22), + ("Janice", 2, 2, 17), + ("Jemma", 2, 4, 22), + ("Joan", 5, 3, 24), + ("Joyce", 8, 3, 29), + ("Jude", 4, 3, 22), + ("Julie", 6, 2, 22), + ("Juliet", 7, 4, 31), + ("Kate", 5, 3, 24), + ("Nancy", 8, 4, 32), + ("Nathalie", 9, 5, 38), + ("Nicole", 0, 2, 14), + ("Patricia", 1, 1, 13), + ("Patrick", 6, 1, 19), + ("Roberta", 3, 5, 26), + ("Suzanne", 5, 1, 18), + ("Vickie", 7, 1, 20), + ("Wendie", 5, 2, 21), + ("Zoe", 8, 3, 29) + ] + +SHIFTS = [("Emergency", "monday", 2, 8, 3, 5), + ("Emergency", "monday", 8, 12, 4, 7), + ("Emergency", "monday", 12, 18, 2, 5), + ("Emergency", "monday", 18, 2, 3, 7), + ("Consultation", "monday", 8, 12, 10, 13), + ("Consultation", "monday", 12, 18, 8, 12), + ("Cardiac_Care", "monday", 8, 12, 10, 13), + ("Cardiac_Care", "monday", 12, 18, 8, 12), + ("Emergency", "tuesday", 8, 12, 4, 7), + ("Emergency", "tuesday", 12, 18, 2, 5), + ("Emergency", "tuesday", 18, 2, 3, 7), + ("Consultation", "tuesday", 8, 12, 10, 13), + ("Consultation", "tuesday", 12, 18, 8, 12), + ("Cardiac_Care", "tuesday", 8, 12, 4, 7), + ("Cardiac_Care", "tuesday", 12, 18, 2, 5), + ("Cardiac_Care", "tuesday", 18, 2, 3, 7), + ("Emergency", "wednesday", 2, 8, 3, 5), + ("Emergency", "wednesday", 8, 12, 4, 7), + ("Emergency", "wednesday", 12, 18, 2, 5), + ("Emergency", "wednesday", 18, 2, 3, 7), + ("Consultation", "wednesday", 8, 12, 10, 13), + ("Consultation", "wednesday", 12, 18, 8, 12), + ("Emergency", "thursday", 2, 8, 3, 5), + ("Emergency", "thursday", 8, 12, 4, 7), + ("Emergency", "thursday", 12, 18, 2, 5), + ("Emergency", "thursday", 18, 2, 3, 7), + ("Consultation", "thursday", 8, 12, 10, 13), + ("Consultation", "thursday", 12, 18, 8, 12), + ("Emergency", "friday", 2, 8, 3, 5), + ("Emergency", "friday", 8, 12, 4, 7), + ("Emergency", "friday", 12, 18, 2, 5), + ("Emergency", "friday", 18, 2, 3, 7), + ("Consultation", "friday", 8, 12, 10, 13), + ("Consultation", "friday", 12, 18, 8, 12), + ("Emergency", "saturday", 2, 12, 5, 7), + ("Emergency", "saturday", 12, 20, 7, 9), + ("Emergency", "saturday", 20, 2, 12, 12), + ("Emergency", "sunday", 2, 12, 5, 7), + ("Emergency", "sunday", 12, 20, 7, 9), + ("Emergency", "sunday", 20, 2, 12, 12), + ("Geriatrics", "sunday", 8, 10, 2, 5)] + +NURSE_SKILLS = {"Anne": ["Anaesthesiology", "Oncology", "Pediatrics"], + "Betsy": ["Cardiac_Care"], + "Cathy": ["Anaesthesiology"], + "Cecilia": ["Anaesthesiology", "Oncology", "Pediatrics"], + "Chris": ["Cardiac_Care", "Oncology", "Geriatrics"], + "Gloria": ["Pediatrics"], "Jemma": ["Cardiac_Care"], + "Joyce": ["Anaesthesiology", "Pediatrics"], + "Julie": ["Geriatrics"], "Juliet": ["Pediatrics"], + "Kate": ["Pediatrics"], "Nancy": ["Cardiac_Care"], + "Nathalie": ["Anaesthesiology", "Geriatrics"], + "Patrick": ["Oncology"], "Suzanne": ["Pediatrics"], + "Wendie": ["Geriatrics"], + "Zoe": ["Cardiac_Care"] + } + +VACATIONS = [("Anne", "friday"), + ("Anne", "sunday"), + ("Cathy", "thursday"), + ("Cathy", "tuesday"), + ("Joan", "thursday"), + ("Joan", "saturday"), + ("Juliet", "monday"), + ("Juliet", "tuesday"), + ("Juliet", "thursday"), + ("Nathalie", "sunday"), + ("Nathalie", "thursday"), + ("Isabelle", "monday"), + ("Isabelle", "thursday"), + ("Patricia", "saturday"), + ("Patricia", "wednesday"), + ("Nicole", "friday"), + ("Nicole", "wednesday"), + ("Jude", "tuesday"), + ("Jude", "friday"), + ("Debbie", "saturday"), + ("Debbie", "wednesday"), + ("Joyce", "sunday"), + ("Joyce", "thursday"), + ("Chris", "thursday"), + ("Chris", "tuesday"), + ("Cecilia", "friday"), + ("Cecilia", "wednesday"), + ("Patrick", "saturday"), + ("Patrick", "sunday"), + ("Cindy", "sunday"), + ("Dee", "tuesday"), + ("Dee", "friday"), + ("Jemma", "friday"), + ("Jemma", "wednesday"), + ("Bethanie", "wednesday"), + ("Bethanie", "tuesday"), + ("Betsy", "monday"), + ("Betsy", "thursday"), + ("David", "monday"), + ("Gloria", "monday"), + ("Jane", "saturday"), + ("Jane", "sunday"), + ("Janelle", "wednesday"), + ("Janelle", "friday"), + ("Julie", "sunday"), + ("Kate", "tuesday"), + ("Kate", "monday"), + ("Nancy", "sunday"), + ("Roberta", "friday"), + ("Roberta", "saturday"), + ("Janice", "tuesday"), + ("Janice", "friday"), + ("Suzanne", "monday"), + ("Vickie", "wednesday"), + ("Vickie", "friday"), + ("Wendie", "thursday"), + ("Wendie", "saturday"), + ("Zoe", "saturday"), + ("Zoe", "sunday")] + +NURSE_ASSOCIATIONS = [("Isabelle", "Dee"), + ("Anne", "Patrick")] + +NURSE_INCOMPATIBILITIES = [("Patricia", "Patrick"), + ("Janice", "Wendie"), + ("Suzanne", "Betsy"), + ("Janelle", "Jane"), + ("Gloria", "David"), + ("Dee", "Jemma"), + ("Bethanie", "Dee"), + ("Roberta", "Zoe"), + ("Nicole", "Patricia"), + ("Vickie", "Dee"), + ("Joan", "Anne") + ] + +SKILL_REQUIREMENTS = [("Emergency", "Cardiac_Care", 1)] + +DEFAULT_WORK_RULES = TWorkRules(40) + + +# ---------------------------------------------------------------------------- +# Prepare the data for modeling +# ---------------------------------------------------------------------------- +# subclass the namedtuple to refine the str() method as the nurse's name +class TNurse(namedtuple("TNurse1", ["name", "seniority", "qualification", "pay_rate"])): + def __str__(self): + return self.name + + +# specialized namedtuple to redefine its str() method +class TShift(namedtuple("TShift", + ["department", "day", "start_time", "end_time", "min_requirement", "max_requirement"])): + + def __str__(self): + # keep first two characters in department, uppercase + dept2 = self.department[0:4].upper() + # keep 3 days of weekday + dayname = self.day[0:3] + return '{}_{}_{:02d}'.format(dept2, dayname, self.start_time).replace(" ", "_") + + +class ShiftActivity(object): + @staticmethod + def to_abstime(day_index, time_of_day): + """ Convert a pair (day_index, time) into a number of hours since Monday 00:00 + + :param day_index: The index of the day from 1 to 7 (Monday is 1). + :param time_of_day: An integer number of hours. + + :return: + """ + time = 24 * (day_index - 1) + time += time_of_day + return time + + def __init__(self, weekday, start_time_of_day, end_time_of_day): + assert (start_time_of_day >= 0) + assert (start_time_of_day <= 24) + assert (end_time_of_day >= 0) + assert (end_time_of_day <= 24) + + self._weekday = weekday + self._start_time_of_day = start_time_of_day + self._end_time_of_day = end_time_of_day + # conversion to absolute time. + start_day_index = day_to_day_week(self._weekday) + self.start_time = self.to_abstime(start_day_index, start_time_of_day) + end_day_index = start_day_index if end_time_of_day > start_time_of_day else start_day_index + 1 + self.end_time = self.to_abstime(end_day_index, end_time_of_day) + assert self.end_time > self.start_time + + @property + def duration(self): + return self.end_time - self.start_time + + def overlaps(self, other_shift): + if not isinstance(other_shift, ShiftActivity): + return False + else: + return other_shift.end_time > self.start_time and other_shift.start_time < self.end_time + + +def solve(model, **kwargs): + # Here, we set the number of threads for CPLEX to 2 and set the time limit to 2mins. + if kwargs.pop('parameter_sets', None) == None: + model.parameters.threads = 2 + model.parameters.mip.tolerances.mipgap = 0.000001 + model.parameters.timelimit = 120 # nurse should not take more than that ! + sol = model.solve(log_output=True, **kwargs) + if sol is not None: + print("solution for a cost of {}".format(model.objective_value)) + print_information(model) + # print_solution(model) + return model.objective_value + else: + print("* model is infeasible") + return None + + +def load_data(model, shifts_, nurses_, nurse_skills, vacations_=None, + nurse_associations_=None, nurse_imcompatibilities_=None): + """ Usage: load_data(shifts, nurses, nurse_skills, vacations) """ + model.number_of_overlaps = 0 + model.work_rules = DEFAULT_WORK_RULES + model.shifts = [TShift(*shift_row) for shift_row in shifts_] + model.nurses = [TNurse(*nurse_row) for nurse_row in nurses_] + model.skill_requirements = SKILL_REQUIREMENTS + model.nurse_skills = nurse_skills + # transactional data + model.vacations = [TVacation(*vacation_row) for vacation_row in vacations_] if vacations_ else [] + model.nurse_associations = [TNursePair(*npr) for npr in nurse_associations_]\ + if nurse_associations_ else [] + model.nurse_incompatibilities = [TNursePair(*npr) for npr in nurse_imcompatibilities_]\ + if nurse_imcompatibilities_ else [] + + # computed + model.departments = set(sh.department for sh in model.shifts) + + + print('#nurses: {0}'.format(len(model.nurses))) + print('#shifts: {0}'.format(len(model.shifts))) + print('#vacations: {0}'.format(len(model.vacations))) + print("#associations=%d" % len(model.nurse_associations)) + print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) + + +def setup_data(model): + """ compute internal data """ + # compute shift activities (start, end duration) and stor ethem in a dict indexed by shifts + model.shift_activities = {s: ShiftActivity(s.day, s.start_time, s.end_time) for s in model.shifts} + # map from nurse names to nurse tuples. + model.nurses_by_id = {n.name: n for n in model.nurses} + + +def setup_variables(model): + all_nurses, all_shifts = model.nurses, model.shifts + # one binary variable for each pair (nurse, shift) equal to 1 iff nurse n is assigned to shift s + model.nurse_assignment_vars = model.binary_var_matrix(all_nurses, all_shifts, 'NurseAssigned') + # for each nurse, allocate one variable for work time + model.nurse_work_time_vars = model.continuous_var_dict(all_nurses, lb=0, name='NurseWorkTime') + # and two variables for over_average and under-average work time + model.nurse_over_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, + name='NurseOverAverageWorkTime') + model.nurse_under_average_time_vars = model.continuous_var_dict(all_nurses, lb=0, + name='NurseUnderAverageWorkTime') + # finally the global average work time + model.average_nurse_work_time = model.continuous_var(lb=0, name='AverageWorkTime') + + +def setup_constraints(model): + all_nurses = model.nurses + all_shifts = model.shifts + nurse_assigned = model.nurse_assignment_vars + nurse_work_time = model.nurse_work_time_vars + shift_activities = model.shift_activities + nurses_by_id = model.nurses_by_id + max_work_time = model.work_rules.work_time_max + + # define average + model.add_constraint( + len(all_nurses) * model.average_nurse_work_time == model.sum(nurse_work_time[n] for n in all_nurses), "average") + + # compute nurse work time , average and under, over + for n in all_nurses: + work_time_var = nurse_work_time[n] + model.add_constraint( + work_time_var == model.sum(nurse_assigned[n, s] * shift_activities[s].duration for s in all_shifts), + "work_time_{0!s}".format(n)) + + # relate over/under average worktime variables to the worktime variables + # the trick here is that variables have zero lower bound + # however, thse variables are not completely defined by this constraint, + # only their difference is. + # if these variables are part of the objective, CPLEX wil naturally minimize their value, + # as expected + model.add_constraint( + work_time_var == model.average_nurse_work_time + + model.nurse_over_average_time_vars[n] + - model.nurse_under_average_time_vars[n], + "average_work_time_{0!s}".format(n)) + + # state the maximum work time as a constraint, so that is can be relaxed, + # should the problem become infeasible. + model.add_constraint(work_time_var <= max_work_time, "max_time_{0!s}".format(n)) + + # vacations + v = 0 + for vac_nurse_id, vac_day in model.vacations: + vac_n = nurses_by_id[vac_nurse_id] + for shift in (s for s in all_shifts if s.day == vac_day): + v += 1 + model.add_constraint(nurse_assigned[vac_n, shift] == 0, + "medium_vacations_{0!s}_{1!s}_{2!s}".format(vac_n, vac_day, shift)) + #print('#vacation cts: {0}'.format(v)) + + # a nurse cannot be assigned overlapping shifts + # post only one constraint per couple(s1, s2) + number_of_overlaps = 0 + nb_shifts = len(all_shifts) + for i1 in range(nb_shifts): + for i2 in range(i1 + 1, nb_shifts): + s1 = all_shifts[i1] + s2 = all_shifts[i2] + if shift_activities[s1].overlaps(shift_activities[s2]): + number_of_overlaps += 1 + for n in all_nurses: + model.add_constraint(nurse_assigned[n, s1] + nurse_assigned[n, s2] <= 1, + "high_overlapping_{0!s}_{1!s}_{2!s}".format(s1, s2, n)) + #print('# overlapping cts: {0}'.format(number_of_overlaps)) + + for s in all_shifts: + demand_min = s.min_requirement + demand_max = s.max_requirement + total_assigned = model.sum(nurse_assigned[n, s] for n in model.nurses) + model.add_constraint(total_assigned >= demand_min, + "high_req_min_{0!s}_{1}".format(s, demand_min)) + model.add_constraint(total_assigned <= demand_max, + "medium_req_max_{0!s}_{1}".format(s, demand_max)) + + for (dept, skill, required) in model.skill_requirements: + if required > 0: + for dsh in (s for s in all_shifts if dept == s.department): + model.add_constraint(model.sum(nurse_assigned[skilled_nurse, dsh] for skilled_nurse in + (n for n in all_nurses if + n.name in model.nurse_skills.keys() and skill in model.nurse_skills[ + n.name])) >= required, + "high_required_{0!s}_{1!s}_{2!s}_{3!s}".format(dept, skill, required, dsh)) + + # nurse-nurse associations + # for each pair of associated nurses, their assignment variables are equal + # over all shifts. + c = 0 + for (nurse_id1, nurse_id2) in model.nurse_associations: + if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: + nurse1 = nurses_by_id[nurse_id1] + nurse2 = nurses_by_id[nurse_id2] + for s in all_shifts: + c += 1 + ctname = 'medium_ct_nurse_assoc_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) + model.add_constraint(nurse_assigned[nurse1, s] == nurse_assigned[nurse2, s], ctname) + + # nurse-nurse incompatibilities + # for each pair of incompatible nurses, the sum of assigned variables is less than one + # in other terms, both nurses can never be assigned to the same shift + c = 0 + for (nurse_id1, nurse_id2) in model.nurse_incompatibilities: + if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id: + nurse1 = nurses_by_id[nurse_id1] + nurse2 = nurses_by_id[nurse_id2] + for s in all_shifts: + c += 1 + ctname = 'medium_ct_nurse_incompat_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c) + model.add_constraint(nurse_assigned[nurse1, s] + nurse_assigned[nurse2, s] <= 1, ctname) + + model.total_number_of_assignments = model.sum(nurse_assigned[n, s] for n in all_nurses for s in all_shifts) + model.nurse_costs = [model.nurse_assignment_vars[n, s] * n.pay_rate * model.shift_activities[s].duration for n in + model.nurses + for s in model.shifts] + model.total_salary_cost = model.sum(model.nurse_costs) + + +def setup_objective(model): + model.add_kpi(model.total_salary_cost, "Total salary cost") + model.add_kpi(model.total_number_of_assignments, "Total number of assignments") + model.add_kpi(model.average_nurse_work_time, "average work time") + + total_over_average_worktime = model.sum(model.nurse_over_average_time_vars[n] for n in model.nurses) + total_under_average_worktime = model.sum(model.nurse_under_average_time_vars[n] for n in model.nurses) + model.add_kpi(total_over_average_worktime, "Total over-average worktime") + model.add_kpi(total_under_average_worktime, "Total under-average worktime") + total_fairness = total_over_average_worktime + total_under_average_worktime + model.add_kpi(total_fairness, "Total fairness") + + model.minimize_static_lex([model.total_salary_cost, total_fairness, model.total_number_of_assignments]) + + +def print_information(model): + print("#shifts=%d" % len(model.shifts)) + print("#nurses=%d" % len(model.nurses)) + print("#vacations=%d" % len(model.vacations)) + print("#nurse skills=%d" % len(model.nurse_skills)) + print("#nurse associations=%d" % len(model.nurse_associations)) + print("#incompatibilities=%d" % len(model.nurse_incompatibilities)) + model.print_information() + model.report_kpis() + + +def print_solution(model): + print("*************************** Solution ***************************") + print("Allocation By Department:") + for d in model.departments: + print("\t{}: {}".format(d, sum( + model.nurse_assignment_vars[n, s].solution_value for n in model.nurses for s in model.shifts if + s.department == d))) + print("Cost By Department:") + for d in model.departments: + cost = sum( + model.nurse_assignment_vars[n, s].solution_value * n.pay_rate * model.shift_activities[s].duration for n in + model.nurses for s in model.shifts if s.department == d) + print("\t{}: {}".format(d, cost)) + print("Nurses Assignments") + for n in sorted(model.nurses): + total_hours = sum( + model.nurse_assignment_vars[n, s].solution_value * model.shift_activities[s].duration for s in model.shifts) + print("\t{}: total hours:{}".format(n.name, total_hours)) + for s in model.shifts: + if model.nurse_assignment_vars[n, s].solution_value == 1: + print("\t\t{}: {} {}-{}".format(s.day, s.department, s.start_time, s.end_time)) + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- + +def build(context=None, **kwargs): + mdl = Model("Nurses", context=context, **kwargs) + load_data(mdl, SHIFTS, NURSES, NURSE_SKILLS, VACATIONS, NURSE_ASSOCIATIONS, + NURSE_INCOMPATIBILITIES) + setup_data(mdl) + setup_variables(mdl) + setup_constraints(mdl) + setup_objective(mdl) + return mdl + + +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- + +if __name__ == '__main__': + # Build model + model = build() + + # Solve the model and print solution + solve(model) + + print(model.solve_details) + + # Save the CPLEX solution as "solution.json" program output + with get_environment().get_output_stream("solution.json") as fp: + model.solution.export(fp, "json") + + model.end() + + model = build() + paramsets = model.build_multiobj_paramsets(timelimits=[70,60,50] , mipgaps=[0.000003, 0.000002, 0.000001]) + solve(model, clean_before_solve=True, parameter_sets=paramsets) + print(model.solve_details) + + model = build() + paramsets = model.create_parameter_sets() + cplex = model.get_cplex() + for i,p in enumerate(paramsets): + p.add(cplex.parameters.timelimit, 70+i) + p.add(cplex.parameters.mip.tolerances.mipgap, 0.000001*i) + p.add(cplex.parameters.threads, 2+i) + solve(model, clean_before_solve=True, parameter_sets=paramsets) + print(model.solve_details) + model.end() \ No newline at end of file diff --git a/examples/mp/modeling/production.py b/examples/mp/modeling/production.py index 6a8b3ea..dbc5230 100644 --- a/examples/mp/modeling/production.py +++ b/examples/mp/modeling/production.py @@ -1,108 +1,107 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - -"""The model aims at minimizing the production cost for a number of products -while satisfying customer demand. Each product can be produced either inside -the company or outside, at a higher cost. - -The inside production is constrained by the company's resources, while outside -production is considered unlimited. - -The model first declares the products and the resources. -The data consists of the description of the products (the demand, the inside -and outside costs, and the resource consumption) and the capacity of the -various resources. - -The variables for this problem are the inside and outside production for each -product. -""" - -from docplex.mp.model import Model -from docplex.util.environment import get_environment - - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- -PRODUCTS = [("kluski", 100, 0.6, 0.8), - ("capellini", 200, 0.8, 0.9), - ("fettucine", 300, 0.3, 0.4)] - -# resources are a list of simple tuples (name, capacity) -RESOURCES = [("flour", 20), - ("eggs", 40)] - -CONSUMPTIONS = {("kluski", "flour"): 0.5, - ("kluski", "eggs"): 0.2, - ("capellini", "flour"): 0.4, - ("capellini", "eggs"): 0.4, - ("fettucine", "flour"): 0.3, - ("fettucine", "eggs"): 0.6} - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- -def build_production_problem(products, resources, consumptions, **kwargs): - """ Takes as input: - - a list of product tuples (name, demand, inside, outside) - - a list of resource tuples (name, capacity) - - a list of consumption tuples (product_name, resource_named, consumed) - """ - mdl = Model(name='production', **kwargs) - # --- decision variables --- - mdl.inside_vars = mdl.continuous_var_dict(products, name=lambda p: 'inside_%s' % p[0]) - mdl.outside_vars = mdl.continuous_var_dict(products, name=lambda p: 'outside_%s' % p[0]) - - # --- constraints --- - # demand satisfaction - mdl.add_constraints((mdl.inside_vars[prod] + mdl.outside_vars[prod] >= prod[1], 'ct_demand_%s' % prod[0]) for prod in products) - - # --- resource capacity --- - mdl.add_constraints((mdl.sum(mdl.inside_vars[p] * consumptions[p[0], res[0]] for p in products) <= res[1], - 'ct_res_%s' % res[0]) for res in resources) - - # --- objective --- - mdl.total_inside_cost = mdl.sum(mdl.inside_vars[p] * p[2] for p in products) - mdl.add_kpi(mdl.total_inside_cost, "inside cost") - mdl.total_outside_cost = mdl.sum(mdl.outside_vars[p] * p[3] for p in products) - mdl.add_kpi(mdl.total_outside_cost, "outside cost") - mdl.minimize(mdl.total_inside_cost + mdl.total_outside_cost) - return mdl - - -def print_production_solution(mdl, products): - obj = mdl.objective_value - print("* Production model solved with objective: {:g}".format(obj)) - print("* Total inside cost=%g" % mdl.total_inside_cost.solution_value) - for p in products: - print("Inside production of {product}: {ins_var}".format - (product=p[0], ins_var=mdl.inside_vars[p].solution_value)) - print("* Total outside cost=%g" % mdl.total_outside_cost.solution_value) - for p in products: - print("Outside production of {product}: {out_var}".format - (product=p[0], out_var=mdl.outside_vars[p].solution_value)) - - -def build_default_production_problem(**kwargs): - return build_production_problem(PRODUCTS, RESOURCES, CONSUMPTIONS, **kwargs) - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- -if __name__ == '__main__': - # Build the model - model = build_production_problem(PRODUCTS, RESOURCES, CONSUMPTIONS) - model.print_information() - # Solve the model. - if model.solve(): - print_production_solution(model, PRODUCTS) - # Save the CPLEX solution as "solution.json" program output - with get_environment().get_output_stream("solution.json") as fp: - model.solution.export(fp, "json") - else: - print("Problem has no solution") - +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + +"""The model aims at minimizing the production cost for a number of products +while satisfying customer demand. Each product can be produced either inside +the company or outside, at a higher cost. + +The inside production is constrained by the company's resources, while outside +production is considered unlimited. + +The model first declares the products and the resources. +The data consists of the description of the products (the demand, the inside +and outside costs, and the resource consumption) and the capacity of the +various resources. + +The variables for this problem are the inside and outside production for each +product. +""" + +from docplex.mp.model import Model +from docplex.util.environment import get_environment + + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- +PRODUCTS = [("kluski", 100, 0.6, 0.8), + ("capellini", 200, 0.8, 0.9), + ("fettucine", 300, 0.3, 0.4)] + +# resources are a list of simple tuples (name, capacity) +RESOURCES = [("flour", 20), + ("eggs", 40)] + +CONSUMPTIONS = {("kluski", "flour"): 0.5, + ("kluski", "eggs"): 0.2, + ("capellini", "flour"): 0.4, + ("capellini", "eggs"): 0.4, + ("fettucine", "flour"): 0.3, + ("fettucine", "eggs"): 0.6} + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- +def build_production_problem(mdl, products, resources, consumptions, **kwargs): + """ Takes as input: + - a list of product tuples (name, demand, inside, outside) + - a list of resource tuples (name, capacity) + - a list of consumption tuples (product_name, resource_named, consumed) + """ + # --- decision variables --- + mdl.inside_vars = mdl.continuous_var_dict(products, name=lambda p: 'inside_%s' % p[0]) + mdl.outside_vars = mdl.continuous_var_dict(products, name=lambda p: 'outside_%s' % p[0]) + + # --- constraints --- + # demand satisfaction + mdl.add_constraints((mdl.inside_vars[prod] + mdl.outside_vars[prod] >= prod[1], 'ct_demand_%s' % prod[0]) for prod in products) + + # --- resource capacity --- + mdl.add_constraints((mdl.sum(mdl.inside_vars[p] * consumptions[p[0], res[0]] for p in products) <= res[1], + 'ct_res_%s' % res[0]) for res in resources) + + # --- objective --- + mdl.total_inside_cost = mdl.sum(mdl.inside_vars[p] * p[2] for p in products) + mdl.add_kpi(mdl.total_inside_cost, "inside cost") + mdl.total_outside_cost = mdl.sum(mdl.outside_vars[p] * p[3] for p in products) + mdl.add_kpi(mdl.total_outside_cost, "outside cost") + mdl.minimize(mdl.total_inside_cost + mdl.total_outside_cost) + return mdl + + +def print_production_solution(mdl, products): + obj = mdl.objective_value + print("* Production model solved with objective: {:g}".format(obj)) + print("* Total inside cost=%g" % mdl.total_inside_cost.solution_value) + for p in products: + print("Inside production of {product}: {ins_var}".format + (product=p[0], ins_var=mdl.inside_vars[p].solution_value)) + print("* Total outside cost=%g" % mdl.total_outside_cost.solution_value) + for p in products: + print("Outside production of {product}: {out_var}".format + (product=p[0], out_var=mdl.outside_vars[p].solution_value)) + +def build_default_production_problem(**kwargs): + mdl = Model( **kwargs) + return build_production_problem(mdl, PRODUCTS, RESOURCES, CONSUMPTIONS) +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- +if __name__ == '__main__': + # Build the model + with Model(name='production') as model: + model = build_production_problem(model, PRODUCTS, RESOURCES, CONSUMPTIONS) + model.print_information() + # Solve the model. + if model.solve(): + print_production_solution(model, PRODUCTS) + # Save the CPLEX solution as "solution.json" program output + with get_environment().get_output_stream("solution.json") as fp: + model.solution.export(fp, "json") + else: + print("Problem has no solution") + diff --git a/examples/mp/modeling/sailcopw.py b/examples/mp/modeling/sailcopw.py index 08ef1a4..26f46b6 100644 --- a/examples/mp/modeling/sailcopw.py +++ b/examples/mp/modeling/sailcopw.py @@ -1,84 +1,87 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - - -from docplex.mp.model import Model -from docplex.util.environment import get_environment - - -# The company Sailco must determine how many sailboats to produce over several time periods, -# while satisfying demand and minimizing costs. -# The demand for the periods is known and an inventory of boats is available initially. -# In each period, Sailco can produce boats inside at a fixed cost per boat. -# Additional boats can be produced outside at a higher cost per boat. -# There is an inventory cost per boat per period. -# The business objective is to minimize the overall cost, which is the sum of the -# production cost and inventory cost. -# The production cost is modeled using a *piecewise-linear* function. - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- -nb_periods = 4 -demand = {1: 40, 2: 60, 3: 75, 4: 25} - -regular_cost = 400 -capacity = 40 -extra_cost = 450 - -initial_inventory = 10 -inventory_cost = 20 - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- - - -def build_sailcopw_model(**kwargs): - mdl = Model(name="sailcopw", **kwargs) - periods0 = range(nb_periods + 1) - periods1 = range(1, nb_periods + 1) - - boats = mdl.continuous_var_dict(periods1, name="boat") - # full range from 0 to nb_periods - inv = mdl.continuous_var_dict(periods0, name="inv") - - # --- - # piecewise cost: - # up to zero boat cost is zero. - # up to capacity, each boat costs the regular cost - # above capacity, unit cost is extra cost (higher than regular cost...) - pwc = mdl.piecewise(preslope=0, breaksxy=[(0,0), (capacity, capacity * regular_cost)], postslope=extra_cost) - total_pw_cost = mdl.sum(pwc(boats[t]) for t in periods1) - mdl.add_kpi(total_pw_cost, "Total piecewise cost") - total_inventory_cost = inventory_cost * mdl.sum(inv[t1] for t1 in periods1) - mdl.add_kpi(total_inventory_cost, 'Total inventory cost') - - mdl.minimize(total_pw_cost + total_inventory_cost) - - # initial inventory - mdl.add_constraint(inv[0] == initial_inventory) - # balance - mdl.add_constraints([boats[t] + inv[t - 1] == inv[t] + demand.get(t,0) for t in periods1]) - - return mdl - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- - -if __name__ == '__main__': - sailm = build_sailcopw_model() - s = sailm.solve(log_output=True) - if s: - sailm.report() - sailm.print_solution() - - # Save the CPLEX solution as "solution.json" program output - with get_environment().get_output_stream("solution.json") as fp: - sailm.solution.export(fp, "json") - else: - print("Problem has no solution") +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + + +from docplex.mp.model import Model +from docplex.util.environment import get_environment + + +# The company Sailco must determine how many sailboats to produce over several time periods, +# while satisfying demand and minimizing costs. +# The demand for the periods is known and an inventory of boats is available initially. +# In each period, Sailco can produce boats inside at a fixed cost per boat. +# Additional boats can be produced outside at a higher cost per boat. +# There is an inventory cost per boat per period. +# The business objective is to minimize the overall cost, which is the sum of the +# production cost and inventory cost. +# The production cost is modeled using a *piecewise-linear* function. + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- +nb_periods = 4 +demand = {1: 40, 2: 60, 3: 75, 4: 25} + +regular_cost = 400 +capacity = 40 +extra_cost = 450 + +initial_inventory = 10 +inventory_cost = 20 + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- + +def build_default_sailcopw_model(**kwargs): + mdl = Model("sailcopw") + return build_sailcopw_model(mdl, **kwargs) + +def build_sailcopw_model(mdl, **kwargs): + periods0 = range(nb_periods + 1) + periods1 = range(1, nb_periods + 1) + + boats = mdl.continuous_var_dict(periods1, name="boat") + # full range from 0 to nb_periods + inv = mdl.continuous_var_dict(periods0, name="inv") + + # --- + # piecewise cost: + # up to zero boat cost is zero. + # up to capacity, each boat costs the regular cost + # above capacity, unit cost is extra cost (higher than regular cost...) + pwc = mdl.piecewise(preslope=0, breaksxy=[(0,0), (capacity, capacity * regular_cost)], postslope=extra_cost) + total_pw_cost = mdl.sum(pwc(boats[t]) for t in periods1) + mdl.add_kpi(total_pw_cost, "Total piecewise cost") + total_inventory_cost = inventory_cost * mdl.sum(inv[t1] for t1 in periods1) + mdl.add_kpi(total_inventory_cost, 'Total inventory cost') + + mdl.minimize(total_pw_cost + total_inventory_cost) + + # initial inventory + mdl.add_constraint(inv[0] == initial_inventory) + # balance + mdl.add_constraints([boats[t] + inv[t - 1] == inv[t] + demand.get(t,0) for t in periods1]) + + return mdl + +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- + +if __name__ == '__main__': + with Model(name="sailcopw") as sailm: + build_sailcopw_model(sailm) + s = sailm.solve(log_output=True) + if s: + sailm.report() + sailm.print_solution() + + # Save the CPLEX solution as "solution.json" program output + with get_environment().get_output_stream("solution.json") as fp: + sailm.solution.export(fp, "json") + else: + print("Problem has no solution") diff --git a/examples/mp/modeling/sport_scheduling.py b/examples/mp/modeling/sport_scheduling.py index 76ff2df..557299f 100644 --- a/examples/mp/modeling/sport_scheduling.py +++ b/examples/mp/modeling/sport_scheduling.py @@ -1,139 +1,140 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - -from collections import namedtuple - -from docplex.mp.model import Model -from docplex.util.environment import get_environment - - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- -nbs = (8, 3, 2) - -team_div1 = {"Baltimore Ravens", "Cincinnati Bengals", "Cleveland Browns", - "Pittsburgh Steelers", "Houston Texans", "Indianapolis Colts", - "Jacksonville Jaguars", "Tennessee Titans", "Buffalo Bills", - "Miami Dolphins", "New England Patriots", "New York Jets", - "Denver Broncos", "Kansas City Chiefs", "Oakland Raiders", - "San Diego Chargers"} - -team_div2 = {"Chicago Bears", "Detroit Lions", "Green Bay Packers", - "Minnesota Vikings", "Atlanta Falcons", "Carolina Panthers", - "New Orleans Saints", "Tampa Bay Buccaneers", "Dallas Cowboys", - "New York Giants", "Philadelphia Eagles", "Washington Redskins", - "Arizona Cardinals", "San Francisco 49ers", "Seattle Seahawks", - "St. Louis Rams"} - -Match = namedtuple("Matches", ["team1", "team2", "is_divisional"]) - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- -def build_sports(**kwargs): - print("* building sport scheduling model instance") - mdl = Model('sportSchedCPLEX', **kwargs) - - nb_teams_in_division, nb_intra_divisional, nb_inter_divisional = nbs - assert len(team_div1) == len(team_div2) - mdl.teams = list(team_div1 | team_div2) - # team index ranges from 1 to 2N - team_range = range(1, 2 * nb_teams_in_division + 1) - - # Calculate the number of weeks necessary. - nb_weeks = (nb_teams_in_division - 1) * nb_intra_divisional + nb_teams_in_division * nb_inter_divisional - weeks = range(1, nb_weeks + 1) - mdl.weeks = weeks - - print("{0} games, {1} intradivisional, {2} interdivisional" - .format(nb_weeks, (nb_teams_in_division - 1) * nb_intra_divisional, - nb_teams_in_division * nb_inter_divisional)) - - # Season is split into two halves. - first_half_weeks = range(1, nb_weeks // 2 + 1) - nb_first_half_games = nb_weeks // 3 - - # All possible matches (pairings) and whether of not each is intradivisional. - matches = [Match(t1, t2, 1 if (t2 <= nb_teams_in_division or t1 > nb_teams_in_division) else 0) - for t1 in team_range for t2 in team_range if t1 < t2] - mdl.matches = matches - # Number of games to play between pairs depends on - # whether the pairing is intradivisional or not. - nb_play = {m: nb_intra_divisional if m.is_divisional == 1 else nb_inter_divisional for m in matches} - - plays = mdl.binary_var_matrix(keys1=matches, keys2=weeks, - name=lambda mw: "play_%d_%d_w%d" % (mw[0].team1, mw[0].team2, mw[1])) - mdl.plays = plays - - for m in matches: - mdl.add_constraint(mdl.sum(plays[m, w] for w in weeks) == nb_play[m], - "correct_nb_games_%d_%d" % (m.team1, m.team2)) - - for w in weeks: - # Each team must play exactly once in a week. - for t in team_range: - max_teams_in_division = (plays[m, w] for m in matches if m.team1 == t or m.team2 == t) - mdl.add_constraint(mdl.sum(max_teams_in_division) == 1, - "plays_exactly_once_%d_%s" % (w, t)) - - # Games between the same teams cannot be on successive weeks. - mdl.add_constraints(plays[m, w] + plays[m, w + 1] <= 1 - for w in weeks for m in matches if w < nb_weeks) - - # Some intradivisional games should be in the first half. - for t in team_range: - max_teams_in_division = [plays[m, w] for w in first_half_weeks for m in matches if - m.is_divisional == 1 and (m.team1 == t or m.team2 == t)] - - mdl.add_constraint(mdl.sum(max_teams_in_division) >= nb_first_half_games, - "in_division_first_half_%s" % t) - - # postpone divisional matches as much as possible - # we weight each play variable with the square of w. - mdl.maximize(mdl.sum(plays[m, w] * w * w for w in weeks for m in matches if m.is_divisional)) - return mdl - -# a named tuple to store solution -TSolution = namedtuple("TSolution", ["week", "is_divisional", "team1", "team2"]) - - -def print_sports_solution(mdl): - # iterate with weeks first - solution = [TSolution(w, m.is_divisional, mdl.teams[m.team1], mdl.teams[m.team2]) - for w in mdl.weeks for m in mdl.matches - if mdl.plays[m, w].to_bool()] - - currweek = 0 - print("Intradivisional games are marked with a *") - for s in solution: - # assume records are sorted by increasing week indices. - if s.week != currweek: - currweek = s.week - print(" == == == == == == == == == == == == == == == == ") - print("On week %d" % currweek) - - print(" {0:s}{1} will meet the {2}".format("*" if s.is_divisional else "", s.team1, s.team2)) - - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- -if __name__ == '__main__': - # Build the model - model = build_sports() - model.print_information() - # Solve the model. If a key has been specified above, the solve - # will use IBM Decision Optimization on cloud. - if model.solve(): - model.report() - print_sports_solution(model) - # Save the CPLEX solution as "solution.json" program output - with get_environment().get_output_stream("solution.json") as fp: - model.solution.export(fp, "json") - else: - print("Problem could not be solved: " + model.solve_details.get_status()) +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + +from collections import namedtuple + +from docplex.mp.model import Model +from docplex.util.environment import get_environment + + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- +nbs = (8, 3, 2) + +team_div1 = {"Baltimore Ravens", "Cincinnati Bengals", "Cleveland Browns", + "Pittsburgh Steelers", "Houston Texans", "Indianapolis Colts", + "Jacksonville Jaguars", "Tennessee Titans", "Buffalo Bills", + "Miami Dolphins", "New England Patriots", "New York Jets", + "Denver Broncos", "Kansas City Chiefs", "Oakland Raiders", + "San Diego Chargers"} + +team_div2 = {"Chicago Bears", "Detroit Lions", "Green Bay Packers", + "Minnesota Vikings", "Atlanta Falcons", "Carolina Panthers", + "New Orleans Saints", "Tampa Bay Buccaneers", "Dallas Cowboys", + "New York Giants", "Philadelphia Eagles", "Washington Redskins", + "Arizona Cardinals", "San Francisco 49ers", "Seattle Seahawks", + "St. Louis Rams"} + +Match = namedtuple("Matches", ["team1", "team2", "is_divisional"]) + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- +def build_sports(**kwargs): + print("* building sport scheduling model instance") + mdl = Model('sportSchedCPLEX', **kwargs) + + nb_teams_in_division, nb_intra_divisional, nb_inter_divisional = nbs + assert len(team_div1) == len(team_div2) + mdl.teams = list(team_div1 | team_div2) + # team index ranges from 1 to 2N + team_range = range(1, 2 * nb_teams_in_division + 1) + + # Calculate the number of weeks necessary. + nb_weeks = (nb_teams_in_division - 1) * nb_intra_divisional + nb_teams_in_division * nb_inter_divisional + weeks = range(1, nb_weeks + 1) + mdl.weeks = weeks + + print("{0} games, {1} intradivisional, {2} interdivisional" + .format(nb_weeks, (nb_teams_in_division - 1) * nb_intra_divisional, + nb_teams_in_division * nb_inter_divisional)) + + # Season is split into two halves. + first_half_weeks = range(1, nb_weeks // 2 + 1) + nb_first_half_games = nb_weeks // 3 + + # All possible matches (pairings) and whether of not each is intradivisional. + matches = [Match(t1, t2, 1 if (t2 <= nb_teams_in_division or t1 > nb_teams_in_division) else 0) + for t1 in team_range for t2 in team_range if t1 < t2] + mdl.matches = matches + # Number of games to play between pairs depends on + # whether the pairing is intradivisional or not. + nb_play = {m: nb_intra_divisional if m.is_divisional == 1 else nb_inter_divisional for m in matches} + + plays = mdl.binary_var_matrix(keys1=matches, keys2=weeks, + name=lambda mw: "play_%d_%d_w%d" % (mw[0].team1, mw[0].team2, mw[1])) + mdl.plays = plays + + for m in matches: + mdl.add_constraint(mdl.sum(plays[m, w] for w in weeks) == nb_play[m], + "correct_nb_games_%d_%d" % (m.team1, m.team2)) + + for w in weeks: + # Each team must play exactly once in a week. + for t in team_range: + max_teams_in_division = (plays[m, w] for m in matches if m.team1 == t or m.team2 == t) + mdl.add_constraint(mdl.sum(max_teams_in_division) == 1, + "plays_exactly_once_%d_%s" % (w, t)) + + # Games between the same teams cannot be on successive weeks. + mdl.add_constraints(plays[m, w] + plays[m, w + 1] <= 1 + for w in weeks for m in matches if w < nb_weeks) + + # Some intradivisional games should be in the first half. + for t in team_range: + max_teams_in_division = [plays[m, w] for w in first_half_weeks for m in matches if + m.is_divisional == 1 and (m.team1 == t or m.team2 == t)] + + mdl.add_constraint(mdl.sum(max_teams_in_division) >= nb_first_half_games, + "in_division_first_half_%s" % t) + + # postpone divisional matches as much as possible + # we weight each play variable with the square of w. + mdl.maximize(mdl.sum(plays[m, w] * w * w for w in weeks for m in matches if m.is_divisional)) + return mdl + +# a named tuple to store solution +TSolution = namedtuple("TSolution", ["week", "is_divisional", "team1", "team2"]) + + +def print_sports_solution(mdl): + # iterate with weeks first + solution = [TSolution(w, m.is_divisional, mdl.teams[m.team1], mdl.teams[m.team2]) + for w in mdl.weeks for m in mdl.matches + if mdl.plays[m, w].to_bool()] + + currweek = 0 + print("Intradivisional games are marked with a *") + for s in solution: + # assume records are sorted by increasing week indices. + if s.week != currweek: + currweek = s.week + print(" == == == == == == == == == == == == == == == == ") + print("On week %d" % currweek) + + print(" {0:s}{1} will meet the {2}".format("*" if s.is_divisional else "", s.team1, s.team2)) + + +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- +if __name__ == '__main__': + # Build the model + model = build_sports() + model.print_information() + # Solve the model. If a key has been specified above, the solve + # will use IBM Decision Optimization on cloud. + if model.solve(): + model.report() + print_sports_solution(model) + # Save the CPLEX solution as "solution.json" program output + with get_environment().get_output_stream("solution.json") as fp: + model.solution.export(fp, "json") + else: + print("Problem could not be solved: " + model.solve_details.get_status()) + model.end() diff --git a/examples/mp/workflow/cutstock.py b/examples/mp/workflow/cutstock.py index 4294627..456145e 100644 --- a/examples/mp/workflow/cutstock.py +++ b/examples/mp/workflow/cutstock.py @@ -1,272 +1,276 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2016 -# -------------------------------------------------------------------------- - -from collections import namedtuple -import json - -from docplex.util.environment import get_environment -from docplex.mp.model import Model - - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- -DEFAULT_ROLL_WIDTH = 110 -DEFAULT_ITEMS = [(1, 20, 48), (2, 45, 35), (3, 50, 24), (4, 55, 10), (5, 75, 8)] -DEFAULT_PATTERNS = [(i, 1) for i in range(1, 6)] # (1, 1), (2, 1) etc -DEFAULT_PATTERN_ITEM_FILLED = [(p, p, 1) for p in range(1, 6)] # pattern1 for item1, pattern2 for item2, etc. - -FIRST_GENERATION_DUALS = [1, 1, 1, 1, 0] - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- -class TItem(object): - def __init__(self, item_id, item_size, demand): - self.id = item_id - self.size = item_size - self.demand = demand - self.dual_value = -1 - - @classmethod - def make(cls, args): - arg_id = args[0] - arg_size = args[1] - arg_demand = args[2] - return cls(arg_id, arg_size, arg_demand) - - def __str__(self): - return 'item%d' % self.id - - -class TPattern(namedtuple("TPattern", ["id", "cost"])): - def __str__(self): - return 'pattern%d' % self.id - -# --- - - -def make_cutstock_pattern_generation_model(items, roll_width, **kwargs): - gen_model = Model(name='cutstock_generate_patterns', **kwargs) - # store data - gen_model.items = items - gen_model.roll_width = roll_width - # default values - gen_model.duals = [1] * len(items) - # 1. create variables: one per item - gen_model.use_vars = gen_model.integer_var_list(keys=items, ub=999999, name='use') - # 2 setup constraint: - # --- sum of item usage times item sizes must be less than roll width - gen_model.add(gen_model.dot(gen_model.use_vars, (it.size for it in items)) <= roll_width) - - # store dual expression for dynamic edition - gen_model.use_dual_expr = 1 - gen_model.dot(gen_model.use_vars, gen_model.duals) - # minimize - gen_model.minimize(gen_model.use_dual_expr) - - return gen_model - - -def cutstock_update_duals(gmodel, new_duals): - # update the duals array and the the duals expression... - # edition is propagated to the objective of the model. - gmodel.duals = new_duals - use_vars = gmodel.use_vars - assert len(new_duals) == len(use_vars) - updated_used = [(use, -new_duals[u]) for u, use in enumerate(use_vars)] - # this modification is notified to the objective. - gmodel.use_dual_expr.set_coefficients(updated_used) - return gmodel - - -def make_custstock_master_model(item_table, pattern_table, fill_table, roll_width, **kwargs): - m = Model(name='custock_master', **kwargs) - - # store data as properties - m.items = [TItem.make(it_row) for it_row in item_table] - m.items_by_id = {it.id: it for it in m.items} - m.patterns = [TPattern(*pattern_row) for pattern_row in pattern_table] - m.patterns_by_id = {pat.id: pat for pat in m.patterns} - m.max_pattern_id = max(pt.id for pt in m.patterns) - - # build a dictionary storing how much each pattern fills each item. - m.pattern_item_filled = {(m.patterns_by_id[p], m.items_by_id[i]): f for (p, i, f) in fill_table} - m.roll_width = roll_width - - # --- variables - # one cut var per pattern... - m.MAX_CUT = 9999 - m.cut_vars = m.continuous_var_dict(m.patterns, lb=0, ub=m.MAX_CUT, name="cut") - - # --- add fill constraints - # - all_patterns = m.patterns - all_items = m.items - m.item_fill_cts = [] - for item in all_items: - item_fill_ct = m.sum( - m.cut_vars[p] * m.pattern_item_filled.get((p, item), 0) for p in all_patterns) >= item.demand - item_fill_ct.name = 'ct_fill_{0!s}'.format(item) - m.item_fill_cts.append(item_fill_ct) - m.add_constraints(m.item_fill_cts) - - # --- minimize total cut stock - m.total_cutting_cost = m.sum(m.cut_vars[p] * p.cost for p in all_patterns) - m.minimize(m.total_cutting_cost) - - return m - - -def add_pattern_to_master_model(master_model, item_usages): - """ Adds a new pattern to the master model. - - This function performs the following: - - 1. build a new pattern instance from item usages (taken from sub-model) - 2. add it to the master model - 3. update decision objects with this new pattern. - """ - new_pattern_id = max(pt.id for pt in master_model.patterns) + 1 - new_pattern = TPattern(new_pattern_id, 1) - master_model.patterns.append(new_pattern) - for used, item in zip(item_usages, master_model.items): - master_model.pattern_item_filled[new_pattern, item] = used - - # --- add one decision variable, linked to the new pattern. - new_pattern_cut_var = master_model.continuous_var(lb=0, ub=master_model.MAX_CUT, - name='cut_{0}'.format(new_pattern_id)) - master_model.cut_vars[new_pattern] = new_pattern_cut_var - - # update constraints - for item, ct in zip(master_model.items, master_model.item_fill_cts): - # update fill constraint by changing lhs - ctlhs = ct.lhs - filled = master_model.pattern_item_filled[new_pattern, item] - if filled: - ctlhs += new_pattern_cut_var * filled - - # update objective: - # side-effect on the total cutting cost expr propagates to the objective. - cost_expr = master_model.total_cutting_cost - cost_expr += new_pattern_cut_var * new_pattern.cost # this performw a side effect! - - return master_model - - -def cutstock_print_solution(cutstock_model): - patterns = cutstock_model.patterns - cut_var_values = {p: cutstock_model.cut_vars[p].solution_value for p in patterns} - pattern_item_filled = cutstock_model.pattern_item_filled - print("| Nb of cuts | Pattern | Pattern's detail (# of item1,item2,...) |") - print("| {} |".format("-" * 70)) - for p in patterns: - if cut_var_values[p] >= 1e-3: - pattern_detail = {b.id: pattern_item_filled[a, b] for a, b in pattern_item_filled if - a == p} - print( - "| {:<10g} | {!s:9} | {!s:45} |".format(cut_var_values[p], - p, - pattern_detail)) - print("| {} |".format("-" * 70)) - - -def cutstock_save_as_json(model, json_file): - patterns = model.patterns - cut_var_values = {p: model.cut_vars[p].solution_value for p in patterns} - solution = [] - for p in patterns: - if cut_var_values[p] >= 1e-3: - pattern_detail = {b.id: model.pattern_item_filled[(a, b)] for (a, b) in model.pattern_item_filled if - a == p} - n = {'pattern': str(p), - 'cuts': "%g" % cut_var_values[p], - 'details': pattern_detail} - solution.append(n) - json_file.write(json.dumps(solution, indent=3).encode('utf-8')) - - -def cutstock_solve(item_table, pattern_table, fill_table, roll_width, **kwargs): - verbose = kwargs.pop('verbose', True) - master_model = make_custstock_master_model(item_table, pattern_table, fill_table, roll_width, **kwargs) - - # these two fields contain named tuples - items = master_model.items - patterns = master_model.patterns - gen_model = make_cutstock_pattern_generation_model(items, roll_width, **kwargs) - - rc_eps = 1e-6 - obj_eps = 1e-4 - loop_count = 0 - best = 0 - curr = 1e+20 - ms = None - - while loop_count < 100 and abs(best - curr) >= obj_eps: - ms = master_model.solve(**kwargs) - loop_count += 1 - best = curr - if not ms: - print('{}> master model fails, stop'.format(loop_count)) - break - else: - assert ms - curr = master_model.objective_value - if verbose: - print('{}> new column generation iteration, #patterns={}, best={:g}, curr={:g}' - .format(loop_count, len(patterns), best, curr)) - duals = master_model.dual_values(master_model.item_fill_cts) - if verbose: - print('{0}> moving duals from master to sub model: {1}' - .format(loop_count, list(map(lambda x: float('%0.2f' % x), duals)))) - cutstock_update_duals(gen_model, duals) - gs = gen_model.solve(**kwargs) - if not gs: - print('{}> slave model fails, stop'.format(loop_count)) - break - - rc_cost = gen_model.objective_value - if rc_cost <= -rc_eps: - if verbose: - print('{}> slave model runs with obj={:g}'.format(loop_count, rc_cost)) - else: - if verbose: - print('{}> pattern-generator model stops, obj={:g}'.format(loop_count, rc_cost)) - break - - use_values = gen_model.solution.get_values(gen_model.use_vars) - if verbose: - print('{}> add new pattern to master data: {}'.format(loop_count, str(use_values))) - # make a new pattern with use values - if not (loop_count < 100 and abs(best - curr) >= obj_eps): - print('* terminating: best-curr={:g}'.format(abs(best - curr))) - break - add_pattern_to_master_model(master_model, use_values) - - if ms: - if verbose: - print('\n* Cutting-stock column generation terminates, best={:g}, #loops={}'.format(curr, loop_count)) - cutstock_print_solution(master_model) - return ms - else: - print("!!!! Cutting-stock column generation fails !!!!") - return None - - -def cutstock_solve_default(**kwargs): - return cutstock_solve(DEFAULT_ITEMS, DEFAULT_PATTERNS, DEFAULT_PATTERN_ITEM_FILLED, DEFAULT_ROLL_WIDTH, - **kwargs) - - -# ----------------------------------------------------------------------------- -# Solve the model and display the result -# ----------------------------------------------------------------------------- -if __name__ == '__main__': - s = cutstock_solve_default() - assert abs(s.objective_value - 46.25) <= 0.1 - # Save the solution as "solution.json" program output. - with get_environment().get_output_stream("solution.json") as fp: - cutstock_save_as_json(s.model, fp) +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2022 +# -------------------------------------------------------------------------- + +from collections import namedtuple +import json + +from docplex.util.environment import get_environment +from docplex.mp.model import Model + + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- +DEFAULT_ROLL_WIDTH = 110 +DEFAULT_ITEMS = [(1, 20, 48), (2, 45, 35), (3, 50, 24), (4, 55, 10), (5, 75, 8)] +DEFAULT_PATTERNS = [(i, 1) for i in range(1, 6)] # (1, 1), (2, 1) etc +DEFAULT_PATTERN_ITEM_FILLED = [(p, p, 1) for p in range(1, 6)] # pattern1 for item1, pattern2 for item2, etc. + +FIRST_GENERATION_DUALS = [1, 1, 1, 1, 0] + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- +class TItem(object): + def __init__(self, item_id, item_size, demand): + self.id = item_id + self.size = item_size + self.demand = demand + self.dual_value = -1 + + @classmethod + def make(cls, args): + arg_id = args[0] + arg_size = args[1] + arg_demand = args[2] + return cls(arg_id, arg_size, arg_demand) + + def __str__(self): + return 'item%d' % self.id + + +class TPattern(namedtuple("TPattern", ["id", "cost"])): + def __str__(self): + return 'pattern%d' % self.id + +# --- + + +def make_cutstock_pattern_generation_model(items, roll_width, **kwargs): + gen_model = Model(name='cutstock_generate_patterns', **kwargs) + # store data + gen_model.items = items + gen_model.roll_width = roll_width + # default values + gen_model.duals = [1] * len(items) + # 1. create variables: one per item + gen_model.use_vars = gen_model.integer_var_list(keys=items, ub=999999, name='use') + # 2 setup constraint: + # --- sum of item usage times item sizes must be less than roll width + gen_model.add(gen_model.dot(gen_model.use_vars, (it.size for it in items)) <= roll_width) + + # store dual expression for dynamic edition + gen_model.use_dual_expr = 1 - gen_model.dot(gen_model.use_vars, gen_model.duals) + # minimize + gen_model.minimize(gen_model.use_dual_expr) + + return gen_model + + +def cutstock_update_duals(gmodel, new_duals): + # update the duals array and the the duals expression... + # edition is propagated to the objective of the model. + gmodel.duals = new_duals + use_vars = gmodel.use_vars + assert len(new_duals) == len(use_vars) + updated_used = [(use, -new_duals[u]) for u, use in enumerate(use_vars)] + # this modification is notified to the objective. + gmodel.use_dual_expr.set_coefficients(updated_used) + return gmodel + + +def make_custstock_master_model(item_table, pattern_table, fill_table, roll_width, **kwargs): + m = Model(name='custock_master', **kwargs) + + # store data as properties + m.items = [TItem.make(it_row) for it_row in item_table] + m.items_by_id = {it.id: it for it in m.items} + m.patterns = [TPattern(*pattern_row) for pattern_row in pattern_table] + m.patterns_by_id = {pat.id: pat for pat in m.patterns} + m.max_pattern_id = max(pt.id for pt in m.patterns) + + # build a dictionary storing how much each pattern fills each item. + m.pattern_item_filled = {(m.patterns_by_id[p], m.items_by_id[i]): f for (p, i, f) in fill_table} + m.roll_width = roll_width + + # --- variables + # one cut var per pattern... + m.MAX_CUT = 9999 + m.cut_vars = m.continuous_var_dict(m.patterns, lb=0, ub=m.MAX_CUT, name="cut") + + # --- add fill constraints + # + all_patterns = m.patterns + all_items = m.items + m.item_fill_cts = [] + for item in all_items: + item_fill_ct = m.sum( + m.cut_vars[p] * m.pattern_item_filled.get((p, item), 0) for p in all_patterns) >= item.demand + item_fill_ct.name = 'ct_fill_{0!s}'.format(item) + m.item_fill_cts.append(item_fill_ct) + m.add_constraints(m.item_fill_cts) + + # --- minimize total cut stock + m.total_cutting_cost = m.sum(m.cut_vars[p] * p.cost for p in all_patterns) + m.minimize(m.total_cutting_cost) + + return m + + +def add_pattern_to_master_model(master_model, item_usages): + """ Adds a new pattern to the master model. + + This function performs the following: + + 1. build a new pattern instance from item usages (taken from sub-model) + 2. add it to the master model + 3. update decision objects with this new pattern. + """ + new_pattern_id = max(pt.id for pt in master_model.patterns) + 1 + new_pattern = TPattern(new_pattern_id, 1) + master_model.patterns.append(new_pattern) + for used, item in zip(item_usages, master_model.items): + master_model.pattern_item_filled[new_pattern, item] = used + + # --- add one decision variable, linked to the new pattern. + new_pattern_cut_var = master_model.continuous_var(lb=0, ub=master_model.MAX_CUT, + name='cut_{0}'.format(new_pattern_id)) + master_model.cut_vars[new_pattern] = new_pattern_cut_var + + # update constraints + for item, ct in zip(master_model.items, master_model.item_fill_cts): + # update fill constraint by changing lhs + ctlhs = ct.lhs + filled = master_model.pattern_item_filled[new_pattern, item] + if filled: + ctlhs += new_pattern_cut_var * filled + + # update objective: + # side-effect on the total cutting cost expr propagates to the objective. + cost_expr = master_model.total_cutting_cost + cost_expr += new_pattern_cut_var * new_pattern.cost # this performw a side effect! + + return master_model + + +def cutstock_print_solution(cutstock_model): + patterns = cutstock_model.patterns + cut_var_values = {p: cutstock_model.cut_vars[p].solution_value for p in patterns} + pattern_item_filled = cutstock_model.pattern_item_filled + print("| Nb of cuts | Pattern | Pattern's detail (# of item1,item2,...) |") + print("| {} |".format("-" * 70)) + for p in patterns: + if cut_var_values[p] >= 1e-3: + pattern_detail = {b.id: pattern_item_filled[a, b] for a, b in pattern_item_filled if + a == p} + print( + "| {:<10g} | {!s:9} | {!s:45} |".format(cut_var_values[p], + p, + pattern_detail)) + print("| {} |".format("-" * 70)) + + +def cutstock_save_as_json(model, json_file): + patterns = model.patterns + cut_var_values = {p: model.cut_vars[p].solution_value for p in patterns} + solution = [] + for p in patterns: + if cut_var_values[p] >= 1e-3: + pattern_detail = {b.id: model.pattern_item_filled[(a, b)] for (a, b) in model.pattern_item_filled if + a == p} + n = {'pattern': str(p), + 'cuts': "%g" % cut_var_values[p], + 'details': pattern_detail} + solution.append(n) + json_file.write(json.dumps(solution, indent=3).encode('utf-8')) + + +def cutstock_solve(item_table, pattern_table, fill_table, roll_width, **kwargs): + verbose = kwargs.pop('verbose', True) + master_model = make_custstock_master_model(item_table, pattern_table, fill_table, roll_width, **kwargs) + + # these two fields contain named tuples + items = master_model.items + patterns = master_model.patterns + gen_model = make_cutstock_pattern_generation_model(items, roll_width, **kwargs) + + rc_eps = 1e-6 + obj_eps = 1e-4 + loop_count = 0 + best = 0 + curr = 1e+20 + ms = None + + while loop_count < 100 and abs(best - curr) >= obj_eps: + ms = master_model.solve(**kwargs) + loop_count += 1 + best = curr + if not ms: + print('{}> master model fails, stop'.format(loop_count)) + break + else: + assert ms + curr = master_model.objective_value + if verbose: + print('{}> new column generation iteration, #patterns={}, best={:g}, curr={:g}' + .format(loop_count, len(patterns), best, curr)) + duals = master_model.dual_values(master_model.item_fill_cts) + if verbose: + print('{0}> moving duals from master to sub model: {1}' + .format(loop_count, list(map(lambda x: float('%0.2f' % x), duals)))) + cutstock_update_duals(gen_model, duals) + gs = gen_model.solve(**kwargs) + if not gs: + print('{}> slave model fails, stop'.format(loop_count)) + break + + rc_cost = gen_model.objective_value + if rc_cost <= -rc_eps: + if verbose: + print('{}> slave model runs with obj={:g}'.format(loop_count, rc_cost)) + else: + if verbose: + print('{}> pattern-generator model stops, obj={:g}'.format(loop_count, rc_cost)) + break + + use_values = gen_model.solution.get_values(gen_model.use_vars) + if verbose: + print('{}> add new pattern to master data: {}'.format(loop_count, str(use_values))) + # make a new pattern with use values + if not (loop_count < 100 and abs(best - curr) >= obj_eps): + print('* terminating: best-curr={:g}'.format(abs(best - curr))) + break + add_pattern_to_master_model(master_model, use_values) + + ret = None + if ms: + if verbose: + print('\n* Cutting-stock column generation terminates, best={:g}, #loops={}'.format(curr, loop_count)) + cutstock_print_solution(master_model) + ret = ms + else: + print("!!!! Cutting-stock column generation fails !!!!") + ret = None + gen_model.end() + + return (master_model, ret) + +def cutstock_solve_default(**kwargs): + return cutstock_solve(DEFAULT_ITEMS, DEFAULT_PATTERNS, DEFAULT_PATTERN_ITEM_FILLED, DEFAULT_ROLL_WIDTH, + **kwargs) + + +# ----------------------------------------------------------------------------- +# Solve the model and display the result +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + m,s = cutstock_solve_default() + assert abs(s.objective_value - 46.25) <= 0.1 + # Save the solution as "solution.json" program output. + with get_environment().get_output_stream("solution.json") as fp: + cutstock_save_as_json(m, fp) + m.end() diff --git a/examples/mp/workflow/lagrangian_relaxation.py b/examples/mp/workflow/lagrangian_relaxation.py index 1179376..aebe770 100644 --- a/examples/mp/workflow/lagrangian_relaxation.py +++ b/examples/mp/workflow/lagrangian_relaxation.py @@ -1,133 +1,133 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - -import json - -from docplex.util.environment import get_environment -from docplex.mp.model import Model - - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- -B = [15, 15, 15] -C = [ - [ 6, 10, 1], - [12, 12, 5], - [15, 4, 3], - [10, 3, 9], - [8, 9, 5] -] -A = [ - [ 5, 7, 2], - [14, 8, 7], - [10, 6, 12], - [ 8, 4, 15], - [ 6, 12, 5] -] - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- -def run_GAP_model(As, Bs, Cs, **kwargs): - with Model('GAP per Wolsey -without- Lagrangian Relaxation', **kwargs) as mdl: - print("#As={}, #Bs={}, #Cs={}".format(len(As), len(Bs), len(Cs))) - number_of_cs = len(C) - # variables - x_vars = [mdl.binary_var_list(c, name=None) for c in Cs] - - # constraints - mdl.add_constraints(mdl.sum(xv) <= 1 for xv in x_vars) - - mdl.add_constraints(mdl.sum(x_vars[ii][j] * As[ii][j] for ii in range(number_of_cs)) <= bs - for j, bs in enumerate(Bs)) - - # objective - total_profit = mdl.sum(mdl.scal_prod(x_i, c_i) for c_i, x_i in zip(Cs, x_vars)) - mdl.maximize(total_profit) - # mdl.print_information() - s = mdl.solve() - assert s is not None - obj = s.objective_value - print("* GAP with no relaxation run OK, best objective is: {:g}".format(obj)) - return obj - - -def run_GAP_model_with_Lagrangian_relaxation(As, Bs, Cs, max_iters=101, **kwargs): - with Model('GAP per Wolsey -with- Lagrangian Relaxation', **kwargs) as mdl: - print("#As={}, #Bs={}, #Cs={}".format(len(As), len(Bs), len(Cs))) - number_of_cs = len(Cs) - c_range = range(number_of_cs) - # variables - x_vars = [mdl.binary_var_list(c, name=None) for c in Cs] - p_vars = mdl.continuous_var_list(Cs, name='p') # new for relaxation - - mdl.add_constraints(mdl.sum(xv) == 1 - pv for xv, pv in zip(x_vars, p_vars)) - - mdl.add_constraints(mdl.sum(x_vars[ii][j] * As[ii][j] for ii in c_range) <= bs - for j, bs in enumerate(Bs)) - - # lagrangian relaxation loop - eps = 1e-6 - loop_count = 0 - best = 0 - initial_multiplier = 1 - multipliers = [initial_multiplier] * len(Cs) - - total_profit = mdl.sum(mdl.scal_prod(x_i, c_i) for c_i, x_i in zip(Cs, x_vars)) - mdl.add_kpi(total_profit, "Total profit") - - while loop_count <= max_iters: - loop_count += 1 - # rebuilt at each loop iteration - total_penalty = mdl.scal_prod(p_vars, multipliers) - mdl.maximize(total_profit + total_penalty) - s = mdl.solve() - if not s: - print("*** solve fails, stopping at iteration: %d" % loop_count) - break - best = s.objective_value - penalties = [pv.solution_value for pv in p_vars] - print('%d> new lagrangian iteration:\n\t obj=%g, m=%s, p=%s' % (loop_count, best, str(multipliers), str(penalties))) - - do_stop = True - justifier = 0 - for k in c_range: - penalized_violation = penalties[k] * multipliers[k] - if penalized_violation >= eps: - do_stop = False - justifier = penalized_violation - break - - if do_stop: - print("* Lagrangian relaxation succeeds, best={:g}, penalty={:g}, #iterations={}" - .format(best, total_penalty.solution_value, loop_count)) - break - else: - # update multipliers and start loop again. - scale_factor = 1.0 / float(loop_count) - multipliers = [max(multipliers[i] - scale_factor * penalties[i], 0.) for i in c_range] - print('{0}> -- loop continues, m={1!s}, justifier={2:g}'.format(loop_count, multipliers, justifier)) - - return best - - -def run_default_GAP_model_with_lagrangian_relaxation(**kwargs): - return run_GAP_model_with_Lagrangian_relaxation(As=A, Bs=B, Cs=C, **kwargs) - - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- -if __name__ == '__main__': - # Run the model. If a key has been specified above, the model will run on - # IBM Decision Optimization on cloud. - gap_best_obj = run_GAP_model(A, B, C) - relaxed_best = run_GAP_model_with_Lagrangian_relaxation(A, B, C) - # save the relaxed solution - with get_environment().get_output_stream("solution.json") as fp: - fp.write(json.dumps({"objectiveValue": relaxed_best}).encode('utf-8')) +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + +import json + +from docplex.util.environment import get_environment +from docplex.mp.model import Model + + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- +B = [15, 15, 15] +C = [ + [ 6, 10, 1], + [12, 12, 5], + [15, 4, 3], + [10, 3, 9], + [8, 9, 5] +] +A = [ + [ 5, 7, 2], + [14, 8, 7], + [10, 6, 12], + [ 8, 4, 15], + [ 6, 12, 5] +] + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- +def run_GAP_model(As, Bs, Cs, **kwargs): + with Model('GAP per Wolsey -without- Lagrangian Relaxation', **kwargs) as mdl: + print("#As={}, #Bs={}, #Cs={}".format(len(As), len(Bs), len(Cs))) + number_of_cs = len(C) + # variables + x_vars = [mdl.binary_var_list(c, name=None) for c in Cs] + + # constraints + mdl.add_constraints(mdl.sum(xv) <= 1 for xv in x_vars) + + mdl.add_constraints(mdl.sum(x_vars[ii][j] * As[ii][j] for ii in range(number_of_cs)) <= bs + for j, bs in enumerate(Bs)) + + # objective + total_profit = mdl.sum(mdl.scal_prod(x_i, c_i) for c_i, x_i in zip(Cs, x_vars)) + mdl.maximize(total_profit) + # mdl.print_information() + s = mdl.solve() + assert s is not None + obj = s.objective_value + print("* GAP with no relaxation run OK, best objective is: {:g}".format(obj)) + return obj + + +def run_GAP_model_with_Lagrangian_relaxation(As, Bs, Cs, max_iters=101, **kwargs): + with Model('GAP per Wolsey -with- Lagrangian Relaxation', **kwargs) as mdl: + print("#As={}, #Bs={}, #Cs={}".format(len(As), len(Bs), len(Cs))) + number_of_cs = len(Cs) + c_range = range(number_of_cs) + # variables + x_vars = [mdl.binary_var_list(c, name=None) for c in Cs] + p_vars = mdl.continuous_var_list(Cs, name='p') # new for relaxation + + mdl.add_constraints(mdl.sum(xv) == 1 - pv for xv, pv in zip(x_vars, p_vars)) + + mdl.add_constraints(mdl.sum(x_vars[ii][j] * As[ii][j] for ii in c_range) <= bs + for j, bs in enumerate(Bs)) + + # lagrangian relaxation loop + eps = 1e-6 + loop_count = 0 + best = 0 + initial_multiplier = 1 + multipliers = [initial_multiplier] * len(Cs) + + total_profit = mdl.sum(mdl.scal_prod(x_i, c_i) for c_i, x_i in zip(Cs, x_vars)) + mdl.add_kpi(total_profit, "Total profit") + + while loop_count <= max_iters: + loop_count += 1 + # rebuilt at each loop iteration + total_penalty = mdl.scal_prod(p_vars, multipliers) + mdl.maximize(total_profit + total_penalty) + s = mdl.solve() + if not s: + print("*** solve fails, stopping at iteration: %d" % loop_count) + break + best = s.objective_value + penalties = [pv.solution_value for pv in p_vars] + print('%d> new lagrangian iteration:\n\t obj=%g, m=%s, p=%s' % (loop_count, best, str(multipliers), str(penalties))) + + do_stop = True + justifier = 0 + for k in c_range: + penalized_violation = penalties[k] * multipliers[k] + if penalized_violation >= eps: + do_stop = False + justifier = penalized_violation + break + + if do_stop: + print("* Lagrangian relaxation succeeds, best={:g}, penalty={:g}, #iterations={}" + .format(best, total_penalty.solution_value, loop_count)) + break + else: + # update multipliers and start loop again. + scale_factor = 1.0 / float(loop_count) + multipliers = [max(multipliers[i] - scale_factor * penalties[i], 0.) for i in c_range] + print('{0}> -- loop continues, m={1!s}, justifier={2:g}'.format(loop_count, multipliers, justifier)) + + return best + + +def run_default_GAP_model_with_lagrangian_relaxation(**kwargs): + return run_GAP_model_with_Lagrangian_relaxation(As=A, Bs=B, Cs=C, **kwargs) + + +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- +if __name__ == '__main__': + # Run the model. If a key has been specified above, the model will run on + # IBM Decision Optimization on cloud. + gap_best_obj = run_GAP_model(A, B, C) + relaxed_best = run_GAP_model_with_Lagrangian_relaxation(A, B, C) + # save the relaxed solution + with get_environment().get_output_stream("solution.json") as fp: + fp.write(json.dumps({"objectiveValue": relaxed_best}).encode('utf-8')) diff --git a/examples/mp/workflow/load_balancing.py b/examples/mp/workflow/load_balancing.py index 0c7f03b..08c386c 100644 --- a/examples/mp/workflow/load_balancing.py +++ b/examples/mp/workflow/load_balancing.py @@ -1,246 +1,247 @@ -# -------------------------------------------------------------------------- -# Source file provided under Apache License, Version 2.0, January 2004, -# http://www.apache.org/licenses/ -# (c) Copyright IBM Corp. 2015, 2018 -# -------------------------------------------------------------------------- - -# Source: http://blog.yhathq.com/posts/how-yhat-does-cloud-balancing.html - -from collections import namedtuple - -from docplex.mp.model import Model - - -# ---------------------------------------------------------------------------- -# Initialize the problem data -# ---------------------------------------------------------------------------- -class TUser(namedtuple("TUser", ["id", "running", "sleeping", "current_server"])): - def __str__(self): - return self.id - - -SERVERS = ["server002", "server003", "server001", "server006", "server007", "server004", "server005"] - -USERS = [("user013", 2, 1, "server002"), - ("user014", 0, 2, "server002"), - ("user015", 0, 4, "server002"), - ("user016", 1, 4, "server002"), - ("user017", 0, 3, "server002"), - ("user018", 0, 2, "server002"), - ("user019", 0, 2, "server002"), - ("user020", 0, 1, "server002"), - ("user021", 4, 4, "server002"), - ("user022", 0, 1, "server002"), - ("user023", 0, 3, "server002"), - ("user024", 1, 2, "server002"), - ("user025", 0, 1, "server003"), - ("user026", 0, 1, "server003"), - ("user027", 1, 1, "server003"), - ("user028", 0, 1, "server003"), - ("user029", 2, 1, "server003"), - ("user030", 0, 5, "server003"), - ("user031", 0, 2, "server003"), - ("user032", 0, 3, "server003"), - ("user033", 1, 1, "server003"), - ("user034", 0, 1, "server003"), - ("user035", 0, 1, "server003"), - ("user036", 4, 1, "server003"), - ("user037", 7, 1, "server003"), - ("user038", 2, 1, "server003"), - ("user039", 0, 3, "server003"), - ("user040", 1, 2, "server003"), - ("user001", 0, 2, "server001"), - ("user002", 0, 3, "server001"), - ("user003", 5, 4, "server001"), - ("user004", 0, 1, "server001"), - ("user005", 0, 1, "server001"), - ("user006", 0, 2, "server001"), - ("user007", 0, 4, "server001"), - ("user008", 0, 1, "server001"), - ("user009", 5, 1, "server001"), - ("user010", 7, 1, "server001"), - ("user011", 4, 5, "server001"), - ("user012", 0, 4, "server001"), - ("user062", 0, 1, "server006"), - ("user063", 3, 5, "server006"), - ("user064", 0, 1, "server006"), - ("user065", 0, 3, "server006"), - ("user066", 3, 1, "server006"), - ("user067", 0, 1, "server006"), - ("user068", 0, 1, "server006"), - ("user069", 0, 2, "server006"), - ("user070", 3, 2, "server006"), - ("user071", 0, 1, "server006"), - ("user072", 5, 3, "server006"), - ("user073", 0, 1, "server006"), - ("user074", 0, 1, "server006"), - ("user075", 0, 2, "server007"), - ("user076", 1, 1, "server007"), - ("user077", 1, 1, "server007"), - ("user078", 0, 1, "server007"), - ("user079", 0, 3, "server007"), - ("user080", 0, 1, "server007"), - ("user081", 4, 1, "server007"), - ("user082", 1, 1, "server007"), - ("user041", 0, 1, "server004"), - ("user042", 2, 1, "server004"), - ("user043", 5, 2, "server004"), - ("user044", 5, 2, "server004"), - ("user045", 0, 2, "server004"), - ("user046", 1, 5, "server004"), - ("user047", 0, 1, "server004"), - ("user048", 0, 3, "server004"), - ("user049", 5, 1, "server004"), - ("user050", 0, 2, "server004"), - ("user051", 0, 3, "server004"), - ("user052", 0, 3, "server004"), - ("user053", 0, 1, "server004"), - ("user054", 0, 2, "server004"), - ("user055", 0, 3, "server005"), - ("user056", 3, 1, "server005"), - ("user057", 0, 3, "server005"), - ("user058", 0, 2, "server005"), - ("user059", 0, 1, "server005"), - ("user060", 0, 5, "server005"), - ("user061", 0, 2, "server005") - ] - -# ---------------------------------------------------------------------------- -# Prepare the data for modeling -# ---------------------------------------------------------------------------- -DEFAULT_MAX_PROCESSES_PER_SERVER = 50 - - -def _is_migration(user, server): - """ Returns True if server is not the user's current - Used in setup of constraints. - """ - return server != user.current_server - - -# ---------------------------------------------------------------------------- -# Build the model -# ---------------------------------------------------------------------------- - -def build_load_balancing_model(servers, users_, max_process_per_server=DEFAULT_MAX_PROCESSES_PER_SERVER, **kwargs): - m = Model(name='load_balancing', **kwargs) - - # decision objects - - users = [TUser(*user_row) for user_row in users_] - - active_var_by_server = m.binary_var_dict(servers, name='isActive') - - def user_server_pair_namer(u_s): - u, s = u_s - return '%s_to_%s' % (u.id, s) - - assign_user_to_server_vars = m.binary_var_matrix(users, servers, user_server_pair_namer) - - m.add_constraints( - m.sum(assign_user_to_server_vars[u, s] * u.running for u in users) <= max_process_per_server for s in servers) - # each assignment var is <= active_server(s) - for s in servers: - for u in users: - ct_name = 'ct_assign_to_active_{0!s}_{1!s}'.format(u, s) - m.add_constraint(assign_user_to_server_vars[u, s] <= active_var_by_server[s], ct_name) - - # sum of assignment vars for (u, all s in servers) == 1 - for u in users: - ct_name = 'ct_unique_server_%s' % (u[0]) - m.add_constraint(m.sum((assign_user_to_server_vars[u, s] for s in servers)) == 1, ct_name) - - number_of_active_servers = m.sum((active_var_by_server[svr] for svr in servers)) - m.add_kpi(number_of_active_servers, "Number of active servers") - - number_of_migrations = m.sum( - assign_user_to_server_vars[u, s] for u in users for s in servers if - _is_migration(u, s)) - m.add_kpi(number_of_migrations, "Total number of migrations") - - max_sleeping_workload = m.integer_var(name="max_sleeping_processes") - for s in servers: - ct_name = 'ct_define_max_sleeping_%s' % s - m.add_constraint( - m.sum( - assign_user_to_server_vars[u, s] * u.sleeping for u in users) <= max_sleeping_workload, - ct_name) - m.add_kpi(max_sleeping_workload, "Max sleeping workload") - # Set objective function - # m.minimize(number_of_active_servers) - m.minimize_static_lex([number_of_active_servers, number_of_migrations, max_sleeping_workload]) - - # attach artefacts to model for reporting - m.users = users - m.servers = servers - m.active_var_by_server = active_var_by_server - m.assign_user_to_server_vars = assign_user_to_server_vars - m.max_sleeping_workload = max_sleeping_workload - - return m - - -def lb_report(mdl): - active_servers = sorted([s for s in mdl.servers if mdl.active_var_by_server[s].solution_value == 1]) - print("Active Servers: {0} = {1}".format(len(active_servers), active_servers)) - print("*** User/server assignments , #migrations={0} ***".format( - mdl.kpi_by_name("number of migrations").solution_value)) - # for (u, s) in sorted(mdl.assign_user_to_server_vars): - # if mdl.assign_user_to_server_vars[(u, s)].solution_value == 1: - # print("{} uses {}, migration: {}".format(u, s, "yes" if _is_migration(u, s) else "no")) - print("*** Servers sleeping processes ***") - for s in active_servers: - sleeping = sum(mdl.assign_user_to_server_vars[u, s].solution_value * u.sleeping for u in mdl.users) - print("Server: {} #sleeping={}".format(s, sleeping)) - - -def make_default_load_balancing_model(**kwargs): - return build_load_balancing_model(SERVERS, USERS, **kwargs) - - -def lb_save_solution_as_json(mdl, json_file): - """Saves the solution for this model as JSON. - - Note that this is not a CPLEX Solution file, as this is the result of post-processing a CPLEX solution - """ - import json - solution_dict = {} - # active server - active_servers = sorted([s for s in mdl.servers if mdl.active_var_by_server[s].solution_value == 1]) - solution_dict["active servers"] = active_servers - - # sleeping processes by server - sleeping_processes = {} - for s in active_servers: - sleeping = sum(mdl.assign_user_to_server_vars[u, s].solution_value * u.sleeping for u in mdl.users) - sleeping_processes[s] = sleeping - solution_dict["sleeping processes by server"] = sleeping_processes - -# user assignment - user_assignment = [] - for (u, s) in sorted(mdl.assign_user_to_server_vars): - if mdl.assign_user_to_server_vars[(u, s)].solution_value == 1: - n = { - 'user': u.id, - 'server': s, - 'migration': "yes" if _is_migration(u, s) else "no" - } - user_assignment.append(n) - solution_dict['user assignment'] = user_assignment - json_file.write(json.dumps(solution_dict, indent=3).encode('utf-8')) - -# ---------------------------------------------------------------------------- -# Solve the model and display the result -# ---------------------------------------------------------------------------- - -if __name__ == '__main__': - lbm = make_default_load_balancing_model() - - # Run the model. - lbs = lbm.solve(log_output=True) - lb_report(lbm) - # save json, used in worker tests - from docplex.util.environment import get_environment - with get_environment().get_output_stream("solution.json") as fp: - lb_save_solution_as_json(lbm, fp) - +# -------------------------------------------------------------------------- +# Source file provided under Apache License, Version 2.0, January 2004, +# http://www.apache.org/licenses/ +# (c) Copyright IBM Corp. 2015, 2018 +# -------------------------------------------------------------------------- + +# Source: http://blog.yhathq.com/posts/how-yhat-does-cloud-balancing.html + +from collections import namedtuple + +from docplex.mp.model import Model + + +# ---------------------------------------------------------------------------- +# Initialize the problem data +# ---------------------------------------------------------------------------- +class TUser(namedtuple("TUser", ["id", "running", "sleeping", "current_server"])): + def __str__(self): + return self.id + + +SERVERS = ["server002", "server003", "server001", "server006", "server007", "server004", "server005"] + +USERS = [("user013", 2, 1, "server002"), + ("user014", 0, 2, "server002"), + ("user015", 0, 4, "server002"), + ("user016", 1, 4, "server002"), + ("user017", 0, 3, "server002"), + ("user018", 0, 2, "server002"), + ("user019", 0, 2, "server002"), + ("user020", 0, 1, "server002"), + ("user021", 4, 4, "server002"), + ("user022", 0, 1, "server002"), + ("user023", 0, 3, "server002"), + ("user024", 1, 2, "server002"), + ("user025", 0, 1, "server003"), + ("user026", 0, 1, "server003"), + ("user027", 1, 1, "server003"), + ("user028", 0, 1, "server003"), + ("user029", 2, 1, "server003"), + ("user030", 0, 5, "server003"), + ("user031", 0, 2, "server003"), + ("user032", 0, 3, "server003"), + ("user033", 1, 1, "server003"), + ("user034", 0, 1, "server003"), + ("user035", 0, 1, "server003"), + ("user036", 4, 1, "server003"), + ("user037", 7, 1, "server003"), + ("user038", 2, 1, "server003"), + ("user039", 0, 3, "server003"), + ("user040", 1, 2, "server003"), + ("user001", 0, 2, "server001"), + ("user002", 0, 3, "server001"), + ("user003", 5, 4, "server001"), + ("user004", 0, 1, "server001"), + ("user005", 0, 1, "server001"), + ("user006", 0, 2, "server001"), + ("user007", 0, 4, "server001"), + ("user008", 0, 1, "server001"), + ("user009", 5, 1, "server001"), + ("user010", 7, 1, "server001"), + ("user011", 4, 5, "server001"), + ("user012", 0, 4, "server001"), + ("user062", 0, 1, "server006"), + ("user063", 3, 5, "server006"), + ("user064", 0, 1, "server006"), + ("user065", 0, 3, "server006"), + ("user066", 3, 1, "server006"), + ("user067", 0, 1, "server006"), + ("user068", 0, 1, "server006"), + ("user069", 0, 2, "server006"), + ("user070", 3, 2, "server006"), + ("user071", 0, 1, "server006"), + ("user072", 5, 3, "server006"), + ("user073", 0, 1, "server006"), + ("user074", 0, 1, "server006"), + ("user075", 0, 2, "server007"), + ("user076", 1, 1, "server007"), + ("user077", 1, 1, "server007"), + ("user078", 0, 1, "server007"), + ("user079", 0, 3, "server007"), + ("user080", 0, 1, "server007"), + ("user081", 4, 1, "server007"), + ("user082", 1, 1, "server007"), + ("user041", 0, 1, "server004"), + ("user042", 2, 1, "server004"), + ("user043", 5, 2, "server004"), + ("user044", 5, 2, "server004"), + ("user045", 0, 2, "server004"), + ("user046", 1, 5, "server004"), + ("user047", 0, 1, "server004"), + ("user048", 0, 3, "server004"), + ("user049", 5, 1, "server004"), + ("user050", 0, 2, "server004"), + ("user051", 0, 3, "server004"), + ("user052", 0, 3, "server004"), + ("user053", 0, 1, "server004"), + ("user054", 0, 2, "server004"), + ("user055", 0, 3, "server005"), + ("user056", 3, 1, "server005"), + ("user057", 0, 3, "server005"), + ("user058", 0, 2, "server005"), + ("user059", 0, 1, "server005"), + ("user060", 0, 5, "server005"), + ("user061", 0, 2, "server005") + ] + +# ---------------------------------------------------------------------------- +# Prepare the data for modeling +# ---------------------------------------------------------------------------- +DEFAULT_MAX_PROCESSES_PER_SERVER = 50 + + +def _is_migration(user, server): + """ Returns True if server is not the user's current + Used in setup of constraints. + """ + return server != user.current_server + + +# ---------------------------------------------------------------------------- +# Build the model +# ---------------------------------------------------------------------------- + +def build_load_balancing_model(servers, users_, max_process_per_server=DEFAULT_MAX_PROCESSES_PER_SERVER, **kwargs): + m = Model(name='load_balancing', **kwargs) + + # decision objects + + users = [TUser(*user_row) for user_row in users_] + + active_var_by_server = m.binary_var_dict(servers, name='isActive') + + def user_server_pair_namer(u_s): + u, s = u_s + return '%s_to_%s' % (u.id, s) + + assign_user_to_server_vars = m.binary_var_matrix(users, servers, user_server_pair_namer) + + m.add_constraints( + m.sum(assign_user_to_server_vars[u, s] * u.running for u in users) <= max_process_per_server for s in servers) + # each assignment var is <= active_server(s) + for s in servers: + for u in users: + ct_name = 'ct_assign_to_active_{0!s}_{1!s}'.format(u, s) + m.add_constraint(assign_user_to_server_vars[u, s] <= active_var_by_server[s], ct_name) + + # sum of assignment vars for (u, all s in servers) == 1 + for u in users: + ct_name = 'ct_unique_server_%s' % (u[0]) + m.add_constraint(m.sum((assign_user_to_server_vars[u, s] for s in servers)) == 1, ct_name) + + number_of_active_servers = m.sum((active_var_by_server[svr] for svr in servers)) + m.add_kpi(number_of_active_servers, "Number of active servers") + + number_of_migrations = m.sum( + assign_user_to_server_vars[u, s] for u in users for s in servers if + _is_migration(u, s)) + m.add_kpi(number_of_migrations, "Total number of migrations") + + max_sleeping_workload = m.integer_var(name="max_sleeping_processes") + for s in servers: + ct_name = 'ct_define_max_sleeping_%s' % s + m.add_constraint( + m.sum( + assign_user_to_server_vars[u, s] * u.sleeping for u in users) <= max_sleeping_workload, + ct_name) + m.add_kpi(max_sleeping_workload, "Max sleeping workload") + # Set objective function + # m.minimize(number_of_active_servers) + m.minimize_static_lex([number_of_active_servers, number_of_migrations, max_sleeping_workload]) + + # attach artefacts to model for reporting + m.users = users + m.servers = servers + m.active_var_by_server = active_var_by_server + m.assign_user_to_server_vars = assign_user_to_server_vars + m.max_sleeping_workload = max_sleeping_workload + + return m + + +def lb_report(mdl): + active_servers = sorted([s for s in mdl.servers if mdl.active_var_by_server[s].solution_value == 1]) + print("Active Servers: {0} = {1}".format(len(active_servers), active_servers)) + print("*** User/server assignments , #migrations={0} ***".format( + mdl.kpi_by_name("number of migrations").solution_value)) + # for (u, s) in sorted(mdl.assign_user_to_server_vars): + # if mdl.assign_user_to_server_vars[(u, s)].solution_value == 1: + # print("{} uses {}, migration: {}".format(u, s, "yes" if _is_migration(u, s) else "no")) + print("*** Servers sleeping processes ***") + for s in active_servers: + sleeping = sum(mdl.assign_user_to_server_vars[u, s].solution_value * u.sleeping for u in mdl.users) + print("Server: {} #sleeping={}".format(s, sleeping)) + + +def make_default_load_balancing_model(**kwargs): + return build_load_balancing_model(SERVERS, USERS, **kwargs) + + +def lb_save_solution_as_json(mdl, json_file): + """Saves the solution for this model as JSON. + + Note that this is not a CPLEX Solution file, as this is the result of post-processing a CPLEX solution + """ + import json + solution_dict = {} + # active server + active_servers = sorted([s for s in mdl.servers if mdl.active_var_by_server[s].solution_value == 1]) + solution_dict["active servers"] = active_servers + + # sleeping processes by server + sleeping_processes = {} + for s in active_servers: + sleeping = sum(mdl.assign_user_to_server_vars[u, s].solution_value * u.sleeping for u in mdl.users) + sleeping_processes[s] = sleeping + solution_dict["sleeping processes by server"] = sleeping_processes + +# user assignment + user_assignment = [] + for (u, s) in sorted(mdl.assign_user_to_server_vars): + if mdl.assign_user_to_server_vars[(u, s)].solution_value == 1: + n = { + 'user': u.id, + 'server': s, + 'migration': "yes" if _is_migration(u, s) else "no" + } + user_assignment.append(n) + solution_dict['user assignment'] = user_assignment + json_file.write(json.dumps(solution_dict, indent=3).encode('utf-8')) + +# ---------------------------------------------------------------------------- +# Solve the model and display the result +# ---------------------------------------------------------------------------- + +if __name__ == '__main__': + lbm = make_default_load_balancing_model() + + # Run the model. + lbs = lbm.solve(log_output=True) + lb_report(lbm) + # save json, used in worker tests + from docplex.util.environment import get_environment + with get_environment().get_output_stream("solution.json") as fp: + lb_save_solution_as_json(lbm, fp) + lbm.end() + diff --git a/examples/mp/workflow/populate.py b/examples/mp/workflow/populate.py index 54e9a11..1a7c094 100644 --- a/examples/mp/workflow/populate.py +++ b/examples/mp/workflow/populate.py @@ -56,7 +56,7 @@ def populate_from_model(mdl, :param eps_diff: precision to use for testing variable difference :param verbose: optional flag to print results. - :return: the solution pool as returned by `docplex.mp.Model.populate()` + :return: the model and the solution pool as returned by `docplex.mp.Model.populate()` """ print(f"* running populate on model: '{mdl.name}', gap={gap}, intensity={pool_intensity}, capacity={pool_capacity}") # set the solution pool relative gap parameter to obtain solutions @@ -113,7 +113,7 @@ def populate_from_model(mdl, numdiff += 1 print("%-15s %-10g %02d / %d" % (s, objval_i, numdiff, numcols)) - return solnpool + return mdl, solnpool if __name__ == "__main__": @@ -124,7 +124,8 @@ def populate_from_model(mdl, filename = join(dirname(abspath(__file__)), "sports.lp") else: filename = sys.argv[1] - populate_from_file(filename) + mdl, sol_pool = populate_from_file(filename) + mdl.end() # * building sport scheduling model instance