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()