diff --git a/e2e/calculate-all-params.e2e-spec.ts b/e2e/calculate-all-params.e2e-spec.ts
index 9533c2fca374524be2406530d9c2c63526783d37..f865b65b4acae6de12ea0668c6806f48a2e0e41d 100644
--- a/e2e/calculate-all-params.e2e-spec.ts
+++ b/e2e/calculate-all-params.e2e-spec.ts
@@ -23,6 +23,7 @@ describe("ngHyd − calculate all parameters of all calculators", () => {
     11, 12, 13, 15, 17, 18, 19, 20,
     21,
     // 22 - Solveur is not calculated here because it is not independent
+    23, 24, 25
   ];
 
   // for each calculator
diff --git a/e2e/calculator.po.ts b/e2e/calculator.po.ts
index 59910d18d4cca60e5cae4b6dbbace582f84701d2..c324c85c35fd2981e3f79ea227d0dbe962da3a1c 100644
--- a/e2e/calculator.po.ts
+++ b/e2e/calculator.po.ts
@@ -196,10 +196,12 @@ export class CalculatorPage {
     const inputs = this.getParamInputs();
     await inputs.each(async (i) => {
       if (await i.isDisplayed()) {
+        // N in YAXN child of SPP module must not be float
+        const isN = (await i.getAttribute("id")).includes("_N"); // @TODO strengthen this clodo test
         const hasDot = (await i.getAttribute("value")).includes(".");
         const hasExponent = (await i.getAttribute("value")).includes("e");
         let keys = "" + Math.floor(Math.random() * 9) + 1;
-        if (! hasDot && ! hasExponent) {
+        if (! hasDot && ! hasExponent && ! isN) {
           keys = "." + keys;
         }
         await i.sendKeys(keys);
diff --git a/e2e/check-translations.e2e-spec.ts b/e2e/check-translations.e2e-spec.ts
index b0c10f97084467c212a1497b3425d4b21b9cbe47..69b622c5962ad22d05a394779a253e3dfdc5c113 100644
--- a/e2e/check-translations.e2e-spec.ts
+++ b/e2e/check-translations.e2e-spec.ts
@@ -25,7 +25,7 @@ describe("ngHyd − check translation of all calculators", () => {
   });
 
   // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/
-  const calcTypes = [ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21, 22 ];
+  const calcTypes = [ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25 ];
 
   // options of "Language" selector on preferences page
   const langs = [ "English", "Français" ];
diff --git a/e2e/clone-all-calc.e2e-spec.ts b/e2e/clone-all-calc.e2e-spec.ts
index 8c32c1ff622ac258da4f6cd15fc6e03d7c434146..95168422ee8dd785c34b8602bfb069d271cfbb2c 100644
--- a/e2e/clone-all-calc.e2e-spec.ts
+++ b/e2e/clone-all-calc.e2e-spec.ts
@@ -23,6 +23,7 @@ describe("ngHyd − clone all calculators with all possible <select> values", ()
     11, 12, 13, 15, 17, 18, 19, 20,
     21,
     // 22 - Solveur is not cloned here because it is not independent
+    23, 24, 25
   ];
 
   // for each calculator
diff --git a/jalhyd_branch b/jalhyd_branch
index 27ca6c76e1b922d9099ed9e32d0ff1db90526ce3..3655f37254651c8ed297a08824278f2d111e642e 100644
--- a/jalhyd_branch
+++ b/jalhyd_branch
@@ -1 +1 @@
-165-ajout-de-la-loi-d-ouvrage-de-deversoir-noye
+160-ajout-du-module-mathematique-y-a-x-b
diff --git a/src/app/calculators/spp/spp.config.json b/src/app/calculators/spp/spp.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..0c3c2427242c7a15825837810866460d93f825a8
--- /dev/null
+++ b/src/app/calculators/spp/spp.config.json
@@ -0,0 +1,38 @@
+[
+    {
+        "id": "fs_spp",
+        "type": "fieldset",
+        "defaultOperation": "SUM",
+        "fields": [
+            {
+                "id": "select_spp_operation",
+                "type": "select",
+                "source": "spp_operation"
+            },
+            "Y"
+        ]
+    },
+    {
+        "id": "fs_yaxn",
+        "type": "fieldset_template",
+        "calcType": "YAXN",
+        "fields": [
+            "A",
+            "X",
+            "N"
+        ]
+    },
+    {
+        "id": "yaxn_container",
+        "type": "template_container",
+        "templates": [
+            "fs_yaxn"
+        ]
+    },
+    {
+        "type": "options",
+        "idCal": "Y",
+        "operationSelectId": "select_spp_operation",
+        "_help": "util/trigo.html"
+    }
+]
\ No newline at end of file
diff --git a/src/app/calculators/spp/spp.en.json b/src/app/calculators/spp/spp.en.json
new file mode 100644
index 0000000000000000000000000000000000000000..cbc4a10f3562024223f4537006b243e6b316b3ec
--- /dev/null
+++ b/src/app/calculators/spp/spp.en.json
@@ -0,0 +1,17 @@
+{
+    "fs_spp": "Parameters",
+
+    "select_spp_operation": "Operation",
+    "select_spp_operation_0": "Sum",
+    "select_spp_operation_1": "Product",
+
+    "Y": "Y",
+
+    "yaxn_container": "Powers",
+
+    "fs_yaxn": "Power",
+
+    "A": "A",
+    "X": "X",
+    "N": "N"
+}
\ No newline at end of file
diff --git a/src/app/calculators/spp/spp.fr.json b/src/app/calculators/spp/spp.fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..dd2ae4c5b2f68fead14d3e1e64e0ec221cdd1237
--- /dev/null
+++ b/src/app/calculators/spp/spp.fr.json
@@ -0,0 +1,17 @@
+{
+    "fs_spp": "Paramètres",
+
+    "select_spp_operation": "Opération",
+    "select_spp_operation_0": "Somme",
+    "select_spp_operation_1": "Produit",
+
+    "Y": "Y",
+
+    "yaxn_container": "Puissances",
+
+    "fs_yaxn": "Puissance",
+
+    "A": "A",
+    "X": "X",
+    "N": "N"
+}
\ No newline at end of file
diff --git a/src/app/calculators/trigo/trigo.config.json b/src/app/calculators/trigo/trigo.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..6dbb51a8c4590022f6b10d9fc72652b9c7682b63
--- /dev/null
+++ b/src/app/calculators/trigo/trigo.config.json
@@ -0,0 +1,32 @@
+[
+    {
+        "id": "fs_trigo",
+        "type": "fieldset",
+        "defaultOperation": "COS",
+        "defaultUnit": "DEG",
+        "fields": [
+            {
+                "id": "select_operation",
+                "type": "select",
+                "source": "trigo_operation"
+            },
+            {
+                "id": "select_unit",
+                "type": "select",
+                "source": "trigo_unit"
+            }
+        ]
+    },
+    {
+        "id": "fs_params",
+        "type": "fieldset",
+        "fields": [ "X", "Y" ]
+    },
+    {
+        "type": "options",
+        "idCal": "Y",
+        "operationSelectId": "select_operation",
+        "unitSelectId": "select_unit",
+        "_help": "util/trigo.html"
+    }
+]
\ No newline at end of file
diff --git a/src/app/calculators/trigo/trigo.en.json b/src/app/calculators/trigo/trigo.en.json
new file mode 100644
index 0000000000000000000000000000000000000000..9208d6dcaed2d4670e24fe0729678c00fac881dc
--- /dev/null
+++ b/src/app/calculators/trigo/trigo.en.json
@@ -0,0 +1,20 @@
+{
+    "fs_trigo": "Trigonometric parameters",
+
+    "select_operation": "Operation",
+    "select_operation_0": "cos",
+    "select_operation_1": "sin",
+    "select_operation_2": "tan",
+    "select_operation_3": "cosh",
+    "select_operation_4": "sinh",
+    "select_operation_5": "tanh",
+
+    "select_unit": "Unit",
+    "select_unit_0": "Degrees",
+    "select_unit_1": "Radians",
+
+    "fs_params": "Equation parameters",
+
+    "X": "X",
+    "Y": "Y"
+}
\ No newline at end of file
diff --git a/src/app/calculators/trigo/trigo.fr.json b/src/app/calculators/trigo/trigo.fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..15c5a8a4eaaeb08f0ba37d9cc332e6e2c8a4b52d
--- /dev/null
+++ b/src/app/calculators/trigo/trigo.fr.json
@@ -0,0 +1,20 @@
+{
+    "fs_trigo": "Paramètres trigonométriques",
+
+    "select_operation": "Opération",
+    "select_operation_0": "cos",
+    "select_operation_1": "sin",
+    "select_operation_2": "tan",
+    "select_operation_3": "cosh",
+    "select_operation_4": "sinh",
+    "select_operation_5": "tanh",
+
+    "select_unit": "Unité",
+    "select_unit_0": "Degrés",
+    "select_unit_1": "Radians",
+
+    "fs_params": "Paramètres de l'équation",
+
+    "X": "X",
+    "Y": "Y"
+}
\ No newline at end of file
diff --git a/src/app/calculators/yaxb/yaxb.config.json b/src/app/calculators/yaxb/yaxb.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..1a02898b73e6ad4d0d99160054e9b9608b6cec18
--- /dev/null
+++ b/src/app/calculators/yaxb/yaxb.config.json
@@ -0,0 +1,12 @@
+[
+    {
+        "id": "fs_yaxb",
+        "type": "fieldset",
+        "fields": [ "Y", "A", "X", "B" ]
+    },
+    {
+        "type": "options",
+        "idCal": "Y",
+        "_help": "util/yaxb.html"
+    }
+]
\ No newline at end of file
diff --git a/src/app/calculators/yaxb/yaxb.en.json b/src/app/calculators/yaxb/yaxb.en.json
new file mode 100644
index 0000000000000000000000000000000000000000..a3595ae59ad8bbdccab7c9c9b2398f64e279f596
--- /dev/null
+++ b/src/app/calculators/yaxb/yaxb.en.json
@@ -0,0 +1,8 @@
+{
+    "fs_yaxb": "Equation parameters",
+
+    "Y": "Y",
+    "A": "A",
+    "X": "X",
+    "B": "B"
+}
\ No newline at end of file
diff --git a/src/app/calculators/yaxb/yaxb.fr.json b/src/app/calculators/yaxb/yaxb.fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..97f602e313f8419c7c5cf10da2dfd71ce7faeb5a
--- /dev/null
+++ b/src/app/calculators/yaxb/yaxb.fr.json
@@ -0,0 +1,8 @@
+{
+    "fs_yaxb": "Paramètres de l'équation",
+
+    "Y": "Y",
+    "A": "A",
+    "X": "X",
+    "B": "B"
+}
\ No newline at end of file
diff --git a/src/app/components/calculator-list/calculator-list.component.ts b/src/app/components/calculator-list/calculator-list.component.ts
index 94552a7d5f97a6680ad543569b18048308af6d1f..7e801fc650a1145c914b44e84d4d7d604fc1a723 100644
--- a/src/app/components/calculator-list/calculator-list.component.ts
+++ b/src/app/components/calculator-list/calculator-list.component.ts
@@ -12,6 +12,7 @@ import { FormulairePab } from "../../formulaire/definition/concrete/form-pab";
 import { HttpService } from "../../services/http.service";
 import { AppComponent } from "../../app.component";
 import { FormulaireMacrorugoCompound } from "../../formulaire/definition/concrete/form-macrorugo-compound";
+import { FormulaireSPP } from "../../formulaire/definition/concrete/form-spp";
 
 
 @Component({
@@ -87,6 +88,7 @@ export class CalculatorListComponent implements OnInit {
                         t !== CalculatorType.Structure
                         && t !== CalculatorType.Section
                         && t !== CalculatorType.CloisonAval
+                        && t !== CalculatorType.YAXN
                     ) {
                         unusedTheme.calculators.push({
                             type: t,
@@ -135,6 +137,15 @@ export class CalculatorListComponent implements OnInit {
                     }
                 }
             }
+            // on ajoute un YAXN après l'ouverture du module de calcul "somme / produit de puissances"
+            if (f instanceof FormulaireSPP) {
+                for (const e of f.allFormElements) {
+                    if (e instanceof FieldsetContainer) {
+                        e.addFromTemplate(0);
+                        break;
+                    }
+                }
+            }
         });
     }
 
@@ -196,7 +207,9 @@ export class CalculatorListComponent implements OnInit {
 
     public onKC() {
         for (const i of this.items) {
-            i.image.path = "assets/images/themes/sp.jpg";
+            const img = [ "assets/images/themes/sp.jpg", "assets/images/themes/autres.jpg" ];
+            const idx = Math.floor(Math.random() * 2);
+            i.image.path = img[idx];
             i.image.credits = "lol";
         }
     }
diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts
index 17b4a031842cd12a666edb9e49018c440fef991b..8f22524ebefbcd33f6350777ceabc413423df9e4 100644
--- a/src/app/components/fieldset-container/fieldset-container.component.ts
+++ b/src/app/components/fieldset-container/fieldset-container.component.ts
@@ -67,7 +67,7 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
     public constructor(private i18nService: I18nService) {}
 
     /**
-     * Ajoute un nouveau sous-nub (Structure, PabCloisons selon le cas)
+     * Ajoute un nouveau sous-nub (Structure, PabCloisons, YAXN… selon le cas)
      * dans un nouveau fieldset
      */
     private addSubNub(after?: FieldSet, clone: boolean = false) {
@@ -75,11 +75,12 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
             const newFs = this._container.addFromTemplate(0, after.indexAsKid());
             if (clone) {
                 // replace in-place to change properties (overkill)
+                // @TODO why only those two ? loop over all properties in a generic way ?
                 newFs.setPropValue("structureType", after.properties.getPropValue("structureType"));
                 newFs.setPropValue("loiDebit", after.properties.getPropValue("loiDebit"));
-                // after.nub.properties
+                // copy param values
                 for (const p of after.nub.prms) {
-                    newFs.nub.getParameter(p.symbol).singleValue = p.singleValue;
+                    newFs.nub.getParameter(p.symbol).loadObjectRepresentation(p.objectRepresentation());
                 }
             }
         } else {
diff --git a/src/app/components/fixedvar-results/fixed-results.component.ts b/src/app/components/fixedvar-results/fixed-results.component.ts
index 5d676ff58a6c1cd83ee93042e48aca5179695aa9..ab00d23500b5ffc722e01a3bcae00b68a843ccd4 100644
--- a/src/app/components/fixedvar-results/fixed-results.component.ts
+++ b/src/app/components/fixedvar-results/fixed-results.component.ts
@@ -8,7 +8,7 @@ import { FormulaireService } from "../../services/formulaire.service";
 import { ResultsComponent } from "./results.component";
 import { AppComponent } from "../../app.component";
 
-import { Structure, capitalize } from "jalhyd";
+import { capitalize } from "jalhyd";
 
 import { sprintf } from "sprintf-js";
 
@@ -95,10 +95,14 @@ export class FixedResultsComponent extends ResultsComponent {
             for (const fp of this.fixedParams) {
                 if (fp.symbol === symbol) {
                     let label = this.formattedLabel(fp);
-                    // add structure position before label
-                    if (fp.paramDefinition.parentNub instanceof Structure) {
-                        const pos = fp.paramDefinition.parentNub.findPositionInParent();
-                        label = this.intlService.localizeText("INFO_OUVRAGE") + " n°" + (pos + 1) + ": " + label;
+                    const nub = fp.paramDefinition.parentNub;
+                    // add child type and position before label
+                    if (nub && nub.parent && nub.parent.childrenType) {
+                        const pos = nub.findPositionInParent();
+                        // label = this.intlService.localizeText("INFO_OUVRAGE") + " n°" + (pos + 1) + ": " + label;
+                        const cn = capitalize(this.intlService.childName(nub.parent));
+                        label = sprintf(this.intlService.localizeText("INFO_STUFF_N"), cn)
+                            + (pos + 1) + ": " + label;
                     }
                     label += this._fixedResults.getHelpLink(symbol);
                     data.push({
@@ -189,10 +193,14 @@ export class FixedResultsComponent extends ResultsComponent {
         // 1. fixed parameters
         for (const fp of this.fixedParams) {
             let label = this.formattedLabel(fp);
-            // add structure position before label
-            if (fp.paramDefinition.parentNub instanceof Structure) {
-                const pos = fp.paramDefinition.parentNub.findPositionInParent();
-                label = this.intlService.localizeText("INFO_OUVRAGE") + " n°" + (pos + 1) + ": " + label;
+            const nub = fp.paramDefinition.parentNub;
+            // add child type and position before label
+            if (nub && nub.parent && nub.parent.childrenType) {
+                const pos = nub.findPositionInParent();
+                // label = this.intlService.localizeText("INFO_OUVRAGE") + " n°" + (pos + 1) + ": " + label;
+                const cn = capitalize(this.intlService.childName(nub.parent));
+                label = sprintf(this.intlService.localizeText("INFO_STUFF_N"), cn)
+                    + (pos + 1) + ": " + label;
             }
             label += this._fixedResults.getHelpLink(fp.symbol);
             data.push({
diff --git a/src/app/components/fixedvar-results/fixedvar-results.component.html b/src/app/components/fixedvar-results/fixedvar-results.component.html
index 0b7fc09fc5ae489d93a4520fb4c4083a83c2cae9..7676b026859cc72cbef157cf5b94edaa9a57d1d4 100644
--- a/src/app/components/fixedvar-results/fixedvar-results.component.html
+++ b/src/app/components/fixedvar-results/fixedvar-results.component.html
@@ -2,7 +2,7 @@
     <!-- journal -->
     <log></log>
 
-    <results-chart *ngIf="showVarResults"></results-chart>
+    <results-chart [hidden]="! showVarResultsChart"></results-chart>
 
     <div>
         <!-- table des résultats fixés -->
diff --git a/src/app/components/fixedvar-results/fixedvar-results.component.ts b/src/app/components/fixedvar-results/fixedvar-results.component.ts
index 813d377388bd7df11008b45f2c7e470ebfb53494..58bbbf90ce9027236b72fed848e2bbc46a772dbf 100644
--- a/src/app/components/fixedvar-results/fixedvar-results.component.ts
+++ b/src/app/components/fixedvar-results/fixedvar-results.component.ts
@@ -167,6 +167,13 @@ export class FixedVarResultsComponent extends ResultsComponent implements DoChec
         return this._varResults && this._varResults.hasResults;
     }
 
+    /**
+     * affichage du graphique des résultats variés
+     */
+    public get showVarResultsChart(): boolean {
+        return this._varResults && this._varResults.hasPlottableResults;
+    }
+
     public getFixedResultClass(i: number) {
         // tslint:disable-next-line:no-bitwise
         return "result_id_" + String(i & 1);
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index ddc8c982456a20fcbb59cfd187fe2c4c48adfab7..55795bf09cad69d49e958d137669b3e6c3110f03 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -329,8 +329,9 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
     public get calculatorsUsingThisOne(): any {
         const sources = Session.getInstance().getDependingNubs(this._formulaire.currentNub.uid, undefined, true, true);
         return sources.map((s) => {
+            const form = this.formulaireService.getFormulaireFromNubId(s.uid);
             return {
-                label: this.formulaireService.getFormulaireFromNubId(s.uid).calculatorName,
+                label: (form !== undefined) ? form.calculatorName : "unknown Nub",
                 uid: s.uid
             };
         });
diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts
index f49b62c0e4ebdfc7d6f8815b6266a017f9db1ae6..0e946cfed7e85da247d361a39223b2c21dee94e4 100644
--- a/src/app/components/generic-input/generic-input.component.ts
+++ b/src/app/components/generic-input/generic-input.component.ts
@@ -1,6 +1,6 @@
 import { Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, ViewChild } from "@angular/core";
 import { NgModel } from "@angular/forms";
-import { isNumeric, Structure, Pab, MacrorugoCompound } from "jalhyd";
+import { isNumeric, Structure, Pab, MacrorugoCompound, SPP } from "jalhyd";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
 import { NgParameter } from "../../formulaire/ngparam";
 import { I18nService } from "../../services/internationalisation.service";
@@ -42,10 +42,9 @@ export abstract class GenericInputComponent implements OnChanges {
             if (this._model instanceof NgParameter) {
                 const param = this._model as NgParameter;
                 id = param.symbol;
-                // if inside a nested Structure, prefix with Structure position
-                // to disambiguate
+                // if inside a child Nub, prefix with child position to disambiguate
                 const nub = param.paramDefinition.parentNub;
-                if (nub && (nub instanceof Structure || nub.parent instanceof Pab || nub.parent instanceof MacrorugoCompound)) {
+                if (nub && nub.parent && nub.parent.childrenType) {
                     id = nub.findPositionInParent() + "_" + id;
                 }
             }
diff --git a/src/app/components/param-computed/param-computed.component.ts b/src/app/components/param-computed/param-computed.component.ts
index b6779c083d1b94e387982d52039ecf76caa40851..54bbe75fb9a94dcd6d9c9e3b8046aae442fbc1ae 100644
--- a/src/app/components/param-computed/param-computed.component.ts
+++ b/src/app/components/param-computed/param-computed.component.ts
@@ -25,10 +25,9 @@ export class ParamComputedComponent {
      */
     public get inputId() {
         let id = "calc_" + this.param.symbol;
-        // if inside a nested Structure, prefix with Structure position
-        // to disambiguate
+        // if inside a child Nub, prefix with child position to disambiguate
         const nub = this.param.paramDefinition.parentNub;
-        if (nub && nub instanceof Structure) {
+        if (nub && nub.parent && nub.parent.childrenType) {
             id = nub.findPositionInParent() + "_" + id;
         }
         return id;
diff --git a/src/app/components/param-link/param-link.component.ts b/src/app/components/param-link/param-link.component.ts
index ab7b72c470193eb6142cba906fac41e843b88170..0db7fb0376ed46f7d5573de1a52aaa2e2892eb73 100644
--- a/src/app/components/param-link/param-link.component.ts
+++ b/src/app/components/param-link/param-link.component.ts
@@ -1,7 +1,7 @@
 import { Component, Input, Output, EventEmitter, OnChanges, OnDestroy } from "@angular/core";
 
 import { NgParameter } from "../../formulaire/ngparam";
-import { LinkedValue, ParamValueMode, Observer, Structure, acSection, ParamDefinition } from "jalhyd";
+import { LinkedValue, ParamValueMode, Observer, Structure, acSection, ParamDefinition, ChildNub } from "jalhyd";
 import { FormulaireService } from "../../services/formulaire.service";
 import { I18nService } from "../../services/internationalisation.service";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
@@ -41,10 +41,9 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
 
     public get selectId() {
         let id = "linked_" + this.param.symbol;
-        // if inside a nested Structure, prefix with Structure position
-        // to disambiguate
+        // if inside a child Nub, prefix with child position to disambiguate
         const nub = this.param.paramDefinition.parentNub;
-        if (nub && nub instanceof Structure) {
+        if (nub && nub.parent && nub.parent.childrenType) {
             id = nub.findPositionInParent() + "_" + id;
         }
         return id;
@@ -169,23 +168,16 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
             preview = NgParameter.preview(i.element as ParamDefinition, true);
         }
 
-        // 1. Paramètre / résultat d'un ouvrage dans un Nub de type ParallelStructure
-        if (i.nub instanceof Structure) {
+        // 1. Paramètre / résultat d'un Nub enfant au sein d'un Nub parent
+        if (i.nub instanceof ChildNub) {
             let pos: number;
             pos = i.nub.findPositionInParent();
-            if (i.isResult()) {
-                // résultat d'ouvrage
-                return `${preview} - ` + sprintf(
-                    this.intlService.localizeText("INFO_LINKED_VALUE_DEVICE_RESULT"),
-                    s, c, (pos + 1)
-                );
-            } else {
-                // paramètre d'ouvrage
-                return `${preview} - ` + sprintf(
-                    this.intlService.localizeText("INFO_LINKED_VALUE_DEVICE"),
-                    s, c, (pos + 1)
-                );
-            }
+            return `${preview} - ` + sprintf(
+                this.intlService.localizeText("INFO_LINKED_VALUE_CHILD"),
+                s, c,
+                this.intlService.childName(i.nub.parent).toLowerCase()
+                , (pos + 1)
+            );
         } else
         // 2. Paramètre / résultat d'une section dans un Nub de type SectionNub
         if (i.nub instanceof acSection) {
diff --git a/src/app/components/param-values/param-values.component.ts b/src/app/components/param-values/param-values.component.ts
index a22cc098c9f4c24ac26af7f548a5473baf3e2c58..6246b682532bdbb0ae75c66e2926be2b2934aec3 100644
--- a/src/app/components/param-values/param-values.component.ts
+++ b/src/app/components/param-values/param-values.component.ts
@@ -45,10 +45,9 @@ export class ParamValuesComponent implements AfterViewInit, Observer {
 
     public get inputId() {
         let id = "var_" + this.param.symbol;
-        // if inside a nested Structure, prefix with Structure position
-        // to disambiguate
+        // if inside a child Nub, prefix with child position to disambiguate
         const nub = this.param.paramDefinition.parentNub;
-        if (nub && nub instanceof Structure) {
+        if (nub && nub.parent && nub.parent.childrenType) {
             id = nub.findPositionInParent() + "_" + id;
         }
         return id;
diff --git a/src/app/config.json b/src/app/config.json
index 1b2da220febf7661f5a2d1fed5cf860609dbca72..55c2352e46bbccece0549f96f672334d6f2aac03 100644
--- a/src/app/config.json
+++ b/src/app/config.json
@@ -56,6 +56,14 @@
             },
             "calculators": [ 8, 9, 10 ]
         },
+        {
+            "name": "OUTILS_MATHEMATIQUES",
+            "image": {
+                "path": "maths.jpg",
+                "credits": "Toms Baugis, \"Lineāli\" / CC BY 2.0"
+            },
+            "calculators": [ 22, 23, 24, 25 ]
+        },
         {
             "_comment": "card for calculators not used in any theme",
             "image": {
diff --git a/src/app/formulaire/definition/concrete/form-spp.ts b/src/app/formulaire/definition/concrete/form-spp.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a12a196b676fa84a28b143a0a57e209c051eb016
--- /dev/null
+++ b/src/app/formulaire/definition/concrete/form-spp.ts
@@ -0,0 +1,152 @@
+import { FormulaireBase } from "./form-base";
+import { FieldsetTemplate } from "../../fieldset-template";
+import { FieldSet } from "../../fieldset";
+import { FormulaireNode } from "../../formulaire-node";
+import { FieldsetContainer } from "../../fieldset-container";
+
+import { SPP, Nub, Props, Session, YAXN, IObservable } from "jalhyd";
+
+/**
+ * Formulaire pour "somme / produit de puissances"
+ */
+export class FormulaireSPP extends FormulaireBase {
+
+    /** id of select configuring operation */
+    private _operationSelectId: string;
+
+    public get sppNub(): SPP {
+        return this.currentNub as SPP;
+    }
+
+    protected parseOptions(json: {}) {
+        super.parseOptions(json);
+        this._operationSelectId = this.getOption(json, "operationSelectId");
+    }
+
+    public afterParseFieldset(fs: FieldSet) {
+        if (this._operationSelectId) {
+            const sel = fs.getFormulaireNodeById(this._operationSelectId);
+            if (sel) {
+                fs.properties.addObserver(this);
+            }
+        }
+    }
+
+    private createYaxnNub(templ: FieldsetTemplate): Nub {
+        const params = {};
+        params["calcType"] = templ.calcTypeFromConfig;
+        return this.createYaxn(new Props(params));
+    }
+
+    /**
+     * ajoute un nub YAXN
+     * @param mr nub à ajouter
+     * @param after position après laquelle insérer le nub, à la fin sinon
+     */
+    private addYaxnNub(mr: YAXN, after?: number) {
+        this.sppNub.addChild(mr, after);
+    }
+
+    /**
+     * Asks JaLHyd to create a YAXN nub as a child of the current Calculator Module
+     * and return it; does not store it in the Session (for YAXN, not for Calculator Modules)
+     * @param p properties for the new Nub
+     */
+    protected createYaxn(p: Props): YAXN {
+        return Session.getInstance().createNub(p, this.sppNub) as YAXN;
+    }
+
+    /**
+     * Replaces the given YAXN sn in the current calculator module,
+     * with a new one built with properties "params"
+     * @param mr YAXN to replace
+     * @param params properties to build the new Nub (calcType)
+     */
+    protected replaceNub(mr: YAXN, params: Props): Nub {
+        const parent = this.sppNub;
+        const newStructure = this.createYaxn(params);
+        parent.replaceChildInplace(mr, newStructure);
+        return newStructure;
+    }
+
+    public createFieldset(parent: FormulaireNode, json: {}, data?: {}, nub?: Nub): FieldSet {
+        if (json["calcType"] === "YAXN") {
+            // indice après lequel insérer le nouveau FieldSet
+            const after = data["after"];
+
+            const res: FieldSet = new FieldSet(parent);
+            let mrn: Nub;
+            if (nub) { // use existing Nub (build interface based on model)
+                mrn = nub;
+            } else {
+                mrn = this.createYaxnNub(data["template"]);
+                this.addYaxnNub(mrn as YAXN, after);
+            }
+            res.setNub(mrn, false);
+
+            if (after !== undefined) {
+                parent.kids.splice(after + 1, 0, res);
+            } else {
+                parent.kids.push(res);
+            }
+
+            this.resetResults();
+
+            return res;
+        } else {
+            return super.createFieldset(parent, json, data);
+        }
+    }
+
+    public moveFieldsetUp(fs: FieldSet) {
+        if (fs.nub instanceof YAXN) {
+            // déplacement du nub
+            fs.nub.parent.moveChildUp(fs.nub);
+            // déplacement du fieldset
+            this.fieldsetContainer.moveFieldsetUp(fs);
+
+            this.resetResults();
+        } else {
+            super.moveFieldsetUp(fs);
+        }
+    }
+
+    public moveFieldsetDown(fs: FieldSet) {
+        if (fs.nub instanceof YAXN) {
+            // déplacement du nub
+            fs.nub.parent.moveChildDown(fs.nub);
+            // déplacement du fieldset
+            this.fieldsetContainer.moveFieldsetDown(fs);
+
+            this.resetResults();
+        } else { super.moveFieldsetDown(fs); }
+    }
+
+    public removeFieldset(fs: FieldSet) {
+        if (fs.nub instanceof YAXN) {
+            // suppression du sous-nub dans le Nub parent
+            this.sppNub.deleteChild(fs.nub.findPositionInParent());
+            // suppression du fieldset
+            this.fieldsetContainer.removeFieldset(fs);
+
+            this.resetResults();
+        } else { super.removeFieldset(fs); }
+    }
+
+    // interface Observer
+
+    public update(sender: IObservable, data: any) {
+        super.update(sender, data);
+        if (data.action === "propertyChange") {
+            this.reset();
+        }
+    }
+
+    private get fieldsetContainer(): FieldsetContainer {
+        const n = this.getFormulaireNodeById("yaxn_container");
+        if (n === undefined || !(n instanceof FieldsetContainer)) {
+            throw new Error("l'élément 'yaxn_container' n'est pas du type FieldsetContainer");
+        }
+        return n as FieldsetContainer;
+    }
+}
diff --git a/src/app/formulaire/definition/concrete/form-trigo.ts b/src/app/formulaire/definition/concrete/form-trigo.ts
new file mode 100644
index 0000000000000000000000000000000000000000..499d6e03a4365f25b74545581edd6c882f84a97f
--- /dev/null
+++ b/src/app/formulaire/definition/concrete/form-trigo.ts
@@ -0,0 +1,46 @@
+import { IObservable } from "jalhyd";
+
+import { FormulaireBase } from "./form-base";
+import { FieldSet } from "../../fieldset";
+
+/**
+ * Formulaire pour les fonctions trigonométriques
+ */
+export class FormulaireTrigo extends FormulaireBase {
+
+    /** id of select configuring operation */
+    private _operationSelectId: string;
+
+    /** id of select configuring unit */
+    private _unitSelectId: string;
+
+    protected parseOptions(json: {}) {
+        super.parseOptions(json);
+        this._operationSelectId = this.getOption(json, "operationSelectId");
+        this._unitSelectId = this.getOption(json, "unitSelectId");
+    }
+
+    public afterParseFieldset(fs: FieldSet) {
+        if (this._operationSelectId) {
+            const sel = fs.getFormulaireNodeById(this._operationSelectId);
+            if (sel) {
+                fs.properties.addObserver(this);
+            }
+        }
+        if (this._unitSelectId) {
+            const sel = fs.getFormulaireNodeById(this._unitSelectId);
+            if (sel) {
+                fs.properties.addObserver(this);
+            }
+        }
+    }
+
+    // interface Observer
+
+    public update(sender: IObservable, data: any) {
+        super.update(sender, data);
+        if (data.action === "propertyChange") {
+            this.reset();
+        }
+    }
+}
diff --git a/src/app/formulaire/fieldset.ts b/src/app/formulaire/fieldset.ts
index 0681164586f2f3c9ac4b8e798ea6dd2963ee1772..03c9fb022532530ec2e75160563e2bab7cd98712 100644
--- a/src/app/formulaire/fieldset.ts
+++ b/src/app/formulaire/fieldset.ts
@@ -11,6 +11,9 @@ import {
     GrilleProfile,
     BiefRegime,
     Solveur,
+    TrigoOperation,
+    TrigoUnit,
+    SPPOperation,
 } from "jalhyd";
 
 import { FormulaireElement } from "./formulaire-element";
@@ -288,6 +291,15 @@ export class FieldSet extends FormulaireElement implements Observer {
             case "fs_water_line": // Bief
                 this.setSelectValueFromProperty("select_regime", "regime");
                 break;
+
+            case "fs_trigo": // Trigo
+                this.setSelectValueFromProperty("select_operation", "trigoOperation");
+                this.setSelectValueFromProperty("select_unit", "trigoUnit");
+                break;
+
+            case "fs_spp": // SPP
+                this.setSelectValueFromProperty("select_spp_operation", "sppOperation");
+                break;
         }
     }
 
@@ -351,6 +363,9 @@ export class FieldSet extends FormulaireElement implements Observer {
         this.setPropertyValueFromConfig(json, "defaultGridType", "gridType", GrilleType);
         this.setPropertyValueFromConfig(json, "defaultRegime", "regime", BiefRegime);
         this.setPropertyValueFromConfig(json, "varCalc", "varCalc");
+        this.setPropertyValueFromConfig(json, "defaultOperation", "trigoOperation", TrigoOperation);
+        this.setPropertyValueFromConfig(json, "defaultUnit", "trigoUnit", TrigoUnit);
+        this.setPropertyValueFromConfig(json, "defaultOperation", "sppOperation", SPPOperation);
 
         this.updateFields();
     }
@@ -442,6 +457,15 @@ export class FieldSet extends FormulaireElement implements Observer {
                         case "select_regime": // Bief
                             this.setPropValue("regime", data.value.value);
                             break;
+                        case "select_operation": // Trigo
+                            this.setPropValue("trigoOperation", data.value.value);
+                            break;
+                        case "select_unit": // Trigo
+                            this.setPropValue("trigoUnit", data.value.value);
+                            break;
+                        case "select_spp_operation": // SPP
+                            this.setPropValue("sppOperation", data.value.value);
+                            break;
                     }
                     break;
             }
diff --git a/src/app/formulaire/select-field.ts b/src/app/formulaire/select-field.ts
index b2ea54a8c44b80617e149fd9e6f05dc907a57239..eeed82a32dfcc790056c811a20d0f53398c264e5 100644
--- a/src/app/formulaire/select-field.ts
+++ b/src/app/formulaire/select-field.ts
@@ -9,9 +9,9 @@ import {
     LoiDebit,
     GrilleType,
     GrilleProfile,
-    Solveur,
-    ParamValueMode,
-    Session
+    TrigoUnit,
+    TrigoOperation,
+    SPPOperation
  } from "jalhyd";
 
 import { Field } from "./field";
@@ -198,6 +198,22 @@ export class SelectField extends Field {
                 this.addEntry(new SelectEntry(this._entriesBaseId + BiefRegime.Fluvial, BiefRegime.Fluvial));
                 this.addEntry(new SelectEntry(this._entriesBaseId + BiefRegime.Torrentiel, BiefRegime.Torrentiel));
                 break;
+
+            case "trigo_operation": // Trigo: opération (cos, sin…)
+                for (let j = 0; j < Object.keys(TrigoOperation).length / 2; j++) {
+                    this.addEntry(new SelectEntry(this._entriesBaseId + j, j));
+                }
+                break;
+
+            case "trigo_unit": // Trigo: unité (degrés, radians)
+                this.addEntry(new SelectEntry(this._entriesBaseId + TrigoUnit.DEG, TrigoUnit.DEG));
+                this.addEntry(new SelectEntry(this._entriesBaseId + TrigoUnit.RAD, TrigoUnit.RAD));
+                break;
+
+            case "spp_operation": // SPP: opération (somme, produit)
+                this.addEntry(new SelectEntry(this._entriesBaseId + SPPOperation.SUM, SPPOperation.SUM));
+                this.addEntry(new SelectEntry(this._entriesBaseId + SPPOperation.PRODUCT, SPPOperation.PRODUCT));
+                break;
         }
 
         this.afterParseConfig();
diff --git a/src/app/results/calculator-results.ts b/src/app/results/calculator-results.ts
index e0bb77520499adbce851ad772bfccc2e5c37fe49..27c0b89b34d23d1e3ec05549ff1fbe174db7e2bb 100644
--- a/src/app/results/calculator-results.ts
+++ b/src/app/results/calculator-results.ts
@@ -23,11 +23,11 @@ export abstract class CalculatorResults {
         if (referenceNub) {
             const children = referenceNub.getChildren();
             const parameterNub = p.paramDefinition.parentNub;
-            const cn = capitalize(ServiceFactory.instance.i18nService.childName(parameterNub));
             if (children.includes(parameterNub)) {
+                const cn = capitalize(ServiceFactory.instance.i18nService.childName(parameterNub.parent));
                 isChildParam = true;
                 const pos = parameterNub.findPositionInParent() + 1;
-                res = sprintf(ServiceFactory.instance.i18nService.localizeText("INFO_OUVRAGE_N"), cn) + pos + " : ";
+                res = sprintf(ServiceFactory.instance.i18nService.localizeText("INFO_STUFF_N"), cn) + pos + " : ";
             }
         }
         if (displaySymbol && ! isChildParam) {
diff --git a/src/app/results/param-calc-results.ts b/src/app/results/param-calc-results.ts
index 60c0e257d0b40dd636f2d1ab62cb6e9964382674..3f5c8c4b0e8364c7e2486e6300f8103a4304dff4 100644
--- a/src/app/results/param-calc-results.ts
+++ b/src/app/results/param-calc-results.ts
@@ -36,6 +36,15 @@ export abstract class CalculatedParamResults extends CalculatorResults {
     }
 
     public get hasResults(): boolean {
+        if (this.result === undefined) {
+            return false;
+        }
+        return true;
+        // return ! this.result.hasOnlyErrors;
+    }
+
+    /** return true if there is something to display on the variable results chart */
+    public get hasPlottableResults(): boolean {
         if (this.result === undefined) {
             return false;
         }
diff --git a/src/app/results/var-results.ts b/src/app/results/var-results.ts
index 3594635b688b37996e7779b5684bc136d97a847c..48038a9c60c9610dc522f095f600d22990449666 100644
--- a/src/app/results/var-results.ts
+++ b/src/app/results/var-results.ts
@@ -1,7 +1,7 @@
 import { CalculatorResults } from "./calculator-results";
 import { CalculatedParamResults } from "./param-calc-results";
 import { NgParameter } from "../formulaire/ngparam";
-import { ResultElement, ParamFamily, capitalize } from "jalhyd";
+import { ResultElement, ParamFamily, capitalize, Nub } from "jalhyd";
 import { ServiceFactory } from "../services/service-factory";
 import { PlottableData } from "./plottable-data";
 import { ChartType } from "./chart-type";
@@ -132,7 +132,7 @@ export class VarResults extends CalculatedParamResults implements PlottableData
         const match = /^([0-9]+)_(.+)$/.exec(symbol);
         if (match !== null) {
             const pos = +match[1];
-            ct = sn.getChildren()[pos].calcType;
+            // only parent translation file is loaded; look for children translations in it // ct = sn.getChildren()[pos].calcType;
             symbol = match[2];
             const cn = capitalize(ServiceFactory.instance.i18nService.childName(sn));
             ret += sprintf(ServiceFactory.instance.i18nService.localizeText("INFO_STUFF_N"), cn) + (pos + 1) + " : ";
@@ -214,13 +214,15 @@ export class VarResults extends CalculatedParamResults implements PlottableData
             }
         }
         // children results
-        const sn = this.result.sourceNub;
-        for (const c of sn.getChildren()) {
-            if (c.result) {
-                // using latest ResultElement; results count / types are supposed to be the same on every iteration
-                for (const k of c.result.resultElement.keys) {
-                    if (k.indexOf("ENUM_") === -1) { // ENUM variables are not plottable
-                        res.push(c.findPositionInParent() + "_" + k);
+        if (this.result) {
+            const sn = this.result.sourceNub;
+            for (const c of sn.getChildren()) {
+                if (c.result) {
+                    // using latest ResultElement; results count / types are supposed to be the same on every iteration
+                    for (const k of c.result.resultElement.keys) {
+                        if (k.indexOf("ENUM_") === -1) { // ENUM variables are not plottable
+                            res.push(c.findPositionInParent() + "_" + k);
+                        }
                     }
                 }
             }
@@ -254,8 +256,10 @@ export class VarResults extends CalculatedParamResults implements PlottableData
         const families: { [key: string]: string[] } = {};
         // variating parameters
         for (const v of this._variatedParams) {
-            const f = ParamFamily[v.paramDefinition.family];
-            if (f !== undefined) {
+            // exclude pseudo-family "ANY"
+            const fam = v.paramDefinition.family;
+            if (fam !== undefined && fam !== ParamFamily.ANY) {
+                const f = ParamFamily[fam];
                 if (! (f in families)) {
                     families[f] = [];
                 }
@@ -264,8 +268,10 @@ export class VarResults extends CalculatedParamResults implements PlottableData
         }
         // results
         for (const erk of this.resultKeys) {
-            const f = ParamFamily[this.result.sourceNub.getFamily(erk)];
-            if (f !== undefined) {
+            const fam = this.result.sourceNub.getFamily(erk);
+            // exclude pseudo-family "ANY"
+            if (fam !== undefined && fam !== ParamFamily.ANY) {
+                const f = ParamFamily[fam];
                 if (! (f in families)) {
                     families[f] = [];
                 }
@@ -273,17 +279,21 @@ export class VarResults extends CalculatedParamResults implements PlottableData
             }
         }
         // children results
-        const sn = this.result.sourceNub;
-        for (const c of sn.getChildren()) {
-            if (c.result) {
-                for (const k of c.result.resultElement.keys) {
-                    const f = ParamFamily[this.result.sourceNub.getFamily(k)];
-                    if (f !== undefined) {
-                        if (! (f in families)) {
-                            families[f] = [];
+        if (this.result) {
+            const sn = this.result.sourceNub;
+            for (const c of sn.getChildren()) {
+                if (c.result) {
+                    for (const k of c.result.resultElement.keys) {
+                        const fam = this.result.sourceNub.getFamily(k);
+                        // exclude pseudo-family "ANY"
+                        if (fam !== undefined && fam !== ParamFamily.ANY) {
+                            const f = ParamFamily[fam];
+                            if (! (f in families)) {
+                                families[f] = [];
+                            }
+                            const pos = c.findPositionInParent();
+                            families[f].push(pos + "_" + k);
                         }
-                        const pos = c.findPositionInParent();
-                        families[f].push(pos + "_" + k);
                     }
                 }
             }
@@ -296,17 +306,22 @@ export class VarResults extends CalculatedParamResults implements PlottableData
      * (used by tooltip functions)
      */
     public getVariatingParametersSymbols(): string[] {
-        const sn = this.result.sourceNub;
-        return this._variatedParams.map((vp) => {
-            // detect if variated param is a children param
-            const parameterNub = vp.paramDefinition.parentNub;
-            const children = sn.getChildren();
-            let symb = vp.symbol;
-            if (children.includes(parameterNub)) {
-                symb = parameterNub.findPositionInParent() + "_" + symb;
-            }
-            return symb;
-        });
+        if (this.result && this.result.sourceNub) {
+            return this._variatedParams.map(vp => this.getVariatingParameterSymbol(vp, this.result.sourceNub));
+        } else {
+            return [];
+        }
+    }
+
+    public getVariatingParameterSymbol(vp: NgParameter, sourceNub: Nub): string {
+        // detect if variated param is a children param
+        const parameterNub = vp.paramDefinition.parentNub;
+        const children = sourceNub.getChildren();
+        let symb = vp.symbol;
+        if (children.includes(parameterNub)) {
+            symb = parameterNub.findPositionInParent() + "_" + symb;
+        }
+        return symb;
     }
 
     public update() {
@@ -346,8 +361,10 @@ export class VarResults extends CalculatedParamResults implements PlottableData
         if (this.resultKeys.length > 0) {
             defaultY = this.resultKeys[0];
         }
-        this.chartX = this.chartX || this.variatedParameters[this.longest].symbol;
         this.chartY = defaultY;
+        if (this.chartX === undefined || ! this.getAvailableXAxis().includes(this.chartX)) {
+            this.chartX = this.getVariatingParameterSymbol(this.variatedParameters[this.longest], this.result.sourceNub);
+        }
 
         // calculator type for translation
         const sn = this.result.sourceNub;
diff --git a/src/app/services/formulaire.service.ts b/src/app/services/formulaire.service.ts
index 536af784a86eea59425290587c39de106a2af409..e0224f8d6a24e30dbfa8b3293c5e990acfccaef4 100644
--- a/src/app/services/formulaire.service.ts
+++ b/src/app/services/formulaire.service.ts
@@ -13,7 +13,8 @@ import {
     Pab,
     Props,
     Cloisons,
-    CloisonAval
+    CloisonAval,
+    SPP
 } from "jalhyd";
 
 import { ApplicationSetupService } from "./app-setup.service";
@@ -40,6 +41,8 @@ import { FormulaireGrille } from "../formulaire/definition/concrete/form-grille"
 import { FormulaireBief } from "../formulaire/definition/concrete/form-bief";
 import { FormulaireSolveur } from "../formulaire/definition/concrete/form-solveur";
 import { AppComponent } from "../app.component";
+import { FormulaireSPP } from "../formulaire/definition/concrete/form-spp";
+import { FormulaireTrigo } from "../formulaire/definition/concrete/form-trigo";
 
 @Injectable()
 export class FormulaireService extends Observable {
@@ -86,6 +89,9 @@ export class FormulaireService extends Observable {
         this.calculatorPaths[CalculatorType.Pente] = "pente";
         this.calculatorPaths[CalculatorType.Bief] = "bief";
         this.calculatorPaths[CalculatorType.Solveur] = "solveur";
+        this.calculatorPaths[CalculatorType.YAXB] = "yaxb";
+        this.calculatorPaths[CalculatorType.Trigo] = "trigo";
+        this.calculatorPaths[CalculatorType.SPP] = "spp";
     }
 
     private get _intlService(): I18nService {
@@ -335,6 +341,14 @@ export class FormulaireService extends Observable {
                 f = new FormulaireSolveur();
                 break;
 
+            case CalculatorType.SPP:
+                f = new FormulaireSPP();
+                break;
+
+            case CalculatorType.Trigo:
+                f = new FormulaireTrigo();
+                break;
+
             default:
                 f = new FormulaireBase();
         }
@@ -390,6 +404,18 @@ export class FormulaireService extends Observable {
                 }
             }
 
+            // add fieldsets for existing YAXN if needed
+            // (when loading session only)
+            if (f.currentNub instanceof SPP) {
+                for (const c of f.currentNub.getChildren()) {
+                    for (const e of f.allFormElements) {
+                        if (e instanceof FieldsetContainer) { // @TODO manage many containers one day ?
+                            e.addFromTemplate(0, undefined, c);
+                        }
+                    }
+                }
+            }
+
             // when creating a new Pab, add one wall with one device, plus the downwall
             // (when loading session, those items are already present)
             if (
diff --git a/src/assets/images/themes/maths.jpg b/src/assets/images/themes/maths.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..10f0b30eaf229a4a57e704b3ce5a797b27075d5b
Binary files /dev/null and b/src/assets/images/themes/maths.jpg differ
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index b12fe6900f404f69dad1c63a622f23a2756d659d..cff254e1e0e15cad3b7d547bb8deeeaa27798fd9 100644
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -16,6 +16,7 @@
     "ERROR_DICHO_NULL_STEP": "Dichotomy (initial interval search): invalid null step",
     "ERROR_DICHO_TARGET_TOO_HIGH": "Dichotomy: the solution %targetSymbol%=%targetValue% is greater than the maximum computable value %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
     "ERROR_DICHO_TARGET_TOO_LOW": "Dichotomy: the solution %targetSymbol%=%targetValue%  is lower than the minimum computable value %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
+    "ERROR_DIVISION_BY_ZERO": "Division by zero",
     "ERROR_ELEVATION_ZI_LOWER_THAN_Z2": "Upstream elevation is lower than downstream elevation",
     "ERROR_IN_CALC_CHAIN": "An error occurred in calculation chain",
     "WARNING_ERROR_IN_CALC_CHAIN_STEPS": "Errors occurred during chain calculation",
@@ -27,6 +28,7 @@
     "ERROR_MINMAXSTEP_MIN": "Value is not in [%s,%s[",
     "ERROR_MINMAXSTEP_STEP": "Value is not in %s",
     "ERROR_NEWTON_DERIVEE_NULLE": "Null function derivative in Newton computation",
+    "ERROR_NON_INTEGER_POWER_ON_NEGATIVE_NUMBER": "Exponent of a negative number must be an integer value",
     "ERROR_PAB_CALC_Z1_CLOISON": "Error in calculating the upstream water elevation of a wall",
     "ERROR_PAB_Z1_LOWER_THAN_Z2": "Upstream water elevation should be higher than downstream water elevation",
     "ERROR_PAB_Z1_LOWER_THAN_UPSTREAM_WALL": "Upstream water elevation is too low for water to flow through the first wall",
@@ -56,8 +58,9 @@
     "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCOR": "Non convergence of the calculation of the corresponding elevation (Newton's method)",
     "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HNORMALE": "Non convergence of the calculation of the normal depth (Newton's method)",
     "ERROR_SECTION_PENTE_NEG_NULLE_HNORMALE_INF": "The slope is negative or zero, the normal depth is infinite",
+    "ERROR_SECTION_SURFACE_NULLE": "Section: calculation is impossible when surface is null",
+    "ERROR_SOMETHING_FAILED_IN_CHILD": "Calculation of child module #%number% failed",
     "ERROR_STRUCTURE_Q_TROP_ELEVE": "The flow passing through the other devices is too high: the requested parameter is not calculable.",
-    "ERROR_SECTION_SURFACE_NULLE": "Section : calcul impossible à cause d'une surface nulle",
     "INFO_CALCULATOR_CALC_NAME": "Calculator name",
     "INFO_CALCULATOR_CALCULER": "Compute",
     "INFO_CALCULATOR_CLONE": "Duplicate",
@@ -152,10 +155,12 @@
     "INFO_STUFF_MOVED": "%s #%s moved",
     "INFO_STUFF_REMOVED": "%s #%s removed",
     "INFO_STUFF_N": "%s #",
-    "INFO_CHILD_TYPE_STRUCTURE": "device",
-    "INFO_CHILD_TYPE_STRUCTURE_PLUR": "devices",
+    "INFO_CHILD_TYPE_OUVRAGE": "device",
+    "INFO_CHILD_TYPE_OUVRAGE_PLUR": "devices",
     "INFO_CHILD_TYPE_MACRORUGO": "apron",
     "INFO_CHILD_TYPE_MACRORUGO_PLUR": "aprons",
+    "INFO_CHILD_TYPE_PUISSANCE": "power",
+    "INFO_CHILD_TYPE_PUISSANCE_PLUR": "powers",
     "INFO_FIELDSET_ADD": "Add",
     "INFO_FIELDSET_COPY": "Copy",
     "INFO_FIELDSET_REMOVE": "Remove",
@@ -282,8 +287,7 @@
     "INFO_LIB_ZRAM": "Upstream apron elevation",
     "INFO_LIB_ZRMB": "Downstream basin bottom elevation",
     "INFO_LIB_ZT": "Triangle top elevation",
-    "INFO_LINKED_VALUE_DEVICE_RESULT": "%s (%s, device %s)",
-    "INFO_LINKED_VALUE_DEVICE": "%s (%s, device %s)",
+    "INFO_LINKED_VALUE_CHILD": "%s (%s, %s %s)",
     "INFO_LINKED_VALUE_EXTRA_RESULT_OF": "%s (%s)",
     "INFO_LINKED_VALUE_EXTRA_RESULT": "%s (%s)",
     "INFO_LINKED_VALUE_RESULT": "%s (%s)",
@@ -443,6 +447,8 @@
     "INFO_SNACKBAR_SETTINGS_SAVED": "Settings saved on this device",
     "INFO_SOLVEUR_TITRE": "Multimodule solver",
     "INFO_SOLVEUR_TITRE_COURT": "Solver",
+    "INFO_SPP_TITRE": "Sum and product of powers",
+    "INFO_SPP_TITRE_COURT": "SPP",
     "INFO_THEME_CREDITS": "Credit",
     "INFO_THEME_DEVALAISON_TITRE": "Downstream migration",
     "INFO_THEME_DEVALAISON_DESCRIPTION": "Tools for dimensioning the structures present on the water intakes of hydroelectric power plants known as \"ichthyocompatible\" and consisting of fine grid planes associated with one or more outlets.",
@@ -454,6 +460,8 @@
     "INFO_THEME_LOIS_D_OUVRAGES_TITRE": "Hydraulic structures",
     "INFO_THEME_MODULES_INUTILISES_DESCRIPTION": "Various calculation modules",
     "INFO_THEME_MODULES_INUTILISES_TITRE": "Other calculation modules",
+    "INFO_THEME_OUTILS_MATHEMATIQUES_TITRE": "Mathematical tools",
+    "INFO_THEME_OUTILS_MATHEMATIQUES_DESCRIPTION": "Miscellaneous generic mathematical tools",
     "INFO_THEME_PASSE_A_BASSIN_DESCRIPTION": "Tools for sizing a fish pass made with pools also called fish steps",
     "INFO_THEME_PASSE_A_BASSIN_TITRE": "Fish ladder",
     "INFO_THEME_PASSE_NATURELLE_DESCRIPTION": "Tools for sizing a natural fish pass also called macroroughness pass or rock-ramp fish pass",
@@ -466,6 +474,10 @@
     "INFO_EXAMPLE_LABEL_PAB_COMPLETE": "Standard fish ladder",
     "INFO_EXAMPLES_TITLE": "Examples",
     "INFO_EXAMPLES_SUBTITLE": "Load standard examples",
+    "INFO_YAXB_TITRE": "Linear function",
+    "INFO_YAXB_TITRE_COURT": "Linear f.",
+    "INFO_TRIGO_TITRE": "Trigonometric function",
+    "INFO_TRIGO_TITRE_COURT": "Trigo. f.",
     "WARNING_WARNINGS_ABSTRACT": "%nb% warnings occurred during calculation",
     "WARNING_REMOUS_ARRET_CRITIQUE": "Calculation stopped: critical elevation reached at abscissa %x%",
     "WARNING_STRUCTUREKIVI_HP_TROP_ELEVE": "h/p must not be greater than 2.5. h/p is forced to 2.5",
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index adf9a72ce481cee7026e216e955fbc6563446343..808e3ae547a622ddd956b324cf43eac9092a9cbb 100644
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -16,6 +16,7 @@
     "ERROR_DICHO_NULL_STEP": "Dichotomie&nbsp;: le pas pour la recherche de l'intervalle de départ ne devrait pas être nul",
     "ERROR_DICHO_TARGET_TOO_HIGH": "Dichotomie&nbsp;: la solution %targetSymbol%=%targetValue% est supérieure à la valeur maximale calculable %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
     "ERROR_DICHO_TARGET_TOO_LOW": "Dichotomie&nbsp;: la solution %targetSymbol%=%targetValue% est inférieure à la valeur minimale calculable %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
+    "ERROR_DIVISION_BY_ZERO": "Division par zéro",
     "ERROR_ELEVATION_ZI_LOWER_THAN_Z2": "La cote amont est plus basse que la cote aval",
     "ERROR_IN_CALC_CHAIN": "Une erreur est survenue dans la chaîne de calcul",
     "WARNING_ERROR_IN_CALC_CHAIN_STEPS": "Des erreurs sont survenues durant le calcul en chaîne",
@@ -27,6 +28,7 @@
     "ERROR_MINMAXSTEP_MIN": "La valeur n'est pas dans [%s,%s[",
     "ERROR_MINMAXSTEP_STEP": "La valeur n'est pas dans %s",
     "ERROR_NEWTON_DERIVEE_NULLE": "Dérivée nulle dans un calcul par la méthode de Newton",
+    "ERROR_NON_INTEGER_POWER_ON_NEGATIVE_NUMBER": "L'exposant d'un nombre négatif doit être une valeur entière",
     "ERROR_PAB_CALC_Z1_CLOISON": "Erreur de calcul de la cote de l'eau amont d'une cloison",
     "ERROR_PAB_Z1_LOWER_THAN_Z2": "La cote de l'eau amont doit être supérieure à la cote de l'eau aval",
     "ERROR_PAB_Z1_LOWER_THAN_UPSTREAM_WALL": "La cote de l'eau amont est trop basse pour que l'eau s'écoule à travers la première cloison",
@@ -56,7 +58,8 @@
     "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HCOR": "Non convergence du calcul de la hauteur correspondante (Méthode de Newton)",
     "ERROR_SECTION_NON_CONVERGENCE_NEWTON_HNORMALE": "Non convergence du calcul de la hauteur normale (Méthode de Newton)",
     "ERROR_SECTION_PENTE_NEG_NULLE_HNORMALE_INF": "La pente est négative ou nulle, la hauteur normale est infinie",
-    "ERROR_SECTION_SURFACE_NULLE": "Section: calculation is impossible when surface is null",
+    "ERROR_SECTION_SURFACE_NULLE": "Section : calcul impossible à cause d'une surface nulle",
+    "ERROR_SOMETHING_FAILED_IN_CHILD": "Le calcul du module enfant n°%number% a échoué",
     "ERROR_STRUCTURE_Q_TROP_ELEVE": "Le débit passant par les autres ouvrages est trop élevé: le paramètre demandé n'est pas calculable.",
     "INFO_CALCULATOR_CALC_NAME": "Nom du module de calcul",
     "INFO_CALCULATOR_CALCULER": "Calculer",
@@ -152,10 +155,12 @@
     "INFO_STUFF_MOVED": "%s n°%s déplacé(e)",
     "INFO_STUFF_REMOVED": "%s n°%s supprimé(e)",
     "INFO_STUFF_N": "%s n°",
-    "INFO_CHILD_TYPE_STRUCTURE": "ouvrage",
-    "INFO_CHILD_TYPE_STRUCTURE_PLUR": "ouvrages",
+    "INFO_CHILD_TYPE_OUVRAGE": "ouvrage",
+    "INFO_CHILD_TYPE_OUVRAGE_PLUR": "ouvrages",
     "INFO_CHILD_TYPE_MACRORUGO": "radier",
     "INFO_CHILD_TYPE_MACRORUGO_PLUR": "radiers",
+    "INFO_CHILD_TYPE_PUISSANCE": "puissance",
+    "INFO_CHILD_TYPE_PUISSANCE_PLUR": "puissances",
     "INFO_FIELDSET_ADD": "Ajouter",
     "INFO_FIELDSET_COPY": "Copier",
     "INFO_FIELDSET_REMOVE": "Supprimer",
@@ -281,8 +286,7 @@
     "INFO_LIB_ZRAM": "Cote du radier amont",
     "INFO_LIB_ZRMB": "Cote de radier mi-bassin",
     "INFO_LIB_ZT": "Cote haute du triangle",
-    "INFO_LINKED_VALUE_DEVICE_RESULT": "%s (%s, ouvrage %s)",
-    "INFO_LINKED_VALUE_DEVICE": "%s (%s, ouvrage %s)",
+    "INFO_LINKED_VALUE_CHILD": "%s (%s, %s %s)",
     "INFO_LINKED_VALUE_EXTRA_RESULT_OF": "%s (%s)",
     "INFO_LINKED_VALUE_EXTRA_RESULT": "%s (%s)",
     "INFO_LINKED_VALUE_RESULT": "%s (%s)",
@@ -442,6 +446,8 @@
     "INFO_SNACKBAR_SETTINGS_SAVED": "Paramètres enregistrés sur cet appareil",
     "INFO_SOLVEUR_TITRE": "Solveur multimodule",
     "INFO_SOLVEUR_TITRE_COURT": "Solveur",
+    "INFO_SPP_TITRE": "Somme et produit de puissances",
+    "INFO_SPP_TITRE_COURT": "SPP",
     "INFO_THEME_CREDITS": "Crédit",
     "INFO_THEME_DEVALAISON_TITRE": "Dévalaison",
     "INFO_THEME_DEVALAISON_DESCRIPTION": "Outils de dimensionnements des ouvrages présents sur les prises d'eau des centrales hydroélectriques dites \"ichtyocompatibles\" et constituées de plans de grilles fines associés à un ou plusieurs exutoires.",
@@ -453,6 +459,8 @@
     "INFO_THEME_LOIS_D_OUVRAGES_TITRE": "Lois d'ouvrages",
     "INFO_THEME_MODULES_INUTILISES_DESCRIPTION": "Modules de calculs divers",
     "INFO_THEME_MODULES_INUTILISES_TITRE": "Autres modules de calcul",
+    "INFO_THEME_OUTILS_MATHEMATIQUES_TITRE": "Outils mathématiques",
+    "INFO_THEME_OUTILS_MATHEMATIQUES_DESCRIPTION": "Divers outils mathématiques génériques",
     "INFO_THEME_PASSE_A_BASSIN_DESCRIPTION": "Outils de dimensionnement d'une passe à poissons de type passe à bassins ou encore appelée échelle à poisson",
     "INFO_THEME_PASSE_A_BASSIN_TITRE": "Passe à bassins",
     "INFO_THEME_PASSE_NATURELLE_DESCRIPTION": "Outils de dimensionnement d'une passe à poissons de type passe naturelle ou encore appelée passe à macro-rugosités",
@@ -465,6 +473,10 @@
     "INFO_EXAMPLE_LABEL_PAB_COMPLETE": "Passe à bassins type",
     "INFO_EXAMPLES_TITLE": "Exemples",
     "INFO_EXAMPLES_SUBTITLE": "Charger des exemples types",
+    "INFO_YAXB_TITRE": "Fonction affine",
+    "INFO_YAXB_TITRE_COURT": "F. affine",
+    "INFO_TRIGO_TITRE": "Fonction trigonométrique",
+    "INFO_TRIGO_TITRE_COURT": "F. trigo.",
     "WARNING_WARNINGS_ABSTRACT": "%nb% avertissements rencontrés lors du calcul",
     "WARNING_REMOUS_ARRET_CRITIQUE": "Arrêt du calcul&nbsp;: hauteur critique atteinte à l'abscisse %x%",
     "WARNING_STRUCTUREKIVI_HP_TROP_ELEVE": "h/p ne doit pas être supérieur à 2,5. h/p est forcé à 2,5",