nurses_multiobj.py¶
This example solves the problem of finding an optimal assignment of nurses to shifts, using multi-objectives. Instead of minimizing an overall cost made of salary cost, fairness and number of assignments, we use COS 12.9 multiobjective solve to specify the 3 kpis.
This sample require COS 12.9.
1# --------------------------------------------------------------------------
2# Source file provided under Apache License, Version 2.0, January 2004,
3# http://www.apache.org/licenses/
4# (c) Copyright IBM Corp. 2015, 2018
5# --------------------------------------------------------------------------
6
7from collections import namedtuple
8
9from docplex.mp.model import Model
10from docplex.mp.constants import ObjectiveSense
11from docplex.util.environment import get_environment
12
13# ----------------------------------------------------------------------------
14# Initialize the problem data
15# ----------------------------------------------------------------------------
16
17# utility to convert a weekday string to an index in 0..6
18_all_days = ["monday",
19 "tuesday",
20 "wednesday",
21 "thursday",
22 "friday",
23 "saturday",
24 "sunday"]
25
26
27def day_to_day_week(day):
28 day_map = {day: d for d, day in enumerate(_all_days)}
29 return day_map[day.lower()]
30
31
32TWorkRules = namedtuple("TWorkRules", ["work_time_max"])
33TVacation = namedtuple("TVacation", ["nurse", "day"])
34TNursePair = namedtuple("TNursePair", ["firstNurse", "secondNurse"])
35TSkillRequirement = namedtuple("TSkillRequirement", ["department", "skill", "required"])
36
37
38NURSES = [("Anne", 11, 1, 25),
39 ("Bethanie", 4, 5, 28),
40 ("Betsy", 2, 2, 17),
41 ("Cathy", 2, 2, 17),
42 ("Cecilia", 9, 5, 38),
43 ("Chris", 11, 4, 38),
44 ("Cindy", 5, 2, 21),
45 ("David", 1, 2, 15),
46 ("Debbie", 7, 2, 24),
47 ("Dee", 3, 3, 21),
48 ("Gloria", 8, 2, 25),
49 ("Isabelle", 3, 1, 16),
50 ("Jane", 3, 4, 23),
51 ("Janelle", 4, 3, 22),
52 ("Janice", 2, 2, 17),
53 ("Jemma", 2, 4, 22),
54 ("Joan", 5, 3, 24),
55 ("Joyce", 8, 3, 29),
56 ("Jude", 4, 3, 22),
57 ("Julie", 6, 2, 22),
58 ("Juliet", 7, 4, 31),
59 ("Kate", 5, 3, 24),
60 ("Nancy", 8, 4, 32),
61 ("Nathalie", 9, 5, 38),
62 ("Nicole", 0, 2, 14),
63 ("Patricia", 1, 1, 13),
64 ("Patrick", 6, 1, 19),
65 ("Roberta", 3, 5, 26),
66 ("Suzanne", 5, 1, 18),
67 ("Vickie", 7, 1, 20),
68 ("Wendie", 5, 2, 21),
69 ("Zoe", 8, 3, 29)
70 ]
71
72SHIFTS = [("Emergency", "monday", 2, 8, 3, 5),
73 ("Emergency", "monday", 8, 12, 4, 7),
74 ("Emergency", "monday", 12, 18, 2, 5),
75 ("Emergency", "monday", 18, 2, 3, 7),
76 ("Consultation", "monday", 8, 12, 10, 13),
77 ("Consultation", "monday", 12, 18, 8, 12),
78 ("Cardiac_Care", "monday", 8, 12, 10, 13),
79 ("Cardiac_Care", "monday", 12, 18, 8, 12),
80 ("Emergency", "tuesday", 8, 12, 4, 7),
81 ("Emergency", "tuesday", 12, 18, 2, 5),
82 ("Emergency", "tuesday", 18, 2, 3, 7),
83 ("Consultation", "tuesday", 8, 12, 10, 13),
84 ("Consultation", "tuesday", 12, 18, 8, 12),
85 ("Cardiac_Care", "tuesday", 8, 12, 4, 7),
86 ("Cardiac_Care", "tuesday", 12, 18, 2, 5),
87 ("Cardiac_Care", "tuesday", 18, 2, 3, 7),
88 ("Emergency", "wednesday", 2, 8, 3, 5),
89 ("Emergency", "wednesday", 8, 12, 4, 7),
90 ("Emergency", "wednesday", 12, 18, 2, 5),
91 ("Emergency", "wednesday", 18, 2, 3, 7),
92 ("Consultation", "wednesday", 8, 12, 10, 13),
93 ("Consultation", "wednesday", 12, 18, 8, 12),
94 ("Emergency", "thursday", 2, 8, 3, 5),
95 ("Emergency", "thursday", 8, 12, 4, 7),
96 ("Emergency", "thursday", 12, 18, 2, 5),
97 ("Emergency", "thursday", 18, 2, 3, 7),
98 ("Consultation", "thursday", 8, 12, 10, 13),
99 ("Consultation", "thursday", 12, 18, 8, 12),
100 ("Emergency", "friday", 2, 8, 3, 5),
101 ("Emergency", "friday", 8, 12, 4, 7),
102 ("Emergency", "friday", 12, 18, 2, 5),
103 ("Emergency", "friday", 18, 2, 3, 7),
104 ("Consultation", "friday", 8, 12, 10, 13),
105 ("Consultation", "friday", 12, 18, 8, 12),
106 ("Emergency", "saturday", 2, 12, 5, 7),
107 ("Emergency", "saturday", 12, 20, 7, 9),
108 ("Emergency", "saturday", 20, 2, 12, 12),
109 ("Emergency", "sunday", 2, 12, 5, 7),
110 ("Emergency", "sunday", 12, 20, 7, 9),
111 ("Emergency", "sunday", 20, 2, 12, 12),
112 ("Geriatrics", "sunday", 8, 10, 2, 5)]
113
114NURSE_SKILLS = {"Anne": ["Anaesthesiology", "Oncology", "Pediatrics"],
115 "Betsy": ["Cardiac_Care"],
116 "Cathy": ["Anaesthesiology"],
117 "Cecilia": ["Anaesthesiology", "Oncology", "Pediatrics"],
118 "Chris": ["Cardiac_Care", "Oncology", "Geriatrics"],
119 "Gloria": ["Pediatrics"], "Jemma": ["Cardiac_Care"],
120 "Joyce": ["Anaesthesiology", "Pediatrics"],
121 "Julie": ["Geriatrics"], "Juliet": ["Pediatrics"],
122 "Kate": ["Pediatrics"], "Nancy": ["Cardiac_Care"],
123 "Nathalie": ["Anaesthesiology", "Geriatrics"],
124 "Patrick": ["Oncology"], "Suzanne": ["Pediatrics"],
125 "Wendie": ["Geriatrics"],
126 "Zoe": ["Cardiac_Care"]
127 }
128
129VACATIONS = [("Anne", "friday"),
130 ("Anne", "sunday"),
131 ("Cathy", "thursday"),
132 ("Cathy", "tuesday"),
133 ("Joan", "thursday"),
134 ("Joan", "saturday"),
135 ("Juliet", "monday"),
136 ("Juliet", "tuesday"),
137 ("Juliet", "thursday"),
138 ("Nathalie", "sunday"),
139 ("Nathalie", "thursday"),
140 ("Isabelle", "monday"),
141 ("Isabelle", "thursday"),
142 ("Patricia", "saturday"),
143 ("Patricia", "wednesday"),
144 ("Nicole", "friday"),
145 ("Nicole", "wednesday"),
146 ("Jude", "tuesday"),
147 ("Jude", "friday"),
148 ("Debbie", "saturday"),
149 ("Debbie", "wednesday"),
150 ("Joyce", "sunday"),
151 ("Joyce", "thursday"),
152 ("Chris", "thursday"),
153 ("Chris", "tuesday"),
154 ("Cecilia", "friday"),
155 ("Cecilia", "wednesday"),
156 ("Patrick", "saturday"),
157 ("Patrick", "sunday"),
158 ("Cindy", "sunday"),
159 ("Dee", "tuesday"),
160 ("Dee", "friday"),
161 ("Jemma", "friday"),
162 ("Jemma", "wednesday"),
163 ("Bethanie", "wednesday"),
164 ("Bethanie", "tuesday"),
165 ("Betsy", "monday"),
166 ("Betsy", "thursday"),
167 ("David", "monday"),
168 ("Gloria", "monday"),
169 ("Jane", "saturday"),
170 ("Jane", "sunday"),
171 ("Janelle", "wednesday"),
172 ("Janelle", "friday"),
173 ("Julie", "sunday"),
174 ("Kate", "tuesday"),
175 ("Kate", "monday"),
176 ("Nancy", "sunday"),
177 ("Roberta", "friday"),
178 ("Roberta", "saturday"),
179 ("Janice", "tuesday"),
180 ("Janice", "friday"),
181 ("Suzanne", "monday"),
182 ("Vickie", "wednesday"),
183 ("Vickie", "friday"),
184 ("Wendie", "thursday"),
185 ("Wendie", "saturday"),
186 ("Zoe", "saturday"),
187 ("Zoe", "sunday")]
188
189NURSE_ASSOCIATIONS = [("Isabelle", "Dee"),
190 ("Anne", "Patrick")]
191
192NURSE_INCOMPATIBILITIES = [("Patricia", "Patrick"),
193 ("Janice", "Wendie"),
194 ("Suzanne", "Betsy"),
195 ("Janelle", "Jane"),
196 ("Gloria", "David"),
197 ("Dee", "Jemma"),
198 ("Bethanie", "Dee"),
199 ("Roberta", "Zoe"),
200 ("Nicole", "Patricia"),
201 ("Vickie", "Dee"),
202 ("Joan", "Anne")
203 ]
204
205SKILL_REQUIREMENTS = [("Emergency", "Cardiac_Care", 1)]
206
207DEFAULT_WORK_RULES = TWorkRules(40)
208
209
210# ----------------------------------------------------------------------------
211# Prepare the data for modeling
212# ----------------------------------------------------------------------------
213# subclass the namedtuple to refine the str() method as the nurse's name
214class TNurse(namedtuple("TNurse1", ["name", "seniority", "qualification", "pay_rate"])):
215 def __str__(self):
216 return self.name
217
218
219# specialized namedtuple to redefine its str() method
220class TShift(namedtuple("TShift",
221 ["department", "day", "start_time", "end_time", "min_requirement", "max_requirement"])):
222
223 def __str__(self):
224 # keep first two characters in department, uppercase
225 dept2 = self.department[0:4].upper()
226 # keep 3 days of weekday
227 dayname = self.day[0:3]
228 return '{}_{}_{:02d}'.format(dept2, dayname, self.start_time).replace(" ", "_")
229
230
231class ShiftActivity(object):
232 @staticmethod
233 def to_abstime(day_index, time_of_day):
234 """ Convert a pair (day_index, time) into a number of hours since Monday 00:00
235
236 :param day_index: The index of the day from 1 to 7 (Monday is 1).
237 :param time_of_day: An integer number of hours.
238
239 :return:
240 """
241 time = 24 * (day_index - 1)
242 time += time_of_day
243 return time
244
245 def __init__(self, weekday, start_time_of_day, end_time_of_day):
246 assert (start_time_of_day >= 0)
247 assert (start_time_of_day <= 24)
248 assert (end_time_of_day >= 0)
249 assert (end_time_of_day <= 24)
250
251 self._weekday = weekday
252 self._start_time_of_day = start_time_of_day
253 self._end_time_of_day = end_time_of_day
254 # conversion to absolute time.
255 start_day_index = day_to_day_week(self._weekday)
256 self.start_time = self.to_abstime(start_day_index, start_time_of_day)
257 end_day_index = start_day_index if end_time_of_day > start_time_of_day else start_day_index + 1
258 self.end_time = self.to_abstime(end_day_index, end_time_of_day)
259 assert self.end_time > self.start_time
260
261 @property
262 def duration(self):
263 return self.end_time - self.start_time
264
265 def overlaps(self, other_shift):
266 if not isinstance(other_shift, ShiftActivity):
267 return False
268 else:
269 return other_shift.end_time > self.start_time and other_shift.start_time < self.end_time
270
271
272def solve(model, **kwargs):
273 # Here, we set the number of threads for CPLEX to 2 and set the time limit to 2mins.
274 if kwargs.pop('parameter_sets', None) == None:
275 model.parameters.threads = 2
276 model.parameters.mip.tolerances.mipgap = 0.000001
277 model.parameters.timelimit = 120 # nurse should not take more than that !
278 sol = model.solve(log_output=True, **kwargs)
279 if sol is not None:
280 print("solution for a cost of {}".format(model.objective_value))
281 print_information(model)
282 # print_solution(model)
283 return model.objective_value
284 else:
285 print("* model is infeasible")
286 return None
287
288
289def load_data(model, shifts_, nurses_, nurse_skills, vacations_=None,
290 nurse_associations_=None, nurse_imcompatibilities_=None):
291 """ Usage: load_data(shifts, nurses, nurse_skills, vacations) """
292 model.number_of_overlaps = 0
293 model.work_rules = DEFAULT_WORK_RULES
294 model.shifts = [TShift(*shift_row) for shift_row in shifts_]
295 model.nurses = [TNurse(*nurse_row) for nurse_row in nurses_]
296 model.skill_requirements = SKILL_REQUIREMENTS
297 model.nurse_skills = nurse_skills
298 # transactional data
299 model.vacations = [TVacation(*vacation_row) for vacation_row in vacations_] if vacations_ else []
300 model.nurse_associations = [TNursePair(*npr) for npr in nurse_associations_]\
301 if nurse_associations_ else []
302 model.nurse_incompatibilities = [TNursePair(*npr) for npr in nurse_imcompatibilities_]\
303 if nurse_imcompatibilities_ else []
304
305 # computed
306 model.departments = set(sh.department for sh in model.shifts)
307
308
309 print('#nurses: {0}'.format(len(model.nurses)))
310 print('#shifts: {0}'.format(len(model.shifts)))
311 print('#vacations: {0}'.format(len(model.vacations)))
312 print("#associations=%d" % len(model.nurse_associations))
313 print("#incompatibilities=%d" % len(model.nurse_incompatibilities))
314
315
316def setup_data(model):
317 """ compute internal data """
318 # compute shift activities (start, end duration) and stor ethem in a dict indexed by shifts
319 model.shift_activities = {s: ShiftActivity(s.day, s.start_time, s.end_time) for s in model.shifts}
320 # map from nurse names to nurse tuples.
321 model.nurses_by_id = {n.name: n for n in model.nurses}
322
323
324def setup_variables(model):
325 all_nurses, all_shifts = model.nurses, model.shifts
326 # one binary variable for each pair (nurse, shift) equal to 1 iff nurse n is assigned to shift s
327 model.nurse_assignment_vars = model.binary_var_matrix(all_nurses, all_shifts, 'NurseAssigned')
328 # for each nurse, allocate one variable for work time
329 model.nurse_work_time_vars = model.continuous_var_dict(all_nurses, lb=0, name='NurseWorkTime')
330 # and two variables for over_average and under-average work time
331 model.nurse_over_average_time_vars = model.continuous_var_dict(all_nurses, lb=0,
332 name='NurseOverAverageWorkTime')
333 model.nurse_under_average_time_vars = model.continuous_var_dict(all_nurses, lb=0,
334 name='NurseUnderAverageWorkTime')
335 # finally the global average work time
336 model.average_nurse_work_time = model.continuous_var(lb=0, name='AverageWorkTime')
337
338
339def setup_constraints(model):
340 all_nurses = model.nurses
341 all_shifts = model.shifts
342 nurse_assigned = model.nurse_assignment_vars
343 nurse_work_time = model.nurse_work_time_vars
344 shift_activities = model.shift_activities
345 nurses_by_id = model.nurses_by_id
346 max_work_time = model.work_rules.work_time_max
347
348 # define average
349 model.add_constraint(
350 len(all_nurses) * model.average_nurse_work_time == model.sum(nurse_work_time[n] for n in all_nurses), "average")
351
352 # compute nurse work time , average and under, over
353 for n in all_nurses:
354 work_time_var = nurse_work_time[n]
355 model.add_constraint(
356 work_time_var == model.sum(nurse_assigned[n, s] * shift_activities[s].duration for s in all_shifts),
357 "work_time_{0!s}".format(n))
358
359 # relate over/under average worktime variables to the worktime variables
360 # the trick here is that variables have zero lower bound
361 # however, thse variables are not completely defined by this constraint,
362 # only their difference is.
363 # if these variables are part of the objective, CPLEX wil naturally minimize their value,
364 # as expected
365 model.add_constraint(
366 work_time_var == model.average_nurse_work_time
367 + model.nurse_over_average_time_vars[n]
368 - model.nurse_under_average_time_vars[n],
369 "average_work_time_{0!s}".format(n))
370
371 # state the maximum work time as a constraint, so that is can be relaxed,
372 # should the problem become infeasible.
373 model.add_constraint(work_time_var <= max_work_time, "max_time_{0!s}".format(n))
374
375 # vacations
376 v = 0
377 for vac_nurse_id, vac_day in model.vacations:
378 vac_n = nurses_by_id[vac_nurse_id]
379 for shift in (s for s in all_shifts if s.day == vac_day):
380 v += 1
381 model.add_constraint(nurse_assigned[vac_n, shift] == 0,
382 "medium_vacations_{0!s}_{1!s}_{2!s}".format(vac_n, vac_day, shift))
383 #print('#vacation cts: {0}'.format(v))
384
385 # a nurse cannot be assigned overlapping shifts
386 # post only one constraint per couple(s1, s2)
387 number_of_overlaps = 0
388 nb_shifts = len(all_shifts)
389 for i1 in range(nb_shifts):
390 for i2 in range(i1 + 1, nb_shifts):
391 s1 = all_shifts[i1]
392 s2 = all_shifts[i2]
393 if shift_activities[s1].overlaps(shift_activities[s2]):
394 number_of_overlaps += 1
395 for n in all_nurses:
396 model.add_constraint(nurse_assigned[n, s1] + nurse_assigned[n, s2] <= 1,
397 "high_overlapping_{0!s}_{1!s}_{2!s}".format(s1, s2, n))
398 #print('# overlapping cts: {0}'.format(number_of_overlaps))
399
400 for s in all_shifts:
401 demand_min = s.min_requirement
402 demand_max = s.max_requirement
403 total_assigned = model.sum(nurse_assigned[n, s] for n in model.nurses)
404 model.add_constraint(total_assigned >= demand_min,
405 "high_req_min_{0!s}_{1}".format(s, demand_min))
406 model.add_constraint(total_assigned <= demand_max,
407 "medium_req_max_{0!s}_{1}".format(s, demand_max))
408
409 for (dept, skill, required) in model.skill_requirements:
410 if required > 0:
411 for dsh in (s for s in all_shifts if dept == s.department):
412 model.add_constraint(model.sum(nurse_assigned[skilled_nurse, dsh] for skilled_nurse in
413 (n for n in all_nurses if
414 n.name in model.nurse_skills.keys() and skill in model.nurse_skills[
415 n.name])) >= required,
416 "high_required_{0!s}_{1!s}_{2!s}_{3!s}".format(dept, skill, required, dsh))
417
418 # nurse-nurse associations
419 # for each pair of associated nurses, their assignment variables are equal
420 # over all shifts.
421 c = 0
422 for (nurse_id1, nurse_id2) in model.nurse_associations:
423 if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id:
424 nurse1 = nurses_by_id[nurse_id1]
425 nurse2 = nurses_by_id[nurse_id2]
426 for s in all_shifts:
427 c += 1
428 ctname = 'medium_ct_nurse_assoc_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c)
429 model.add_constraint(nurse_assigned[nurse1, s] == nurse_assigned[nurse2, s], ctname)
430
431 # nurse-nurse incompatibilities
432 # for each pair of incompatible nurses, the sum of assigned variables is less than one
433 # in other terms, both nurses can never be assigned to the same shift
434 c = 0
435 for (nurse_id1, nurse_id2) in model.nurse_incompatibilities:
436 if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id:
437 nurse1 = nurses_by_id[nurse_id1]
438 nurse2 = nurses_by_id[nurse_id2]
439 for s in all_shifts:
440 c += 1
441 ctname = 'medium_ct_nurse_incompat_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c)
442 model.add_constraint(nurse_assigned[nurse1, s] + nurse_assigned[nurse2, s] <= 1, ctname)
443
444 model.total_number_of_assignments = model.sum(nurse_assigned[n, s] for n in all_nurses for s in all_shifts)
445 model.nurse_costs = [model.nurse_assignment_vars[n, s] * n.pay_rate * model.shift_activities[s].duration for n in
446 model.nurses
447 for s in model.shifts]
448 model.total_salary_cost = model.sum(model.nurse_costs)
449
450
451def setup_objective(model):
452 model.add_kpi(model.total_salary_cost, "Total salary cost")
453 model.add_kpi(model.total_number_of_assignments, "Total number of assignments")
454 model.add_kpi(model.average_nurse_work_time, "average work time")
455
456 total_over_average_worktime = model.sum(model.nurse_over_average_time_vars[n] for n in model.nurses)
457 total_under_average_worktime = model.sum(model.nurse_under_average_time_vars[n] for n in model.nurses)
458 model.add_kpi(total_over_average_worktime, "Total over-average worktime")
459 model.add_kpi(total_under_average_worktime, "Total under-average worktime")
460 total_fairness = total_over_average_worktime + total_under_average_worktime
461 model.add_kpi(total_fairness, "Total fairness")
462
463 model.minimize_static_lex([model.total_salary_cost, total_fairness, model.total_number_of_assignments])
464
465
466def print_information(model):
467 print("#shifts=%d" % len(model.shifts))
468 print("#nurses=%d" % len(model.nurses))
469 print("#vacations=%d" % len(model.vacations))
470 print("#nurse skills=%d" % len(model.nurse_skills))
471 print("#nurse associations=%d" % len(model.nurse_associations))
472 print("#incompatibilities=%d" % len(model.nurse_incompatibilities))
473 model.print_information()
474 model.report_kpis()
475
476
477def print_solution(model):
478 print("*************************** Solution ***************************")
479 print("Allocation By Department:")
480 for d in model.departments:
481 print("\t{}: {}".format(d, sum(
482 model.nurse_assignment_vars[n, s].solution_value for n in model.nurses for s in model.shifts if
483 s.department == d)))
484 print("Cost By Department:")
485 for d in model.departments:
486 cost = sum(
487 model.nurse_assignment_vars[n, s].solution_value * n.pay_rate * model.shift_activities[s].duration for n in
488 model.nurses for s in model.shifts if s.department == d)
489 print("\t{}: {}".format(d, cost))
490 print("Nurses Assignments")
491 for n in sorted(model.nurses):
492 total_hours = sum(
493 model.nurse_assignment_vars[n, s].solution_value * model.shift_activities[s].duration for s in model.shifts)
494 print("\t{}: total hours:{}".format(n.name, total_hours))
495 for s in model.shifts:
496 if model.nurse_assignment_vars[n, s].solution_value == 1:
497 print("\t\t{}: {} {}-{}".format(s.day, s.department, s.start_time, s.end_time))
498
499
500# ----------------------------------------------------------------------------
501# Build the model
502# ----------------------------------------------------------------------------
503
504def build(context=None, **kwargs):
505 mdl = Model("Nurses", context=context, **kwargs)
506 load_data(mdl, SHIFTS, NURSES, NURSE_SKILLS, VACATIONS, NURSE_ASSOCIATIONS,
507 NURSE_INCOMPATIBILITIES)
508 setup_data(mdl)
509 setup_variables(mdl)
510 setup_constraints(mdl)
511 setup_objective(mdl)
512 return mdl
513
514
515# ----------------------------------------------------------------------------
516# Solve the model and display the result
517# ----------------------------------------------------------------------------
518
519if __name__ == '__main__':
520 # Build model
521 model = build()
522
523 # Solve the model and print solution
524 solve(model)
525
526 print(model.solve_details)
527
528 # Save the CPLEX solution as "solution.json" program output
529 with get_environment().get_output_stream("solution.json") as fp:
530 model.solution.export(fp, "json")
531
532 model.end()
533
534 model = build()
535 paramsets = model.build_multiobj_paramsets(timelimits=[70,60,50] , mipgaps=[0.000003, 0.000002, 0.000001])
536 solve(model, clean_before_solve=True, parameter_sets=paramsets)
537 print(model.solve_details)
538
539 model = build()
540 paramsets = model.create_parameter_sets()
541 cplex = model.get_cplex()
542 for i,p in enumerate(paramsets):
543 p.add(cplex.parameters.timelimit, 70+i)
544 p.add(cplex.parameters.mip.tolerances.mipgap, 0.000001*i)
545 p.add(cplex.parameters.threads, 2+i)
546 solve(model, clean_before_solve=True, parameter_sets=paramsets)
547 print(model.solve_details)
548 model.end()