Download this file
#!/usr/bin/env pypy3
import sys
from collections import OrderedDict
from random import random, uniform, randrange, sample
class Composition:
def __init__(self, fat, saturated_fat, glucides, sugars, proteins, salt):
self.fat = fat
self.saturated_fat = saturated_fat
self.glucides = glucides
self.sugars = sugars
self.proteins = proteins
self.salt = salt
def params(self):
return [self.fat, self.saturated_fat, self.glucides, self.sugars, self.proteins, self.salt]
def __add__(self, other):
return self.__class__(self.fat + other.fat, self.saturated_fat + other.saturated_fat,
self.glucides + other.glucides, self.sugars + other.sugars,
self.proteins + other.proteins, self.salt + other.salt)
def __mul__(self, k):
return self.__class__(self.fat * k, self.saturated_fat * k, self.glucides * k,
self.sugars * k, self.proteins * k, self.salt * k)
def __eq__(self, other):
return (self.fat == other.fat and
self.saturated_fat == other.saturated_fat and
self.glucides == other.glucides and
self.sugars == other.sugars and
self.proteins == other.proteins and
self.salt == other.salt)
def _format(self, label, value):
s = f"{(label + ':'):12s} "
if value:
return s + f"{value:5.2f} g\n"
else:
return s + "?\n"
def __str__(self):
return self._format("Fat", self.fat) + \
self._format(" Saturated", self.saturated_fat) + \
self._format("Glucides", self.glucides) + \
self._format(" Sugars", self.sugars) + \
self._format("Proteins", self.proteins) + \
self._format("Salt", self.salt)
def __repr__(self):
return repr([self.fat, self.saturated_fat, self.glucides, self.sugars, self.proteins, self.salt])
@classmethod
def zero(cls):
return cls(0,0,0,0,0,0)
class Ingredient:
register = {}
def __init__(self, name, *composition):
self.name = name
self.composition = Composition(*composition)
self.register[name] = self
@classmethod
def resolve(cls, name):
return cls.register[name]
class Recipe:
def __init__(self, target, ingredients, fixed_ingredients=OrderedDict()):
self.target = target
self.ingredients = ingredients
self.fixed_ingredients = fixed_ingredients
def format(self, xs, amount):
return "\n".join(
f"{i + ':':16s} {q*100:4.1f} g | {q*amount:5.1f} g"
for i, q in zip(
self.ingredients + list(self.fixed_ingredients.keys()),
self.transform(xs) + list(self.fixed_ingredients.values()))) + \
"\n" + "-" * 33 + f"\nTotal: 100 g | {amount:5.1f} g\n"
def format_composition(self, solution):
return self.composition(self.transform(solution))
def transform(self, xs):
t = 1 - sum(self.fixed_ingredients.values())
ys = []
for x in xs:
ys.append((t - sum(ys)) * x)
ys.append(t - sum(ys))
return ys
def composition(self, qs):
return sum(
(i * q for i,q in zip(
(Ingredient.resolve(i).composition for i in self.ingredients + list(self.fixed_ingredients.keys())),
qs + list(self.fixed_ingredients.values()))),
Composition.zero())
class ProgressBar:
def __init__(self, steps, width=80):
self.max_steps = steps
self.max_width = width
self.current_step = 0
self.current_width = 0
self.render()
def render(self):
self.clear()
print("[" + "-" * self.current_width + " " * (self.max_width - 2 - self.current_width) + "]", end="")
sys.stdout.flush()
def update(self):
self.current_step += 1
width = round(self.current_step / self.max_steps * (self.max_width - 2))
if width > self.current_width:
self.current_width = width
self.render()
def clear(self):
print("\r" + " " * self.max_width + "\r", end="")
Ingredient("Milk", 3.5, 2.2, 4.8, 4.8, 3.0, 0.11)
Ingredient("Oil", 100, 5.8, 0, 0, 0, 0)
Ingredient("Cream", 35, 23.4, 3.0, 3.0, 2.0, 0.07)
Ingredient("Milk powder", 1.0, 0.6, 40.0, 40.0, 47.0, 1.4)
Ingredient("Sugar", 0, 0, 100.0, 100.0, 0, 0)
Ingredient("Glucose sirup", 0, 0, 78.0, 25.7, 0, 0)
Ingredient("Egg", 9.2, 2.4, 0.3, 0.3, 12.6, 0.28)
Ingredient("Egg yolk", 29.2, 7.7, 0.2, 0.2, 15.9, 0.135)
Ingredient("Glucose", 0, 0, 100.0, 100.0, 0, 0)
Ingredient("Hazelnut", 61, 4.5, 3.4, 3.4, 14.1, 0.0025)
Ingredient("Cocoa", 21.7, 10.1, 11, 0, 18.5, 0.15)
Ingredient("Coconut milk", 21.3, 18.9, 2.8, 2.8, 2.0, 0.03)
Ingredient("Chocolate", 36.1, 22.3, 45.3, 39.5, 7.8, 0.11)
class FitnessFunction:
def __init__(self, recipe):
self.recipe = recipe
self.domain_bounds = [(0.0,1.0)] * (len(self.recipe.ingredients) - 1)
def __call__(self, xs):
qs = self.recipe.transform(xs)
for q1,q2 in zip(qs[:-1], qs[1:]):
if q2 > q1:
return float("Inf")
return sum((x-y)**2 for x,y in zip(self.recipe.composition(qs).params(), self.recipe.target.params()) if y is not None)
class DifferentialEvolution:
def __init__(self, func, nb_generations, domain, np=100, f=0.8, cr=0.3, logfile=None, verbose=True):
self.func = func
self.nb_generations = nb_generations
self.domain = domain
self.np = np
self.f = f
self.cr = cr
if logfile:
self.logfile = open(logfile, "w")
else:
self.logfile = None
self.verbose = verbose
self.population = [ self.random_candidate() for _ in range(self.np) ]
self.best = (float("Inf"), [])
def random_candidate(self):
x = [uniform(d0, d1) for d0, d1 in self.domain]
return (self.func(x), x)
def adjust(self, xj, j):
dmin, dmax = self.domain[j]
if xj < dmin:
return 2 * dmin - xj
elif xj > dmax:
return 2 * dmax - xj
else:
return xj
def candidate(self, i, x):
r = randrange(len(x))
(fa,a), (fb,b), (fc,c) = sample(self.population[:i] + self.population[i+1:], 3)
y = [
(self.adjust(aj + self.f * (bj - cj), j) if random() < self.cr or j == r else xj)
for j, (aj, bj, cj, xj) in enumerate(zip(a,b,c,x))
]
return (self.func(y), y)
def step(self):
candidates = [self.candidate(i, x) for i,(fx,x) in enumerate(self.population)]
self.population = [
((fy, y) if fy < fx else (fx, x))
for ((fx, x), (fy, y)) in zip(self.population, candidates)
]
def _log(self, iteration):
f_best, best = self.best
if self.logfile:
self.logfile.write(f"{iteration} {f_best} {best}\n")
self.logfile.flush()
if self.verbose:
print(iteration, f"{f_best} (" + " ".join(f"{s:.3f}" for s in best) + ")")
def run(self):
progress = ProgressBar(self.nb_generations)
for generation in range(self.nb_generations):
progress.update()
self.step()
best = min(self.population)
if best[0] < self.best[0]:
self.best = best
self._log(generation)
progress.clear()
return best
def print_report(recipe, error, best):
print("target:")
print(recipe.target)
print("result:")
print(recipe.format_composition(best))
print("error:", error)
print("best:")
print(recipe.format(best, 500))
if __name__ == "__main__":
recipe = Recipe(
Composition(None, None, 22.7, 18.3, None, 0.2),
["Milk", "Milk powder", "Sugar", "Glucose sirup", "Glucose"],
OrderedDict([("Egg", 0.2)]))
_3_kaveria = Recipe(
Composition(8.9, 5.7, 22.7, 18.3, 7.0, 0.2),
["Milk", "Cream", "Milk powder", "Sugar", "Glucose sirup", "Egg", "Glucose"])
gianduja = Recipe(
Composition(None, None, 22.7, 18.3, None, 0.2),
["Milk", "Sugar", "Glucose sirup", "Milk powder", "Glucose"],
OrderedDict([("Egg", 0.2), ("Hazelnut", 0.055), ("Cocoa", 0.055)]))
coconut = Recipe(
Composition(None, None, 22.7, 18.3, None, 0.2),
["Coconut milk", "Sugar", "Glucose sirup"],
OrderedDict([("Egg", 0.083)]))
# https://www.picard.fr/produits/glace-chocolat-000000000000084022.html
chocolat = Recipe(
Composition(11.0, 6.8, 27.0, 24.0, 5.0, 0.09),
#["Sugar", "Egg yolk", "Milk powder"],
["Sugar", "Milk powder"],
#OrderedDict([("Milk", 0.595), ("Chocolate", 0.192), ("Cocoa", 0.011)]))
OrderedDict([("Milk", 0.545), ("Egg", 0.1), ("Chocolate", 0.192), ("Cocoa", 0.011)]))
fitness = FitnessFunction(chocolat)
de = DifferentialEvolution(fitness, 40000, fitness.domain_bounds, verbose=False)
error, best = de.run()
print_report(fitness.recipe, error, best)