Article technique

Calcul itératif des références circulaires en Delphi

Pour calculer des références circulaires intentionnelles en Delphi, HotXLS expose le calcul itératif sur son moteur XLSX : passez TXLSXWorkbook.Iterate à True et TXLSXWorkbook.Recalculate mène chaque cycle de références détecté vers un point fixe — jusqu'à IterateCount passes, ou jusqu'à ce que chaque cellule varie de moins que IterateDelta — au lieu de renvoyer #REF! et de renoncer

Cette distinction compte plus que ne le laisse croire un simple booléen. Le même moteur qui détecte un cycle de références et refuse de boucler dessus va, une propriété basculée, évaluer ce cycle délibérément jusqu'à ce qu'il se stabilise. Garder les deux comportements bien distincts — quand un cycle est un défaut à signaler et quand il est un modèle à résoudre — est tout le sujet de cet article

Pourquoi une référence circulaire produit-elle une erreur par défaut ?

Par défaut, HotXLS traite tout cycle de références comme une erreur de saisie et le signale au lieu de le calculer. TXLSXWorkbook.Recalculate construit un graphe de dépendances de formules, évalue chaque cellule de formule en ordre topologique, et renvoie lxErrorRef dès qu'il trouve un cycle — les nœuds qui ne peuvent jamais être libérés pendant le tri topologique. Ces membres du cycle conservent leurs valeurs mises en cache précédentes ; chaque formule hors du cycle s'évalue encore normalement. La mécanique de ce graphe, et la raison pour laquelle les membres du cycle sont sautés plutôt que bouclés, sont traitées dans l'article compagnon sur le recalcul incrémental des formules et le graphe de dépendances

Le comportement par défaut est le plus sûr, car la plupart des cycles sont des bogues : une ligne de synthèse accidentellement tirée dans sa propre plage SUM, un copier-coller qui a décalé une référence sur elle-même. Un code d'erreur bien visible au moment du recalcul est exactement ce que vous voulez dans ces cas. Mais une classe de modèles précise et importante est circulaire à dessein. Les échéanciers à intérêts composés, les répartitions circulaires de coûts ou de frais généraux entre services, et les calculs de frais pilotés par le solde décrivent tous une valeur qui alimente légitimement ses propres entrées — et Excel ne les calcule qu'une fois que l'utilisateur coche Fichier → Options → Formules → Activer le calcul itératif

Comment activer le calcul itératif dans HotXLS ?

HotXLS reflète cette case à cocher d'Excel avec trois propriétés sur TXLSXWorkbook : Iterate active le mode, et quand elle vaut True un cycle détecté est confié à un solveur itératif au lieu de produire lxErrorRef. Prenez la paire de cellules canonique des intérêts composés, où le solde de clôture dépend des intérêts et où les intérêts dépendent du solde

Diagramme de la résolution par HotXLS d'une référence circulaire en Delphi : avec Iterate à False, Recalculate renvoie lxErrorRef, tandis qu'avec Iterate à True le cycle du solde et des intérêts passe par un solveur itératif jusqu'à la convergence de B3 et B4
Avec Iterate à False, HotXLS signale le cycle comme lxErrorRef ; avec Iterate à True, le même cycle devient un problème de solveur qui fait converger B3 et B4 vers un point fixe
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Model');
    Sheet.Cells[1, 2].Value   := 1000;     // B1 : capital initial
    Sheet.Cells[2, 2].Value   := 0.05;     // B2 : taux de la période
    Sheet.Cells[3, 2].Formula := 'B1+B4';  // B3 : solde     = capital + intérêts
    Sheet.Cells[4, 2].Formula := 'B3*B2';  // B4 : intérêts  = solde * taux

    Book.Iterate := True;                   // adhésion au calcul itératif
    if Book.Recalculate = lxOk then
      // B3 converge vers 1052,63..., B4 vers 52,63...
      Report(Sheet.Cells[3, 2].Value);
  finally
    Book.Free;
  end;
end;

