Aller au contenu principal

GFM Estimateur de quantité d'ingrédients

Le Gap Filling Module Estimateur de quantité d'ingrédients estime la quantité de chaque ingrédient dans un produit alimentaire en se basant sur les données nutritionnelles. Il utilise l'optimisation convexe (CVXPY) pour minimiser la différence entre les valeurs nutritionnelles déclarées sur l'emballage du produit et les valeurs calculées dérivées des nutriments individuels des ingrédients.

Référence rapide

PropriétéDescription
S'exécute surFoodProcessingActivityNode avec valeurs nutritionnelles disponibles sur les noeuds parents
DépendancesUnitWeightConversionGapFillingWorker, AddClientNodesGapFillingWorker, MatchProductNameGapFillingWorker, IngredientSplitterGapFillingWorker, NutrientSubdivisionGapFillingWorker, LinkTermToActivityNodeGapFillingWorker
Entrée cléDéclaration nutritionnelle du produit, liste d'ingrédients avec profils nutritionnels
SortieQuantités d'ingrédients estimées (kg par unité de produit parent)
DéclencheurLe produit possède des valeurs nutritionnelles et une déclaration d'ingrédients

Conditions d'exécution

Le module se déclenche lorsque :

  1. Le noeud est un FoodProcessingActivityNode
  2. Les noeuds parents disposent de valeurs nutritionnelles (provenant de l'interface de programmation applicative ou de la base de données)
  3. Le produit possède une déclaration d'ingrédients analysée par le Gap Filling Module Séparateur d'ingrédients
  4. Tous les Gap Filling Modules requis en dépendance ont terminé leur exécution

Sortie clé

Le module produit :

  • Quantités d'ingrédients : Poids estimé de chaque ingrédient en kg par unité de produit parent
  • Statut de solution : Statut du solveur d'optimisation (optimal, infaisable, non borné)
  • Carrés des erreurs : Erreurs quadratiques par nutriment pour l'évaluation de la qualité
  • Nutriments estimés : Valeurs nutritionnelles calculées à partir des quantités estimées

Méthodologie scientifique

L'estimateur de quantité d'ingrédients résout un problème d'optimisation sous contraintes pour trouver les pourcentages d'ingrédients qui correspondent au mieux aux valeurs nutritionnelles déclarées tout en respectant les contraintes légales et physiques.

Formulation du problème

L'algorithme minimise la différence normalisée entre les nutriments calculés et déclarés :

minimize ||A_norm * x - b_norm||

Où :

  • A : Matrice nutritionnelle M x N (M nutriments, N ingrédients)
  • x : Vecteur N x 1 des quantités d'ingrédients (fractions du total, somme égale à 1)
  • b : Vecteur M x 1 des valeurs nutritionnelles déclarées
  • A_norm, b_norm : Versions normalisées utilisant l'écart-type

Normalisation

Chaque nutriment est normalisé par son écart-type de population pour assurer une pondération égale :

A_norm = (A.T / norm_vector).T
b_norm = b / norm_vector

Cette normalisation utilise des statistiques dérivées de milliers de déclarations de produits :

NutrimentMoyenneÉcart-typeMédiane
Énergie (kilocalorie)284,84170,00275,0
Lipides (g)12,6112,677,4
Acides gras saturés (g)5,486,292,5
Glucides (g)27,4724,1916,2
Sucre/Saccharose (g)15,3617,057,3
Protéines (g)6,945,765,8
Chlorure de sodium (g)0,770,850,4
Fibres (g)2,882,592,2
Sodium (mg)302,92938,0715,74
Chlore (mg)461,02515,61242,64

Nutriments acceptés

Seuls les nutriments disposant de données complètes dans la base de données Eaternity sont utilisés pour l'optimisation :

ACCEPTED_NUTRIENTS = {
"energy",
"fat",
"saturated_fat",
"carbohydrates",
"water",
"sucrose",
"protein",
"sodium",
"chlorine",
"fibers",
}

Système de contraintes

L'optimisation inclut plusieurs types de contraintes :

1. Contrainte de somme unitaire

Toutes les fractions d'ingrédients à chaque niveau hiérarchique doivent totaliser 1 (100 %) :

F @ x == g

Où F est la matrice de contraintes d'égalité assurant que les ingrédients de chaque niveau totalisent correctement.

2. Contrainte d'ordre décroissant

Conformément à la réglementation européenne sur l'étiquetage alimentaire, les ingrédients doivent être listés par ordre décroissant de poids :

C @ x <= d

Où C est une matrice de contraintes imposant :

  • ingredient[i] >= ingredient[i+1] pour les ingrédients de même niveau

Exception : Cette contrainte ne s'applique pas aux subdivisions (variantes nutritionnelles d'un même ingrédient).

