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 sur | FoodProcessingActivityNode avec valeurs nutritionnelles disponibles sur les noeuds parents |
| Dépendances | UnitWeightConversionGapFillingWorker, AddClientNodesGapFillingWorker, MatchProductNameGapFillingWorker, IngredientSplitterGapFillingWorker, NutrientSubdivisionGapFillingWorker, LinkTermToActivityNodeGapFillingWorker |
| Entrée clé | Déclaration nutritionnelle du produit, liste d'ingrédients avec profils nutritionnels |
| Sortie | Quantités d'ingrédients estimées (kg par unité de produit parent) |
| Déclencheur | Le produit possède des valeurs nutritionnelles et une déclaration d'ingrédients |
Conditions d'exécution
Le module se déclenche lorsque :
- Le noeud est un
FoodProcessingActivityNode - Les noeuds parents disposent de valeurs nutritionnelles (provenant de l'interface de programmation applicative ou de la base de données)
- Le produit possède une déclaration d'ingrédients analysée par le Gap Filling Module Séparateur d'ingrédients
- 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 :
| Nutriment | Moyenne | Écart-type | Médiane |
|---|---|---|---|
| Énergie (kilocalorie) | 284,84 | 170,00 | 275,0 |
| Lipides (g) | 12,61 | 12,67 | 7,4 |
| Acides gras saturés (g) | 5,48 | 6,29 | 2,5 |
| Glucides (g) | 27,47 | 24,19 | 16,2 |
| Sucre/Saccharose (g) | 15,36 | 17,05 | 7,3 |
| Protéines (g) | 6,94 | 5,76 | 5,8 |
| Chlorure de sodium (g) | 0,77 | 0,85 | 0,4 |
| Fibres (g) | 2,88 | 2,59 | 2,2 |
| Sodium (mg) | 302,92 | 938,07 | 15,74 |
| Chlore (mg) | 461,02 | 515,61 | 242,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 cacao | Sucre | Beurre de cacao | Lait en poudre | |
|---|---|---|---|---|
| Énergie | 228 | 400 | 884 | 496 |
| Lipides | 14 | 0 | 99,8 | 26,7 |
| Acides gras saturés | 8,1 | 0 | 60,5 | 16,7 |
| Glucides | 11,5 | 100 | 0 | 38,4 |
| Sucres | 0,5 | 100 | 0 | 38,4 |
| Protéines | 19,6 | 0 | 0 | 26,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é » :
- Essayer le repli sans estimation : Si tous les pourcentages sont fixés, les utiliser directement
- Gérer les subdivisions : Attribuer les quantités de subdivision en fonction de la quantité de production du parent
- 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
| Module | Relation |
|---|---|
| Ingredient Splitter | Analyse les déclarations d'ingrédients en hiérarchie structurée |
| Nutrient Subdivision | Crée des variantes séchées/modifiées pour la modélisation de la perte d'eau |
| Match Product Name | Lie les ingrédients aux entrées de la base de données avec profils nutritionnels |
| Unit Weight Conversion | Convertit les quantités entre unités |
Références
-
Documentation CVXPY. https://www.cvxpy.org/
-
Solveur ECOS. Domahidi, A., Chu, E., & Boyd, S. (2013). ECOS: An SOCP solver for embedded systems. European Control Conference.
-
Réglementation de l'Union européenne sur l'étiquetage alimentaire. Règlement (UE) n° 1169/2011
-
Distance de Mahalanobis. https://fr.wikipedia.org/wiki/Distance_de_Mahalanobis
-
Données de composition alimentaire EuroFIR. http://www.eurofir.org/