From 2f8e8051ef53be1e07c91e45087afee2d008a7dd Mon Sep 17 00:00:00 2001
From: "mathias.chouet" <mathias.chouet@irstea.fr>
Date: Thu, 23 May 2019 15:53:28 +0200
Subject: [PATCH] work on #62adapted CalcSerie() to multiple varying parameters

removed valueMode constraint on max. 1 variated parameter
added jasmine tests
---
 spec/param/param_modes.spec.ts    |   8 +-
 spec/param/param_multivar.spec.ts | 296 ++++++++++++++++++++++++++++++
 src/nub.ts                        | 209 ++++++++++++---------
 src/param/param-definition.ts     | 136 +++++---------
 src/param/param-values.ts         |  20 ++
 src/section/section_parametree.ts |  65 +------
 src/structure/structure.ts        |   4 +-
 7 files changed, 486 insertions(+), 252 deletions(-)
 create mode 100644 spec/param/param_multivar.spec.ts

diff --git a/spec/param/param_modes.spec.ts b/spec/param/param_modes.spec.ts
index b01100c4..bf3ecb6b 100644
--- a/spec/param/param_modes.spec.ts
+++ b/spec/param/param_modes.spec.ts
@@ -141,17 +141,12 @@ function createEnv() {
 function checkConsistency(nubToCheck: Nub) {
     expect(nubToCheck.calculatedParam).toBeDefined();
     let calcCount = 0;
-    let varCount = 0;
     for (const p of nubToCheck.parameterIterator) {
         if (p.valueMode === ParamValueMode.CALCUL) {
             calcCount++;
         }
-        if (p.valueMode === ParamValueMode.MINMAX || p.valueMode === ParamValueMode.LISTE) {
-            varCount++;
-        }
     }
     expect(calcCount).toBe(1);
-    expect(varCount).toBeLessThanOrEqual(1);
 
     nubToCheck.CalcSerie();
     expect(nubToCheck.result).toBeDefined();
@@ -232,8 +227,7 @@ describe("cohérence des modes de paramètres : ", () => {
         testModesPermutations(nub2);
     });
 
-    // @TODO reenable after fixing jalhyd#74
-    xit("ouvrages en parallèle : Cloisons", () => {
+    it("ouvrages en parallèle : Cloisons", () => {
         createEnv();
         testModesPermutations(nub3);
     });
diff --git a/spec/param/param_multivar.spec.ts b/spec/param/param_multivar.spec.ts
new file mode 100644
index 00000000..f10a1038
--- /dev/null
+++ b/spec/param/param_multivar.spec.ts
@@ -0,0 +1,296 @@
+import { CreateStructure, ExtensionStrategy, LoiDebit, ParallelStructure, ParallelStructureParams,
+         ParamValueMode, Structure} from "../../src/index";
+import { RectangularStructureParams } from "../../src/structure/structure_cem88d";
+
+let prms1: ParallelStructureParams;
+let nub1: ParallelStructure;
+let struct1: Structure;
+let prmsStruct: RectangularStructureParams;
+
+function createSingleNubEnv() {
+    prms1 = new ParallelStructureParams(0.5, 102, 101.5);
+    nub1 = new ParallelStructure(prms1);
+    struct1 = CreateStructure(LoiDebit.Cunge80, nub1);
+    prmsStruct = struct1.prms as RectangularStructureParams;
+    nub1.addChild(struct1);
+}
+
+function createLinkedNubEnv() {
+    // TODO
+}
+
+/**
+ * Generates a random-length list of numbers close
+ * @param refVal reference value
+ * @param minSize minimum size of the list to be returned
+ * @param maxSize maximum size of the list to be returned
+ * @param maxDev maximum deviation, so that generated numbers
+ *               value v verifies (refVal - maxDev) < v < (refVal + maxDev)
+ * @param sort if true, list will be ordered
+ */
+function randomList(
+    refVal: number, minSize: number = 1, maxSize: number = 20, maxDev: number = 3, sort: boolean = true
+): number[] {
+    let list: number[] = [];
+    const size = minSize + Math.floor(Math.random() * ((maxSize - minSize) + 1)); // between minSize and maxSize
+    for (let i = 0; i < size; i++) {
+        let dev = (Math.random() * (2 * maxDev)) - (maxDev + 1); // between -maxDev and maxDev
+        dev = Number(Math.round(Number(dev + "e3")) + "e-3"); // trick to properly round to 3 decimals
+        list.push(refVal + dev);
+    }
+    if (sort) {
+        list = list.sort((a, b) => a - b);
+    }
+    return list;
+}
+
+describe("multiple variated parameters - ", () => {
+
+    describe("on the same Nub, with REPEAT_LAST strategy - ", () => {
+
+        it("test 1 : minmax < list", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.5); // 5 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6 ]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(9);
+        });
+
+        it("test 2 : minmax < minmax", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.5); // 5 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            prmsStruct.L.setValues(1.89, 2.1, 0.03); // 8 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.MINMAX);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(8);
+        });
+
+        it("test 3 : list < minmax", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.1); // 21 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6 ]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(21);
+        });
+
+        it("test 4 : list < list", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues([ 100, 101, 102 ]); // 3 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6 ]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.LISTE);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(9);
+        });
+
+        it("test 5 : list = list", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues([ 100, 100.3, 100.7, 101, 101.3, 101.7, 102, 102.3, 115 ]); // 9 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6 ]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.LISTE);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(9);
+        });
+
+        it("test 6 : list = minmax", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.5); // 5 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            prmsStruct.L.setValues(1.89, 2.15, 0.06); // 5 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.MINMAX);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(5);
+        });
+
+        it("test 7 : all parameters varying with random lengths", () => {
+            createSingleNubEnv();
+            let longest = 0;
+            let sparedParam = null;
+            // set all parameters to LIST mode with random length (except the first that has to stay in CALC mode)
+            for (const p of nub1.parameterIterator) {
+                if (p.visible) {
+                    if (! sparedParam) {
+                        sparedParam = p;
+                    } else {
+                        const rl = randomList(p.singleValue);
+                        longest = Math.max(longest, rl.length);
+                        p.setValues(rl);
+                        p.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
+                    }
+                }
+            }
+            // check that all parameters are varying
+            for (const p of nub1.parameterIterator) {
+                if (p.visible && p !== sparedParam) {
+                    expect(p.hasMultipleValues).toBe(true);
+                }
+            }
+            // check that calculation works and length of result is length of the longest values list
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(longest);
+        });
+    });
+
+    describe("on the same Nub, with RECYCLE strategy - ", () => {
+
+        it("test 1 : minmax < list", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.5); // 5 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.RECYCLE;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.RECYCLE;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(9);
+        });
+
+        it("test 2 : minmax < minmax", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.5); // 5 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.RECYCLE;
+            prmsStruct.L.setValues(1.89, 2.1, 0.03); // 8 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.RECYCLE;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.MINMAX);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(8);
+        });
+
+        it("test 3 : list < minmax", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.1); // 21 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.RECYCLE;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6 ]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.RECYCLE;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(21);
+        });
+
+        it("test 4 : list < list", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues([ 100, 101, 102 ]); // 3 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.RECYCLE;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6 ]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.RECYCLE;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.LISTE);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(9);
+        });
+
+        it("test 5 : list = list", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues([ 100, 100.3, 100.7, 101, 101.3, 101.7, 102, 102.3, 115 ]); // 9 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.RECYCLE;
+            prmsStruct.L.setValues([ 1.89, 1.91, 1.99, 2, 1.7, 2.1, 2.18, 2.23, 2.6 ]); // 9 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.RECYCLE;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.LISTE);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.LISTE);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(9);
+        });
+
+        it("test 6 : list = minmax", () => {
+            createSingleNubEnv();
+            prms1.Z1.setValues(100, 102, 0.5); // 5 values
+            prms1.Z1.extensionStrategy = ExtensionStrategy.RECYCLE;
+            prmsStruct.L.setValues(1.89, 2.15, 0.06); // 5 values
+            prmsStruct.L.extensionStrategy = ExtensionStrategy.RECYCLE;
+            // check that both parameters are varying
+            expect(prms1.Z1.valueMode).toBe(ParamValueMode.MINMAX);
+            expect(prmsStruct.L.valueMode).toBe(ParamValueMode.MINMAX);
+            // check that calculation works
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(5);
+        });
+
+        it("test 7 : all parameters varying with random lengths", () => {
+            createSingleNubEnv();
+            let longest = 0;
+            let sparedParam = null;
+            // set all parameters to LIST mode with random length (except the first that has to stay in CALC mode)
+            for (const p of nub1.parameterIterator) {
+                if (p.visible) {
+                    if (! sparedParam) {
+                        sparedParam = p;
+                    } else {
+                        const rl = randomList(p.singleValue);
+                        longest = Math.max(longest, rl.length);
+                        p.setValues(rl);
+                        p.extensionStrategy = ExtensionStrategy.RECYCLE;
+                    }
+                }
+            }
+            // check that all parameters are varying
+            for (const p of nub1.parameterIterator) {
+                if (p.visible && p !== sparedParam) {
+                    expect(p.hasMultipleValues).toBe(true);
+                }
+            }
+            // check that calculation works and length of result is length of the longest values list
+            const res = nub1.CalcSerie();
+            expect(res).toBeDefined();
+            expect(res.resultElements.length).toBe(longest);
+        });
+    });
+
+    describe("on different linked Nubs - ", () => {
+        // TODO
+    });
+});
diff --git a/src/nub.ts b/src/nub.ts
index b0877e6d..22aa2f40 100644
--- a/src/nub.ts
+++ b/src/nub.ts
@@ -3,7 +3,7 @@ import { Dichotomie } from "./dichotomie";
 import { acSection, IParamDefinitionIterator, Pab, ParamDefinition, ParamsEquation,
          ParamsEquationArrayIterator, Session, Structure } from "./index";
 import { LinkedValue } from "./linked-value";