3. Contraintes de pourcentage fixe

Lorsque les pourcentages sont déclarés sur l'emballage :

# Contrainte de pourcentage exact
x[ingredient_idx] == fixed_percentage * x[parent_idx]

# Ou pour les ingrédients de niveau racine
x[ingredient_idx] == fixed_percentage

4. Contraintes de pourcentage minimum/maximum

Lorsque seules des bornes sont spécifiées :

# Pourcentage minimum : ingrédient >= min_percentage * parent
x[col_idx] - min_pct * x[parent_col_idx] >= 0

# Pourcentage maximum : ingrédient <= max_percentage * parent
x[col_idx] - max_pct * x[parent_col_idx] <= 0

5. Non-négativité

Toutes les quantités d'ingrédients doivent être non négatives :

x >= 0

Détails d'implémentation

Configuration du solveur

Le module utilise le solveur ECOS via CVXPY :

problem.solve(solver="ECOS")

ECOS (Embedded Conic Solver) est choisi pour son efficacité avec les programmes coniques du second ordre et sa capacité à gérer la fonction objectif des moindres carrés.

Gestion de la hiérarchie des ingrédients

L'algorithme gère les déclarations d'ingrédients imbriquées en utilisant un système de tuples de niveaux :

# Exemple de hiérarchie :
# "Chocolat (cacao 30 %, sucre), Lait en poudre (lait, lactose)"
# Tuples de niveaux :
# (0,) -> Chocolat
# (0, 0) -> cacao (30 % du chocolat)
# (0, 1) -> sucre
# (1,) -> Lait en poudre
# (1, 0) -> lait
# (1, 1) -> lactose

La matrice de contraintes assure que :

  • Les sous-ingrédients totalisent la quantité de leur ingrédient parent
  • Les contraintes d'ordre s'appliquent à chaque niveau

Traitements spéciaux

Ajustement pour la fermentation

Pour les produits alcoolisés, l'algorithme ajuste les nutriments pour tenir compte de la fermentation :

# Conversion de l'alcool (équation de Gay-Lussac)
# 180 g de sucre -> 92 g d'éthanol + 88 g de CO2
alc_as_sugar = alc.value / 0.47

# Ajuster le sucre et les glucides
sugar.value += alc_as_sugar
carbs.value += alc_as_sugar

# Ajuster l'énergie (alcool : 7 kcal/g, sucre : 4 kcal/g)
energy.value += alc_as_sugar * 4.0 # Rajouter les calories du sucre
energy.value -= alc.value * 7.0 # Retirer les calories de l'alcool

Séparation du chlorure de sodium

Lorsque le chlorure de sodium (sel) est déclaré, il est séparé en sodium et chlore pour l'optimisation :

NA_PERCENT_IN_NACL = 39.34  # 39,34 % de sodium
CL_PERCENT_IN_NACL = 60.66 # 60,66 % de chlore

new_sodium = old_sodium + (sodium_chloride * 39.34 / 100)
new_chlorine = old_chlorine + (sodium_chloride * 60.66 / 100)

Exclusion de l'eau

