1

I struggle a bit to understand the different steps that are used in this example: diet.py, I've added the same code below. Also not an expert in modelling. As I understand it:

  1. the first part makes tuples of the three lists for the model to search in. However, I don't understand what happens in this part: food_nutrients = {(fn[0], nutrients[n].name)....}
  2. Then, the variable 'qty' defines a variable dictionary of the food items for all food 'f', which is limited by the lb and up values.
  3. Then the 'amount' defines the sum of all food items multiplied by their nutritional values and the range defines the constraints for the sum of the nutritional values (?)
  4. finally, the problem minimises the cost.

So what we achieve, is a list of quantities of different foods that fulfill the nutritional needs, AND minimizes the cost.

Now, if I want to have two or more list of constraints, e.g. a set of nutritional requirements for both adults and for kids, defined by a certain number of people in each group, how could I add this? Can I simply add copies of the existing steps and thereby have the two groups running in parallel (as two models)? Or is there a way that I can fulfil the needs of both groups, AND minimise the cost of the sum of foods in both groups? E.g. minimize: sum(qty[f])*f_unitcost + sum(qty_kids[f])*f_unitcost.

Any clearance on this would help a lot!

Thanks

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

1 Answers1

1

If the 2 models are independent then it's better to "have the two groups running in parallel (as two models)" with your own words.

Let me use a story that is even simpler than the diet one. (Unless you prefer diet to zoo)

Suppose you have 2 schools with 300 kids and 350 kids.

Then you can write:

from docplex.mp.model import Model

#Two independent schools, first school 300 kids, second one 350

mdl = Model(name='buses')
nbbus40 = mdl.integer_var(name='nbBus40')
nbbus30 = mdl.integer_var(name='nbBus30')
mdl.add_constraint(nbbus40*40 + nbbus30*30 >= 300, 'kids')
mdl.minimize(nbbus40*500 + nbbus30*400)

mdl.solve(log_output=True,)

for v in mdl.iter_integer_vars():
    print(v," = ",v.solution_value)

mdlbis = Model(name='busesbis')
nbbus40bis = mdlbis.integer_var(name='nbBus40bis')
nbbus30bis = mdlbis.integer_var(name='nbBus30bis')
mdlbis.add_constraint(nbbus40bis*40 + nbbus30bis*30 >= 350, 'kids')
mdlbis.minimize(nbbus40bis*500 + nbbus30bis*400)

mdlbis.solve(log_output=True,)

for v in mdl.iter_integer_vars():
    print(v," = ",v.solution_value)

for v in mdlbis.iter_integer_vars():
    print(v," = ",v.solution_value)

But suppose you need to combine them because of a coupling constraint.

from docplex.mp.model import Model

#Two independent schools, first school 300 kids, second one 350
#Combined in a single model because of a coupling constraint
#(Total number of buses)

mdl = Model(name='buses')
nbbus40 = mdl.integer_var(name='nbBus40')
nbbus30 = mdl.integer_var(name='nbBus30')
nbbus40bis = mdl.integer_var(name='nbBus40bis')
nbbus30bis = mdl.integer_var(name='nbBus30bis')
mdl.add_constraint(nbbus40*40 + nbbus30*30 >= 300, 'kids')
mdl.add_constraint(nbbus40bis*40 + nbbus30bis*30 >= 350, 'kids2')
mdl.add_constraint(nbbus40+nbbus30+nbbus40bis+nbbus30bis<=17,'total nb of buses')
mdl.minimize(nbbus40*500 + nbbus30*400+nbbus40bis*500 + nbbus30bis*400)

mdl.solve(log_output=True,)

for v in mdl.iter_integer_vars():
    print(v," = ",v.solution_value)
Alex Fleischer
  • 9,276
  • 2
  • 12
  • 15
  • Okay, that makes sense. What if they are dependent from being constraint by the total cost of both diets? In your example, this could be to have a third constraint about the total cost of the two buses or the total use of fuel? So the two constraints about diets (or number of children) are independent, but then they are dependent in the third constraint about cost (or fuel/busprices). Hope it makes sense. – Caroline Gebara Jan 14 '22 at 09:26