-import { ParamCalculability } from "./param/param-definition";
+import { ExtensionStrategy, ParamCalculability } from "./param/param-definition";
 import { ParamValueMode } from "./param/param-value-mode";
 import { ParamValues } from "./param/param-values";
 import { Props } from "./props";
@@ -146,8 +146,22 @@ export abstract class Nub extends ComputeNode implements IObservable {
     }
 
     /**
-     * Resets the calculated parameter to the default one if it is in SINGLE
-     * mode, or else to the first calculable parameter other than requirer.
+     * Finds the previous calculated parameter and sets its mode to SINGLE
+     */
+    public unsetCalculatedParam(except: ParamDefinition) {
+        for (const p of this.parameterIterator) {
+            if (p !== except) {
+                p.setValueMode(ParamValueMode.SINGLE, false);
+            }
+        }
+    }
+
+    /**
+     * Tries to reset the calculated parameter, successively, to :
+     *  - the default one if it is in SINGLE mode
+     *  - the first SINGLE calculable parameter other than requirer
+     *  - the first MINMAX/LISTE calculable parameter other than requirer
+     * 
      * If no default calculated parameter is defined, does nothing.
      */
     public resetDefaultCalculatedParam(requirer?: ParamDefinition) {
@@ -155,15 +169,24 @@ export abstract class Nub extends ComputeNode implements IObservable {
             // if default calculated param is not eligible to CALC mode
             if (
                 requirer === this._defaultCalculatedParam
-                || this._defaultCalculatedParam.valueMode !== ParamValueMode.SINGLE
+                || ! [
+                        ParamValueMode.SINGLE,
+                        ParamValueMode.MINMAX,
+                        ParamValueMode.LISTE
+                    ].includes(this._defaultCalculatedParam.valueMode
+                )
             ) {
                 // first SINGLE calculable parameter if any
-                const newCalculatedParam = this.findFirstSingleParameter(requirer);
+                let newCalculatedParam = this.findFirstSingleParameter(requirer);
+                if (! newCalculatedParam) {
+                    // first MIMAX/LISTE calculable parameter if any
+                    newCalculatedParam = this.findFirstVariableParameter(requirer);
+                }
                 if (newCalculatedParam) {
                     this.calculatedParam = newCalculatedParam;
                 } else {
                     // @TODO throws when a new linkable nub is added, and all parameters are already linked !
-                    throw Error("resetDefaultCalculatedParam : could not find any SINGLE parameter");
+                    throw Error("resetDefaultCalculatedParam : could not find any SINGLE/MINMAX/LISTE parameter");
                 }
             } else {
                 // default one
@@ -195,42 +218,23 @@ export abstract class Nub extends ComputeNode implements IObservable {
     }
 
     /**
-     * Resets all parameters to SINGLE mode, except sourceParam (the one that asked
-     * for the change) and parameters in LINK mode.
-     * If exceptMode is defined, parameters in this mode won't be affected either.
-     *
-     * Used to ensure that
-     *  - exactly one parameter is in CALCUL mode
-     *  - at most one parameter variates (MIMAX / LISTE mode)
+     * Returns the first visible calculable parameter other than "Pr",
+     * and other than otherThan, that is set to MINMAX or LISTE mode
+     * (there might be none)
      */
-    public ensureParametersConsistency(sourceParam: ParamDefinition, exceptMode: ParamValueMode|ParamValueMode[] = []) {
-        if (! Array.isArray(exceptMode)) {
-            exceptMode = [ exceptMode ];
-        }
-        // console.log(`nub ${this.uid} (${this.constructor.name})`);
-        // console.log(`(calculated parameter: ${this.calculatedParam.symbol})`);
+    public findFirstVariableParameter(otherThan?: ParamDefinition) {
         for (const p of this.parameterIterator) {
             if (
-                p !== sourceParam
-                && ! exceptMode.includes(p.valueMode)
-                && (
-                    // don't touch linked parameters
-                    p.valueMode !== ParamValueMode.LINK
-                    || ( // unless they are linked to multiple values…
-                        p.isReferenceDefined()
-                        && p.hasMultipleValues
-                        // …and we're not skipping multiple values
-                        && ! exceptMode.includes(ParamValueMode.MINMAX)
-                        && ! exceptMode.includes(ParamValueMode.LISTE)
-                    )
-                )
-                && p.valueMode !== ParamValueMode.SINGLE
+                [ ParamCalculability.EQUATION, ParamCalculability.DICHO ].includes(p.calculability)
+                && p.symbol !== "Pr"
+                && p.visible
+                && p !== otherThan
+                && [ ParamValueMode.SINGLE, ParamValueMode.MINMAX, ParamValueMode.LISTE ].includes(p.valueMode)
             ) {
-                // console.log(">> resetting", p.symbol);
-                // "false" prevents infinite loop when managing CALC params
-                p.setValueMode(ParamValueMode.SINGLE, false);
+                return p;
             }
         }
+        return undefined;
     }
 
     /**
@@ -304,42 +308,37 @@ export abstract class Nub extends ComputeNode implements IObservable {
      * @param sDonnee éventuel symbole / paire symbole-uid du paramètre à calculer
      */
     public CalcSerie(rInit?: number, sDonnee?: any): Result {
-        // instance de ParamValues utilisée pour le paramètre varié (qui peut être un paramètre référencé (importé))
-        let variatedParam: ParamDefinition;
-        let variatedValues: ParamValues;
+        // variated parameters caracteristics
+        const variated: Array<{ param: ParamDefinition, values: ParamValues, extendedValues: number[] }> = [];
 
+        // prepare calculation
         this.progress = 0;
         this.triggerChainCalculation();
-
         this.copySingleValuesToSandboxValues();
 
         // check which values are variating, if any
         for (const p of this.parameterIterator) {
-            switch (p.valueMode) {
-                case ParamValueMode.SINGLE:
-                case ParamValueMode.CALCUL:
-                    break;
-
-                case ParamValueMode.LISTE:
-                case ParamValueMode.MINMAX:
-                    variatedParam = this.setVariatedValues(p, variatedParam);
-                    break;
-
-                case ParamValueMode.LINK:
-                    if (
-                        p.isReferenceDefined()
-                        && p.referencedValue.hasMultipleValues()
-                    ) {
-                        variatedParam = this.setVariatedValues(p, variatedParam);
-                    }
-                    break;
-
-                default:
-                    // tslint:disable-next-line:max-line-length
-                    throw new Error(`CalcSerie() : valeur de ParamValueMode ${ParamValueMode[p.valueMode]} non prise en charge`);
+            if (
+                p.valueMode === ParamValueMode.LISTE
+                || p.valueMode === ParamValueMode.MINMAX
+                || (
+                    p.valueMode === ParamValueMode.LINK
+                    && p.isReferenceDefined()
+                    && p.referencedValue.hasMultipleValues()
+                )
+            ) {
+                variated.push({
+                    param: p,
+                    // extract variated values from variated Parameters
+                    // (in LINK mode, proxies to target data)
+                    values: p.paramValues,
+                    // values list by default, will be extended later
+                    extendedValues: p.paramValues.getInferredValuesList()
+                });
             }
         }
 
+        // find calculated parameter
         let computedSymbol: any;
         if (sDonnee) {
             computedSymbol = sDonnee;
@@ -354,31 +353,74 @@ export abstract class Nub extends ComputeNode implements IObservable {
             rInit = this.calculatedParam.v;
         }
 
-        if (variatedParam === undefined) {
-            this._result = this.Calc(computedSymbol, rInit); // résultat dans this._result
-            // update progress to 100%
+        if (variated.length === 0) { // no parameter is varying
+            this._result = this.doCalc(computedSymbol, rInit);
             this.progress = 100;
 
-        } else {
-            // extract variated values from variated Parameter
-            // (in LINK mode, proxies to target data)
-            variatedValues = variatedParam.paramValues;
+        } else { // at least one parameter is varying
+            // find longest series
+            let longest = 0;
+            for (let i = 0; i < variated.length; i++) {
+                if (variated[i].values.valuesIterator.count() > variated[longest].values.valuesIterator.count()) {
+                    longest = i;
+                }
+            }
+
+            // for all variating parameters other than the longest one,
+            // complete the series depending on the strategy (recycle / extend)
+            const size = variated[longest].values.valuesIterator.count();
+            for (let i = 0; i < variated.length; i++) {
+                if (i !== longest) {
+                    const vp = variated[i].param;
+                    const vxv = variated[i].extendedValues;
+                    // if sizes differ
+                    if (vxv.length < size) {
+                        const newValues = [...vxv]; // array copy
+                        // extend values
+                        switch (vp.extensionStrategy) {
+                            case ExtensionStrategy.REPEAT_LAST:
+                                while (newValues.length < size) {
+                                    newValues.push(vxv[vxv.length - 1]);
+                                }
+                                break;
+
+                            case ExtensionStrategy.RECYCLE:
+                                let j = 0;
+                                while (newValues.length < size) {
+                                    newValues.push(vxv[j]);
+                                    j ++;
+                                    if (j === vxv.length) {
+                                        j = 0;
+                                    }
+                                }
+                                break;
+                        }
+                        // store extended values
+                        variated[i].extendedValues = newValues;
+                    }
+                }
+            }
 
             // grant the remaining percentage of the progressbar to local calculation
             // (should be 50% if any chain calculation occurred, 100% otherwise)
             let progressStep;
             const remainingProgress = 100 - this.progress;
-            const nbValues = variatedValues.valuesIterator.count();
+            const nbValues = variated[longest].extendedValues.length;
             progressStep = remainingProgress / nbValues;
 
             const res = new Result(undefined, this);
 
-            variatedValues.initValuesIterator(false);
-            while (variatedValues.hasNext) {
-                const currentIteratorValue = variatedValues.next();
-                variatedParam.v = currentIteratorValue.value; // copy to local sandbox value
+            // iterate over longest series @TODO iterate over extendedValues directly ?
+            variated[longest].values.initValuesIterator(false);
+            let index = 0;
+            while (variated[longest].values.hasNext) {
+                variated[longest].values.next(); // move on
+                // get next value for all variating parameters
+                for (const v of variated) {
+                    v.param.v = v.extendedValues[index]; // copy to local sandbox value
+                }
 
-                this.Calc(computedSymbol, rInit);  // résultat dans this._result
+                this.doCalc(computedSymbol, rInit);  // résultat dans this._result
                 if (this._result.ok) {
                     res.addResultElement(this._result.resultElement);
                     res.addLog(this._result.log);
@@ -387,6 +429,7 @@ export abstract class Nub extends ComputeNode implements IObservable {
                 res.globalLog.addLog(this._result.globalLog);
                 // update progress
                 this.progress += progressStep;
+                index ++;
             }
             this._result = res;
             // round progress to 100%
@@ -1098,6 +1141,10 @@ export abstract class Nub extends ComputeNode implements IObservable {
         this._observable.notifyObservers(data, sender);
     }
 
+    protected doCalc(computedSymbol?: any, rInit?: number) {
+        return this.Calc(computedSymbol, rInit);
+    }
+
     /**
      * Returns values of parameters and the result of the calculation for the calculated parameter
      * @param sVarCalc Symbol of the calculated param
@@ -1147,18 +1194,6 @@ export abstract class Nub extends ComputeNode implements IObservable {
         return Nub.prototype.Calc.call(this, sVarCalc, rInit);
     }
 
-    /**
-     * Sets the variated values and warns if there were already some
-     */
-    protected setVariatedValues(newValues: ParamDefinition, oldValues: ParamDefinition): ParamDefinition {
-        if (oldValues === undefined) {
-            return newValues;
-        } else {
-            // tslint:disable-next-line:max-line-length
-            throw new Error(`CalcSerie() : Paramètres à varier redondant : ${newValues.name}`);
-        }
-    }
-
     /**
      * Used by GUI to update results display
      */
diff --git a/src/param/param-definition.ts b/src/param/param-definition.ts
index 263440ea..ee48deda 100644
--- a/src/param/param-definition.ts
+++ b/src/param/param-definition.ts
@@ -43,6 +43,19 @@ export enum ParamFamily {
     // SPEEDS // vitesses, seulement des résultats
 }
 
+/**
+ * Strategy to apply when multiple parameters are variating, and
+ * values series have to be extended for sizes to match
+ */
+export enum ExtensionStrategy {
+    REPEAT_LAST,    // repeat last value as many times as needed
+    RECYCLE         // repeat the whole series from the beginning
+    // autres propositions :
+    // PAD_LEFT     // pad with zeroes at the beginning ?
+    // PAD_RIGHT    // pad with zeroes at the end ?
+    // INTERPOLATE  // insert regular steps between first and last value
+}
+
 /**
  * Paramètre avec symbole, famille, domaine de définition, calculabilité,
  * pointant éventuellement vers un autre paramètre / résultat
@@ -55,6 +68,9 @@ export class ParamDefinition implements INamedIterableValues, IObservable {
     /** sandbox value used during calculation */
     public v: number;
 
+    /** extension strategy, when multiple parameters vary */
+    public extensionStrategy: ExtensionStrategy;
+
     /** mode de génération des valeurs : min/max, liste, ... */
     private _valueMode: ParamValueMode;
 
@@ -90,7 +106,7 @@ export class ParamDefinition implements INamedIterableValues, IObservable {
         this._observable = new Observable();
         this._paramValues = new ParamValues();
 
-        // set single value and copy it to currentValue
+        // set single value and copy it to sandbox value
         this._paramValues.singleValue = val;
         this.v = val;
 
@@ -98,6 +114,7 @@ export class ParamDefinition implements INamedIterableValues, IObservable {
         this._family = family;
         this.visible = visible;
         this.valueMode = ParamValueMode.SINGLE;
+        this.extensionStrategy = ExtensionStrategy.REPEAT_LAST;
 
         if (d instanceof ParamDomain) {
             this._domain = d;
@@ -203,8 +220,8 @@ export class ParamDefinition implements INamedIterableValues, IObservable {
     }
 
     /**
-     * Sets the value mode and asks the Nub to ensure all parameters requirements are met
-     * @see Nub::ensureParametersConsistency()
+     * Sets the value mode and asks the Nub to ensure there is only one parameter
+     * in CALC mode
      *
      * If propagateToCalculatedParam is true, any param that goes from CALC mode
      * to any other mode will trigger a reset of the default calculated param at
@@ -221,88 +238,19 @@ export class ParamDefinition implements INamedIterableValues, IObservable {
             return;
         }
 
-        switch (oldMode) {
-            case ParamValueMode.SINGLE:  // ancien état
-                switch (newMode) {
-                    case ParamValueMode.MINMAX:  // nouvel état
-                    case ParamValueMode.LISTE:
-                        this.ensureParametersConsistency(this, ParamValueMode.CALCUL);
-                        break;
-
-                    case ParamValueMode.CALCUL:  // nouvel état
-                        this.ensureParametersConsistency(this, [ ParamValueMode.MINMAX, ParamValueMode.LISTE ]);
-                        break;
-
-                    case ParamValueMode.LINK:  // nouvel état
-                        if (
-                            this.isReferenceDefined()
-                            && this.hasMultipleValues
-                        ) {
-                            this.ensureParametersConsistency(this); // reset existing variable param
-                        }
-                        break;
-                }
-                break;
-
-            case ParamValueMode.LISTE:  // ancien état
-            case ParamValueMode.MINMAX:
-                switch (newMode) {
-                    case ParamValueMode.CALCUL:  // nouvel état
-                        this.ensureParametersConsistency(this);
-                        break;
-
-                    case ParamValueMode.LINK:  // nouvel état
-                        if (
-                            this.isReferenceDefined()
-                            && this.hasMultipleValues
-                        ) {
-                            this.ensureParametersConsistency(this); // reset existing variable param
-                        }
-                        break;
-                }
-                break;
-
-            case ParamValueMode.LINK:  // ancien état
-                switch (newMode) {
-                    case ParamValueMode.MINMAX:  // nouvel état
-                    case ParamValueMode.LISTE:
-                        this.ensureParametersConsistency(this, ParamValueMode.CALCUL);
-                        break;
-
-                    case ParamValueMode.CALCUL:  // nouvel état
-                        this.ensureParametersConsistency(this, [ ParamValueMode.MINMAX, ParamValueMode.LISTE ]);
-                        break;
-                }
-                break;
-
-            case ParamValueMode.CALCUL:  // ancien état
-                switch (newMode) {
-                    case ParamValueMode.SINGLE:  // nouvel état
-                        break;
-
-                    case ParamValueMode.MINMAX:  // nouvel état
-                    case ParamValueMode.LISTE:
-                        this.ensureParametersConsistency(this);
-                        break;
-
-                    case ParamValueMode.LINK:  // nouvel état
-                        if (
-                            this.isReferenceDefined()
-                            && this.hasMultipleValues
-                        ) {
-                            this.ensureParametersConsistency(this); // reset existing variable param
-                        }
-                        break;
+        if (oldMode === ParamValueMode.CALCUL) {
+            if (propagateToCalculatedParam) {
+                // Set default calculated parameter, only if previous CALC param was
+                // manually set to something else than CALC
+                if (this.parentComputeNode && this.parentComputeNode instanceof Nub) {
+                    this.parentComputeNode.resetDefaultCalculatedParam(this);
                 }
-
-                if (propagateToCalculatedParam) {
-                    // Set default calculated parameter, only if previous CALC param was
-                    // manually set to something else than CALC
-                    if (this.parentComputeNode && this.parentComputeNode instanceof Nub) {
-                        this.parentComputeNode.resetDefaultCalculatedParam(this);
-                    }
-                }
-
+            }
+        } else if (newMode === ParamValueMode.CALCUL) {
+            // set old CALC param to SINGLE mode
+            if (this.parentComputeNode && this.parentComputeNode instanceof Nub) {
+                this.parentComputeNode.unsetCalculatedParam(this);
+            }
         }
 
         // set new mode
@@ -500,6 +448,15 @@ export class ParamDefinition implements INamedIterableValues, IObservable {
         return this.paramValues.singleValue;
     }
 
+    /**
+     * Returns values as a number list, for LISTE and MINMAX modes;
+     * in MINMAX mode, infers the list from min/max/step values
+     */
+    public getInferredValuesList() {
+        this.checkValueMode([ParamValueMode.LISTE, ParamValueMode.MINMAX, ParamValueMode.LINK]);
+        return this.paramValues.getInferredValuesList();
+    }
+
     /**
      * Magic method to define current values into Paramvalues set; defines the mode
      * accordingly by detecting values nature
@@ -1076,17 +1033,6 @@ export class ParamDefinition implements INamedIterableValues, IObservable {
         return res;
     }
 
-    /**
-     * Asks parent Nub if any to ensure parameters consistency
-     */
-    protected ensureParametersConsistency(sourceParam: ParamDefinition,
-                                          exceptMode: ParamValueMode|ParamValueMode[] = []
-    ) {
-        if (this.parentComputeNode && this.parentComputeNode instanceof Nub) {
-            this.parentComputeNode.ensureParametersConsistency(sourceParam, exceptMode);
-        }
-    }
-
     /**
      * notification envoyée après la modification de la valeur du paramètre
      */
diff --git a/src/param/param-values.ts b/src/param/param-values.ts
index 9d850c7d..d4919b3f 100644
--- a/src/param/param-values.ts
+++ b/src/param/param-values.ts
@@ -104,6 +104,26 @@ export class ParamValues implements IterableValues {
         }
     }
 
+    /**
+     * Returns values as a number list, for LISTE and MINMAX modes;
+     * in MINMAX mode, infers the list from min/max/step values
+     */
+    public getInferredValuesList() {
+        if (this.valueMode === ParamValueMode.LISTE) {
+            return this.valueList;
+        }
+        if (this.valueMode === ParamValueMode.MINMAX) {
+            const values = [ this.min ];
+            let latestVal = this.min + this.step;
+            while (latestVal <= this.max) {
+                values.push(latestVal);
+                latestVal += this.step;
+            }
+            return values;
+        }
+        throw new Error("ParamValues.getInferredValuesList() : incorrect value mode" + ParamValueMode[this.valueMode]);
+    }
+
     // -- iterator-related methods
 
     /**
diff --git a/src/section/section_parametree.ts b/src/section/section_parametree.ts
index d09e8708..a943d060 100644
--- a/src/section/section_parametree.ts
+++ b/src/section/section_parametree.ts
@@ -69,67 +69,6 @@ export class SectionParametree extends SectionNub {
         }
     }
 
-    public CalcSerie(rInit?: number, sDonnee?: string): Result {
-
-        this.triggerChainCalculation();
-
-        let variatedParam: ParamDefinition;
-        let variatedValues: ParamValues;
-
-        for (const p of this.parameterIterator) {
-            // checks which values are variating, if any
-            switch (p.valueMode) {
-                case ParamValueMode.SINGLE:
-                case ParamValueMode.CALCUL:
-                    break;
-
-                case ParamValueMode.LISTE:
-                case ParamValueMode.MINMAX:
-                    variatedParam = this.setVariatedValues(p, variatedParam);
-                    break;
-
-                case ParamValueMode.LINK:
-                    if (
-                        p.isReferenceDefined()
-                        && p.referencedValue.hasMultipleValues()
-                    ) {
-                        variatedParam = this.setVariatedValues(p, variatedParam);
-                    }
-                    break;
-
-                default:
-                    // tslint:disable-next-line:max-line-length
-                    throw new Error(`CalcSerie() : valeur de ParamValueMode ${ParamValueMode[p.valueMode]} non prise en charge`);
-            }
-        }
-
-        if (variatedParam === undefined) {
-            this.Calc(); // résultat dans this._result
-        } else {
-            // extract variated values from variated Parameter
-            // (in LINK mode, proxies to target data)
-            variatedValues = variatedParam.paramValues;
-
-            const res = new Result(undefined, this);
-            variatedValues.initValuesIterator(false);
-            while (variatedValues.hasNext) {
-                variatedValues.next();
-
-                this.Calc();  // résultat dans this._result
-                if (this._result.ok) {
-                    res.addResultElement(this._result.resultElement);
-                    res.addLog(this._result.log);
-                }
-                res.globalLog.addLog(this._result.globalLog);
-            }
-            this._result = res;
-
-        }
-
-        this.notifyResultUpdated();
-        return this._result;
-    }
-
     /**
      * Aucune variable à calculer plus que les autres, on stocke toutes les
      * valeurs des variables à calcul dans les résultats complémentaires
@@ -198,6 +137,10 @@ export class SectionParametree extends SectionNub {
         return this._result;
     }
 
+    protected doCalc(computedSymbol?: any, rInit?: number) {
+        return this.Calc();
+    }
+
     // tslint:disable-next-line:no-empty
     protected setParametersCalculability(): void {}
 
diff --git a/src/structure/structure.ts b/src/structure/structure.ts
index 2547d78f..68eb9930 100644
--- a/src/structure/structure.ts
+++ b/src/structure/structure.ts
@@ -106,9 +106,9 @@ export abstract class Structure extends Nub {
      * Forwards to parent, that has vsibility over
      * all the parameters, including the Structure ones
      */
-    public ensureParametersConsistency(sourceParam: ParamDefinition, exceptMode: ParamValueMode|ParamValueMode[] = []) {
+    public unsetCalculatedParam(except: ParamDefinition) {
         if (this.parent) {
-            this.parent.ensureParametersConsistency(sourceParam, exceptMode);
+            this.parent.unsetCalculatedParam(except);
         }
     }
 
-- 
GitLab