La teneur en eau est exclue de l'optimisation directe car elle est implicitement gérée par la subdivision nutritionnelle :

incoming_nutrients.quantities = {
k: v for k, v in incoming_nutrients.quantities.items()
if k != get_nutrient_term("water").uid
}

Ingrédients non alimentaires

Les ingrédients non alimentaires (additifs, conservateurs, numéros E) se voient attribuer une quantité nulle :

for uid in non_food_ingredient_uids:
calc_graph.apply_mutation(
PropMutation(
node_uid=uid,
prop_name="amount",
prop=QuantityProp(
value=0.0,
unit_term_uid=kilogram_term.uid,
),
)
)

Construction de la matrice de contraintes

Matrice nutritionnelle (A)

La matrice nutritionnelle M x N associe les profils nutritionnels des ingrédients à l'optimisation :

def make_nutrient_matrix(flat_nutrients, product_order, nutrient_order, M, N):
matrix = np.zeros((M, N))
for iy, product_key in enumerate(product_order):
if flat_nutrients[product_key] is None:
continue # Laisser la colonne à zéro
for ix, nutrient_key in enumerate(nutrient_order):
if nutrient_key in flat_nutrients[product_key].quantities:
matrix[ix, iy] = flat_nutrients[product_key].quantities[nutrient_key].value
matrix[np.isnan(matrix)] = 0.0
return matrix

Matrice de contraintes d'ordre (C)

Impose l'ordre décroissant des ingrédients :

# Exemple pour [(0,), (1,), (1,0), (1,1), (2,)]
# Structure de la matrice :
# [[-1, 1, 0, 0, 0], # ingredient[0] >= ingredient[1]
# [ 0, -1, 0, 0, 1], # ingredient[1] >= ingredient[4]
# [ 0, 0, -1, 1, 0], # sub[0] >= sub[1]
# [ 0, 0, 0, -1, 0],
# [ 0, 0, 0, 0, -1]]

Matrice d'égalité (F)

Assure que les ingrédients totalisent correctement à chaque niveau :

# Exemple : F @ x = g
# [[1, 1, 0, 0, 1], # Le niveau supérieur totalise 1
# [0, -1, 1, 1, 0]] # Les sous-ingrédients totalisent le parent
# g = [1, 0]

Exemple de calcul

Scénario : Tablette de chocolat avec la déclaration :

  • « Pâte de cacao (45 %), Sucre, Beurre de cacao, Lait en poudre (5 %) »
  • Nutriments déclarés : 530 kcal, 32 g de lipides, 20 g d'acides gras saturés, 52 g de glucides, 48 g de sucres, 6 g de protéines

Étape 1 : Construire la hiérarchie des ingrédients

Tuples de niveaux :
(0,) -> Pâte de cacao [45 % fixé]
(1,) -> Sucre
(2,) -> Beurre de cacao
(3,) -> Lait en poudre [5 % fixé]

Étape 2 : Construire les matrices

Matrice nutritionnelle A (pour 100 g de chaque ingrédient) :

Pâte de cacaoSucreBeurre de cacaoLait en poudre
Énergie228400884496
Lipides14099,826,7
Acides gras saturés8,1060,516,7
Glucides11,5100038,4
Sucres0,5100038,4
Protéines19,60026,3

