MATER // GUIDELINES
Il se lit du général vers le particulier — parcourir le chapitre I avant tout autre est recommandé.
Avant de déclarer quoi que ce soit, chaque objet du système doit être classifié. Ce choix conditionne tout le reste.
Les quatre catégories
exogenous_stock + control_flow_sharescontrol_flow_shares (+ exogenous_stock si tampon)uncontrolled_objects + init_non_exo_in_use_stockuncontrolled_objects (alimenté par TP+ ou recyclage)Pourquoi déclarer uncontrolled_stock ? Le cheminement intellectuel
Mater est un système de contrôle, pas un comptable
Un logiciel de comptabilité matière se contente d'enregistrer : consommation = stock − flux_entrant + flux_sortant. Mater ne fait pas ça. Son mécanisme central est un contrôleur par rétroaction : il observe l'écart entre un stock cible et le stock réel, et commande la production pour résorber cet écart.
La conséquence : par défaut, tout stock est infini
Si un objet est consommé et qu'aucune instruction contraire n'est donnée, le contrôleur génère automatiquement une commande de production (tp_demand) pour compenser exactement cette consommation. Consommer 1 000 t de bauxite déclenche une commande de 1 000 t de bauxite. Le stock ne bouge jamais.
C'est le comportement souhaité pour de l'acier, de l'aluminium, de l'énergie. C'est le comportement absurde pour du lithium en gisement ou du charbon en réserve.
uncontrolled_stock = instruction au contrôleur : "ne compense pas".Ce n'est pas une étiquette sur la physique de l'objet. C'est une décision de modélisation.
La question à se poser : "Si ce stock est consommé, est-ce que je veux que Mater commande automatiquement pour compenser ?"
Si la réponse est non →
uncontrolled_stock.
Le cas du matériau partiellement alimenté par recyclage
Considérons de l'aluminium secondaire (alu_sec), issu du recyclage de câbles en fin de vie :
| Sans uncontrolled_stock | Avec uncontrolled_stock |
|---|---|
Chaque tonne consommée génère un tp_demand → production ex nihilo d'alu_sec. La tension recyclage insuffisant est invisible. |
Le tp_demand est filtré. Seul le flux de recyclage alimente le stock. unmet_demand signale exactement quand et de combien le déficit apparaît. |
uncontrolled_stock ne bloque pas le recyclage. Le flux de recyclage entre toujours dans le stock — il n'est pas affecté par le flag. Ce que uncontrolled_stock supprime, c'est uniquement la compensation commandée par le contrôleur.
La frontière de décision
| Situation | Statut |
|---|---|
| Alimenté uniquement par recyclage, jamais par production commandée | uncontrolled_stock |
| Alimenté uniquement par un stock initial fini (réserve) | uncontrolled_stock |
| Alimenté par recyclage ET par production primaire commandée | Producible |
| Alimenté par co-production (TP+) d'un autre procédé | uncontrolled_stock (Régime C) |
Cas limites du chapitre I
Déchet de P1 = produit principal de P2
Un co-produit de P1 peut être l'input que P2 cherche à produire (ex : laitier de haut fourneau valorisé comme liant). L'objet a deux sources : TP+ depuis P1, et production commandée depuis P2. Il est producible, pas uncontrolled_stock. Déclarer cet objet uncontrolled_stock serait une erreur — il a une production commandée légitime.
Contradiction uncontrolled_stock + exogenous_stock
Si un objet est dans uncontrolled_objects ET possède un exogenous_stock, Mater émet un warning. Le tp_demand est bien filtré, mais le feedforward continue de générer un control_flow. Les deux instructions se contredisent.
exogenous_stock si l'intention est un stock fini, ou en retirant l'objet de uncontrolled_objects si la production commandée est légitime.
Statut différent selon la localisation
uncontrolled_objects accepte des tuples (location, object). Il est possible de déclarer le lithium non-producible en France et producible en Australie (mines actives), sans aucun artifice supplémentaire.
Ressource devenant producible dans le temps
Mater ne gère pas le changement de statut uncontrolled_stock → producible en cours de simulation. Le contournement : modéliser deux objets distincts — la réserve (uncontrolled_stock) et l'objet extrait (producible), reliés par un procédé TP dont les coefficients varient dans le temps.
- Classer chaque objet avant toute déclaration : produit final, intermédiaire, ressource finie, ou co-produit.
uncontrolled_stockest une instruction au contrôleur, pas une propriété physique. La question : "veux-je que Mater compense automatiquement la consommation ?"- Le recyclage continue d'alimenter un stock
uncontrolled_stock— seule la commandetp_demandest supprimée. - Toute contradiction
uncontrolled_stock+exogenous_stockgénère un warning et un comportement hybride indéfini.
Object Composition (OC) — incorporation physique
Un objet A contient B : B est incorporé à la fabrication de A et reste dans A pendant toute sa durée de vie.
object_composition : (location, objet_parent, objet_composant) → coefficient (t/t, unité/unité…)
control_flow_shares : (location, objet_produit, process) → part du procédé
Le coefficient OC peut varier dans le temps — c'est une série temporelle dans la DataFrame. Un allègement progressif (câble passant de 0,5 à 0,3 t Cu/km entre 2025 et 2050) se déclare directement en renseignant la colonne temporelle correspondante.
Conséquence sur IUSEM — point critique
L'indicateur in_use_stock_embedded_material (IUSEM) estime la quantité de matière B contenue dans le parc d'objets A en service. Sa formule interne :
Mater applique le coefficient courant à l'ensemble du parc, toutes cohortes confondues. La valeur exacte serait :
où OC(k) est le coefficient en vigueur au moment de la production de la cohorte k.
OC(k). Les flux control_flow, tp_demand, IUS, unmet_demand ne sont pas affectés.
Direction du biais
| Situation OC | Effet sur IUSEM |
|---|---|
| OC décroissant (allègement) | Coefficient courant < historique → IUSEM sous-estime les vieilles cohortes |
| OC croissant (densification) | IUSEM surestime les vieilles cohortes |
| OC stable | Biais nul |
| Transition rapide + vieux parc majoritaire | Biais maximal |
Contournement via age_cohort
L'output in_use_stock conserve la dimension age_cohort. Le calcul correct s'effectue hors modèle :
ius = mater.get("in_use_stock") # dimension age_cohort disponible
oc = params.object_composition # séries temporelles
# Pour chaque cohorte, appliquer OC au moment de sa production
iusem_correct = (
ius
.groupby(level=["location", "object", "object_composition", "age_cohort"])
.sum()
# puis joindre OC[age_cohort] pour chaque ligne
)
Thermodynamic Process (TP) — consommation ou co-production
Un procédé P consomme B (sans l'incorporer dans le produit) ou génère C en co-produit.
thermodynamic_process : (location, process, objet) → coefficient
# Négatif : B est consommé par le procédé (énergie, catalyseur, eau, réserve extraite)
# Positif : C est co-produit inévitable (déchet, chaleur résiduelle, émission)
# L'objet PRODUIT par le procédé n'apparaît PAS ici — il est dans control_flow_shares
Amélioration d'efficacité progressive
Un procédé consomme 100 kWh/unité en 2025 et 65 kWh/unité en 2045. Aucune déclaration supplémentaire — le coefficient varie, la consommation suit automatiquement.
Substitution d'input dans le procédé
La part d'énergie fossile vs renouvelable dans un procédé change. Ce sont deux objets distincts (gaz naturel, électricité) avec des TP distincts dont les coefficients évoluent en sens inverse. La somme peut rester constante — c'est la façon propre de modéliser une transition de mix énergétique.
Changement de technologie (via CFS)
Si le procédé P1 (ancienne technologie) est remplacé par P2 (nouvelle technologie), modéliser via control_flow_shares : la part de P1 décroît, celle de P2 croît. Les coefficients TP de P1 et P2 peuvent être constants — c'est la composition du mix qui change.
Assembly Stock (AS) — composant à durée de vie inférieure
AS est le mécanisme approprié quand un composant B a une durée de vie plus courte que le produit A qui le contient et doit être remplacé périodiquement pendant la vie de A.
assembly_stock : (location, objet_parent, objet_composant) → coefficient (flux-based)
Le coefficient représente la fraction du parc parent renouvelée par unité de temps — c'est un coefficient de flux, pas un coefficient de stock. Un coefficient de 0,20 signifie que 20 % du parc de voitures renouvelle sa batterie à chaque pas de temps annuel.
assembly_stock était traité comme une matrice de stock via clôture transitive, puis multiplié sur le stock de référence. Ce mécanisme créait du double-comptage avec la clôture OC et était incompatible avec les chaînes longues. AS est désormais un tableau de coefficients de flux : la demande est propagée depuis les parents vers les enfants par un mécanisme dédié.
Quand préférer AS à OC
- Durée de vie de B < durée de vie de A, avec remplacement pendant la vie de A (batterie dans un véhicule, fenêtres dans un bâtiment)
- Ratio d'incorporation croissant dans le temps avec impact sur les remplacements dans le parc existant — AS capture ce comportement, OC ne l'affecte que pour les nouvelles productions
- Composant partagé entre plusieurs produits parents A1 et A2 dont la demande évolue différemment
exogenous_stock(B) = IUS(A) × taux_remplacement_annuel et l'injecter comme scénario.
AS — propagation en ordre topologique et diagnostic
Mécanisme de propagation
La demande AS est propagée des parents vers les enfants dans l'ordre topologique du graphe d'assemblage, au sein d'un même pas de temps k. Aucune itération temporelle supplémentaire n'est nécessaire.
Si B est lui-même parent d'un enfant C, la demande AS sur C est calculée depuis CF_total(B), qui inclut déjà la contribution de A. La propagation est donc complète en une seule passe topologique — sans délai temporel, quel que soit le nombre de niveaux d'assemblage.
# Déclaration AS : batterie dans une voiture (renouvellement 20%/an)
assembly_stock = {
("FR", "voiture", "batterie") : 0.20,
("FR", "batterie", "lithium_cel") : 0.05, # 5% des cellules renouvelées/an
}
# → à chaque pas k :
# CF(voiture)[k] = 1000 unités (feedforward standard)
# as_demand(batterie)[k] = 0.20 × 1000 = 200 unités (propagation AS niveau 1)
# as_demand(lithium_cel)[k] = 0.05 × (CF_std(batterie) + 200) (propagation AS niveau 2)
Output de diagnostic : as_driven_demand_flow
L'output as_driven_demand_flow expose, à chaque pas de temps, la demande cumulée injectée par le mécanisme AS pour chaque objet enfant :
as_df = mater.outputs.get("as_driven_demand_flow")
# Index : (location, object) — objet enfant
# Colonnes : time
# Valeurs : demande AS cumulée depuis tous les parents déclarés
# Exemple : inspecter la demande AS sur les batteries
print(as_df.loc[("FR", "batterie")])
Si as_driven_demand_flow est nul pour un enfant attendu, vérifier que control_flow_shares est bien déclaré sur l'objet parent — le mécanisme AS se greffe sur le control_flow du parent et ne se déclenche que si ce dernier est actif.
Interaction AS × uncontrolled_stock
Un enfant B peut être uncontrolled_stock (ex : lithium en réserve finie, cuivre recyclé). Dans ce cas la demande AS est bien calculée, mais le filtre uncontrolled_stock l'empêche de générer un control_flow. Si la demande dépasse le stock disponible de B, unmet_demand(B) le signale avec la quantité exacte manquante.
- OC : incorporation physique, coefficient variable dans le temps nativement supporté.
- IUSEM est approximé quand OC varie : biais systématique sur l'estimation du matériau embarqué dans les vieilles cohortes. Les flux de simulation restent corrects. Contournement via
age_cohorthors modèle. - TP : consommation (négatif) ou co-production (positif). Coefficient variable, substitution de mix via CFS.
- AS : réservé aux composants à durée de vie inférieure au parent avec remplacement périodique. Coefficient de flux (pas de stock) — la demande se propage en ordre topologique intrapas. Output
as_driven_demand_flowpour le diagnostic.
La résolution simultanée — rupture conceptuelle essentielle
Avant le fix Jacobi, Mater calculait les flux dans un ordre séquentiel : la consommation des inputs ne se répercutait qu'au pas de temps k+1. Résultat : une confusion entre "itération du solveur" et "pas de temps physique". Les stocks d'inputs apparaissaient négatifs pendant un pas puis se rétablissaient — des fantômes numériques sans signification.
Après le fix, Mater utilise une boucle Jacobi qui résout l'ensemble des dépendances algébriques à l'intérieur du pas de temps k, avant de passer à k+1. Ce n'est pas de la propagation temporelle — c'est de la résolution simultanée.
Chaîne OC — cascade de composition
Deux régimes selon que l'objet intermédiaire est "actionnable" (a un control_flow_shares) ou non.
Régime 1 — intermédiaire B sans CFS
La fermeture transitive est calculée : le lien A→B→C est traité comme A→B direct + A→C direct (coefficient = coeff_AB × coeff_BC). La consommation de C est générée depuis la production de A en une seule passe Jacobi.
exogenous_stock (même nul) ou un control_flow_shares. Sans ça, B est structurellement détecté comme uncontrolled_stock → son tp_demand est filtré → IUS(B) déplétion → la chaîne aval peut être cassée.
Régime 2 — intermédiaire B avec CFS
Seul le lien direct A→B est utilisé. La propagation vers C se fait via Jacobi : production de A → tp_demand(B) → production de B via son procédé → tp_demand(C). Chaque maillon ajoute une passe Jacobi.
| Régime 1 (B sans CFS) | Régime 2 (B avec CFS) | |
|---|---|---|
| Propagation vers C | Fermeture transitive (1 passe) | Jacobi itératif (N passes) |
| B doit avoir exo_stock ? | Oui (sinon uncontrolled_stock) | Non nécessaire |
| C peut être uncontrolled_stock ? | ✓ | ✓ |
| Observabilité de B | Stock de B dans les outputs | Stock de B + procédé de B actifs |
Exemple — Régime 1 : câble → conducteur alu → aluminium (sans CFS sur le conducteur)
Le conducteur alu est un intermédiaire purement physique : il n'a pas de procédé de production propre déclaré dans le modèle. Il est simplement la forme que prend l'aluminium une fois mis en forme dans le câble.
# Déclaration OC
object_composition = {
("FR", "câble", "conducteur_alu") : 0.82, # t alu/km de câble
("FR", "conducteur_alu","aluminium") : 1.00, # 1:1, le conducteur est de l'alu
}
# conducteur_alu : pas de CFS, mais exogenous_stock = 0 obligatoire
# → Régime 1 : Mater applique la fermeture transitive
# → câble consomme aluminium au coefficient 0.82 × 1.00 = 0.82 t/km, en 1 passe Jacobi
Exemple — Régime 2 : câble → conducteur alu → aluminium (avec CFS sur le conducteur)
Le conducteur alu a ici son propre procédé de tréfilage (P_trefilage), ce qui permet de tracer son stock et de lui associer des consommations énergétiques via TP.
# Déclaration OC
object_composition = {
("FR", "câble", "conducteur_alu") : 0.82,
("FR", "conducteur_alu","aluminium") : 1.00,
}
# Déclaration CFS sur le conducteur
control_flow_shares = {
("FR", "conducteur_alu", "P_trefilage") : 1.0,
}
# → Régime 2 : propagation Jacobi en 3 passes
# Passe 1 : feedforward(câble) → CF(câble)
# Passe 2 : CF(câble) → tp_demand(conducteur_alu) → CF(conducteur_alu)
# Passe 3 : CF(conducteur_alu) → tp_demand(aluminium) → CF(aluminium)
Chaîne TP — condition bloquante
Pour une chaîne A (consomme D) → D (consomme E) :
| Itération Jacobi | Ce qui se calcule |
|---|---|
| 1 | feedforward(A) → control_flow(A) |
| 2 | P_A tourne → tp_demand(D) → control_flow(D) |
| 3 | P_D tourne → tp_demand(E) → control_flow(E) |
| 4 | Convergence |
control_flow_shares pointant vers P_D. Sans CFS sur D, P_D n'a aucune activité calculée — E n'est jamais consommé, la chaîne s'arrête à D même si D est correctement produit.
Exemple — filière aluminium : aluminium → alumine → bauxite
La filière Hall-Héroult / Bayer est le cas TP archétypal : chaque étape consomme une ressource en amont, qui est elle-même un objet producible avec son propre procédé.
# Chaîne TP : aluminium consomme alumine, alumine consomme bauxite
thermodynamic_process = {
("FR", "P_electrolysis", "alumine") : -1.93, # t alumine / t alu (négatif = consommé)
("FR", "P_bayer", "bauxite") : -4.80, # t bauxite / t alumine
}
control_flow_shares = {
("FR", "aluminium", "P_electrolysis") : 1.0, # OBLIGATOIRE — sinon P_electrolysis inactive
("FR", "alumine", "P_bayer") : 1.0, # OBLIGATOIRE — sinon bauxite jamais consommée
}
# bauxite → uncontrolled_stock (stock géologique fini)
# Itérations Jacobi pour câble → alu → alumine → bauxite : 4 passes
# ① CF(câble) ② tp_demand(alu) ③ tp_demand(alumine) ④ tp_demand(bauxite)
CFS sur alumine est une erreur silencieuse : aluminium est correctement produit, mais bauxite n'est jamais consommée. La chaîne s'arrête à alumine sans warning explicite.
Performance — profondeur vs largeur
La profondeur de la chaîne détermine le nombre d'itérations Jacobi, pas sa largeur :
- N objets en parallèle consommés par A → 2 itérations, quelle que soit N
- N objets en chaîne linéaire → N+1 itérations
Chaînes mixtes OC + TP — la filière câble aluminium complète
En pratique, un modèle combine toujours des liens OC (composition physique) et des liens TP (consommation de ressources par les procédés). L'exemple ci-dessous reconstitue la chaîne complète : du câble fini jusqu'à la bauxite extraite, en passant par le tréfilage et l'électrolyse.
Ce schéma illustre deux règles clés en pratique :
- Les liens OC (bleus) forment la colonne vertébrale produit — ce que contient physiquement le câble.
- Les liens TP (oranges pointillés) descendent des procédés vers leurs consommations — invisibles dans la composition du câble, mais bien tracés dans les flux.
- La largeur (deux consommations d'énergie en parallèle) ne coûte rien en itérations Jacobi. La profondeur (câble → conducteur → alu → alumine → bauxite) détermine le nombre de passes.
Circularités TP
Une circularité TP existe quand A consomme B et B consomme A (ex : acier nécessaire aux infrastructures électriques, électricité nécessaire à la production d'acier). La boucle Jacobi est conçue pour résoudre ces couplages, mais l'introduction d'une non-linéarité dans la formule de control_flow (Option A, section IV) peut dans certains cas ralentir ou compromettre la convergence.
RuntimeError est levée. Une détection statique des cycles TP est prévue (développement futur).
- La résolution Jacobi est simultanée intrapas, pas séquentielle. Aucun stock négatif fantôme.
- Chaîne OC : Régime 1 (transitif, 1 passe) vs Régime 2 (Jacobi, N passes). CFS obligatoire pour Régime 2.
- Chaîne TP : CFS obligatoire sur chaque intermédiaire. Profondeur = nombre d'itérations.
- En l'absence de CFS ou exo_stock sur un intermédiaire : détection heuristique → comportement uncontrolled_stock → chaîne potentiellement cassée.
Recyclage simple
Un objet A, en fin de vie, libère du matériau B (récupérable tel quel) ou B' (forme dégradée).
collection_rate : fraction d'objets A collectés en fin de vie
recycling_rate : (location, objet_collecté, composant_récupéré, forme_dégradée) → taux
recycling_flow_shares : destination du matériau recyclé
# B ou B' dans uncontrolled_objects s'ils ne sont alimentés que par recyclage
Le flux secondary_production(B) alimente IUS(B) à chaque fin de vie d'une cohorte de A. Si B est uncontrolled_stock, ce flux est la seule entrée dans son stock — quand la consommation dépasse le recyclage, unmet_demand(B) le signale.
Tension recyclage / production primaire — limitation actuelle et évolution prévue
Comportement actuel
Le tp_demand généré par la consommation d'aluminium est ajouté au control_flow après le filtre de positivité :
Conséquence : le tp_demand est toujours honoré intégralement par la production primaire, indépendamment du stock disponible issu du recyclage. La substitution recyclé/primaire n'est pas dynamique.
Évolution planifiée — Option A
La formule cible pour les objets avec exogenous_stock (stock tampon) :
Avec cette formule, si le stock disponible (recyclage accumulé) couvre la demande aval, la production primaire est nulle. Si le stock ne couvre qu'une partie de la demande, le primaire complète exactement le déficit. La substitution devient dynamique.
Stock tampon intermédiaire — quand mettre un exogenous_stock sur un intermédiaire
| Cas | Comportement | Avantage Option A |
|---|---|---|
Sans exogenous_stock (just-in-time) |
Le tp_demand est la seule commande. Si recyclage excédentaire → accumulation infinie, aucun signal ne réduit la production primaire. |
Non applicable |
Avec exogenous_stock non nul (stock tampon) |
Le feedforward pilote le stock vers une cible. Comportement identique au just-in-time dans l'état actuel. | Le surplus de stock sera valorisé pour réduire la production primaire |
La valeur de stock cible représente un niveau de sécurité — typiquement quelques semaines à quelques mois de consommation. Ce calibrage reste à la discrétion du modélisateur.
- Recyclage simple :
collection_rate+recycling_rate+uncontrolled_stockpour le recyclé. Le flux de recyclage est physiquement tracé. - Substitution dynamique primaire/secondaire : non disponible dans la version actuelle (Option A planifiée). Actuellement, la production primaire couvre toujours l'intégralité du
tp_demand. - Stock tampon intermédiaire :
exogenous_stocknon nul sur un objet intermédiaire. Prérequis de l'Option A.
Un IUS négatif est le signal universel de pénurie dans Mater. Mais deux causes très différentes produisent ce symptôme identique, et les remèdes sont opposés.
Lire les outputs de diagnostic
Trois outputs sont spécifiquement conçus pour le diagnostic de pénurie et de propagation AS. Ils sont absents si aucun événement ne les déclenche.
unmet_demand — déficits sur objets uncontrolled_stock
Présent dans outputs uniquement si au moins un objet uncontrolled_stock a un IUS < 0 à un pas de temps donné. Absent ou vide si tous les stocks uncontrolled_stock sont suffisants.
ud = mater.outputs.get("unmet_demand")
if ud is not None and not ud.empty:
# Index : (location, object) — objets uncontrolled_stock en déficit
# Colonnes : time
# Valeurs : quantité manquante (positif = déficit)
# Résumé : déficit cumulé sur toute la simulation, par objet
print(ud.sum(axis=1).sort_values(ascending=False))
# Profil temporel d'un objet spécifique
print(ud.loc[("FR", "bauxite")])
# Premier pas de temps en déficit
first_shortage = (ud > 0).idxmax(axis=1)
print(first_shortage)
unmet_demand est un résultat physique, pas une erreur de modèle. Il mesure exactement de combien et quand le système ne peut plus fournir ce qui lui est demandé. Si ce résultat est inattendu, vérifier : (1) la valeur de init_non_exo_in_use_stock pour l'objet concerné, (2) que les flux de recyclage sont bien déclarés, (3) que les coefficients TP de consommation ne sont pas surestimés.
as_driven_demand_flow — demande propagée par AS
Présent uniquement si assembly_stock est déclaré et non vide. Expose la demande injectée aux objets enfants via le mécanisme AS, séparément du control_flow standard.
as_df = mater.outputs.get("as_driven_demand_flow")
if as_df is not None and not as_df.empty:
# Index : (location, object) — objets enfants recevant une demande AS
# Colonnes : time
# Valeurs : demande AS cumulée depuis tous les parents
# Part AS dans la demande totale d'un objet
cf = mater.outputs["control_flow"]
share = as_df.div(cf, fill_value=0).fillna(0)
print(share.loc[("FR", "batterie")])
mater.log — trace d'exécution
Écrit dans <run_name>/mater.log à chaque appel à run(). Contient le nombre d'itérations Jacobi par pas de temps, les warnings uncontrolled_stock (pénuries détectées), et les éventuelles RuntimeError de divergence. Utile pour calibrer la complexité du modèle et diagnostiquer les convergences lentes.
# Extraire les lignes WARNING depuis le log
with open(f"{run_name}/mater.log") as f:
warnings = [l for l in f if "WARNING" in l]
print("\n".join(warnings))
Insufficient input — stock non renouvelable épuisé
| Propriété | Détail |
|---|---|
| Cause | Un objet uncontrolled_stock consommé au-delà de son stock disponible |
| Signal | outputs["unmet_demand"] non vide, avec objets concernés et amplitudes par période |
| Interprétation | Signal physique réel — ce n'est pas une erreur de modèle, c'est un résultat |
| Remède si non voulu | Augmenter init_non_exo_in_use_stock, réviser les coefficients, ou décaler la temporalité |
Stock cible insuffisant — trajectoire scénario en dessous de la demande effective
| Propriété | Détail |
|---|---|
| Cause | Un objet producible avec exogenous_stock inférieur à ce que la demande aval requiert (demande remontant des chaînes OC, TP ou AS) |
| Signal | IUS négatif sur un objet producible. Pas de unmet_demand automatique dans ce cas — le signal doit être détecté manuellement |
| Interprétation | C'est une décision de scénario (ou une erreur de calibrage) : la trajectoire exogenous_stock a été fixée en dessous de ce que les chaînes aval demandent réellement |
| Remède | Réviser l'exogenous_stock pour l'aligner sur la demande effective, ou accepter explicitement le dépassement comme contrainte de scénario |
Détection manuelle
# IUS négatif sur un objet producible = stock cible insuffisant
ius = mater.get("in_use_stock").groupby(level=["location","object"]).sum()
shortfall = ius[(ius < 0).any(axis=1)]
if not shortfall.empty:
print("Stock cible insuffisant sur :", shortfall.index.tolist())
À ne pas confondre avec une contrainte de capacité de procédé
Dans Mater, la capacité de production désigne la capacité des procédés (process_max_capacity, process_reference_capacity) — par exemple la puissance installée d'une turbine ou le rendement d'un four. Cette contrainte n'est pas active dans la version actuelle : la capacité de production s'ajuste immédiatement à la demande, sans plafonnement. Un procédé peut donc toujours produire exactement ce qui lui est demandé, quelle que soit l'ampleur du signal.
process_max_capacity n'est pas encore implémenté. La capacité s'ajuste de façon illimitée à la demande. Un IUS négatif sur un objet producible traduit donc toujours un stock cible mal calibré, jamais un plafond de procédé atteint. Cette fonctionnalité est prévue dans une version future.
Plusieurs inputs, un seul qui manque
Quand un procédé consomme plusieurs inputs (B et C) et que C s'épuise, le procédé continue de tourner dans le modèle. B est toujours consommé normalement. Seul C tire son IUS en négatif et génère unmet_demand(C).
unmet_demand(C) indique la quantité de C qui aurait dû exister pour que le procédé tourne comme simulé — c'est un écart entre la trajectoire modélisée et ce que le système physique peut fournir.
- IUS < 0 sur
uncontrolled_stock→ insufficient input → signal physique viaunmet_demand. - IUS < 0 sur producible → insufficient capacity → plafond
exo_stocktrop bas. - Input manquant : le procédé continue, les autres inputs sont consommés. Pas de backpropagation.
Cas documentés mais contre-nature
Co-produit commandé par sa propre demande
Si la demande en C (co-produit de P) explose mais que celle en A (produit principal) s'effondre, le modèle ne tire pas la production de A pour satisfaire C. C est non commandable — aucune rétroaction vers P. unmet_demand(C) signale le déficit, mais sans action corrective automatique.
Maintenance de parc existant
Remplacer des composants B en fin de vie dans des A encore en service (hors nouvelle production) n'est pas natif. Contournement : calculer hors modèle exogenous_stock(B) = IUS(A) × taux_remplacement et l'injecter.
Cascade OC profonde avec IUSEM
La fermeture transitive corrige la consommation, mais in_use_stock_embedded_material n'utilise que les liens OC directs. Les matériaux terminaux d'une cascade longue (A→B→C→D) n'apparaissent pas directement dans IUSEM(A) — recalcul hors modèle nécessaire.
Hors scope — évolutions planifiées
| Fonctionnalité | Description | Statut |
|---|---|---|
| Option A — substitution dynamique | Formule max(feedforward + tp_demand, 0) pour objets avec stock tampon |
Planifiée — conditionnée à détection cycles TP |
| Détection statique des cycles TP | Graphe de dépendances à l'initialisation, warning si cycle implique exogenous_stock |
Planifiée |
| Backpropagation de contrainte | Si un input manque, propager la réduction de production vers le procédé et ses autres inputs | Non prévu à court terme |
Impact de la profondeur des chaînes
Le nombre d'itérations Jacobi par pas de temps est déterminé par la profondeur des chaînes, pas leur largeur :
| Topologie | Itérations Jacobi | Impact |
|---|---|---|
| N objets en parallèle consommés par A | 2 — quelle que soit N | Nul |
| N objets en chaîne linéaire | N + 1 | Linéaire avec N |
| Profondeur 15, T=100 pas annuels | 1 600 passes totales | vs 100 sans chaîne |
Désactivation des diagnostics
Le calcul de unmet_demand est post-boucle (une seule passe pandas en fin de simulation) : son coût unitaire est faible. Un flag compute_diagnostics=False est prévu pour désactiver ce calcul en mode production intensive. En attendant, le coût reste négligeable devant celui de la boucle temporelle.
Recommandations générales pour les runs en masse
- Limiter la profondeur des chaînes TP/OC Régime 2 à ce qui est nécessaire.
- Préférer OC Régime 1 (fermeture transitive) quand l'observabilité des intermédiaires n'est pas requise.
- Vérifier la convergence Jacobi sur un run de validation avant le lancement en masse : le nombre d'itérations par pas de temps est observable dans les logs.
- Isoler les paramètres qui varient dans l'analyse de sensibilité pour éviter de reconstruire les DataFrames complètes à chaque run — ne faire varier que les colonnes temporelles concernées.
Mécanique
_control_flow_trade_inverse_computation modifie les control_flow pour allouer la production entre localisations selon une matrice d'échange. Les flux échangés apparaissent dans traded_control_flow. Le calcul du trade est effectué à chaque pas de temps, avant la boucle Jacobi.
Interaction avec les chaînes
La demande issue d'une localisation A peut être satisfaite par la production d'une localisation B. Les tp_demand remontés dans les chaînes amont sont générés dans la localisation productrice (B), pas dans la localisation consommatrice (A). Il faut s'assurer que les chaînes amont (CFS, TP) sont correctement déclarées pour la localisation productrice.
Interaction avec uncontrolled_stock
Le statut uncontrolled_stock est déclaré par tuple (location, object). Un objet peut être uncontrolled_stock dans une localisation et producible (importé via trade) dans une autre. Le trade ne contourne pas le flag uncontrolled_stock — si l'objet est uncontrolled_stock dans la localisation productrice, il n'y aura pas de production commandée, même pour l'export.