Optimisation de recettes via le solveur linaire

La fonction @glop.optimize() permet de calculer la recette en maximisant ou minimisant une certaine caractéristique sous certaines contraintes.

@glop.optimize({target: {var: nut['Protéines'], task: "max"}, constraints: {
               {var: "recipeQtyUsed", min: 1, max: 1},
               {var: compo['Cerise, dénoyautée, crue'], min: 0.3, max: "inf"},
               {var: compo['Beurre à 82% MG, doux'], min: 0.05, max: 0.2},
               {var: compo['Pâte brisée, crue'], min: 0.1, max: 0.3},
               {var: compo['Farine de blé tendre ou froment T45 (pour pâtisserie)'], min: 0.125, max: 0.2},
               {var: compo['Sucre blanc'], min: 0.05, max: 0.2}
               }})

Dans cet exemple, on cherche à maximiser la quantité de protéines en imposant des encadrements pour chaque ingrédient ainsi qu'une quantité totale égale à 1.

L'argument est un objet avec la structure suivante :

  • target spécifie l'objectif de l'optimisation et contient les paramètres :
    • var spécifiant la charactéristique à optimiser,
    • task valant "min" ou "max" selon qu'il faille la minimiser ou la maximiser ;
  • constraints est un ensemble d'objets spécifiant des contraintes à respecter pour l'optimisation et dont chacune d'elles contient les champs :
    • var spécifiant l'objet de la contrainte,
    • min désignant la valeur minimale acceptée pour cet objet, ou "-inf" (avec les guillemets) si pas de valeur minimale,
    • max désignant la valeur maximale acceptée pour cet objet, ou "inf" (avec les guillemets) si pas de valeur maximale.

Les caractéristiques supportées sont  :

  • les compositions (compo[]) ;
  • les nutriments (nut[]) ;
  • les coûts (cost[]) ;
  • les allergènes (allergen[]) ;
  • les ingrédients (ing[]).

Les objets de contraintes sont soit des caractéristiques, soit des identifiants de contraintes spéciales. Une contrainte spéciale nécessite plusieurs caractéristiques pour la définir. Actuellement, la seule implémentée est "recipeQtyUsed" qui est la somme de toutes les quantités de composants dans la recette.

Le résultat de cette fonction est un objet contenant :

  • coefficients qui indique la quantité de chaque composant dans la formule optimisée, en excluant les composants dont la quantité calculée est zéro ;
  • value qui indique la valeur de la fonction d'objectif pour la recette optimisée ;
  • status qui vaut optimal si la recette calculée est optimale et feasible si elle vérifie toutes les contraintes sans être garantie optimale.

S'il est impossible de respecter toutes les contraintes spécifiées, alors l'appel à @glop.optimize() échoue avec l'erreur Linear program is unfeasible.

Exemple d'optimisation avec le service Glop

Prenons par exemple une recette constituée de 5 ingrédients : ing1, ing2, ing3, ing4 et ing5.

Chaque ingrédient possède des caractéristiques physico-chimiques (PC) et un coût qui lui sont propres, voici un tableau définissant la constitution des ingrédients :

IngrédientPC1PC2PC3PC4Coût unitaire
ing16530281008
ing27028301008
ing36528.5601008
ing41000001
ing56733670100

On cherche à minimiser le coût total de la recette avec les contraintes suivantes :

  • le volume totale doit être de 1000 (unité arbitraire)
  • PC1 doit être compris entre 70000 et 80000
  • PC2 est une quantité expimée en pourcentages et doit être comprise entre 20% et 30%
  • PC3 doit être compris entre 40000 et 50000
  • PC4 doit être compris entre 70000 et 80000
  • ing1 doit être compris entre 1% et 100% (du volume total)
  • ing2 doit être compris entre 1% et 100% (du volume total)
  • ing3 doit être compris entre 10 et 1000
  • ing4 doit être compris entre 10% et 25% (du volume total)
  • ing5 doit être compris entre 50% et 100% (du volume total)

Le formule qui doit être entrée est la suivante :

@glop.optimize({target: {var:   cost['Coût'], task: "min"}, constraints: {
               {var:    physico['PC1'] , min: 70000, max: 80000},
               {var:    physico['PC2'], min: 20000, max: 30000},
               {var:    physico['PC3'], min: 40000, max: 50000},
               {var:    physico['PC4'], min: 70000, max: 80000},
               {var:    compo['ing1'], min: 10, max:1000},
               {var:    compo['ing2'], min: 10, max:1000},
               {var:    compo['ing3'], min: 10, max:1000},
               {var:    compo['ing4'], min: 100, max:250},
               {var:    compo['ing5'], min: 500, max:1000},
               {var: "recipeQtyUsed", min: 1000, max: 1000}
               }})

