.. _example_cqm_diet_reals:
=============
Diet Planning
=============
This example demonstrates the use of a `Leap `_
hybrid :ref:`cqm_sdk` (CQM) solver on a simple mixed-integer linear-programming (MILP)
type of optimization problem, which can be expressed as a linear objective and
constraints with integer and real-valued variables. Other examples include
*quadratic* objectives and constraints, which better make use of the strengths of
D-Wave's solvers.
The goal of this problem is to optimize the taste of a diet's foods while
keeping to the dieter's budget and daily requirements on macro-nutrients.
Example Requirements
====================
.. include:: hybrid_solver_service.rst
:start-after: example-requirements-start-marker
:end-before: example-requirements-end-marker
Solution Steps
==============
.. include:: hybrid_solver_service.rst
:start-after: example-steps-start-marker
:end-before: example-steps-end-marker
This example formulates this problem as a :ref:`constrained quadratic model `
and uses the :class:`~dwave.system.samplers.LeapHybridCQMSampler` to find good
solutions.
Formulate the Problem
=====================
The table below shows a selection of foods chosen by a dieter, ranked for
the dieter's taste on a scale of one to ten, with evaluations (not necessarily
realistic) of nutrients and costs.
.. _example_cqm_diet_reals_table_1:
.. list-table:: Nutrients, Cost, and Taste Rankings for Available Foods
:header-rows: 1
* - **Food**
- **Calories**
- **Protein**
- **Fat**
- **Carbs**
- **Fiber**
- **Taste**
- **Cost**
* - Rice
- 100
- 3
- 1
- 22
- 2
- 7
- 2.50
* - Tofu
- 140
- 17
- 9
- 3
- 2
- 2
- 4.0
* - Bananas
- 90
- 1
- 0
- 23
- 3
- 10
- 1.0
* - Lentils
- 150
- 9
- 0
- 25
- 4
- 3
- 1.30
* - Bread
- 270
- 9
- 3
- 50
- 3
- 5
- 0.25
* - Avocado
- 300
- 4
- 30
- 20
- 14
- 5
- 2.00
The following table shows the dieter's daily requirements for selected nutrients.
.. _example_cqm_diet_reals_table_2:
.. list-table:: Daily Required Nutrients
:header-rows: 1
* - **Nutrient**
- **Calories**
- **Protein**
- **Fat**
- **Carbs**
- **Fiber**
* - **Daily Requirement**
- 2000
- 50
- 30
- 130
- 30
For simplicity, store the contents of the two tables above as a dict. The dict
also contains information on whether the portion of a particular food is best
treated as continuous (for example, rice can be any weight), in which case it
should be represented with real-valued variables, or as a discrete unit (such as
fruit that is eaten whole), in which case it is best represented by integer-valued
variables.
>>> foods = {
... 'rice': {'Calories': 100, 'Protein': 3, 'Fat': 1, 'Carbs': 22, 'Fiber': 2,
... 'Taste': 7, 'Cost': 2.5, 'Units': 'continuous'},
... 'tofu': {'Calories': 140, 'Protein': 17, 'Fat': 9, 'Carbs': 3, 'Fiber': 2,
... 'Taste': 2, 'Cost': 4.0, 'Units': 'continuous'},
... 'banana': {'Calories': 90, 'Protein': 1, 'Fat': 0, 'Carbs': 23, 'Fiber': 3,
... 'Taste': 10, 'Cost': 1.0, 'Units': 'discrete'},
... 'lentils': {'Calories': 150, 'Protein': 9, 'Fat': 0, 'Carbs': 25, 'Fiber': 4,
... 'Taste': 3, 'Cost': 1.3, 'Units': 'continuous'},
... 'bread': {'Calories': 270, 'Protein': 9, 'Fat': 3, 'Carbs': 50, 'Fiber': 3,
... 'Taste': 5, 'Cost': 0.25, 'Units': 'continuous'},
... 'avocado': {'Calories': 300, 'Protein': 4, 'Fat': 30, 'Carbs': 20, 'Fiber': 14,
... 'Taste': 5, 'Cost': 2.0, 'Units': 'discrete'}}
...
>>> min_nutrients = {"Protein": 50, "Fat": 30, "Carbs": 130, "Fiber": 30}
>>> max_calories = 2000
Variables
=========
Instantiate some real and integer variables\ [#]_ in a list, :code:`quantities`,
that in the solutions will be assigned values for the selected quantities of
every available food.
>>> quantities = [dimod.Real(f"{food}") if foods[food]["Units"] == "continuous"
... else dimod.Integer(f"{food}")
... for food in foods.keys()]
.. [#]
Always keep in mind that such "variables" are actually
:class:`~dimod.QuadraticModel` objects,
>>> quantities[0]
QuadraticModel({'rice': 1.0}, {}, 0.0, {'rice': 'REAL'}, dtype='float64')
with a single variable with the requested label; e.g., :code:`rice`. This
means, for example, that multiplying by two doubles the linear bias,
>>> 2*quantities[0]
QuadraticModel({'rice': 2.0}, {}, 0.0, {'rice': 'REAL'}, dtype='float64')
multiplying two such "variables" creates a quadratic bias,
>>> quantities[0] * quantities[1] # doctest: +SKIP
QuadraticModel({'rice': 0.0, 'tofu': 0.0}, {('tofu', 'rice'): 1.0}, 0.0,
... {'rice': 'REAL', 'tofu': 'REAL'}, dtype='float64')
but multiplying three such quadratic models requires a non-quadratic term
and so :code:`quantities[0] * quantities[1] * quantities[2]` cannot generate
a quadratic model and results in an error.
Bounds on the range of values for non-binary variables shrink the solution
space the solver must search, so it is helpful to set such bounds; for many
problems, you can find bounds from your knowledge of the problem. In this case,
no food by itself should be assigned a quantity that exceeds :code:`max_calories`.
>>> for ind, food in enumerate(foods.keys()):
... ub = max_calories / foods[food]["Calories"]
... quantities[ind].set_upper_bound(food, ub)
The maximum quantity of rice, for example, which here has 100 calories per portion,
is 20 portions because :math:`20*100 = 2000`.
>>> quantities[0].upper_bound("rice")
20.0
Lower bounds are actually *required* in this problem for correct formulation:
a valid mathematical solution might be to offset the calories of gorging on large
numbers of tasty bananas by eating a negative amount of high-in-calories bread, so
the formulation must include the impossibility of consuming negative quantities
of food. Because Ocean sets a default value of zero for `~dimod.quadratic.Real`
variables, no explicit configuration is needed.
You can now formulate an :term:`objective function` and any constraints feasible
solutions must meet, and set these in your CQM.
Objective Function
------------------
The objective function must maximize taste of the diet's foods while minimizing
purchase cost.
To maximize taste and minimize cost is to assign values to the variables that
represent quantities of each food, :math:`q_i`, such that when multiplied by
coefficients representing the cost, :math:`c_i`, or taste, :math:`t_i`,
of each food, form the linear terms of the following summations to be optimized:
.. math::
\min \sum_i q_i c_i \qquad \text{Minimize cost}
\max \sum_i q_i t_i \qquad \text{Maximize taste}
To optimize two different objectives, taste and cost, requires weighing one
against the other. A simple way to do this, is to set priority weights; for example,
.. math::
\text{objective} = \alpha \text{(objective 1)} + \beta \text{(objective 2)}
By setting, for example :math:`\alpha=2, \beta=1`, you double the priority of the
first objective compared to the second.
Instantiate a CQM.
>>> cqm = dimod.ConstrainedQuadraticModel()
You can define a utility function, :code:`total_mix`, to calculate the summations
for any given category such as calories.
>>> def total_mix(quantity, category):
... return sum(q * c for q, c in zip(quantity, (foods[food][category] for food in foods.keys())))
Set the objective\ [#]_. Because Ocean solvers minimize objectives, to maximize taste,
:code:`Taste` is multiplied by `-1` and minimized.
>>> cqm.set_objective(-total_mix(quantities, "Taste") + 6*total_mix(quantities, "Cost"))
.. [#] Section `Tuning the Solution`_ belows shows how the priority weights
:math:`\alpha, \beta` above were chosen.
Constraints
-----------
The problem has the following constraints of the
:ref:`Daily Required Nutrients ` table:
1. Calories: no more than 2000
2. Protein: at least 50
3. Fat: at least 30
4. Carbs: at least 130
5. Fiber: at least 30
Constrain the diet's calorie intake to the required daily maximum.
>>> cqm.add_constraint(total_mix(quantities, "Calories") <= max_calories, label="Calories")
'Calories'
Require that the daily minimum of each nutrient is met or exceeded.
>>> for nutrient, amount in min_nutrients.items():
... cqm.add_constraint(total_mix(quantities, nutrient) >= amount, label=nutrient)
'Protein'
'Fat'
'Carbs'
'Fiber'
You can access these constraints as a dict with the labels as keys:
>>> list(cqm.constraints.keys())
['Calories', 'Protein', 'Fat', 'Carbs', 'Fiber']
>>> print(cqm.constraints["Calories"].to_polystring())
100*rice + 140*tofu + 90*banana + 150*lentils + 270*bread + 300*avocado <= 2000.0
>>> print(cqm.constraints["Protein"].to_polystring())
3*rice + 17*tofu + banana + 9*lentils + 9*bread + 4*avocado >= 50.0
Solve the Problem by Sampling
=============================
D-Wave's quantum cloud service provides cloud-based
:std:doc:`hybrid solvers ` you can
submit arbitrary QMs to. These solvers, which implement state-of-the-art
classical algorithms together with intelligent allocation of the quantum
processing unit (QPU) to parts of the problem where it benefits most, are
designed to accommodate even very large problems. Leap's solvers can
relieve you of the burden of any current and future development and optimization
of hybrid algorithms that best solve your problem.
Ocean software's :doc:`dwave-system `
:class:`~dwave.system.samplers.LeapHybridCQMSampler` class enables you to
easily incorporate Leap's hybrid CQM solvers into your application:
>>> from dwave.system import LeapHybridCQMSampler
>>> sampler = LeapHybridCQMSampler() # doctest: +SKIP
Submit the CQM to the selected solver. For one particular execution, the CQM
hybrid sampler returned 49 samples, out of which 25 were solutions that met all
the constraints.
>>> sampleset = sampler.sample_cqm(cqm) # doctest: +SKIP
>>> feasible_sampleset = sampleset.filter(lambda row: row.is_feasible) # doctest: +SKIP
>>> print("{} feasible solutions of {}.".format(len(feasible_sampleset), len(sampleset))) # doctest: +SKIP
25 feasible solutions of 49.
You can define a utility function, :code:`print_diet`, to display returned
solutions in an intuitive format.
>>> def print_diet(sample):
... diet = {food: round(quantity, 1) for food, quantity in sample.items()}
... print(f"Diet: {diet}")
... taste_total = sum(foods[food]["Taste"] * amount for food, amount in sample.items())
... cost_total = sum(foods[food]["Cost"] * amount for food, amount in sample.items())
... print(f"Total taste of {round(taste_total, 2)} at cost {round(cost_total, 2)}")
... for constraint in cqm.iter_constraint_data(sample):
... print(f"{constraint.label} (nominal: {constraint.rhs_energy}): {round(constraint.lhs_energy)}")
The best solution found in this current execution was a diet of bread and bananas,
with avocado completing the required fiber and fat portions:
>>> best = feasible_sampleset.first.sample # doctest: +SKIP
>>> print_diet(best) # doctest: +SKIP
Diet: {'avocado': 1.0, 'banana': 6.0, 'bread': 4.1, 'lentils': 0.3, 'rice': 0.0, 'tofu': 0.0}
Total taste of 86.56 at cost 9.46
Calories (nominal: 2000): 2000
Protein (nominal: 50): 50
Fat (nominal: 30): 42
Carbs (nominal: 130): 372
Fiber (nominal: 30): 46
Tuning the Solution
===================
Consider sampling each part of the combined objective on its own (i.e.,
:math:`\alpha=1, \beta=0` and :math:`\alpha=0, \beta=1`), and comparing the
best solutions. Start with taste:
>>> cqm.set_objective(-total_mix(quantities, "Taste"))
>>> sampleset_taste = sampler.sample_cqm(cqm) # doctest: +SKIP
>>> feasible_sampleset_taste = sampleset_taste.filter(lambda row: row.is_feasible) # doctest: +SKIP
>>> best_taste = feasible_sampleset_taste.first # doctest: +SKIP
>>> print(round(best_taste.energy)) # doctest: +SKIP
-177
>>> print_diet(best_taste.sample) # doctest: +SKIP
Diet: {'avocado': 0.0, 'banana': 17.0, 'bread': 0.0, 'lentils': 0.0, 'rice': 0.0, 'tofu': 3.3}
Total taste of 176.93 at cost 30.41
Calories (nominal: 2000): 2000
Protein (nominal: 50): 74
Fat (nominal: 30): 30
Carbs (nominal: 130): 402
Fiber (nominal: 30): 58
You can see that this diet is high in bananas, the tastiest food, and makes up
for that food's low levels of protein and fat with tofu.
Next, for cost:
>>> cqm.set_objective(total_mix(quantities, "Cost"))
>>> sampleset_cost = sampler.sample_cqm(cqm) # doctest: +SKIP
>>> feasible_sampleset_cost = sampleset_cost.filter(lambda row: row.is_feasible) # doctest: +SKIP
>>> best_cost = feasible_sampleset_cost.first # doctest: +SKIP
>>> print(round(best_cost.energy)) # doctest: +SKIP
3
>>> print_diet(best_cost.sample) # doctest: +SKIP
Diet: {'avocado': 1.0, 'banana': 0.0, 'bread': 5.3, 'lentils': 0.0, 'rice': 0.0, 'tofu': 0.0}
Total taste of 31.67 at cost 3.33
Calories (nominal: 2000): 1740
Protein (nominal: 50): 52
Fat (nominal: 30): 46
Carbs (nominal: 130): 287
Fiber (nominal: 30): 30
This diet is ranked as less tasty than the previous but much cheaper. It relies
mainly on bread and uses avocado to add fat and fiber.
Because of the differences in energy scale between the two parts of the
combined objective, :math:`177 \gg 3`, if you do not multiply the part
representing cost by some positive factor, optimal solutions will maximize taste
and neglect cost. That is, if in
:math:`\text{objective} = \alpha \text{(objective 1)} + \beta \text{(objective 2)}`
you set set :math:`\alpha = \beta = 1`, solutions will likely be close or identical
to those found when optimizing for taste alone.
>>> cqm.set_objective(-total_mix(quantities, "Taste") + 1 * total_mix(quantities, "Cost"))
>>> sampleset = sampler.sample_cqm(cqm) # doctest: +SKIP
>>> feasible_sampleset = sampleset.filter(lambda row: row.is_feasible) # doctest: +SKIP
>>> best = feasible_sampleset.first.sample # doctest: +SKIP
>>> print_diet(best) # doctest: +SKIP
Diet: {'avocado': 0.0, 'banana': 17.0, 'bread': 0.0, 'lentils': 0.0, 'rice': 0.0, 'tofu': 3.3}
Total taste of 176.93 at cost 30.41
Calories (nominal: 2000): 2000
Protein (nominal: 50): 74
Fat (nominal: 30): 30
Carbs (nominal: 130): 402
Fiber (nominal: 30): 58
Compare the best solutions found when optimizing for taste and cost alone. Notice
that to reduce 27 units of cost (:math:`30 - 3`) in the latter solution, taste
was decreased by 145 (:math:`177 - 32`), for a ratio of :math:`145/27 \approx 5.5`.
To give each part of the combined objective a similar weighting, the
`Objective Function`_ section above multiplied the part of the objective that
minimizes cost by a factor of :math:`\beta=6`.
The graphic below shows the solution energies of the combined objective and both
of its parts for :math:`\alpha = 1, \beta = \{1, 2, 3, ... 19, 20\}`.
.. figure:: ../_images/diet_solutions_energy.png
:name: DietSolutionsEnergy
:alt: image
:align: center
:scale: 70 %
Energy of the objective for various multipliers of the cost.
For low (:math:`1-5`) ratios of :math:`\frac{\beta}{\alpha}` solutions are optimized
for taste alone; for high ratios (:math:`> 15`) solutions are optimized for cost.
The relationship between this ratio and the weightings of the two parts of the
combined optimization is non-linear, so while you can use such reasoning as was
done above to find a starting point for "good" relative weightings, typically
you need to experiment.
Notice that in all the previous solutions, the resulting diet relied on only two
or three foods. If the dieter wants a more diverse diet, you can enforce that by
setting appropriate bounds on the variables (or, equivalently, adding constraints
on minimum/maximum quantities of each food).
>>> cqm.set_objective(-total_mix(quantities, "Taste") + 6*total_mix(quantities, "Cost"))
>>> for variable in cqm.variables:
... cqm.set_lower_bound(variable, 1)
>>> sampleset_diverse = sampler.sample_cqm(cqm) # doctest: +SKIP
>>> feasible_sampleset_diverse = sampleset_diverse.filter(lambda row: row.is_feasible) # doctest: +SKIP
>>> best_diverse = feasible_sampleset_diverse.first.sample # doctest: +SKIP
>>> print_diet(best_diverse) # doctest: +SKIP
Diet: {'avocado': 1.0, 'banana': 11.0, 'bread': 1.2, 'lentils': 1.0, 'rice': 1.0, 'tofu': 1.0}
Total taste of 132.93 at cost 21.1
Calories (nominal: 2000): 2000
Protein (nominal: 50): 55
Fat (nominal: 30): 44
Carbs (nominal: 130): 382
Fiber (nominal: 30): 59