B3 référence B4 et B4 référence B3, donc le graphe de dépendances signale un cycle à deux nœuds. Avec Iterate laissée à sa valeur par défaut False, cette paire reviendrait en lxErrorRef et aucune des deux cellules ne se stabiliserait. Avec True, Recalculate amorce le cycle à partir des valeurs mises en cache courantes et réévalue ses membres passe après passe, en réinjectant les sorties de chaque passe comme entrées de la suivante, jusqu'à ce que les nombres cessent de bouger. Ici la forme close est principal / (1 - rate), si bien que le solde se fixe à 1052,63 et les intérêts à 52,63 — les mêmes chiffres qu'Excel produit avec l'itération activée

Qu'est-ce qui arrête l'itération ?

Deux conditions d'arrêt indépendantes bornent le solveur, et comprendre les deux est ce qui évite à un modèle de tomber en erreur ou de tourner sans fin. IterateCount est le plafond dur du nombre de réévaluations des membres du cycle ; sa valeur par défaut est 100, comme dans Excel. IterateDelta est le seuil de convergence : après chaque passe, le solveur mesure la plus grande variation numérique sur toutes les cellules du cycle, et dès que ce maximum descend sous IterateDelta — 0,001 par défaut — la boucle de passes se rompt par anticipation. La condition atteinte la première met fin à l'itération

Organigramme de l'arrêt du calcul itératif de HotXLS en Delphi : chaque passe compare la plus grande variation de cellule à IterateDelta pour sortir par anticipation, sinon l'itération se termine au plafond IterateCount, et les deux fins renvoient lxOk
Chaque passe mesure la plus grande variation par cellule face à IterateDelta ; à défaut, la boucle s'arrête discrètement une fois le budget IterateCount épuisé et renvoie tout de même lxOk
Book.Iterate := True;
Book.IterateCount := 1000;    // plafond dur : au plus 1000 passes sur le cycle
Book.IterateDelta := 0.0001;  // convergence : arrêt dès que chaque cellule bouge de < 0,0001

case Book.Recalculate of
  lxOk:
    // le cycle a convergé, OU il a atteint le plafond de 1000 passes et gardé
    // les valeurs de sa dernière itération -- les deux chemins renvoient lxOk si Iterate vaut True
    SaveWorkbook(Book);
  lxErrorRef:
    // atteignable seulement avec Iterate = False : le cycle a été signalé, pas résolu
    LogWarning('Circular reference with iteration disabled');
end;

Une conséquence mérite d'être énoncée sans détour, car elle est la limite honnête de la fonctionnalité. Quand le plafond est atteint sans que la variation descende sous IterateDelta, SolveCycleIteratively ne lève pas d'erreur — il renvoie lxOk et laisse les cellules porter les valeurs de leur dernière itération, exactement comme Excel écrit les derniers nombres calculés quand sa propre limite d'itération est atteinte sans convergence. Un code de retour réussi de Recalculate en mode itératif signifie donc « le solveur a tourné », pas « le solveur a convergé ». Un modèle dont la boucle de rétroaction diverge ou oscille consommera discrètement toutes les passes de IterateCount et rendra des nombres qui ne sont pas du tout un point fixe, sans qu'aucune exception marque la différence

Comment le réglage est-il enregistré dans les fichiers XLSX et XLS ?

Les réglages de calcul itératif persistent dans les deux formats de feuille de calcul, si bien qu'un classeur ouvert dans Excel se comporte comme votre code Delphi l'a configuré. Côté XLSX, l'écrivain n'émet l'élément OOXML <calcPr> que lorsque Iterate vaut True, et il omet tout attribut resté à sa valeur par défaut afin de garder une sortie minimale : un classeur qui utilise les valeurs par défaut écrit simplement <calcPr iterate="1"/>, tandis que iterateCount n'apparaît que s'il diffère de 100 et iterateDelta que s'il diffère de 0,001. À l'ouverture, TXLSXWorkbook relit les trois mêmes attributs, si bien que l'aller-retour est symétrique