Matrice de contraintes C (contraintes d'ordre) :

[[-1, 1, 0, 0],    # x[0] >= x[1]
[0, -1, 1, 0], # x[1] >= x[2]
[0, 0, -1, 1], # x[2] >= x[3]
[0, 0, 0, -1]]

Étape 3 : Résoudre l'optimisation

Avec les contraintes fixes x[0] = 0,45 et x[3] = 0,05 :

minimize ||A_norm @ x - b_norm||
subject to:
x[0] + x[1] + x[2] + x[3] = 1
x[0] = 0,45
x[3] = 0,05
x[0] >= x[1] >= x[2] >= x[3]
x >= 0

Étape 4 : Solution

Pâte de cacao :   45,0 %  (fixé)
Sucre : 30,2 % (estimé)
Beurre de cacao : 19,8 % (estimé)
Lait en poudre : 5,0 % (fixé)

Étape 5 : Sortie vers le graphe

Chaque noeud d'ingrédient reçoit une quantité en kg :

# Pour 1 kg de produit :
cocoa_mass.amount = 0,450 kg
sugar.amount = 0,302 kg
cocoa_butter.amount = 0,198 kg
milk_powder.amount = 0,050 kg

Mécanismes de repli

En cas d'échec de l'optimisation

Si l'optimisation retourne « infaisable » ou « non borné » :

  1. Essayer le repli sans estimation : Si tous les pourcentages sont fixés, les utiliser directement
  2. Gérer les subdivisions : Attribuer les quantités de subdivision en fonction de la quantité de production du parent
  3. Journaliser l'erreur de données : Enregistrer l'échec pour révision manuelle
if len(flat_nutrients) == len(fixed_percentages):
if set(flat_nutrients.keys()) == set(fixed_percentages.keys()):
self.handle_fixed_percentages(flat_nutrients, fixed_percentages, calc_graph)
return

Nutriments manquants

Lorsque les ingrédients de base manquent de données nutritionnelles :

  • Si tous les pourcentages sont connus, utiliser les pourcentages fixes
  • Sinon, lever une erreur demandant les données à l'équipe scientifique

Métriques de qualité

Carrés des erreurs

Le module calcule les erreurs quadratiques par nutriment pour l'évaluation de la qualité :

error_squares[nutrient] = ((estimated - declared) / STD)^2

Cette erreur normalisée permet la comparaison entre produits indépendamment de l'échelle.

Statut de la solution

Le solveur retourne un statut indiquant la qualité de la solution :

  • optimal : Solution valide trouvée
  • optimal_inaccurate : Solution trouvée mais pouvant présenter des problèmes numériques
  • infeasible : Aucune solution valide n'existe (contraintes conflictuelles)
  • unbounded : Problème mal contraint

Limitations connues

Couverture des données

  • Nécessite des profils nutritionnels pour tous les ingrédients dans la base de données Eaternity
  • Seuls 10 nutriments sont utilisés pour l'optimisation (pas tous les nutriments déclarés)
  • Statistiques nutritionnelles basées sur des données de produits européens

Hypothèses du modèle

  • Ingrédients listés par ordre décroissant (réglementation de l'Union européenne)
  • Pourcentages des sous-ingrédients relatifs au parent (peut varier en pratique)
  • Relation linéaire entre les quantités d'ingrédients et les nutriments
  • Les modifications nutritionnelles liées à la transformation ne sont pas modélisées directement

Considérations numériques

  • Les solutions légèrement négatives (> -1e-5) sont ramenées à 1e-10
  • Les solutions NaN déclenchent des erreurs
  • Le solveur peut échouer sur des problèmes mal conditionnés

Gap Filling Modules connexes

ModuleRelation
Ingredient SplitterAnalyse les déclarations d'ingrédients en hiérarchie structurée
Nutrient SubdivisionCrée des variantes séchées/modifiées pour la modélisation de la perte d'eau
Match Product NameLie les ingrédients aux entrées de la base de données avec profils nutritionnels
Unit Weight ConversionConvertit les quantités entre unités

Références

  1. Documentation CVXPY. https://www.cvxpy.org/

  2. Solveur ECOS. Domahidi, A., Chu, E., & Boyd, S. (2013). ECOS: An SOCP solver for embedded systems. European Control Conference.

  3. Réglementation de l'Union européenne sur l'étiquetage alimentaire. Règlement (UE) n° 1169/2011

  4. Distance de Mahalanobis. https://fr.wikipedia.org/wiki/Distance_de_Mahalanobis

  5. Données de composition alimentaire EuroFIR. http://www.eurofir.org/