diff --git a/e2e/calculate-all-params.e2e-spec.ts b/e2e/calculate-all-params.e2e-spec.ts
index e71105a5496733ae74e92a9956249268417cb4a5..49d6cc12c0ed94f86a31e88f7effe3e278f513e1 100644
--- a/e2e/calculate-all-params.e2e-spec.ts
+++ b/e2e/calculate-all-params.e2e-spec.ts
@@ -1,9 +1,5 @@
-import { AppPage } from "./app.po";
 import { ListPage } from "./list.po";
 import { CalculatorPage } from "./calculator.po";
-import { Navbar } from "./navbar.po";
-import { SideNav } from "./sidenav.po";
-import { browser } from "protractor";
 
 /**
  * For all calculators, try to calculate every parameter: check that only one parameter
@@ -32,11 +28,9 @@ describe("ngHyd − calculate all parameters of all calculators", () => {
         // get all parameters IDs
         const inputs = await calcPage.getParamInputsHavingCalcMode();
 
-        // console.log("> Inputs having calc", inputs.length);
         if (inputs.length > 0) {
           // for each param
           for (const input of inputs) {
-            // console.log(">> Trying", await input.getAttribute("id"));
             // click "calc" mode button for this parameter
             await calcPage.setParamMode(input, "cal");
             // check that only 1 button is in "calc" state
@@ -45,7 +39,7 @@ describe("ngHyd − calculate all parameters of all calculators", () => {
             // check that "compute" button is active
             const calcButton = calcPage.getCalculateButton();
             const disabledState = await calcButton.getAttribute("disabled");
-            expect(disabledState).not.toBe("disabled");
+            expect(disabledState).not.toBe("true");
             // click "compute" button
             await calcButton.click();
             // check that result is not empty
diff --git a/e2e/calculate-linked-params.e2e-spec.ts b/e2e/calculate-linked-params.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..179b55f54a5877f9ffd1c0f99ba62cc6a44b1d9c
--- /dev/null
+++ b/e2e/calculate-linked-params.e2e-spec.ts
@@ -0,0 +1,490 @@
+import { AppPage } from "./app.po";
+import { ListPage } from "./list.po";
+import { CalculatorPage } from "./calculator.po";
+import { Navbar } from "./navbar.po";
+import { SideNav } from "./sidenav.po";
+import { browser } from "protractor";
+
+/**
+ * Uses an example configuration to calculate :
+ *  - with a parameter linked to a single parameter
+ *  - with a parameter linked to a single parameter, plus local variated parameter
+ *  - with a parameter linked to a variated parameter
+ *  - with a parameter linked to a single result
+ *  - with a parameter linked to a single result, plus local variated parameter
+ *  - with a parameter linked to a variated result
+ *  - with a parameter linked to a single extra result
+ *  - with a parameter linked to a single extra result, plus local variated parameter
+ *  - with a parameter linked to a variated extra result
+ *
+ *  => plus all those combinations in indeirect link mode (linked parameter target
+ *     is also a linked parameter)
+ */
+describe("ngHyd − calculate with linked parameters", () => {
+  let listPage: ListPage;
+  let calcPage: CalculatorPage;
+  let navBar: Navbar;
+  let startPage: AppPage;
+
+  beforeEach(() => {
+    listPage = new ListPage();
+    calcPage = new CalculatorPage();
+    navBar = new Navbar();
+    startPage = new AppPage();
+
+  });
+
+  async function computeAndCheckPresenceOfResults() {
+    // check that "compute" button is active
+    const calcButton = calcPage.getCalculateButton();
+    const disabledState = await calcButton.getAttribute("disabled");
+    expect(disabledState).not.toBe("true");
+    // click "compute" button
+    await calcButton.click();
+    // check that result is not empty
+    const hasResults = await calcPage.hasResults();
+    expect(hasResults).toBe(true);
+  }
+
+  it(" − direct links : parameter linked to a single parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (R uniforme)
+    const Y = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y);
+    await calcPage.changeSelectValue(sel, 1);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a single parameter, plus local variated parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (R uniforme)
+    const Y = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y);
+    await calcPage.changeSelectValue(sel, 1);
+    // vary W
+    const W = calcPage.getInputById("W");
+    await calcPage.setParamMode(W, "var");
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a variated parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // vary Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "var");
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (R uniforme)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 1);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a single result", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // calculate Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "cal");
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (R uniforme)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 1);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a single result, plus local variated parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // calculate Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "cal");
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (R uniforme)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 1);
+    // vary W
+    const W = calcPage.getInputById("W");
+    await calcPage.setParamMode(W, "var");
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a variated result", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // vary Q
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "var");
+    // calculate Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "cal");
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (R uniforme)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 1);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a single extra result", async () => {
+    // create a Déversoirs dénoyés
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(9);
+
+    // create a Régime uniforme
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(3);
+    // link Q to CvQT (Déversoirs dénoyés)
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "link");
+    const sel = await calcPage.getLinkedValueSelect(Q);
+    await calcPage.changeSelectValue(sel, 1);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a single extra result, plus local variated parameter", async () => {
+    // create a Déversoirs dénoyés
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(9);
+
+    // create a Régime uniforme
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(3);
+    // link Q to CvQT (Déversoirs dénoyés)
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "link");
+    const sel = await calcPage.getLinkedValueSelect(Q);
+    await calcPage.changeSelectValue(sel, 1);
+    // vary YB
+    const YB = calcPage.getInputById("YB");
+    await calcPage.setParamMode(YB, "var");
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − direct links : parameter linked to a variated extra result", async () => {
+    // create a Déversoirs dénoyés
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(9);
+    // vary BR
+    const BR = calcPage.getInputById("BR");
+    await calcPage.setParamMode(BR, "var");
+
+    // create a Régime uniforme
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(3);
+    // link Q to CvQT (Déversoirs dénoyés)
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "link");
+    const sel = await calcPage.getLinkedValueSelect(Q);
+    await calcPage.changeSelectValue(sel, 1);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a single parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Y to Y (R uniforme)
+    const Yproxy = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Yproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Yproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (Sec param)
+    const Y = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Yproxy, "link");
+    const sel = await calcPage.getLinkedValueSelect(Yproxy);
+    await calcPage.changeSelectValue(selYproxy, 3);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a single parameter, plus local variated parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Y to Y (R uniforme)
+    const Yproxy = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Yproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Yproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (Sec param)
+    const Y = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y);
+    await calcPage.changeSelectValue(sel, 3);
+    // vary W
+    const W = calcPage.getInputById("W");
+    await calcPage.setParamMode(W, "var");
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a variated parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // vary Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "var");
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Y to Y (R uniforme)
+    const Yproxy = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Yproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Yproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (Sec param)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 3);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a single result", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // calculate Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "cal");
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Y to Y (R uniforme)
+    const Yproxy = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Yproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Yproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (Sec param)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 3);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a single result, plus local variated parameter", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // calculate Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "cal");
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Y to Y (R uniforme)
+    const Yproxy = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Yproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Yproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (Sec param)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 3);
+    // vary W
+    const W = calcPage.getInputById("W");
+    await calcPage.setParamMode(W, "var");
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a variated result", async () => {
+    // create a Régime uniforme
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(3);
+    // vary Q
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "var");
+    // calculate Y
+    const Y1 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y1, "cal");
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Y to Y (R uniforme)
+    const Yproxy = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Yproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Yproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a PAB : dimensions
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(5);
+    // link Y to Y (Sec param)
+    const Y2 = calcPage.getInputById("Y");
+    await calcPage.setParamMode(Y2, "link");
+    const sel = await calcPage.getLinkedValueSelect(Y2);
+    await calcPage.changeSelectValue(sel, 3);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a single extra result", async () => {
+    // create a Déversoirs dénoyés
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(9);
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Q to CvQT (Déversoirs dénoyés)
+    const Qproxy = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Qproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Qproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a Régime uniforme
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(3);
+    // link Q to CvQT (Sec param)
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "link");
+    const sel = await calcPage.getLinkedValueSelect(Q);
+    await calcPage.changeSelectValue(sel, 3);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a single extra result, plus local variated parameter", async () => {
+    // create a Déversoirs dénoyés
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(9);
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Q to CvQT (Déversoirs dénoyés)
+    const Qproxy = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Qproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Qproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a Régime uniforme
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(3);
+    // link Q to CvQT (Sec param)
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "link");
+    const sel = await calcPage.getLinkedValueSelect(Q);
+    await calcPage.changeSelectValue(sel, 3);
+    // vary YB
+    const YB = calcPage.getInputById("YB");
+    await calcPage.setParamMode(YB, "var");
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+  it(" − indirect links : parameter linked to a variated extra result", async () => {
+    // create a Déversoirs dénoyés
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(9);
+    // vary BR
+    const BR = calcPage.getInputById("BR");
+    await calcPage.setParamMode(BR, "var");
+
+    // create a Section paramétrée
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(2);
+    // link Q to CvQT (Déversoirs dénoyés)
+    const Qproxy = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Qproxy, "link");
+    const selYproxy = await calcPage.getLinkedValueSelect(Qproxy);
+    await calcPage.changeSelectValue(selYproxy, 1);
+
+    // create a Régime uniforme
+    await navBar.clickNewCalculatorButton();
+    await listPage.clickMenuEntryForCalcType(3);
+    // link Q to CvQT (Sec param)
+    const Q = calcPage.getInputById("Q");
+    await calcPage.setParamMode(Q, "link");
+    const sel = await calcPage.getLinkedValueSelect(Q);
+    await calcPage.changeSelectValue(sel, 3);
+
+    await computeAndCheckPresenceOfResults();
+  });
+
+});
diff --git a/e2e/calculator.po.ts b/e2e/calculator.po.ts
index 75d501000108cad0f8829f67d9638d5414f1ee2e..69cc7a7b04f36837903b10a36fcb7f6173faefd7 100644
--- a/e2e/calculator.po.ts
+++ b/e2e/calculator.po.ts
@@ -29,6 +29,10 @@ export class CalculatorPage {
     return element(by.css("mat-select#" + id));
   }
 
+  async getSelectValueText(select: ElementFinder) {
+    return await select.element(by.css(".mat-select-value-text > span")).getText();
+  }
+
   getInputById(id: string) {
     return element(by.css("input#" + id));
   }
@@ -46,6 +50,14 @@ export class CalculatorPage {
     return element.all(by.css('mat-button-toggle.radio_cal[ng-reflect-checked="true"]'));
   }
 