Comme la caractéristique physico-chimique PC2 est exprimée en pourcentages dans beCPG (par exemple : taux de glucides) il faut multiplier ses contraintes par la quantité totale voulue car le système ne peut pas savoir qu'il s'agit d'un rapport sur le volume, on a donc PC2 min = 20 x 1000 = 20000 et PC2 max = 30 x 1000 = 30000

Le résultat obtenu par la formule SPEL est un JSON avec la structure suivante :

{"components":[{"id":"","name":"ing1","value":157.81249999999991},{"id":"","name":"ing2","value":10.0},{"id":"","name":"ing3","value":532.1875},{"id":"","name":"ing4","value":250.0},{"id":"","name":"ing5","value":50.0}], "value":10850.0, "status":"optimal"}

Le résultat "value" est 10850.0, il s'agit du coût total minimisé.

Il est possible de stocker le résultat dans un objet "glopData" et d'afficher les différentes valeurs obtenues pour chaque composition dans des colonnes dynamiques :

var glopData = @glop.optimize({target: {var:   cost['Coût'], task: "min"}, constraints: {
               {var:    physico['PC1'] , min: 70000, max: 80000},
               {var:    physico['PC2'], min: 20000, max: 30000},
               {var:    physico['PC3'], min: 40000, max: 50000},
               {var:    physico['PC4'], min: 70000, max: 80000},
               {var:    compo['ing1'], min: 10, max:1000},
               {var:    compo['ing2'], min: 10, max:1000},
               {var:    compo['ing3'], min: 10, max:1000},
               {var:    compo['ing4'], min: 100, max:250},
               {var:    compo['ing5'], min: 500, max:1000},
               {var: "recipeQtyUsed", min: 1000, max: 1000}
               }});
setGlopData(#glopData);
glopData.getStatus();

La colonne dynamique s'écrit :

entity.glopData.getComponentValue(dataListItemEntity.nodeRef)

Nouvelle version du GLOP service (à partir de la 3.2.4)

Une nouvelle version permet d'utiliser le service GLOP plus facilement.

Nous partons d'un Produit Fini qui contient 3 Matières Premières.

glop-initial

Nous souhaitons optimiser la recette pour atteindre certaines cibles de nutriments et de quantités mises en oeuvre.

Il faut tout d'abord ajouter les colonnes "Cible d'optimisation" et "Valeur d'optimisation" sur la liste Composition et sur la liste Nutriments :

image-20220603100305138

Il faut ensuite renseigner les différentes cibles de contrainte souhaitées pour chaque ligne dans la colonne "Cible d'optimisation". Une cible peut être :

  • une valeur unique, ex: 58.2
  • deux valeurs séparées par un tiret "-", ex: 44-48, ce qui correspond à valeur minimale et valeur maximale
  • complétée par une tolérance entre parenthèses, ex: 58.2 (10) ou 44-48 (15), ce qui va définir une valeur de tolérance pour cette cible exprimée en %
  • vide, ce qui signifie que la ligne n'est pas considérée comme une contrainte

Il faut ensuite créer la formule SPEL qui va appeler le service GLOP pour effectuer le calcul.

Voici dans notre cas la formule utilisée :

var glopData = @glop.optimize({target: {var:  cost['workspace://SpacesStore/db597446-3458-405a-96d1-09b9593df1bb'], task: "min"}, constraints: {
               {list: compoList}, 
               {list: nutList}}});

setGlopData(#glopData);
glopData.getStatus();

Voici le résultat :

image-20220603101618236

Ici on constate que le système a pu trouver une solution exprimée dans les colonnes "Valeur d'optimisation".

Par défaut, le système cherche à résoudre le problème en conservant la quantité totale mise en oeuvre initiale dans le produit. C'est la raison pour laquelle les valeurs obtenues pour la composition ont pour somme totale 100 kg, la valeur totale initiale.

Il est possible de définir une contrainte sur cette valeur totale en ajoutant la ligne de contrainte

{var:"recipeQtyUsed", min:90,max:90}

Application de tolérances

Lorsque le système ne trouve pas de solution pour le problème, il est possible d'appliquer des tolérances (en %) sur les différentes contraintes.

  • Soit depuis la formule SPEL : {list: compoList, tol:10} (ici, la tolérance est appliquée sur toute la liste)
  • Soit depuis la colonne "Cible d'optimisation" : 6 (10) (ici, la tolérance est surchargée et appliquée sur la ligne uniquement)

Voici le réultat :

image-20220603102834706

On peut constater la valeur d'optimisation "Protéines" est orange car le système a dû appliquer la tolérance de 10% pour trouver une solution.

Le statut de l'optimisation est alors "suboptimal".

results matching ""

    No results matching ""