From 4ca9ee36c261682acecf39ff34b1a108094a5936 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Thu, 8 Apr 2021 10:37:19 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20un=20nouveau=20m=C3=A9canisme:=20r?= =?UTF-8?q?=C3=A9soudre=20la=20r=C3=A9f=C3=A9rence=20circulaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce mécanisme permet d'activer le calcul itératif pour trouver la valeur de la règle qui résout la référence circulaire. Il est possible pour une règle de se référencer elle-même. Par défaut, le moteur considère qu'il s'agit d'un cycle non voulu, et renvoie 'null' comme valeur pour la règle en question, en affichant un avertissement. Mais dans certains cas, la formule est bonne et le cycle est voulu. La valeur de la règle attendue est donc celle qui résout l'équation obtenue via la référence cyclique. Lorsque l'on active cette fonctionnalité, le moteur va procéder par essaie erreur jusqu'à trouver cette valeur. Note : la résolution de cycle est coûteuse en temps de calcul. Il faut donc veiller à ne pas la cumuler avec l'évaluation d'un autre mécanisme coûteux comme l'inversion numérique par exemple. --- mon-entreprise/package.json | 2 +- publicodes/core/source/AST/graph.ts | 2 + publicodes/core/source/AST/index.ts | 13 +++ publicodes/core/source/AST/types.ts | 12 +- publicodes/core/source/evaluation.ts | 4 +- publicodes/core/source/index.ts | 11 +- .../core/source/mecanisms/abattement.ts | 2 +- publicodes/core/source/mecanisms/inversion.ts | 8 +- publicodes/core/source/mecanisms/plafond.ts | 3 +- publicodes/core/source/mecanisms/plancher.ts | 2 +- publicodes/core/source/mecanisms/product.ts | 2 +- .../résoudre-référence-circulaire.ts | 109 ++++++++++++++++++ .../core/source/mecanisms/trancheUtils.ts | 4 +- publicodes/core/source/mecanisms/unité.ts | 2 +- .../core/source/mecanisms/variations.ts | 2 +- publicodes/core/source/parse.ts | 3 + publicodes/core/source/reference.ts | 3 +- publicodes/core/source/rule.ts | 55 ++++++--- publicodes/core/source/uniroot.ts | 4 +- .../résoudre-référence-circulaire.yaml | 68 +++++++++++ publicodes/docs/mecanisms.yaml | 35 ++++++ publicodes/ui-react/source/Explanation.tsx | 8 +- .../mecanisms/RésoudreRéférenceCirculaire.tsx | 19 +++ publicodes/ui-react/source/rule/RulePage.tsx | 1 - 24 files changed, 328 insertions(+), 46 deletions(-) create mode 100644 publicodes/core/source/mecanisms/résoudre-référence-circulaire.ts create mode 100644 publicodes/core/test/mécanismes/résoudre-référence-circulaire.yaml create mode 100644 publicodes/ui-react/source/mecanisms/RésoudreRéférenceCirculaire.tsx diff --git a/mon-entreprise/package.json b/mon-entreprise/package.json index 8320a0524..2a8ce2e5b 100644 --- a/mon-entreprise/package.json +++ b/mon-entreprise/package.json @@ -109,7 +109,7 @@ "build:stats": "webpack --config webpack.prod.js --profile --json > stats.json", "build:analyze-bundle": "ANALYZE_BUNDLE=1 yarn run build", "build:dev": "FR_BASE_URL='http://localhost:5000${path}' EN_BASE_URL='http://localhost:5001${path}' yarn run build", - "clean": "rimraf dist node_modules source/data", + "clean": "rimraf dist node_modules 'source/data/!(versement-transport.json)'", "test": "yarn test:file \"./{,!(node_modules)/**/}!(webpack).test.{js,ts}\"", "test:file": "yarn mocha-webpack --webpack-config ./webpack.dev.js --include test/componentTestSetup.js --require mock-local-storage --require test/helpers/browser.js", "test:bundlesize": "bundlesize", diff --git a/publicodes/core/source/AST/graph.ts b/publicodes/core/source/AST/graph.ts index 3d072fa66..5429346ae 100644 --- a/publicodes/core/source/AST/graph.ts +++ b/publicodes/core/source/AST/graph.ts @@ -28,6 +28,8 @@ function buildRuleDependancies(rule: RuleNode): Array { return node.explanation.amendedSituation.flatMap((s) => fn(s[1])) case 'reference': return [...acc, node.dottedName as string] + case 'résoudre référence circulaire': + return [] case 'rule': // Cycle from parent dependancies are ignored at runtime, // so we don' detect them statically diff --git a/publicodes/core/source/AST/index.ts b/publicodes/core/source/AST/index.ts index 65a1d9736..f9f42880a 100644 --- a/publicodes/core/source/AST/index.ts +++ b/publicodes/core/source/AST/index.ts @@ -114,6 +114,8 @@ const traverseASTNode: TraverseFunction = (fn, node) => { return traverseArrayNode(fn, node) case 'durée': return traverseDuréeNode(fn, node) + case 'résoudre référence circulaire': + return traverseRésoudreRéférenceCirculaireNode(fn, node) case 'inversion': return traverseInversionNode(fn, node) case 'operation': @@ -261,6 +263,17 @@ const traversePlancherNode: TraverseFunction<'plancher'> = (fn, node) => ({ }, }) +const traverseRésoudreRéférenceCirculaireNode: TraverseFunction<'résoudre référence circulaire'> = ( + fn, + node +) => ({ + ...node, + explanation: { + ...node.explanation, + valeur: fn(node.explanation.valeur), + }, +}) + const traversePlafondNode: TraverseFunction<'plafond'> = (fn, node) => ({ ...node, explanation: { diff --git a/publicodes/core/source/AST/types.ts b/publicodes/core/source/AST/types.ts index 5cfa47b27..92349e638 100644 --- a/publicodes/core/source/AST/types.ts +++ b/publicodes/core/source/AST/types.ts @@ -1,24 +1,23 @@ import { AbattementNode } from '../mecanisms/abattement' import { ApplicableSiNode } from '../mecanisms/applicable' import { ArrondiNode } from '../mecanisms/arrondi' -import { OperationNode } from '../mecanisms/operation' import { BarèmeNode } from '../mecanisms/barème' -import { ReferenceNode } from '../reference' -import { RuleNode } from '../rule' import { TouteCesConditionsNode } from '../mecanisms/condition-allof' import { UneDeCesConditionsNode } from '../mecanisms/condition-oneof' import { DuréeNode } from '../mecanisms/durée' import { GrilleNode } from '../mecanisms/grille' import { InversionNode } from '../mecanisms/inversion' import { MaxNode } from '../mecanisms/max' -import { PlafondNode } from '../mecanisms/plafond' import { MinNode } from '../mecanisms/min' import { NonApplicableSiNode } from '../mecanisms/nonApplicable' +import { PossibilityNode } from '../mecanisms/one-possibility' +import { OperationNode } from '../mecanisms/operation' import { ParDéfautNode } from '../mecanisms/parDéfaut' +import { PlafondNode } from '../mecanisms/plafond' import { PlancherNode } from '../mecanisms/plancher' import { ProductNode } from '../mecanisms/product' import { RecalculNode } from '../mecanisms/recalcul' -import { PossibilityNode } from '../mecanisms/one-possibility' +import { RésoudreRéférenceCiruclaireNode } from '../mecanisms/résoudre-référence-circulaire' import { SituationNode } from '../mecanisms/situation' import { SommeNode } from '../mecanisms/sum' import { SynchronisationNode } from '../mecanisms/synchronisation' @@ -26,7 +25,9 @@ import { TauxProgressifNode } from '../mecanisms/tauxProgressif' import { UnitéNode } from '../mecanisms/unité' import { VariableTemporelleNode } from '../mecanisms/variableTemporelle' import { VariationNode } from '../mecanisms/variations' +import { ReferenceNode } from '../reference' import { ReplacementRule } from '../replacement' +import { RuleNode } from '../rule' import { Temporal } from '../temporal' export type ConstantNode = { @@ -57,6 +58,7 @@ export type ASTNode = ( | PlancherNode | ProductNode | RecalculNode + | RésoudreRéférenceCiruclaireNode | SituationNode | SommeNode | SynchronisationNode diff --git a/publicodes/core/source/evaluation.ts b/publicodes/core/source/evaluation.ts index aef471908..447b67c42 100644 --- a/publicodes/core/source/evaluation.ts +++ b/publicodes/core/source/evaluation.ts @@ -2,8 +2,8 @@ import Engine, { EvaluationFunction } from '.' import { ASTNode, ConstantNode, - Evaluation, EvaluatedNode, + Evaluation, NodeKind, } from './AST/types' import { warning } from './error' @@ -53,7 +53,7 @@ function convertNodesToSameUnit(this: Engine, nodes, mecanismName) { } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], `Les unités des éléments suivants sont incompatibles entre elles : \n\t\t${ node?.name || node?.rawNode }\n\t\t${firstNodeWithUnit?.name || firstNodeWithUnit?.rawNode}'`, diff --git a/publicodes/core/source/index.ts b/publicodes/core/source/index.ts index 5827307b4..4cbf72475 100644 --- a/publicodes/core/source/index.ts +++ b/publicodes/core/source/index.ts @@ -12,15 +12,18 @@ import { Rule, RuleNode } from './rule' import * as utils from './ruleUtils' import { formatUnit, getUnitKey } from './units' -const emptyCache = () => ({ - _meta: { ruleStack: [] }, +const emptyCache = (): Cache => ({ + _meta: { + parentRuleStack: [], + evaluationRuleStack: [], + }, nodes: new Map(), }) type Cache = { _meta: { - ruleStack: Array - parentEvaluationStack?: Array + parentRuleStack: Array + evaluationRuleStack: Array inversionFail?: | { given: string diff --git a/publicodes/core/source/mecanisms/abattement.ts b/publicodes/core/source/mecanisms/abattement.ts index 956bd0100..33dc9b936 100644 --- a/publicodes/core/source/mecanisms/abattement.ts +++ b/publicodes/core/source/mecanisms/abattement.ts @@ -25,7 +25,7 @@ const evaluateAbattement: EvaluationFunction<'abattement'> = function (node) { } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], "Impossible de convertir les unités de l'allègement entre elles", e ) diff --git a/publicodes/core/source/mecanisms/inversion.ts b/publicodes/core/source/mecanisms/inversion.ts index aafad83dc..dd540ea0f 100644 --- a/publicodes/core/source/mecanisms/inversion.ts +++ b/publicodes/core/source/mecanisms/inversion.ts @@ -1,9 +1,9 @@ -import parse from '../parse' import { EvaluationFunction } from '..' import { ConstantNode, Unit } from '../AST/types' import { mergeMissing } from '../evaluation' import { registerEvaluationFunction } from '../evaluationFunctions' import { convertNodeToUnit } from '../nodeUnits' +import parse from '../parse' import { Context } from '../parsePublicodes' import { ReferenceNode } from '../reference' import uniroot from '../uniroot' @@ -54,7 +54,6 @@ export const evaluateInversion: EvaluationFunction<'inversion'> = function ( } const evaluatedInversionGoal = this.evaluate(inversionGoal) const unit = 'unit' in node ? node.unit : evaluatedInversionGoal.unit - const originalCache = this.cache const originalSituation = { ...this.parsedSituation } let inversionNumberOfIterations = 0 @@ -63,7 +62,6 @@ export const evaluateInversion: EvaluationFunction<'inversion'> = function ( inversionNumberOfIterations++ this.resetCache() this.cache._meta = { ...originalCache._meta } - this.parsedSituation[node.explanation.ruleToInverse] = { unit: unit, nodeKind: 'unité', @@ -139,9 +137,11 @@ export const evaluateInversion: EvaluationFunction<'inversion'> = function ( // // Uncomment to display the two attempts and their result // console.table([{ x: x1, y: y1 }, { x: x2, y: y2 }]) - // console.log('iteration:', inversionNumberOfIterations) + // console.log('iteration inversion:', inversionNumberOfIterations) + this.cache = originalCache this.parsedSituation = originalSituation + return { ...node, unit, diff --git a/publicodes/core/source/mecanisms/plafond.ts b/publicodes/core/source/mecanisms/plafond.ts index 952a55e23..0743bfe66 100644 --- a/publicodes/core/source/mecanisms/plafond.ts +++ b/publicodes/core/source/mecanisms/plafond.ts @@ -1,4 +1,3 @@ -import { last } from 'ramda' import { EvaluationFunction } from '..' import { ASTNode } from '../AST/types' import { warning } from '../error' @@ -28,7 +27,7 @@ const evaluate: EvaluationFunction<'plafond'> = function (node) { } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], "L'unité du plafond n'est pas compatible avec celle de la valeur à encadrer", e ) diff --git a/publicodes/core/source/mecanisms/plancher.ts b/publicodes/core/source/mecanisms/plancher.ts index 6f06f4425..c4578e305 100644 --- a/publicodes/core/source/mecanisms/plancher.ts +++ b/publicodes/core/source/mecanisms/plancher.ts @@ -26,7 +26,7 @@ const evaluate: EvaluationFunction<'plancher'> = function (node) { } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], "L'unité du plancher n'est pas compatible avec celle de la valeur à encadrer", e ) diff --git a/publicodes/core/source/mecanisms/product.ts b/publicodes/core/source/mecanisms/product.ts index 334aac143..612df9b95 100644 --- a/publicodes/core/source/mecanisms/product.ts +++ b/publicodes/core/source/mecanisms/product.ts @@ -44,7 +44,7 @@ const productEffect: EvaluationFunction = function ({ } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], "Impossible de convertir l'unité du plafond du produit dans celle de l'assiette", e ) diff --git a/publicodes/core/source/mecanisms/résoudre-référence-circulaire.ts b/publicodes/core/source/mecanisms/résoudre-référence-circulaire.ts new file mode 100644 index 000000000..b7e4d70d2 --- /dev/null +++ b/publicodes/core/source/mecanisms/résoudre-référence-circulaire.ts @@ -0,0 +1,109 @@ +import { EvaluationFunction } from '..' +import { ASTNode, ConstantNode, Unit } from '../AST/types' +import { registerEvaluationFunction } from '../evaluationFunctions' +import parse from '../parse' +import { Context } from '../parsePublicodes' +import uniroot from '../uniroot' +import { UnitéNode } from './unité' + +export type RésoudreRéférenceCiruclaireNode = { + explanation: { + ruleToSolve: string + valeur: ASTNode + } + nodeKind: 'résoudre référence circulaire' +} + +export const evaluateRésoudreRéférenceCirculaire: EvaluationFunction<'résoudre référence circulaire'> = function ( + node +) { + const originalCache = this.cache + let inversionNumberOfIterations = 0 + + const evaluateWithValue = ( + n: number, + unit: Unit = { numerators: [], denominators: [] } + ) => { + inversionNumberOfIterations++ + this.resetCache() + + this.parsedSituation[node.explanation.ruleToSolve] = { + unit: unit, + nodeKind: 'unité', + explanation: { + nodeKind: 'constant', + nodeValue: n, + type: 'number', + } as ConstantNode, + } as UnitéNode + return this.evaluate(node.explanation.valeur) + } + + let nodeValue: number | null | undefined = null + + const x0 = 0 + let valeur = evaluateWithValue(x0) + + const y0 = valeur.nodeValue as number + const unit = valeur.unit + const missingVariables = valeur.missingVariables + let i = 0 + if (y0 !== null) { + // The `uniroot` function parameter. It will be called with its `min` and + // `max` arguments, so we can use our cached nodes if the function is called + // with the already computed x1 or x2. + const test = (x: number): number => { + if (x === x0) { + return y0 - x0 + } + valeur = evaluateWithValue(x, unit) + const y = valeur.nodeValue + i++ + return (y as number) - x + } + + const defaultMin = -1_000_000 + const defaultMax = 100_000_000 + + nodeValue = uniroot(test, defaultMin, defaultMax, 1, 30, 2) + } + if (nodeValue === undefined) { + nodeValue = null + this.cache._meta.inversionFail = true + } + if (nodeValue != null) { + originalCache.nodes.forEach((v, k) => this.cache.nodes.set(k, v)) + } + console.log('iteration résoudre référence circulaire :', i) + + this.cache = originalCache + delete this.parsedSituation[node.explanation.ruleToSolve] + return { + ...node, + unit, + nodeValue, + explanation: { + ...node.explanation, + valeur, + inversionNumberOfIterations, + }, + missingVariables, + } +} + +export default function parseRésoudreRéférenceCirculaire(v, context: Context) { + return { + explanation: { + ruleToSolve: context.dottedName, + valeur: parse(v.valeur, context), + }, + nodeKind: 'résoudre référence circulaire', + } as RésoudreRéférenceCiruclaireNode +} + +parseRésoudreRéférenceCirculaire.nom = 'résoudre la référence circulaire' + +registerEvaluationFunction( + 'résoudre référence circulaire', + evaluateRésoudreRéférenceCirculaire +) diff --git a/publicodes/core/source/mecanisms/trancheUtils.ts b/publicodes/core/source/mecanisms/trancheUtils.ts index 2b430e06b..415240676 100644 --- a/publicodes/core/source/mecanisms/trancheUtils.ts +++ b/publicodes/core/source/mecanisms/trancheUtils.ts @@ -63,7 +63,7 @@ export function evaluatePlafondUntilActiveTranche( } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], `L'unité du plafond de la tranche n°${ i + 1 } n'est pas compatible avec celle l'assiette`, @@ -103,7 +103,7 @@ export function evaluatePlafondUntilActiveTranche( ) { evaluationError( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], `Le plafond de la tranche n°${ i + 1 } a une valeur inférieure à celui de la tranche précédente` diff --git a/publicodes/core/source/mecanisms/unité.ts b/publicodes/core/source/mecanisms/unité.ts index ac57c027c..a27b39e41 100644 --- a/publicodes/core/source/mecanisms/unité.ts +++ b/publicodes/core/source/mecanisms/unité.ts @@ -37,7 +37,7 @@ registerEvaluationFunction(parseUnité.nom, function evaluate(node) { } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], "Erreur lors de la conversion d'unité explicite", e ) diff --git a/publicodes/core/source/mecanisms/variations.ts b/publicodes/core/source/mecanisms/variations.ts index 7cff54a1c..1c8c3b9c0 100644 --- a/publicodes/core/source/mecanisms/variations.ts +++ b/publicodes/core/source/mecanisms/variations.ts @@ -120,7 +120,7 @@ const evaluate: EvaluationFunction<'variations'> = function (node) { } catch (e) { warning( this.options.logger, - this.cache._meta.ruleStack[0], + this.cache._meta.evaluationRuleStack[0], `L'unité de la branche n° ${ i + 1 } du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`, diff --git a/publicodes/core/source/parse.ts b/publicodes/core/source/parse.ts index 04cabbb0d..46bbb96ba 100644 --- a/publicodes/core/source/parse.ts +++ b/publicodes/core/source/parse.ts @@ -22,6 +22,7 @@ import plafond from './mecanisms/plafond' import plancher from './mecanisms/plancher' import { mecanismProduct } from './mecanisms/product' import { mecanismRecalcul } from './mecanisms/recalcul' +import résoudreRéférenceCirculaire from './mecanisms/résoudre-référence-circulaire' import situation from './mecanisms/situation' import { mecanismSum } from './mecanisms/sum' import { mecanismSynchronisation } from './mecanisms/synchronisation' @@ -147,6 +148,7 @@ ${e.message}` } } +// Chainable mecanisme in their composition order (first one is applyied first) const chainableMecanisms = [ applicable, nonApplicable, @@ -156,6 +158,7 @@ const chainableMecanisms = [ plafond, parDéfaut, situation, + résoudreRéférenceCirculaire, abattement, ] function parseChainedMecanisms(rawNode, context: Context): ASTNode { diff --git a/publicodes/core/source/reference.ts b/publicodes/core/source/reference.ts index 6a4a5afaf..8e98ff256 100644 --- a/publicodes/core/source/reference.ts +++ b/publicodes/core/source/reference.ts @@ -1,8 +1,6 @@ -import { EvaluatedNode } from './AST/types' import { InternalError } from './error' import { registerEvaluationFunction } from './evaluationFunctions' import { Context } from './parsePublicodes' -import { RuleNode } from './rule' export type ReferenceNode = { nodeKind: 'reference' @@ -26,6 +24,7 @@ registerEvaluationFunction('reference', function evaluateReference(node) { if (!node.dottedName) { throw new InternalError(node) } + const explanation = this.evaluate(this.parsedRules[node.dottedName]) return { ...node, diff --git a/publicodes/core/source/rule.ts b/publicodes/core/source/rule.ts index d4493aec1..172deed64 100644 --- a/publicodes/core/source/rule.ts +++ b/publicodes/core/source/rule.ts @@ -1,6 +1,8 @@ import { ASTNode, EvaluatedNode } from './AST/types' +import { warning } from './error' import { bonus, mergeMissing } from './evaluation' import { registerEvaluationFunction } from './evaluationFunctions' +import { capitalise0 } from './format' import parse, { mecanismKeys } from './parse' import { Context } from './parsePublicodes' import { ReferenceNode } from './reference' @@ -10,7 +12,6 @@ import { ReplacementRule, } from './replacement' import { nameLeaf, ruleParents } from './ruleUtils' -import { capitalise0 } from './format' export type Rule = { formule?: Record | string @@ -119,25 +120,53 @@ export default function parseRule( } registerEvaluationFunction('rule', function evaluate(node) { - if (this.cache[node.dottedName]) { - return this.cache[node.dottedName] - } const explanation = { ...node.explanation } - - const verifyParentApplicability = !this.cache._meta.ruleStack.includes( - node.dottedName - ) - this.cache._meta.ruleStack.unshift(node.dottedName) let parent: EvaluatedNode | null = null - if (explanation.parent && verifyParentApplicability) { - parent = this.evaluate(explanation.parent) as EvaluatedNode + if (explanation.parent) { + if (this.cache._meta.parentRuleStack.includes(node.dottedName)) { + parent = { nodeValue: null } as EvaluatedNode + } else { + this.cache._meta.parentRuleStack.unshift(node.dottedName) + parent = this.evaluate(explanation.parent) as EvaluatedNode + this.cache._meta.parentRuleStack.shift() + } explanation.parent = parent } let valeur: EvaluatedNode | null = null if (!parent || parent.nodeValue !== false) { - valeur = this.evaluate(explanation.valeur) as EvaluatedNode + if ( + this.cache._meta.evaluationRuleStack.filter( + (dottedName) => dottedName === node.dottedName + ).length > 15 // I don't know why this magic number, but below, cycle are detected "too early", which leads to blank value in brut-net simulator + ) { + warning( + this.options.logger, + node.dottedName, + ` + Un cycle a été détecté dans lors de l'évaluation de cette règle. + Par défaut cette règle sera évaluée à 'null'. + + Pour indiquer au moteur de résoudre la référence circulaire en trouvant le point fixe + de la fonction, il vous suffit d'ajouter l'attribut suivant niveau de la règle : + + ${node.dottedName}: + "résoudre la référence circulaire: oui" + ... + + ` + ) + + valeur = { nodeValue: null } as EvaluatedNode + } else { + this.cache._meta.evaluationRuleStack.unshift(node.dottedName) + valeur = this.evaluate(explanation.valeur) as EvaluatedNode + this.cache._meta.evaluationRuleStack.shift() + } + explanation.valeur = valeur } + // if (valeur.nodeValue === '') { + const evaluation = { ...node, explanation, @@ -148,7 +177,5 @@ registerEvaluationFunction('rule', function evaluate(node) { ), ...(valeur && 'unit' in valeur && { unit: valeur.unit }), } - this.cache._meta.ruleStack.shift() - this.cache[node.dottedName] = evaluation return evaluation }) diff --git a/publicodes/core/source/uniroot.ts b/publicodes/core/source/uniroot.ts index 3023d7f86..e1fd03744 100644 --- a/publicodes/core/source/uniroot.ts +++ b/publicodes/core/source/uniroot.ts @@ -109,7 +109,9 @@ export default function uniroot( if ((fb > 0 && fc > 0) || (fb < 0 && fc < 0)) { ;(c = a), (fc = fa) // Adjust c for it to have a sign opposite to that of b } - + if (Math.abs(fb) < errorTol) { + return b + } if (Math.abs(fb) < acceptableErrorTol) { fallback = b } diff --git a/publicodes/core/test/mécanismes/résoudre-référence-circulaire.yaml b/publicodes/core/test/mécanismes/résoudre-référence-circulaire.yaml new file mode 100644 index 000000000..841abfefb --- /dev/null +++ b/publicodes/core/test/mécanismes/résoudre-référence-circulaire.yaml @@ -0,0 +1,68 @@ +fx: +x: + résoudre la référence circulaire: oui + valeur: fx + exemples: + - nom: affine + situation: + fx: 200 - x + valeur attendue: 100 + - nom: quadratique + situation: + fx: 0.2 * x * x - 400 * x + 500 + valeur attendue: 2003.743 + # CF https://www.wolframalpha.com/input/?i=x%3D0.2x%C2%B2-400x%2B500 + +CA: + unité: € + plancher: 0€ + formule: + inversion numérique: + avec: + - net + +net: + résoudre la référence circulaire: oui + unité: € + formule: CA - 50% * net + + +net après impôt: + formule: 80% * net + unité: € + +cycle avec inversion et situation vide: + exemples: + - nom: CA + situation: + cycle avec inversion et situation vide: CA + valeur attendue: null + # - nom: net + # situation: + # cycle avec inversion et situation vide: net + # valeur attendue: null + # - nom: net après impôt + # situation: + # cycle avec inversion et situation vide: net après impôt + # valeur attendue: null + +cycle avec la règle à inverser fixée dans la situation: + valeur: net + exemples: + - situation: + CA: 10000 + valeur attendue: 6666.666 + +cycle avec la règle du cycle fixée dans la situation: + valeur: CA + exemples: + - situation: + net: 1000 + valeur attendue: 1500 + +# cycle avec une règle reliée fixée dans la situation: +# valeur: net +# exemples: +# - situation: +# net après impôt: 8000 +# valeur attendue: 10000 diff --git a/publicodes/docs/mecanisms.yaml b/publicodes/docs/mecanisms.yaml index 5615c95cc..4bf0ccc36 100644 --- a/publicodes/docs/mecanisms.yaml +++ b/publicodes/docs/mecanisms.yaml @@ -494,6 +494,7 @@ synchronisation: n'est pas stable. Se référer aux exemples existants. inversion numérique: + chainable: oui description: >- Il est souhaitable de rédiger les règles de calcul en publicodes de la même façon qu'elles sont décrites dans la loi ou les @@ -523,3 +524,37 @@ inversion numérique: (calculée ou saisie), et procéder à l'inversion décrite plus haut à partir de celle-ci. Sinon, ces possibilités d'inversions seront listées comme manquantes. + +résoudre la référence circulaire: + description: | + Active le calcul itératif pour trouver la valeur de la règle qui résout + la référence circulaire. + + Il est possible pour une règle de se référencer elle-même. Par défaut, le + moteur considère qu'il s'agit d'un cycle non voulu, et renvoie 'null' comme valeur + pour la règle en question, en affichant un avertissement. + + Mais dans certains cas, la formule est bonne et le cycle est voulu. La valeur de la + règle attendue est donc celle qui résout l'équation obtenue via la référence cyclique. + + Lorsque l'on active cette fonctionnalité, le moteur va procéder par essai-erreur jusqu'à + trouver cette valeur. + + Note : la résolution de cycle est coûteuse en temps de calcul. Il faut donc veiller à + ne pas la cumuler avec l'évaluation d'un autre mécanisme coûteux comme l'inversion numérique + par exemple. + + + exemples: + base: >- + x: + valeur: 4 * x - 5 + résoudre la référence circulaire: oui + calcul du revenu professionnel: >- + chiffre d'affaires: 10000 €/an + + cotisations: 25% * revenu professionnel + + revenu professionnel: + valeur: chiffre d'affaires - cotisations + résoudre la référence circulaire: oui diff --git a/publicodes/ui-react/source/Explanation.tsx b/publicodes/ui-react/source/Explanation.tsx index 76ce028a2..bafe2d689 100644 --- a/publicodes/ui-react/source/Explanation.tsx +++ b/publicodes/ui-react/source/Explanation.tsx @@ -1,8 +1,10 @@ -import { ConstantNode, Leaf } from './mecanisms/common' +import { useContext } from 'react' +import { EngineContext } from './contexts' import Abattement from './mecanisms/Abattement' import ApplicableSi from './mecanisms/Applicable' import Arrondi from './mecanisms/Arrondi' import Barème from './mecanisms/Barème' +import { ConstantNode, Leaf } from './mecanisms/common' import Composantes from './mecanisms/Composantes' import Durée from './mecanisms/Durée' import Grille from './mecanisms/Grille' @@ -19,6 +21,7 @@ import Recalcul from './mecanisms/Recalcul' import Replacement from './mecanisms/Replacement' import ReplacementRule from './mecanisms/ReplacementRule' import Rule from './mecanisms/Rule' +import RésoudreRéférenceCirculaire from './mecanisms/RésoudreRéférenceCirculaire' import Situation from './mecanisms/Situation' import Somme from './mecanisms/Somme' import Synchronisation from './mecanisms/Synchronisation' @@ -28,8 +31,6 @@ import UneDeCesConditions from './mecanisms/UneDeCesConditions' import UnePossibilité from './mecanisms/UnePossibilité' import Unité from './mecanisms/Unité' import Variations from './mecanisms/Variations' -import { useContext } from 'react' -import { EngineContext } from './contexts' const UIComponents = { constant: ConstantNode, @@ -61,6 +62,7 @@ const UIComponents = { 'toutes ces conditions': ToutesCesConditions, 'une de ces conditions': UneDeCesConditions, 'une possibilité': UnePossibilité, + 'résoudre référence circulaire': RésoudreRéférenceCirculaire, unité: Unité, 'variable temporelle': () => '[variable temporelle]', variations: Variations, diff --git a/publicodes/ui-react/source/mecanisms/RésoudreRéférenceCirculaire.tsx b/publicodes/ui-react/source/mecanisms/RésoudreRéférenceCirculaire.tsx new file mode 100644 index 000000000..c02c285f3 --- /dev/null +++ b/publicodes/ui-react/source/mecanisms/RésoudreRéférenceCirculaire.tsx @@ -0,0 +1,19 @@ +import Explanation from '../Explanation' +import { Mecanism } from './common' + +export default function MecanismRésoudreRéférenceCirculaire({ explanation }) { + return ( + +

+ {' '} + Cette valeur a été retrouvé en résolvant la référence circulaire dans la + formule ci dessous :{' '} +

+ + +
+ ) +} diff --git a/publicodes/ui-react/source/rule/RulePage.tsx b/publicodes/ui-react/source/rule/RulePage.tsx index fc354b0fe..ef9e59fc7 100644 --- a/publicodes/ui-react/source/rule/RulePage.tsx +++ b/publicodes/ui-react/source/rule/RulePage.tsx @@ -19,7 +19,6 @@ export default function Rule({ dottedName, engine, language }) { return

Cette règle est introuvable dans la base

} const rule = engine.evaluate(engine.getRule(dottedName)) - const { description, question } = rule.rawNode const { parent, valeur } = rule.explanation return (