Le moteur BIFF8 hérité (.xls), TXLSWorkbook, porte l'état équivalent dans trois enregistrements séparés sous un autre triplet de propriétés. EnableIteration correspond à l'enregistrement CalcIter ($0011, [MS-XLS] §2.4.33), MaxIterations à l'enregistrement CalcCount ($000C, [MS-XLS] §2.4.31), et MaxIterationChange à l'enregistrement CalcDelta ($0010, [MS-XLS] §2.4.32). Les accesseurs en écriture imposent les plages de la spécification — CalcCount doit se situer dans 1..32767, donc MaxIterations est borné, et un MaxIterationChange négatif revient à la valeur par défaut de 0,001. Réglez-les sur un classeur .xls chargé et les trois enregistrements de calcul sont écrits fidèlement à la sauvegarde

var
  Book: TXLSWorkbook;   // moteur BIFF8 (.xls)
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('model.xls');
    Book.EnableIteration    := True;    // enregistrement CalcIter  $0011
    Book.MaxIterations      := 500;     // enregistrement CalcCount $000C (borné à 1..32767)
    Book.MaxIterationChange := 0.0001;  // enregistrement CalcDelta $0010
    Book.SaveAs('model.xls');           // les trois enregistrements de calcul font l'aller-retour
  finally
    Book.Free;
  end;
end;

Notez la séparation délibérée des noms : le moteur XLSX parle Iterate / IterateCount / IterateDelta (le vocabulaire OOXML), tandis que le moteur BIFF8 parle EnableIteration / MaxIterations / MaxIterationChange (aligné sur les noms d'enregistrements [MS-XLS]). Les deux triplets décrivent les trois mêmes boutons — un interrupteur, un plafond d'itérations et un delta de convergence — avec les mêmes valeurs par défaut : désactivé, 100 et 0,001

Correspondance de la persistance du calcul itératif dans HotXLS : les propriétés de TXLSXWorkbook rangées comme attributs calcPr en XLSX et les propriétés de TXLSWorkbook rangées comme enregistrements CalcIter, CalcCount et CalcDelta en BIFF8, avec des valeurs par défaut identiques en Delphi
Les deux moteurs exposent les trois mêmes boutons ; OOXML les porte comme attributs calcPr tandis que BIFF8 les empaquette dans les enregistrements CalcIter, CalcCount et CalcDelta

Quand une référence circulaire est-elle un bogue plutôt qu'un modèle ?

Activer l'itération n'est pas un moyen de faire disparaître les avertissements de référence circulaire, et la traiter ainsi est le piège. Activer Iterate globalement convertit chaque cycle accidentel — ceux que le code d'erreur par défaut était là pour attraper — en un nombre silencieusement convergé ou silencieusement non convergé. La discipline est inverse : gardez Iterate à False comme mode normal afin que les vraies erreurs de saisie remontent encore en lxErrorRef, et n'activez l'itération que sur les classeurs dont la circularité est voulue et comprise

Quand un cycle apparaît et que vous ne savez pas de quel type il est, le traceur d'évaluation de formules est l'outil qui les distingue : tracez la formule suspecte et la chaîne de références qui se replie sur elle-même devient visible étape par étape, ce qui vous permet de décider si elle encode une vraie boucle de rétroaction ou une auto-référence égarée. Il est aussi utile de se rappeler qu'une cellule du cycle peut appeler n'importe quelle fonction intégrée en parcourant la boucle — le même calculateur qui résout une formule d'ingénierie ou de nombres complexes évalue les membres du cycle à chaque passe — si bien qu'un modèle qui diverge relève souvent d'un problème de formule à l'intérieur de la boucle, pas d'un problème de réglages d'itération

La liste de contrôle pratique est courte. Vérifiez que la boucle possède un vrai point fixe avant de compter sur l'itération ; gardez IterateDelta assez serré pour que « convergé » veuille bien dire ce dont votre modèle a besoin ; et après un Recalculate censé converger, contrôlez une sortie connue plutôt que de vous fier au seul lxOk, puisque ce code ne sait pas distinguer une convergence d'un plafond d'itérations atteint

Le calcul itératif des références circulaires fait partie du moteur XLSX du HotXLS Delphi Excel Component, aux côtés du recalcul incrémental par graphe de dépendances sur lequel il repose et de la persistance OOXML et BIFF8 qui porte le réglage dans chaque fichier que vous écrivez. Pour les modèles financiers et d'ingénierie circulaires à dessein, c'est la différence entre un code d'erreur et une réponse