+  getAddStructureButton() {
+    return element(by.css("fieldset-container .hyd-window-btns button.add-structure"));
+  }
+
+  getAllLinkButtons() {
+    return element.all(by.css("mat-button-toggle.radio_link"));
+  }
+
   scrollTo(elt: ElementFinder) {
     browser.controlFlow().execute(function() {
       browser.executeScript("arguments[0].scrollIntoView(true)", elt.getWebElement());
@@ -108,11 +120,23 @@ export class CalculatorPage {
     await button.click();
     // for "var" mode, close the modal
     if (mode === "var") {
-      await browser.sleep(500);
+      await browser.sleep(500); // wait for the modal to appear
       await element(by.css("dialog-edit-param-values .mat-dialog-actions button")).click();
+      await browser.sleep(500); // wait for the navbar to reappear after modal dismissal
     }
   }
 
+  /**
+   * @param elt an <input> element
+   */
+  async getLinkedValueSelect(elt: ElementFinder): Promise<ElementFinder> {
+    // get parent (div.container)
+    const container = await this.findParentContainer(elt) as ElementFinder;
+    // find <select>
+    const select = container.element(by.css("param-link mat-select"));
+    return select;
+  }
+
   /**
    * Returns an object containing all the calculator's inputs values, indexed
    * by parameter ID
diff --git a/e2e/clone-calc.e2e-spec.ts b/e2e/clone-calc.e2e-spec.ts
index 459b3d7677ad34afc3e41932b10226fc678d8eb6..b768d34f0585a7c6e742cad964bdaa86703e87c6 100644
--- a/e2e/clone-calc.e2e-spec.ts
+++ b/e2e/clone-calc.e2e-spec.ts
@@ -2,7 +2,6 @@ import { AppPage } from "./app.po";
 import { ListPage } from "./list.po";
 import { CalculatorPage } from "./calculator.po";
 import { Navbar } from "./navbar.po";
-import { SideNav } from "./sidenav.po";
 import { browser } from "protractor";
 
 /**
@@ -13,14 +12,12 @@ describe("ngHyd − clone a calculator", () => {
   let listPage: ListPage;
   let calcPage: CalculatorPage;
   let navbar: Navbar;
-  let sidenav: SideNav;
 
   beforeEach(() => {
     startPage = new AppPage();
     listPage = new ListPage();
     calcPage = new CalculatorPage();
     navbar = new Navbar();
-    sidenav = new SideNav();
   });
 
   it("when cloning a calculator, the clone should have the same values for all parameters", async () => {
@@ -75,22 +72,7 @@ describe("ngHyd − clone a calculator", () => {
       const displayedVal = await calcPage.getInputById(k).getAttribute("value");
       expect(displayedVal).toBe("" + v);
     });
-  });
-
-  it("cloning a parallel-structures calculator should work", async () => {
-    await startPage.navigateTo();
-
-    // create source module to clone
-    await navbar.clickNewCalculatorButton();
-    await listPage.clickMenuEntryForCalcType(8); // Lois d'ouvrages
-    await browser.sleep(500);
-
-    // otherwise clickCloneCalcButton() fails with "Element is not clickable at point"
-    // await browser.executeScript("window.scrollTo(0, 0);");
-    await calcPage.clickCloneCalcButton();
-    await browser.sleep(500);
 
-    // check existence of the cloned module
-    expect(await navbar.getAllCalculatorTabs().count()).toBe(2);
+    // @TODO check linked value (see above)
   });
 });
diff --git a/e2e/compute-reset-chained-links.e2e-spec.ts b/e2e/compute-reset-chained-links.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dc72e70c07c07f8c04bfed9bb41286b03c22a127
--- /dev/null
+++ b/e2e/compute-reset-chained-links.e2e-spec.ts
@@ -0,0 +1,117 @@
+import { AppPage } from "./app.po";
+import { CalculatorPage } from "./calculator.po";
+import { Navbar } from "./navbar.po";
+import { SideNav } from "./sidenav.po";
+import { browser } from "protractor";
+
+/**
+ * Load a session containing 3 calculators, having linked parameters
+ * from one to another, triggers computation from the top-most, triggers
+ * results reset from the bottom-most.
+ * Does it once with parameters linked to parameters, once with parameters
+ * linked to results.
+ */
+describe("ngHyd − compute then reset chained results", () => {
+  let startPage: AppPage;
+  let calcPage: CalculatorPage;
+  let navbar: Navbar;
+  let sidenav: SideNav;
+
+  function init() {
+      startPage = new AppPage();
+      calcPage = new CalculatorPage();
+      navbar = new Navbar();
+      sidenav = new SideNav();
+  }
+  beforeEach(init);
+
+  it("when loading session-cascade-params.json, computation should not be chained, but results reset should", async () => {
+    // load session file
+    await startPage.navigateTo();
+    await navbar.clickMenuButton();
+    await browser.sleep(200);
+    await sidenav.clickLoadSessionButton();
+    await browser.sleep(200);
+    await sidenav.loadSessionFile("./session-cascade-params.json");
+    await browser.sleep(500);
+    expect(await navbar.getAllCalculatorTabs().count()).toBe(3);
+
+    // 1. get down-most module
+    await navbar.clickCalculatorTabForUid("dWs5bm");
+
+    // check that "compute" button is active
+    const calcButton = calcPage.getCalculateButton();
+    const disabledState = await calcButton.getAttribute("disabled");
+    expect(disabledState).not.toBe("true");
+    // click "compute" button
+    await calcButton.click();
+
+    // only down-most module should have results
+    let hasResults = await calcPage.hasResults();
+    // other two should not
+    await navbar.clickCalculatorTabForUid("OGFzOH");
+    hasResults = await calcPage.hasResults();
+    expect(hasResults).toBe(false);
+    await navbar.clickCalculatorTabForUid("NWp1a3");
+    hasResults = await calcPage.hasResults();
+    expect(hasResults).toBe(false);
+
+    // 2. get up-most module
+    await navbar.clickCalculatorTabForUid("OGFzOH");
+
+    // modify any input (for ex. "Ks")
+    await calcPage.getInputById("Ks").clear();
+    await calcPage.getInputById("Ks").sendKeys("42");
+
+    // check all 3 modules for absence of results
+    for (let i = 0; i < 3; i++) {
+      await navbar.clickCalculatorTab(i);
+      hasResults = await calcPage.hasResults();
+      expect(hasResults).toBe(false);
+    }
+  });
+
+  it("when loading session-cascade-results.json, computation and results reset should be chained", async () => {
+    // load session file
+    await startPage.navigateTo();
+    await navbar.clickMenuButton();
+    await browser.sleep(200);
+    await sidenav.clickLoadSessionButton();
+    await browser.sleep(200);
+    await sidenav.loadSessionFile("./session-cascade-results.json");
+    await browser.sleep(500);
+    expect(await navbar.getAllCalculatorTabs().count()).toBe(3);
+
+    // 1. get down-most module (PAB Dimensions)
+    await navbar.clickCalculatorTabForUid("bGZqcz");
+
+    // check that "compute" button is active
+    const calcButton = calcPage.getCalculateButton();
+    const disabledState = await calcButton.getAttribute("true");
+    expect(disabledState).not.toBe("true");
+    // click "compute" button
+    await calcButton.click();
+
+    // check all 3 modules for results
+    for (let i = 0; i < 3; i++) {
+      await navbar.clickCalculatorTab(i);
+      const hasResults = await calcPage.hasResults();
+      expect(hasResults).toBe(true);
+    }
+
+    // 2. get up-most module (Macro-rugo)
+    await navbar.clickCalculatorTabForUid("dnRiY2");
+
+    // modify any input (for ex. "Ks")
+    await calcPage.getInputById("Ks").clear();
+    await calcPage.getInputById("Ks").sendKeys("42");
+
+    // check all 3 modules for absence of results
+    for (let i = 0; i < 3; i++) {
+      await navbar.clickCalculatorTab(i);
+      const hasResults = await calcPage.hasResults();
+      expect(hasResults).toBe(false);
+    }
+  });
+
+});
diff --git a/e2e/link-parallel-devices.e2e-spec.ts b/e2e/link-parallel-devices.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f5c73aa4a2a8142ea8af8b6963a3f864d42a4745
--- /dev/null
+++ b/e2e/link-parallel-devices.e2e-spec.ts
@@ -0,0 +1,47 @@
+import { AppPage } from "./app.po";
+import { ListPage } from "./list.po";
+import { CalculatorPage } from "./calculator.po";
+
+/**
+ * Load a session containing 4 calculators, having multiple linked parameters
+ * from one to another
+ * @TODO les valeurs des Select sont comparées au français, pas très générique :/
+ */
+describe("ngHyd − load session with multiple linked parameters − ", () => {
+  let startPage: AppPage;
+  let listPage: ListPage;
+  let calcPage: CalculatorPage;
+
+  function init() {
+      startPage = new AppPage();
+      calcPage = new CalculatorPage();
+      listPage = new ListPage();
+  }
+  beforeEach(init);
+
+  it("when creating parallel structures, devices should be linkable to one another", async () => {
+
+    // 1. check Lois d'ouvrages
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(8);
+    await calcPage.getAddStructureButton().click();
+    const nb1 = await calcPage.getAllLinkButtons().count();
+    expect(nb1).toBe(8); // link buttons on children but not on parent
+
+    // 2. check Passe à bassin: Cloisons
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(10);
+    await calcPage.getAddStructureButton().click();
+    const nb2 = await calcPage.getAllLinkButtons().count();
+    expect(nb2).toBe(4); // link buttons on children but not on parent
+
+
+    // 3. check Lois de déversoirs dénoyés
+    await startPage.navigateTo();
+    await listPage.clickMenuEntryForCalcType(9);
+    await calcPage.getAddStructureButton().click();
+    const nb3 = await calcPage.getAllLinkButtons().count();
+    expect(nb3).toBe(6); // link buttons on children but not on parent
+
+  });
+});
diff --git a/e2e/load-linked-params.e2e-spec.ts b/e2e/load-linked-params.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2b67e12b65d7869855d7d8af034ef52985175a30
--- /dev/null
+++ b/e2e/load-linked-params.e2e-spec.ts
@@ -0,0 +1,100 @@
+import { AppPage } from "./app.po";
+import { ListPage } from "./list.po";
+import { CalculatorPage } from "./calculator.po";
+import { Navbar } from "./navbar.po";
+import { SideNav } from "./sidenav.po";
+import { browser } from "protractor";
+
+/**
+ * Load a session containing 4 calculators, having multiple linked parameters
+ * from one to another
+ * @TODO les valeurs des Select sont comparées au français, pas très générique :/
+ */
+describe("ngHyd − load session with multiple linked parameters − ", () => {
+  let startPage: AppPage;
+  let listPage: ListPage;
+  let calcPage: CalculatorPage;
+  let navbar: Navbar;
+  let sidenav: SideNav;
+
+  function init() {
+      startPage = new AppPage();
+      calcPage = new CalculatorPage();
+      navbar = new Navbar();
+      sidenav = new SideNav();
+      listPage = new ListPage();
+  }
+  beforeEach(init);
+
+   it("when loading session-liens-spaghetti.json, all links should point to the right target", async () => {
+    await startPage.navigateTo();
+
+    await navbar.clickMenuButton();
+    await browser.sleep(200);
+
+    await sidenav.clickLoadSessionButton();
+    await browser.sleep(200);
+
+    await sidenav.loadSessionFile("./session-liens-spaghetti.json");
+    await browser.sleep(500);
+
+    expect(await navbar.getAllCalculatorTabs().count()).toBe(5);
+
+    // 1. check Section paramétrée
+    await navbar.clickCalculatorTab(0);
+
+    // check target params values
+    const sp_lb = calcPage.getSelectById("linked_LargeurBerge");
+    const sp_lbv = await calcPage.getSelectValueText(sp_lb);
+    expect(sp_lbv).toEqual("L (Ouvrages, ouvrage 3)");
+
+    // 2. check Passe à macro-rugosités
+    await navbar.clickCalculatorTab(1);
+
+    // check target params values
+    const mr_b = calcPage.getSelectById("linked_B");
+    const mr_bv = await calcPage.getSelectValueText(mr_b);
+    expect(mr_bv).toEqual("L (résultat de Ouvrages)");
+
+    const mr_q = calcPage.getSelectById("linked_Q");
+    const mr_qv = await calcPage.getSelectValueText(mr_q);
+    expect(mr_qv).toEqual("Q (R. uniforme)");
+
+    // 3. check Lois d'ouvrages
+    await navbar.clickCalculatorTab(2);
+
+    // check target params values
+    const lo_z1 = calcPage.getSelectById("linked_Z1");
+    const lo_z1v = await calcPage.getSelectValueText(lo_z1);
+    expect(lo_z1v).toEqual("ZF1 (Macro-rugo.)");
+
+    const lo_l = calcPage.getSelectById("linked_L"); // attention ID non unique, voir nghyd#173
+    const lo_lv = await calcPage.getSelectValueText(lo_l);
+    expect(lo_lv).toEqual("L (résultat de Ouvrages)");
+
+    const lo_w = calcPage.getSelectById("linked_W"); // attention ID non unique, voir nghyd#173
+    const lo_wv = await calcPage.getSelectValueText(lo_w);
+    expect(lo_wv).toEqual("W (Ouvrages, ouvrage 2)");
+
+    const lo_cd = calcPage.getSelectById("linked_Cd"); // attention ID non unique, voir nghyd#173
+    const lo_cdv = await calcPage.getSelectValueText(lo_cd);
+    expect(lo_cdv).toEqual("Cd (Ouvrages, ouvrage 1)");
+
+    // 4. check Régime uniforme
+    await navbar.clickCalculatorTab(3);
+
+    // check target params values
+    const lo_q = calcPage.getSelectById("linked_Q");
+    const lo_qv = await calcPage.getSelectValueText(lo_q);
+    expect(lo_qv).toEqual("CvQT (Déver. dénoyés, résultat complémentaire)");
+
+    // 5. check Déver. dénoyés
+    await navbar.clickCalculatorTab(4);
+
+    // check target params values
+    const lo_br = calcPage.getSelectById("linked_BR");
+    const lo_brv = await calcPage.getSelectValueText(lo_br);
+    expect(lo_brv).toEqual("LargeurBerge (Sec. param.)");
+  });
+
+});
diff --git a/e2e/navbar.po.ts b/e2e/navbar.po.ts
index 2112ceecf8ae3666311cee6fe7627b09dc433b9a..621416ac95243f7d0fd7917d45e556b8cc73e4ff 100644
--- a/e2e/navbar.po.ts
+++ b/e2e/navbar.po.ts
@@ -5,6 +5,10 @@ export class Navbar {
     return element.all(by.css("#tabs-container > button.calculator-button"));
   }
 
+  getCalculatorTabForUid(uid: string) {
+    return element(by.css("#tabs-container > button.calculator-button.calculator-uid-" + uid));
+  }
+
   getNewCalculatorButton() {
     return element(by.css("#new-calculator"));
   }
@@ -18,6 +22,11 @@ export class Navbar {
     await tabs.get(n).click();
   }
 
+  async clickCalculatorTabForUid(uid: string) {
+    const tab = this.getCalculatorTabForUid(uid);
+    await tab.click();
+  }
+
   async clickRandomCalculatorTab(n: number) {
     const tabs = this.getAllCalculatorTabs();
     const l = await tabs.count();
diff --git a/e2e/session-6-calc.test.json b/e2e/session-6-calc.test.json
index 54df75d63bd35b630fa75b19a592a9d41be3e817..a24a0012f32b98e2ecd3461342b56bcbb33d764c 100644
--- a/e2e/session-6-calc.test.json
+++ b/e2e/session-6-calc.test.json
@@ -1 +1,342 @@
-{"session":[{"uid":"NHY0cX","props":{"calcType":5,"nodeType":0},"meta":{"title":"PAB : dimensions"},"parameters":[{"symbol":"Pr","mode":"SINGLE","value":0.0001},{"symbol":"L","mode":"SINGLE","value":2},{"symbol":"W","mode":"SINGLE","value":1},{"symbol":"Y","mode":"SINGLE","value":0.5},{"symbol":"V","mode":"CALCUL"}]},{"uid":"YzAwMW","props":{"calcType":11,"nodeType":0},"meta":{"title":"Macro-rugo."},"parameters":[{"symbol":"Pr","mode":"SINGLE","value":0.0001},{"symbol":"ZF1","mode":"SINGLE","value":12.5},{"symbol":"L","mode":"SINGLE","value":6},{"symbol":"B","mode":"SINGLE","value":1},{"symbol":"If","mode":"SINGLE","value":0.05},{"symbol":"Q","mode":"CALCUL"},{"symbol":"Y","mode":"SINGLE","value":0.6},{"symbol":"Ks","mode":"SINGLE","value":0.01},{"symbol":"C","mode":"SINGLE","value":0.05},{"symbol":"PBD","mode":"SINGLE","value":0.5},{"symbol":"PBH","mode":"SINGLE","value":0.8},{"symbol":"Cd0","mode":"SINGLE","value":1.5}]},{"uid":"dGc5MD","props":{"calcType":8,"nodeType":0},"meta":{"title":"Ouvrages"},"structures":[{"uid":"NjZob3","props":{"calcType":7,"nodeType":5,"structureType":1,"loiDebit":1},"parameters":[{"symbol":"ZDV","mode":"SINGLE","value":100},{"symbol":"W","mode":"SINGLE","value":0.5},{"symbol":"L","mode":"SINGLE","value":2},{"symbol":"Cd","mode":"SINGLE","value":0.6}]}],"parameters":[{"symbol":"Pr","mode":"SINGLE","value":0.0001},{"symbol":"Q","mode":"CALCUL"},{"symbol":"Z1","mode":"SINGLE","value":102},{"symbol":"Z2","mode":"SINGLE","value":101.5}]},{"uid":"OGZ4cm","props":{"varCalc":"Hs","calcType":2,"nodeType":2},"meta":{"title":"Sec. param."},"parameters":[{"symbol":"Pr","mode":"SINGLE","value":0.0001},{"symbol":"Ks","mode":"SINGLE","value":40},{"symbol":"Q","mode":"SINGLE","value":1.2},{"symbol":"If","mode":"SINGLE","value":0.001},{"symbol":"YB","mode":"SINGLE","value":1},{"symbol":"Y","mode":"SINGLE","value":0.8},{"symbol":"LargeurBerge","mode":"SINGLE","value":2.5}]},{"uid":"ZTNvMD","props":{"methodeResolution":0,"calcType":4,"nodeType":2},"meta":{"title":"Remous"},"parameters":[{"symbol":"Pr","mode":"SINGLE","value":0.0001},{"symbol":"Yamont","mode":"SINGLE","value":0.15},{"symbol":"Yaval","mode":"SINGLE","value":0.4},{"symbol":"Long","mode":"SINGLE","value":100},{"symbol":"Dx","mode":"SINGLE","value":5},{"symbol":"Ks","mode":"SINGLE","value":40},{"symbol":"Q","mode":"SINGLE","value":1.2},{"symbol":"If","mode":"SINGLE","value":0.001},{"symbol":"YB","mode":"SINGLE","value":1},{"symbol":"Y","mode":"SINGLE","value":0.2863766123093061},{"symbol":"LargeurBerge","mode":"SINGLE","value":2.5}]},{"uid":"eWllan","props":{"calcType":1,"nodeType":0},"meta":{"title":"Lechapt-Calmon"},"parameters":[{"symbol":"Pr","mode":"SINGLE","value":0.0001},{"symbol":"Q","mode":"SINGLE","value":3},{"symbol":"D","mode":"SINGLE","value":1.2},{"symbol":"J","mode":"CALCUL"},{"symbol":"Lg","mode":"SINGLE","value":100},{"symbol":"L","mode":"SINGLE","value":"1.863"},{"symbol":"M","mode":"SINGLE","value":"2"},{"symbol":"N","mode":"SINGLE","value":"5.33"}]}]}
\ No newline at end of file
+{
+    "session": [
+        {
+            "uid": "NHY0cX",
+            "props": {
+                "calcType": 5,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "PAB : dimensions"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "L",
+                    "mode": "SINGLE",
+                    "value": 2
+                },
+                {
+                    "symbol": "W",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.5
+                },
+                {
+                    "symbol": "V",
+                    "mode": "CALCUL"
+                }
+            ]
+        },
+        {
+            "uid": "YzAwMW",
+            "props": {
+                "calcType": 11,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "Macro-rugo."
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "ZF1",
+                    "mode": "SINGLE",
+                    "value": 12.5
+                },
+                {
+                    "symbol": "L",
+                    "mode": "SINGLE",
+                    "value": 6
+                },
+                {
+                    "symbol": "B",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.05
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.6
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 0.01
+                },
+                {
+                    "symbol": "C",
+                    "mode": "SINGLE",
+                    "value": 0.05
+                },
+                {
+                    "symbol": "PBD",
+                    "mode": "SINGLE",
+                    "value": 0.5
+                },
+                {
+                    "symbol": "PBH",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "Cd0",
+                    "mode": "SINGLE",
+                    "value": 1.5
+                }
+            ]
+        },
+        {
+            "uid": "dGc5MD",
+            "props": {
+                "calcType": 8,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "Ouvrages"
+            },
+            "structures": [
+                {
+                    "uid": "NjZob3",
+                    "props": {
+                        "calcType": 7,
+                        "nodeType": 5,
+                        "structureType": 1,
+                        "loiDebit": 1
+                    },
+                    "parameters": [
+                        {
+                            "symbol": "ZDV",
+                            "mode": "SINGLE",
+                            "value": 100
+                        },
+                        {
+                            "symbol": "W",
+                            "mode": "SINGLE",
+                            "value": 0.5
+                        },
+                        {
+                            "symbol": "L",
+                            "mode": "SINGLE",
+                            "value": 2
+                        },
+                        {
+                            "symbol": "Cd",
+                            "mode": "SINGLE",
+                            "value": 0.6
+                        }
+                    ]
+                }
+            ],
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "Z1",
+                    "mode": "SINGLE",
+                    "value": 102
+                },
+                {
+                    "symbol": "Z2",
+                    "mode": "SINGLE",
+                    "value": 101.5
+                }
+            ]
+        },
+        {
+            "uid": "OGZ4cm",
+            "props": {
+                "varCalc": "Hs",
+                "calcType": 2,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "Sec. param."
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 1.2
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "SINGLE",
+                    "value": 2.5
+                }
+            ]
+        },
+        {
+            "uid": "ZTNvMD",
+            "props": {
+                "methodeResolution": 0,
+                "calcType": 4,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "Remous"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Yamont",
+                    "mode": "SINGLE",
+                    "value": 0.15
+                },
+                {
+                    "symbol": "Yaval",
+                    "mode": "SINGLE",
+                    "value": 0.4
+                },
+                {
+                    "symbol": "Long",
+                    "mode": "SINGLE",
+                    "value": 100
+                },
+                {
+                    "symbol": "Dx",
+                    "mode": "SINGLE",
+                    "value": 5
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 1.2
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.2863766123093061
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "SINGLE",
+                    "value": 2.5
+                }
+            ]
+        },
+        {
+            "uid": "eWllan",
+            "props": {
+                "calcType": 1,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "Lechapt-Calmon"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 3
+                },
+                {
+                    "symbol": "D",
+                    "mode": "SINGLE",
+                    "value": 1.2
+                },
+                {
+                    "symbol": "J",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "Lg",
+                    "mode": "SINGLE",
+                    "value": 100
+                },
+                {
+                    "symbol": "L",
+                    "mode": "SINGLE",
+                    "value": "1.863"
+                },
+                {
+                    "symbol": "M",
+                    "mode": "SINGLE",
+                    "value": "2"
+                },
+                {
+                    "symbol": "N",
+                    "mode": "SINGLE",
+                    "value": "5.33"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/e2e/session-cascade-params.json b/e2e/session-cascade-params.json
new file mode 100644
index 0000000000000000000000000000000000000000..773053d5409813c93e2adfe6811d03ac299945d3
--- /dev/null
+++ b/e2e/session-cascade-params.json
@@ -0,0 +1,146 @@
+{
+    "session": [
+        {
+            "uid": "OGFzOH",
+            "props": {
+                "varCalc": "Hs",
+                "calcType": 2,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "Sec. param."
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 1.2
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "SINGLE",
+                    "value": 2.5
+                }
+            ]
+        },
+        {
+            "uid": "NWp1a3",
+            "props": {
+                "calcType": 3,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "R. uniforme"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "LINK",
+                    "targetNub": "OGFzOH",
+                    "targetParam": "LargeurBerge"
+                }
+            ]
+        },
+        {
+            "uid": "dWs5bm",
+            "props": {
+                "calcType": 3,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "R. uniforme 1"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "LINK",
+                    "targetNub": "NWp1a3",
+                    "targetParam": "LargeurBerge"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/e2e/session-cascade-results.json b/e2e/session-cascade-results.json
new file mode 100644
index 0000000000000000000000000000000000000000..3c144185070cf9e20ba23b314555d023eb9e6376
--- /dev/null
+++ b/e2e/session-cascade-results.json
@@ -0,0 +1,159 @@
+{
+    "session": [
+        {
+            "uid": "dnRiY2",
+            "props": {
+                "calcType": 11,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "Macro-rugo."
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "ZF1",
+                    "mode": "SINGLE",
+                    "value": 12.5
+                },
+                {
+                    "symbol": "L",
+                    "mode": "SINGLE",
+                    "value": 6
+                },
+                {
+                    "symbol": "B",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "If",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 1.57
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.6
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 0.01
+                },
+                {
+                    "symbol": "C",
+                    "mode": "SINGLE",
+                    "value": 0.05
+                },
+                {
+                    "symbol": "PBD",
+                    "mode": "SINGLE",
+                    "value": 0.5
+                },
+                {
+                    "symbol": "PBH",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "Cd0",
+                    "mode": "SINGLE",
+                    "value": 1.5
+                }
+            ]
+        },
+        {
+            "uid": "YjZyND",
+            "props": {
+                "calcType": 3,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "R. uniforme"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "LINK",
+                    "targetNub": "dnRiY2",
+                    "targetParam": "Q_GuideTech"
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "CALCUL"
+                }
+            ]
+        },
+        {
+            "uid": "bGZqcz",
+            "props": {
+                "calcType": 5,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "PAB : dimensions"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "L",
+                    "mode": "SINGLE",
+                    "value": 2
+                },
+                {
+                    "symbol": "W",
+                    "mode": "LINK",
+                    "targetNub": "YjZyND",
+                    "targetParam": "LargeurBerge"
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.5
+                },
+                {
+                    "symbol": "V",
+                    "mode": "CALCUL"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/e2e/session-liens-spaghetti.json b/e2e/session-liens-spaghetti.json
new file mode 100644
index 0000000000000000000000000000000000000000..87b17d2a13cc46819f2620f39cf5ba412304773f
--- /dev/null
+++ b/e2e/session-liens-spaghetti.json
@@ -0,0 +1,372 @@
+{
+    "session": [
+        {
+            "uid": "OW9rd3",
+            "props": {
+                "varCalc": "Hs",
+                "calcType": 2,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "Sec. param."
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 1.2
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "LINK",
+                    "targetNub": "dmh4Z3",
+                    "targetParam": "L"
+                }
+            ]
+        },
+        {
+            "uid": "dTB3ZG",
+            "props": {
+                "calcType": 11,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "Macro-rugo."
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "ZF1",
+                    "mode": "SINGLE",
+                    "value": 12.5
+                },
+                {
+                    "symbol": "L",
+                    "mode": "SINGLE",
+                    "value": 6
+                },
+                {
+                    "symbol": "B",
+                    "mode": "LINK",
+                    "targetNub": "ZW9icn",
+                    "targetParam": "L"
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.05
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "LINK",
+                    "targetNub": "OGhuZj",
+                    "targetParam": "Q"
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 0.01
+                },
+                {
+                    "symbol": "C",
+                    "mode": "SINGLE",
+                    "value": 0.05
+                },
+                {
+                    "symbol": "PBD",
+                    "mode": "SINGLE",
+                    "value": 0.5
+                },
+                {
+                    "symbol": "PBH",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "Cd0",
+                    "mode": "MINMAX",
+                    "min": 0.75,
+                    "max": 3,
+                    "step": 0.1125
+                }
+            ]
+        },
+        {
+            "uid": "ZW9icn",
+            "props": {
+                "calcType": 8,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "Ouvrages"
+            },
+            "structures": [
+                {
+                    "uid": "cjdyYW",
+                    "props": {
+                        "calcType": 7,
+                        "nodeType": 5,
+                        "structureType": 1,
+                        "loiDebit": 1
+                    },
+                    "parameters": [
+                        {
+                            "symbol": "ZDV",
+                            "mode": "SINGLE",
+                            "value": 100
+                        },
+                        {
+                            "symbol": "W",
+                            "mode": "SINGLE",
+                            "value": 0.5
+                        },
+                        {
+                            "symbol": "L",
+                            "mode": "CALCUL"
+                        },
+                        {
+                            "symbol": "Cd",
+                            "mode": "SINGLE",
+                            "value": 0.6
+                        }
+                    ]
+                },
+                {
+                    "uid": "Ynpnaj",
+                    "props": {
+                        "calcType": 7,
+                        "nodeType": 5,
+                        "structureType": 1,
+                        "loiDebit": 1
+                    },
+                    "parameters": [
+                        {
+                            "symbol": "ZDV",
+                            "mode": "SINGLE",
+                            "value": 100
+                        },
+                        {
+                            "symbol": "W",
+                            "mode": "SINGLE",
+                            "value": 0.5
+                        },
+                        {
+                            "symbol": "L",
+                            "mode": "LINK",
+                            "targetNub": "ZW9icn",
+                            "targetParam": "L"
+                        },
+                        {
+                            "symbol": "Cd",
+                            "mode": "SINGLE",
+                            "value": 0.6
+                        }
+                    ]
+                },
+                {
+                    "uid": "dmh4Z3",
+                    "props": {
+                        "calcType": 7,
+                        "nodeType": 5,
+                        "structureType": 1,
+                        "loiDebit": 1
+                    },
+                    "parameters": [
+                        {
+                            "symbol": "ZDV",
+                            "mode": "SINGLE",
+                            "value": 100
+                        },
+                        {
+                            "symbol": "W",
+                            "mode": "LINK",
+                            "targetNub": "Ynpnaj",
+                            "targetParam": "W"
+                        },
+                        {
+                            "symbol": "L",
+                            "mode": "SINGLE",
+                            "value": 2
+                        },
+                        {
+                            "symbol": "Cd",
+                            "mode": "LINK",
+                            "targetNub": "cjdyYW",
+                            "targetParam": "Cd"
+                        }
+                    ]
+                }
+            ],
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 0.5
+                },
+                {
+                    "symbol": "Z1",
+                    "mode": "LINK",
+                    "targetNub": "dTB3ZG",
+                    "targetParam": "ZF1"
+                },
+                {
+                    "symbol": "Z2",
+                    "mode": "SINGLE",
+                    "value": 101.5
+                }
+            ]
+        },
+        {
+            "uid": "OGhuZj",
+            "props": {
+                "calcType": 3,
+                "nodeType": 2
+            },
+            "meta": {
+                "title": "R. uniforme"
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "LINK",
+                    "targetNub": "NGlpMn",
+                    "targetParam": "CvQT"
+                },
+                {
+                    "symbol": "If",
+                    "mode": "MINMAX",
+                    "min": 0.0005,
+                    "max": 0.002,
+                    "step": 0.00007500000000000001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "CALCUL"
+                }
+            ]
+        },
+        {
+            "uid": "NGlpMn",
+            "props": {
+                "calcType": 9,
+                "nodeType": 0
+            },
+            "meta": {
+                "title": "Déver. dénoyés"
+            },
+            "structures": [
+                {
+                    "uid": "M29rcW",
+                    "props": {
+                        "calcType": 7,
+                        "nodeType": 5,
+                        "structureType": 0,
+                        "loiDebit": 7
+                    },
+                    "parameters": [
+                        {
+                            "symbol": "ZDV",
+                            "mode": "SINGLE",
+                            "value": 100
+                        },
+                        {
+                            "symbol": "L",
+                            "mode": "SINGLE",
+                            "value": 2
+                        },
+                        {
+                            "symbol": "Cd",
+                            "mode": "SINGLE",
+                            "value": 0.4
+                        }
+                    ]
+                }
+            ],
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "CALCUL"
+                },
+                {
+                    "symbol": "Z1",
+                    "mode": "SINGLE",
+                    "value": 102
+                },
+                {
+                    "symbol": "BR",
+                    "mode": "LINK",
+                    "targetNub": "OW9rd3",
+                    "targetParam": "LargeurBerge"
+                },
+                {
+                    "symbol": "ZR",
+                    "mode": "SINGLE",
+                    "value": 99
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/e2e/session-optional-params.test.json b/e2e/session-optional-params.test.json
index 804c559cb54b2dfa3f367b66a9ca29de7e953e4a..6fb0e40e804a24c0926dec69b77e432a75c46355 100644
--- a/e2e/session-optional-params.test.json
+++ b/e2e/session-optional-params.test.json
@@ -1 +1,57 @@
-{"session":[{"uid":"N2U4OH","props":{"varCalc":"Hs","calcType":2,"nodeType":4},"meta":{"title":"Sec. param."},"parameters":[{"symbol":"Pr","mode":"SINGLE","value":0.0001},{"symbol":"Ks","mode":"SINGLE","value":40},{"symbol":"Q","mode":"SINGLE","value":1.2},{"symbol":"If","mode":"SINGLE","value":0.001},{"symbol":"YB","mode":"SINGLE","value":1},{"symbol":"Y","mode":"SINGLE","value":0.8},{"symbol":"LargeurBerge","mode":"SINGLE","value":4},{"symbol":"k","mode":"SINGLE","value":0.5}]}]}
\ No newline at end of file
+{
+    "session": [
+        {
+            "uid": "N2U4OH",
+            "props": {
+                "varCalc": "Hs",
+                "calcType": 2,
+                "nodeType": 4
+            },
+            "meta": {
+                "title": "Sec. param."
+            },
+            "parameters": [
+                {
+                    "symbol": "Pr",
+                    "mode": "SINGLE",
+                    "value": 0.0001
+                },
+                {
+                    "symbol": "Ks",
+                    "mode": "SINGLE",
+                    "value": 40
+                },
+                {
+                    "symbol": "Q",
+                    "mode": "SINGLE",
+                    "value": 1.2
+                },
+                {
+                    "symbol": "If",
+                    "mode": "SINGLE",
+                    "value": 0.001
+                },
+                {
+                    "symbol": "YB",
+                    "mode": "SINGLE",
+                    "value": 1
+                },
+                {
+                    "symbol": "Y",
+                    "mode": "SINGLE",
+                    "value": 0.8
+                },
+                {
+                    "symbol": "LargeurBerge",
+                    "mode": "SINGLE",
+                    "value": 4
+                },
+                {
+                    "symbol": "k",
+                    "mode": "SINGLE",
+                    "value": 0.5
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/jalhyd_branch b/jalhyd_branch
index 1f7391f92b6a3792204e07e99f71f643cc35e7e1..6ba102133f195085ddf25826b3ac5dbc6162e06a 100644
--- a/jalhyd_branch
+++ b/jalhyd_branch
@@ -1 +1 @@
-master
+54-ameliorer-le-filtre-de-choix-des-parametres-pouvant-etre-lies
diff --git a/src/app/app.component.html b/src/app/app.component.html
index f2863b8e4cdfe3487b6b04c8626fa864f7f60cb9..fc3d49453c11e6096b8b155b1bf1d34f08fb0ded 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -40,9 +40,9 @@
 
     <!-- calculators list as a tabs bar-->
     <div id="tabs-container" [hidden]="! tabsFitInNavbar">
-      <button mat-raised-button color="primary" *ngFor="let c of calculators" class="calculator-button" [title]="c.title"
-        [routerLink]="['/calculator/',c.uid]" [color]="c.active ? '' : 'primary'" [class.active]="c.active"
-        (click)="setActiveCalc(c.uid)">
+      <button mat-raised-button color="primary" *ngFor="let c of calculators" class="" [title]="c.title"
+        [routerLink]="['/calculator/',c.uid]" [color]="c.active ? '' : 'primary'" (click)="setActiveCalc(c.uid)"
+        [ngClass]="['calculator-button', 'calculator-uid-' + c.uid, c.active ? 'active' : '' ]">
 
         <span class="calc-name">
           {{ c.title }}
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index c45d95ed140f69b985c28b720d717217d322a516..98e0c6c01190ec530e9de00bbd4607cee816e439 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -115,7 +115,7 @@ button:focus {
         border-bottom: none;
     }
     .calculator-button.mat-raised-button {
-        height: calc(#{$navbar_height} - 3px);
+        height: calc(#{$navbar_height} - 5px);
 
         &:not([class*="mat-elevation-z"]) {
             box-shadow: -1px -2px 2px 0 rgba(0,0,0,.4),1px -2px 5px 0 rgba(0,0,0,.1);
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 4e9c48e531fd1469389d35ccac7ccb011f166064..e12e5260384b0dd74c73386f24e6389de43142df 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -18,6 +18,7 @@ import { nghydDateRev } from "../date_revision";
 import { DialogConfirmEmptySessionComponent } from "./components/dialog-confirm-empty-session/dialog-confirm-empty-session.component";
 import { DialogLoadSessionComponent } from "./components/dialog-load-session/dialog-load-session.component";
 import { DialogSaveSessionComponent } from "./components/dialog-save-session/dialog-save-session.component";
+import { NotificationsService } from "./services/notifications/notifications.service";
 
 
 @Component({
@@ -66,6 +67,7 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
     private router: Router,
     private formulaireService: FormulaireService,
     private httpService: HttpService,
+    private notificationsService: NotificationsService,
     private confirmEmptySessionDialog: MatDialog,
     private saveSessionDialog: MatDialog,
     private loadSessionDialog: MatDialog,
@@ -76,6 +78,7 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
     ServiceFactory.instance.applicationSetupService = appSetupService;
     ServiceFactory.instance.i18nService = intlService;
     ServiceFactory.instance.formulaireService = formulaireService;
+    ServiceFactory.instance.notificationsService = notificationsService;
 
     this.router.events.subscribe((event: Event) => {
       // close side navigation when clicking a calculator tab
@@ -217,16 +220,7 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
   // interface Observer
 
   update(sender: any, data: any): void {
-    if (sender instanceof ErrorService) {
-      // on ouvre un dialogue avec le message d'erreur reçu
-      // if (this._displayErrorDialog) {
-      //   let dialogRef = this.dialog.open(AlertDialog);
-      //   let ad: AlertDialog = dialogRef.componentInstance;
-      //   ad.text = String(data);
-      // }
-      // else
-      console.log(data);
-    } else if (sender instanceof FormulaireService) {
+    if (sender instanceof FormulaireService) {
       switch (data["action"]) {
         case "createForm":
         // add newly created form to calculators list
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 2759ef9f56aad5027654e2911e2c776c4e34f6e5..059c34d50c5b202c8865a3ab6fe161f492104f85 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -19,6 +19,7 @@ import {
   MatCardModule,
   MatTableModule,
   MatSnackBarModule,
+  MatBadgeModule,
   ErrorStateMatcher,
   MatButtonToggleModule
 } from "@angular/material";
@@ -43,6 +44,7 @@ import { FormulaireService } from "./services/formulaire/formulaire.service";
 import { I18nService } from "./services/internationalisation/internationalisation.service";
 import { HttpService } from "./services/http/http.service";
 import { ApplicationSetupService } from "./services/app-setup/app-setup.service";
+import { NotificationsService } from "./services/notifications/notifications.service";
 
 import { AppComponent } from "./app.component";
 import { NgParamInputComponent } from "./components/ngparam-input/ngparam-input.component";
@@ -52,7 +54,6 @@ import { ParamComputedComponent } from "./components/param-computed/param-comput
 import { ParamFieldLineComponent } from "./components/param-field-line/param-field-line.component";
 import { ParamValuesComponent } from "./components/param-values/param-values.component";
 import { SelectFieldLineComponent } from "./components/select-field-line/select-field-line.component";
-import { CheckFieldLineComponent } from "./components/check-field-line/check-field-line.component";
 import { CalculatorResultsComponent } from "./components/calculator-results/calculator-results.component";
 import { FixedVarResultsComponent } from "./components/fixedvar-results/fixedvar-results.component";
 import { SectionResultsComponent } from "./components/section-results/section-results.component";
@@ -104,6 +105,7 @@ const appRoutes: Routes = [
     ChartModule,
     HttpClientModule,
     FlexLayoutModule,
+    MatBadgeModule,
     MatButtonModule,
     MatButtonToggleModule,
     MatCardModule,
@@ -140,7 +142,6 @@ const appRoutes: Routes = [
     CalculatorListComponent,
     CalculatorNameComponent,
     CalculatorResultsComponent,
-    CheckFieldLineComponent,
     DialogConfirmCloseCalcComponent,
     DialogConfirmEmptySessionComponent,
     DialogEditParamComputedComponent,
@@ -189,6 +190,7 @@ const appRoutes: Routes = [
     FormulaireService,
     HttpService,
     I18nService,
+    NotificationsService,
     {
       provide: ErrorStateMatcher,
       useClass: ImmediateErrorStateMatcher
diff --git a/src/app/components/app-setup/app-setup.component.ts b/src/app/components/app-setup/app-setup.component.ts
index 0b373a0d2b671309579c175a9c31e5f0a750b199..87b96145b475d9387b827c0f3066d338f16a0793 100644
--- a/src/app/components/app-setup/app-setup.component.ts
+++ b/src/app/components/app-setup/app-setup.component.ts
@@ -83,7 +83,7 @@ export class ApplicationSetupComponent extends BaseComponent implements Observer
     public storePreferences() {
         this.appSetupService.saveValuesIntoLocalStorage();
         this.snackBar.open(this.intlService.localizeText("INFO_SNACKBAR_SETTINGS_SAVED"), "OK", {
-            duration: 1200
+            duration: 2500
         });
     }
 
@@ -91,7 +91,7 @@ export class ApplicationSetupComponent extends BaseComponent implements Observer
         const text = this.intlService.localizeText("INFO_SNACKBAR_DEFAULT_SETTINGS_RESTORED");
         this.appSetupService.restoreDefaultValues().then(() => {
             this.snackBar.open(text, "OK", {
-                duration: 1200
+                duration: 2500
             });
         });
     }
diff --git a/src/app/components/base-param-input/base-param-input.component.ts b/src/app/components/base-param-input/base-param-input.component.ts
index e04b0a4607e2d3632eab3e07700c7a8b2c0bb888..4c31d115f57d1680dc8dc8c005c555ff03983ba3 100644
--- a/src/app/components/base-param-input/base-param-input.component.ts
+++ b/src/app/components/base-param-input/base-param-input.component.ts
@@ -34,7 +34,7 @@ export class NgBaseParam extends Observable {
     }
 
     public checkValue(val: number) {
-        return this._param.checkValue(val);
+        return this._param.checkValueAgainstDomain(val);
     }
 
     public checkMin(min: number): boolean {
@@ -71,11 +71,11 @@ export class NgBaseParam extends Observable {
             msg = ServiceFactory.instance.i18nService.localizeText("ERROR_PARAM_NULL");
         } else {
             try {
-                this._param.checkValue(v);
+                this._param.checkValueAgainstDomain(v);
                 valid = true;
             } catch (e) {
                 if (e instanceof Message) {
-                    // @TODO ici au début le service de localisation n'a pas encore chargé ses messages…
+                    // ici au début le service de localisation n'a pas encore chargé ses messages…
                     msg = ServiceFactory.instance.i18nService.localizeMessage(e);
                 } else {
                     msg = "invalid value";
@@ -92,8 +92,8 @@ export class NgBaseParam extends Observable {
     templateUrl: "../generic-input/generic-input.component.html",
 })
 export class BaseParamInputComponent extends GenericInputComponent {
-    constructor(private intlService: I18nService, cdRef: ChangeDetectorRef) {
-        super(cdRef);
+    constructor(intlService: I18nService, cdRef: ChangeDetectorRef) {
+        super(cdRef, intlService);
     }
 
     /**
diff --git a/src/app/components/check-field-line/check-field-line.component.html b/src/app/components/check-field-line/check-field-line.component.html
deleted file mode 100644
index f00f6fca5843423f7d52b41f5a509c5a70648fde..0000000000000000000000000000000000000000
--- a/src/app/components/check-field-line/check-field-line.component.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<tr>
-    <td align="right">{{ check.label }}</td>
-    <td colspan="3">
-        <input type="checkbox" [(ngModel)]=currentValue (ngModelChange)="onChange($event)">
-    </td>
-</tr>
\ No newline at end of file
diff --git a/src/app/components/check-field-line/check-field-line.component.ts b/src/app/components/check-field-line/check-field-line.component.ts
deleted file mode 100644
index ae159f511062947071e1ccfefddfec5653ecb09e..0000000000000000000000000000000000000000
--- a/src/app/components/check-field-line/check-field-line.component.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Component, Input, Output, EventEmitter } from "@angular/core";
-
-import { CheckField } from "../../formulaire/check-field";
-
-@Component({
-    selector: "check-field-line",
-    templateUrl: "./check-field-line.component.html",
-})
-export class CheckFieldLineComponent {
-    @Input()
-    public check: CheckField;
-
-    public currentValue: boolean;
-
-    constructor() { }
-
-    public onChange(event: any) {
-        this.check.setValue(event);
-    }
-}
diff --git a/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.html b/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.html
index 8018436a73c3111ceb3ee4e5888067a83e049ac4..f1abc0500a58e9143591b98877d6575857a7fbcf 100644
--- a/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.html
+++ b/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.html
@@ -1,7 +1,18 @@
 <h1 mat-dialog-title [innerHTML]="uitextCloseCalcTitle"></h1>
 <div mat-dialog-content>
-  <p [innerHTML]="uitextCloseCalcBody"></p>
+
+  <div *ngIf="dependingNubs.length">
+    {{ uitextDependingModules }}
+    <mat-list role="list">
+      <mat-list-item role="listitem" *ngFor="let dn of dependingNubs">
+        {{ dn }}
+      </mat-list-item>
+    </mat-list>
+  </div>
 </div>
+
+<p [innerHTML]="uitextCloseCalcBody"></p>
+
 <div mat-dialog-actions>
   <button mat-raised-button color="primary" [mat-dialog-close]="false" cdkFocusInitial>
     {{ uitextNo }}
diff --git a/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.scss b/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..268f18ad89927376209eddd80ee16d1a8065535f
--- /dev/null
+++ b/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.scss
@@ -0,0 +1,3 @@
+.mat-list-base .mat-list-item {
+    height: 24px;
+}
diff --git a/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.ts b/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.ts
index e8a1f44ca145c27fef780d49aeaead3e49706aa1..63cdc8fa4a9bb903145cce4501f13aabb90caca9 100644
--- a/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.ts
+++ b/src/app/components/dialog-confirm-close-calc/dialog-confirm-close-calc.component.ts
@@ -1,18 +1,30 @@
 import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material";
 import { Inject, Component } from "@angular/core";
+import { FormulaireService } from "../../services/formulaire/formulaire.service";
 import { I18nService } from "../../services/internationalisation/internationalisation.service";
+import { Session } from "jalhyd";
 
 @Component({
     selector: "dialog-confirm-close-calc",
     templateUrl: "dialog-confirm-close-calc.component.html",
+    styleUrls: [
+      "dialog-confirm-close-calc.component.scss"
+    ]
 })
 export class DialogConfirmCloseCalcComponent {
 
+    private _dependingNubs: any[] = [];
+
     constructor(
         public dialogRef: MatDialogRef<DialogConfirmCloseCalcComponent>,
         private intlService: I18nService,
+        private formService: FormulaireService,
         @Inject(MAT_DIALOG_DATA) public data: any
-    ) { }
+    ) {
+        this._dependingNubs = Session.getInstance().getDependingNubs(data.uid).map((n) => {
+            return this.formService.getFormulaireFromNubId(n.uid).calculatorName;
+        });
+    }
 
     public get uitextYes() {
       return this.intlService.localizeText("INFO_OPTION_YES");
@@ -30,4 +42,12 @@ export class DialogConfirmCloseCalcComponent {
       return this.intlService.localizeText("INFO_CLOSE_DIALOGUE_TEXT");
     }
 
+    public get uitextDependingModules() {
+      return this.intlService.localizeText("INFO_CLOSE_DIALOGUE_DEPENDING_MODULES");
+    }
+
+    public get dependingNubs() {
+      return this._dependingNubs;
+    }
+
 }
diff --git a/src/app/components/dialog-edit-param-computed/dialog-edit-param-computed.component.html b/src/app/components/dialog-edit-param-computed/dialog-edit-param-computed.component.html
index 9efaf288795c96ec273dc07655f1f13889cfad2a..517a0f0d1a378c689b6a7aabfef85ae47c944daf 100644
--- a/src/app/components/dialog-edit-param-computed/dialog-edit-param-computed.component.html
+++ b/src/app/components/dialog-edit-param-computed/dialog-edit-param-computed.component.html
@@ -4,7 +4,6 @@
 
   <div mat-dialog-content>
     <ngparam-input [title]="param.title" ></ngparam-input>
-    <!-- (change)="onInputChange($event)" -->
   </div>
 
   <div mat-dialog-actions>
diff --git a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html
index c4ace21ec7c7b7c93417e7d709af024c2a9db285..8fbe604a524054fb39e7dc0727076d78e1e348b9 100644
--- a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html
+++ b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.html
@@ -20,7 +20,7 @@
         <form>
             <mat-form-field>
                 <input matInput class="form-control" type="number" inputmode="numeric" name="min-value" step="0.01"
-                    [placeholder]="uitextValeurMini" [(ngModel)]="param.minValue" #min="ngModel" name="min"
+                    [placeholder]="uitextValeurMini" [(ngModel)]="minValue" #min="ngModel" name="min"
                     [appJalhydModelValidationMin]="param" required pattern="^-?([0-9]*\.)?([0-9]+[Ee]-?)?[0-9]+$">
 
                 <mat-error *ngIf="min.errors">
@@ -35,7 +35,7 @@
 
             <mat-form-field>
                 <input matInput class="form-control" type="number" inputmode="numeric" name="max-value" step="0.01"
-                    [placeholder]="uitextValeurMaxi" [(ngModel)]="param.maxValue" #max="ngModel" name="max"
+                    [placeholder]="uitextValeurMaxi" [(ngModel)]="maxValue" #max="ngModel" name="max"
                     [appJalhydModelValidationMax]="param" required pattern="^-?([0-9]*\.)?([0-9]+[Ee]-?)?[0-9]+$">
 
                     <mat-error *ngIf="max.errors">
@@ -50,7 +50,7 @@
 
             <mat-form-field>
                 <input matInput class="form-control" type="number" inputmode="numeric" name="step-value" step="0.01"
-                    [placeholder]="uitextPasVariation" [(ngModel)]="param.stepValue" #step="ngModel" name="step"
+                    [placeholder]="uitextPasVariation" [(ngModel)]="stepValue" #step="ngModel" name="step"
                     [appJalhydModelValidationStep]="param" required pattern="^-?([0-9]*\.)?([0-9]+[Ee]-?)?[0-9]+$">
 
                     <mat-error *ngIf="step.errors">
diff --git a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss
index 111eef7498f0d945ad5b49c5ca290bdfb9f6c426..0ffd355d6448f01b32eac82575ee4dba7d17a7d4 100644
--- a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss
+++ b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.scss
@@ -19,6 +19,7 @@ mat-form-field {
 
 .min-max-step-container {
     margin-top: -8px;
+    padding-bottom: 1em; // for possible mat-error on step field
 }
 
 .mat-dialog-content {
diff --git a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
index 0d1ad08b82e61020358e4a1cce7c4fed4f917c66..bb9168725ae178f4ac4e7e95fbf6e51ce306831f 100644
--- a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
+++ b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
@@ -114,6 +114,32 @@ export class DialogEditParamValuesComponent implements OnInit {
         };
     }
 
+    // proxy to model values
+
+    public get minValue() {
+        return this.param.minValue;
+    }
+
+    public set minValue(v) {
+        this.param.setMinValue(this, v);
+    }
+
+    public get maxValue() {
+        return this.param.maxValue;
+    }
+
+    public set maxValue(v) {
+        this.param.setMaxValue(this, v);
+    }
+
+    public get stepValue() {
+        return this.param.stepValue;
+    }
+
+    public set stepValue(v) {
+        this.param.setStepValue(this, v);
+    }
+
     /**
      * regular expression pattern for values list validation (depends on decimal separator)
      */
@@ -172,7 +198,7 @@ export class DialogEditParamValuesComponent implements OnInit {
                 vals.push(Number(e));
             }
         });
-        this.param.valueList = vals;
+        this.param.setValueList(this, vals);
     }
 
     public toggleViewChart() {
@@ -280,24 +306,24 @@ export class DialogEditParamValuesComponent implements OnInit {
         // init min / max / step
         if (this.isMinMax) {
             if (this.param.minValue === undefined) {
-                this.param.minValue = this.param.getValue() / 2;
+                this.param.setMinValue(this, this.param.getValue() / 2);
             }
             if (this.param.maxValue === undefined) {
-                this.param.maxValue = this.param.getValue() * 2;
+                this.param.setMaxValue(this, this.param.getValue() * 2);
             }
             let step = this.param.stepValue;
             if (step === undefined) {
                 step = (this.param.maxValue - this.param.minValue) / 20;
             }
-            this.param.stepValue = step;
+            this.param.setStepValue(this, step);
         }
         // init values list
         if (this.isListe) {
             if (this.param.valueList === undefined) {
                 if (this.param.isDefined) {
-                    this.param.valueList = [ this.param.getValue() ];
+                    this.param.setValueList(this, [ this.param.getValue() ]);
                 } else {
-                    this.param.valueList = [];
+                    this.param.setValueList(this, []);
                 }
                 // set form control initial value
                 this.valuesListForm.controls.valuesList.setValue(this.valuesList);
diff --git a/src/app/components/dialog-load-session/dialog-load-session.component.html b/src/app/components/dialog-load-session/dialog-load-session.component.html
index 0473b4cdeb95ad579bf3a19d62aeb8fda50f2427..97e2d6e4296093ab83d1575c2dee75b65b934bae 100644
--- a/src/app/components/dialog-load-session/dialog-load-session.component.html
+++ b/src/app/components/dialog-load-session/dialog-load-session.component.html
@@ -13,7 +13,8 @@
     </mat-form-field>
 
     <div class="cb-container">
-      <mat-checkbox [name]="c.uid" *ngFor="let c of calculators" [(ngModel)]="c.selected" [ngModelOptions]="{standalone: true}">
+      <mat-checkbox [name]="c.uid" *ngFor="let c of calculators" (change)="checkLinkedParamsDependencies()"
+        [(ngModel)]="c.selected" [ngModelOptions]="{standalone: true}">
         {{ c.title }}
       </mat-checkbox>
     </div>
@@ -26,6 +27,20 @@
         {{ uitextNone }}
       </button>
     </div>
+
+    <div class="dependencies-problems" *ngIf="dependenciesProblems.length > 0">
+      <mat-list role="list">
+        <mat-list-item role="listitem" *ngFor="let dp of dependenciesProblems">
+          <mat-icon color="warn">error_outline</mat-icon> {{ dp.message }}
+        </mat-list-item>
+      </mat-list>
+      <p>
+        <button mat-raised-button (click)="fixDependencies()"
+            [matBadge]="dependenciesProblems.length" matBadgeColor="warn">
+          {{ uitextFixMissingDependencies }}
+        </button>
+      </p>
+    </div>
   </div>
 
   <div mat-dialog-actions>
diff --git a/src/app/components/dialog-load-session/dialog-load-session.component.scss b/src/app/components/dialog-load-session/dialog-load-session.component.scss
index e612ad2cf5490552d96d024c1122ef4cd1bbfc1f..f9cad276513dfef8f4fd62cf8ecc57e177dc538b 100644
--- a/src/app/components/dialog-load-session/dialog-load-session.component.scss
+++ b/src/app/components/dialog-load-session/dialog-load-session.component.scss
@@ -17,3 +17,24 @@ mat-form-field {
         margin-right: 5px;
     }
 }
+
+.dependencies-problems {
+
+    .mat-list-base {
+        padding-top: 0;
+
+        .mat-list-item {
+            height: 32px;
+            font-size: .9em;
+
+            ::ng-deep .mat-list-item-content {
+                padding-left: 0;
+
+                mat-icon {
+                    transform: scale(0.8);
+                    margin-right: 5px;
+                }
+            }
+        }
+    }
+}
diff --git a/src/app/components/dialog-load-session/dialog-load-session.component.ts b/src/app/components/dialog-load-session/dialog-load-session.component.ts
index 3aa55c836a420dd0966dd5df2ae72687a382911e..b91673e4bfaf924ba1e5350971bf702c7c2f841f 100644
--- a/src/app/components/dialog-load-session/dialog-load-session.component.ts
+++ b/src/app/components/dialog-load-session/dialog-load-session.component.ts
@@ -17,6 +17,8 @@ export class DialogLoadSessionComponent {
 
     public loadSessionForm: FormGroup;
 
+    public dependenciesProblems: any[] = [];
+
     constructor(
       public dialogRef: MatDialogRef<DialogLoadSessionComponent>,
       private intlService: I18nService,
@@ -32,12 +34,84 @@ export class DialogLoadSessionComponent {
       for (const c of this.calculators) {
         c.selected = true;
       }
+      // re-run dependency checking
+      this.checkLinkedParamsDependencies();
     }
 
     public selectNone() {
       for (const c of this.calculators) {
         c.selected = false;
       }
+      // re-run dependency checking
+      this.checkLinkedParamsDependencies();
+    }
+
+    public checkLinkedParamsDependencies() {
+      this.dependenciesProblems = [];
+      // for all checked Nubs
+      this.calculators.forEach((c) => {
+        if (c.selected) {
+          // do all required nubs are checked ?
+          c.requires.forEach((r) => {
+            if (! this.isCalculatorOrParentSelected(r)) {
+              const realUid = this.getUidOrParentUid(r);
+              const depTitle = this.getTitleFromUid(realUid);
+              this.dependenciesProblems.push({
+                requiring: c.title,
+                required: depTitle,
+                requiredUid: realUid,
+                message: c.title + " " + this.intlService.localizeText("INFO_REQUIRES") + " " + depTitle
+              });
+            }
+          });
+        }
+      });
+    }
+
+    public fixDependencies() {
+      for (const dp of this.dependenciesProblems) {
+        this.selectRequiredModule(dp.requiredUid);
+      }
+    }
+
+    private isCalculatorOrParentSelected(uid: string): boolean {
+      let isSelected = false;
+      this.calculators.forEach((c) => {
+        if (c.uid === uid || c.children.includes(uid)) {
+          isSelected = c.selected;
+        }
+      });
+      return isSelected;
+    }
+
+    private getUidOrParentUid(uid: string): string {
+      let realUid: string;
+      this.calculators.forEach((c) => {
+        if (c.uid === uid || c.children.includes(uid)) {
+          realUid = c.uid;
+        }
+      });
+      return realUid;
+    }
+
+    private getTitleFromUid(uid: string): string {
+      let title: string;
+      this.calculators.forEach((c) => {
+        if (c.uid === uid) {
+          title = c.title;
+        }
+      });
+      return title;
+    }
+
+    private selectRequiredModule(uid: string) {
+      this.calculators.forEach((c) => {
+        if (c.uid === uid) {
+          c.selected = true;
+        }
+      });
+      // re-run dependency checking
+      this.checkLinkedParamsDependencies();
     }
 
     public onFileSelected(event: any) {
@@ -94,4 +168,8 @@ export class DialogLoadSessionComponent {
     public get uitextLoadSessionTitle() {
       return this.intlService.localizeText("INFO_DIALOG_LOAD_SESSION_TITLE");
     }
+
+    public get uitextFixMissingDependencies() {
+      return this.intlService.localizeText("INFO_DIALOG_FIX_MISSING_DEPENDENCIES");
+    }
 }
diff --git a/src/app/components/field-set/field-set.component.html b/src/app/components/field-set/field-set.component.html
index e048b39153d6154fea40eec191fbefd36abab6e6..b87bbe724705e0503c85490c8e4cbdabb19188d8 100644
--- a/src/app/components/field-set/field-set.component.html
+++ b/src/app/components/field-set/field-set.component.html
@@ -3,7 +3,7 @@
         {{ title }}
     </mat-card-title>
     <div *ngIf="showButtons" class="hyd-window-btns">
-        <button type="button" mat-icon-button (click)="onAddClick()">
+        <button type="button" mat-icon-button (click)="onAddClick()" class="add-structure">
             <mat-icon>add_box</mat-icon>
         </button>
         <button type="button" mat-icon-button [disabled]="! enableRemoveButton" (click)="onRemoveClick()">
@@ -25,7 +25,5 @@
 
         <select-field-line *ngIf="isSelectField(p)" [_select]=p>
         </select-field-line>
-
-        <check-field-line *ngIf="isCheckField(p)" [check]=p></check-field-line>
     </ng-template>
 </mat-card-content>
diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts
index 170ba7cda872e75c9591c4b2022933f40cec04cf..1541cba64a890778294eeaa5309ece21a2204796 100644
--- a/src/app/components/field-set/field-set.component.ts
+++ b/src/app/components/field-set/field-set.component.ts
@@ -6,7 +6,6 @@ import { ParamFieldLineComponent } from "../param-field-line/param-field-line.co
 import { Field } from "../../formulaire/field";
 import { InputField } from "../../formulaire/input-field";
 import { SelectField } from "../../formulaire/select-field";
-import { CheckField } from "../../formulaire/check-field";
 
 @Component({
     selector: "field-set",
@@ -172,13 +171,6 @@ export class FieldSetComponent implements DoCheck {
         return f instanceof SelectField && f.isDisplayed;
     }
 
-    /**
-     * détermine si un Field est du type CheckField
-     */
-    private isCheckField(f: Field): boolean {
-        return f instanceof CheckField && f.isDisplayed;
-    }
-
     /*
      * gestion des événements clic sur les radios :
      * réception d'un message du composant enfant (param-field)
diff --git a/src/app/components/fixedvar-results/fixedvar-results.component.ts b/src/app/components/fixedvar-results/fixedvar-results.component.ts
index d6dca7a41a47100383d7af0cc518c1190b680ad1..3392fb698cf71523bca8cd1a036c9f8380bfb0dc 100644
--- a/src/app/components/fixedvar-results/fixedvar-results.component.ts
+++ b/src/app/components/fixedvar-results/fixedvar-results.component.ts
@@ -98,6 +98,7 @@ export class FixedVarResultsComponent implements DoCheck {
             this.resultsGraphComponent.results = undefined;
         }
 
+        // set _doUpdate flag so that results are rebuilt on the next Angular display cycle
         this._doUpdate = false;
         if (this._fixedResults !== undefined) {
             this._doUpdate = this._fixedResults.hasResults || this._fixedResults.hasLog;
diff --git a/src/app/components/generic-calculator/calc-name.component.ts b/src/app/components/generic-calculator/calc-name.component.ts
index 208d11e9737a1484ac513de75271a67efc88c7fe..b4c109b6f308abd98de68791949468534bf8c2b3 100644
--- a/src/app/components/generic-calculator/calc-name.component.ts
+++ b/src/app/components/generic-calculator/calc-name.component.ts
@@ -1,6 +1,7 @@
-import { Component, Input } from "@angular/core";
+import { Component } from "@angular/core";
 import { GenericInputComponent } from "../generic-input/generic-input.component";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
+import { I18nService } from "../../services/internationalisation/internationalisation.service";
 
 @Component({
     selector: "calc-name",
@@ -11,8 +12,8 @@ import { FormulaireDefinition } from "../../formulaire/definition/form-definitio
 })
 export class CalculatorNameComponent extends GenericInputComponent {
 
-    constructor() {
-        super(null);
+    constructor(intlService: I18nService) {
+        super(null, intlService);
     }
 
     /**
@@ -39,31 +40,11 @@ export class CalculatorNameComponent extends GenericInputComponent {
         this.updateAndValidateUI();
     }
 
-    /**
-     * valide une valeur de modèle : est ce une valeur acceptable ? (par ex, nombre dans un intervalle, valeur dans une liste, ...)
-     * @param v valide la valeur du modèle
-     * @returns isValid : true si la valeur est valide, false sinon
-     * @returns message : message d'erreur
-     */
     protected validateModelValue(v: any): { isValid: boolean, message: string } {
-        let msg;
-        let valid = false;
-
-        if (!(typeof (v) === "string") || v.length < 1) {
-            msg = "Veuillez entrer un nom";
-        } else {
-            valid = true;
-        }
-
-        return { isValid: valid, message: msg };
+        // no model validation for a simple string
+        return { isValid: true, message: undefined };
     }
 
-    /**
-     * valide une valeur saisie dans l'UI (forme de la saisie : est ce bien une date, un nombre, ...)
-     * @param ui valide la valeur saisie
-     * @returns isValid : true si la valeur est valide, false sinon
-     * @returns message : message d'erreur
-     */
     protected validateUIValue(ui: string): { isValid: boolean, message: string } {
         let valid = false;
         let msg: string;
@@ -77,10 +58,9 @@ export class CalculatorNameComponent extends GenericInputComponent {
         return { isValid: valid, message: msg };
     }
 
-    /**
-     * convertit une valeur saisie dans l'UI en valeur affectable au modèle
-     */
-    protected uiToModel(ui: string): any {
-        return ui;
+    public updateModelFromUI() {
+        if (this.validateUI()) {
+            this.setAndValidateModel(this, this.uiValue); // do NOT cast UI value to Number
+        }
     }
 }
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index af807c10911ab4ab8b0269dd1066b9cc02d6d9b6..31842b9690c9e477dfd037eebcbe24686421144d 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -1,7 +1,7 @@
 import { Component, OnInit, DoCheck, OnDestroy, ViewChild, ViewChildren, QueryList, AfterViewChecked } from "@angular/core";
 import { ActivatedRoute, Router } from "@angular/router";
 
-import { Observer, Session, ParallelStructure } from "jalhyd";
+import { Observer, Session, ParamValueMode } from "jalhyd";
 
 import { FormulaireService } from "../../services/formulaire/formulaire.service";
 import { I18nService } from "../../services/internationalisation/internationalisation.service";
@@ -122,7 +122,7 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
     /**
      * détermine si un FormulaireElement est du type FieldsetContainer
      */
-    private isFieldsetContainer(fe: any): boolean {
+    public isFieldsetContainer(fe: any): boolean {
         return fe instanceof FieldsetContainer;
     }
 
@@ -164,9 +164,7 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
     }
 
     ngDoCheck() {
-        if (this._formulaire !== undefined) {
-            this.isCalculateDisabled = !(this._formulaire.isValid && this._isUIValid);
-        }
+        this.isCalculateDisabled = ! this._isUIValid;
     }
 
     ngOnDestroy() {
@@ -192,8 +190,10 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
     /*
      * gestion des événements clic sur les radios
      */
-    private onRadioClick(info: any) {
-        this.updateLinkedParameters();
+    public onRadioClick(info: any) {
+        if (info.param.valueMode === ParamValueMode.LINK) {
+            this.updateLinkedParameters(); // only when switching to LINK mode
+        }
         this._pendingRadioClick = true;
         this._pendingRadioClickInfo = info;
     }
@@ -246,11 +246,6 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
         }
     }
 
-    private getFieldsetStyleDisplay(id: string) {
-        const isDisplayed: boolean = this._formulaire.isDisplayed(id);
-        return isDisplayed ? "block" : "none";
-    }
-
     private setForm(f: FormulaireDefinition) {
         if (this._formulaire !== undefined) {
             this._formulaire.removeObserver(this);
@@ -313,11 +308,7 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
                     // accumulator (valeur précédente du résultat)
                     acc,
                     // currentValue (élément courant dans le tableau)
-                    fieldset,
-                    // currentIndex (indice courant dans le tableau)
-                    currIndex,
-                    // array (tableau parcouru)
-                    array
+                    fieldset
                 ) => {
                     return acc && fieldset.isValid;
                 }
@@ -331,11 +322,7 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
                     // accumulator (valeur précédente du résultat)
                     acc,
                     // currentValue (élément courant dans le tableau)
-                    fieldsetContainer,
-                    // currentIndex (indice courant dans le tableau)
-                    currIndex,
-                    // array (tableau parcouru)
-                    array
+                    fieldsetContainer
                 ) => {
                     return acc && fieldsetContainer.isValid;
                 }
@@ -344,25 +331,34 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
         }
     }
 
+    public getFieldsetStyleDisplay(id: string) {
+        const isDisplayed: boolean = this._formulaire.isDisplayed(id);
+        return isDisplayed ? "block" : "none";
+    }
+
     /**
      * réception d'un événement de validité d'un FieldSetComponent
      */
-    private OnFieldsetValid() {
+    public OnFieldsetValid() {
         this.updateUIValidity();
     }
 
     /**
      * réception d'un événement de validité d'un FieldsetContainerComponent
      */
-    private onFieldsetContainerValid() {
+    public onFieldsetContainerValid() {
         this.updateUIValidity();
     }
 
     /**
      * réception d'un événement de changement de valeur d'un input
      */
-    private onInputChange() {
-        this._formulaire.resetResults();
+    public onInputChange() {
+        this._formulaire.resetResults([]);
+    }
+
+    public openHelp() {
+        window.open("assets/docs-fr/calculators/" + this._formulaire.helpLink + "/", "_blank");
     }
 
     /**
@@ -375,10 +371,6 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
         return false;
     }
 
-    private openHelp() {
-        window.open("assets/docs-fr/calculators/" + this._formulaire.helpLink + "/", "_blank");
-    }
-
     public saveCalculator() {
         this.formulaireService.saveForm(this._formulaire);
     }
@@ -386,7 +378,12 @@ export class GenericCalculatorComponent extends BaseComponent implements OnInit,
     public closeCalculator() {
         const dialogRef = this.confirmCloseCalcDialog.open(
             DialogConfirmCloseCalcComponent,
-            { disableClose: true }
+            {
+                data: {
+                  uid: this._formulaire.currentNub.uid
+                },
+                disableClose: true
+            }
         );
         dialogRef.afterClosed().subscribe(result => {
             if (result) {
diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts
index 4b34c81133b49c704c6fdb00f2427f0f1f47c1e4..1bac270188ab49a87be7951a421abad277d45947 100644
--- a/src/app/components/generic-input/generic-input.component.ts
+++ b/src/app/components/generic-input/generic-input.component.ts
@@ -4,6 +4,7 @@ import { BaseComponent } from "../base/base.component";
 import { isNumeric } from "jalhyd";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
 import { NgParameter } from "../../formulaire/ngparam";
+import { I18nService } from "../../services/internationalisation/internationalisation.service";
 
 /**
  * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur
@@ -88,7 +89,7 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
 
     @ViewChild("inputControl") inputField: NgModel;
 
-    constructor(private cdRef: ChangeDetectorRef) {
+    constructor(private cdRef: ChangeDetectorRef, protected intlService: I18nService) {
         super();
     }
 
@@ -129,7 +130,7 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
         }
     }
 
-    private validateUI() {
+    protected validateUI() {
         const { isValid, message } = this.validateUIValue(this._uiValue);
         this._errorMessageUI = message;
         this.detectChanges();
@@ -182,11 +183,10 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
      * événement de changement de la valeur du modèle
      */
     private emitModelChanged() {
-        // console.log("emit model change", this.constructor.name);
         this.change.emit({ "action": "model", "value": this.getModelValue() });
     }
 
-    private setAndValidateModel(sender: any, v: any) {
+    protected setAndValidateModel(sender: any, v: any) {
         this.setModelValue(sender, v);
         this.emitModelChanged();
 
@@ -205,7 +205,7 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
      * MAJ et validation de l'UI
      */
     protected updateAndValidateUI() {
-        this._uiValue = this.modelToUI(this.getModelValue());
+        this._uiValue = String(this.getModelValue());
         this.validateUI();
     }
 
@@ -227,7 +227,7 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
      */
     public updateModelFromUI() {
         if (this.validateUI()) {
-            this.setAndValidateModel(this, this.uiToModel(this._uiValue));
+            this.setAndValidateModel(this, +this._uiValue); // cast UI value to Number
         }
     }
 
@@ -240,7 +240,6 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
      * appelé quand les @Input changent
      */
     public ngOnChanges() {
-        // console.log("ng on changes (generic input)", this.constructor.name);
         this.updateAll();
     }
 
@@ -272,13 +271,6 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
      */
     protected abstract validateModelValue(v: any): { isValid: boolean, message: string };
 
-    /**
-     * convertit le modèle en valeur affichable par l'UI
-     */
-    protected modelToUI(v: any): string {
-        return String(v);
-    }
-
     /**
      * valide une valeur saisie dans l'UI (forme de la saisie : est ce bien une date, un nombre, ...)
      * @param ui saisie à valider
@@ -290,113 +282,11 @@ export abstract class GenericInputComponent extends BaseComponent implements OnC
         let msg: string;
 
         if (! isNumeric(ui)) {
-            msg = "Veuillez entrer une valeur numérique";
+            msg = this.intlService.localizeText("ERROR_PARAM_MUST_BE_A_NUMBER");
         } else {
             valid = true;
         }
 
         return { isValid: valid, message: msg };
     }
-
-    /**
-     * convertit une valeur saisie dans l'UI en valeur affectable au modèle
-     */
-    protected uiToModel(ui: string): any {
-        return +ui;
-    }
-}
-
-/*
- * exemples d'utilisation de GenericInputComponent
- */
-
-/*
-import { Component } from "@angular/core";
-
-import { isNumeric, Message } from "jalhyd";
-
-// exemple où le modèle est un type simple (number)
-
-@Component({
-    selector: "test-input",
-    template: `<div class="md-form form-sm">
-    <input  type="text" id="form1" class="form-control" [disabled]="isDisabled" [(ngModel)]="uiValue">
-    <label for="form1">{{title}}</label>
-    <small *ngIf="showError" class="text-danger">{{errorMessage}}</small>
-</div>`
-})
-export class TestInputComponent extends GenericInputComponent {
-    constructor() {
-        super();
-        this._model = 0;
-    }
-
-    protected getModelValue(): any {
-        return this._model;
-    }
-
-    protected setModelValue(v: any) {
-        this._model = v;
-    }
-
-    protected validateModelValue(v: any): { isValid: boolean, message: string } {
-        let msg = undefined;
-        let valid = false;
-
-        if (v < 0)
-            msg = "La valeur n'est pas >= 0 ";
-        else
-            valid = true;
-
-        return { isValid: valid, message: msg };
-    }
-
-    protected uiToModel(ui: string): any {
-        return +ui;
-    }
-}
-
-
-// exemple où le modèle est une classe dont on ne gère qu'un membre
-
-import { ParamDefinition } from "jalhyd";
-
-@Component({
-    selector: "test2-input",
-    template: `<div class="md-form form-sm">
-    <input type="text" id="form1" class="form-control" [disabled]="isDisabled" [(ngModel)]="uiValue">
-    <label for="form1">{{title}}</label>
-    <small *ngIf="showError" class="text-danger">{{errorMessage}}</small>
-</div>`
-})
-export class Test2InputComponent extends GenericInputComponent {
-    constructor() {
-        super();
-    }
-
-    // paramètre géré
-    private get _param(): ParamDefinition {
-        return this._model;
-    }
-
-    protected getModelValue(): any {
-        return this._param.getValue();
-    }
-
-    protected setModelValue(v: any) {
-        this._param.setValue(v);
-    }
-
-    protected validateModelValue(v: any): { isValid: boolean, message: string } {
-        let msg = undefined;
-        let valid = false;
-
-        if (v < 0)
-            msg = "La valeur n'est pas >= 0 ";
-        else
-            valid = true;
-
-        return { isValid: valid, message: msg };
-    }
 }
-/**/
diff --git a/src/app/components/ngparam-input/ngparam-input.component.ts b/src/app/components/ngparam-input/ngparam-input.component.ts
index 7e1af55cead345ce13871671d3174e359cfbc07f..2717729d82fb4eb87e2ba8d810d656c9da122167 100644
--- a/src/app/components/ngparam-input/ngparam-input.component.ts
+++ b/src/app/components/ngparam-input/ngparam-input.component.ts
@@ -29,8 +29,8 @@ export class NgParamInputComponent extends GenericInputComponent implements Obse
      */
     private _tmp: number;
 
-    constructor(private intlService: I18nService, cdRef: ChangeDetectorRef) {
-        super(cdRef);
+    constructor(intlService: I18nService, cdRef: ChangeDetectorRef) {
+        super(cdRef, intlService);
     }
 
     /**
diff --git a/src/app/components/param-field-line/param-field-line.component.html b/src/app/components/param-field-line/param-field-line.component.html
index a9847d2b272656a448098d22908b4487672e131b..acea333d21ba24a32813148e8aa38eb7147670f6 100644
--- a/src/app/components/param-field-line/param-field-line.component.html
+++ b/src/app/components/param-field-line/param-field-line.component.html
@@ -10,23 +10,23 @@
         <param-computed *ngIf="isRadioCalChecked" [title]="title" [param]="param"></param-computed>
 
         <!-- composant pour gérer le cas "paramètre à varier" (min-max/liste de valeurs) -->
-        <param-values *ngIf="isRadioVarChecked" [title]="title" [param]="param" (valid)=onParamValuesValid($event)></param-values>
+        <param-values *ngIf="isRadioVarChecked" [title]="title" [param]="param" (change)="onInputChange($event)" (valid)=onParamValuesValid($event)></param-values>
     
         <!-- composant pour gérer le cas "paramètre lié" -->
-        <param-link *ngIf="isRadioLinkChecked" [title]="title" [param]="param" (valid)=onParamValuesValid($event)></param-link>
+        <param-link *ngIf="isRadioLinkChecked" [title]="title" [param]="param" (change)="onInputChange($event)" (valid)=onParamValuesValid($event)></param-link>
     </div>
 
     <div class="toggle-group-container" fxFlex="0 0 auto">
         <mat-button-toggle-group *ngIf="hasRadioFix() || hasRadioVar() || hasRadioCal() || hasRadioLink()">
 
             <mat-button-toggle class="radio_fix" value="radio_fix" 
-                (click)="onRadioClick('fix')" [checked]="isRadioFixChecked">
+                (click)="onRadioClick('fix')" [checked]="isRadioFixChecked" [disabled]="! canExitCalcMode()">
                 <span fxHide.xxs>{{ uitextParamFixe }}</span>
                 <span fxHide.gt-xxs>F</span>
             </mat-button-toggle>
         
             <mat-button-toggle class="radio_var" value="radio_var" *ngIf="hasRadioVar()"
-                (click)="onRadioClick('var')" [checked]="isRadioVarChecked">
+                (click)="onRadioClick('var')" [checked]="isRadioVarChecked" [disabled]="! canExitCalcMode()">
                 <span fxHide.xxs>{{ uitextParamVarier }}</span>
                 <span fxHide.gt-xxs>V</span>
             </mat-button-toggle>
@@ -38,7 +38,7 @@
             </mat-button-toggle>
         
             <mat-button-toggle class="radio_link" value="radio_link" *ngIf="hasRadioLink()"
-                (click)="onRadioClick('link')" [checked]="isRadioLinkChecked">
+                (click)="onRadioClick('link')" [checked]="isRadioLinkChecked" [disabled]="! canExitCalcMode()">
                 <span fxHide.xxs>{{ uitextParamLie }}</span>
                 <span fxHide.gt-xxs>L</span>
             </mat-button-toggle>
diff --git a/src/app/components/param-field-line/param-field-line.component.ts b/src/app/components/param-field-line/param-field-line.component.ts
index 435f63cfbf3c00df06a53b986e5008c731abc8d3..06a78419f964544919d962f0dbe3c70903509b9d 100644
--- a/src/app/components/param-field-line/param-field-line.component.ts
+++ b/src/app/components/param-field-line/param-field-line.component.ts
@@ -4,7 +4,7 @@ import { I18nService } from "../../services/internationalisation/internationalis
 import { NgParameter, ParamRadioConfig } from "../../formulaire/ngparam";
 import { NgParamInputComponent } from "../ngparam-input/ngparam-input.component";
 import { ServiceFactory } from "../../services/service-factory";
-import { ParamValueMode, CalculatorType, ParallelStructure } from "jalhyd";
+import { ParamValueMode, ParallelStructure, Nub } from "jalhyd";
 import { FormulaireService } from "../../services/formulaire/formulaire.service";
 import { ParamLinkComponent } from "../param-link/param-link.component";
 import { ParamComputedComponent } from "../param-computed/param-computed.component";
@@ -199,14 +199,14 @@ export class ParamFieldLineComponent implements OnChanges {
         if (this._formService.formulaires.length > 0) {
             // au moins 2 modules de calcul ouverts
             if (this._formService.formulaires.length > 1) {
-                return this._formService.filterLinkableValues(this._formService.getLinkableValues(this.param)).length > 0;
+                return this._formService.getLinkableValues(this.param).length > 0;
             }
 
             // ou un seul module de calcul "ouvrages parallèles"
-            if (this._formService.formulaires[0].calculatorType === CalculatorType.ParallelStructure) {
-                const ps: ParallelStructure = this._formService.formulaires[0].currentNub as ParallelStructure;
+            if (this._formService.formulaires[0].currentNub instanceof ParallelStructure) {
+                const ps: ParallelStructure = this._formService.formulaires[0].currentNub;
                 if (ps.structures.length > 1) {
-                    return this._formService.filterLinkableValues(this._formService.getLinkableValues(this.param)).length > 0;
+                    return this._formService.getLinkableValues(this.param).length > 0;
                 }
             }
 
@@ -214,13 +214,28 @@ export class ParamFieldLineComponent implements OnChanges {
         return false;
     }
 
-    private onRadioClick(option: string) {
+    /**
+     * Returns true if the current parameter is in CALC mode but no SINGLE
+     * parameter is available to take its place
+     */
+    public canExitCalcMode() {
+        let ret = true;
+        if (this.param.paramDefinition.isCalculated) {
+            const nub = this.param.paramDefinition.parentNub;
+            const p = nub.findFirstSingleParameter(this.param.paramDefinition);
+            ret = (p !== undefined);
+        }
+        return ret;
+    }
+
+    public onRadioClick(option: string) {
         const oldValue = this.param.valueMode;
         switch (option) {
             case "fix":
                 this.param.valueMode = ParamValueMode.SINGLE;
-                // @WTF why do we reset the value here ?
-                this.param.setValue(this, this.param.paramDefinition.paramValues.singleValue);
+                // reset the value to avoid "undefined" after exiting CALC or LINK mode
+                // @TODO not always necessary; find out why
+                this.param.setValue(this, this.param.paramDefinition.singleValue);
                 break;
 
             case "var":
@@ -236,7 +251,7 @@ export class ParamFieldLineComponent implements OnChanges {
                 break;
 
             case "cal":
-                this.param.valueMode = ParamValueMode.CALCUL;
+                this.param.setCalculated(); // sets mode to CALCUL and more
                 break;
 
             case "link":
@@ -277,7 +292,7 @@ export class ParamFieldLineComponent implements OnChanges {
     /**
      * réception d'un événement de validité de ParamValuesComponent
      */
-    private onParamValuesValid(event: boolean) {
+    public onParamValuesValid(event: boolean) {
         this._isRangeValid = event;
         this.emitValidity();
     }
diff --git a/src/app/components/param-link/param-link.component.html b/src/app/components/param-link/param-link.component.html
index c1a76465e33645de39ec1e72745b402a1f0bfc9d..9f8fce8bf91dae36d2053e6e08df9389756e4552 100644
--- a/src/app/components/param-link/param-link.component.html
+++ b/src/app/components/param-link/param-link.component.html
@@ -5,9 +5,6 @@
             {{ selectItemLabel(e) }}
         </mat-option>
     </mat-select>
-    <mat-error>
-        {{ message }}
-    </mat-error>
 </mat-form-field>
 
 <mat-icon #tooltip="matTooltip" [matTooltip]="tooltipText" matTooltipClass="linked-param-tooltip"
diff --git a/src/app/components/param-link/param-link.component.ts b/src/app/components/param-link/param-link.component.ts
index a382729a442cb38418794b05b04206a723860bce..7c3e8887e091705db9277d4bf60ac516f667322b 100644
--- a/src/app/components/param-link/param-link.component.ts
+++ b/src/app/components/param-link/param-link.component.ts
@@ -2,10 +2,11 @@ import { Component, Input, Output, EventEmitter, OnChanges, OnDestroy } from "@a
 
 import { NgParameter } from "../../formulaire/ngparam";
 import { ServiceFactory } from "../../services/service-factory";
-import { ParamValueMode, Observer, Structure, ParallelStructure } from "jalhyd";
+import { LinkedValue, ParamValueMode, Observer, Structure } from "jalhyd";
 import { FormulaireService } from "../../services/formulaire/formulaire.service";
 import { I18nService } from "../../services/internationalisation/internationalisation.service";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
+import { sprintf } from "sprintf-js";
 
 @Component({
     selector: "param-link",
@@ -26,32 +27,18 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
     private valid: EventEmitter<boolean>;
 
     /**
-     * indice actuel du paramètre sélectionné dans la liste
+     * événement signalant un changement de valeur du modèle
+     * @TODO l'utiliser aussi pour le changement de validité à
+     * la place de this.valid, comme dans GenericInputComponent
      */
-    private _currentIndex = -1;
+    @Output()
+    protected change = new EventEmitter<any>();
 
-    /**
-     * message affiché à côté du select des paramètres
-     */
-    private _message: string;
+    /** indice actuel du paramètre sélectionné dans la liste */
+    private _currentIndex = -1;
 
-    /**
-     * liste des paramètres liables sous la forme
-     * {"name":<étiquette>, "value":<valeur liable>, "nub":<Nub d'origine du paramètre>, "formTitle":<nom du module de calcul lié au nub>}
-     *
-     * l'étiquette "name" (cf. INubReference.defineReference dans jalhyd) est de la forme <n | ouvrage[n] | N1>[.[N2]]
-     *     n : indice de de l'ouvrage dans le cas des ouvrages parallèles
-     *     N1 : un nom de paramètre/résultat (dans le cas d'un résultat, il est suivi d'un point)
-     *     N2 : nom de résultat complémentaire (optionnel)
-     *     ex :
-     *       Q, Z1 (paramètres)
-     *       J. (résultat)
-     *       .Yf (résultat complémentaire du résultat courant)
-     *       Q.Yf (résultat complémentaire du résultat nommé "Q")
-     *       0.Q : paramètre Q du 1er ouvrage (ouvrages parallèles)
-     *       ouvrage[1].Q_Mode : résultat complémentaire du 2ème ouvrage (ouvrages parallèles)
-     */
-    private _linkableParams: any[];
+    /** liste des paramètres liables */
+    private _linkableParams: LinkedValue[];
 
     private _formService: FormulaireService;
 
@@ -71,24 +58,21 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
         return this._linkableParams;
     }
 
-    public get message() {
-        return this._message;
-    }
-
     public get label() {
         return this.intlService.localizeText("INFO_PARAMFIELD_PARAMLIE_LABEL");
     }
 
-    public set currentLinkedParam(p: any) {
+    public set currentLinkedParam(p: LinkedValue) {
         for (let i = 0; i < this._linkableParams.length; i++) {
-            if (this._linkableParams[i].nub.uid === p.nub.uid) {
+            const li = this._linkableParams[i];
+            if (li.equals(p)) {
                 this.linkTo(i);
                 break;
             }
         }
     }
 
-    public get currentLinkedParam() {
+    public get currentLinkedParam(): LinkedValue {
         if (this._linkableParams !== undefined) {
             if (this._currentIndex !== -1 && this._currentIndex < this._linkableParams.length) {
                 return this._linkableParams[this._currentIndex];
@@ -117,65 +101,93 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
     }
 
     /**
-     * attribut "label" d'une entrée du select des paramètres
+     * Label d'une entrée du select des paramètres liés
      */
-    private selectItemLabel(i: any): string {
-        const s = i.name; // nom associé au paramètre/à la valeur
-        const c = i.formTitle; // nom du module de calcul
+    private selectItemLabel(i: LinkedValue): string {
+        const s = i.symbol; // nom associé au paramètre/à la valeur
+        const c = i.meta["formTitle"]; // nom du module de calcul
 
-        // pointeur direct vers une Structure dans un Nub de type parallèle
-        if (i.nub && i.nub instanceof Structure) {
+        // 1. Paramètre / résultat d'un ouvrage dans un Nub de type parallèle
+        if (i.nub instanceof Structure) {
             let p: number;
             p = i.nub.findPositionInParent();
-            // forme xxx. (résultat)
-            const re4 = /([^\.]+)\.$/;
-            const match4 = re4.exec(s);
-            if (match4 !== null) {
+            if (i.isResult()) {
                 // résultat d'ouvrage
-                return `${match4[1]} (résultat de ${c}, ouvrage ${p + 1})`;
+                return sprintf(
+                    this.intlService.localizeText("INFO_LINKED_VALUE_DEVICE_RESULT"),
+                    s, c, (p + 1)
+                );
             } else {
                 // paramètre d'ouvrage
-                return `${s} (${c}, ouvrage ${p + 1})`;
+                return sprintf(
+                    this.intlService.localizeText("INFO_LINKED_VALUE_DEVICE"),
+                    s, c, (p + 1)
+                );
             }
+        } else
+        // 2. Résultat
+        if (i.isResult()) {
+            return sprintf(
+                this.intlService.localizeText("INFO_LINKED_VALUE_RESULT"),
+                s, c
+            );
+        } else
+        // 3. Résultat complémentaire
+        if (i.isExtraResult()) {
+            if (i.meta["result"]) {
+                // @TODO not used ?
+                return sprintf(
+                    this.intlService.localizeText("INFO_LINKED_VALUE_EXTRA_RESULT_OF"),
+                    s, c, i.meta["result"]
+                );
+            } else {
+                return sprintf(
+                    this.intlService.localizeText("INFO_LINKED_VALUE_EXTRA_RESULT"),
+                    s, c
+                );
+            }
+        } else {
+            // 4. Paramètre (cas général)
+            return `${s} (${c})`;
         }
-
-        const re1 = /([^\.]+)\.$/;  // forme xxx. (résultat)
-        const match1 = re1.exec(s);
-        if (match1 !== null) {
-            return `${match1[1]} (résultat de ${c})`;
-        }
-
-        const re2 = /([^\.]+)\.(.+)/;  // forme xxx.yyy (résultat complémentaire)
-        const match2 = re2.exec(s);
-        if (match2 !== null) {
-            return `${match2[2]} (${c}, résultat complémentaire de ${match2[1]})`;
-        }
-
-        const re3 = /^\.(.+)/;  // forme .xxx (résultat complémentaire)
-        const match3 = re3.exec(s);
-        if (match3 !== null) {
-            return `${match3[1]} (${c}, résultat complémentaire)`;
-        }
-
-        return `${s} (${c})`; // forme simple (paramètre)
     }
 
     /**
-     * lie le paramètre géré à un des paramètres liables de la liste
+     * lie le paramètre géré à un des paramètres liables de la liste; appelé
+     * systématiquement lorsqu'on construit le formulaire, même si le
+     * paramètre est déjà lié
      * @param index indice dans la liste
      */
     private linkTo(index: number) {
         if (this._currentIndex !== index) {
             this._currentIndex = index;
             const lp = this._linkableParams[index];
-            this.param.linkToParameter(lp.nub, lp.name);
+            this.param.linkToValue(lp);
+            // reset form results when a new target is selected
+            this.emitModelChanged();
+        }
+    }
+
+    /**
+     * événement de changement de la valeur du modèle
+     */
+    private emitModelChanged() {
+        let value;
+        try {
+            value = this.currentLinkedParam.getValue();
+        } catch (e) {
+            // console.log("undefined target value (pending calculation)");
         }
+        this.change.emit({
+            "action": "model",
+            "value": value
+        });
     }
 
     public updateParamList() {
         // liste des paramètres liables
         if (this.param.valueMode === ParamValueMode.LINK) {
-            this._linkableParams = this._formService.filterLinkableValues(this._formService.getLinkableValues(this.param));
+            this._linkableParams = this._formService.getLinkableValues(this.param);
         } else {
             this._linkableParams = [];
         }
@@ -187,8 +199,8 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
                 if (this.param.valueMode === ParamValueMode.LINK && this._linkableParams !== undefined) {
                     let i = 0;
                     for (const e of this._linkableParams) {
-                        if (this.param.paramDefinition.paramValues.referencedNub
-                            && e.nub.uid === this.param.paramDefinition.paramValues.referencedNub["_uid"]) {
+                        const refValue = this.param.paramDefinition.referencedValue;
+                        if (refValue && e.equals(refValue)) {
                             this._currentIndex = i;
                             break;
                         } else {
@@ -198,10 +210,10 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
                 }
                 this.linkTo(Math.max(this._currentIndex, 0)); // might still be -1
             }
-            this._message = undefined;
         } else {
             this._currentIndex = -1;
-            this._message = "Aucun paramètre compatible trouvé";
+            // back to SINGLE mode by default
+            this.param.valueMode = ParamValueMode.SINGLE;
         }
     }
 
@@ -234,12 +246,12 @@ export class ParamLinkComponent implements OnChanges, Observer, OnDestroy {
                         if (
                             lp.nub.uid === f.currentNub.uid
                             || ( // repeatable structure inside parent module
-                                lp.nub.parent instanceof ParallelStructure
+                                lp.nub instanceof Structure
                                 && lp.nub.parent.uid === f.currentNub.uid
                             )
                         ) {
                             // update <select> option
-                            lp.formTitle = data.value;
+                            lp.meta["formTitle"] = data.value;
                         }
                     }
                     break;
diff --git a/src/app/components/param-values/param-values.component.ts b/src/app/components/param-values/param-values.component.ts
index d9020c604a97d39c8819f8ade23705c5dd447afe..95c9e3c71a6a8a95a7399f84c05c1bd35e04644c 100644
--- a/src/app/components/param-values/param-values.component.ts
+++ b/src/app/components/param-values/param-values.component.ts
@@ -1,10 +1,8 @@
-import { Component, Input, AfterViewInit } from "@angular/core";
+import { Component, Input, AfterViewInit, Output, EventEmitter } from "@angular/core";
 import { NgParameter } from "../../formulaire/ngparam";
 import { DialogEditParamValuesComponent } from "../dialog-edit-param-values/dialog-edit-param-values.component";
 import { MatDialog } from "@angular/material";
-import { ParamValueMode } from "jalhyd";
-import { I18nService } from "../../services/internationalisation/internationalisation.service";
-import { ApplicationSetupService } from "../../services/app-setup/app-setup.service";
+import { ParamValueMode, Observer } from "jalhyd";
 
 @Component({
     selector: "param-values",
@@ -13,7 +11,7 @@ import { ApplicationSetupService } from "../../services/app-setup/app-setup.serv
         "./param-values.component.scss"
     ]
 })
-export class ParamValuesComponent implements AfterViewInit {
+export class ParamValuesComponent implements AfterViewInit, Observer {
 
     @Input()
     public param: NgParameter;
@@ -21,11 +19,14 @@ export class ParamValuesComponent implements AfterViewInit {
     @Input()
     public title: string;
 
+    /**
+     * événement signalant un changement de valeur du modèle
+     */
+    @Output()
+    protected change = new EventEmitter<any>();
 
     constructor(
-        private editValuesDialog: MatDialog,
-        private intlService: I18nService,
-        private appSetupService: ApplicationSetupService
+        private editValuesDialog: MatDialog
     ) { }
 
     public get isMinMax() {
@@ -62,5 +63,26 @@ export class ParamValuesComponent implements AfterViewInit {
                 this.openDialog();
             });
         }
+        // subscribe to parameter values change (through dialog actions)
+        this.param.addObserver(this);
     }
+
+    /**
+     * événement de changement de la valeur du modèle
+     */
+    private emitModelChanged() {
+        this.change.emit({ "action": "model", "value": this.param.getValue() });
+    }
+
+    public update(sender: any, data: any): void {
+        if (sender instanceof DialogEditParamValuesComponent) {
+            switch (data.action) {
+                case "ngparamAfterValue":
+                    // tell the form to clear the results
+                    this.emitModelChanged();
+                    break;
+            }
+        }
+    }
+
 }
diff --git a/src/app/components/remous-results/remous-results.component.ts b/src/app/components/remous-results/remous-results.component.ts
index d62a814e06bf032b3120d17d9a4058c5cb91096c..a59ea0c96e29d13db57292e22e0e3674d2c2f131 100644
--- a/src/app/components/remous-results/remous-results.component.ts
+++ b/src/app/components/remous-results/remous-results.component.ts
@@ -350,7 +350,7 @@ export class RemousResultsComponent implements DoCheck {
     }
 
     private get abscisseIterator(): INumberIterator {
-        return this._remousResults.varResults.variatedParameter.paramDefinition.paramValues.getValuesIterator();
+        return this._remousResults.varResults.variatedParameter.paramDefinition.valuesIterator;
     }
 
     private connectRessaut(lineFlu: LineData, lineTor: LineData) {
diff --git a/src/app/components/section-results/section-results.component.ts b/src/app/components/section-results/section-results.component.ts
index 34248eae2ec6dd6791f074dfb6a5397add5867df..40eb93801e81de9b4babf83ebb273ae8d69450bd 100644
--- a/src/app/components/section-results/section-results.component.ts
+++ b/src/app/components/section-results/section-results.component.ts
@@ -6,7 +6,6 @@ import { SectionCanvasComponent } from "../section-canvas/section-canvas.compone
 import { SectionResults } from "../../results/section-results";
 import { ApplicationSetupService } from "../../services/app-setup/app-setup.service";
 import { CalculatorResults } from "../../results/calculator-results";
-import { FixedResults } from "../../results/fixed-results";
 
 @Component({
     selector: "section-results",
diff --git a/src/app/formulaire/check-field.ts b/src/app/formulaire/check-field.ts
deleted file mode 100644
index 073816613239ed3eea16a1b7d68aa8ba4b7b89d3..0000000000000000000000000000000000000000
--- a/src/app/formulaire/check-field.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Field } from "./field";
-import { FormulaireNode } from "./formulaire-node";
-
-export class CheckField extends Field {
-    private _value: boolean;
-
-    constructor(parent: FormulaireNode) {
-        super(parent);
-        this._value = false;
-    }
-
-    public getValue(): boolean {
-        return this._value;
-    }
-
-    public setValue(val: boolean) {
-        this._value = val;
-    }
-
-    public get isValid(): boolean {
-        return true;
-    }
-
-    public parseConfig(json: {}, data?: {}) {
-        this._confId = json["id"];
-        this.setValue(json["value"] === "true");
-    }
-}
diff --git a/src/app/formulaire/definition/concrete/form-base.ts b/src/app/formulaire/definition/concrete/form-base.ts
index 6b28a0a2f5c228bfe4b62389295b7470d6ab58d3..f1afa17ea1f9be2b22cbc48e1d3ec06b886f4650 100644
--- a/src/app/formulaire/definition/concrete/form-base.ts
+++ b/src/app/formulaire/definition/concrete/form-base.ts
@@ -1,24 +1,25 @@
-import { FormDefFixedVar } from "../form-def-fixedvar";
-import { FormResultFixedVar } from "../form-result-fixedvar";
 import { FormulaireDefinition } from "../form-definition";
 import { CalculatorResults } from "../../../results/calculator-results";
 import { FormDefParamToCalculate } from "../form-def-paramcalc";
+import { FormResult } from "../form-result";
+import { FormCompute } from "../form-compute";
+import { FormResultFixedVar } from "../form-result-fixedvar";
 import { FormComputeFixedVar } from "../form-compute-fixedvar";
+import { ServiceFactory } from "../../../services/service-factory";
 
 export class FormulaireBase extends FormulaireDefinition {
 
-    private _formParamCalc: FormDefParamToCalculate;
+    protected _formParamCalc: FormDefParamToCalculate;
 
-    private _formCompute: FormComputeFixedVar;
+    protected _formCompute: FormCompute;
 
-    private _formResult: FormResultFixedVar;
+    protected _formResult: FormResult;
 
     constructor() {
         super();
-        // this._formFixedVar = new FormDefFixedVar(this);
         this._formParamCalc = new FormDefParamToCalculate(this);
         this._formResult = new FormResultFixedVar(this, false);
-        this._formCompute = new FormComputeFixedVar(this, this._formResult);
+        this._formCompute = new FormComputeFixedVar(this, (this._formResult as FormResultFixedVar));
     }
 
     protected completeParse(json: {}) {
@@ -26,15 +27,17 @@ export class FormulaireBase extends FormulaireDefinition {
     }
 
     /**
-     * gestion du clic sur les radios "paramètre fixé, à varier, à calculer"
+     * Resets the form results, the results panel on screen, the model
+     * results, and does the same for all depending modules
      */
-    public onRadioClick(info: string) {
-        super.onRadioClick(info);
-        this._formParamCalc.onRadioClick(info);
-    }
-
-    public resetResults() {
+    public resetResults(visited: string[] = []) {
+        visited.push(this.currentNub.uid);
+        // reset GUI results
         this._formResult.resetResults();
+        // reset model results
+        this.currentNub.resetResult();
+        // reset the result panels of all forms depending on this one
+        ServiceFactory.instance.formulaireService.resetAllDependingFormsResults(this, visited);
     }
 
     public doCompute() {
diff --git a/src/app/formulaire/definition/concrete/form-courbe-remous.ts b/src/app/formulaire/definition/concrete/form-courbe-remous.ts
index e65fc2e3b4f99018c1458281f0b282f336fafb0c..99be68efbe0f6056950e05f519570a271993fbaf 100644
--- a/src/app/formulaire/definition/concrete/form-courbe-remous.ts
+++ b/src/app/formulaire/definition/concrete/form-courbe-remous.ts
@@ -3,16 +3,12 @@ import { IObservable, MethodeResolution } from "jalhyd";
 import { FormResultRemous } from "../form-result-remous";
 import { FormDefSection } from "../form-def-section";
 import { FormComputeCourbeRemous } from "../form-compute-courbe-remous";
-import { FormulaireDefinition } from "../form-definition";
-import { CalculatorResults } from "../../../results/calculator-results";
 import { FieldSet } from "../../fieldset";
+import { FormulaireBase } from "./form-base";
 
-export class FormulaireCourbeRemous extends FormulaireDefinition {
-    private _formSection: FormDefSection;
-
-    private _formCompute: FormComputeCourbeRemous;
+export class FormulaireCourbeRemous extends FormulaireBase {
 
-    private _formResult: FormResultRemous;
+    private _formSection: FormDefSection;
 
     /**
      * id du select configurant la méthode de résolution
@@ -23,7 +19,7 @@ export class FormulaireCourbeRemous extends FormulaireDefinition {
         super();
         this._formSection = new FormDefSection(this);
         this._formResult = new FormResultRemous(this);
-        this._formCompute = new FormComputeCourbeRemous(this, this._formSection, this._formResult);
+        this._formCompute = new FormComputeCourbeRemous(this, this._formSection, (this._formResult as FormResultRemous));
         // default properties
         this._props["methodeResolution"] = MethodeResolution.Trapezes;
         this._props["varCalc"] = undefined; // important
@@ -50,27 +46,12 @@ export class FormulaireCourbeRemous extends FormulaireDefinition {
         }
     }
 
-    public resetResults() {
-        this._formResult.resetResults();
-        // prévenir les composants qu'il faut détecter les changements
-        this.notifyReset();
-    }
-
-    public doCompute() {
-        this._formCompute.doCompute();
-    }
-
-    public get hasResults(): boolean {
-        return this._formResult.hasResults;
-    }
-
-    public get results(): CalculatorResults[] {
-        return this._formResult.results;
-    }
-
     // interface Observer
 
     update(sender: IObservable, data: any) {
+
+        super.update(sender, data);
+
         if (sender instanceof FieldSet && data.action === "propertyChange") {
             switch (sender.id) {
                 case "fs_section":
diff --git a/src/app/formulaire/definition/concrete/form-lechapt-calmon.ts b/src/app/formulaire/definition/concrete/form-lechapt-calmon.ts
index 69124f6461849b5fb874de3a0806496a761876e4..db299120666ff8b777715e0a25fe388e005c912a 100644
--- a/src/app/formulaire/definition/concrete/form-lechapt-calmon.ts
+++ b/src/app/formulaire/definition/concrete/form-lechapt-calmon.ts
@@ -2,9 +2,17 @@ import { Observer } from "jalhyd";
 import { SelectField } from "../../select-field";
 import { FormulaireBase } from "./form-base";
 import { NgParamInputComponent } from "../../../components/ngparam-input/ngparam-input.component";
+import { FormResultFixedVar } from "../form-result-fixedvar";
+import { FormComputeFixedVar } from "../form-compute-fixedvar";
 
 export class FormulaireLechaptCalmon extends FormulaireBase implements Observer {
 
+    constructor() {
+        super();
+        this._formResult = new FormResultFixedVar(this, false);
+        this._formCompute = new FormComputeFixedVar(this, (this._formResult as FormResultFixedVar));
+    }
+
     protected completeParse(json: {}) {
         super.completeParse(json);
         // abonnement au changement de valeur du select de matériau
@@ -18,6 +26,9 @@ export class FormulaireLechaptCalmon extends FormulaireBase implements Observer
     // interface Observer
 
     public update(sender: any, data: any) {
+
+        super.update(sender, data);
+
         // en cas de changement de valeur du select de matériau, effacement des résultats et MAJ des champs L,M,N
         if (sender instanceof SelectField) {
             if (data.action === "select") {
diff --git a/src/app/formulaire/definition/concrete/form-parallel-structures.ts b/src/app/formulaire/definition/concrete/form-parallel-structures.ts
index 2f3f6bd410c07cd00463391ab33f025f9a8a3a75..ec840a97075a5039d7851a883607fb9cb9346d2c 100644
--- a/src/app/formulaire/definition/concrete/form-parallel-structures.ts
+++ b/src/app/formulaire/definition/concrete/form-parallel-structures.ts
@@ -1,8 +1,5 @@
 import { Structure, Nub, ParallelStructure, StructureType, LoiDebit, StructureProperties, Props, Session } from "jalhyd";
 
-import { FormulaireDefinition } from "../form-definition";
-import { CalculatorResults } from "../../../results/calculator-results";
-import { FormDefParamToCalculate } from "../form-def-paramcalc";
 import { FormDefParallelStructures } from "../form-def-parallel-structures";
 import { FormComputeParallelStructures } from "../form-compute-parallel-structures";
 import { FormResultFixedVar } from "../form-result-fixedvar";
@@ -12,19 +9,12 @@ import { SelectField } from "../../select-field";
 import { NgParameter } from "../../ngparam";
 import { FieldsetTemplate } from "../../fieldset-template";
 import { FormulaireNode } from "../../formulaire-node";
+import { FormulaireBase } from "./form-base";
 
-export class FormulaireParallelStructure extends FormulaireDefinition {
-
-    // private _formFixedVar: FormDefFixedVar;
+export class FormulaireParallelStructure extends FormulaireBase {
 
     private _formParallelStruct: FormDefParallelStructures;
 
-    private _formParamCalc: FormDefParamToCalculate;
-
-    private _formCompute: FormComputeParallelStructures;
-
-    private _formResult: FormResultFixedVar;
-
     /**
      * id du select configurant le type d'ouvrage
      */
@@ -32,11 +22,9 @@ export class FormulaireParallelStructure extends FormulaireDefinition {
 
     constructor() {
         super();
-        // this._formFixedVar = new FormDefFixedVar(this);
-        this._formParamCalc = new FormDefParamToCalculate(this);
         this._formResult = new FormResultFixedVar(this, false);
         this._formParallelStruct = new FormDefParallelStructures();
-        this._formCompute = new FormComputeParallelStructures(this, this._formParallelStruct, this._formResult);
+        this._formCompute = new FormComputeParallelStructures(this, this._formParallelStruct, (this._formResult as FormResultFixedVar));
     }
 
     private createStructNub(templ: FieldsetTemplate): Nub {
@@ -168,14 +156,12 @@ export class FormulaireParallelStructure extends FormulaireDefinition {
     }
 
     public removeFieldset(fs: FieldSet) {
-        // console.log("==> FormParallelStructures removeFieldset", fs.uid);
         if (fs.nub instanceof Structure) {
             // suppression du sous-nub dans le Nub parent
             this.deleteNub(fs.nub);
 
             // suppression du fieldset
             this.fieldsetContainer.removeFieldset(fs);
-            // console.log("====> nb struct dans le parent après", (this.currentNub as ParallelStructure).structures.length);
 
             this.resetResults();
         } else { super.removeFieldset(fs); }
@@ -186,35 +172,6 @@ export class FormulaireParallelStructure extends FormulaireDefinition {
         this.subscribeFieldsetContainer();
     }
 
-    /**
-     * gestion du clic sur les radios "paramètre fixé, à varier, à calculer"
-     */
-    public onRadioClick(info: string) {
-        super.onRadioClick(info);
-        this._formParamCalc.onRadioClick(info);
-    }
-
-    /**
-     * @return une chaîne représentant le "contexte" courant (ici, combinaison type d'ouvrage-loi de débit)
-     * @param fs FieldSet contenant les listes déroulantes type d'ouvrage et loi de débit
-     */
-
-    public resetResults() {
-        this._formResult.resetResults();
-    }
-
-    public doCompute() {
-        this._formCompute.doCompute();
-    }
-
-    public get hasResults(): boolean {
-        return this._formResult.hasResults;
-    }
-
-    public get results(): CalculatorResults[] {
-        return this._formResult.results;
-    }
-
     private get fieldsetContainer(): FieldsetContainer {
         const n = this.getFormulaireNodeById("struct_container");
         if (n === undefined || !(n instanceof FieldsetContainer)) {
@@ -381,6 +338,9 @@ export class FormulaireParallelStructure extends FormulaireDefinition {
     // interface Observer
 
     public update(sender: any, data: any) {
+
+        super.update(sender, data);
+
         if (sender instanceof FieldsetContainer) {
             switch (data.action) {
                 case "newFieldset":
diff --git a/src/app/formulaire/definition/concrete/form-regime-uniforme.ts b/src/app/formulaire/definition/concrete/form-regime-uniforme.ts
index 7e607af99ce8f91d2834f7575732ddef14479ede..3d846e31d8bfaebe117edd59cfcd77d1f0076d97 100644
--- a/src/app/formulaire/definition/concrete/form-regime-uniforme.ts
+++ b/src/app/formulaire/definition/concrete/form-regime-uniforme.ts
@@ -1,31 +1,19 @@
-import { FormDefFixedVar } from "../form-def-fixedvar";
 import { IObservable, Observer } from "jalhyd";
 import { FormResultFixedVar } from "../form-result-fixedvar";
-import { FormulaireDefinition } from "../form-definition";
 import { FormDefSection } from "../form-def-section";
-import { CalculatorResults } from "../../../results/calculator-results";
-import { FormDefParamToCalculate } from "../form-def-paramcalc";
 import { FieldSet } from "../../fieldset";
 import { FormComputeFixedVar } from "../form-compute-fixedvar";
+import { FormulaireBase } from "./form-base";
 
-export class FormulaireRegimeUniforme extends FormulaireDefinition implements Observer {
-    private _formFixedVar: FormDefFixedVar;
-
-    private _formParamCalc: FormDefParamToCalculate;
+export class FormulaireRegimeUniforme extends FormulaireBase implements Observer {
 
     private _formSection: FormDefSection;
 
-    private _formCompute: FormComputeFixedVar;
-
-    private _formResult: FormResultFixedVar;
-
     constructor() {
         super();
-        this._formFixedVar = new FormDefFixedVar(this);
-        this._formParamCalc = new FormDefParamToCalculate(this);
         this._formSection = new FormDefSection(this);
         this._formResult = new FormResultFixedVar(this, true);
-        this._formCompute = new FormComputeFixedVar(this, this._formResult);
+        this._formCompute = new FormComputeFixedVar(this, (this._formResult as FormResultFixedVar));
     }
 
     protected parseOptions(json: {}) {
@@ -42,43 +30,17 @@ export class FormulaireRegimeUniforme extends FormulaireDefinition implements Ob
         super.completeParse(json);
     }
 
-    /**
-     * gestion du clic sur les radios "paramètre fixé, à varier, à calculer"
-     */
-    public onRadioClick(info: string) {
-        super.onRadioClick(info);
-        this._formParamCalc.onRadioClick(info);
-    }
-
-    public resetResults() {
-        this._formResult.resetResults();
-    }
-
-    public doCompute() {
-        this._formCompute.doCompute();
-    }
-
-    public get hasResults(): boolean {
-        return this._formResult.hasResults;
-    }
-
-    public get results(): CalculatorResults[] {
-        return this._formResult.results;
-    }
-
-    public reset() {
-        super.reset();
-    }
-
     // interface Observer
 
     update(sender: IObservable, data: any) {
+
+        super.update(sender, data);
+
         // changement de propriété du FieldSet contenant le select de choix du type de section
         if (sender instanceof FieldSet && sender.id === "fs_section" && data.action === "propertyChange") {
             this.replaceCurrentNub(sender.properties);
             for (const fs of this.allFieldsets) {
                 fs.setNub(this._currentNub);
-                this._formParamCalc.setDefault();
                 // treat the fieldset as new to re-subscribe to Nub properties change events
                 this.afterParseFieldset(fs);
             }
diff --git a/src/app/formulaire/definition/concrete/form-section-parametree.ts b/src/app/formulaire/definition/concrete/form-section-parametree.ts
index 04508ccaffacc47426b4077ef424b9bed31a01c9..1a3ef62b123e5dbdd21200d20c06966c34da069f 100644
--- a/src/app/formulaire/definition/concrete/form-section-parametree.ts
+++ b/src/app/formulaire/definition/concrete/form-section-parametree.ts
@@ -3,26 +3,18 @@ import { IObservable } from "jalhyd";
 import { FormResultSection } from "../form-result-section";
 import { FormDefSection } from "../form-def-section";
 import { FormComputeSectionParametree } from "../form-compute-section-parametree";
-import { FormulaireDefinition } from "../form-definition";
-import { CalculatorResults } from "../../../results/calculator-results";
-import { FormDefFixedVar } from "../form-def-fixedvar";
 import { FieldSet } from "../../fieldset";
+import { FormulaireBase } from "./form-base";
 
-export class FormulaireSectionParametree extends FormulaireDefinition {
-    private _formFixedVar: FormDefFixedVar;
+export class FormulaireSectionParametree extends FormulaireBase {
 
     private _formSection: FormDefSection;
 
-    private _formCompute: FormComputeSectionParametree;
-
-    private _formSectionResult: FormResultSection;
-
     constructor() {
         super();
-        this._formFixedVar = new FormDefFixedVar(this);
         this._formSection = new FormDefSection(this);
-        this._formSectionResult = new FormResultSection(this, this._formSection);
-        this._formCompute = new FormComputeSectionParametree(this, this._formSection, this._formSectionResult);
+        this._formResult = new FormResultSection(this, this._formSection);
+        this._formCompute = new FormComputeSectionParametree(this, this._formSection, (this._formResult as FormResultSection));
         // default properties
         this._props["varCalc"] = "Hs";
     }
@@ -36,33 +28,12 @@ export class FormulaireSectionParametree extends FormulaireDefinition {
         this._formSection.afterParseFieldset(fs);
     }
 
-    /**
-     * gestion du clic sur les radios "paramètre fixé, à varier, à calculer"
-     */
-    public onRadioClick(info: string) {
-        super.onRadioClick(info);
-        this._formFixedVar.onRadioClick(info);
-    }
-
-    public resetResults() {
-        this._formSectionResult.resetResults();
-    }
-
-    public doCompute() {
-        this._formCompute.doCompute();
-    }
-
-    public get hasResults(): boolean {
-        return this._formSectionResult.hasResults;
-    }
-
-    public get results(): CalculatorResults[] {
-        return this._formSectionResult.results;
-    }
-
     // interface Observer
 
     update(sender: IObservable, data: any) {
+
+        super.update(sender, data);
+
         // changement de propriété du FieldSet contenant le select de choix du type de section
         if (sender instanceof FieldSet && data.action === "propertyChange") {
             switch (sender.id) {
diff --git a/src/app/formulaire/definition/form-compute-courbe-remous.ts b/src/app/formulaire/definition/form-compute-courbe-remous.ts
index dc3a6a2e26789f4c9f2431fbb663e1150b4fbd81..b69986e83c954b0795184a17145b424b6c3e7958 100644
--- a/src/app/formulaire/definition/form-compute-courbe-remous.ts
+++ b/src/app/formulaire/definition/form-compute-courbe-remous.ts
@@ -7,6 +7,11 @@ import { FormCompute } from "./form-compute";
 import { FormResultRemous } from "./form-result-remous";
 
 export class FormComputeCourbeRemous extends FormCompute {
+
+    private resultYn: Result;
+
+    private resultYc: Result;
+
     constructor(formBase: FormulaireDefinition, private _formSection: FormDefSection, formResult: FormResultRemous) {
         super(formBase, formResult);
     }
@@ -21,8 +26,15 @@ export class FormComputeCourbeRemous extends FormCompute {
         const prmCR: CourbeRemousParams = cr.prms as CourbeRemousParams;
         const sect: acSection = prmCR.Sn;
 
-        const Yn: Result = sect.Calc("Yn"); // hauteur normale
-        const Yc: Result = sect.Calc("Yc"); // hauteur critique
+        this.resultYn = sect.Calc("Yn"); // hauteur normale
+        this.resultYc = sect.Calc("Yc"); // hauteur critique
+
+        this.reaffectResultComponents();
+    }
+
+    protected reaffectResultComponents() {
+        const cr: CourbeRemous = this._formBase.currentNub as CourbeRemous;
+        const prmCR: CourbeRemousParams = cr.prms as CourbeRemousParams;
 
         this.remousResults.parameters = prmCR;
 
@@ -33,8 +45,8 @@ export class FormComputeCourbeRemous extends FormCompute {
         this.remousResults.result = cr.calculRemous(this.remousResults.extraParamSymbol);
 
         // données du graphe
-        this.remousResults.hauteurNormale = Yn.resultElement;
-        this.remousResults.hauteurCritique = Yc.resultElement;
+        this.remousResults.hauteurNormale = this.resultYn.resultElement;
+        this.remousResults.hauteurCritique = this.resultYc.resultElement;
         if (this.remousResults.extraParamSymbol) {
             this.remousResults.extraGraph = ["Hs", "Hsc", "Yf", "Yt", "Yco"].indexOf(this.remousResults.extraParamSymbol) === -1;
         } else {
diff --git a/src/app/formulaire/definition/form-compute-fixedvar.ts b/src/app/formulaire/definition/form-compute-fixedvar.ts
index 151b875bfbae1e4c4178b8c09b228b54e2febc7f..205e9e9dc902ca3dda27fc0950ed57abe193d65f 100644
--- a/src/app/formulaire/definition/form-compute-fixedvar.ts
+++ b/src/app/formulaire/definition/form-compute-fixedvar.ts
@@ -1,4 +1,4 @@
-import { Nub, Result, ComputeNode, ParamValueMode } from "jalhyd";
+import { Nub, Result, ComputeNode } from "jalhyd";
 
 import { FormCompute } from "./form-compute";
 import { NgParameter, ParamRadioConfig } from "../ngparam";
@@ -45,22 +45,29 @@ export class FormComputeFixedVar extends FormCompute {
     protected compute() {
         const nub: Nub = this._formBase.currentNub;
         const computedParam: NgParameter = this.getComputedParameter();
+
+        const res: Result = this.runNubCalc(nub);
+
+        this.reaffectResultComponents();
+    }
+
+    protected reaffectResultComponents() {
+        const nub: Nub = this._formBase.currentNub;
+        const computedParam: NgParameter = this.getComputedParameter();
+        this.formResult.resetResults(); // to avoid adding fixed parameters more than once (see below)
         this.formResult.addFixedParameters();
         const varParam: NgParameter = this.getVariatedParameter();
 
         if (varParam === undefined) {
             // pas de paramètre à varier
-            const res: Result = this.runNubCalc(nub, computedParam);
-            this.formResult.fixedResults.result = res;
+            this.formResult.fixedResults.result = nub.result;
             this.formResult.fixedResults.calculatedParameter = computedParam;
         } else {
             // il y a un paramètre à varier
-            const res: Result = this.runNubCalc(nub, computedParam);
-
             this.formResult.varResults.variatedParameter = varParam;
             this.formResult.varResults.calculatedParameter = computedParam;
 
-            this.formResult.varResults.result = res;
+            this.formResult.varResults.result = nub.result;
             this.formResult.varResults.update(false);
         }
     }
diff --git a/src/app/formulaire/definition/form-compute-parallel-structures.ts b/src/app/formulaire/definition/form-compute-parallel-structures.ts
index 9daa4cbd6cf61b2db18eb0e1173ee28dfc3ed49d..8f169e5e3e04cfb4ef366ed472fde9739a7377c2 100644
--- a/src/app/formulaire/definition/form-compute-parallel-structures.ts
+++ b/src/app/formulaire/definition/form-compute-parallel-structures.ts
@@ -1,4 +1,4 @@
-import { ComputeNode, ParallelStructure } from "jalhyd";
+import { ComputeNode, ParallelStructure, Structure, ParamDefinition } from "jalhyd";
 
 import { FormComputeFixedVar } from "./form-compute-fixedvar";
 import { FormResultFixedVar } from "./form-result-fixedvar";
@@ -38,15 +38,19 @@ export class FormComputeParallelStructures extends FormComputeFixedVar {
     }
 
     /**
-     * construit un identifiant de type "n.X" avec "n" l'index de l'ouvrage auquel appartient le paramètre et "X" son symbole
+     * construit un identifiant de type { uid: "abcdef", symbol: "X" }
+     * avec "abcdef" l'index de l'ouvrage et "X" son paramètre
      */
-    protected getParameterRefid(p: NgParameter) {
-        const [fsc, fs, i] = this.structureParents(p);
-        if (i === -1) {
+    protected getParameterRefid(p: ParamDefinition): any {
+        const nub = p.parentComputeNode;
+        if (nub instanceof Structure) {
+            return {
+                uid: nub.uid,
+                symbol: p.symbol
+            };
+        } else {
             return super.getParameterRefid(p);
         }
-
-        return `${i}.${p.symbol}`;
     }
 
     protected setParameterValue(node: ComputeNode, p: NgParameter, val: number) {
diff --git a/src/app/formulaire/definition/form-compute-section-parametree.ts b/src/app/formulaire/definition/form-compute-section-parametree.ts
index fc41974d9505d7c5da73d60c060969968198c296..4c78233a03ba545ca5521449b61033ddcac7e726 100644
--- a/src/app/formulaire/definition/form-compute-section-parametree.ts
+++ b/src/app/formulaire/definition/form-compute-section-parametree.ts
@@ -12,6 +12,8 @@ import { FormulaireNode } from "../formulaire-node";
 
 export class FormComputeSectionParametree extends FormCompute {
 
+    private tmpResult: Result;
+
     constructor(formBase: FormulaireDefinition, private _formSection: FormDefSection, formResult: FormResult) {
         super(formBase, formResult);
     }
@@ -49,7 +51,7 @@ export class FormComputeSectionParametree extends FormCompute {
         const computedParam: NgParameter = this.createParameter(computedParamInfo.symbol, this._formBase);
         this._varResults.calculatedParameter = computedParam;
 
-        this._varResults.result = this.runNubCalc(sectNub, computedParam);
+        this._varResults.result = this.runNubCalc(sectNub);
         this._varResults.update(false);
     }
 
@@ -62,20 +64,25 @@ export class FormComputeSectionParametree extends FormCompute {
 
         const sectNub: SectionParametree = this._formBase.currentNub as SectionParametree;
 
-        const sect: acSection = sectNub.section;
-        this._sectionResults.section = sect;
-
-        const tmpResult: Result = sectNub.CalcSerie(
+        this.tmpResult = sectNub.CalcSerie(
             undefined, // valeur initiale, non utilisée dans ce cas
             undefined // variable à calculer, non utilisée
         );
 
+        this.reaffectResultComponents();
+    }
+
+    protected reaffectResultComponents() {
+        const sectNub: SectionParametree = this._formBase.currentNub as SectionParametree;
+        const sect: acSection = sectNub.section;
+        this._sectionResults.section = sect;
+
         // résultats de section (avec le graphique de section)
-        this._sectionResults.result = tmpResult;
+        this._sectionResults.result = this.tmpResult;
 
         // résultats complémentaires des paramètres fixés
         this._formSectionResult.addSectionFixedParameters(false);
-        this._formSectionResult.fixedResults.result = tmpResult;
+        this._formSectionResult.fixedResults.result = this.tmpResult;
     }
 
     /**
diff --git a/src/app/formulaire/definition/form-compute.ts b/src/app/formulaire/definition/form-compute.ts
index 4e8494753862af7cb478da8204cf880959a5a5b5..5f077f0c282a6119bf66f6c0f15ce7551a57611b 100644
--- a/src/app/formulaire/definition/form-compute.ts
+++ b/src/app/formulaire/definition/form-compute.ts
@@ -1,11 +1,14 @@
-import { Nub, Result, ParamDomainValue } from "jalhyd";
+import { Nub, Result, ParamDomainValue, Observer, ParamDefinition } from "jalhyd";
 
 import { FormResult } from "./form-result";
 import { FormulaireDefinition } from "./form-definition";
 import { NgParameter } from "../ngparam";
 
-export abstract class FormCompute {
+export abstract class FormCompute implements Observer {
+
     constructor(protected _formBase: FormulaireDefinition, protected _formResult: FormResult) {
+        // indirectly subscribe to Nub result updates
+        this._formBase.addObserver(this);
     }
 
     protected abstract compute();
@@ -18,19 +21,37 @@ export abstract class FormCompute {
      * retourne un identifiant du paramètre dans le formulaire
      * surchargé dans le cas des ouvrages //
      */
-    protected getParameterRefid(p: NgParameter) {
+    protected getParameterRefid(p: ParamDefinition) {
         return p.symbol;
     }
 
     /**
-     * lance le calcul d'un paramètre en déterminant une valeur initiale
+     * Copies current Nub result into result components for display on page.
+     * Should be called every time the Nub result changes.
+     * Must be idempotent.
+     */
+    protected abstract reaffectResultComponents();
+
+    /**
+     * Lance le calcul d'un paramètre en déterminant une valeur initiale.
+     * Si nécessaire déclenche un calcul en chaîne des modules en amont.
      */
-    protected runNubCalc(nub: Nub, computedParam: NgParameter): Result {
+    protected runNubCalc(nub: Nub, computedParam?: ParamDefinition): Result {
         let init: number;
+
+        // by default, use Nub's calculatedParam
+        if (computedParam === undefined) {
+            computedParam = nub.calculatedParam;
+        }
+
+        // require chain computation; redundant with Nub.CalcSerie but required
+        // to get initial value here...
+        const computedParamValue = computedParam.getValue();
+
         switch (computedParam.domain.domain) {
             case ParamDomainValue.ANY:
                 if (computedParam && computedParam.isDefined) {
-                    init = computedParam.getValue();
+                    init = computedParamValue;
                 }
                 if (init === undefined) {
                     init = 0;
@@ -39,7 +60,7 @@ export abstract class FormCompute {
 
             case ParamDomainValue.POS_NULL:
                 if (computedParam && computedParam.isDefined) {
-                    init = Math.max(computedParam.getValue(), 0);
+                    init = Math.max(computedParamValue, 0);
                 }
                 if (init === undefined) {
                     init = 0;
@@ -52,7 +73,7 @@ export abstract class FormCompute {
 
             case ParamDomainValue.NOT_NULL:
                 if (computedParam && computedParam.isDefined) {
-                    init = computedParam.getValue();
+                    init = computedParamValue;
                 }
                 if (init === undefined || init === 0) {
                     init = 1e-8;
@@ -61,7 +82,7 @@ export abstract class FormCompute {
 
             case ParamDomainValue.POS:
                 if (computedParam && computedParam.isDefined) {
-                    init = Math.max(computedParam.getValue(), 1e-8);
+                    init = Math.max(computedParamValue, 1e-8);
                 }
                 if (init === undefined) {
                     init = 1e-8;
@@ -72,6 +93,9 @@ export abstract class FormCompute {
         return nub.CalcSerie(init, this.getParameterRefid(computedParam));
     }
 
+    /**
+     * Triggers computation of the Nub, updates form results
+     */
     public doCompute() {
         this._formResult.resetResults();
 
@@ -81,4 +105,17 @@ export abstract class FormCompute {
             "action": "resultsUpdated",
         }, this._formBase);
     }
+
+    // interface Observer
+
+    public update(sender: any, data: any): void {
+        if (sender instanceof Nub) {
+            switch (data.action) {
+                case "nubResultUpdated":
+                    // forward Nub results update notification to FormCompute objects
+                    this.reaffectResultComponents();
+                    break;
+            }
+        }
+    }
 }
diff --git a/src/app/formulaire/definition/form-def-fixedvar.ts b/src/app/formulaire/definition/form-def-fixedvar.ts
index d34f67110ac5f75bf50c5fc2e78637133d63ba95..efad7a577bc72be79b8b854a4897ff558149987b 100644
--- a/src/app/formulaire/definition/form-def-fixedvar.ts
+++ b/src/app/formulaire/definition/form-def-fixedvar.ts
@@ -1,180 +1,14 @@
-import { ParamValueMode } from "jalhyd";
-
-import { ParamRadioConfig, NgParameter } from "../ngparam";
 import { FormulaireDefinition } from "./form-definition";
 
 /**
  * gestion des formulaires avec "paramètre fixé" et "paramètre à varier"
  */
 export class FormDefFixedVar {
+
     protected _formBase: FormulaireDefinition;
 
     constructor(base: FormulaireDefinition) {
         this._formBase = base;
     }
 
-    /**
-     * remet les radios de tous les paramètres à FIX sauf "me" et ceux (celui) à l'état "except"
-     */
-    protected resetOtherRadio(me: NgParameter, except?: ParamRadioConfig) {
-        for (const p of this._formBase.allFormElements) {
-            if (p instanceof NgParameter) {
-                if (p !== me && p.radioState !== except
-                    && p.radioState !== ParamRadioConfig.LINK
-                    && p.radioConfig !== ParamRadioConfig.FIX) {
-
-                    p.valueMode = ParamValueMode.SINGLE;
-                }
-            }
-        }
-    }
-
-    /**
-     * gère un changement de mode pour un paramètre
-     * règles :
-     *   - 1 seul paramètre CAL à la fois
-     *   - 1 seul paramètre multivalué (VAR) à la fois (directement ou par liaison)
-     *   - plusieurs paramètres FIX à la fois possible
-     *   - plusieurs paramètres LINK à la fois possible si les 2 1ères règles sont respectées
-     *
-     * analyse :
-     * ancien état    nouvel état    action(s)
-     * FIX            VAR            action1
-     * FIX            CAL            action2
-     * FIX            LINK           si paramètre lié FIX : aucune
-     *                               si paramètre lié VAR : action1
-     *                               si paramètre lié CAL : si valeur unique : aucune
-     *                                                      si valeur multiple : action1
-     *                               si paramètre lié LINK : recommencer ce cas avec le paramètre lié
-     *
-     * VAR            FIX            aucune
-     * VAR            CAL            action2
-     * VAR            LINK           si paramètre lié FIX : aucune
-     *                               si paramètre lié VAR : aucune
-     *                               si paramètre lié CAL : si valeur unique : aucune
-     *                                                      si valeur multiple : aucune
-     *                               si paramètre lié LINK : recommencer ce cas avec le paramètre lié
-     *
-     * CAL            FIX            action5
-     * CAL            VAR            action3 + action5
-     * CAL            LINK           si paramètre lié FIX : aucune
-     *                               si paramètre lié VAR : action3 + action4|action5
-     *                               si paramètre lié CAL : action3 + action4|action5
-     *                               si paramètre lié LINK : recommencer ce cas avec le paramètre lié
-     *
-     * action1 : reset (à FIX) de tous les autres paramètres que celui modifié sauf celui à CAL
-     * action2 : reset (à FIX) de tous les autres paramètres que celui modifié sauf celui/ceux à VAR
-     * action3 : reset (à FIX) de tous les autres paramètres que celui modifié
-     * action4 : mettre le paramètre désigné par la conf comme "par défault" à CAL
-     * action5 : mettre le 1er paramètre du module de calcul à CAL
-     */
-    protected processRadioStateChange(sourceParam: NgParameter, oldState: ParamValueMode) {
-        switch (oldState) {
-            case ParamValueMode.SINGLE:  // ancien état
-                switch (sourceParam.valueMode) {
-                    case ParamValueMode.MINMAX:  // nouvel état
-                    case ParamValueMode.LISTE:
-                        this.resetOtherRadio(sourceParam, ParamRadioConfig.CAL);
-                        break;
-
-                    case ParamValueMode.CALCUL:  // nouvel état
-                        this.resetOtherRadio(sourceParam, ParamRadioConfig.VAR);
-                        break;
-
-                    case ParamValueMode.LINK:  // nouvel état
-                        if (sourceParam.paramDefinition.hasMultipleValues) {
-                            this.resetOtherRadio(sourceParam, ParamRadioConfig.CAL);
-                        } else {
-                            const refParamValues = sourceParam.paramDefinition.referencedParamValues;
-                            if (refParamValues !== undefined) { // cad si on référence un paramètre et non un Result par ex
-                                if (refParamValues.valueMode === ParamValueMode.LINK) {
-                                    throw new Error(`références de paramètre en chaîne non pris en charge`);
-                                }
-                            } // cas à traiter
-                        }
-                        break;
-                }
-                break;
-
-            case ParamValueMode.LISTE:  // ancien état
-            case ParamValueMode.MINMAX:
-                switch (sourceParam.valueMode) {
-                    case ParamValueMode.CALCUL:  // nouvel état
-                        this.resetOtherRadio(sourceParam, ParamRadioConfig.VAR);
-                        break;
-
-                    case ParamValueMode.LINK:  // nouvel état
-                        // mode du paramètre référencé
-                        const refParamValues = sourceParam.paramDefinition.referencedParamValues;
-                        if (refParamValues.valueMode === ParamValueMode.LINK) {
-                            throw new Error(`références de paramètre en chaîne non pris en charge`);
-                        }
-                        break;
-                }
-                break;
-
-            case ParamValueMode.LINK:  // ancien état
-                switch (sourceParam.valueMode) {
-                    case ParamValueMode.MINMAX:  // nouvel état
-                    case ParamValueMode.LISTE:
-                        this.resetOtherRadio(sourceParam, ParamRadioConfig.CAL);
-                        break;
-
-                    case ParamValueMode.CALCUL:  // nouvel état
-                        this.resetOtherRadio(sourceParam, ParamRadioConfig.VAR);
-                        break;
-                }
-                break;
-        }
-    }
-
-    /**
-     * modifie les boutons radio "fix", "var", "cal" de tous les paramètres
-     * en fonction de la modification de l'état d'un des paramètres
-     * @param uid id numérique unique du paramètre source
-     * @param option nouvel état "fix", "var" ou "cal" du paramètre source
-     */
-    protected resetRadiosAndResults(sourceParam: NgParameter, oldState: ParamValueMode) {
-        this.processRadioStateChange(sourceParam, oldState);
-
-        // on vérifie qu'il y a au moins un paramètre "à calculer" et sinon, on prend le 1er qui est à "fixé"
-        if (this._formBase.getDisplayedParamFromState(ParamRadioConfig.CAL) === undefined) {
-            let newCal: NgParameter;
-
-            for (const p of this._formBase.allFormElements) {
-                if (p instanceof NgParameter) {
-                    // change all radio button groups except the one that sent the event
-                    if (p.radioConfig === ParamRadioConfig.CAL && p.radioState === ParamRadioConfig.FIX && p !== sourceParam) {
-                        newCal = p;
-                        break;
-                    }
-                }
-                if (newCal) {
-                    break;
-                }
-            }
-            // if the current calculated parameter was set to another mode, set a new param
-            // to calculated mode (there must always be at least one)
-            if (newCal) {
-                newCal.valueMode = ParamValueMode.CALCUL;
-            }
-        }
-    }
-
-    private logParams() {
-        for (const fe of this._formBase.allFormElements) {
-            if (fe instanceof NgParameter) {
-                console.log(`${fe.paramDefinition.symbol} : ${ParamValueMode[fe.paramDefinition.valueMode]}`);
-            }
-        }
-    }
-
-    /**
-     * gestion des événements clic sur les radios
-     */
-    public onRadioClick(info: any) {
-        const param: NgParameter = info.param; // paramètre source de l'événement radio
-        const old: ParamValueMode = info.oldValueMode; // ancien état (radio)
-        this.resetRadiosAndResults(param, old);
-    }
 }
diff --git a/src/app/formulaire/definition/form-def-parallel-structures.ts b/src/app/formulaire/definition/form-def-parallel-structures.ts
index a4856994d88a189b00a3d07e67a66a25113147b6..5206d9cc0f74141f5474fb99eafef8d12dfd6427 100644
--- a/src/app/formulaire/definition/form-def-parallel-structures.ts
+++ b/src/app/formulaire/definition/form-def-parallel-structures.ts
@@ -1,7 +1,3 @@
-import { CalculatorType, StructureType, LoiDebit } from "jalhyd";
-
-import { FieldSet } from "../fieldset";
-
 /**
  * gestion des formulaires "ouvrages parallèles"
  */
diff --git a/src/app/formulaire/definition/form-def-paramcalc.ts b/src/app/formulaire/definition/form-def-paramcalc.ts
index cd867bde8188ebef8cc6f3861d886fdf774e8e2b..c16a0397eaf376fcb6c15c4a2b03db1d471aa282 100644
--- a/src/app/formulaire/definition/form-def-paramcalc.ts
+++ b/src/app/formulaire/definition/form-def-paramcalc.ts
@@ -1,6 +1,3 @@
-import { ParamValueMode } from "jalhyd";
-
-import { NgParameter } from "../ngparam";
 import { FormulaireDefinition } from "./form-definition";
 import { FormDefFixedVar } from "./form-def-fixedvar";
 
@@ -8,97 +5,12 @@ import { FormDefFixedVar } from "./form-def-fixedvar";
  * gestion des formulaires avec "paramètre à calculer" (conduite distributrice, Lechapt-Calmon, régime uniforme, passes à bassin)
  */
 export class FormDefParamToCalculate extends FormDefFixedVar {
-    /**
-     * symbole du paramètre à calculer par défaut (cf config "idCal")
-     */
-    private _defaultCalculatedParam: string;
 
     constructor(base: FormulaireDefinition) {
         super(base);
     }
 
     public parseOptions(json: {}) {
-        this._defaultCalculatedParam = undefined;
-        // browse config file to find "options" chapter
-        for (const k in json) {
-            const o = json[k];
-            if (o.type === "options") {
-                // id du paramètre à calculer par défaut
-                this._defaultCalculatedParam = o["idCal"];
-                // this._formBase
-                if (this._defaultCalculatedParam && ! this.findCalculatedParam()) {
-                    const p = this.setDefault();
-                    p.isDefault = true;
-                }
-            }
-        }
-    }
-
-    /**
-     * Find the parameter that is set to CALC mode
-     */
-    private findCalculatedParam() {
-        for (const p of this._formBase.currentNub.parameterIterator) {
-            if (p.valueMode === ParamValueMode.CALCUL) {
-                return p;
-            }
-        }
-    }
-
-    /**
-     * met le paramètre par défaut à CAL sauf si c'est "except"
-     * @param except paramètre à ne pas remettre à CAL
-     */
-    public setDefault(except?: NgParameter): NgParameter {
-        const defaultParamCal: NgParameter = this._formBase.getParamFromSymbol(this._defaultCalculatedParam);
-        if (except === undefined || defaultParamCal.uid !== except.uid) {
-            defaultParamCal.valueMode = ParamValueMode.CALCUL;
-        }
-        return defaultParamCal;
     }
 
-    /**
-     * @see FormDefFixedVar.processRadioStateChange pour l'analyse
-     */
-    protected processRadioStateChange(sourceParam: NgParameter, oldState: ParamValueMode) {
-        super.processRadioStateChange(sourceParam, oldState);
-
-        switch (oldState) {
-            case ParamValueMode.CALCUL:  // ancien état
-                switch (sourceParam.valueMode) {
-                    case ParamValueMode.SINGLE:  // nouvel état
-                        this.setDefault(sourceParam);
-                        break;
-
-                    case ParamValueMode.MINMAX:  // nouvel état
-                    case ParamValueMode.LISTE:
-                        super.resetOtherRadio(sourceParam);
-                        this.setDefault(sourceParam);
-                        break;
-
-                    case ParamValueMode.LINK:  // nouvel état
-                        if (sourceParam.paramDefinition.hasMultipleValues) {
-                            super.resetOtherRadio(sourceParam);
-                            this.setDefault();
-                        } else {
-                            // mode du paramètre référencé
-                            const refParamValues = sourceParam.paramDefinition.referencedParamValues;
-                            if (refParamValues !== undefined) {
-                                switch (refParamValues.valueMode) {
-                                    case ParamValueMode.MINMAX:
-                                    case ParamValueMode.LISTE:
-                                    case ParamValueMode.CALCUL:
-                                        super.resetOtherRadio(sourceParam);
-                                        this.setDefault();
-                                        break;
-
-                                    case ParamValueMode.LINK:
-                                        throw new Error(`références de paramètre en chaîne non pris en charge`); // cas à traiter
-                                }
-                            }
-                        }
-                        break;
-                }
-        }
-    }
 }
diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts
index 37d42aeb2b53a5276fa2dcdbd23ee7e820cf0106..0c43a6bf980161dcaabb17989f6fc704c888135c 100644
--- a/src/app/formulaire/definition/form-definition.ts
+++ b/src/app/formulaire/definition/form-definition.ts
@@ -1,4 +1,4 @@
-import { CalculatorType, ComputeNodeType, Nub, Props, Observer, Session, ParallelStructure } from "jalhyd";
+import { CalculatorType, ComputeNodeType, Nub, Props, Observer, Session } from "jalhyd";
 
 import { FormulaireElement } from "../formulaire-element";
 import { NgParameter, ParamRadioConfig } from "../ngparam";
@@ -36,6 +36,9 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     /** clé-valeurs du fichier de localisation spécifique à ce module */
     private _specificLocalisation: StringMap;
 
+    /** ISO 639-1 language code of the current language (to avoid unnecessary localisation reload) */
+    private _currentLanguage: string;
+
     constructor() {
         super(undefined);
     }
@@ -54,6 +57,10 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         return this._specificLocalisation;
     }
 
+    public get currentLanguage() {
+        return this._currentLanguage;
+    }
+
     public get calculatorType(): CalculatorType {
         const props = this._currentNub === undefined ? this.defaultProperties : (this._currentNub.properties as Props).props;
         return props["calcType"];
@@ -87,7 +94,7 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     }
 
     public initNub(props?: Props) {
-        this._currentNub = this.createNub(props ? props : new Props(this.defaultProperties));
+        this.currentNub = this.createNub(props ? props : new Props(this.defaultProperties));
     }
 
     public get currentNub(): Nub {
@@ -101,7 +108,14 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
                 `Nub ${n.properties["calcType"]} incompatible avec le formulaire ${this._calculatorName} (${this._props["calcType"]})`
             );
         }
+        // unsubscribe from old Nub
+        if (this._currentNub) {
+            this._currentNub.removeObserver(this);
+        }
+        // replace Nub
         this._currentNub = n;
+        // subscribe to new Nub (for result updates)
+        this._currentNub.addObserver(this);
     }
 
     /**
@@ -155,7 +169,7 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
      * @param uid id à rechercher
      */
     public hasNubId(uid: string): boolean {
-        return this._currentNub.uid === uid;
+        return (this._currentNub && this._currentNub.uid === uid);
     }
 
     public moveFieldsetUp(fs: FieldSet) {
@@ -269,17 +283,6 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         }
 
         this.completeParse(this._jsonConfig);
-
-        // console.log("-----");
-
-        // for (const n of Session.getInstance().NubIterator) {
-        //     console.log(n.nub);
-        //     for (const p of n.nub.parameterIterator)
-        //         console.log(`${p.symbol} uid  ${p.uid} props ${n.properties} mode ${p.valueMode} val ${p.uncheckedValue}`);
-        // }
-
-        // logObject(this._fieldSets, "fieldsets");
-        // logObject(this._dependencies, "dependences");
         this.parseDependencies(this._jsonConfig);
     }
 
@@ -355,12 +358,22 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         }, this);
     }
 
+    /**
+     * Forwards Nub's result updated notification.
+     * Used by FormCompute to update results display
+     */
+    protected notifyNubResultUpdated(sender) {
+        this.notifyObservers({
+            action: "nubResultUpdated"
+        }, sender);
+    }
+
     /**
      * réinitialisation du formulaire suite à un changement d'une valeur, d'une option, ... :
      * effacement des résultats, application des dépendances, ...
      */
     public reset() {
-        this.resetResults();
+        this.resetResults([]);
         this.applyDependencies();
 
         // prévenir les composants qu'il faut détecter les changements
@@ -393,19 +406,19 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         }
     }
 
-    public abstract resetResults();
+    public abstract resetResults(visited: string[]);
     public abstract doCompute();
     public abstract get hasResults(): boolean;
     public abstract get results(): CalculatorResults[];
 
-    public updateLocalisation(localisation: StringMap) {
+    public updateLocalisation(localisation: StringMap, lang: string) {
         this._specificLocalisation = localisation;
+        this._currentLanguage = lang;
         for (const fe of this.topFormElements) {
             fe.updateLocalisation(localisation);
         }
-
         if (this.hasResults) {
-            this.doCompute(); // pour mettre à jour la langue
+            this.doCompute(); // pour mettre à jour la langue des intitulés de résultats
         }
     }
 
@@ -421,21 +434,14 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         return (<FormulaireElement>this.getFormulaireNodeById(id)).isDisplayed;
     }
 
-    public get isValid(): boolean {
-        let res = true;
-        for (const fs of this.allFieldsets) {
-            if (fs.isDisplayed) {
-                res = res && fs.isValid;
-            }
-        }
-        return res;
-    }
-
     /**
      * gestion d'un clic sur les radios
      */
     public onRadioClick(info: any) {
-        this.reset();
+        // if mode changed, reset form results
+        if (info.oldValueMode !== info.param.valueMode) {
+            this.reset();
+        }
     }
 
     /**
@@ -477,28 +483,16 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         return undefined;
     }
 
-    /**
-      * @param json conf du formulaire
-      * @param uidMap table de correspondance uid dans le fichier de conf <-> uid en mémoire
-      */
-    public updateParamsLinks(json: {}, uidMap: {}[]) {
-        for (const ks in json) {
-            switch (ks) {
-                case "elements":
-                    let n = 0;
-                    for (const e of json[ks]) {
-                        if (Object.keys(e)[0] === "fieldset") {
-                            this.getNthFieldset(n).updateParamsLinks(e["fieldset"], uidMap);
-                            n++;
-                        }
-                    }
-                    break;
-            }
-        }
-    }
-
     //  interface Observer
 
     public update(sender: any, data: any) {
+        if (sender instanceof Nub) {
+            switch (data.action) {
+                case "resultUpdated":
+                    // forward Nub results update notification to FormCompute objects
+                    this.notifyNubResultUpdated(sender);
+                    break;
+            }
+        }
     }
 }
diff --git a/src/app/formulaire/definition/form-result-fixedvar.ts b/src/app/formulaire/definition/form-result-fixedvar.ts
index 291a6373a5fca0df5663b970d250b34256a400ed..be7137587ba266eb3ec4c47289d6393a0bcd95cf 100644
--- a/src/app/formulaire/definition/form-result-fixedvar.ts
+++ b/src/app/formulaire/definition/form-result-fixedvar.ts
@@ -1,5 +1,3 @@
-import { ResultElement, cLog, ParamValueMode } from "jalhyd";
-
 import { FixedResults } from "../../results/fixed-results";
 import { GraphType, VarResults } from "../../results/var-results";
 import { ParamRadioConfig, NgParameter } from "../ngparam";
diff --git a/src/app/formulaire/field.ts b/src/app/formulaire/field.ts
index c8d2f84b60290e67cbf9e3b570156758cb9a59ec..3bdbef3c0a504e0364f48ced8fffdaaebc1cccc1 100644
--- a/src/app/formulaire/field.ts
+++ b/src/app/formulaire/field.ts
@@ -4,7 +4,6 @@ import { Dependency } from "./dependency/dependency";
 import { isNumber } from "util";
 
 export abstract class Field extends FormulaireElement {
-    public abstract get isValid();
 
     public abstract getValue(): any;
     public abstract setValue(sender: any, val: any): void;
diff --git a/src/app/formulaire/fieldset.ts b/src/app/formulaire/fieldset.ts
index 5dd19f2e5d86440901aa34acfc4c06a75c06677f..ff2789eb397c9c9e4cdd7f06657be1d9669e92d7 100644
--- a/src/app/formulaire/fieldset.ts
+++ b/src/app/formulaire/fieldset.ts
@@ -2,7 +2,6 @@ import { CalculatorType, ComputeNodeType, ParamDefinition, LoiDebit, StructureTy
 
 import { FormulaireElement } from "./formulaire-element";
 import { Field } from "./field";
-import { CheckField } from "./check-field";
 import { SelectField } from "./select-field";
 import { NgParameter, ParamRadioConfig } from "./ngparam";
 import { ServiceFactory } from "../services/service-factory";
@@ -75,24 +74,6 @@ export class FieldSet extends FormulaireElement implements Observer {
         }
     }
 
-    public get isValid(): boolean {
-        let res = true;
-        for (const f of this.kids) {
-            if (f instanceof Field) {
-                if (f.isDisplayed) {
-                    res = res && f.isValid;
-                }
-            }
-        }
-        return res;
-    }
-
-    private parse_check(json: {}): CheckField {
-        const res: CheckField = new CheckField(this);
-        res.parseConfig(json);
-        return res;
-    }
-
     private parse_select(json: {}): SelectField {
         const res: SelectField = new SelectField(this);
         res.parseConfig(json);
@@ -152,8 +133,6 @@ export class FieldSet extends FormulaireElement implements Observer {
 
         if (res) {
             res.parseConfig(json, { "radioConfig": default_radio_config });
-            // set parent Nub on Parameter to ensure UID availability
-            res.paramDefinition.parent = this._nub;
         }
 
         return res;
@@ -174,9 +153,6 @@ export class FieldSet extends FormulaireElement implements Observer {
             } else if (field["type"] === "select") {
                 const param = this.parse_select(field);
                 this.addField(param);
-            } else if (field["type"] === "check") {
-                const param = this.parse_check(field);
-                this.addField(param);
             }
         }
     }
@@ -401,46 +377,4 @@ export class FieldSet extends FormulaireElement implements Observer {
             }
         }
     }
-
-    /**
-     * MAJ des liens entre paramètres lors de la désérialisation
-     * @param json conf du fieldset issue du fichier
-     * @param uidMap table de correspondance uid dans le fichier de conf <-> uid en mémoire
-     */
-    public updateParamsLinks(json: {}, uidMap: {}[]) {
-        for (const ks in json) {
-            switch (ks) {
-                case "elements":
-                    for (const e of json[ks]) {
-                        if (Object.keys(e)[0] === "param") {
-                            const prm = e["param"];
-                            if (prm["values"]["mode"] === "LINK") {
-                                // id du formulaire cible dans le fichier
-                                const oldFormUid = +prm["values"]["form_uid"];
-
-                                // correspondance avec l'objet mémoire
-                                let newFormUid: string;
-                                for (const m of uidMap) {
-                                    if (m["type"] === "form" && m["old"] === oldFormUid) {
-                                        newFormUid = m["new"];
-                                        break;
-                                    }
-                                }
-
-                                // formulaire dont le Nub est la cible du lien
-                                const destForm: FormulaireDefinition
-                                    = ServiceFactory.instance.formulaireService.getFormulaireFromId(newFormUid);
-
-                                // paramètre source (celui qui est lié à une valeur)
-                                const src: NgParameter = this.getFormulaireNodeById(prm["id"]) as NgParameter;
-
-                                // création du lien
-                                src.paramDefinition.defineReference(destForm.currentNub, prm["values"]["ref"]);
-                            }
-                            break;
-                        }
-                    }
-            }
-        }
-    }
 }
diff --git a/src/app/formulaire/ngparam.ts b/src/app/formulaire/ngparam.ts
index 30ec5d0d52392f2c20621ba5b2deac71eadfdc0a..3f9f58711da6fb78f6f1b78e2f5ea7a6dd4ce287 100644
--- a/src/app/formulaire/ngparam.ts
+++ b/src/app/formulaire/ngparam.ts
@@ -1,5 +1,5 @@
 import { Interval, ParamDefinition, ParamDomain, ParamValueMode, INumberIterator,
-    Nub, Observer, asObservable, ParamCalculability } from "jalhyd";
+    Observer, asObservable, ParamCalculability, LinkedValue } from "jalhyd";
 
 import { sprintf } from "sprintf-js";
 
@@ -11,24 +11,16 @@ import { ServiceFactory } from "../services/service-factory";
 import { FormulaireNode } from "./formulaire-node";
 
 export enum ParamRadioConfig {
-    /**
-     * pas de radio, paramètre modifiable à la main uniquement
-     */
+    /** pas de radio, paramètre modifiable à la main uniquement */
     FIX,
 
-    /**
-     * boutons radio "paramètre fixé" et "paramètre à varier"
-     */
+    /** boutons radio "paramètre fixé" et "paramètre à varier" */
     VAR,
 
-    /**
-     * boutons radio "paramètre fixé", "paramètre à varier" et "paramètre à calculer"
-     */
+    /** boutons radio "paramètre fixé", "paramètre à varier" et "paramètre à calculer" */
     CAL,
 
-    /**
-     * boutons radio "paramètre fixé", "paramètre à varier" et "paramètre à calculer", "paramètre lié"
-     */
+    /** boutons radio "paramètre fixé", "paramètre à varier" et "paramètre à calculer", "paramètre lié" */
     LINK
 }
 
@@ -37,33 +29,30 @@ export enum ParamRadioConfig {
  */
 export class NgParameter extends InputField implements Observer {
 
-    constructor(private _paramDef: ParamDefinition, parent: FormulaireNode) {
-        super(parent);
-    }
     public unit: string;
     public radioConfig: ParamRadioConfig;
 
-    /**
-     * true si ce paramètre est celui par défaut dans un formulaire
-     * (cf. fichier de conf des modules de calcul, objet "options", champ "idCal")
-     */
-    public isDefault = false; // archi bug du langage ! si on relit cette propriété sans l'avoir modifiée entre-temps, elle vaut undefined !
+    constructor(private _paramDef: ParamDefinition, parent: FormulaireNode) {
+        super(parent);
+        this.radioConfig = this.radioState;
+    }
 
     /**
      * Returns a text preview of the current value(s), depending on the value mode
      */
     public static preview(p: ParamDefinition): string {
         let valuePreview: string;
+        const i18n = ServiceFactory.instance.i18nService;
+        const nDigits = ServiceFactory.instance.applicationSetupService.displayDigits;
 
         switch (p.valueMode) {
             case ParamValueMode.SINGLE:
-                valuePreview = String(p.getValue());
+                valuePreview = String(p.getValue().toFixed(nDigits));
                 break;
             case ParamValueMode.MINMAX:
-                const nDigits = ServiceFactory.instance.applicationSetupService.displayDigits;
-                let min: any = p.paramValues.min;
-                let max: any = p.paramValues.max;
-                let step: any = p.paramValues.step;
+                let min: any = p.min;
+                let max: any = p.max;
+                let step: any = p.step;
                 if (min) {
                     min = min.toFixed(nDigits);
                 }
@@ -73,24 +62,73 @@ export class NgParameter extends InputField implements Observer {
                 if (step) {
                     step = step.toFixed(nDigits);
                 }
-                valuePreview = ServiceFactory.instance.i18nService.localizeText("INFO_PARAMFIELD_PARAMVARIER_MINMAXSTEP");
+                valuePreview = i18n.localizeText("INFO_PARAMFIELD_PARAMVARIER_MINMAXSTEP");
                 valuePreview = sprintf(valuePreview, min, max, step);
                 break;
             case ParamValueMode.LISTE:
-                valuePreview = ServiceFactory.instance.i18nService.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES");
-                const vals = p.paramValues.valueList || [];
-                valuePreview += " " + vals.slice(0, 5).join("; ") + "…";
+                valuePreview = i18n.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES");
+                const vals = p.valueList || [];
+                valuePreview += " " + vals.slice(0, 5).map((v) => {
+                    return v.toFixed(nDigits);
+                }).join("; ") + "…";
                 break;
             case ParamValueMode.CALCUL:
-                valuePreview = ServiceFactory.instance.i18nService.localizeText("INFO_PARAMFIELD_IN_CALCULATION");
+                valuePreview = i18n.localizeText("INFO_PARAMFIELD_IN_CALCULATION");
                 if (p.calculability === ParamCalculability.DICHO) {
-                    valuePreview += " (" + ServiceFactory.instance.i18nService.localizeText("INFO_PARAMFIELD_IN_CALCULATION_INITIAL_VALUE")
-                        + ": " + p.getValue() + ")";
+                    valuePreview += " (" + i18n.localizeText("INFO_PARAMFIELD_IN_CALCULATION_INITIAL_VALUE")
+                        + ": " + p.getValue().toFixed(nDigits) + ")";
                 }
                 break;
             case ParamValueMode.LINK:
-                // recursive call
-                valuePreview = NgParameter.preview(p.referencedObject as ParamDefinition);
+                if (p.isReferenceDefined()) {
+                    if (p.referencedValue.isParameter()) {
+                        const targetParam = (p.referencedValue.element as ParamDefinition);
+                        // calculated param ?
+                        if (targetParam.valueMode === ParamValueMode.CALCUL) {
+                            // was the result already computed ?
+                            // @WAARNING .result might be set but the computation might have failed (dichotomy for ex.)
+                            if (p.referencedValue.nub.result) {
+                                if (p.referencedValue.hasMultipleValues()) {
+                                    // like LIST mode
+                                    valuePreview = i18n.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES");
+                                    valuePreview += " " + p.referencedValue.nub.result.getCalculatedValues().map((v) => {
+                                        return v.toFixed(nDigits);
+                                    }).slice(0, 5).join("; ") + "…";
+                                } else {
+                                    const vCalc = p.referencedValue.nub.result.vCalc;
+                                    if (vCalc) {
+                                        valuePreview = String(vCalc.toFixed(nDigits));
+                                    } else {
+                                        // computation has been run but has failed
+                                        valuePreview = i18n.localizeText("INFO_PARAMFIELD_CALCULATION_FAILED");
+                                    }
+                                }
+                            } else {
+                                valuePreview = i18n.localizeText("INFO_PARAMFIELD_IN_CALCULATION");
+                            }
+                        } else {
+                            // recursive call
+                            valuePreview =  NgParameter.preview(targetParam);
+                        }
+                    } else {
+                        // was the result already computed ?
+                        try {
+                            const remoteValues = p.referencedValue.getParamValues();
+                            if (p.referencedValue.hasMultipleValues()) {
+                                // like LIST mode
+                                valuePreview = i18n.localizeText("INFO_PARAMFIELD_PARAMVARIER_VALUES");
+                                valuePreview += " " + remoteValues.valueList.slice(0, 5).map((v) => {
+                                    return v.toFixed(nDigits);
+                                }).join("; ") + "…";
+                            } else {
+                                // like SINGLE mode
+                                valuePreview = String(remoteValues.currentValue.toFixed(nDigits));
+                            }
+                        } catch (e) {
+                            valuePreview = i18n.localizeText("INFO_PARAMFIELD_IN_CALCULATION");
+                        }
+                    }
+                }
         }
 
         return valuePreview;
@@ -101,7 +139,7 @@ export class NgParameter extends InputField implements Observer {
     }
 
     get domain(): ParamDomain {
-        return this._paramDef.getDomain();
+        return this._paramDef.domain;
     }
 
     public set confId(id: string) {
@@ -111,10 +149,6 @@ export class NgParameter extends InputField implements Observer {
         this._confId = id;
     }
 
-    private get _paramValues() {
-        return this._paramDef.paramValues;
-    }
-
     public get paramDefinition() {
         return this._paramDef;
     }
@@ -144,15 +178,19 @@ export class NgParameter extends InputField implements Observer {
         return this._paramDef.valueMode;
     }
 
+    /**
+     * Unlinks the parameter and updates its value when value mode changes
+     */
     public set valueMode(m: ParamValueMode) {
         // undefined si on clique en dehors du select après l'avoir ouvert (cad sans avoir fait de sélection)
         // et au même niveau, cad à côté du bouton et non à côté du menu déroulant
         if (m !== undefined && this._paramDef.valueMode !== m) {
+            const nDigits = ServiceFactory.instance.applicationSetupService.displayDigits;
             this.unlinkParameter();
             this._paramDef.valueMode = m;
             this.notifyObservers({
                 "action": "valueModeChange",
-                "value": this._paramDef.getValue()
+                "value": Number(this._paramDef.getValue().toFixed(nDigits))
             });
         }
     }
@@ -162,39 +200,23 @@ export class NgParameter extends InputField implements Observer {
     }
 
     public get minValue() {
-        return this._paramValues.min;
-    }
-
-    public set minValue(v: number) {
-        this._paramValues.min = v;
+        return this._paramDef.min;
     }
 
     public get maxValue() {
-        return this._paramValues.max;
-    }
-
-    public set maxValue(v: number) {
-        this._paramValues.max = v;
+        return this._paramDef.max;
     }
 
     public get stepRefValue(): Interval {
-        return this._paramValues.stepRefValue;
+        return this._paramDef.stepRefValue;
     }
 
     public get stepValue() {
-        return this._paramValues.step;
-    }
-
-    public set stepValue(v: number) {
-        this._paramValues.step = v;
+        return this._paramDef.step;
     }
 
     public get valueList() {
-        return this._paramValues.valueList;
-    }
-
-    public set valueList(l: number[]) {
-        this._paramValues.valueList = l;
+        return this._paramDef.valueList;
     }
 
     public get isValid() {
@@ -236,8 +258,19 @@ export class NgParameter extends InputField implements Observer {
         throw new Error("invalid parameter radio configuration " + s);
     }
 
+    /**
+     * Sets this parameter as the one to be computed
+     */
+    public setCalculated() {
+        this.paramDefinition.setCalculated();
+    }
+
+    /**
+     * Asks the ParamDefinition for its current value
+     * @TODO replace with singleValue to avoid displaying computation results ?
+     */
     public getValue() {
-        return this._paramDef.v;
+        return this._paramDef.getValue();
     }
 
     /**
@@ -265,11 +298,43 @@ export class NgParameter extends InputField implements Observer {
         this.notifyValueModified(sender);
     }
 
+    public setMinValue(sender: any, v: number) {
+        const changed = (this._paramDef.min !== v);
+        this._paramDef.min = v;
+        if (changed) {
+            this.notifyValueModified(sender);
+        }
+    }
+
+    public setMaxValue(sender: any, v: number) {
+        const changed = (this._paramDef.max !== v);
+        this._paramDef.max = v;
+        if (changed) {
+            this.notifyValueModified(sender);
+        }
+    }
+
+    public setStepValue(sender: any, v: number) {
+        const changed = (this._paramDef.step !== v);
+        this._paramDef.step = v;
+        if (changed) {
+            this.notifyValueModified(sender);
+        }
+    }
+
+    public setValueList(sender: any, l: number[]) {
+        const changed = (JSON.stringify(this._paramDef.valueList) !== JSON.stringify(l));
+        this._paramDef.valueList = l;
+        if (changed) {
+            this.notifyValueModified(sender);
+        }
+    }
+
     /**
      * supprime un lien avec un paramètre
      */
     private unlinkParameter() {
-        const o = asObservable(this._paramDef.referencedObject);
+        const o = asObservable(this._paramDef.referencedValue);
         if (this.valueMode === ParamValueMode.LINK) {
             this._paramDef.undefineReference();
             if (o !== undefined) {
@@ -281,34 +346,44 @@ export class NgParameter extends InputField implements Observer {
     /**
      * crée le lien avec un paramètre
      */
-    public linkToParameter(n: Nub, ref: string) {
+    public linkToValue(target: LinkedValue) {
         const changed: boolean =
-            this._paramDef.valueMode !== ParamValueMode.LINK || this._paramDef.referencedNub !== n ||
-            this._paramDef.referenceDefinition !== ref;
+            // changement de mode
+            ! this._paramDef.isReferenceDefined()
+            // ou changement de référence
+            || ! this._paramDef.referencedValue.equals(target);
+
         if (changed) {
-            let o = asObservable(this._paramDef.referencedObject);
+            let o = asObservable(this._paramDef.referencedValue);
             if (o !== undefined) {
                 o.removeObserver(this);
             }
 
-            this.valueMode = ParamValueMode.LINK;
+            // this.valueMode = ParamValueMode.LINK; // supposed to be done at model level by instruction below
             // changement de lien
-            this._paramDef.defineReference(n, ref);
+            this._paramDef.defineReferenceFromLinkedValue(target);
 
-            o = asObservable(this._paramDef.referencedObject);
+            o = asObservable(this._paramDef.referencedValue);
             if (o !== undefined) {
                 o.addObserver(this); // pour être prévenu des changements de valeur de l'object référencé
             }
 
+            let value;
+            try {
+                value = this.getValue();
+            } catch (e) {
+                // console.log("undefined target value (pending calculation)");
+            }
+
             this.notifyObservers({
                 "action": "valueLinkChange",
-                "value": this.getValue()
+                "value": value
             });
         }
     }
 
     public checkValue(val: number) {
-        this._paramDef.checkValue(val);
+        this._paramDef.checkValueAgainstDomain(val);
     }
 
     public checkList(l: number[]) {
@@ -339,8 +414,6 @@ export class NgParameter extends InputField implements Observer {
             this.setValue(this, +val);
         }
         this.radioConfig = NgParameter.getRadioConfig(radioConfig);
-        // tslint:disable-next-line:max-line-length
-        this.isDefault = false; // malgré le fait qu'il soit initialisé dans la déclaration de la classe NgParam à false, quand on relit sa valeur, il vaut undefined (merci Microsoft)
     }
 
     public verifiesDependency(d: Dependency): boolean {
diff --git a/src/app/formulaire/select-field.ts b/src/app/formulaire/select-field.ts
index df078bfbf1a912061129b46786d72cb03e331b7d..c60b4dd506e3f122f01716f8ca95055251ad80ea 100644
--- a/src/app/formulaire/select-field.ts
+++ b/src/app/formulaire/select-field.ts
@@ -51,10 +51,6 @@ export class SelectField extends Field {
         }
     }
 
-    public get isValid(): boolean {
-        return true;
-    }
-
     public getLabel() {
         if (this._selectedEntry) {
             return this._selectedEntry.label;
diff --git a/src/app/results/var-results.ts b/src/app/results/var-results.ts
index 752664fcb2e5f46fb8492fe82c4913ce98778963..51208365da42c0a35e6f15778fa47550f1f6ff55 100644
--- a/src/app/results/var-results.ts
+++ b/src/app/results/var-results.ts
@@ -116,7 +116,6 @@ export class VarResults extends CalculatedParamResults {
      * @param symbol parameter / result symbol (ex: "Q")
      */
     public getValuesSeries(symbol: string) {
-        // console.log("GVS for symbol", symbol);
         const series = [];
         // 1. calculated param ?
         if (this._calculatedParam.symbol === symbol) {
diff --git a/src/app/services/formulaire/formulaire.service.ts b/src/app/services/formulaire/formulaire.service.ts
index 066e82f7abe338d63e5ab8f387841dc84e66e679..79f728dcbe4404a21f454ebb9657b523d3fcbad1 100644
--- a/src/app/services/formulaire/formulaire.service.ts
+++ b/src/app/services/formulaire/formulaire.service.ts
@@ -3,7 +3,7 @@ import { Injectable } from "@angular/core";
 import { decode } from "he";
 import { saveAs } from "file-saver";
 
-import { CalculatorType, EnumEx, Observable, ParamDefinition, Session, Nub, ParallelStructure } from "jalhyd";
+import { CalculatorType, LinkedValue, Observable, ParamDefinition, Session, Nub, ParallelStructure } from "jalhyd";
 
 import { HttpService } from "../../services/http/http.service";
 import { I18nService } from "../../services/internationalisation/internationalisation.service";
@@ -11,7 +11,6 @@ import { FormulaireDefinition } from "../../formulaire/definition/form-definitio
 import { FormulaireElement } from "../../formulaire/formulaire-element";
 import { InputField } from "../../formulaire/input-field";
 import { SelectField } from "../../formulaire/select-field";
-import { CheckField } from "../../formulaire/check-field";
 import { StringMap } from "../../stringmap";
 import { FormulaireBase } from "../../formulaire/definition/concrete/form-base";
 import { FormulaireLechaptCalmon } from "../../formulaire/definition/concrete/form-lechapt-calmon";
@@ -22,6 +21,7 @@ import { FormulaireParallelStructure } from "../../formulaire/definition/concret
 import { NgParameter } from "../../formulaire/ngparam";
 import { FieldsetContainer } from "../..//formulaire/fieldset-container";
 import { ApplicationSetupService } from "../app-setup/app-setup.service";
+import { NotificationsService } from "../notifications/notifications.service";
 
 @Injectable()
 export class FormulaireService extends Observable {
@@ -36,7 +36,9 @@ export class FormulaireService extends Observable {
     constructor(
         private i18nService: I18nService,
         private appSetupService: ApplicationSetupService,
-        private httpService: HttpService
+        private httpService: HttpService,
+        private intlService: I18nService,
+        private notificationsService: NotificationsService
     ) {
         super();
         this._formulaires = [];
@@ -97,27 +99,17 @@ export class FormulaireService extends Observable {
     }
 
     /**
-     * Met à jour la langue du formulaire
-     * @param formId id unique du formulaire
-     * @param localisation ensemble id-message traduit
-     */
-    private updateFormulaireLocalisation(formId: string, localisation: StringMap) {
-        for (const f of this._formulaires) {
-            if (f.uid === formId) {
-                f.updateLocalisation(localisation);
-                break;
-            }
-        }
-    }
-
-    /**
-     * charge la localisation et met à jour la langue du formulaire
+     * Loads localisation file corresponding to current language then updates all form strings,
+     * only if form language was not already set to current language
      */
     public loadUpdateFormulaireLocalisation(f: FormulaireDefinition): Promise<FormulaireDefinition> {
-        return this.loadLocalisation(f.calculatorType).then(localisation => {
-            this.updateFormulaireLocalisation(f.uid, localisation);
-            return f;
-        });
+        const requiredLang = this._intlService.currentLanguage;
+        if (requiredLang !== f.currentLanguage) {
+            return this.loadLocalisation(f.calculatorType).then(localisation => {
+                f.updateLocalisation(localisation, requiredLang);
+                return f;
+            });
+        }
     }
 
     /**
@@ -302,14 +294,6 @@ export class FormulaireService extends Observable {
         return <InputField>s;
     }
 
-    public getCheckField(formId: string, elemId: string): CheckField {
-        const s = this.getFormulaireElementById(formId, elemId);
-        if (!(s instanceof CheckField)) {
-            throw new Error("Form element with id '" + elemId + "' is not a checkbox");
-        }
-        return <CheckField>s;
-    }
-
     public getSelectField(formId: string, elemId: string): SelectField {
         const s = this.getFormulaireElementById(formId, elemId);
         if (!(s instanceof SelectField)) {
@@ -413,8 +397,14 @@ export class FormulaireService extends Observable {
                 "action": "closeForm",
                 "form": form
             });
+
+            // reset the result panels of all forms depending on this one
+            // @TODO UI should detect model change instead of doing this manually
+            this.resetAllDependingFormsResults(form, [], false);
         }
         if (nub) {
+            // reset model results (important, also resets dependent Nubs results in chain)
+            form.currentNub.resetResult();
             Session.getInstance().deleteNub(nub);
         }
     }
@@ -456,16 +446,13 @@ export class FormulaireService extends Observable {
     private readSingleFile(file: File): Promise<any> {
         return new Promise<any>((resolve, reject) => {
             const fr = new FileReader();
-
             fr.onload = () => {
                 resolve(fr.result);
             };
-
             fr.onerror = () => {
                 fr.abort();
                 reject(new Error(`Erreur de lecture du fichier ${file.name}`));
             };
-
             fr.readAsText(file);
         });
     }
@@ -512,10 +499,27 @@ export class FormulaireService extends Observable {
             // liste des noms de modules de calcul
             if (data.session && Array.isArray(data.session)) {
                 data.session.forEach((e: any) => {
-                   res.push({
-                       uid: e.uid,
-                       title: e.meta && e.meta.title ? e.meta.title : undefined
-                    });
+                    const nubInfo = {
+                        uid: e.uid,
+                        title: e.meta && e.meta.title ? e.meta.title : undefined,
+                        requires: [],
+                        children: []
+                    };
+                    // list linked params dependencies for each Nub
+                    if (e.parameters) {
+                        e.parameters.forEach((p) => {
+                            if (p.targetNub && ! nubInfo.requires.includes(p.targetNub)) {
+                                nubInfo.requires.push(p.targetNub);
+                            }
+                        });
+                    }
+                    // list children nubs for each Nub
+                    if (e.structures) {
+                        e.structures.forEach((p) => {
+                            nubInfo.children.push(p.uid);
+                        });
+                    }
+                    res.push(nubInfo);
                 });
             }
             return res;
@@ -530,93 +534,36 @@ export class FormulaireService extends Observable {
     }
 
     /**
-     * met à jour les liens d'un formulaire
-     * @param json conf du formulaire
-     * @param formInfos métadonnées sur les formulaires chargés
-     * @param form formulaire dont on met à jour les liens
-     * @param uidMap table de correspondance uid dans le fichier de conf <-> uid en mémoire
+     * Demande à la Session JalHYd la liste des paramètres/résultats pouvant être liés au
+     * paramètre fourni
      */
-    private updateFormLinks(json: {}, formInfos: any[], form: FormulaireDefinition, uidMap: {}[]) {
-        for (const i of formInfos) {
-            if (i["uid"] === json["uid"] && i["selected"]) {
-                form.updateParamsLinks(json, uidMap);
-            }
-        }
-    }
-
-    /**
-     * MAJ des liens entre paramètres lors de la désérialisation
-     */
-    /* private updateParamsLinks(json: {}, formInfos: any[], oldFormCount: number) {
-        // table de correspondance des uid fichier <-> objets mémoire
-        // forme : tableau d'objets de la forme :
-        // { "type" : <type de l'objet. "form" pour formulaire>,
-        //   "old": <uid dans le fichier>,
-        //   "new": <uid de l'objet mémoire>}
-        const uidMap = [];
-        for (const ks in json) {
-            switch (ks) {
-                case "elements":
-                    let n = oldFormCount;
-                    for (const e of json[ks]) {
-                        if (Object.keys(e)[0] === "form") {
-                            uidMap.push({
-                                "type": "form",
-                                "old": e["form"]["uid"],
-                                "new": this._formulaires[n].uid
-                            });
-                            n++;
-                        }
-                    }
-            }
-        }
-        // MAJ liens
-        for (const ks in json) {
-            switch (ks) {
-                case "elements":
-                    let n = 0;
-                    for (const e of json[ks]) {
-                        if (Object.keys(e)[0] === "form") {
-                            this.updateFormLinks(e["form"], formInfos, this._formulaires[n + oldFormCount], uidMap);
-                            n++;
+    public getLinkableValues(p: NgParameter): LinkedValue[] {
+        let linkableValues: LinkedValue[] = [];
+        if (p) {
+            linkableValues = Session.getInstance().getLinkableValues(p.paramDefinition);
+            // join form names to ease usage
+            for (let i = 0; i < linkableValues.length; i++) {
+                const lv = linkableValues[i];
+                for (const f of this._formulaires) {
+                    if (f.currentNub) {
+                        if (f.currentNub.uid === lv.nub.uid) {
+                            lv.meta["formTitle"] = f.calculatorName;
+                        } else {
+                            // child structures ?
+                            if (f.currentNub instanceof ParallelStructure) {
+                                for (const s of f.currentNub.structures) {
+                                    if (s.uid === lv.nub.uid) {
+                                        lv.meta["formTitle"] = f.calculatorName;
+                                    }
+                                }
+                            }
                         }
                     }
-                    break;
-
-                default:
-                    throw new Error(`session file : invalid key '${ks}' in session object`);
-            }
-        }
-    } */
-
-    /**
-     * @returns liste des valeurs liables à un paramètre sous la forme d'un tableau d'objets
-     * {"param":<paramètre lié>, "nub":<Nub d'origine du paramètre lié>, "formTitle":<nom du module de calcul liée au nub>}
-     * @param p paramètre qui sert de clé de recherche des paramètres liables
-     */
-    public getLinkableValues(p: NgParameter): any[] {
-        const res: any[] = [];
-        if (p !== undefined) {
-            for (const f of this._formulaires) {
-                // nub associé au formulaire
-                const sn = f.currentNub;
-                try {
-                    // on vérifie que le paramètre en entrée appartient au nub
-                    const np = sn.getParameter(p.symbol);
-                    // si oui, on demande à exclure des valeurs retournées le résultat du même nom que le paramètre
-                    const exclude = np !== undefined ? p.paramDefinition.uid === np.uid : false;
-                    // valeurs liables
-                    const ps = sn.getLinkableValues(p.paramDefinition, undefined, exclude);
-                    for (const npp of ps) {
-                        npp["formTitle"] = f.calculatorName;
-                        res.push(npp);
-                    }
-                } catch (e) {
-                    //  p.symbol n'existe pas dans le nub testé
                 }
             }
         }
-        return res;
+        linkableValues = this.filterLinkableValues(linkableValues);
+        return linkableValues;
     }
 
     /**
@@ -626,16 +573,12 @@ export class FormulaireService extends Observable {
      * @param values valeurs liables (modifié par la méthode)
      */
     public filterLinkableValues(values: any[]): any[] {
-        // suppression des paramètres non affichés
-
         for (let i = values.length - 1; i >= 0; i--) {
             const v = values[i].value;
             if (v instanceof ParamDefinition) {
                 // pour chaque paramètre...
                 const prm: ParamDefinition = v;
-
                 const parentForm: FormulaireDefinition = this.getParamdefParentForm(prm);
-
                 // ... on cherche s'il est affiché dans son parent
                 let found = false;
                 if (parentForm !== undefined) {
@@ -648,13 +591,37 @@ export class FormulaireService extends Observable {
                         }
                     }
                 }
-
                 if (!found) {
                     values.splice(i, 1);
                 }
             }
         }
-
         return values;
     }
+
+    /**
+     * Resets the results of all forms depending on the given form "f"
+     * @param f
+     */
+    public resetAllDependingFormsResults(f: FormulaireDefinition, visited: string[] = [], notify: boolean = true) {
+        const dependingNubs = Session.getInstance().getDependingNubs(f.currentNub.uid);
+        for (const dn of dependingNubs) {
+            if (! visited.includes(dn.uid)) {
+                const form = this.getFormulaireFromNubId(dn.uid);
+                if (form) {
+                    const hadResults = form.hasResults;
+                    // form might not have a result, but still have another form depending on it !
+                    form.resetResults(visited);
+                    if (hadResults) {
+                        if (notify) {
+                            this.notificationsService.notify(
+                                this.intlService.localizeText("INFO_SNACKBAR_RESULTS_INVALIDATED") + " " + form.calculatorName,
+                                1500
+                            );
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/src/app/services/notifications/notifications.service.ts b/src/app/services/notifications/notifications.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9caf0d7ffb1a7832f671261980764a7e30a29046
--- /dev/null
+++ b/src/app/services/notifications/notifications.service.ts
@@ -0,0 +1,49 @@
+import { Injectable } from "@angular/core";
+import { MatSnackBar } from "@angular/material";
+
+/**
+ * Displays a notifications queue as consecutive snackbars
+ */
+@Injectable()
+export class NotificationsService {
+
+    /** FIFO queue for notifications */
+    private notifications: any[];
+
+    private isOpen: boolean;
+
+    public constructor(
+        private snackBar: MatSnackBar
+    ) {
+        this.notifications = [];
+        this.isOpen = false;
+    }
+
+    /** Push a notification and display it as soon as possible */
+    public notify(message: string, duration: number, action: string = "OK") {
+        this.notifications.push({
+            message: message,
+            duration: duration,
+            action: action
+        });
+        this.show();
+    }
+
+    /** Show all messages in the FIFO queue one after another */
+    public show() {
+        if (! this.isOpen) {
+            // process next notification
+            if (this.notifications.length > 0) {
+                const notif = this.notifications.shift();
+                this.isOpen = true;
+                const ref = this.snackBar.open(notif.message, notif.action, {
+                    duration: notif.duration
+                });
+                ref.afterDismissed().subscribe(() => {
+                    this.isOpen = false;
+                    this.show();
+                });
+            }
+        }
+    }
+}
diff --git a/src/app/services/service-factory.ts b/src/app/services/service-factory.ts
index 0627bdf99a223a0bf9b7712945dcdb7849075c1a..838a00cfc468b4c98954a00f73d4b7d9cc6b9c37 100644
--- a/src/app/services/service-factory.ts
+++ b/src/app/services/service-factory.ts
@@ -2,6 +2,7 @@ import { ApplicationSetupService } from "./app-setup/app-setup.service";
 import { FormulaireService } from "./formulaire/formulaire.service";
 import { I18nService } from "./internationalisation/internationalisation.service";
 import { HttpService } from "./http/http.service";
+import { NotificationsService } from "./notifications/notifications.service";
 
 export class ServiceFactory {
     private static _instance: ServiceFactory; // instance pour le pattern singleton
@@ -16,6 +17,8 @@ export class ServiceFactory {
 
     public httpService: HttpService;
 
+    public notificationsService: NotificationsService;
+
     public static get instance() {
         if (ServiceFactory._instance === undefined) {
             ServiceFactory._instance = new ServiceFactory();
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index bed80e3329ae40f6b823691ce9dc656df5862bdb..66c9f9d6b2cad2a4dd1c6b03f91eefc3cb7ef127 100644
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -22,6 +22,7 @@
     "ERROR_PARAMDEF_VALUE_POS": "value %value% of '%symbol%' parameter is invalid (cannot be <=0)",
     "ERROR_PARAMDEF_VALUE_POSNULL": "value %value% of '%symbol%' parameter is invalid (cannot be <0)",
     "ERROR_PARAMDEF_VALUE_UNDEFINED": "value of '%symbol%' parameter is undefined",
+    "ERROR_PARAMDEF_LINKED_VALUE_UNDEFINED": "value of '%symbol%' linked parameter is undefined",
     "ERROR_PARAMDOMAIN_INTERVAL_BOUNDS": "invalid %minValue%/%maxValue% min/max boundaries for 'interval' parameter definition domain",
     "ERROR_PARAMDOMAIN_INVALID": "parameter '%symbol%: non supported '%domain%' definition domain",
     "ERROR_REMOUS_PAS_CALCUL_DEPUIS_AMONT": "Upstream boundary condition < Critical elevation: no possible calculation from upstream",
@@ -43,6 +44,7 @@
     "INFO_CLOISONS_TITRE_COURT": "Cross walls",
     "INFO_CLOSE_DIALOGUE_TEXT": "Warning ! Parameters and results will be lost. Really close?",
     "INFO_CLOSE_DIALOGUE_TITRE": "Please confirm",
+    "INFO_CLOSE_DIALOGUE_DEPENDING_MODULES": "The following modules depend on the one you are closing:",
     "INFO_CONDUITEDISTRIBUTRICE_TITRE": "Distributor pipe",
     "INFO_CONDUITEDISTRIBUTRICE_TITRE_COURT": "D. pipe",
     "INFO_COURBEREMOUS_TITRE": "Backwater curves",
@@ -60,6 +62,7 @@
     "INFO_EXTRARES_ENUM_STRUCTUREFLOWREGIME_2": "Submerged",
     "INFO_EXTRARES_ENUM_STRUCTUREFLOWREGIME_3": "Zero flow",
     "INFO_DIALOG_COMPUTED_VALUE_TITLE": "Edit initial value",
+    "INFO_DIALOG_FIX_MISSING_DEPENDENCIES": "Fix missing dependencies",
     "INFO_DIALOG_LOAD_SESSION_FILENAME": "Choose a file",
     "INFO_DIALOG_LOAD_SESSION_TITLE": "Load calculator modules",
     "INFO_DIALOG_SAVE_SESSION_FILENAME": "File name",
@@ -124,6 +127,11 @@
     "INFO_LIB_ZDV": "Crest weir elevation or gate base",
     "INFO_LIB_ZRAM": "Upstream apron elevation",
     "INFO_LIB_ZT": "Triangle top elevation",
+    "INFO_LINKED_VALUE_DEVICE": "%s (%s, device %s)",
+    "INFO_LINKED_VALUE_RESULT": "%s (result of %s)",
+    "INFO_LINKED_VALUE_DEVICE_RESULT": "%s (result of %s, device %s)",
+    "INFO_LINKED_VALUE_EXTRA_RESULT": "%s (%s, extra result)",
+    "INFO_LINKED_VALUE_EXTRA_RESULT_OF": "%s (%s, extra result of %s)",
     "INFO_MACRORUGO_TITRE": "Rock-ramp fishpasses",
     "INFO_MACRORUGO_TITRE_COURT": "RR fishpasses",
     "INFO_MENU_HELP_TITLE": "Help",
@@ -156,6 +164,7 @@
     "INFO_PARAMFIELD_GRAPH_TYPE_HISTOGRAM": "Histogram",
     "INFO_PARAMFIELD_GRAPH_SELECT_X_AXIS": "Variable for X axis",
     "INFO_PARAMFIELD_GRAPH_SELECT_Y_AXIS": "Variable for Y axis",
+    "INFO_PARAMFIELD_CALCULATION_FAILED": "Calculation failed",
     "INFO_PARAMFIELD_IN_CALCULATION": "In calculation",
     "INFO_PARAMFIELD_IN_CALCULATION_INITIAL_VALUE": "initial value",
     "INFO_PARAMFIELD_PARAMCALCULER": "Calculate",
@@ -194,12 +203,15 @@
     "INFO_REMOUS_LARGEUR_BERGE": "Width at embankment level = %B% m",
     "INFO_REMOUS_RESSAUT_DEHORS": "Hydraulic jump detected %sens% abscissa %x% m",
     "INFO_REMOUS_RESSAUT_HYDRO": "Hydraulic jump detected between abscissa %xmin% and %xmax% m",
+    "INFO_REQUIRES": "requires",
     "INFO_SECTIONPARAMETREE_TITRE": "Parametric section",
     "INFO_SECTIONPARAMETREE_TITRE_COURT": "Param. section",
     "INFO_SETUP_NEWTON_MAX_ITER": "Newton iteration limit",
     "INFO_SETUP_PRECISION_AFFICHAGE": "Display accuracy",
     "INFO_SETUP_PRECISION_CALCUL": "Computation accuracy",
     "INFO_SETUP_TITLE": "Application setup",
+    "INFO_SNACKBAR_RESULTS_CALCULATED": "Results calculated for",
+    "INFO_SNACKBAR_RESULTS_INVALIDATED": "Results invalidated for",
     "INFO_SNACKBAR_SETTINGS_SAVED": "Settings saved on this device",
     "INFO_SNACKBAR_DEFAULT_SETTINGS_RESTORED": "Default settings restored",
     "INFO_THEME_CREDITS": "Credit",
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index ea0bae031beaa5d1c775bd30ed78d428f35be3fa..c8d611b36384e93c205ce550552e61c34df6bfc1 100644
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -22,6 +22,7 @@
     "ERROR_PARAMDEF_VALUE_POS": "La valeur %value% du paramètre '%symbol%' est incorrecte (<=0)",
     "ERROR_PARAMDEF_VALUE_POSNULL": "La valeur %value% du paramètre '%symbol%' est incorrecte (<0)",
     "ERROR_PARAMDEF_VALUE_UNDEFINED": "La valeur du paramètre %symbol% n'est pas définie",
+    "ERROR_PARAMDEF_LINKED_VALUE_UNDEFINED": "La valeur du paramètre lié %symbol% n'est pas définie",
     "ERROR_PARAMDOMAIN_INTERVAL_BOUNDS": "Les bornes (%minValue%/%maxValue%) de l'intervalle sont incorrectes",
     "ERROR_PARAMDOMAIN_INVALID": "Paramètre '%symbol%'&nbsp;: le domaine de définition '%domain%' est incorrect",
     "ERROR_REMOUS_PAS_CALCUL_DEPUIS_AMONT": "Condition limite amont > Hauteur critique&nbsp;: pas de calcul possible depuis l'amont",
@@ -43,6 +44,7 @@
     "INFO_CLOISONS_TITRE_COURT": "Cloisons",
     "INFO_CLOSE_DIALOGUE_TEXT": "Attention&nbsp;! Les paramètres et résultats du module de calcul seront perdus. Vraiment fermer&nbsp;?",
     "INFO_CLOSE_DIALOGUE_TITRE": "Confirmer la fermeture",
+    "INFO_CLOSE_DIALOGUE_DEPENDING_MODULES": "Les modules suivants dépendent de celui que vous êtes en train de fermer :",
     "INFO_CONDUITEDISTRIBUTRICE_TITRE": "Conduite distributrice",
     "INFO_CONDUITEDISTRIBUTRICE_TITRE_COURT": "Conduite distri.",
     "INFO_COURBEREMOUS_TITRE": "Courbes de remous",
@@ -60,6 +62,7 @@
     "INFO_EXTRARES_ENUM_STRUCTUREFLOWREGIME_2": "Noyé",
     "INFO_EXTRARES_ENUM_STRUCTUREFLOWREGIME_3": "Débit nul",
     "INFO_DIALOG_COMPUTED_VALUE_TITLE": "Modifier la valeur initiale",
+    "INFO_DIALOG_FIX_MISSING_DEPENDENCIES": "Résoudre les dépendances",
     "INFO_DIALOG_LOAD_SESSION_FILENAME": "Choisir un fichier",
     "INFO_DIALOG_LOAD_SESSION_TITLE": "Charger des modules de calcul",
     "INFO_DIALOG_SAVE_SESSION_FILENAME": "Nom de fichier",
@@ -124,6 +127,11 @@
     "INFO_LIB_ZDV": "Cote de la crête du déversoir ou du radier de la vanne",
     "INFO_LIB_ZRAM": "Cote du radier amont",
     "INFO_LIB_ZT": "Cote haute du triangle",
+    "INFO_LINKED_VALUE_DEVICE": "%s (%s, ouvrage %s)",
+    "INFO_LINKED_VALUE_RESULT": "%s (résultat de %s)",
+    "INFO_LINKED_VALUE_DEVICE_RESULT": "%s (résultat de %s, ouvrage %s)",
+    "INFO_LINKED_VALUE_EXTRA_RESULT": "%s (%s, résultat complémentaire)",
+    "INFO_LINKED_VALUE_EXTRA_RESULT_OF": "%s (%s, résultat complémentaire de %s)",
     "INFO_MENU_HELP_TITLE": "Aide",
     "INFO_MENU_LOAD_SESSION_TITLE": "Charger une session",
     "INFO_MENU_SAVE_SESSION_TITLE": "Enregistrer la session",
@@ -156,6 +164,7 @@
     "INFO_PARAMFIELD_GRAPH_TYPE_HISTOGRAM": "Histogramme",
     "INFO_PARAMFIELD_GRAPH_SELECT_X_AXIS": "Variable en abscisse",
     "INFO_PARAMFIELD_GRAPH_SELECT_Y_AXIS": "Variable en ordonnée",
+    "INFO_PARAMFIELD_CALCULATION_FAILED": "Échec du calcul",
     "INFO_PARAMFIELD_IN_CALCULATION": "En calcul",
     "INFO_PARAMFIELD_IN_CALCULATION_INITIAL_VALUE": "valeur initiale",
     "INFO_PARAMFIELD_PARAMCALCULER": "calculer",
@@ -194,12 +203,15 @@
     "INFO_REMOUS_LARGEUR_BERGE": "Largeur au niveau des berges = %B% m",
     "INFO_REMOUS_RESSAUT_DEHORS": "Ressaut hydraulique détecté à l'%sens% de l'abscisse %x% m",
     "INFO_REMOUS_RESSAUT_HYDRO": "Ressaut hydraulique détecté entre les abscisses %xmin% et %xmax% m",
+    "INFO_REQUIRES": "dépend de",
     "INFO_SECTIONPARAMETREE_TITRE": "Section paramétrée",
     "INFO_SECTIONPARAMETREE_TITRE_COURT": "Sec. param.",
     "INFO_SETUP_NEWTON_MAX_ITER": "Newton : nombre d'itérations maximum",
     "INFO_SETUP_PRECISION_AFFICHAGE": "Précision d'affichage",
     "INFO_SETUP_PRECISION_CALCUL": "Précision de calcul",
     "INFO_SETUP_TITLE": "Paramètres de l'application",
+    "INFO_SNACKBAR_RESULTS_CALCULATED": "Résultats calculés pour",
+    "INFO_SNACKBAR_RESULTS_INVALIDATED": "Résultats invalidés pour",
     "INFO_SNACKBAR_SETTINGS_SAVED": "Paramètres enregistrés sur cet appareil",
     "INFO_SNACKBAR_DEFAULT_SETTINGS_RESTORED": "Paramètres par défaut restaurés",
     "INFO_THEME_CREDITS": "Crédit",