diff --git a/.gitignore b/.gitignore
index 78a09cc0fddf6a5df8b83f762a55c96a25f888bd..d484f6a04277b6d27baea18c96717919acad33b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,10 +7,12 @@
 /src/assets/docs
 /release
 /build
+/docs/pdf_build
 
 # dependencies
 /node_modules
 /src/date_revision.ts
+/ngsw-config.json
 
 # IDEs and editors
 /.idea
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0ff5191b950de080294be0f3e7535a68cb136e46..92811cedb1fad95a05865e51acd895e050f929ce 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -79,19 +79,34 @@ test:
   script:
     - npm run e2e
 
-build:
+.build:
   stage: build
-  only:
-    - pushes
-    - tags
-    - schedules
-    - web
   artifacts:
     expire_in: 10 min
     paths:
       - dist/
   script:
-    - npm run build -- --base-href=/cassiopee/$CI_COMMIT_REF_NAME/
+    # -baseref option is used by npm to set the npm_config_basehref environment variable
+    # used in package.json
+    - npm run build-href -basehref=$BASEHREF
+
+build-dev:
+  extends: .build
+  only:
+    - tags
+    - pushes
+    - schedules
+    - web
+  variables:
+    BASEHREF: "/cassiopee/$CI_COMMIT_REF_NAME/"
+
+build-prod:
+  extends: .build
+  only:
+    - tags
+    - devel
+  variables:
+    BASEHREF: "/"
 
 clean-stale-branches:
   stage: clean-stale-branches
@@ -109,7 +124,7 @@ deploy-dev:
     - tags
     - web
   dependencies:
-    - build
+    - build-dev
   script:
     # Copie de la branche / du tag
     - ./scripts/deploy-version.sh $CI_COMMIT_REF_NAME $DEV_LOGIN $DEV_HOST $DEV_PATH
@@ -120,7 +135,7 @@ deploy-ovh-dev:
   only:
     - devel
   dependencies:
-    - build
+    - build-prod
   script:
     # Copie de la branche / du tag
     - ./scripts/deploy-version.sh prod-devel $PROD_LOGIN $PROD_HOST $PROD_DEV_PATH
@@ -131,7 +146,7 @@ deploy-prod:
     variables:
       - $CI_COMMIT_REF_NAME == "stable"
   dependencies:
-    - build
+    - build-prod
   script:
     - ./scripts/deploy-version.sh prod $PROD_LOGIN $PROD_HOST $PROD_PATH
 
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index bfe798cdd5cacba7673fc10e9e3939e19a15f00e..0000000000000000000000000000000000000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-    "spellright.language": [
-        "fr"
-    ],
-    "spellright.documentTypes": [
-        "latex",
-        "plaintext",
-        "markdown"
-    ],
-    "cSpell.words": [
-        "Cassan",
-        "DICHO",
-        "MACRORUGO",
-        "fdescribe",
-        "jalhyd",
-        "nghyd",
-        "prms"
-    ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 5750c79b6ba0413eaaf5bd25fba416d4790817f8..0fc8a9725a79ebaa03c2684ef07a93917538361a 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Requirements for developping Cassiopee can be achieved by manually install the r
  * npm
  * python3
  * pandoc ^2 (optional, for PDF documentation only)
- * texlive (optional, for PDF documentation only)
+ * texlive, texlive-bibtex-extra, texlive-latex-extra, latexmk (optional, for PDF documentation only)
 
 Building the HTML documentation requires MkDocs and some extensions:
 
diff --git a/angular.json b/angular.json
index 7af026e53aae99397903e282c14a0c8b32ea96ae..1b463deca38d935c10c23184080e848f7e21a6fd 100644
--- a/angular.json
+++ b/angular.json
@@ -49,7 +49,8 @@
             ],
             "scripts": [
               "node_modules/marked/marked.min.js",
-              "node_modules/katex/dist/katex.min.js"
+              "node_modules/katex/dist/katex.min.js",
+              "node_modules/katex/dist/contrib/auto-render.min.js"
             ],
             "allowedCommonJsDependencies": [
               "chartjs-plugin-zoom",
diff --git a/docs/en/calculators/hyd_en_charge/lechapt-calmon.md b/docs/en/calculators/hyd_en_charge/lechapt-calmon.md
index ee025f7132a3d3ac30521a69f5be9a834301c629..ba97396cb5fa2362d01c04983159eab37b4bbd6d 100644
--- a/docs/en/calculators/hyd_en_charge/lechapt-calmon.md
+++ b/docs/en/calculators/hyd_en_charge/lechapt-calmon.md
@@ -8,7 +8,7 @@ It allows the calculation of the value of one of the following quantities:
 - Pipe diameter (m)
 - Total head loss (m)
 - Pipe length (m)
-- Singular pressure loss coefficient (m)
+- Singular pressure loss coefficient
 
 The total head loss is the sum of the linear head losses \(J_L\) obtained from the Lechapt and Calmon abacuses and singular \(J_S\) depending on the above coefficient.
 
diff --git a/docs/en/calculators/hyd_en_charge/perte_de_charge.md b/docs/en/calculators/hyd_en_charge/perte_de_charge.md
new file mode 100644
index 0000000000000000000000000000000000000000..3ed9b791fec622b298ce49440f77f31c9b895302
--- /dev/null
+++ b/docs/en/calculators/hyd_en_charge/perte_de_charge.md
@@ -0,0 +1,34 @@
+# Pressure loss
+
+This module computes linear head losses in a circular pipe with the following laws:
+
+- [Lechapt et Calmon](lechapt-calmon.md)
+- [Strickler](strickler.md)
+
+
+The following values can be computed:
+
+- Flow (m<sup>3</sup>/s)
+- Pipe diameter (m)
+- Total pressure loss (m)
+- Pipe length (m)
+- Singular head loss coefficient
+
+The total pressure loss is the sum of linear losses \(J_{lin}\), according to the used law, and singular losses \(J_{loc}\) depending on the above coefficient.
+
+## Singular pressure loss
+
+$$ J_{loc} = K_{loc} \frac{V^2}{2g}$$
+
+Given&nbsp;:
+
+- \(K_{loc}\)&nbsp;: singular head loss coefficient
+- \(V\)&nbsp;: water speed in the pipe (\(V = 4 Q / \pi / D^2\))
+
+## Linear head loss coefficient
+
+$$ K_{lin} = \frac{2g J_{lin}}{V^2} $$
+
+## Darcy head loss coefficient 
+
+$$ f_D = \frac{2g J D}{l_T V^2} $$
diff --git a/docs/en/calculators/hyd_en_charge/strickler.md b/docs/en/calculators/hyd_en_charge/strickler.md
new file mode 100644
index 0000000000000000000000000000000000000000..ea7870e9f1127378070137d44385da317cedb72c
--- /dev/null
+++ b/docs/en/calculators/hyd_en_charge/strickler.md
@@ -0,0 +1,11 @@
+# Strickler formula
+
+This linear head loss law is parameterised by the Stricker coefficient \(K_S\).
+
+The other law parameters are shared with all pressure loss formulas:
+
+- \(Q\) : flow (m<sup>3</sup>/s)
+- \(D\) : pipe diameter (m)
+- \(l_T\) : pipe length (m)
+
+$$J_L=\frac{l_T.Q^2}{(K_S.\pi.D^2/4)^2.(D/4)^{4/3}}$$
diff --git a/docs/en/calculators/structures/fente_noyee.md b/docs/en/calculators/structures/fente_noyee.md
index fc77817d5f45b67540b6654cff0c573b4dad65b8..b7a3fac759ee81fd2ca78d53618edebf6fa1f645 100644
--- a/docs/en/calculators/structures/fente_noyee.md
+++ b/docs/en/calculators/structures/fente_noyee.md
@@ -6,12 +6,44 @@
 
 Larinier (1992) suggests the following equation:
 
-$$Q = \mu b H_1\sqrt{2g \Delta H}$$
+$$Q = C_d b H_1\sqrt{2g \Delta H}$$
 
 With:
 
 * *b* the slot width in m
 * *H<sub>1</sub>* the head on the slot m
-* *μ* the discharge coefficient (equal to 0.65 by default).
+* *C<sub>d</sub>* the discharge coefficient.
 
-The discharge coefficient *μ* may vary from 0.65 for a sharp-edged slot to more than 0.85 when the slot profile is rounded.
+# Discharge coefficient **C<sub>d</sub>** for the submerged slot formula (vertical slot fish ladder)
+
+The discharge coefficient **C<sub>d</sub>** is an important parameter for the design of vertical slot weirs. This term is integrated in the formula of conveyance of a submerged slot, which connects the transited flow with the fall between basins, the upstream head on the slot and its width.
+
+**For single vertical slot weirs**, model-reduced studies and numerical simulations have determined average values of discharge coefficients for different configurations, for a typical slot and baffle geometry defined from vertical slot weirs built in France (Figure 1). A different slot and baffle geometry can induce deviations from the average values of the discharge coefficients according to the configurations indicated below.
+
+![Figure 1](fente_noyee_fig1.png)
+
+*Figure 1: typical slot and baffle geometry from which the discharge coefficient values were determined (Ballu, 2017)*
+
+* **Under smooth apron conditions (no bottom roughness) and in the absence of a weir in the slot**, Wang *et al.* (2010) and Ballu *et al.* (2017) showed that the value of **C<sub>d</sub>** is not significantly influenced by discharge and depends mainly on the slope **S** (%) and the ratio of the basin width **B** to the slot width **b** (**B/b**).
+
+* **Under smooth invert conditions (no bottom roughness) with the presence of a weir in the slot**, under 3 configurations of weir heights **h<sub>s</sub>/b** related to slot width **b** (**h<sub>s</sub>/b** = 0.5, 1, and 1.5), Ballu *et al.* (2015) and Ballu (2017) showed that the value of **C<sub>d</sub>** is not significantly influenced by discharge, and is mainly influenced by slope **S**, aspect ratio **B/b**, and weir height **h<sub>s</sub>/b**.
+
+* **Under apron conditions with precast bottom roughnesses and in the absence of a weir in the slot**, with evenly distributed roughnesses of height **h<sub>r</sub>/b** = 2/3 and diameter **Ø<sub>r</sub>/b** = 1/2, under 2 configurations of densities **d<sub>r</sub>** of 10% and 15%, Ballu *et al.* (2017) and Ballu (2017) showed that the value of **C<sub>d</sub>** is not significantly influenced by discharge, and is mainly influenced by slope **S**, aspect ratio **B/b**, and the presence of the bottom roughness **d<sub>r</sub>**.
+
+Depending on these different configurations, the average values of the discharge coefficient range from about 0.62 to nearly 0.88 (Figure 2).
+
+![Figure 2](fente_noyee_fig2.png)
+
+*Figure 2: average values and uncertainties (95% confidence intervals, k = 2) of the discharge coefficients according to the slope S and the shape ratio B/b, for different configurations: (A) smooth raft without weir in the slot, (B) bottom roughness d<sub>r</sub> = 10% without weir in the slot, (C) bottom roughness d<sub>r</sub> = 15% without weir in the slot, (D) weir in the slot h<sub>s</sub>/b = 0.5 and smooth raft, (E) weir in the slot h<sub>s</sub>/b = 1 and smooth raft, (F) weir in the slot h<sub>s</sub>/b = 1.5 and smooth raft*
+
+**For passes with double vertical slots**, **C<sub>d</sub>** values between 0.75 and 0.80 are commonly used for recently constructed devices with longitudinal slopes between 4% and 5.5%. Studies are under way to clarify the influence of different configurations on discharge coefficients and their corresponding values.
+
+**Bibliography:**
+
+Ballu A., Pineau G., Calluaud D., David L. (2015). Experimental study of the influence of sills on vertical slot fishway flow. *36<sup>th</sup> IAHR World Congress*, 7p.
+
+Ballu A. (2017). Étude numérique et expérimentale de l’écoulement turbulent au sein des passes à poissons à fentes verticales. Analyse de l’écoulement tridimensionnel et instationnaire. *Thèse de l’Université de Poitiers*, 223p.
+
+Ballu A., Calluaud D., Pineau G., david L. (2017). Experimental study of the influence of macro‑roughnesses on vertical slot fishway flows. *La Houille Blanche*, 2: 9-14.
+
+Wang R.W., David L., Larinier M. (2010). Contribution of experimental fluid mechanics to the design of vertical slot fish passes. *Knowledge and Management of Aquatic Ecosystems*, 396(2).
diff --git a/docs/en/calculators/structures/fente_noyee_fig1.png b/docs/en/calculators/structures/fente_noyee_fig1.png
new file mode 100644
index 0000000000000000000000000000000000000000..15680c7cefede3a3c1282e3b1f20ff3219a212de
Binary files /dev/null and b/docs/en/calculators/structures/fente_noyee_fig1.png differ
diff --git a/docs/en/calculators/structures/fente_noyee_fig2.png b/docs/en/calculators/structures/fente_noyee_fig2.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c72b515e39b440eae1f429ff091a0b686b41453
Binary files /dev/null and b/docs/en/calculators/structures/fente_noyee_fig2.png differ
diff --git a/docs/fr/calculators/hyd_en_charge/lechapt-calmon.md b/docs/fr/calculators/hyd_en_charge/lechapt-calmon.md
index 6001e557244786c0b6d2f9890bb159d4460d98e4..2cfcad148958c173b3bdeec91fa97052fb76473a 100644
--- a/docs/fr/calculators/hyd_en_charge/lechapt-calmon.md
+++ b/docs/fr/calculators/hyd_en_charge/lechapt-calmon.md
@@ -1,26 +1,12 @@
-# Lechapt et Calmon
-
-Ce module permet de calculer les pertes de charge dans une conduite circulaire à partir des abaques de Lechapt et Calmon.
-
-Il permet le calcul de la valeur d'une des grandeurs suivantes&nbsp;:
-
-- Débit (m<sup>3</sup>/s)
-- Diamètre du tuyau (m)
-- Perte de charge totale (m)
-- Longueur du tuyau (m)
-- Coefficient de perte de charge singulière (m)
-
-La perte de charge totale est la somme des pertes de charges linéaires \(J_L\) obtenue à partir des abaques de Lechapt et Calmon et singulières \(J_S\) dépendantes du coefficient ci-dessus.
-
-## Formule de Lechapt et Calmon
+# Formule de Lechapt et Calmon
 
 La formule de Lechapt et Calmon est basée sur des ajustements de la formule de  [Cyril Frank Colebrook](http://fr.wikipedia.org/wiki/Cyril_Frank_Colebrook)&nbsp;:
 
-$$J_L=\frac{l_T}{1000}L.Q^M.D^{-N}$$
+$$J_{lin}=\frac{l_T}{1000}L.Q^M.D^{-N}$$
 
 Avec&nbsp;:
 
-- \(J_L\)&nbsp;: la perte de charge linéaire en m&nbsp;;
+- \(J_{lin}\)&nbsp;: la perte de charge linéaire en m&nbsp;;
 - \(l_T\)&nbsp;: la longueur du tuyau en m&nbsp;;
 - \(Q\)&nbsp;: le débit en L/s&nbsp;;
 - \(D\)&nbsp;: le diamètre de la conduite en m&nbsp;;
@@ -43,21 +29,4 @@ Le tableau de correspondance des coefficients est le suivant&nbsp;:
 | Tuyau hydrauliquement lisse - 0.05 &le; D &le; 0.2 | 0.00 | 0.916 | 1.78 | 4.78 |
 | Tuyau hydrauliquement lisse - 0.25 &le; D &le; 1 | 0.00 | 0.971 | 1.81 | 4.81 |
 
-Table: Matériaux et coefficients utilisés dans la formule de Lechapt et Calmon
-
-## Perte de charge singulière
-
-$$ J_S = K_S \frac{V^2}{2g}$$
-
-Avec&nbsp;:
-
-- \(K_S\)&nbsp;: le coefficient de perte de charge singulière
-- \(V\)&nbsp;: la vitesse de l'eau dans la conduite (\(V = 4 Q / \pi / D^2\))
-
-## Coefficient de perte de charge linéaire
-
-$$ K_L = \frac{2g J_L}{V^2} $$
-
-## Coefficient de perte de charge de Darcy
-
-$$ f_D = \frac{2g J D}{l_T V^2} $$
+Table&nbsp;: Matériaux et coefficients utilisés dans la formule de Lechapt et Calmon
diff --git a/docs/fr/calculators/hyd_en_charge/perte_de_charge.md b/docs/fr/calculators/hyd_en_charge/perte_de_charge.md
new file mode 100644
index 0000000000000000000000000000000000000000..2cc8b986876d9defc3bd85b0716da084e5fd81ee
--- /dev/null
+++ b/docs/fr/calculators/hyd_en_charge/perte_de_charge.md
@@ -0,0 +1,33 @@
+# Perte de charge
+
+Ce module permet de calculer les pertes de charge dans une conduite circulaire à partir des lois suivantes donnant les pertes de charge linéaires :
+
+- [Lechapt et Calmon](lechapt-calmon.md)
+- [Strickler](strickler.md)
+
+Il permet le calcul de la valeur d'une des grandeurs suivantes&nbsp;:
+
+- Débit (m<sup>3</sup>/s)
+- Diamètre du tuyau (m)
+- Perte de charge totale (m)
+- Longueur du tuyau (m)
+- Coefficient de perte de charge locale (singulière) (m)
+
+La perte de charge totale est la somme des pertes de charges linéaires \(J_{lin}\) (données par la loi utilisée) et locales \(J_{loc}\) dépendantes du coefficient ci-dessus.
+
+## Perte de charge locale
+
+$$ J_{loc} = K_{loc} \frac{V^2}{2g}$$
+
+Avec&nbsp;:
+
+- \(K_{loc}\)&nbsp;: le coefficient de perte de charge locale
+- \(V\)&nbsp;: la vitesse de l'eau dans la conduite (\(V = 4 Q / \pi / D^2\))
+
+## Coefficient de perte de charge linéaire
+
+$$ K_{lin} = \frac{2g J_{lin}}{V^2} $$
+
+## Coefficient de perte de charge de Darcy
+
+$$ f_D = \frac{2g J D}{l_T V^2} $$
diff --git a/docs/fr/calculators/hyd_en_charge/strickler.md b/docs/fr/calculators/hyd_en_charge/strickler.md
new file mode 100644
index 0000000000000000000000000000000000000000..7f393129602d4fbcb82e994031ba278e87cd22ce
--- /dev/null
+++ b/docs/fr/calculators/hyd_en_charge/strickler.md
@@ -0,0 +1,11 @@
+# Formule de Strickler
+
+Cette loi de perte de charge linéaire est paramétrée par le coefficient de Stricker \(K_S\).
+
+Les autres paramètres sont ceux communs à tous les calculs de perte de charge&nbsp;:
+
+- \(Q\) : débit (m<sup>3</sup>/s)
+- \(D\) : diamètre du tuyau (m)
+- \(l_T\) : Longueur du tuyau (m)
+
+$$J_L=\frac{l_T.Q^2}{(K_S.\pi.D^2/4)^2.(D/4)^{4/3}}$$
diff --git a/docs/fr/calculators/structures/fente_noyee.md b/docs/fr/calculators/structures/fente_noyee.md
index efd3dcf9819d330ebb6dcbc3a2b293bf81b4a1e3..b31bdfb8ccdfa6c104a2416ae0a91780abd51c87 100644
--- a/docs/fr/calculators/structures/fente_noyee.md
+++ b/docs/fr/calculators/structures/fente_noyee.md
@@ -6,12 +6,44 @@
 
 Larinier (1992) propose l'équation suivante&nbsp;:
 
-$$Q = \mu b H_1\sqrt{2g \Delta H}$$
+$$Q = C_d b H_1\sqrt{2g \Delta H}$$
 
 Avec&nbsp;:
 
 * *b* la largeur de la fente en m&nbsp;
 * *H<sub>1</sub>* la charge sur la fente m&nbsp;
-* *μ* le coefficient de débit (égal à 0.65 par défaut).
+* *C<sub>d</sub>* le coefficient de débit.
 
-Le coefficient de débit *μ* peut varier de 0.65 pour une fente à arêtes vives à plus de 0.85 lorsque le profil de la fente est arrondi.
+# Coefficient de débit C<sub>d</sub> pour la formule de la fente noyée (passe à bassins à fente verticale)
+
+Le coefficient de débit **C<sub>d</sub>** est un paramètre important pour le dimensionnement des passes à fente(s) verticale(s). Ce terme est intégré dans la formule de débitance d’un déversoir de type fente noyée, qui relie le débit transité avec la chute entre bassins, la charge amont sur la fente et sa largeur.
+
+**Pour les passes à simple fente verticale**, des études réalisées sur modèle-réduit et par simulations numériques ont permis de déterminer des valeurs moyennes de coefficients de débit selon différentes configurations, pour une géométrie type de fente et de déflecteurs définie à partir des passes à fentes verticales construites en France (Figure 1). Une géométrie différente de fente et de déflecteur peut induire des écarts par rapport aux valeurs moyennes des coefficients de débit selon les configurations indiquées ci-dessous.
+
+![Figure 1](fente_noyee_fig1.png)
+
+*Figure 1 : géométrie type de fente et de déflecteurs à partir de laquelle les valeurs des coefficients de débit ont été déterminés (Ballu, 2017)*
+
+* **Dans des conditions de radier lisse (sans rugosité de fond) et en l’absence de seuil dans la fente**, Wang *et al.* (2010) et Ballu *et al.* (2017) ont montré que la valeur du **C<sub>d</sub>** n’est pas significativement influencée par le débit et dépend principalement de la pente **S** (%) et du rapport entre la largeur des bassins **B** et la largeur de la fente **b** (**B/b**).
+
+* **Dans des conditions de radier lisse (sans rugosité de fond) avec la présence d’un seuil dans la fente**, selon 3 configurations de hauteurs de seuils **h<sub>s</sub>** rapportées à la largeur de la fente **b** (**h<sub>s</sub>/b** = 0.5, 1 et 1.5), Ballu *et al.* (2015) et Ballu (2017) ont montré que la valeur du **C<sub>d</sub>** n’est pas significativement influencée par le débit, et qu’elle est principalement influencée par la pente **S**, le rapport de forme **B/b** et la hauteur des seuils **h<sub>s</sub>**.
+
+* **Dans des conditions de radier avec rugosités de fond préfabriquées et en l’absence de seuil dans la fente**, avec des rugosités régulièrement réparties de hauteur **h<sub>r</sub>/b** = 2/3 et de diamètre **Ø<sub>r</sub>/b** = 1/2, selon 2 configurations de densités **d<sub>r</sub>** de 10% et 15%, Ballu *et al.* (2017) et Ballu (2017) ont montré que la valeur du **C<sub>d</sub>** n’est pas significativement influencée par le débit, et qu’elle est principalement influencée par la pente **S**, le rapport de forme **B/b** et la présence de la rugosité de fond **d<sub>r</sub>**.
+
+* En fonction de ces différentes configurations, les valeurs moyennes du coefficient de débit s’étalent d’environ 0.62 jusqu’à près de 0.88 (Figure 2).
+
+![Figure 2](fente_noyee_fig2.png)
+
+*Figure 2 : valeurs moyennes et incertitudes (intervalles de confiance à 95%, k=2) des coefficients de débits selon la pente S et le rapport de forme B/b pour différentes configurations&nbsp;: (A) radier lisse sans seuil dans la fente, (B) rugosités de fond d<sub>r</sub> = 10% sans seuil dans la fente, (C) rugosités de fond d<sub>r</sub> = 15% sans seuil dans la fente, (D) seuil dans la fente h<sub>s</sub>/b = 0.5 et radier lisse, (E) seuil dans la fente h<sub>s</sub>/b = 1 et radier lisse, (F) seuil dans la fente h<sub>s</sub>/b = 1.5 et radier lisse*
+
+**Pour les passes à doubles fentes verticales**, des valeurs de **C<sub>d</sub>** comprises entre 0.75 et 0.80 sont couramment retenues pour les dispositifs construits récemment avec des pentes longitudinales entre 4% et 5.5%. Des études sont en cours afin de préciser l’influence de différentes configurations sur les coefficients de débits et leurs valeurs correspondantes.
+
+**Bibliographie :**
+
+Ballu A., Pineau G., Calluaud D., David L. (2015). Experimental study of the influence of sills on vertical slot fishway flow. *36<sup>th</sup> IAHR World Congress*, 7p.
+
+Ballu A. (2017). Étude numérique et expérimentale de l’écoulement turbulent au sein des passes à poissons à fentes verticales. Analyse de l’écoulement tridimensionnel et instationnaire. *Thèse de l’Université de Poitiers*, 223p.
+
+Ballu A., Calluaud D., Pineau G., david L. (2017). Experimental study of the influence of macro‑roughnesses on vertical slot fishway flows. *La Houille Blanche*, 2: 9-14.
+
+Wang R.W., David L., Larinier M. (2010). Contribution of experimental fluid mechanics to the design of vertical slot fish passes. *Knowledge and Management of Aquatic Ecosystems*, 396(2).
diff --git a/docs/fr/calculators/structures/fente_noyee_fig1.png b/docs/fr/calculators/structures/fente_noyee_fig1.png
new file mode 100644
index 0000000000000000000000000000000000000000..15680c7cefede3a3c1282e3b1f20ff3219a212de
Binary files /dev/null and b/docs/fr/calculators/structures/fente_noyee_fig1.png differ
diff --git a/docs/fr/calculators/structures/fente_noyee_fig2.png b/docs/fr/calculators/structures/fente_noyee_fig2.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c72b515e39b440eae1f429ff091a0b686b41453
Binary files /dev/null and b/docs/fr/calculators/structures/fente_noyee_fig2.png differ
diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts
index 66fc56982414a0e7df933c7c27896a33b30b9c37..660387982096c186f74ab647f194ae67735f1ef5 100644
--- a/e2e/app.e2e-spec.ts
+++ b/e2e/app.e2e-spec.ts
@@ -7,7 +7,7 @@ import { browser } from "protractor";
 describe("ngHyd − start page", () => {
     let page: AppPage;
 
-    beforeEach(() => {
+    beforeAll(() => {
         page = new AppPage();
     });
 
diff --git a/e2e/bief-empty-fields.e2e-spec.ts b/e2e/bief-empty-fields.e2e-spec.ts
index fd9fc0a449086ced16e0b67183c2ccb3856a56f3..391c4523878f6b09a55d931dd079788bd2166ab0 100644
--- a/e2e/bief-empty-fields.e2e-spec.ts
+++ b/e2e/bief-empty-fields.e2e-spec.ts
@@ -10,7 +10,7 @@ describe("Check fields are empty in 'up/downstream elevations of a reach' calcul
     let calcPage: CalculatorPage;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
diff --git a/e2e/calc-all-examples.e2e-spec.ts b/e2e/calc-all-examples.e2e-spec.ts
index 5fc9e4b76bd2906739fd202a8fa65b23c1bca2ab..570e5f19ca4c8dad3e6691548d658640496f280e 100644
--- a/e2e/calc-all-examples.e2e-spec.ts
+++ b/e2e/calc-all-examples.e2e-spec.ts
@@ -1,4 +1,3 @@
-import { ListPage } from "./list.po";
 import { CalculatorPage } from "./calculator.po";
 import { Navbar } from "./navbar.po";
 import { browser, by, element } from "protractor";
@@ -8,9 +7,8 @@ import { SideNav } from "./sidenav.po";
 /**
  * Calculate all modules of all examples
  */
-describe("ngHyd − example sessions −", async () => {
+describe("ngHyd − example sessions −", () => {
 
-    let listPage: ListPage;
     let calcPage: CalculatorPage;
     let navbar: Navbar;
     let prefPage: PreferencesPage;
@@ -18,7 +16,6 @@ describe("ngHyd − example sessions −", async () => {
 
     beforeAll(() => {
         jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
-        listPage = new ListPage();
         calcPage = new CalculatorPage();
         navbar = new Navbar();
         prefPage = new PreferencesPage();
@@ -78,7 +75,5 @@ describe("ngHyd − example sessions −", async () => {
             }
             i++;
         }
-
     });
-
 });
diff --git a/e2e/calculate-all-params.e2e-spec.ts b/e2e/calculate-all-params.e2e-spec.ts
index 39129ac615e92f804560d9bd3db5f34e8d14f98b..f19c2f7d0351eaaa91e1988cd7e3a4946febb74a 100644
--- a/e2e/calculate-all-params.e2e-spec.ts
+++ b/e2e/calculate-all-params.e2e-spec.ts
@@ -4,23 +4,29 @@ import { Navbar } from "./navbar.po";
 import { PreferencesPage } from "./preferences.po";
 import { browser, element, by } from "protractor";
 import { testedCalcTypes } from "./tested_calctypes";
+import { scrollPageToTop } from "./util.po";
 
 /**
  * For all calculators, try to calculate every parameter: check that only one parameter
  * is set to CAL mode, trigger the calculation, check that result is not empty
  */
-describe("ngHyd − calculate all parameters of all calculators", async () => {
+describe("ngHyd − calculate all parameters of all calculators", () => {
     let listPage: ListPage;
     let calcPage: CalculatorPage;
     let navBar: Navbar;
     let prefPage: PreferencesPage;
 
-    beforeEach(() => {
+    beforeAll(async () => {
         listPage = new ListPage();
         calcPage = new CalculatorPage();
         prefPage = new PreferencesPage();
         navBar = new Navbar();
 
+        // disable evil option "empty fields on module creation"
+        await prefPage.navigateTo();
+        await prefPage.disableEvilEmptyFields();
+        await browser.sleep(200);
+
         // increase timeout to avoid "Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL" message
         jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 60 * 1000; // 10 min
     });
@@ -28,18 +34,9 @@ describe("ngHyd − calculate all parameters of all calculators", async () => {
     // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/
     const calcTypes = testedCalcTypes;
 
-    beforeAll(async () => {
-        prefPage = new PreferencesPage();
-        navBar = new Navbar();
-        // disable evil option "empty fields on module creation"
-        await prefPage.navigateTo();
-        await prefPage.disableEvilEmptyFields();
-        await browser.sleep(200);
-    });
-
     // for each calculator
     for (const ct of calcTypes) {
-        if ([ 22, 31, 32, 33, 34 ].includes(ct)) {
+        if ([22, 31, 32, 33, 34].includes(ct)) {
             // omit 22 - Solveur
             // omit 31 - PbCloison
             // omit 32 - PbBassin
@@ -48,7 +45,7 @@ describe("ngHyd − calculate all parameters of all calculators", async () => {
             // (not calculated here because they are not independent)
             continue;
         }
-        describe(" − calculate all parameters of calculator type [" + ct + "]", async () => {
+        describe(" − calculate all parameters of calculator type [" + ct + "]", () => {
             it("", async () => {
                 // go to list page
                 await navBar.clickNewCalculatorButton();
@@ -65,7 +62,7 @@ describe("ngHyd − calculate all parameters of all calculators", async () => {
                         // that the calculable parameters are shown
                         if (ct === 30 && i > 0) {
                             // prevents "Element is not clickable at point"
-                            await browser.executeScript("window.scrollTo(0, 0);");
+                            await scrollPageToTop();
                             const inputLink = element(by.css("#pb-data-results-selector .drs-item a"));
                             await inputLink.click();
                         }
@@ -77,7 +74,7 @@ describe("ngHyd − calculate all parameters of all calculators", async () => {
                         const nbParamsCalc = await calcPage.getCheckedCalcModeButtons().count();
                         expect(nbParamsCalc).toBe(1);
                         // check that "compute" button is active
-                        const calcButton = calcPage.checkCalcButtonEnabled(true);
+                        const calcButton = await calcPage.checkCalcButtonEnabled(true);
                         // click "compute" button
                         await calcButton.click();
                         // check that result is not empty
@@ -87,7 +84,7 @@ describe("ngHyd − calculate all parameters of all calculators", async () => {
                 } else {
                     // module has no calculable params, just click the "compute" button
                     // check that "compute" button is active
-                    const calcButton = calcPage.checkCalcButtonEnabled(true);
+                    const calcButton = await calcPage.checkCalcButtonEnabled(true);
                     // click "compute" button
                     await calcButton.click();
                     // check that result is not empty
@@ -97,5 +94,4 @@ describe("ngHyd − calculate all parameters of all calculators", async () => {
             });
         });
     }
-
 });
diff --git a/e2e/calculate-button-validation.e2e-spec.ts b/e2e/calculate-button-validation.e2e-spec.ts
index 1d55211fb90dbe3130793a11b851dc1d318db8d1..0f472f43eb1e0213cc34a18bf954a9bc5a2574b0 100644
--- a/e2e/calculate-button-validation.e2e-spec.ts
+++ b/e2e/calculate-button-validation.e2e-spec.ts
@@ -10,7 +10,7 @@ describe("Calculate button - ", () => {
     let navBar: Navbar;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         listPage = new ListPage();
         calcPage = new CalculatorPage();
@@ -49,21 +49,21 @@ describe("Calculate button - ", () => {
         await browser.sleep(200);
 
         // check that "compute" button is inactive
-        calcPage.checkCalcButtonEnabled(false);
+        await calcPage.checkCalcButtonEnabled(false);
 
         // back to PAB: chute
         await navBar.clickCalculatorTab(0);
         await browser.sleep(200);
 
         // check that "compute" button is active
-        calcPage.checkCalcButtonEnabled(true);
+        await calcPage.checkCalcButtonEnabled(true);
 
         // back to PAB: dimensions
         await navBar.clickCalculatorTab(1);
         await browser.sleep(200);
 
         // check that "compute" button is inactive
-        calcPage.checkCalcButtonEnabled(false);
+        await calcPage.checkCalcButtonEnabled(false);
     });
 
     describe("check button status in prébarrages - ", () => {
@@ -83,7 +83,7 @@ describe("Calculate button - ", () => {
             await inputQ.sendKeys("-1");
             await browser.sleep(200);
 
-            calcPage.checkCalcButtonEnabled(false);
+            await calcPage.checkCalcButtonEnabled(false);
 
             // upstream item
             // look for g element with id starting by "flowchart-amont-"
@@ -120,7 +120,7 @@ describe("Calculate button - ", () => {
             await browser.sleep(200);
 
             // calculate button disabled ?
-            calcPage.checkCalcButtonEnabled(false);
+            await calcPage.checkCalcButtonEnabled(false);
 
             // upstream item displayed in error ?
             expect(await upstream.getAttribute('class')).toContain("node-highlighted-error"); // upstream item is now selected by default (was 'node-error')
@@ -132,10 +132,48 @@ describe("Calculate button - ", () => {
             await browser.sleep(200);
 
             // calculate button still disabled ? (the basin is not connected to anything)
-            calcPage.checkCalcButtonEnabled(false);
+            await calcPage.checkCalcButtonEnabled(false);
 
             // upstream item displayed not in error ?
             expect(await upstream.getAttribute('class')).not.toContain("node-error");
         });
     });
+
+    async function checkCalculateButtonValidity(calcType: number) {
+        // open calculator
+        await navBar.clickNewCalculatorButton();
+        await listPage.clickMenuEntryForCalcType(calcType);
+        await browser.sleep(200);
+
+        // for each input, set empty and check calculate button is not active
+
+        const inputs = calcPage.getParamInputs();
+        const ninp = await inputs.count();
+        for (let i = 0; i < ninp; i++) {
+            const inp = inputs.get(i);
+            // set input to fixed mode
+            await calcPage.setParamMode(inp, "fix");
+            await browser.sleep(100);
+
+            // clear input
+            await calcPage.clearInput(inp);
+            await browser.sleep(10);
+
+            // check calculate button is disabled
+            await calcPage.checkCalcButtonEnabled(false);
+            await browser.sleep(100);
+
+            // refill input
+            await inp.sendKeys("1");
+            await browser.sleep(100);
+        }
+    }
+
+    it("check status for various calculators", async () => {
+        // "parallel structures" calculator
+        await checkCalculateButtonValidity(8);
+
+        // "fish ladder : cross walls" calculator
+        await checkCalculateButtonValidity(10);
+    });
 });
diff --git a/e2e/calculate-linked-params.e2e-spec.ts b/e2e/calculate-linked-params.e2e-spec.ts
index 56f8dd05fcb11caf5909a18f4d095c1495b90f45..f87d9b16877ea5d8d4d26833d2ef837ed20a4380 100644
--- a/e2e/calculate-linked-params.e2e-spec.ts
+++ b/e2e/calculate-linked-params.e2e-spec.ts
@@ -1,10 +1,10 @@
-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, by } from "protractor";
 import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue } from "./util.po";
 
 /**
  * Uses an example configuration to calculate :
@@ -28,12 +28,15 @@ describe("ngHyd − calculate with linked parameters", () => {
     let sidenav: SideNav;
     let prefPage: PreferencesPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         calcPage = new CalculatorPage();
         navBar = new Navbar();
         prefPage = new PreferencesPage();
         sidenav = new SideNav();
+    });
+
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await prefPage.disableEvilEmptyFields();
@@ -42,7 +45,7 @@ describe("ngHyd − calculate with linked parameters", () => {
 
     async function computeAndCheckPresenceOfResults() {
         // check that "compute" button is active
-        const calcButton = calcPage.checkCalcButtonEnabled(true);
+        const calcButton = await calcPage.checkCalcButtonEnabled(true);
         // click "compute" button
         await calcButton.click();
         // check that result is not empty
@@ -62,7 +65,7 @@ describe("ngHyd − calculate with linked parameters", () => {
         const Y = calcPage.getInputById("Y");
         await calcPage.setParamMode(Y, "link");
         const sel = await calcPage.getLinkedValueSelect(Y);
-        await calcPage.changeSelectValue(sel, 0);
+        await changeSelectValue(sel, 0);
 
         await computeAndCheckPresenceOfResults();
     });
@@ -79,7 +82,7 @@ describe("ngHyd − calculate with linked parameters", () => {
         const Y = calcPage.getInputById("Y");
         await calcPage.setParamMode(Y, "link");
         const sel = await calcPage.getLinkedValueSelect(Y);
-        await calcPage.changeSelectValue(sel, 0);
+        await changeSelectValue(sel, 0);
         // vary W
         const W = calcPage.getInputById("W");
         await calcPage.setParamMode(W, "var");
@@ -102,7 +105,7 @@ describe("ngHyd − calculate with linked parameters", () => {
         const Y2 = calcPage.getInputById("Y");
         await calcPage.setParamMode(Y2, "link");
         const sel = await calcPage.getLinkedValueSelect(Y2);
-        await calcPage.changeSelectValue(sel, 0);
+        await changeSelectValue(sel, 0);
 
         await computeAndCheckPresenceOfResults();
     });
@@ -122,7 +125,7 @@ describe("ngHyd − calculate with linked parameters", () => {
         const Y2 = calcPage.getInputById("Y");
         await calcPage.setParamMode(Y2, "link");
         const sel = await calcPage.getLinkedValueSelect(Y2);
-        await calcPage.changeSelectValue(sel, 0);
+        await changeSelectValue(sel, 0);
 
         await computeAndCheckPresenceOfResults();
     });
@@ -142,7 +145,7 @@ describe("ngHyd − calculate with linked parameters", () => {
         const Y2 = calcPage.getInputById("Y");
         await calcPage.setParamMode(Y2, "link");
         const sel = await calcPage.getLinkedValueSelect(Y2);
-        await calcPage.changeSelectValue(sel, 0);
+        await changeSelectValue(sel, 0);
         // vary W
         const W = calcPage.getInputById("W");
         await calcPage.setParamMode(W, "var");
@@ -168,7 +171,7 @@ describe("ngHyd − calculate with linked parameters", () => {
         const Y2 = calcPage.getInputById("Y");
         await calcPage.setParamMode(Y2, "link");
         const sel = await calcPage.getLinkedValueSelect(Y2);
-        await calcPage.changeSelectValue(sel, 0);
+        await changeSelectValue(sel, 0);
 
         await computeAndCheckPresenceOfResults();
     });
@@ -304,4 +307,43 @@ describe("ngHyd − calculate with linked parameters", () => {
         expect(Number(volume)).toBeCloseTo(44.565, 3);
     });
 
+    it(" − a link target parameter should not be able to link to another parameter", async () => {
+        // create 1st PAB-Chute
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+        await listPage.clickMenuEntryForCalcType(12);
+        await browser.sleep(200);
+
+        //  upstream water level should not have link mode (only one calculator)
+        const Z1_1 = calcPage.getInputById("Z1");
+        expect(await calcPage.inputHasLinkModeButton(Z1_1)).toBe(false);
+
+        // create 2nd PAB-Chute
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+        await listPage.clickMenuEntryForCalcType(12);
+        await browser.sleep(200);
+
+        // back to 1st calculator
+        await navBar.clickCalculatorTab(0);
+        await browser.sleep(200);
+
+        //  upstream water level should have link mode (now there are 2 calculators)
+        expect(await calcPage.inputHasLinkModeButton(Z1_1)).toBe(true);
+
+        // back to 2nd calculator
+        await navBar.clickCalculatorTab(1);
+        await browser.sleep(200);
+
+        // link upstream water level in 2nd calculator to upstream water level in 1st one
+        const Z1_2 = calcPage.getInputById("Z1");
+        await calcPage.setParamMode(Z1_2, "link");
+
+        // back to 1st calculator
+        await navBar.clickCalculatorTab(0);
+        await browser.sleep(200);
+
+        //  upstream water level should not have link mode (already a link target)
+        expect(await calcPage.inputHasLinkModeButton(Z1_1)).toBe(false);
+    });
 });
diff --git a/e2e/calculator.e2e-spec.ts b/e2e/calculator.e2e-spec.ts
index 3c42955ec425f517b45919e36763cb9aa21b40a4..841a5304767c87dad05a9bb2ca9b601f349cee13 100644
--- a/e2e/calculator.e2e-spec.ts
+++ b/e2e/calculator.e2e-spec.ts
@@ -8,7 +8,7 @@ describe("ngHyd − calculator page", () => {
     let page: CalculatorPage;
     let listPage: ListPage;
 
-    beforeEach(() => {
+    beforeAll(() => {
         page = new CalculatorPage();
         listPage = new ListPage();
     });
diff --git a/e2e/calculator.po.ts b/e2e/calculator.po.ts
index 400f4b0abbc88cf80c09be86d6a159f9362823ea..d7552190a253a1a4fad88b89c59d3508acda60ff 100644
--- a/e2e/calculator.po.ts
+++ b/e2e/calculator.po.ts
@@ -1,4 +1,5 @@
-import { by, element, ElementFinder, browser } from "protractor";
+import { by, element, ElementFinder, browser, protractor, ElementArrayFinder, Key } from "protractor";
+import { scrollPageToTop, scrollToElement } from "./util.po";
 
 export class CalculatorPage {
 
@@ -25,10 +26,75 @@ export class CalculatorPage {
         return element(by.css("h1"));
     }
 
+    /**
+     * return all selects in the calculator which id is in the form "select_*"
+     */
+    getAllCalculatorSelects(): ElementArrayFinder {
+        return element.all(by.css("mat-select[id^=select_]")); // all mat-select with id starting with "select_"
+    }
+
+    /**
+     * get the option count of a select
+     */
+    async getMatselectOptionCount(select: string | ElementFinder) {
+        const sel = select instanceof ElementFinder ? select : element(by.id(select));
+        await scrollToElement(sel);
+
+        if (await sel.isPresent() && await sel.isDisplayed()) {
+            await sel.click();
+            const options = element.all(by.css(".cdk-overlay-container mat-option"));
+            await sel.sendKeys(protractor.Key.ESCAPE); // close dropdown
+            return await options.count();
+        }
+    }
+
+    /**
+     * get the text of the all given select options
+     */
+    async getMatselectOptionsText(select: string | ElementFinder): Promise<string[]> {
+        const sel = select instanceof ElementFinder ? select : element(by.id(select));
+        await scrollToElement(sel);
+
+        if (await sel.isPresent() && await sel.isDisplayed()) {
+            await sel.click();
+            const options = element.all(by.css(".cdk-overlay-container mat-option span"));
+            let res = [];
+            const nopt = await options.count();
+            for (let o = 0; o < nopt; o++) {
+                const opt = options.get(o);
+                res.push(await opt.getText())
+            }
+            await sel.sendKeys(protractor.Key.ESCAPE); // close dropdown
+
+            return res;
+        }
+    }
+
+    /**
+     * get select current option
+     */
+    async getSelectCurrentOption(select: ElementFinder): Promise<ElementFinder> {
+        const id = await select.getAttribute("id");
+        return element(by.css("mat-select#" + id + " div[id^=mat-select-value-]"))
+    }
+
+    /**
+     * get text of select current option
+     */
+    async getMatselectCurrentOptionText(select: ElementFinder): Promise<string> {
+        const currentOption = await this.getSelectCurrentOption(select);
+        return await currentOption.element(by.css("span span")).getText();
+    }
+
     getSelectById(id: string) {
         return element(by.id(id));
     }
 
+    async isMatSelectPresent(id: string) {
+        const sel: ElementFinder = element(by.css("mat-select#" + id));
+        return await sel.isPresent();
+    }
+
     async getSelectValueText(select: ElementFinder) {
         return await select.element(by.css(".mat-select-value-text > span")).getText();
     }
@@ -64,11 +130,11 @@ export class CalculatorPage {
     }
 
     getAddStructureButton() {
-        return element(by.css("structure-fieldset-container .hyd-window-btns button.add-structure"));
+        return element(by.css("fieldset-container.structure-fieldsetcontainer .hyd-window-btns button.add-structure"));
     }
 
     getCopyStructureButton() {
-        return element(by.css("structure-fieldset-container .hyd-window-btns button.copy-structure"));
+        return element(by.css("fieldset-container.structure-fieldsetcontainer .hyd-window-btns button.copy-structure"));
     }
 
     getAllLinkButtons() {
@@ -91,11 +157,11 @@ export class CalculatorPage {
         return element.all(by.css("fixedvar-results var-results table tbody tr"));
     }
 
-    scrollTo(elt: ElementFinder) {
-        browser.controlFlow().execute(function () {
-            browser.executeScript("arguments[0].scrollIntoView(true)", elt.getWebElement());
-        });
-    }
+    // scrollTo(elt: ElementFinder) {
+    //     browser.controlFlow().execute(function () {
+    //         browser.executeScript("arguments[0].scrollIntoView(true)", elt.getWebElement());
+    //     });
+    // }
 
     getFixedResultsTable() {
         return element(by.css(".fixed-results-inner-container table"));
@@ -115,6 +181,11 @@ export class CalculatorPage {
         return tr.element(by.css("td:nth-of-type(" + n + ")"));
     }
 
+    async isNgParamPresent(id: string) {
+        const inp: ElementFinder = element(by.css("ngparam-input input#" + id));
+        return await inp.isPresent();
+    }
+
     async inputHasCalcModeButton(input: ElementFinder) {
         // get parent (div.container)
         const container = await this.findParentContainer(input);
@@ -123,6 +194,14 @@ export class CalculatorPage {
         return await button.isPresent();
     }
 
+    async inputHasLinkModeButton(input: ElementFinder) {
+        // get parent (div.container)
+        const container = await this.findParentContainer(input);
+        // find radio buttons
+        const button: ElementFinder = container.element(by.css("mat-button-toggle.radio_link > button"));
+        return await button.isPresent();
+    }
+
     /**
      * @returns true if "fixed mode" button linked to an input is selected
      */
@@ -246,6 +325,7 @@ export class CalculatorPage {
     }
 
     async clickSaveCalcButton() {
+        await scrollPageToTop();
         return await element(by.css("#save-calc")).click();
     }
 
@@ -255,19 +335,12 @@ export class CalculatorPage {
     }
 
     // check that "compute" button is in given enabled/disabled state
-    checkCalcButtonEnabled(enabled: boolean) {
+    async checkCalcButtonEnabled(enabled: boolean) {
         const calcButton = this.getCalculateButton();
-        expect(calcButton.isEnabled()).toBe(enabled);
+        expect(await calcButton.isEnabled()).toBe(enabled);
         return calcButton;
     }
 
-    async changeSelectValue(elt: ElementFinder, index: number) {
-        await elt.click();
-        const optionId = ".cdk-overlay-container mat-option:nth-of-type(" + (index + 1) + ")";
-        const option = element(by.css(optionId));
-        await option.click();
-    }
-
     // find parent element of elt having class "container"
     async findParentContainer(elt: ElementFinder): Promise<ElementFinder> {
         let i = 8; // garde fous
@@ -287,12 +360,13 @@ export class CalculatorPage {
         const container = await this.findParentContainer(elt);
         // find radio buttons
         const button = container.element(by.css("mat-button-toggle.radio_" + mode + " > button"));
-        await browser.executeScript("window.scrollTo(0, 0);"); // sometimes button slides behind navbar and click() fails
+        await scrollPageToTop(); // sometimes button slides behind navbar and click() fails
         await button.click();
         // for "var" mode, close the modal
         if (mode === "var") {
             await browser.sleep(500); // wait for the modal to appear
-            await element(by.css("dialog-edit-param-values .mat-dialog-actions button")).click();
+            //await element(by.css("dialog-edit-param-values .mat-dialog-actions button")).click(); // clique "annuler" et non "valider" :
+            await element(by.css("dialog-edit-param-values .mat-dialog-actions button.mat-warn")).click();
             await browser.sleep(500); // wait for the navbar to reappear after modal dismissal
         }
     }
@@ -350,11 +424,17 @@ export class CalculatorPage {
     /**
      * check that an input is empty
      * @param id input id (parameter name)
+     * @param empty true to check input is empty, false to check it is NOT empty
      */
-    async checkEmptyInput(id: string) {
+    async checkEmptyInput(id: string, empty: boolean = true) {
         const inp = this.getInputById(id);
         const val = await inp.getAttribute("value");
-        expect(val).toEqual("");
+        if (empty) {
+            expect(val).toEqual("");
+        }
+        else {
+            expect(val).not.toEqual("");
+        }
     }
 
     /**
@@ -371,4 +451,22 @@ export class CalculatorPage {
             n++;
         }
     }
+
+    /**
+     * get help button related to calculator
+     */
+    getCalculatorHelpButton() {
+        return element(by.css("#help-calc"));
+    }
+
+    /**
+     * reliable input clearing
+     */
+    async clearInput(inp: ElementFinder) {
+        const txt = await inp.getAttribute('value');
+        const len = txt.length;
+        for (let n = 0; n < len; n++) {
+            await inp.sendKeys(Key.BACK_SPACE);
+        }
+    }
 }
diff --git a/e2e/check-translations.e2e-spec.ts b/e2e/check-translations.e2e-spec.ts
index ee07f746eee2f7af6095f1038d1fe99011599931..df1c7282e951a9cf5e4c4aff8f3bed3dba965fdd 100644
--- a/e2e/check-translations.e2e-spec.ts
+++ b/e2e/check-translations.e2e-spec.ts
@@ -25,69 +25,69 @@ describe("ngHyd − check translation of all calculators", () => {
         sideNav = new SideNav();
     });
 
-    // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/
-    const calcTypes = testedCalcTypes;
+    describe("", () => {
+        // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/
+        const calcTypes = testedCalcTypes;
 
-    // options of "Language" selector on preferences page
-    const langs = ["English", "Français"];
+        // options of "Language" selector on preferences page
+        const langs = ["English", "Français"];
 
-    // for each language
-    for (let i = 0; i < langs.length; i++) {
-        describe("language " + langs[i] + " − ", async () => {
+        // for each language
+        for (let i = 0; i < langs.length; i++) {
+            describe("language " + langs[i] + " − ", () => {
 
-            beforeAll(async () => {
-                await prefPage.navigateTo();
-                await prefPage.changeLanguage(i);
-                await browser.sleep(200);
-                await navBar.clickNewCalculatorButton();
-            });
-
-            beforeEach(function () {
-                jasmine.addMatchers({
-                    toContain: function () {
-                        return {
-                            compare: function (actual, expected) {
-                                const result = {
-                                    pass: actual.includes(expected),
-                                    message: "error" // short message to avoid whole page source being dumped on expect() failure
-                                };
-                                return result;
-                            }
-                        };
-                    }
+                beforeAll(async () => {
+                    await prefPage.navigateTo();
+                    await prefPage.changeLanguage(i);
+                    await browser.sleep(200);
+                    await navBar.clickNewCalculatorButton();
                 });
-            });
-
-            // for each calculator
-            for (const ct of calcTypes) {
-                it(" − check translations of calculator type [" + ct + "]", async () => {
-                    // click calculator button (instanciate)
-                    await listPage.clickMenuEntryForCalcType(ct);
 
-                    // just click the "compute" button with default values
-                    // check that "compute" button is active
-                    const calcButton = calcPage.getCalculateButton();
-                    const disabledState = await calcButton.getAttribute("disabled");
-                    if (!disabledState) {
-                        // click "compute" button
-                        await calcButton.click();
-                        // check that result is not empty
-                        const hasResults = await calcPage.hasResults();
-                        expect(hasResults).toBe(true);
-                    }
+                beforeEach(function () {
+                    jasmine.addMatchers({
+                        toContain: function () {
+                            return {
+                                compare: function (actual, expected) {
+                                    const result = {
+                                        pass: actual.includes(expected),
+                                        message: "error" // short message to avoid whole page source being dumped on expect() failure
+                                    };
+                                    return result;
+                                }
+                            };
+                        }
+                    });
+                });
 
-                    // check absence of "*** message not found" in whole DOM
-                    expect(await browser.getPageSource()).not.toContain("*** message not found", "missing translations found");
+                // for each calculator
+                for (const ct of calcTypes) {
+                    it(" − check translations of calculator type [" + ct + "]", async () => {
+                        // click calculator button (instanciate)
+                        await listPage.clickMenuEntryForCalcType(ct);
 
-                    // empty session
-                    await navBar.clickMenuButton();
-                    await browser.sleep(200);
-                    await sideNav.clickNewSessionButton();
-                    browser.sleep(200);
-                });
-            }
+                        // just click the "compute" button with default values
+                        // check that "compute" button is active
+                        const calcButton = calcPage.getCalculateButton();
+                        const disabledState = await calcButton.getAttribute("disabled");
+                        if (!disabledState) {
+                            // click "compute" button
+                            await calcButton.click();
+                            // check that result is not empty
+                            const hasResults = await calcPage.hasResults();
+                            expect(hasResults).toBe(true);
+                        }
 
-        });
-    }
+                        // check absence of "*** message not found" in whole DOM
+                        expect(await browser.getPageSource()).not.toContain("*** message not found", "missing translations found");
 
+                        // empty session
+                        await navBar.clickMenuButton();
+                        await browser.sleep(200);
+                        await sideNav.clickNewSessionButton();
+                        browser.sleep(200);
+                    });
+                }
+            });
+        }
+    });
 });
diff --git a/e2e/cloisons.e2e-spec.ts b/e2e/cloisons.e2e-spec.ts
index 2febdc57e4263fb9d97e1466736b8e2699942cf9..f08cac6a01a2276e7c90a15c4a3996de805b692a 100644
--- a/e2e/cloisons.e2e-spec.ts
+++ b/e2e/cloisons.e2e-spec.ts
@@ -3,6 +3,7 @@ import { CalculatorPage } from "./calculator.po";
 import { browser } from "protractor";
 import { Navbar } from "./navbar.po";
 import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue } from "./util.po";
 
 /**
  * Cloisons - différents tests qui n'ont pas tant de rapport que ça avec les cloisons :)
@@ -13,11 +14,14 @@ describe("ngHyd − cloisons", () => {
     let navBar: Navbar;
     let prefPage: PreferencesPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         calcPage = new CalculatorPage();
         navBar = new Navbar();
         prefPage = new PreferencesPage();
+    });
+
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await prefPage.disableEvilEmptyFields();
@@ -44,7 +48,7 @@ describe("ngHyd − cloisons", () => {
         await browser.sleep(300);
 
         // 4. change LoiDebit
-        await calcPage.changeSelectValue(calcPage.getSelectById("select_loidebit"), 1);
+        await changeSelectValue(calcPage.getSelectById("select_loidebit"), 1);
         await browser.sleep(300);
 
         // 5. check number of inputs in CALC mode
diff --git a/e2e/clone-all-calc.e2e-spec.ts b/e2e/clone-all-calc.e2e-spec.ts
index e55c8b3f461791ffad1b8207ee81070e7c2b8879..9c454e8644f5294c84bd93f2dae2b0f7fd0bf946 100644
--- a/e2e/clone-all-calc.e2e-spec.ts
+++ b/e2e/clone-all-calc.e2e-spec.ts
@@ -4,6 +4,7 @@ import { Navbar } from "./navbar.po";
 import { browser } from "protractor";
 import { PreferencesPage } from "./preferences.po";
 import { testedCalcTypes } from "./tested_calctypes";
+import { scrollPageToTop } from "./util.po";
 
 /**
  * Clone calculators
@@ -14,56 +15,61 @@ describe("ngHyd − clone all calculators with all possible <select> values", ()
     let navbar: Navbar;
     let prefPage: PreferencesPage;
 
-    beforeEach(async () => {
+    beforeAll(async () => {
         listPage = new ListPage();
         calcPage = new CalculatorPage();
         navbar = new Navbar();
         prefPage = new PreferencesPage();
+    });
+
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await prefPage.disableEvilEmptyFields();
         await browser.sleep(200);
     });
 
-    const calcTypes = testedCalcTypes;
+    describe("", () => {
 
-    // for each calculator
-    for (const ct of calcTypes) {
-        if (ct === 22) {
-            // omit 22 - Solveur is not tested here because it is not independent
-            continue;
-        }
-        describe(" − clone all variations of calculator type [" + ct + "]", async () => {
-            it("", async () => {
-                await navbar.clickNewCalculatorButton();
-                // click calculator button (instanciate)
-                await listPage.clickMenuEntryForCalcType(ct);
+        const calcTypes = testedCalcTypes;
 
-                // get all select IDs outside Structures
-                // get select IDs inside Structures
-                // @TODO set configuration to every combination of <select> options
+        // for each calculator
+        for (const ct of calcTypes) {
+            if (ct === 22) {
+                // omit 22 - Solveur is not tested here because it is not independent
+                continue;
+            }
+            describe(" − clone all variations of calculator type [" + ct + "]", () => {
+                it("", async () => {
+                    await navbar.clickNewCalculatorButton();
+                    // click calculator button (instanciate)
+                    await listPage.clickMenuEntryForCalcType(ct);
 
-                // modify all <input> values and store them
-                await calcPage.modifyAllInputValues();
-                const sourceValues = await calcPage.storeAllInputValues();
+                    // get all select IDs outside Structures
+                    // get select IDs inside Structures
+                    // @TODO set configuration to every combination of <select> options
 
-                // clone calculator
-                await browser.executeScript("window.scrollTo(0, 0);");
-                await calcPage.clickCloneCalcButton();
-                await browser.sleep(300);
+                    // modify all <input> values and store them
+                    await calcPage.modifyAllInputValues();
+                    const sourceValues = await calcPage.storeAllInputValues();
 
-                // check existence of the cloned module
-                expect(await navbar.getAllCalculatorTabs().count()).toBe(2);
+                    // clone calculator
+                    await scrollPageToTop();
+                    await calcPage.clickCloneCalcButton();
+                    await browser.sleep(300);
 
-                // @TODO check <select> values
+                    // check existence of the cloned module
+                    expect(await navbar.getAllCalculatorTabs().count()).toBe(2);
 
-                // read all <input> values and compare them to stored ones
-                const cloneValues = await calcPage.storeAllInputValues();
-                for (const k in cloneValues) {
-                    expect(cloneValues[k]).toBeCloseTo(sourceValues[k]);
-                }
-            });
-        });
-    }
+                    // @TODO check <select> values
 
+                    // read all <input> values and compare them to stored ones
+                    const cloneValues = await calcPage.storeAllInputValues();
+                    for (const k in cloneValues) {
+                        expect(cloneValues[k]).toBeCloseTo(sourceValues[k]);
+                    }
+                });
+            });
+        }
+    });
 });
diff --git a/e2e/clone-calc.e2e-spec.ts b/e2e/clone-calc.e2e-spec.ts
index c09edd3c8681301af749ec21aeda27dc34e51a54..68be6908009032977fbeb2e868faa585c098b3ac 100644
--- a/e2e/clone-calc.e2e-spec.ts
+++ b/e2e/clone-calc.e2e-spec.ts
@@ -1,29 +1,27 @@
-import { AppPage } from "./app.po";
 import { ListPage } from "./list.po";
 import { CalculatorPage } from "./calculator.po";
 import { Navbar } from "./navbar.po";
 import { browser } from "protractor";
 import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue, scrollPageToTop } from "./util.po";
 
 /**
  * Clone calculators
  */
 describe("ngHyd − clone a calculator", () => {
-    let startPage: AppPage;
     let listPage: ListPage;
     let calcPage: CalculatorPage;
     let navbar: Navbar;
     let prefPage: PreferencesPage;
 
-    beforeEach(() => {
-        startPage = new AppPage();
+    beforeAll(async () => {
         listPage = new ListPage();
         calcPage = new CalculatorPage();
-    });
-
-    beforeAll(async () => {
         prefPage = new PreferencesPage();
         navbar = new Navbar();
+    });
+
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await prefPage.disableEvilEmptyFields();
@@ -53,7 +51,7 @@ describe("ngHyd − clone a calculator", () => {
             k: 0.6,
             Ks: 42
         };
-        await calcPage.changeSelectValue(calcPage.getSelectById("select_section"), 3); // mode "parabolique"
+        await changeSelectValue(calcPage.getSelectById("select_section"), 3); // mode "parabolique"
         await calcPage.getInputById("k").clear();
         await calcPage.getInputById("k").sendKeys(sourceValues["k"]);
         await calcPage.getInputById("Ks").clear();
@@ -62,11 +60,11 @@ describe("ngHyd − clone a calculator", () => {
         const debitSP = calcPage.getInputById("Q");
         await calcPage.setParamMode(debitSP, "link");
         await browser.sleep(500);
-        await calcPage.changeSelectValue(calcPage.getSelectById("linked_Q"), 1); // "Courbe de remous"
+        await changeSelectValue(calcPage.getSelectById("linked_Q"), 1); // "Courbe de remous"
         await browser.sleep(500);
 
         // otherwise clickCloneCalcButton() fails with "Element is not clickable at point"
-        await browser.executeScript("window.scrollTo(0, 0);");
+        await scrollPageToTop();
         await calcPage.clickCloneCalcButton();
         await browser.sleep(500);
 
diff --git a/e2e/compute-reset-chained-links.e2e-spec.ts b/e2e/compute-reset-chained-links.e2e-spec.ts
index cd97f196a869fb33e9bbd911faa80d307aa248f7..9f402a1795289368108b56536afbd785a87f3db0 100644
--- a/e2e/compute-reset-chained-links.e2e-spec.ts
+++ b/e2e/compute-reset-chained-links.e2e-spec.ts
@@ -19,14 +19,13 @@ describe("ngHyd − compute then reset chained results − ", () => {
     let sidenav: SideNav;
     let prefPage: PreferencesPage;
 
-    function init() {
+    beforeAll(() => {
         startPage = new AppPage();
         calcPage = new CalculatorPage();
         navbar = new Navbar();
         sidenav = new SideNav();
         prefPage = new PreferencesPage();
-    }
-    beforeEach(init);
+    });
 
     it("when loading session-cascade-params.json, computation should not be chained, but results reset should be", async () => {
         // load session file
@@ -188,5 +187,4 @@ describe("ngHyd − compute then reset chained results − ", () => {
         expect(hasResults4).toBe(false);
 
     });
-
 });
diff --git a/e2e/cote-amont-aval-bief.e2e-spec.ts b/e2e/cote-amont-aval-bief.e2e-spec.ts
index e7a239a02149de505c45d7d9ff5c20ff74b0de2a..1c73805f02c786d1ab5f9425abd575623d558472 100644
--- a/e2e/cote-amont-aval-bief.e2e-spec.ts
+++ b/e2e/cote-amont-aval-bief.e2e-spec.ts
@@ -10,12 +10,14 @@ describe("ngHyd − up/downstream elevations of a reach", () => {
     let listPage: ListPage;
     let calcPage: CalculatorPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
+    });
 
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await browser.sleep(200);
diff --git a/e2e/courbe-remous-empty-fields.e2e-spec.ts b/e2e/courbe-remous-empty-fields.e2e-spec.ts
index 392075ce58b5949070ab5d8b3c147561ee19ae8c..4674b1c5cf1f37d92b0d9ae2ae4f848473b70c22 100644
--- a/e2e/courbe-remous-empty-fields.e2e-spec.ts
+++ b/e2e/courbe-remous-empty-fields.e2e-spec.ts
@@ -10,7 +10,7 @@ describe("Check fields are empty in 'backwater curves' calculator when created w
     let calcPage: CalculatorPage;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
diff --git a/e2e/diagramme-modules.e2e-spec.ts b/e2e/diagramme-modules.e2e-spec.ts
index a7a1b6c41dff8f7e924220ae2c835fa40f97afb3..87a6c8d0ab287bc2eae214a369ade63321572159 100644
--- a/e2e/diagramme-modules.e2e-spec.ts
+++ b/e2e/diagramme-modules.e2e-spec.ts
@@ -17,18 +17,20 @@ function removeMermaidIdFormat(id: string): string {
     return id;
 }
 
-describe("modules diagram", async () => {
+describe("modules diagram", () => {
     let navBar: Navbar;
     let listPage: ListPage;
     let sideNav: SideNav;
     let prefPage: PreferencesPage;
 
-    beforeEach(async () => {
+    beforeAll(async () => {
         listPage = new ListPage();
         navBar = new Navbar();
         sideNav = new SideNav();
         prefPage = new PreferencesPage();
+    });
 
+    beforeEach(async () => {
         await prefPage.navigateTo();
     });
 
diff --git a/e2e/documentation.e2e-spec.ts b/e2e/documentation.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a3a4f14d887f0e108ffe5dabed5a489f83b60ac8
--- /dev/null
+++ b/e2e/documentation.e2e-spec.ts
@@ -0,0 +1,76 @@
+import { ListPage } from "./list.po";
+import { CalculatorPage } from "./calculator.po";
+import { Navbar } from "./navbar.po";
+import { browser, by, element } from "protractor";
+import { PreferencesPage } from "./preferences.po";
+
+describe("documentation − ", () => {
+    let listPage: ListPage;
+    let calcPage: CalculatorPage;
+    let prefPage: PreferencesPage;
+    let navbar: Navbar;
+
+    beforeAll(() => {
+        calcPage = new CalculatorPage();
+        prefPage = new PreferencesPage();
+        navbar = new Navbar();
+        listPage = new ListPage();
+
+        // browser.manage().window().setPosition(2000, 30);
+    });
+
+    async function checkMathjaxInHelp(lang: number) {
+        // change language setup
+        await prefPage.navigateTo();
+        await browser.sleep(200);
+        await prefPage.changeLanguage(lang);
+        await browser.sleep(200);
+
+        // start page
+        await navbar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        // open "fish ladder: fall" calculator
+        await listPage.clickMenuEntryForCalcType(12);
+        await browser.sleep(200);
+
+        // click help
+        await calcPage.getCalculatorHelpButton().click();
+        await browser.sleep(200);
+
+        browser.getAllWindowHandles().then(async (handles) => {
+            const old = browser.ignoreSynchronization
+            browser.ignoreSynchronization = true; // deprecated but the only solution to work in the newly opened tab
+
+            // switch to help tab
+            browser.switchTo().window(handles[1]).then(async () => {
+                await browser.sleep(200);
+                // check Mathjax element is present
+                expect(await element(by.css("mjx-container")).isPresent()).toBe(true);
+            }).then(async () => {
+                // close help tab
+                // await browser.close();
+                // await browser.sleep(200);
+                // switch back to calculator (required to avoid failure of next language test)
+                await browser.switchTo().window(handles[0]);
+                await browser.sleep(200);
+                // browser.ignoreSynchronization = false;
+            }).then(async () => {
+                // switch back to calculator (required to avoid failure of next language test)
+                await browser.switchTo().window(handles[0]);
+                await browser.sleep(200);
+                // browser.ignoreSynchronization = false;
+                browser.ignoreSynchronization = old;
+            });
+        });
+    }
+
+    xit("check Mathjax formula are displayed in calculator French help", async () => {
+        debugger
+        await checkMathjaxInHelp(1); // fr
+    });
+
+    xit("check Mathjax formula are displayed in calculator English help", async () => {
+        await checkMathjaxInHelp(0); // en
+    });
+});
diff --git a/e2e/duplicate-results.e2e-spec.ts b/e2e/duplicate-results.e2e-spec.ts
index f0c0c57077cc6d366f9124172e718bd7d449f37a..bc2032048987c97af88b44aa51656aa056933091 100644
--- a/e2e/duplicate-results.e2e-spec.ts
+++ b/e2e/duplicate-results.e2e-spec.ts
@@ -10,12 +10,14 @@ describe("ngHyd − check that results are not duplicated", () => {
     let listPage: ListPage;
     let calcPage: CalculatorPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
+    });
 
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await browser.sleep(200);
diff --git a/e2e/examples-empty-fields.e2e-spec.ts b/e2e/examples-empty-fields.e2e-spec.ts
index 08f40c467f44678244027dbcd7487e56c16dc6aa..dd59b1a13654cabfe1e60d62b7af5c826b7d3561 100644
--- a/e2e/examples-empty-fields.e2e-spec.ts
+++ b/e2e/examples-empty-fields.e2e-spec.ts
@@ -1,8 +1,8 @@
 import { browser, by, element } from "protractor";
 import { CalculatorPage } from "./calculator.po";
-import { ListPage } from "./list.po";
 import { Navbar } from "./navbar.po";
 import { PreferencesPage } from "./preferences.po"
+import { changeSelectValue } from "./util.po";
 
 /**
  * check that fields are empty on creation
@@ -12,11 +12,13 @@ describe("ngHyd - Check that examples fields are not empty with 'empty fields on
     let navBar: Navbar;
     let calcPage: CalculatorPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
+    });
 
+    beforeEach(async () => {
         // enable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await browser.sleep(200);
@@ -60,7 +62,7 @@ describe("ngHyd - Check that examples fields are not empty with 'empty fields on
 
         // modify 1st structure discharge law
         const dischargeSelect = calcPage.getSelectById("select_loidebit");
-        await calcPage.changeSelectValue(dischargeSelect, 1);
+        await changeSelectValue(dischargeSelect, 1);
         await browser.sleep(200);
 
         // open initial dialog
@@ -81,11 +83,13 @@ describe("ngHyd - Check that examples work with 'empty fields on calculator crea
     let navBar: Navbar;
     let calcPage: CalculatorPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
+    });
 
+    beforeEach(async () => {
         // enable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await browser.sleep(200);
diff --git a/e2e/lechapt-calmon.e2e-spec.ts b/e2e/lechapt-calmon.e2e-spec.ts
index f0b991c6dc52504a8c725ef68306d2a1cc09b229..70ee97d0ae572e2dab970375ded4d4d7e4e92ade 100644
--- a/e2e/lechapt-calmon.e2e-spec.ts
+++ b/e2e/lechapt-calmon.e2e-spec.ts
@@ -3,6 +3,7 @@ import { browser, by } from "protractor";
 import { CalculatorPage } from "./calculator.po";
 import { PreferencesPage } from "./preferences.po";
 import { Navbar } from "./navbar.po";
+import { changeSelectValue } from "./util.po";
 
 /**
  * Check that created/cloned structures have empty fields when
@@ -19,7 +20,6 @@ describe("Lechapt&Calmon - ", () => {
         listPage = new ListPage();
         calcPage = new CalculatorPage();
         navBar = new Navbar();
-        browser.manage().window().setPosition(2000, 30);
     });
 
     beforeEach(async () => {
@@ -35,8 +35,8 @@ describe("Lechapt&Calmon - ", () => {
         await navBar.clickNewCalculatorButton();
         await browser.sleep(200);
 
-        // open Lechapt-Calmon calculator
-        await listPage.clickMenuEntryForCalcType(1);
+        // open Lechapt-Calmon (pressure loss) calculator
+        await listPage.clickMenuEntryForCalcType(35);
         await browser.sleep(200);
     }
 
@@ -45,7 +45,7 @@ describe("Lechapt&Calmon - ", () => {
 
         // select last material type
         const materialSelect = calcPage.getSelectById("select_material");
-        await calcPage.changeSelectValue(materialSelect, 8);
+        await changeSelectValue(materialSelect, 8);
         await browser.sleep(200);
 
         // run calculation
@@ -58,7 +58,7 @@ describe("Lechapt&Calmon - ", () => {
         const pl1 = await res1.all(by.css("td")).get(1).getText();
 
         // select first material type
-        await calcPage.changeSelectValue(materialSelect, 0);
+        await changeSelectValue(materialSelect, 0);
         await browser.sleep(200);
 
         // run calculation
diff --git a/e2e/link-parallel-devices.e2e-spec.ts b/e2e/link-parallel-devices.e2e-spec.ts
index 7d104289d097810bb09cc7d8df1453a5d6bfb085..768c320af8293100ffc7b273f7915d7565537ab6 100644
--- a/e2e/link-parallel-devices.e2e-spec.ts
+++ b/e2e/link-parallel-devices.e2e-spec.ts
@@ -10,17 +10,19 @@ describe("ngHyd − parallel structures with multiple linked parameters − ", (
     let prefPage: PreferencesPage;
     let navBar: Navbar;
 
-    async function init() {
+    beforeAll(() => {
         calcPage = new CalculatorPage();
         listPage = new ListPage();
         prefPage = new PreferencesPage();
         navBar = new Navbar();
+    });
+
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await prefPage.disableEvilEmptyFields();
         await browser.sleep(200);
-    }
-    beforeEach(init);
+    });
 
     it("when creating Parallel Structures, devices should be linkable to one another", async () => {
         await navBar.clickNewCalculatorButton();
@@ -45,5 +47,4 @@ describe("ngHyd − parallel structures with multiple linked parameters − ", (
         const nb3 = await calcPage.getAllLinkButtons().count();
         expect(nb3).toBe(6); // link buttons on children but not on parent
     });
-
 });
diff --git a/e2e/linked-parameter-section-type.e2e-spec.ts b/e2e/linked-parameter-section-type.e2e-spec.ts
index b42c13d0f8ad8249514e3880592938bd726888bb..36dce48fa96badaf2fe34d82d0b9f825b427f0b5 100644
--- a/e2e/linked-parameter-section-type.e2e-spec.ts
+++ b/e2e/linked-parameter-section-type.e2e-spec.ts
@@ -3,6 +3,7 @@ import { ListPage } from "./list.po";
 import { Navbar } from "./navbar.po";
 import { PreferencesPage } from "./preferences.po";
 import { CalculatorPage } from "./calculator.po";
+import { changeSelectValue } from "./util.po";
 
 describe("linked parameter in calculator with section - ", () => {
     let listPage: ListPage;
@@ -10,7 +11,7 @@ describe("linked parameter in calculator with section - ", () => {
     let calcPage: CalculatorPage;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
+    beforeAll( () => {
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
@@ -25,7 +26,7 @@ describe("linked parameter in calculator with section - ", () => {
     });
 
     it("modify section type", async () => {
-        browser.manage().window().setPosition(2000, 30);
+        // browser.manage().window().setPosition(2000, 30);
 
         // open first "parametric section" calculator
         await navBar.clickNewCalculatorButton();
@@ -42,7 +43,7 @@ describe("linked parameter in calculator with section - ", () => {
         await calcPage.setParamMode(inputQ, "link");
 
         // change section type
-        await calcPage.changeSelectValue(calcPage.getSelectById("select_section"), 3); // mode "parabolique"
+        await changeSelectValue(calcPage.getSelectById("select_section"), 3); // mode "parabolique"
 
         // check Q is still in linked mode
         expect(await calcPage.inputIsInLinkedMode(inputQ)).toBe(true);
diff --git a/e2e/list.e2e-spec.ts b/e2e/list.e2e-spec.ts
index 0530208eef5faed06428d890599a8931b5690975..2b2679a5c2245782a6384e52167ddc18475a677a 100644
--- a/e2e/list.e2e-spec.ts
+++ b/e2e/list.e2e-spec.ts
@@ -7,7 +7,7 @@ import { element, by } from "protractor";
 describe("ngHyd − list page", () => {
     let page: ListPage;
 
-    beforeEach(() => {
+    beforeAll(() => {
         page = new ListPage();
     });
 
diff --git a/e2e/load-linked-params.e2e-spec.ts b/e2e/load-linked-params.e2e-spec.ts
index dd8253971f81f12342f83782ebb351145758157d..32d976afff7f5f60b7bced7903b445a18b221e85 100644
--- a/e2e/load-linked-params.e2e-spec.ts
+++ b/e2e/load-linked-params.e2e-spec.ts
@@ -1,5 +1,3 @@
-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";
@@ -12,22 +10,17 @@ import { PreferencesPage } from "./preferences.po";
  * @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 prefPage: PreferencesPage;
     let navbar: Navbar;
     let sidenav: SideNav;
 
-    function init() {
-        startPage = new AppPage();
+    beforeAll(()=> {
         calcPage = new CalculatorPage();
         prefPage = new PreferencesPage();
         navbar = new Navbar();
         sidenav = new SideNav();
-        listPage = new ListPage();
-    }
-    beforeAll(init);
+    });
 
     beforeEach(async () => {
         // force language to prevent issues due to default browser language
diff --git a/e2e/load-malformed-files.e2e-spec.ts b/e2e/load-malformed-files.e2e-spec.ts
index a5c0649b98ba82710b4f92e3e345071a174bf833..9204d87b62129b3438117d0badc4713f2ad6775e 100644
--- a/e2e/load-malformed-files.e2e-spec.ts
+++ b/e2e/load-malformed-files.e2e-spec.ts
@@ -1,4 +1,3 @@
-import { AppPage } from "./app.po";
 import { Navbar } from "./navbar.po";
 import { SideNav } from "./sidenav.po";
 import { browser, element, by } from "protractor";
@@ -13,18 +12,15 @@ import { PreferencesPage } from "./preferences.po";
  * @WARNING error messages are tested in french
  */
 describe("ngHyd − load malformed session files − ", () => {
-    let startPage: AppPage;
     let navbar: Navbar;
     let sidenav: SideNav;
     let prefPage: PreferencesPage;
 
-    function init() {
-        startPage = new AppPage();
+    beforeAll(() => {
         navbar = new Navbar();
         sidenav = new SideNav();
         prefPage = new PreferencesPage();
-    }
-    beforeAll(init);
+    });
 
     beforeEach(async () => {
         // force language to prevent issues due to default browser language
@@ -81,5 +77,4 @@ describe("ngHyd − load malformed session files − ", () => {
         const err = element(by.css(".file-problem .mat-list-item-content"));
         expect(await err.getText()).toContain("Mauvaise version du format de fichier");
     });
-
 });
diff --git a/e2e/load-save-session.e2e-spec.ts b/e2e/load-save-session.e2e-spec.ts
index 6535d326565c835c536cb993be69d7025922ca9d..b565723752ae37daed7b3f2a299c49536908b7a7 100644
--- a/e2e/load-save-session.e2e-spec.ts
+++ b/e2e/load-save-session.e2e-spec.ts
@@ -3,21 +3,104 @@ import { ListPage } from "./list.po";
 import { CalculatorPage } from "./calculator.po";
 import { Navbar } from "./navbar.po";
 import { SideNav } from "./sidenav.po";
-import { browser } from "protractor";
+import { browser, by, element } from "protractor";
 import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue, expectNumber } from "./util.po";
+
+const fs = require("fs");
+const path = require("path");
+const os = require("os");
+
+let startPage: AppPage;
+let listPage: ListPage;
+let calcPage: CalculatorPage;
+let navbar: Navbar;
+let sidenav: SideNav;
+let prefPage: PreferencesPage;
+
+function findDownloadedFile(filename: string): string {
+    const downloadDirs = ["Téléchargements", "Downloads", "/tmp"];
+    for (const d of downloadDirs) {
+        const download_prefix = d.charAt(0) === "/" ? d : path.resolve(os.homedir(), d);
+        const fp = path.resolve(download_prefix, filename);
+        if (fs.existsSync(fp)) {
+            return fp;
+        }
+    }
+}
+
+function deleteDownloadedFile(filename: string) {
+    // see: https://stackoverflow.com/questions/21935696/protractor-e2e-test-case-for-downloading-pdf-file
+    const fp = findDownloadedFile(filename);
+    if (fp !== undefined) {
+        // Make sure the browser doesn't have to rename the download.
+        fs.unlinkSync(fp);
+    }
+}
+
+async function saveSession(): Promise<string> {
+    const sessionFile = "session.json";
+    deleteDownloadedFile(sessionFile);
+
+    await calcPage.clickSaveCalcButton();
+    await browser.sleep(500);
+
+    // cf. protractor.conf.fs, exports.config.capabilities.chromeOptions.prefs.download.default_directory
+    // protractor.conf.fs/exports.config.capabilities.chromeOptions.prefs.download.default_directory DOES NOT WORK !
+
+    // Le code laissé en commentaire tente de corriger un bug :
+    // il s'écoule 40 secondes entre le clic sur le bouton menu (en haut à gauche) et l'ouverture du sidenav.
+    // Ceci ne se produit que lorsqu'on sauve effectivement la session : si on annule la sauvegarde, il n'y a pas de délai.
+    // https://stackoverflow.com/questions/75235558/delay-after-downloading-a-file-in-protractor-test
+
+    //browser.manage().timeouts().implicitlyWait(100);
+    //browser.ignoreSynchronization = true;
+    // await browser.waitForAngularEnabled(false);
+
+    if (true) {
+        await calcPage.getSaveSessionButton().click();
+    } else {
+        const cancel = element(by.css("dialog-save-session button.mat-primary"));
+        await cancel.click();
+    }
+    await browser.sleep(200);
+    // browser.ignoreSynchronization = false;
+    // await browser.waitForAngularEnabled(true);
+
+    // browser.executeScript('window.stop();');
+
+    // const wins = await browser.driver.getAllWindowHandles();
+    // await browser.switchTo().window(wins[0]);
+
+    // await browser.switchTo().activeElement();
+
+    // const bd = element(by.css("body"));
+    // await browser.actions().mouseMove(bd, { x: 0, y: 0 }).click().perform();
+
+    // await navbar.clickCalculatorTab(0);
+    // await browser.sleep(200);
+
+    // browser.actions().sendKeys(protractor.Key.ESCAPE).perform();
+
+    console.log("saveSession() saved to ", findDownloadedFile(sessionFile));
+    return findDownloadedFile(sessionFile);
+}
+
+async function loadSession(path: string) {
+    await navbar.clickMenuButton();
+    await browser.sleep(200);
+
+    await sidenav.clickLoadSessionButton();
+    await browser.sleep(200);
+
+    await sidenav.loadSessionFile(path);
+}
 
 /**
  * Save and load (serialise and unserialise) calculators to/from JSON files
  */
 describe("ngHyd − save and load sessions", () => {
-    let startPage: AppPage;
-    let listPage: ListPage;
-    let calcPage: CalculatorPage;
-    let navbar: Navbar;
-    let sidenav: SideNav;
-    let prefPage: PreferencesPage;
-
-    beforeEach(() => {
+    beforeAll(() => {
         startPage = new AppPage();
         listPage = new ListPage();
         calcPage = new CalculatorPage();
@@ -26,17 +109,16 @@ describe("ngHyd − save and load sessions", () => {
         prefPage = new PreferencesPage();
     });
 
+    beforeEach(() => {
+        jasmine.DEFAULT_TIMEOUT_INTERVAL = 45 * 60 * 1000; // 45 min
+        browser.manage().window().setPosition(2000, 30);
+    });
+
     it("when loading session-6-calc.test.json file from home page, 6 calculators should be loaded", async () => {
         await startPage.navigateTo();
 
-        await navbar.clickMenuButton();
-        await browser.sleep(200);
-
-        await sidenav.clickLoadSessionButton();
-        await browser.sleep(200);
-
-        await sidenav.loadSessionFile("./session/session-6-calc.test.json");
-        await browser.sleep(200);
+        await loadSession("./session/session-6-calc.test.json");
+        await browser.sleep(1000);
 
         expect(await navbar.getAllCalculatorTabs().count()).toBe(6);
     });
@@ -44,13 +126,7 @@ describe("ngHyd − save and load sessions", () => {
     it("when loading session-optional-params.test.json file from home page, the calculator should be loaded", async () => {
         await startPage.navigateTo();
 
-        await navbar.clickMenuButton();
-        await browser.sleep(200);
-
-        await sidenav.clickLoadSessionButton();
-        await browser.sleep(200);
-
-        await sidenav.loadSessionFile("./session/session-optional-params.test.json");
+        await loadSession("./session/session-optional-params.test.json");
         await browser.sleep(200);
 
         expect(await navbar.getAllCalculatorTabs().count()).toBe(1);
@@ -66,31 +142,108 @@ describe("ngHyd − save and load sessions", () => {
         await listPage.clickMenuEntryForCalcType(2); // Section paramétrée
         await browser.sleep(500);
 
-        await calcPage.changeSelectValue(calcPage.getSelectById("select_section"), 2); // mode "trapezoidal"
+        await changeSelectValue(calcPage.getSelectById("select_section"), 2); // mode "trapezoidal"
 
         await calcPage.getInputById("Ks").clear(); // coefficient de Strickler
-        await browser.sleep(1000);
+        await browser.sleep(200);
         await calcPage.getInputById("Ks").sendKeys("42");
-        await browser.sleep(1000);
-
-        await calcPage.clickSaveCalcButton();
-
-        // see: https://stackoverflow.com/questions/21935696/protractor-e2e-test-case-for-downloading-pdf-file
-        const fs = require("fs");
-        const path = require("path");
-        const os = require("os");
-        const filename = path.resolve(os.homedir(), "Téléchargements/session.json");
-        if (fs.existsSync(filename)) {
-            // Make sure the browser doesn't have to rename the download.
-            fs.unlinkSync(filename);
-        }
+        await browser.sleep(200);
 
-        await calcPage.getSaveSessionButton().click();
-        await browser.sleep(1000);
+        const filename = await saveSession();
+        await browser.sleep(500);
         const fileContent = fs.readFileSync(filename, { encoding: "utf8" });
+        await browser.sleep(200);
 
         expect(fileContent).toContain(`"nodeType":"SectionTrapeze"`);
         expect(fileContent).toContain(`{"symbol":"Ks","mode":"SINGLE","value":42}`);
     });
 
+    xit("select value must be recovered when loading a session file", async () => {
+        // start page
+        await startPage.navigateTo();
+        await browser.sleep(200);
+
+        const calcTypes = await listPage.getAvailableCalcTypes();
+
+        const excludedCalculators = [
+            34, // vérificateur (nécessite d'ouvrir plusieurs calculettes)
+        ];
+
+        for (let i = 0; i < calcTypes.length; i++) {
+            const ct = calcTypes[i];
+            if (!excludedCalculators.includes(ct)) {
+                if (i == 0) {
+                    // enable evil option "empty fields on module creation"
+                    await prefPage.navigateTo();
+                    await prefPage.disableEvilEmptyFields();
+                    await browser.sleep(200);
+
+                    // start page
+                    await navbar.clickNewCalculatorButton();
+                    await browser.sleep(200);
+                }
+                else {
+                    // empty session
+                    await navbar.clickMenuButton();
+                    await browser.sleep(200);
+                    await sidenav.clickNewSessionButton();
+                    await browser.sleep(200);
+                }
+
+                // open calculator
+                await listPage.clickMenuEntryForCalcType(ct);
+                await browser.sleep(200);
+
+                // detect selects
+                const selects = calcPage.getAllCalculatorSelects();
+                const nsel = await selects.count();
+                for (let s = 0; s < nsel; s++) {  // /!\ ElementArrayFinder.each() is ASYNCHRONOUS !! https://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.each
+                    const sel = selects.get(s);
+                    const selId = await sel.getAttribute("id");
+
+                    const options = await calcPage.getMatselectOptionsText(sel);
+                    const optionCount = options.length;
+
+                    if (optionCount > 0) {
+                        // index of current selected option
+                        const optTxt = await calcPage.getMatselectCurrentOptionText(sel);
+                        const ind = options.indexOf(optTxt);
+
+                        // select next select option (optionally looping)
+                        const nextInd = (ind + 1) % optionCount;
+                        await changeSelectValue(sel, nextInd);
+                        await browser.sleep(200);
+
+                        // save session
+                        const filename = await saveSession();
+                        await browser.sleep(200);
+
+                        // load session
+                        await loadSession(filename); // bug here : the click on the menu button (top left) takes 40s to take effect and open the side nav!)
+                        await browser.sleep(200);
+                        // the displayed calculator is now the loaded one
+
+                        // check the calculator has been loaded
+                        expectNumber(`calc ${ct} select ${selId} : num calcs`, await navbar.getCalculatorEntriesCount(), 2);
+
+                        // check the select in the loaded session points to the same option
+                        const sel2 = calcPage.getSelectById(selId);
+
+                        // check the same option is in the select
+                        const optTxt2 = await calcPage.getMatselectCurrentOptionText(sel2);
+                        await browser.sleep(100);
+                        const ind2 = options.indexOf(optTxt2);
+                        expectNumber(`calc ${ct} select ${selId} : opt index`, ind2, nextInd);
+
+                        // close last calculator (the loaded one)
+                        await navbar.middleClickCalculatorTab(1);
+                        await browser.sleep(200);
+
+                        // check last calculator has been closed
+                        expectNumber(`calc ${ct} select ${selId} : num calcs(2)`, await navbar.getCalculatorEntriesCount(), 1);
+                    }
+                }
+            }
+        }
+    });
 });
diff --git a/e2e/navbar.po.ts b/e2e/navbar.po.ts
index 4fd34062accddf803373854bdaa30e18aa968298..814da4e216a54d63cb9e2bc443e9786a746a8964 100644
--- a/e2e/navbar.po.ts
+++ b/e2e/navbar.po.ts
@@ -31,8 +31,11 @@ export class Navbar {
         const dropDown = element(by.css("mat-select#selectCalculator"));
         if (await dropDown.isPresent() && await dropDown.isDisplayed()) {
             await dropDown.click();
-            const optionId = ".cdk-overlay-container mat-option#mat-option-" + n;
-            const option = element(by.css(optionId));
+
+            // 1st option is not necessarly "mat-option-0"...
+            const options = element.all(by.css(".cdk-overlay-container mat-option"));
+            const option = options.get(n);
+
             await option.click();
         } else {
             const tabs = this.getAllCalculatorTabs();
diff --git a/e2e/notes.e2e-spec.ts b/e2e/notes.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dd045409966b325d045dff971bcfe7c6441c9f25
--- /dev/null
+++ b/e2e/notes.e2e-spec.ts
@@ -0,0 +1,92 @@
+import { browser, by, element } from "protractor";
+import { AppPage } from "./app.po";
+import { Navbar } from "./navbar.po";
+import { ListPage } from "./list.po";
+import { SideNav } from "./sidenav.po";
+
+describe("check calculator notes", () => {
+    let startPage: AppPage;
+    let navBar: Navbar;
+    let listPage: ListPage;
+    let sidenav: SideNav;
+
+    beforeAll(() => {
+        startPage = new AppPage();
+        navBar = new Navbar();
+        listPage = new ListPage();
+        sidenav = new SideNav();
+    });
+
+    it(" - notes should display properly when opened from a calculator", async () => {
+        await startPage.navigateTo();
+        await browser.sleep(200);
+
+        // open PAB: chute calculator
+        await listPage.clickMenuEntryForCalcType(12);
+        await browser.sleep(200);
+
+        // open notes
+        await navBar.clickMenuButton();
+        await browser.sleep(200);
+        await sidenav.clickNotesButton();
+        await browser.sleep(200);
+
+        // input some text
+        const ta = element(by.css("textarea"));
+        await ta.clear();
+        await ta.sendKeys("azerty123");
+        await browser.sleep(200);
+
+        // reopen calculator
+        await navBar.openNthCalculator(0);
+        await browser.sleep(200);
+
+        // reopen notes
+        await navBar.clickMenuButton();
+        await browser.sleep(200);
+        await sidenav.clickNotesButton();
+        await browser.sleep(200);
+
+        // check text
+        const md = element(by.css("markdown p"));
+        await browser.sleep(200);
+        expect(await md.getText()).toEqual("azerty123");
+    });
+
+    it(" - notes should display properly when opened from modules diagram", async () => {
+        await startPage.navigateTo();
+        await browser.sleep(200);
+
+        // open PAB: chute calculator
+        await listPage.clickMenuEntryForCalcType(12);
+        await browser.sleep(200);
+
+        // open notes
+        await navBar.clickMenuButton();
+        await browser.sleep(200);
+        await sidenav.clickNotesButton();
+        await browser.sleep(200);
+
+        // input some text
+        const ta = element(by.css("textarea"));
+        await ta.clear();
+        await ta.sendKeys("azerty123");
+        await browser.sleep(200);
+
+        // open modules diagram
+        await navBar.clickMenuButton();
+        await browser.sleep(200);
+        await sidenav.clickDiagramButton();
+        await browser.sleep(200);
+
+        // open notes
+        const notesLink = element(by.css("#show-notes a"));
+        notesLink.click();
+        await browser.sleep(200);
+
+        // check text
+        const md = element(by.css("markdown p"));
+        await browser.sleep(200);
+        expect(await md.getText()).toEqual("azerty123");
+    });
+});
diff --git a/e2e/ouvrages-empty-fields.e2e-spec.ts b/e2e/ouvrages-empty-fields.e2e-spec.ts
index 4a28316f8c5a2001f778241fd31c211da1d00fec..06891af7e8aa9c702cf84fd640ec0bff8e260bec 100644
--- a/e2e/ouvrages-empty-fields.e2e-spec.ts
+++ b/e2e/ouvrages-empty-fields.e2e-spec.ts
@@ -1,9 +1,9 @@
 import { ListPage } from "./list.po";
 import { browser, by, element } from "protractor";
 import { CalculatorPage } from "./calculator.po";
-import { AppPage } from "./app.po";
 import { PreferencesPage } from "./preferences.po";
 import { Navbar } from "./navbar.po";
+import { changeSelectValue } from "./util.po";
 
 /**
  * Check that created/cloned structures have empty fields when
@@ -51,7 +51,7 @@ describe("ngHyd - check that created/cloned structures have empty fields - ", ()
 
         // change 1st structure type to rectangular weir
         const structSelect = calcPage.getSelectById("select_structure");
-        await calcPage.changeSelectValue(structSelect, 1);
+        await changeSelectValue(structSelect, 1);
         await browser.sleep(200);
 
         // check 1st structure empty fields
@@ -91,7 +91,7 @@ describe("ngHyd - check that created/cloned structures have empty fields - ", ()
 
         // change 1st structure type to rectangular weir
         const structSelect = calcPage.getSelectById("select_structure");
-        await calcPage.changeSelectValue(structSelect, 1);
+        await changeSelectValue(structSelect, 1);
         await browser.sleep(200);
 
         // copy structure
@@ -102,7 +102,7 @@ describe("ngHyd - check that created/cloned structures have empty fields - ", ()
         // change 2nd structure type to rectangular gate
         const selects = await element.all(by.css("mat-select#select_structure"));
         const structSelect2 = selects[1];
-        await calcPage.changeSelectValue(structSelect2, 5);
+        await changeSelectValue(structSelect2, 5);
         await browser.sleep(200);
 
         // check empty fields
@@ -135,12 +135,12 @@ describe("ngHyd - check that created/cloned structures have empty fields - ", ()
 
         // change 1st structure type to rectangular weir
         const structSelect = calcPage.getSelectById("select_structure");
-        await calcPage.changeSelectValue(structSelect, 1);
+        await changeSelectValue(structSelect, 1);
         await browser.sleep(200);
 
         // change discharge law to Larinier
         const dischargeSelect = calcPage.getSelectById("select_loidebit");
-        await calcPage.changeSelectValue(dischargeSelect, 3);
+        await changeSelectValue(dischargeSelect, 3);
         await browser.sleep(200);
 
         // check empty fields
diff --git a/e2e/pab-cloisons-empty-fields.e2e-spec.ts b/e2e/pab-cloisons-empty-fields.e2e-spec.ts
index 6b19d4c3e901ed6d6a0fd2a13743c82d17b00a6b..749c1708e9af362a270b6b75ac11c069c77dde5a 100644
--- a/e2e/pab-cloisons-empty-fields.e2e-spec.ts
+++ b/e2e/pab-cloisons-empty-fields.e2e-spec.ts
@@ -1,8 +1,7 @@
 import { ListPage } from "./list.po";
 import { PreferencesPage } from "./preferences.po";
-import { browser, by, element } from "protractor";
+import { browser } from "protractor";
 import { CalculatorPage } from "./calculator.po";
-import { AppPage } from "./app.po";
 import { Navbar } from "./navbar.po";
 
 /**
diff --git a/e2e/pab.e2e-spec.ts b/e2e/pab.e2e-spec.ts
index 2f75f8df9d326d084d4302160a18b7ff431de7ac..00e798f1e17ec731bb07c1cbfd7536e7109231f7 100644
--- a/e2e/pab.e2e-spec.ts
+++ b/e2e/pab.e2e-spec.ts
@@ -5,6 +5,7 @@ import { browser, by, element } from "protractor";
 import { AppPage } from "./app.po";
 import { SideNav } from "./sidenav.po";
 import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue, scrollPageToTop } from "./util.po";
 
 /**
  * Clone calculators
@@ -17,7 +18,7 @@ describe("ngHyd − Passe à Bassins", () => {
     let sidenav: SideNav;
     let prefPage: PreferencesPage;
 
-    beforeEach(() => {
+    beforeAll(() => {
         startPage = new AppPage();
         listPage = new ListPage();
         calcPage = new CalculatorPage();
@@ -26,8 +27,7 @@ describe("ngHyd − Passe à Bassins", () => {
         prefPage = new PreferencesPage();
     });
 
-    describe("create PAB - ", async () => {
-
+    describe("create PAB - ", () => {
         beforeEach(async () => {
             // disable evil option "empty fields on module creation"
             await prefPage.navigateTo();
@@ -124,7 +124,7 @@ describe("ngHyd − Passe à Bassins", () => {
             await calcButtonCl.click();
 
             // make sure "Generate PAB" button is visible (it might be hidden behind navbar)
-            await browser.executeScript("window.scrollTo(0, 0);");
+            await scrollPageToTop();
             // generate PAB
             const genButton = calcPage.getGeneratePabButton();
             await genButton.isPresent();
@@ -148,7 +148,7 @@ describe("ngHyd − Passe à Bassins", () => {
 
     });
 
-    describe("generate PAB - ", async () => {
+    describe("generate PAB - ", () => {
 
         beforeEach(async () => {
             // disable evil option "empty fields on module creation"
@@ -182,7 +182,7 @@ describe("ngHyd − Passe à Bassins", () => {
             await calcButtonCl.click();
 
             // make sure "Generate PAB" button is visible (it might be hidden behind navbar)
-            await browser.executeScript("window.scrollTo(0, 0);");
+            await scrollPageToTop();
             // create PAB from it, changing modal parameters
             const genButton = calcPage.getGeneratePabButton();
             await genButton.click();
@@ -227,11 +227,9 @@ describe("ngHyd − Passe à Bassins", () => {
             const hasResults = await calcPage.hasResults();
             expect(hasResults).toBe(true);
         });
-
     });
 
-    describe("load session files - ", async () => {
-
+    describe("load session files - ", () => {
         it("complete example of all PAB modules", async () => {
             await startPage.navigateTo();
             // load
@@ -265,11 +263,9 @@ describe("ngHyd − Passe à Bassins", () => {
             // check absence of logs
             expect(await calcPage.nbLogEntries()).toBe(0);
         });
-
     });
 
-    describe("load regulated variated PAB with calc errors - ", async () => {
-
+    describe("load regulated variated PAB with calc errors - ", () => {
         it("should display logs", async () => {
             await startPage.navigateTo();
             // load
@@ -296,12 +292,10 @@ describe("ngHyd − Passe à Bassins", () => {
 
             // change iteration
             const pve = calcPage.getSelectById("pab-variating-element");
-            calcPage.changeSelectValue(pve, 3);
+            await changeSelectValue(pve, 3);
             await browser.sleep(300);
             // check absence of logs
             expect(await calcPage.nbLogEntries()).toBe(2);
         });
-
     });
-
 });
diff --git a/e2e/parallel-structures.e2e-spec.ts b/e2e/parallel-structures.e2e-spec.ts
index 44312c6bf5344e7d8eed582884fa8220d95121e8..0653fbe8514b7875858231e3a31b57c184430689 100644
--- a/e2e/parallel-structures.e2e-spec.ts
+++ b/e2e/parallel-structures.e2e-spec.ts
@@ -1,6 +1,6 @@
 import { ListPage } from "./list.po";
 import { Navbar } from "./navbar.po";
-import { browser, by, element } from "protractor";
+import { browser } from "protractor";
 import { CalculatorPage } from "./calculator.po";
 import { PreferencesPage } from "./preferences.po";
 
@@ -10,7 +10,7 @@ describe("Check calculated parameter remains the same when copying a structure",
     let calcPage: CalculatorPage;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
diff --git a/e2e/predam-empty-fields.e2e-spec.ts b/e2e/predam-empty-fields.e2e-spec.ts
index 8c1cfb79b5c2ff8309cae6e1814b9c0408dfb02c..1c9f5dd983dc8f8a9fe6724a151d8705c05bb54a 100644
--- a/e2e/predam-empty-fields.e2e-spec.ts
+++ b/e2e/predam-empty-fields.e2e-spec.ts
@@ -13,12 +13,14 @@ describe("ngHyd − check that predam fields are empty", () => {
     let navBar: Navbar;
     let calcPage: CalculatorPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
+    });
 
+    beforeEach(async () => {
         // enable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await browser.sleep(200);
diff --git a/e2e/predam-log.e2e-spec.ts b/e2e/predam-log.e2e-spec.ts
index cb06046b709d03132c176a13c38a1ac1e80889fc..7b3a51cf7ecbb461444e501f658590138939c40d 100644
--- a/e2e/predam-log.e2e-spec.ts
+++ b/e2e/predam-log.e2e-spec.ts
@@ -1,4 +1,4 @@
-import { browser, by, element } from "protractor";
+import { browser } from "protractor";
 import { CalculatorPage } from "./calculator.po";
 import { ListPage } from "./list.po";
 import { Navbar } from "./navbar.po";
@@ -10,12 +10,14 @@ describe("check that low iteration count leads to log messages", () => {
     let navBar: Navbar;
     let calcPage: CalculatorPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
+    });
 
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await browser.sleep(200);
diff --git a/e2e/preferences.e2e-spec.ts b/e2e/preferences.e2e-spec.ts
index e2ac77205912b6c15f1df6487843fbf61aef4658..e7c3d95f34fab443b93ef0ea36384701b9a88272 100644
--- a/e2e/preferences.e2e-spec.ts
+++ b/e2e/preferences.e2e-spec.ts
@@ -7,7 +7,7 @@ import { browser } from "protractor";
 describe("ngHyd − preferences page", () => {
     let page: PreferencesPage;
 
-    beforeEach(() => {
+    beforeAll(() => {
         page = new PreferencesPage();
     });
 
diff --git a/e2e/preferences.po.ts b/e2e/preferences.po.ts
index 264ebb84fdc7f9ab570bb8524e7f61ccd26adb02..39604b8cbc5cf02b2644a058c342b032d61813da 100644
--- a/e2e/preferences.po.ts
+++ b/e2e/preferences.po.ts
@@ -1,4 +1,5 @@
 import { browser, by, element, ElementFinder } from "protractor";
+import { changeSelectValue } from "./util.po";
 
 export class PreferencesPage {
     navigateTo() {
@@ -43,10 +44,7 @@ export class PreferencesPage {
 
     async changeLanguage(index: number) {
         const select = this.getLanguageSelect();
-        await select.click();
-        const optionId = ".cdk-overlay-container mat-option#mat-option-" + index;
-        const option = element(by.css(optionId));
-        await option.click();
+        await changeSelectValue(select, index);
     }
 
     async enableEvilEmptyFields() {
@@ -65,6 +63,23 @@ export class PreferencesPage {
         }
     }
 
+    /**
+     * enable/disable option "empty fields on module creation"
+     * @param b true to enable "empty fields on module creation" option, false to disable it (fill fields with default values)
+     */
+    async setEmptyFields(b: boolean) {
+        await this.navigateTo();
+        await browser.sleep(200);
+
+        if (b) {
+            await this.enableEvilEmptyFields();
+        }
+        else {
+            await this.disableEvilEmptyFields();
+        }
+        await browser.sleep(200);
+    }
+
     async setIterationCount(n: number) {
         const input = this.getInputFromName("nmi");
         input.clear();
diff --git a/e2e/pressure-loss-empty-fields.e2e-spec.ts b/e2e/pressure-loss-empty-fields.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f02085bc3cdfb508d963a304eaa916aa349e25e9
--- /dev/null
+++ b/e2e/pressure-loss-empty-fields.e2e-spec.ts
@@ -0,0 +1,41 @@
+import { ListPage } from "./list.po";
+import { Navbar } from "./navbar.po";
+import { browser } from "protractor";
+import { CalculatorPage } from "./calculator.po";
+import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue } from "./util.po";
+
+describe("Check fields are empty in 'pressure loss' calculator when created with 'empty fields' option -", () => {
+    let listPage: ListPage;
+    let navBar: Navbar;
+    let calcPage: CalculatorPage;
+    let prefPage: PreferencesPage;
+
+    beforeAll( () => {
+        listPage = new ListPage();
+        navBar = new Navbar();
+        calcPage = new CalculatorPage();
+        prefPage = new PreferencesPage();
+    });
+
+    beforeEach(async () => {
+        // enable evil option "empty fields on module creation"
+        await prefPage.navigateTo();
+        await prefPage.enableEvilEmptyFields();
+        await browser.sleep(200);
+    });
+
+    it("with Lechapt-Calmon pressure loss law", async () => {
+        // open "pressure loss" calculator
+        await navBar.clickNewCalculatorButton();
+        await listPage.clickMenuEntryForCalcType(35);
+        await browser.sleep(200);
+
+        // select Lechapt-Calmon pressure loss law
+        const materialSelect = calcPage.getSelectById("select_pressurelosstype");
+        await changeSelectValue(materialSelect, 0);
+        await browser.sleep(200);
+
+        expect(calcPage.checkEmptyOrFilledFields(["Q", "D", "Lg", "Kloc"], [true, true, true, true]));
+    });
+});
diff --git a/e2e/pressure-loss.e2e-spec.ts b/e2e/pressure-loss.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..af37c218b413779f158d755004ae446baf6a5fc6
--- /dev/null
+++ b/e2e/pressure-loss.e2e-spec.ts
@@ -0,0 +1,77 @@
+import { ListPage } from "./list.po";
+import { Navbar } from "./navbar.po";
+import { browser, by, element } from "protractor";
+import { CalculatorPage } from "./calculator.po";
+import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue } from "./util.po";
+
+describe("Pressure loss - ", () => {
+    let listPage: ListPage;
+    let navBar: Navbar;
+    let calcPage: CalculatorPage;
+    let prefPage: PreferencesPage;
+
+    beforeAll(async () => {
+        listPage = new ListPage();
+        navBar = new Navbar();
+        calcPage = new CalculatorPage();
+        prefPage = new PreferencesPage();
+    });
+
+    beforeEach(async () => {
+        // enable evil option "empty fields on module creation"
+        await prefPage.navigateTo();
+        await prefPage.disableEvilEmptyFields();
+        await browser.sleep(200);
+    });
+
+    describe("modify pressure loss law displays the appropriate fields - ", () => {
+        it("Lechapt&Calmon", async () => {
+            // open "pressure loss" calculator
+            await navBar.clickNewCalculatorButton();
+            await listPage.clickMenuEntryForCalcType(35);
+            await browser.sleep(200);
+
+            // select Lechapt-Calmon pressure loss law
+            const materialSelect = calcPage.getSelectById("select_pressurelosstype");
+            await changeSelectValue(materialSelect, 0);
+            await browser.sleep(200);
+
+            // check inputs presence
+            expect(await calcPage.isNgParamPresent("Q")).toBe(true);
+            expect(await calcPage.isNgParamPresent("D")).toBe(true);
+            expect(await calcPage.isNgParamPresent("J")).toBe(true); // en calcul (donc id=calc_J mais ça marche quand même !)
+            expect(await calcPage.isNgParamPresent("Lg")).toBe(true);
+            expect(await calcPage.isNgParamPresent("Kloc")).toBe(true);
+
+            // check material select
+            expect(await calcPage.isMatSelectPresent("select_material")).toBe(true);
+        });
+
+        it("Strickler", async () => {
+            browser.manage().window().setPosition(2000, 30);
+
+            // open "pressure loss" calculator
+            await navBar.clickNewCalculatorButton();
+            await listPage.clickMenuEntryForCalcType(35);
+            await browser.sleep(200);
+
+            // select Strickler pressure loss law
+            const materialSelect = calcPage.getSelectById("select_pressurelosstype");
+            await changeSelectValue(materialSelect, 1);
+            await browser.sleep(200);
+            debugger
+
+            // check inputs presence
+
+            const ks = element(by.id("0_Ks"));
+            expect(await ks.isPresent()).toBe(true); // isNgParamPresent does not work on "0_Ks". Why ? Mystery...
+
+            expect(await calcPage.isNgParamPresent("Q")).toBe(true);
+            expect(await calcPage.isNgParamPresent("D")).toBe(true);
+            expect(await calcPage.isNgParamPresent("J")).toBe(true);
+            expect(await calcPage.isNgParamPresent("Lg")).toBe(true);
+            expect(await calcPage.isNgParamPresent("Kloc")).toBe(true);
+        });
+    });
+});
diff --git a/e2e/regime-uniforme-empty-fields.e2e-spec.ts b/e2e/regime-uniforme-empty-fields.e2e-spec.ts
index ce60476f2ff67af159fd68fb5b825a4e1003e5e7..6c5f1887fb78727335a5fe1d87f5168fa7d9fb02 100644
--- a/e2e/regime-uniforme-empty-fields.e2e-spec.ts
+++ b/e2e/regime-uniforme-empty-fields.e2e-spec.ts
@@ -10,7 +10,7 @@ describe("Check fields are empty in 'uniform flow' calculator when created with
     let calcPage: CalculatorPage;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
diff --git a/e2e/remous.e2e-spec.ts b/e2e/remous.e2e-spec.ts
index d13ccd48bd73cfea28a4b94f39b7243579f7b073..e6a584333dfe6ca3bfe61a9bcd00db74490f5fd0 100644
--- a/e2e/remous.e2e-spec.ts
+++ b/e2e/remous.e2e-spec.ts
@@ -4,6 +4,7 @@ import { browser } from "protractor";
 import { Navbar } from "./navbar.po";
 import { PreferencesPage } from "./preferences.po";
 import { SideNav } from "./sidenav.po";
+import { changeSelectValue } from "./util.po";
 
 /**
  * Remous
@@ -15,12 +16,15 @@ describe("ngHyd − remous", () => {
     let prefPage: PreferencesPage;
     let sidenav: SideNav;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         prefPage = new PreferencesPage();
         calcPage = new CalculatorPage();
         navBar = new Navbar();
         sidenav = new SideNav();
+    });
+
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await prefPage.disableEvilEmptyFields();
@@ -59,7 +63,7 @@ describe("ngHyd − remous", () => {
         await browser.sleep(300);
 
         // 2. Set to trapezoidal section with bank slope of 2m/m and 20 meter width bed
-        await calcPage.changeSelectValue(calcPage.getSelectById("select_section"), 2);
+        await changeSelectValue(calcPage.getSelectById("select_section"), 2);
         await browser.sleep(300);
         await calcPage.getInputById("LargeurFond").clear();
         await browser.sleep(300);
diff --git a/e2e/reset-param-mode.e2e-spec.ts b/e2e/reset-param-mode.e2e-spec.ts
index b6a294b72b6b920c7c407a53416f8c6746da89a1..b4c87cf4ed368419fd7344540f44a22d201d8cd2 100644
--- a/e2e/reset-param-mode.e2e-spec.ts
+++ b/e2e/reset-param-mode.e2e-spec.ts
@@ -1,6 +1,5 @@
 import { ListPage } from "./list.po";
-import { browser, by, element, ElementFinder } from "protractor";
-import { CalculatorPage } from "./calculator.po";
+import { browser, by, element } from "protractor";
 
 /**
  * Parameter mode should be set to its previous mode (fixed/var/...) when cancel is pressed
@@ -9,7 +8,7 @@ import { CalculatorPage } from "./calculator.po";
 describe("ngHyd - check parameter mode is set to its previous value - ", () => {
     let listPage: ListPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
     });
 
diff --git a/e2e/reset-results.e2e-spec.ts b/e2e/reset-results.e2e-spec.ts
index 7b98cde012aa4dcd0560d7b3767e27453c619a09..e0582c7f0520d3ea4c24c02e6c894c70333f4806 100644
--- a/e2e/reset-results.e2e-spec.ts
+++ b/e2e/reset-results.e2e-spec.ts
@@ -3,7 +3,6 @@ import { Navbar } from "./navbar.po";
 import { browser } from "protractor";
 import { CalculatorPage } from "./calculator.po";
 import { PreferencesPage } from "./preferences.po";
-import { cp } from "fs";
 import { SideNav } from "./sidenav.po";
 
 describe("Check results are reset after application settings modification - ", () => {
@@ -13,7 +12,7 @@ describe("Check results are reset after application settings modification - ", (
     let prefPage: PreferencesPage;
     let sideNav: SideNav;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
@@ -55,7 +54,7 @@ describe("Check results are reset after application settings modification - ", (
         // reopen settings
         await navBar.clickMenuButton();
         await browser.sleep(200);
-        const setupBtn = await sideNav.getSetupButton();
+        const setupBtn = sideNav.getSetupButton();
         await setupBtn.click();
         await browser.sleep(200);
 
diff --git a/e2e/section-empty-fields.e2e-spec.ts b/e2e/section-empty-fields.e2e-spec.ts
index 2a336e916256a8e954fe69ff51df6a5b2dbe80e4..9a72987ac48cba807eb77096bfa06833ebef9a84 100644
--- a/e2e/section-empty-fields.e2e-spec.ts
+++ b/e2e/section-empty-fields.e2e-spec.ts
@@ -10,7 +10,7 @@ describe("Check fields are empty in 'parametric section' calculator when created
     let calcPage: CalculatorPage;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
diff --git a/e2e/sidenav.po.ts b/e2e/sidenav.po.ts
index 5b4060f52154b4e5df514da82fc7c279c5dfa581..596db0a86027fdaebb1db9c833e8099d9bbc34d9 100644
--- a/e2e/sidenav.po.ts
+++ b/e2e/sidenav.po.ts
@@ -31,6 +31,10 @@ export class SideNav {
         return element(by.css(`button#confirm-new-session`));
     }
 
+    getNotesButton() {
+        return element(by.css("#side-nav-session-props"));
+    }
+
     async clickLoadSessionButton() {
         const ncb = this.getLoadSessionButton();
         await browser.sleep(500);
@@ -60,4 +64,10 @@ export class SideNav {
             await this.getFileLoadButton().click();
         }
     }
+
+    async clickNotesButton() {
+        const nb = this.getNotesButton();
+        await browser.sleep(200);
+        await nb.click();
+    }
 }
diff --git a/e2e/solveur.e2e-spec.ts b/e2e/solveur.e2e-spec.ts
index a542f32ba20103af9ac8f1e42e886e1df209998e..1b4740864c4e16447ba3c8aa1eed7a06a702474c 100644
--- a/e2e/solveur.e2e-spec.ts
+++ b/e2e/solveur.e2e-spec.ts
@@ -1,24 +1,22 @@
-import { AppPage } from "./app.po";
 import { ListPage } from "./list.po";
 import { CalculatorPage } from "./calculator.po";
 import { Navbar } from "./navbar.po";
 import { browser, by, element } from "protractor";
 import { SideNav } from "./sidenav.po";
 import { PreferencesPage } from "./preferences.po";
+import { changeSelectValue, scrollPageToTop } from "./util.po";
 
 /**
  * Clone calculators
  */
 describe("Solveur - ", () => {
-    let startPage: AppPage;
     let listPage: ListPage;
     let calcPage: CalculatorPage;
     let navbar: Navbar;
     let sidenav: SideNav;
     let prefPage: PreferencesPage;
 
-    beforeAll(async () => {
-        startPage = new AppPage();
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         listPage = new ListPage();
         calcPage = new CalculatorPage();
@@ -27,10 +25,8 @@ describe("Solveur - ", () => {
     });
 
     beforeEach(async () => {
-        await prefPage.navigateTo();
         // disable evil option "empty fields on module creation"
-        await prefPage.disableEvilEmptyFields();
-        await browser.sleep(200);
+        await prefPage.setEmptyFields(false);
         // force language to prevent issues due to default browser language
         await prefPage.changeLanguage(1); // fr
         await browser.sleep(200);
@@ -67,7 +63,7 @@ describe("Solveur - ", () => {
         expect(spV).toContain("Z2 - Cote aval (PAB : chute)");
 
         // check that "compute" button is active
-        const calcButton = calcPage.checkCalcButtonEnabled(true);
+        const calcButton = await calcPage.checkCalcButtonEnabled(true);
         // click "compute" button
         await calcButton.click();
         // check that result is not empty
@@ -75,7 +71,7 @@ describe("Solveur - ", () => {
         expect(hasResults).toBe(true);
 
         // change targetted Nub, check that targetted result changes too
-        await calcPage.changeSelectValue(ntc, 0);
+        await changeSelectValue(ntc, 0);
         const nttV2 = await calcPage.getSelectValueText(ntt);
         expect(nttV2).not.toContain("Puissance dissipée (PV)");
     });
@@ -110,22 +106,22 @@ describe("Solveur - ", () => {
         // Go back to Solveur
         await navbar.clickCalculatorTab(0);
 
-        await calcPage.changeSelectValue(calcPage.getSelectById("select_target_nub"), 1); // "Puissance / PV"
+        await changeSelectValue(calcPage.getSelectById("select_target_nub"), 1); // "Puissance / PV"
         await browser.sleep(500);
-        await calcPage.changeSelectValue(calcPage.getSelectById("select_searched_param"), 2); // "Chute / Z2"
+        await changeSelectValue(calcPage.getSelectById("select_searched_param"), 2); // "Chute / Z2"
         await browser.sleep(500);
         await calcPage.getInputById("Ytarget").sendKeys("318");
 
         // check that "compute" button is active
-        const calcButton = calcPage.checkCalcButtonEnabled(true);
+        const calcButton = await calcPage.checkCalcButtonEnabled(true);
         // click "compute" button
         await calcButton.click();
         // check that result is not empty
         const hasResults = await calcPage.hasResults();
         expect(hasResults).toBe(true);
 
-        // otherwise clickCloneCalcButton() fails with "Element is not clickable at point"
-        await browser.executeScript("window.scrollTo(0, 0);");
+        await scrollPageToTop(); // otherwise clickCloneCalcButton() fails with "Element is not clickable at point"
+
         await calcPage.clickCloneCalcButton();
         await browser.sleep(500);
 
@@ -138,7 +134,7 @@ describe("Solveur - ", () => {
         expect(hasResultsClone1).toBe(false);
 
         // check that "compute" button is active
-        const calcButtonClone = calcPage.checkCalcButtonEnabled(true);
+        const calcButtonClone = await calcPage.checkCalcButtonEnabled(true);
         // click "compute" button
         await calcButtonClone.click();
         // check that result is not empty
@@ -158,7 +154,7 @@ describe("Solveur - ", () => {
 
         // modify searched parameter
         const sel = calcPage.getSelectById("select_searched_param");
-        await calcPage.changeSelectValue(sel, 11);
+        await changeSelectValue(sel, 11);
         await browser.sleep(300);
         const selText = await calcPage.getSelectValueText(sel);
 
@@ -170,7 +166,7 @@ describe("Solveur - ", () => {
         // check "search parameter" value has not changed
         expect(await calcPage.getSelectValueText(sel)).toEqual(selText);
     });
-    
+
     it("check solver searched parameter is set to 'bottom slope'", async () => {
         // open "canal critical slope" example
         const examples = await element.all(by.css("#examples-list .load-example"));
@@ -187,3 +183,275 @@ describe("Solveur - ", () => {
         expect(selText).toEqual("If - Pente du fond (Sec. param.)");
     });
 });
+
+/**
+ * solver with empty fields option
+ */
+describe("Solveur - nghyd#601 with empty fields option", () => {
+    let prefPage: PreferencesPage;
+    let navBar: Navbar;
+    let listPage: ListPage;
+    let calcPage: CalculatorPage;
+
+    beforeAll(() => {
+        prefPage = new PreferencesPage();
+        navBar = new Navbar();
+        listPage = new ListPage();
+        calcPage = new CalculatorPage();
+    });
+
+    async function openCalculator(id: number) {
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        await listPage.clickMenuEntryForCalcType(id);
+        await browser.sleep(200);
+    }
+
+    async function openSolver() {
+        await openCalculator(22);
+    }
+
+    beforeEach(async () => {
+        // enable evil option "empty fields on module creation"
+        await prefPage.setEmptyFields(true);
+    });
+
+    it("check solver with empty fields option does not fill inputs - solver alone", async () => {
+        // open new solver calculator
+        await openSolver();
+
+        // check inputs are empty
+        await calcPage.checkEmptyInput("Ytarget");
+        await browser.sleep(200);
+
+        await calcPage.checkEmptyInput("Xinit");
+        await browser.sleep(200);
+    });
+
+    it("check solver with empty fields option does not fill target parameter input", async () => {
+        // open "parallel structures" calculator
+        await openCalculator(8);
+
+        // open second "parallel structures" calculator
+        await openCalculator(8);
+
+        // link Q to first calculator's
+        const inpQ = calcPage.getInputById("Q");
+        await calcPage.setParamMode(inpQ, "link");
+        await browser.sleep(200);
+
+        // open new solver calculator
+        await openSolver();
+
+        // check target parameter input is empty
+        await calcPage.checkEmptyInput("Ytarget");
+        await browser.sleep(200);
+
+        // check initial value input is not empty
+        await calcPage.checkEmptyInput("Xinit");
+        await browser.sleep(200);
+    });
+
+    it("check removing and recreating solver with empty fields option does not fill target parameter input", async () => {
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        // open "channel flow with hydraulic structures" example
+        const examples = await element.all(by.css("#examples-list .load-example"));
+        await examples[1].click();
+        await browser.sleep(500);
+
+        // close existing "solver" calculator
+        await navBar.middleClickCalculatorTab(3);
+        await browser.sleep(500);
+
+        // open new solver calculator
+        await openSolver();
+
+        // check target parameter input is empty
+        await calcPage.checkEmptyInput("Ytarget");
+        await browser.sleep(200);
+
+        // check initial value input is not empty
+        await calcPage.checkEmptyInput("Xinit", false);
+        await browser.sleep(200);
+    });
+});
+
+/**
+ * solver without empty fields option
+ */
+describe("Solveur - nghyd#601 without empty fields option", () => {
+    let prefPage: PreferencesPage;
+    let navBar: Navbar;
+    let listPage: ListPage;
+    let calcPage: CalculatorPage;
+
+    beforeAll(() => {
+        prefPage = new PreferencesPage();
+        navBar = new Navbar();
+        listPage = new ListPage();
+        calcPage = new CalculatorPage();
+    });
+
+    async function openCalculator(id: number) {
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        await listPage.clickMenuEntryForCalcType(id);
+        await browser.sleep(200);
+    }
+
+    async function openSolver() {
+        await openCalculator(22);
+    }
+
+    beforeEach(async () => {
+        // await browser.manage().window().setPosition(2000, 30);
+
+        // disable evil option "empty fields on module creation"
+        await prefPage.setEmptyFields(false);
+    });
+
+    it("check solver without empty fields option does not fill inputs - solver alone", async () => {
+        // open new solver calculator
+        await openSolver();
+
+        // check inputs are empty
+        await calcPage.checkEmptyInput("Ytarget");
+        await browser.sleep(200);
+
+        await calcPage.checkEmptyInput("Xinit");
+        await browser.sleep(200);
+    });
+
+    it("check solver without empty fields option fills inputs", async () => {
+        // open "parallel structures" calculator
+        await openCalculator(8);
+
+        // open second "parallel structures" calculator
+        await openCalculator(8);
+
+        // link Q to first calculator's
+        const inpQ = calcPage.getInputById("Q");
+        await calcPage.setParamMode(inpQ, "link");
+        await browser.sleep(200);
+
+        // open new solver calculator
+        await openSolver();
+
+        // check target parameter input is not empty
+        await calcPage.checkEmptyInput("Ytarget", false);
+        await browser.sleep(200);
+
+        // check initial value input is not empty
+        await calcPage.checkEmptyInput("Xinit", false);
+        await browser.sleep(200);
+    });
+
+    it("check removing and recreating solver without empty fields option fills inputs", async () => {
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        // open "channel flow with hydraulic structures" example
+        const examples = await element.all(by.css("#examples-list .load-example"));
+        await examples[1].click();
+        await browser.sleep(500);
+
+        // close existing "solver" calculator
+        await navBar.middleClickCalculatorTab(3);
+        await browser.sleep(500);
+
+        // open new solver calculator
+        await openSolver();
+
+        // check target parameter input is not empty
+        await calcPage.checkEmptyInput("Ytarget", false);
+        await browser.sleep(200);
+
+        // check initial value input is not empty
+        await calcPage.checkEmptyInput("Xinit", false);
+        await browser.sleep(200);
+    });
+
+    it(" with empty fields option, check selecting a target module does not fill inputs", async () => {
+        // enable evil option "empty fields on module creation"
+        await prefPage.setEmptyFields(true);
+        await browser.sleep(200);
+
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        // open "channel flow with hydraulic structures" example
+        const examples = await element.all(by.css("#examples-list .load-example"));
+        await examples[1].click();
+        await browser.sleep(500);
+
+        // close existing "solver" calculator
+        await navBar.middleClickCalculatorTab(3);
+        await browser.sleep(500);
+
+        // open new solver calculator
+        await openSolver();
+        await browser.sleep(500);
+
+        // select other target module
+        const ntc = calcPage.getSelectById("select_target_nub");
+        await changeSelectValue(ntc, 1);
+
+        // check target value input is empty
+        await calcPage.checkEmptyInput("Ytarget", true);
+        await browser.sleep(200);
+    });
+});
+
+describe("Solveur - nghyd#606 - a single module cannot be used with solver", () => {
+    let prefPage: PreferencesPage;
+    let navBar: Navbar;
+    let listPage: ListPage;
+    let calcPage: CalculatorPage;
+
+    beforeAll(() => {
+        prefPage = new PreferencesPage();
+        navBar = new Navbar();
+        listPage = new ListPage();
+        calcPage = new CalculatorPage();
+    });
+
+    async function openCalculator(id: number) {
+        await navBar.clickNewCalculatorButton();
+        await browser.sleep(200);
+
+        await listPage.clickMenuEntryForCalcType(id);
+        await browser.sleep(200);
+    }
+
+    async function openSolver() {
+        await openCalculator(22);
+    }
+
+    beforeEach(async () => {
+        // await browser.manage().window().setPosition(2000, 30);
+
+        // disable evil option "empty fields on module creation"
+        await prefPage.setEmptyFields(false);
+    });
+
+    it("", async () => {
+        // open uniform flow calculator
+        await openCalculator(3);
+
+        // open a solver module
+        await openSolver();
+
+        // set speed as target parameter
+        const trs = calcPage.getSelectById("select_target_result");
+        await changeSelectValue(trs, 1);
+        await browser.sleep(200);
+
+        // check searched parameter has options
+        const sps = calcPage.getSelectById("select_searched_param");
+        expect(await calcPage.isSelectEmpty(sps)).toBe(false);
+    });
+});
diff --git a/e2e/tested_calctypes.ts b/e2e/tested_calctypes.ts
index cdd1fc76623d3d0408d0232848ed10438a92395a..be3f390fcabe3e29a74832bd52acc23475208ff8 100644
--- a/e2e/tested_calctypes.ts
+++ b/e2e/tested_calctypes.ts
@@ -1,7 +1,9 @@
 // cannot import JaLHyd here :/
 // @WARNING keep in sync if CalculatorType enum order changes in JaLHyd
 export const testedCalcTypes = [
-    0, 1, 2, 3, 4, 5, 6,
+    0,
+    // omit 1 - LechaptCalmon
+    2, 3, 4, 5, 6,
     // omit 7 - Structure
     8, 9, 10, 11, 12, 13,
     // omit 14 -Section
@@ -11,5 +13,5 @@ export const testedCalcTypes = [
     // omit 26 - YAXN
     27, 28, 29, 30,
     // omit 31 - PbCloison and 32 - PbBassin
-    33, 34
+    33, 34, 35
 ];
diff --git a/e2e/translation.e2e-spec.ts b/e2e/translation.e2e-spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ca1b51b31363e29483079619c3954e815b84efb8
--- /dev/null
+++ b/e2e/translation.e2e-spec.ts
@@ -0,0 +1,97 @@
+import { ListPage } from "./list.po";
+import { Navbar } from "./navbar.po";
+import { browser, by } from "protractor";
+import { CalculatorPage } from "./calculator.po";
+import { PreferencesPage } from "./preferences.po";
+import { SideNav } from "./sidenav.po";
+
+describe("Check translation", () => {
+    let listPage: ListPage;
+    let navBar: Navbar;
+    let calcPage: CalculatorPage;
+    let prefPage: PreferencesPage;
+    let sideNav: SideNav;
+
+    beforeAll(() => {
+        listPage = new ListPage();
+        navBar = new Navbar();
+        calcPage = new CalculatorPage();
+        prefPage = new PreferencesPage();
+        sideNav = new SideNav();
+    });
+
+    beforeEach(async () => {
+        prefPage.setEmptyFields(false);
+    });
+
+    it("variables in results", async () => {
+        // *** results in french ***
+
+        prefPage.changeLanguage(1); // fr
+        await browser.sleep(200);
+
+        // open "fish ladder: fall" calculator
+        await navBar.clickNewCalculatorButton();
+        await listPage.clickMenuEntryForCalcType(12);
+        await browser.sleep(200);
+
+        // set Z2 to variated mode
+        const inpZ2 = calcPage.getInputById("Z2");
+        await calcPage.setParamMode(inpZ2, "var");
+
+        // run calculation
+        await calcPage.getCalculateButton().click();
+        await browser.sleep(500);
+
+        // "variable for X axis" select label
+        const selXaxis = calcPage.getSelectById("selectX");
+        expect(await calcPage.getMatselectCurrentOptionText(selXaxis)).toEqual("Cote aval");
+
+        // "variable for Y axis" select label
+        const selYaxis = calcPage.getSelectById("selectY");
+        expect(await calcPage.getMatselectCurrentOptionText(selYaxis)).toEqual("DH : Chute (m)");
+
+        // fixed results variables
+        const frr = calcPage.getAllFixedResultsRows();
+        let lbl1 = await frr.all(by.css("td")).get(0).getText();
+        expect(lbl1).toEqual("Cote amont (m)");
+
+        // variated results headers
+        const vrh = calcPage.getAllVariatedResultsTableHeaders();
+        let lbl2 = await vrh.get(0).getText();
+        expect(lbl2).toEqual("Cote aval");
+        let lbl3 = await vrh.get(1).getText();
+        expect(lbl3).toEqual("Chute (m)");
+
+        // *** results in english ***
+
+        // setup -> english
+        await navBar.clickMenuButton();
+        await browser.sleep(200);
+        const setupBtn = sideNav.getSetupButton();
+        await setupBtn.click();
+        await browser.sleep(200);
+        await prefPage.changeLanguage(0); // en
+        await browser.sleep(200);
+
+        // back to calculator
+        await navBar.clickCalculatorTab(0);
+        await browser.sleep(200);
+
+        // "variable for X axis" select label
+        expect(await calcPage.getMatselectCurrentOptionText(selXaxis)).toEqual("Downstream elevation");
+
+        // "variable for Y axis" select label
+        expect(await calcPage.getMatselectCurrentOptionText(selYaxis)).toEqual("DH : Fall (m)");
+
+        // fixed results variables
+        lbl1 = await frr.all(by.css("td")).get(0).getText();
+        expect(lbl1).toEqual("Upstream elevation (m)");
+
+        // variated results headers
+        lbl2 = await vrh.get(0).getText();
+        expect(lbl2).toEqual("Downstream elevation");
+        lbl3 = await vrh.get(1).getText();
+        expect(lbl3).toEqual("Fall (m)");
+    });
+});
diff --git a/e2e/util.po.ts b/e2e/util.po.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aded71c166c96b6914bff75af3f3fa4fe409185f
--- /dev/null
+++ b/e2e/util.po.ts
@@ -0,0 +1,37 @@
+import { ElementFinder, browser, by, element } from "protractor";
+
+/**
+ * scroll page to make element visible
+ */
+export async function scrollToElement(elem: ElementFinder) {
+    await browser.executeScript("arguments[0].scrollIntoView({ block: 'center' });", elem.getWebElement());
+    await browser.sleep(50);
+}
+
+/**
+ * scroll page to top
+ */
+export async function scrollPageToTop() {
+    await browser.executeScript("window.scrollTo(0, 0);");
+}
+
+/**
+ * execute expect() on numbers and displays a failure message
+ * @param msg message in caase of failure
+ * @param val value to check
+ * @param expected expected value
+ */
+export function expectNumber(msg: string, val: number, expected: number) {
+    if (val !== expected) {
+        console.log(msg, "got", val, "expected", expected);
+    }
+    expect(val).toEqual(expected);
+}
+
+export async function changeSelectValue(elt: ElementFinder, index: number) {
+    await elt.click();
+    const optionId = ".cdk-overlay-container mat-option:nth-of-type(" + (index + 1) + ")";
+    const option = element(by.css(optionId));
+    await option.click();
+    await browser.sleep(200);
+}
diff --git a/e2e/valeurs-erronees.e2e-spec.ts b/e2e/valeurs-erronees.e2e-spec.ts
index b2f00ce02c72a39a837812793e1f2b344c284260..73f815a396c7d7d9fd07778e5c55e5349246528d 100644
--- a/e2e/valeurs-erronees.e2e-spec.ts
+++ b/e2e/valeurs-erronees.e2e-spec.ts
@@ -10,12 +10,14 @@ describe("ngHyd - check invalid values are removed - ", () => {
     let navBar: Navbar;
     let calcPage: CalculatorPage;
 
-    beforeEach(async () => {
+    beforeAll(() => {
         prefPage = new PreferencesPage();
         listPage = new ListPage();
         navBar = new Navbar();
         calcPage = new CalculatorPage();
+    });
 
+    beforeEach(async () => {
         // disable evil option "empty fields on module creation"
         await prefPage.navigateTo();
         await browser.sleep(200);
@@ -52,6 +54,6 @@ describe("ngHyd - check invalid values are removed - ", () => {
         expect(w).toEqual("");
 
         // check that "compute" button is disabled
-        const calcButton = calcPage.checkCalcButtonEnabled(false);
+        await calcPage.checkCalcButtonEnabled(false);
     });
 });
diff --git a/e2e/variable-param-cancel.e2e-spec.ts b/e2e/variable-param-cancel.e2e-spec.ts
index abe96d8fe36f4a44466ba12e785352fc6df2d73e..c76b7394508a50b09ba7ea412e6c0ee24ae825cb 100644
--- a/e2e/variable-param-cancel.e2e-spec.ts
+++ b/e2e/variable-param-cancel.e2e-spec.ts
@@ -8,7 +8,7 @@ import { by, element } from "protractor";
 describe("ngHyd - check cancel button for variable parameters - ", () => {
     let listPage: ListPage;
 
-    beforeAll(async () => {
+    beforeAll(() => {
         listPage = new ListPage();
     });
 
diff --git a/mkdocs/mkdocs-en.yml b/mkdocs/mkdocs-en.yml
index 23d32b54e163dd0c440211662d9291f291a2d5fd..88ae9eff984662b7f1cedecf71cad1e70443762e 100644
--- a/mkdocs/mkdocs-en.yml
+++ b/mkdocs/mkdocs-en.yml
@@ -29,7 +29,9 @@ nav:
         - general/parametres_application.md
         - general/raccourcis_clavier.md
     - Pipe flow:
+        - calculators/hyd_en_charge/perte_de_charge.md
         - calculators/hyd_en_charge/lechapt-calmon.md
+        - calculators/hyd_en_charge/strickler.md
         - calculators/hyd_en_charge/cond_distri.md
     - Open-channel flow:
         - Uniform flow: calculators/hsl/regime_uniforme.md
diff --git a/mkdocs/mkdocs-fr.yml b/mkdocs/mkdocs-fr.yml
index 4d6aa5fb22c847896e355c13a7681cc064d5b6fb..4c31e2e742799cbf31aa814d73a968674e47d998 100644
--- a/mkdocs/mkdocs-fr.yml
+++ b/mkdocs/mkdocs-fr.yml
@@ -29,7 +29,9 @@ nav:
         - general/parametres_application.md
         - general/raccourcis_clavier.md
     - Hydraulique en charge:
+        - calculators/hyd_en_charge/perte_de_charge.md
         - calculators/hyd_en_charge/lechapt-calmon.md
+        - calculators/hyd_en_charge/strickler.md
         - calculators/hyd_en_charge/cond_distri.md
     - Hydraulique à surface libre:
         - Régime uniforme: calculators/hsl/regime_uniforme.md
diff --git a/ngsw-config.json b/ngsw-config-template.json
similarity index 100%
rename from ngsw-config.json
rename to ngsw-config-template.json
diff --git a/package-lock.json b/package-lock.json
index 9661a8c876a23a46f8118a8d679270b97149b3f7..38115b303424b91cf4ac3333e45e04b2acc7d0b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,7 +27,7 @@
         "@types/pako": "^1.0.4",
         "@types/sprintf-js": "^1.1.2",
         "angular2-hotkeys": "^13.1.0",
-        "chartjs-plugin-zoom": "^1.2.1",
+        "chartjs-plugin-zoom": "^2.0.0",
         "core-js": "^3.23.3",
         "file-saver": "^2.0.5",
         "he": "^1.2.0",
@@ -37,7 +37,7 @@
         "material-design-icons": "^3.0.1",
         "mathjax": "^3.2.2",
         "mermaid": "^9.1.3",
-        "ng2-charts": "^4.0.0",
+        "ng2-charts": "^4.1.1",
         "ngx-markdown": "^14.0.1",
         "ngx-material-file-input": "^4.0.0",
         "ngx-webstorage-service": "^5.0.0",
@@ -84,6 +84,7 @@
       "license": "LGPL-3.0-or-later",
       "dependencies": {
         "@types/base-64": "^1.0.0",
+        "@types/lodash": "^4.14.191",
         "base-64": "^1.0.0"
       },
       "devDependencies": {
@@ -4138,6 +4139,11 @@
       "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
       "dev": true
     },
+    "node_modules/@types/lodash": {
+      "version": "4.14.191",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
+      "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
+    },
     "node_modules/@types/marked": {
       "version": "4.0.7",
       "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
@@ -6263,14 +6269,14 @@
       "peer": true
     },
     "node_modules/chartjs-plugin-zoom": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.2.1.tgz",
-      "integrity": "sha512-2zbWvw2pljrtMLMXkKw1uxYzAne5PtjJiOZftcut4Lo3Ee8qUt95RpMKDWrZ+pBZxZKQKOD/etdU4pN2jxZUmg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.0.tgz",
+      "integrity": "sha512-bqpi7DGy9a5hX7ThKl/xQaLzXvneSwhS0w/lNimZ8AJaoRVMKz5JfUoqwciJYV5ixKXJbgyvwC9HcJnyVsYmjg==",
       "dependencies": {
         "hammerjs": "^2.0.8"
       },
       "peerDependencies": {
-        "chart.js": "^3.2.0"
+        "chart.js": ">=3.2.0"
       }
     },
     "node_modules/cheerio": {
@@ -14124,9 +14130,9 @@
       "dev": true
     },
     "node_modules/ng2-charts": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-4.0.1.tgz",
-      "integrity": "sha512-QYXVZHP/RIRhAw36xeXPL6JZz73fXuHePTyGwBiOfIRGCG7EDYLApnSgEn+Mx+QFKYT1VN9Fsj5b04rrd2nrEw==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-4.1.1.tgz",
+      "integrity": "sha512-iHwXDbmX86lfeH8VRcsaW2tJATsuAZo4kvvC/Yk2l35zOHjevja1qBvO6BAibiDazi9r9aS6ZRJOqWPsz1pP2w==",
       "dependencies": {
         "lodash-es": "^4.17.15",
         "tslib": "^2.3.0"
@@ -14135,7 +14141,7 @@
         "@angular/cdk": ">=14.0.0",
         "@angular/common": ">=14.0.0",
         "@angular/core": ">=14.0.0",
-        "chart.js": "^3.4.0",
+        "chart.js": "^3.4.0 || ^4.0.0",
         "rxjs": "^6.5.3 || ^7.4.0"
       }
     },
@@ -23282,6 +23288,11 @@
       "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
       "dev": true
     },
+    "@types/lodash": {
+      "version": "4.14.191",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
+      "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
+    },
     "@types/marked": {
       "version": "4.0.7",
       "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
@@ -24951,9 +24962,9 @@
       "peer": true
     },
     "chartjs-plugin-zoom": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.2.1.tgz",
-      "integrity": "sha512-2zbWvw2pljrtMLMXkKw1uxYzAne5PtjJiOZftcut4Lo3Ee8qUt95RpMKDWrZ+pBZxZKQKOD/etdU4pN2jxZUmg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.0.tgz",
+      "integrity": "sha512-bqpi7DGy9a5hX7ThKl/xQaLzXvneSwhS0w/lNimZ8AJaoRVMKz5JfUoqwciJYV5ixKXJbgyvwC9HcJnyVsYmjg==",
       "requires": {
         "hammerjs": "^2.0.8"
       }
@@ -29813,6 +29824,7 @@
       "requires": {
         "@types/base-64": "^1.0.0",
         "@types/jasmine": "^4.0.3",
+        "@types/lodash": "^4.14.191",
         "@types/node": "^18.0.3",
         "@typescript-eslint/eslint-plugin": "^5.30.6",
         "@typescript-eslint/parser": "^5.30.6",
@@ -30881,9 +30893,9 @@
       "dev": true
     },
     "ng2-charts": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-4.0.1.tgz",
-      "integrity": "sha512-QYXVZHP/RIRhAw36xeXPL6JZz73fXuHePTyGwBiOfIRGCG7EDYLApnSgEn+Mx+QFKYT1VN9Fsj5b04rrd2nrEw==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-4.1.1.tgz",
+      "integrity": "sha512-iHwXDbmX86lfeH8VRcsaW2tJATsuAZo4kvvC/Yk2l35zOHjevja1qBvO6BAibiDazi9r9aS6ZRJOqWPsz1pP2w==",
       "requires": {
         "lodash-es": "^4.17.15",
         "tslib": "^2.3.0"
diff --git a/package.json b/package.json
index 1a21cb21d20dcf170515f86947fc56fa2eb1a9a3..b7cf7d4e8781930a18da8e1cc310cc48ecc3db55 100644
--- a/package.json
+++ b/package.json
@@ -17,10 +17,11 @@
     "mkdocs": "bash scripts/prepare_mkdocs.sh; cd build; python3 -m mkdocs build -f mkdocs-fr.yml && python3 -m mkdocs build -f mkdocs-en.yml && cd .. && node scripts/mkdocs-postprocess.js",
     "mkdocs2pdf": "node scripts/extract-nghyd-version.js build/cassiopee_version.tex && python3 scripts/mkdocs2pdf.py && node scripts/mkdocs2pdf-postprocess.js",
     "clean": "rm -rf build dist release src/date_revision.ts src/assets/docs",
-    "preprocess": "mkdir -p build; node scripts/preprocessors.js",
+    "preprocess": "mkdir -p build; node scripts/preprocessors.js; npm run service-worker-version; bash scripts/fix-chartjs-plugin-zoom-2.0.0.sh",
     "start": "npm run preprocess && npm run mkdocs && npm run ng serve -- --host 0.0.0.0 --poll 5000",
     "build-no-pdf": "npm run preprocess && npm run mkdocs && npm run ng build -- --configuration production",
-    "build": "npm run preprocess && npm run mkdocs && npm run ng build -- --configuration production && npm run mkdocs2pdf",
+    "build": "npm run build-href -basehref=/",
+    "build-href": "npm run preprocess && npm run mkdocs && npm run ng build -- --configuration production --base-href=$npm_config_basehref && npm run mkdocs2pdf",
     "update-dist-index-mimetypes": "node scripts/update-dist-index-mimetypes.js",
     "electron": "npm run update-dist-index-mimetypes && \"node_modules/.bin/electron\" .",
     "release-linux-nocompile": "npm run update-dist-index-mimetypes && \"node_modules/.bin/electron-builder\"",
@@ -32,8 +33,9 @@
     "release-all": "node scripts/clean_release.js && npm run build-no-pdf && npm run update-dist-index-mimetypes && \"node_modules/.bin/electron-builder\" && \"node_modules/.bin/electron-builder\" --mac && \"node_modules/.bin/electron-builder\" --win",
     "compodoc": "node \"node_modules/@compodoc/compodoc/bin/index-cli.js\" -p src/tsconfig.app.json -s --language fr-FR -d compodoc-fr",
     "viz": "tsviz -recursive src/ nghyd_class_diagram.png",
-    "webapp-nocompile" : "\"node_modules/.bin/http-server\" dist",
-    "webapp" : "npm run build-no-pdf && npm run webapp-nocompile"
+    "webapp-nocompile": "\"node_modules/.bin/http-server\" dist",
+    "webapp": "npm run build-no-pdf && npm run webapp-nocompile",
+    "service-worker-version": "./scripts/update-service-worker-config.sh"
   },
   "private": true,
   "dependencies": {
@@ -53,9 +55,9 @@
     "@ngx-matomo/tracker": "^3.0.0",
     "@types/pako": "^1.0.4",
     "@types/sprintf-js": "^1.1.2",
-    "ng2-charts": "^4.0.0",
+    "ng2-charts": "^4.1.1",
     "angular2-hotkeys": "^13.1.0",
-    "chartjs-plugin-zoom": "^1.2.1",
+    "chartjs-plugin-zoom": "^2.0.0",
     "core-js": "^3.23.3",
     "file-saver": "^2.0.5",
     "he": "^1.2.0",
diff --git a/scripts/deploy-new-stable-version.sh b/scripts/deploy-new-stable-version.sh
index 92733055ec7e96cab769b35875b8710919a7e7cb..18a5f53fa838f07309e5bb9b776f44906fae25e5 100755
--- a/scripts/deploy-new-stable-version.sh
+++ b/scripts/deploy-new-stable-version.sh
@@ -19,6 +19,9 @@ then
     exit 1
 fi
 
+# Angular service worker configuration file
+NGSW_CONF=ngsw-config.json
+
 # 0. changelog
 read -p "Avez-vous rempli jalhyd_branch, et les CHANGELOG de JaLHyd et NgHyd pour la version $VERSION ? (o/N) " -n 1 -r
 echo
@@ -28,8 +31,8 @@ then
     exit 2
 fi
 
-if [[ ! -f ngsw-config.json ]]; then
-  echo "Fichier de configuration du service worker ngsw-config.json non trouvé" >&2
+if [[ ! -f $NGSW_CONF ]]; then
+  echo "Fichier de configuration du service worker $NGSW_CONF non trouvé" >&2
   exit 1
 fi
 
@@ -73,11 +76,7 @@ git push --tags --force
 echo "BUILDING NGHYD"
 cd ..
 
-# 2.1 service worker configuration (application version)
-
-sed -i "/\"version\": \"/s/\": \".*/\": \"$VERSION\"/" ngsw-config.json
-
-# 2.2 update Git repository
+# 2.1 update Git repository
 git checkout master
 git pull --rebase
 npm install
@@ -87,7 +86,7 @@ then
     git commit -a -m "verify dependencies (npm install) before deploying version $VERSION"
 fi
 
-# 2.3 version in package.*
+# 2.2 version in package.*
 npm version "$VERSION" --allow-same-version --git-tag-version=false
 if [ ! -z "$(git status --untracked-files=no --porcelain)" ]
 then
@@ -95,13 +94,13 @@ then
     git commit -a -m "update package.* to version $VERSION"
 fi
 
-# 2.4 tags
+# 2.3 tags
 echo "setting tags to $VERSION version"
 git tag -fa stable -m "stable version"
 sleep 1
 git tag -fa "$VERSION" -m "release version $VERSION"
 
-# 2.5 push code, push tags
+# 2.4 push code, push tags
 git push
 git push --tags --force
 
diff --git a/scripts/deploy-version.sh b/scripts/deploy-version.sh
index 4f05359f918d7669d96fdac20650d999cb946951..4d5bd0687b2b11ebc1bce1b92ee80aa979e0f399 100755
--- a/scripts/deploy-version.sh
+++ b/scripts/deploy-version.sh
@@ -15,19 +15,35 @@ LOGIN="$2"
 HOST="$3"
 DIR="$4"
 
+LOCAL_DIR=dist
+
+function display_local_href()
+{
+  echo "base href in local index.html :"
+  grep "\<base" $LOCAL_DIR/index.html
+}
+
+function display_remote_href()
+{
+  local dir=$1
+  echo "base href in deployed index.html (host=$HOST) :"
+  ssh $LOGIN@$HOST "grep \<base $dir/index.html"
+}
+
 echo "$(basename $0): deploying version $VERSION in $LOGIN@$HOST:$DIR"
 
 if [[ $VERSION == "prod" || $VERSION == "prod-devel" ]]; then
+  display_local_href
+
   # Copie de la branche production
-  rsync -a --delete --exclude=cassiopee-releases -e "ssh -o StrictHostKeyChecking=no" dist/ ${LOGIN}@${HOST}:${DIR}/
-  # Modification du dossier base href
-  echo "updating index.html"
-  if [[ $VERSION == "prod" ]]; then
-    ssh $LOGIN@$HOST "sed -i 's:/cassiopee/stable/:/:g' $DIR/index.html"
-  else
-    ssh $LOGIN@$HOST "sed -i 's:/cassiopee/devel/:/:g' $DIR/index.html"
-  fi
+  rsync -a --delete --exclude=cassiopee-releases -e "ssh -o StrictHostKeyChecking=no" $LOCAL_DIR/ ${LOGIN}@${HOST}:${DIR}/
+
+  display_remote_href $DIR
 else
+  display_local_href
+
   # Copie de la branche / du tag
-  rsync -a --delete --exclude=cassiopee-releases -e "ssh -o StrictHostKeyChecking=no" "dist/" "$LOGIN@$HOST:$DIR/$VERSION"
+  rsync -a --delete --exclude=cassiopee-releases -e "ssh -o StrictHostKeyChecking=no" $LOCAL_DIR/ "$LOGIN@$HOST:$DIR/$VERSION"
+
+  display_remote_href $DIR/$VERSION
 fi
diff --git a/scripts/fix-chartjs-plugin-zoom-2.0.0.sh b/scripts/fix-chartjs-plugin-zoom-2.0.0.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d066111f010c3abf652f84548addf03b65889b27
--- /dev/null
+++ b/scripts/fix-chartjs-plugin-zoom-2.0.0.sh
@@ -0,0 +1,4 @@
+# fix compilation error due to chartjs-plugin-zoom 2.0.0
+# cf. https://gitlab.irstea.fr/cassiopee/nghyd/-/issues/238#note_76247
+
+sed -i "s/const enum UpdateModeEnum {$/enum UpdateModeEnum {/g" node_modules/chartjs-plugin-zoom/types/index.d.ts
diff --git a/scripts/mkdocs-postprocess.js b/scripts/mkdocs-postprocess.js
index 558e8300fe0d45f43ee54530b3a9208fb2725d07..11b259033998cde6bf5014e9a83616bb164a7e5a 100644
--- a/scripts/mkdocs-postprocess.js
+++ b/scripts/mkdocs-postprocess.js
@@ -7,16 +7,8 @@
 const fs = require('fs-extra');
 
 const destPath_JS = "src/assets/docs/javascripts";
-const destPath_FR_JS = "src/assets/docs/fr/javascripts"; // pour la compilation en local (français)
-const destPath_EN_JS = "src/assets/docs/fr/javascripts"; // pour la compilation en local (anglais)
-
 const destPath_CSS = "src/assets/docs/stylesheets";
-const destPath_FR_CSS = "src/assets/docs/fr/stylesheets"; // pour la compilation en local (français)
-const destPath_EN_CSS = "src/assets/docs/en/stylesheets"; // pour la compilation en local (anglais)
-
 const destPath_JS_MJ = destPath_JS + "/mathjax";
-const destPath_FR_JS_MJ = destPath_FR_JS + "/mathjax"; // pour la compilation en local (français)
-const destPath_EN_JS_MJ = destPath_EN_JS + "/mathjax"; // pour la compilation en local (anglais)
 
 // empty destination folder
 fs.emptyDirSync(destPath_JS);
@@ -26,29 +18,74 @@ fs.ensureDirSync(destPath_JS_MJ + "/fonts/HTML-CSS/TeX/", { recursive: true });
 
 // copy required files only
 fs.copySync("node_modules/mathjax/es5/tex-mml-chtml.js", destPath_JS_MJ + "/tex-mml-chtml.js");
-fs.copySync("node_modules/mathjax/es5/tex-mml-chtml.js", destPath_FR_JS_MJ + "/tex-mml-chtml.js");
-fs.copySync("node_modules/mathjax/es5/tex-mml-chtml.js", destPath_EN_JS_MJ + "/tex-mml-chtml.js");
-
 fs.copySync("node_modules/mathjax/es5/output/chtml/fonts", destPath_JS_MJ + "/output/chtml/fonts");
-fs.copySync("node_modules/mathjax/es5/output/chtml/fonts", destPath_FR_JS_MJ + "/output/chtml/fonts");
-fs.copySync("node_modules/mathjax/es5/output/chtml/fonts", destPath_EN_JS_MJ + "/output/chtml/fonts");
-
 fs.copySync("docs/mathjax.config.js", destPath_JS + "/mathjax.config.js");
-fs.copySync("docs/mathjax.config.js", destPath_FR_JS + "/mathjax.config.js");
-fs.copySync("docs/mathjax.config.js", destPath_EN_JS + "/mathjax.config.js");
+fs.copySync("docs/mathjax-scrollbar.css", destPath_CSS + "/mathjax-scrollbar.css");
+fs.copySync("docs/matomo-tracking.js", destPath_JS + "/matomo-tracking.js");
+fs.copySync("node_modules/mermaid/dist/mermaid.min.js", destPath_JS + "/mermaid.min.js");
+fs.copySync("node_modules/mermaid/dist/mermaid.min.js.map", destPath_JS + "/mermaid.min.js.map");
 
-fs.copySync("docs/mathjax-scrollbar.css", destPath_JS + "/mathjax.config.js");
-fs.copySync("docs/mathjax.config.js", destPath_FR_JS + "/mathjax.config.js");
-fs.copySync("docs/mathjax.config.js", destPath_EN_JS + "/mathjax.config.js");
+/*
+Ici, on veut que les ressources copiées ci dessus soit accessibles.
 
-fs.copySync("docs/mathjax-scrollbar.css", destPath_CSS + "/mathjax-scrollbar.css");
-fs.copySync("docs/mathjax-scrollbar.css", destPath_FR_CSS + "/mathjax-scrollbar.css");
-fs.copySync("docs/mathjax-scrollbar.css", destPath_EN_CSS + "/mathjax-scrollbar.css");
+mkdocs-xx.yml :
+extra_css:
+    - ../stylesheets/mathjax-scrollbar.css
 
-fs.copySync("docs/matomo-tracking.js", destPath_JS + "/matomo-tracking.js");
-fs.copySync("docs/matomo-tracking.js", destPath_FR_JS + "/matomo-tracking.js");
-fs.copySync("docs/matomo-tracking.js", destPath_EN_JS + "/matomo-tracking.js");
+Fichiers HTML générés :
+Par ex src/assets/docs/fr/calculators/maths/solver.html contient <link rel="stylesheet" href="../../stylesheets/mathjax-scrollbar.css">
+ce qui fait référence à stylesheets dans src/assets/docs/fr alors qu'on veut référencer stylesheets dans src/assets/docs/
+(le chemin généré dépend de l'emplacement du fichier le contenant).
 
-fs.copySync("node_modules/mermaid/dist/mermaid.min.js", destPath_JS + "/mermaid.min.js");
-fs.copySync("node_modules/mermaid/dist/mermaid.min.js", destPath_FR_JS + "/mermaid.min.js");
-fs.copySync("node_modules/mermaid/dist/mermaid.min.js", destPath_EN_JS + "/mermaid.min.js");
+Le problème, c'est que si on modifie extra_css avec ../../stylesheets/mathjax-scrollbar.css, cela génère la même chose
+(href="../../stylesheets/mathjax-scrollbar.css") vraisemblablement à cause de site_dir (=docs/fr) qui limite les "remontées"
+de niveau de répertoire.
+
+La solution est donc de modifier les fichiers HTML générés pour ajouter ../ dans href="...".
+*/
+
+const cp = require('child_process');
+
+function execBashCmd(cmd) {
+    // console.log(cmd);
+    // console.log(cp.execSync(cmd, { encoding: 'utf-8' }));
+    cp.execSync(cmd);
+}
+
+function escapeSlash(s) {
+    return s.replaceAll('/', '\\\/');
+}
+
+function escapeDot(s) {
+    return s.replaceAll('\.', '\\.');
+}
+
+function escapeDoubleQuote(s) {
+    return s.replaceAll('"', '\\"');
+}
+
+function applyEscapes(s) {
+    return escapeDot(escapeSlash(escapeDoubleQuote(s)));
+}
+
+function replaceHtml(base_dir, s1, s2) {
+    s1 = applyEscapes(s1);
+    s2 = applyEscapes(s2);
+    const cmd = "find " + base_dir + " -name '*.html' -exec sed -i \"s/" + s1 + "/" + s2 + "/g\" {} \\\;";
+    execBashCmd(cmd);
+}
+
+replaceHtml("src/assets/docs/fr/", "../../stylesheets/mathjax-scrollbar.css", "../../../stylesheets/mathjax-scrollbar.css");
+replaceHtml("src/assets/docs/en/", "../../stylesheets/mathjax-scrollbar.css", "../../../stylesheets/mathjax-scrollbar.css");
+
+replaceHtml("src/assets/docs/fr/", "../../javascripts/mathjax.config.js", "../../../javascripts/mathjax.config.js");
+replaceHtml("src/assets/docs/en/", "../../javascripts/mathjax.config.js", "../../../javascripts/mathjax.config.js");
+
+replaceHtml("src/assets/docs/fr/", "../../javascripts/matomo-tracking.js", "../../../javascripts/matomo-tracking.js");
+replaceHtml("src/assets/docs/en/", "../../javascripts/matomo-tracking.js", "../../../javascripts/matomo-tracking.js");
+
+replaceHtml("src/assets/docs/fr/", "../../javascripts/mermaid.min.js", "../../../javascripts/mermaid.min.js");
+replaceHtml("src/assets/docs/en/", "../../javascripts/mermaid.min.js", "../../../javascripts/mermaid.min.js");
+
+replaceHtml("src/assets/docs/fr/", "../../javascripts/mathjax/tex-mml-chtml.js", "../../../javascripts/mathjax/tex-mml-chtml.js");
+replaceHtml("src/assets/docs/en/", "../../javascripts/mathjax/tex-mml-chtml.js", "../../../javascripts/mathjax/tex-mml-chtml.js");
diff --git a/scripts/mkdocs2pdf.py b/scripts/mkdocs2pdf.py
index 6e3cf68040aa15e43debd48019a955e3e13d9b2a..37deb6a8f52ef1d99b61ccf8f1b618790e3fa4a2 100644
--- a/scripts/mkdocs2pdf.py
+++ b/scripts/mkdocs2pdf.py
@@ -20,6 +20,9 @@ import yaml
 import re
 import shutil
 
+# verbose output
+verbose = False
+
 baseDir = os.getcwd()
 buildDir = os.path.join(baseDir, 'build')
 latexSourceDir = os.path.join(baseDir, 'docs/latex')
@@ -38,6 +41,21 @@ def runCommand(cmd):
     if os.waitstatus_to_exitcode(os.system(cmd)) != 0:
         raise RuntimeError("error executing:",cmd)
 
+# Create a symbolic link
+def createLink(src):
+    # check if destination already exists
+    dest = os.path.basename(src)
+    if os.path.exists(dest):
+        if not os.path.islink(dest):
+            raise Exception('{} exists but is not a symbolic link'.format(dest))
+    else:
+        runCommand('ln -s {}'.format(src))
+
+def createEmptyDir(path):
+    if os.path.exists(path):
+        shutil.rmtree(path)
+    os.makedirs(path)
+
 # Reads an MkDocs configuration file
 def readConfig(sYAML):
     f = open(sYAML, 'r')
@@ -130,9 +148,14 @@ def convertMdToTex(filePath):
 def getLatexModel():
     # Clone Git repository
     os.chdir(pdfBuildDir)
-    runCommand(
-        'git clone {} {}'.format(latexModelRepository, latexModelDir)
-    )
+    if os.path.isdir(latexModelDir):
+        # git directory exists, update it
+        os.chdir(latexModelDir)
+        runCommand('git pull')
+        # platform independent "cd .."
+        os.chdir(os.path.dirname(os.getcwd()))
+    else:
+        runCommand('git clone {} {}'.format(latexModelRepository, latexModelDir))
     # back to original working drectory
     os.chdir(baseDir)
 
@@ -146,35 +169,17 @@ def injectContentIntoModel(mergedDocFilenameTex, lang):
     # Symlink necessary resources
     os.chdir(modelDir)
     relPathToMergedTexDoc = os.path.join('..', mergedDocFilenameTex)
-    runCommand(
-        'ln -s {} .'.format(relPathToMergedTexDoc)
-    )
+    createLink(relPathToMergedTexDoc)
     latexTemplate = filenamePrefix + lang + '.tex'
     relPathToLatexTemplate = os.path.join(latexSourceDir, latexTemplate)
-    runCommand(
-        'ln -s {}'.format(relPathToLatexTemplate)
-    )
-    runCommand(
-        'ln -s {}'.format(os.path.join(latexSourceDir, 'logo_pole.png'))
-    )
-    runCommand(
-        'ln -s {}/schema_rugosite_fond.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'ln -s {}/bloc_cylindre.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'ln -s {}/bloc_face_arrondie.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'ln -s {}/bloc_base_carree.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'rm rapport_inrae/logos.tex'
-    )
-    runCommand(
-        'ln -s {} rapport_inrae/'.format(os.path.join(latexSourceDir, 'logos.tex'))
-    )
+    createLink(relPathToLatexTemplate)
+    createLink(os.path.join(latexSourceDir, 'logo_pole.png'))
+    createLink('{}/schema_rugosite_fond.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    createLink('{}/bloc_cylindre.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    createLink('{}/bloc_face_arrondie.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    createLink('{}/bloc_base_carree.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    runCommand('rm rapport_inrae/logos.tex')
+    createLink('{} rapport_inrae/'.format(os.path.join(latexSourceDir, 'logos.tex')))
     # back to original working drectory
     os.chdir(baseDir)
 
@@ -189,9 +194,11 @@ def buildPDF(lang):
     cvt = os.path.join(buildDir, 'cassiopee_version.tex')
     shutil.copy(cvt, modelDir)
 
-    os.system(
-        'latexmk -f -xelatex -pdf -interaction=nonstopmode {} > /dev/null 2>&1'.format(sourceTexFile)
-    )
+    if verbose:
+        os.system('latexmk -f -xelatex -pdf -interaction=nonstopmode {} > /dev/null'.format(sourceTexFile))
+    else:
+        os.system('latexmk -f -xelatex -pdf -interaction=nonstopmode {} > /dev/null 2>&1'.format(sourceTexFile))
+
     # copy generated PDF to release directory
     shutil.copy(outputPdfFile, outputDir)
     # back to original working drectory
@@ -201,9 +208,9 @@ def buildPDF(lang):
 def buildDocForLang(lang):
 
     # Prepare temporary build directory
-    os.makedirs(pdfBuildDir, exist_ok=True)
+    createEmptyDir(pdfBuildDir)
     # Prepare output directory
-    os.makedirs(outputDir, exist_ok=True)
+    createEmptyDir(outputDir)
 
     # Read config
     yamlPath = 'mkdocs/mkdocs-' + lang + '.yml'
diff --git a/scripts/update-service-worker-config.sh b/scripts/update-service-worker-config.sh
new file mode 100755
index 0000000000000000000000000000000000000000..aebfee18394b8cec1cb1f364ad7837b245f4d41e
--- /dev/null
+++ b/scripts/update-service-worker-config.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+# update ngsw-config.json from template and "git describe" output
+
+# error on unset variables
+set -o nounset
+
+# exit on error
+set -o errexit
+
+# Angular service worker configuration file
+NGSW_CONF=ngsw-config.json
+
+# Angular service worker configuration template
+NGSW_CONF_TMPL=ngsw-config-template.json
+
+echo "updating version in Angular service worker configuration" >&2
+
+VERSION=$(git describe)
+
+cp $NGSW_CONF_TMPL $NGSW_CONF
+
+sed -i "/\"version\": \"/s/\": \".*/\": \"$VERSION\"/" $NGSW_CONF
+# check
+if [[ -z $(grep version $NGSW_CONF | grep $VERSION) ]]; then
+  echo "error updating version in service worker configuration file $NGSW_CONF" >&2
+  exit 1
+fi
diff --git a/src/.htaccess b/src/.htaccess
index 34b87a8bc622a8df238f06f0644a8a74641d1579..6a66c8cecffb460998fe5d4487a116958cfd4157 100644
--- a/src/.htaccess
+++ b/src/.htaccess
@@ -1,3 +1,14 @@
+<IfModule mod_rewrite.c>
+	# SSL Redirect
+
+	# enable Rewrite capabilities
+	RewriteEngine On
+	# check the connection is not already HTTPS
+	RewriteCond %{HTTPS} off
+	# rewrite URL with https
+	RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+</IfModule>
+
 <filesMatch "\.json$">
 	FileETag None
 	<ifModule mod_headers.c>
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index d11194ecab2f9f8c1b5ed14ec25d6a671bc1bf23..0e1044ac651ec31ebd7d38d288ab52741aaea25c 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -22,7 +22,7 @@ import { DialogSaveSessionComponent } from "./components/dialog-save-session/dia
 import { QuicknavComponent } from "./components/quicknav/quicknav.component";
 import { NotificationsService } from "./services/notifications.service";
 
-import { decodeHtml } from "./util";
+import { decodeHtml } from "./util/util";
 
 import { HotkeysService, Hotkey } from "angular2-hotkeys";
 
@@ -33,6 +33,8 @@ import { saveAs } from "file-saver";
 import * as XLSX from "xlsx";
 
 import * as pako from "pako";
+import { DialogConfirmComponent } from "./components/dialog-confirm/dialog-confirm.component";
+import { UserConfirmationService } from "./services/user-confirmation.service";
 import { ServiceWorkerUpdateService } from "./services/service-worker-update.service";
 
 @Component({
@@ -85,13 +87,16 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
         private confirmCloseCalcDialog: MatDialog,
         private hotkeysService: HotkeysService,
         private matomoTracker: MatomoTracker,
-        private serviceWorkerUpdateService: ServiceWorkerUpdateService
+        private confirmDialog: MatDialog,
+        private serviceWorkerUpdateService: ServiceWorkerUpdateService,
+        private userConfirmationService: UserConfirmationService
     ) {
         ServiceFactory.httpService = httpService;
         ServiceFactory.applicationSetupService = appSetupService;
         ServiceFactory.i18nService = intlService;
         ServiceFactory.formulaireService = formulaireService;
         ServiceFactory.notificationsService = notificationsService;
+        ServiceFactory.serviceWorkerUpdateService = serviceWorkerUpdateService;
 
         if (!isDevMode()) {
             // évite de mettre en place un bandeau RGPD
@@ -218,10 +223,24 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
     ngOnInit() {
         this.formulaireService.addObserver(this);
         this._innerWidth = window.innerWidth;
+        this.logRevisionInfo();
+
+        // Initialise communication with UserConfirmationService.
+        // When receiving a message from it, open a dialog to ask user to confirm.
+        // Will then reply to UserConfirmationService with a message holding confirmation status.
+        this.userConfirmationService.subscribe(this);
+        this.userConfirmationService.addHandler(this, {
+            next: (data) => this.displayConfirmationDialog(data["title"], data["body"]),
+            error: () => { },
+            complete: () => { },
+        });
     }
 
     ngOnDestroy() {
         this.formulaireService.removeObserver(this);
+
+        // cancel communication link with UserConfirmationService
+        this.userConfirmationService.unsubscribe(this);
     }
 
     @HostListener("window:resize", ["$event"])
@@ -670,6 +689,12 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
         };
     }
 
+    private logRevisionInfo() {
+        const ri = this.revisionInfo;
+        console.log("JaLHyd", ri.jalhyd.date, ri.jalhyd.version);
+        console.log("ngHyd", ri.nghyd.date, ri.nghyd.version);
+    }
+
     /**
      * sauvegarde du/des formulaires
      * @param form formulaire à sélectionner par défaut dans la liste
@@ -809,4 +834,24 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
             }
         }
     }
+
+    /**
+     * display a confirmation display upon request from UserConfirmationService
+     */
+    private displayConfirmationDialog(title: string, text: string) {
+        const dialogRef = this.confirmDialog.open(
+            DialogConfirmComponent,
+            {
+                data: {
+                    title: title,
+                    text: text
+                },
+                disableClose: true
+            }
+        );
+        dialogRef.afterClosed().subscribe(result => {
+            // reply to UserConfirmationService
+            this.userConfirmationService.postConfirmation(this, { "confirm": result });
+        });
+    }
 }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 15eac95408a49c29f830397b606d7f95990029fc..add5d9f1049097195b7edc866d16adb9f9c97497 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -96,6 +96,7 @@ import { JetTrajectoryChartComponent } from "./components/jet-trajectory-chart/j
 import { SessionPropertiesComponent } from "./components/session-properties/session-properties.component";
 import { VerificateurResultsComponent } from "./components/verificateur-results/verificateur-results.component";
 
+import { DialogConfirmComponent } from "./components/dialog-confirm/dialog-confirm.component";
 import { DialogConfirmEmptySessionComponent } from "./components/dialog-confirm-empty-session/dialog-confirm-empty-session.component";
 import { DialogConfirmCloseCalcComponent } from "./components/dialog-confirm-close-calc/dialog-confirm-close-calc.component";
 import { DialogEditPabComponent } from "./components/dialog-edit-pab/dialog-edit-pab.component";
@@ -120,13 +121,13 @@ import { ImmediateErrorStateMatcher } from "./formulaire/immediate-error-state-m
 import { LoadSessionURLComponent } from "./components/load-session-url/load-session-url.component";
 import { DialogShowMessageComponent } from "./components/dialog-show-message/dialog-show-message.component";
 import { DialogConfirmLoadSessionURLComponent } from "./components/dialog-confirm-load-session-url/dialog-confirm-load-session-url.component";
-import { StructureFieldsetContainerComponent } from "./components/structure-fieldset-container/structure-fieldset-container.component";
 import { BasinFieldsetContainerComponent } from "./components/basin-fieldset-container/basin-fieldset-container.component";
 import { PrebarrageService } from "./services/prebarrage.service";
 import { SelectSectionDetailsComponent } from "./components/select-section-details/select-section-details.component";
 import { ServiceWorkerModule } from '@angular/service-worker';
 import { environment } from '../environments/environment';
 import { ServiceWorkerUpdateService } from "./services/service-worker-update.service";
+import { UserConfirmationService } from "./services/user-confirmation.service";
 
 const appRoutes: Routes = [
     { path: "list/search", component: CalculatorListComponent },
@@ -208,6 +209,7 @@ const appRoutes: Routes = [
         CalculatorResultsComponent,
         DialogConfirmCloseCalcComponent,
         DialogConfirmEmptySessionComponent,
+        DialogConfirmComponent,
         DialogEditPabComponent,
         DialogEditParamComputedComponent,
         DialogEditParamValuesComponent,
@@ -222,7 +224,6 @@ const appRoutes: Routes = [
         DialogConfirmLoadSessionURLComponent,
         FieldSetComponent,
         FieldsetContainerComponent,
-        StructureFieldsetContainerComponent,
         BasinFieldsetContainerComponent,
         FixedResultsComponent,
         FixedVarResultsComponent,
@@ -282,7 +283,8 @@ const appRoutes: Routes = [
             provide: ErrorStateMatcher,
             useClass: ImmediateErrorStateMatcher
         },
-        ServiceWorkerUpdateService
+        ServiceWorkerUpdateService,
+        UserConfirmationService
     ],
     schemas: [NO_ERRORS_SCHEMA],
     bootstrap: [AppComponent]
diff --git a/src/app/calculators/courberemous/config.json b/src/app/calculators/courberemous/config.json
index d1682af8b8ab5bdec0470f3f45f807410931babd..14ee8981ce2957e684510c9b379e975ea1d7c916 100644
--- a/src/app/calculators/courberemous/config.json
+++ b/src/app/calculators/courberemous/config.json
@@ -44,8 +44,14 @@
         "type": "fieldset",
         "fields": [
             "Q",
-            "Z1",
-            "Z2"
+            {
+                "id": "Z1",
+                "allowEmpty": true
+            },
+            {
+                "id": "Z2",
+                "allowEmpty": true
+            }
         ]
     },
     {
diff --git a/src/app/calculators/par/config.json b/src/app/calculators/par/config.json
index c7ba05c46cae67a319ff58a2672c70935268aa85..00b267c5932bfe44d9956757b8c5ac64a4e5913b 100644
--- a/src/app/calculators/par/config.json
+++ b/src/app/calculators/par/config.json
@@ -22,7 +22,13 @@
                 "type": "select",
                 "id": "select_partype",
                 "property": "parType",
-                "default": "PLANE"
+                "default": "PLANE",
+                "help": {
+                    "0": "par/theorie_plans.html",
+                    "1": "par/theorie_fatou.html",
+                    "2": "par/theorie_suractif.html",
+                    "3": "par/theorie_mixte.html"
+                }
             },
             "ha",
             "S",
diff --git a/src/app/calculators/par/en.json b/src/app/calculators/par/en.json
index f1d88aa176d3fee207ac7bba0b07b84095c489ac..a1271a1a13c1e76cf1eb2ed92ae12f6de701818e 100644
--- a/src/app/calculators/par/en.json
+++ b/src/app/calculators/par/en.json
@@ -30,7 +30,7 @@
     "Hmin": "Minimal baffle height",
     "Hmax": "Maximal baffle height",
 
-    "select_partype": "Type de passe",
+    "select_partype": "Pass type",
     "PARTYPE_0": "Plane baffles",
     "PARTYPE_1": "Fatou",
     "PARTYPE_2": "Superactive",
diff --git a/src/app/calculators/parsimulation/config.json b/src/app/calculators/parsimulation/config.json
index 810625bf606e5297964528a3893abf79a9e30b38..64b58bfc5ba4b83593035ee3ce6cd58757d8c402 100644
--- a/src/app/calculators/parsimulation/config.json
+++ b/src/app/calculators/parsimulation/config.json
@@ -19,7 +19,13 @@
                 "type": "select",
                 "id": "select_partype",
                 "property": "parType",
-                "default": "PLANE"
+                "default": "PLANE",
+                "help": {
+                    "0": "par/theorie_plans.html",
+                    "1": "par/theorie_fatou.html",
+                    "2": "par/theorie_suractif.html",
+                    "3": "par/theorie_mixte.html"
+                }
             },
             "L",
             "S",
diff --git a/src/app/calculators/parsimulation/en.json b/src/app/calculators/parsimulation/en.json
index 36c46b4b1706938004c1e6724b95665ca8c78524..f6812ebc5e0b5db1988749ad95beac9ae8f74b07 100644
--- a/src/app/calculators/parsimulation/en.json
+++ b/src/app/calculators/parsimulation/en.json
@@ -30,7 +30,7 @@
     "Hmin": "Minimal baffle height",
     "Hmax": "Maximal baffle height",
 
-    "select_partype": "Type de passe",
+    "select_partype": "Pass type",
     "PARTYPE_0": "Plane baffles",
     "PARTYPE_1": "Fatou",
     "PARTYPE_2": "Superactive",
diff --git a/src/app/calculators/lechaptcalmon/config.json b/src/app/calculators/pressureloss/config.json
similarity index 52%
rename from src/app/calculators/lechaptcalmon/config.json
rename to src/app/calculators/pressureloss/config.json
index 1633e8d94288652e6573960f5e2a937b57b40050..c4bbbd561c12cf2ad5a9fff62b02551f55d6191e 100644
--- a/src/app/calculators/lechaptcalmon/config.json
+++ b/src/app/calculators/pressureloss/config.json
@@ -1,8 +1,18 @@
 [
     {
-        "id": "fs_materiau",
+        "id": "fs_pressureloss_law",
         "type": "fieldset",
         "fields": [
+            {
+                "id": "select_pressurelosstype",
+                "type": "select",
+                "property": "pressureLossType",
+                "default": "LechaptCalmon",
+                "help": {
+                    "0": "hyd_en_charge/lechapt-calmon.html",
+                    "1": "hyd_en_charge/strickler.html"
+                }
+            },
             {
                 "id": "select_material",
                 "type": "select",
@@ -11,7 +21,8 @@
             },
             "L",
             "M",
-            "N"
+            "N",
+            "Ks"
         ]
     },
     {
@@ -22,11 +33,11 @@
             "D",
             "J",
             "Lg",
-            "Ks"
+            "Kloc"
         ]
     },
     {
         "type": "options",
-        "help": "hyd_en_charge/lechapt-calmon.html"
+        "help": "hyd_en_charge/perte_de_charge.html"
     }
 ]
diff --git a/src/app/calculators/lechaptcalmon/en.json b/src/app/calculators/pressureloss/en.json
similarity index 72%
rename from src/app/calculators/lechaptcalmon/en.json
rename to src/app/calculators/pressureloss/en.json
index 729994b0eefa9009092b821d43e3410b05358419..238ce4911f254eb944394648f20f4a72d7f5de13 100644
--- a/src/app/calculators/lechaptcalmon/en.json
+++ b/src/app/calculators/pressureloss/en.json
@@ -1,4 +1,9 @@
 {
+    "fs_pressureloss_law": "Pressure loss",
+    "select_pressurelosstype": "Pressure loss law",
+    "PRESSURELOSSTYPE_0": "Lechapt-Calmon",
+    "PRESSURELOSSTYPE_1": "Strickler",
+
     "fs_materiau": "Type of material",
     "select_material": "Choice of material",
     "MATERIAL_0": "Unlined cast iron - Coarse concrete (corrosive water)",
@@ -16,13 +21,14 @@
     "fs_hydraulique": "Hydraulic features",
     "D": "Pipe diameter",
     "J": "Total head loss",
-    "Ks": "Singular head loss coefficient",
+    "Kloc": "Singular head loss coefficient",
     "Lg": "Pipe length",
     "fs_param_calc": "Calculation parameters",
-    "Jl": "Linear head loss",
-    "Kl": "Linear head loss coefficient",
+    "Jlin": "Linear head loss",
+    "Klin": "Linear head loss coefficient",
     "fD": "Darcy friction factor",
+    "Ks": "Strickler coefficient",
 
-    "UNIT_JL": "m",
+    "UNIT_JLIN": "m",
     "UNIT_V": "m/s"
-}
\ No newline at end of file
+}
diff --git a/src/app/calculators/lechaptcalmon/fr.json b/src/app/calculators/pressureloss/fr.json
similarity index 71%
rename from src/app/calculators/lechaptcalmon/fr.json
rename to src/app/calculators/pressureloss/fr.json
index 89884cabd4634f566ed4d86d2a419de79000447f..fd1bd9c2ff98254fba30dbb457da7c4cfa6a4a98 100644
--- a/src/app/calculators/lechaptcalmon/fr.json
+++ b/src/app/calculators/pressureloss/fr.json
@@ -1,4 +1,9 @@
 {
+    "fs_pressureloss_law": "Perte de charge",
+    "select_pressurelosstype": "Loi de perte de charge",
+    "PRESSURELOSSTYPE_0": "Lechapt-Calmon",
+    "PRESSURELOSSTYPE_1": "Strickler",
+
     "fs_materiau": "Type du matériau",
     "select_material": "Choix du matériau",
     "MATERIAL_0": "Fonte ou acier non revêtus - Béton grossier (eau corrosive)",
@@ -16,13 +21,14 @@
     "fs_hydraulique": "Caractéristiques hydrauliques",
     "D": "Diamètre du tuyau",
     "J": "Perte de charge totale",
-    "Ks": "Coefficient de perte de charge singulière",
+    "Kloc": "Coefficient de perte de charge singulière",
     "Lg": "Longueur du tuyau",
     "fs_param_calc": "Paramètres de calcul",
-    "Jl": "Perte de charge linéaire",
-    "Kl": "Coefficient de perte de charge linéaire",
+    "Jlin": "Perte de charge linéaire",
+    "Klin": "Coefficient de perte de charge linéaire",
     "fD": "Coefficient de perte de charge de Darcy",
+    "Ks": "Coefficient de Strickler",
 
-    "UNIT_JL": "m",
+    "UNIT_JLIN": "m",
     "UNIT_V": "m/s"
-}
\ No newline at end of file
+}
diff --git a/src/app/components/calculator-list/calculator-list.component.ts b/src/app/components/calculator-list/calculator-list.component.ts
index ec0ec059a9df1cea2998e70ecbdbe9f0112580fa..d69f071f82e6a107ee923348ef0c58e013ea7400 100644
--- a/src/app/components/calculator-list/calculator-list.component.ts
+++ b/src/app/components/calculator-list/calculator-list.component.ts
@@ -120,7 +120,9 @@ export class CalculatorListComponent implements OnInit {
                             CalculatorType.CloisonAval,
                             CalculatorType.YAXN,
                             CalculatorType.PbBassin,
-                            CalculatorType.PbCloison
+                            CalculatorType.PbCloison,
+                            CalculatorType.LechaptCalmon,
+                            CalculatorType.PressureLossLaw
                         ].includes(t)
                     ) {
                         unusedTheme.calculators.push({
diff --git a/src/app/components/dialog-confirm/dialog-confirm.component.html b/src/app/components/dialog-confirm/dialog-confirm.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..1de1a1bae3d8e2d2cee34ed52dd36424283dc39f
--- /dev/null
+++ b/src/app/components/dialog-confirm/dialog-confirm.component.html
@@ -0,0 +1,12 @@
+<h1 mat-dialog-title [innerHTML]="uitextTitle"></h1>
+<div mat-dialog-content>
+    <p [innerHTML]="uitextBody"></p>
+</div>
+<div mat-dialog-actions [attr.align]="'end'">
+    <button id="cancel" mat-raised-button color="primary" [mat-dialog-close]="false" cdkFocusInitial>
+        {{ uitextNo }}
+    </button>
+    <button id="confirm" mat-raised-button color="warn" [mat-dialog-close]="true">
+        {{ uitextYes }}
+    </button>
+</div>
diff --git a/src/app/components/dialog-confirm/dialog-confirm.component.ts b/src/app/components/dialog-confirm/dialog-confirm.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f8cefaa2526ec20077f992157001690ec99e5c16
--- /dev/null
+++ b/src/app/components/dialog-confirm/dialog-confirm.component.ts
@@ -0,0 +1,38 @@
+import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
+import { Inject, Component } from "@angular/core";
+import { I18nService } from "../../services/internationalisation.service";
+
+@Component({
+    selector: "dialog-confirm",
+    templateUrl: "dialog-confirm.component.html",
+})
+export class DialogConfirmComponent {
+
+    private _title: string;
+    private _text: string;
+
+    constructor(
+        public dialogRef: MatDialogRef<DialogConfirmComponent>,
+        private intlService: I18nService,
+        @Inject(MAT_DIALOG_DATA) public data: any
+    ) {
+        this._title = data.title;
+        this._text = data.text;
+    }
+
+    public get uitextYes() {
+        return this.intlService.localizeText("INFO_OPTION_YES");
+    }
+
+    public get uitextNo() {
+        return this.intlService.localizeText("INFO_OPTION_NO");
+    }
+
+    public get uitextTitle() {
+        return this._title;
+    }
+
+    public get uitextBody() {
+        return this._text;
+    }
+}
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 16768e9c0715efc77bbf7b50d2c4d6b45d3e64ea..47790963b1c2fd3b83d065621b01b1c2326a026a 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
@@ -10,7 +10,7 @@ import { sprintf } from "sprintf-js";
 
 import { ParamValueMode, ExtensionStrategy } from "jalhyd";
 
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { ServiceFactory } from "app/services/service-factory";
 
 @Component({
diff --git a/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts b/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts
index b7aaf61d72def9cd99d15fec5b95d62666de2614..554bc994357a9496566c3ad921f6ec8f4b8176f1 100644
--- a/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts
+++ b/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts
@@ -3,7 +3,7 @@ import { Inject, Component } from "@angular/core";
 
 import { I18nService } from "../../services/internationalisation.service";
 import { MultiDimensionResults } from "../../results/multidimension-results";
-import { fv, longestVarParam } from "../../util";
+import { fv, longestVarParam } from "../../util/util";
 
 @Component({
     selector: "dialog-generate-par-simulation",
diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts
index 448d40a7dc63bfcd47dbec4e10bdb29ba884e857..889b28d60f4dc9f93d981e8536aeb1113eb9abc4 100644
--- a/src/app/components/field-set/field-set.component.ts
+++ b/src/app/components/field-set/field-set.component.ts
@@ -16,7 +16,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { sprintf } from "sprintf-js";
 
 import { capitalize } from "jalhyd";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 
 @Component({
     selector: "field-set",
@@ -265,7 +265,7 @@ export class FieldSetComponent implements DoCheck {
      * réception d'un événement de validité de ParamFieldLineComponent
      */
     public onParamLineValid(event: boolean) {
-        this.updateValidity();
+        this.updateValidity(true);
     }
 
     /**
diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts
index f612f362aa0ac7e2475b785e0e2bbf76bbded771..e07c6514d5088587c89b402db5c6cca6088bc7e7 100644
--- a/src/app/components/fieldset-container/fieldset-container.component.ts
+++ b/src/app/components/fieldset-container/fieldset-container.component.ts
@@ -6,7 +6,7 @@ import { FieldSet } from "../../formulaire/elements/fieldset";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 import { ParamValueMode } from "jalhyd";
 
 @Component({
@@ -89,8 +89,8 @@ export class FieldsetContainerComponent implements DoCheck, AfterViewInit {
             const prms = after.backupParameters();
             // replace in-place to change properties (overkill)
             // @WTF why only those two ?
-            newFs.setNubPropValue("structureType", after.properties.getPropValue("structureType"));
-            newFs.setNubPropValue("loiDebit", after.properties.getPropValue("loiDebit"));
+            newFs.setPropValue("structureType", after.getPropValue("structureType"));
+            newFs.setPropValue("loiDebit", after.getPropValue("loiDebit"));
 
             // au cas où un des paramètres du fieldset source est en mode calcul,
             // on met le paramètre copié en mode fixé (nghyd#567)
diff --git a/src/app/components/fixedvar-results/fixed-results.component.ts b/src/app/components/fixedvar-results/fixed-results.component.ts
index ae76c3e1989e993a78305a906f1a5d91c776ca21..5905c6bcec92f48ac7206ade97b2de2c13a9a484 100644
--- a/src/app/components/fixedvar-results/fixed-results.component.ts
+++ b/src/app/components/fixedvar-results/fixed-results.component.ts
@@ -10,6 +10,7 @@ import { NgParameter } from "../../formulaire/elements/ngparam";
 import { capitalize, Result, ResultElement } from "jalhyd";
 
 import { sprintf } from "sprintf-js";
+import { PressureLoss } from "jalhyd";
 
 @Component({
     selector: "fixed-results",
@@ -256,10 +257,20 @@ export class FixedResultsComponent extends ResultsComponentDirective {
                         if (sn.parent) {
                             ct = sn.parent.calcType;
                         }
-                        const cn = capitalize(this.intlService.childName(c));
-                        let label = sprintf(this.intlService.localizeText("INFO_STUFF_N"), cn)
-                            + (c.findPositionInParent() + 1) + " : "
-                            + this.formService.expandVariableNameAndUnit(ct, k);
+
+                        // do not display child rank in case of pressure loss law
+                        const displayChildRank = !(sn instanceof PressureLoss);
+                        let label: string;
+                        if (displayChildRank) {
+                            const cn = capitalize(this.intlService.childName(c));
+                            label = sprintf(this.intlService.localizeText("INFO_STUFF_N"), cn) + (c.findPositionInParent() + 1);
+                        }
+                        else {
+                            label = capitalize(this.intlService.childName(c));
+                        }
+
+                        label += " : ";
+                        label += this.formService.expandVariableNameAndUnit(ct, k);
                         label += this._fixedResults.getHelpLink(k);
                         data.push({
                             label: label,
diff --git a/src/app/components/fixedvar-results/results.component.ts b/src/app/components/fixedvar-results/results.component.ts
index ae9747dbe0e9f0b4843d4ccb9292105cf295e86f..18b08975073117647fc2b4e3fbfd1e94e5e81a1c 100644
--- a/src/app/components/fixedvar-results/results.component.ts
+++ b/src/app/components/fixedvar-results/results.component.ts
@@ -2,7 +2,7 @@ import screenfull from "screenfull";
 
 import { NgParameter } from "../../formulaire/elements/ngparam";
 import { ServiceFactory } from "../../services/service-factory";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { CalculatorResults } from "../../results/calculator-results";
 
 import { Directive, HostListener } from "@angular/core";
diff --git a/src/app/components/fixedvar-results/var-results.component.ts b/src/app/components/fixedvar-results/var-results.component.ts
index 7493401d2776c1760fbe9c616232aa62ceefecea..f48623acc22a3df02f2a972a5ba0a7b227c93683 100644
--- a/src/app/components/fixedvar-results/var-results.component.ts
+++ b/src/app/components/fixedvar-results/var-results.component.ts
@@ -1,14 +1,14 @@
-import { Component, ViewChild, ElementRef, Input } from "@angular/core";
+import { Component, ViewChild, ElementRef, Input, OnInit } from "@angular/core";
 
 import { MatDialog } from "@angular/material/dialog";
 
 import { VarResults } from "../../results/var-results";
-import { ResultElement, Message, MessageSeverity } from "jalhyd";
+import { ResultElement, Message, MessageSeverity, Observer } from "jalhyd";
 import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "./results.component";
 import { DialogLogEntriesDetailsComponent } from "../dialog-log-entries-details/dialog-log-entries-details.component";
 import { AppComponent } from "../../app.component";
-import { longestVarParam } from "../../../app/util";
+import { longestVarParam } from "../../../app/util/util";
 
 @Component({
     selector: "var-results",
@@ -17,7 +17,7 @@ import { longestVarParam } from "../../../app/util";
         "./var-results.component.scss"
     ]
 })
-export class VarResultsComponent extends ResultsComponentDirective {
+export class VarResultsComponent extends ResultsComponentDirective implements Observer, OnInit {
 
     /** size of the longest variated parameter */
     public size: number;
@@ -42,12 +42,17 @@ export class VarResultsComponent extends ResultsComponentDirective {
         protected logEntriesDetailsDialog: MatDialog
     ) {
         super();
+        this.intlService.addObserver(this);
     }
 
     /** Refreshes results and builds the dataset */
     @Input()
     public set results(r: VarResults) {
         this._varResults = r;
+        this.updateResults();
+    }
+
+    private updateResults() {
         this._results = [];
         this._headers = [];
         this._messages = [];
@@ -190,4 +195,16 @@ export class VarResultsComponent extends ResultsComponentDirective {
             }
         );
     }
+
+    ngOnInit(): void {
+        this._varResults.updateCalculatedParameterHeader();
+    }
+
+    // Observer interface
+
+    update(sender: any, data: any): void {
+        if (sender instanceof I18nService) {
+            this._varResults.update();
+        }
+    }
 }
diff --git a/src/app/components/generic-calculator/calculator.component.html b/src/app/components/generic-calculator/calculator.component.html
index 40cac6c063adec39c968550e7bb7e42cbc26d6e5..1a816d5c87dcf4c891dad500be5c5facd9b989e0 100644
--- a/src/app/components/generic-calculator/calculator.component.html
+++ b/src/app/components/generic-calculator/calculator.component.html
@@ -105,17 +105,17 @@
                                     (tabPressed)="onTabPressed($event)">
                                 </field-set>
 
-                                <structure-fieldset-container *ngIf="isStructureFieldsetContainer(fe)"
+                                <fieldset-container *ngIf="isStructureFieldsetContainer(fe)"
                                     [style.display]="getElementStyleDisplay(fe.id)" [_container]=fe
                                     (radio)=onRadioClick($event) (validChange)=onElementValid() (inputChange)=onInputChange($event)
-                                    (tabPressed)="onTabPressed($event)">
-                                </structure-fieldset-container>
+                                    (tabPressed)="onTabPressed($event)" [class]="getFieldsetContainerClass(fe)">
+                                </fieldset-container>
 
                                 <basin-fieldset-container *ngIf="isBasinFieldsetContainer(fe)"
                                     [style.display]="getElementStyleDisplay(fe.id)" [_container]=fe
                                     (radio)=onRadioClick($event) (validChange)=onElementValid()
                                     (inputChange)=onInputChange($event) (tabPressed)="onTabPressed($event)"
-                                    fxFlex="1 0 auto">
+                                    fxFlex="1 0 auto" [class]="getFieldsetContainerClass(fe)">
                                 </basin-fieldset-container>
                             </ng-template>
                         </div>
@@ -135,15 +135,15 @@
                                 [style.display]="getElementStyleDisplay(fe.id)" [_container]=fe
                                 (radio)=onRadioClick($event) (validChange)=onElementValid() (inputChange)=onInputChange($event)
                                 (tabPressed)="onTabPressed($event)"
-                                fxFlex="1 0 auto">
+                                fxFlex="1 0 auto" [class]="getFieldsetContainerClass(fe)">
                             </fieldset-container>
 
-                            <structure-fieldset-container *ngIf="isStructureFieldsetContainer(fe)"
+                            <fieldset-container *ngIf="isStructureFieldsetContainer(fe)"
                                 [style.display]="getElementStyleDisplay(fe.id)" [_container]=fe
                                 (radio)=onRadioClick($event) (validChange)=onElementValid() (inputChange)=onInputChange($event)
                                 (tabPressed)="onTabPressed($event)"
-                                fxFlex="1 0 auto">
-                            </structure-fieldset-container>
+                                fxFlex="1 0 auto" [class]="getFieldsetContainerClass(fe)">
+                            </fieldset-container>
 
                             <pab-table *ngIf="isPabTable(fe)" [pabTable]=fe (radio)=onRadioClick($event)
                                 (validChange)=onElementValid() (inputChange)=onInputChange($event)
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index 49e81d946b6216bb123c23a0c64dea8c32cbe730..9d5a06dff1a564c2af4d7385e49fcf8b8c425c41 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -28,7 +28,7 @@ import {
     ParallelStructure
 } from "jalhyd";
 
-import { generateValuesCombination, getUnformattedIthResult, getUnformattedIthValue } from "../../util";
+import { generateValuesCombination, getUnformattedIthResult, getUnformattedIthValue } from "../../util/util";
 
 import { AppComponent } from "../../app.component";
 import { FormulaireService } from "../../services/formulaire.service";
@@ -62,7 +62,7 @@ import { sprintf } from "sprintf-js";
 
 import * as XLSX from "xlsx";
 import { ServiceFactory } from "app/services/service-factory";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 import { FormulaireCourbeRemous } from "app/formulaire/definition/form-courbe-remous";
 import { RemousResults } from "app/results/remous-results";
 
@@ -232,6 +232,16 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
         return false;
     }
 
+    public getFieldsetContainerClass(fe: any): string {
+        if (this.isStructureFieldsetContainer(fe)) {
+            return "structure-fieldsetcontainer";
+        }
+        if (this.isBasinFieldsetContainer(fe)) {
+            return "basin-fieldsetcontainer";
+        }
+        return "fieldsetcontainer";
+    }
+
     /** détermine si un FormulaireElement est du type PabTable */
     public isPabTable(fe: any): boolean {
         return fe instanceof PabTable;
@@ -412,6 +422,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
      */
     protected afterFirstViewChecked() {
         this.updateUIValidity();
+        this.formulaire.setFirstDisplayCompleted();
     }
 
     public onCloseForm() {
@@ -567,6 +578,8 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
                     res = res && pstr.getChildren().length > 0;
                     break;
             }
+
+            res = res && this._formulaire.currentNub.isComputable();
         }
 
         this._isUIValid.value = res;
@@ -1259,7 +1272,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
                 const form = this._formulaire as FormulaireFixedVar;
                 const nub = (form.currentNub as Espece);
                 nub.loadPredefinedSpecies(result.selected);
-                form.refreshFieldsets();
+                form.refreshFieldsets(false);
             }
         });
     }
diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts
index d95168143cb67edb5fff3ffe00586e65fe60ad4f..0054b643d07eca347080b5cd1a53d38c5b9df85e 100644
--- a/src/app/components/generic-input/generic-input.component.ts
+++ b/src/app/components/generic-input/generic-input.component.ts
@@ -5,7 +5,7 @@ import { FormulaireDefinition } from "../../formulaire/definition/form-definitio
 import { NgParameter } from "../../formulaire/elements/ngparam";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 
 /**
  * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur
@@ -119,9 +119,10 @@ export abstract class GenericInputComponentDirective implements OnChanges {
      */
     private setAndEmitValid() {
         this._isValid.value = this._isValidUI && this._isValidModel;
-        if (this._isValid.changed) {
-            this.change.emit({ "action": "valid", "value": this._isValid.value });
-        }
+        // if (this._isValid.changed) 
+        // {
+        this.change.emit({ "action": "valid", "value": this._isValid.value });
+        // }
     }
 
     /**
diff --git a/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts b/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts
index 5bd87e2bb12d5177c109a493bcc92517bef823ce..bf4e084e134588c955fd4da70e1a8de96b82808c 100644
--- a/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts
+++ b/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts
@@ -5,7 +5,7 @@ import { BaseChartDirective } from "ng2-charts";
 import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { IYSeries } from "../../results/y-series";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { AppComponent } from "../../app.component";
 
 import { Jet, Result } from "jalhyd";
diff --git a/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts b/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts
index 86eba4301034bb0bc758ebed7904cf1e4ae25f11..039681e3c0195eb26e976b2d3b6ab31fd3189534 100644
--- a/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts
+++ b/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts
@@ -2,7 +2,7 @@ import { Component, Input } from "@angular/core";
 
 import { Result, cLog, Message, MessageCode, MessageSeverity, MRCInclination } from "jalhyd";
 
-import { fv } from "../../../app/util";
+import { fv } from "../../../app/util/util";
 
 import { CalculatorResults } from "../../results/calculator-results";
 import { NgParameter } from "../../formulaire/elements/ngparam";
@@ -92,7 +92,7 @@ export class MacrorugoCompoundResultsComponent extends ResultsComponentDirective
             this.mrcResults
             && this.mrcResults.result
             && this.mrcResults.result.sourceNub
-            && this.mrcResults.result.sourceNub.properties.getPropValue("inclinedApron") === MRCInclination.INCLINED
+            && this.mrcResults.result.sourceNub.getPropValue("inclinedApron") === MRCInclination.INCLINED
         );
     }
 
diff --git a/src/app/components/modules-diagram/modules-diagram.component.ts b/src/app/components/modules-diagram/modules-diagram.component.ts
index 6f227edcbd98057673505915f06def23886df2b3..0ec435a0739a6691b60d18390aab60a0f4227fdc 100644
--- a/src/app/components/modules-diagram/modules-diagram.component.ts
+++ b/src/app/components/modules-diagram/modules-diagram.component.ts
@@ -33,7 +33,7 @@ import * as SvgPanZoom from "svg-pan-zoom";
 
 import { MatomoTracker } from "@ngx-matomo/tracker";
 
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 
 @Component({
     selector: "modules-diagram",
@@ -265,11 +265,11 @@ export class ModulesDiagramComponent implements AfterContentInit, AfterViewCheck
      */
     private describe(n: Nub) {
         let type = CalculatorType[n.calcType];
-        const nt = n.properties.getPropValue("nodeType");
+        const nt = n.getPropValue("nodeType");
         if (nt) {
             type = SectionType[nt];
         } else {
-            const ld = n.properties.getPropValue("loiDebit");
+            const ld = n.getPropValue("loiDebit");
             if (ld !== undefined) {
                 type = LoiDebit[ld];
             }
diff --git a/src/app/components/pab-profile-chart/pab-profile-chart.component.ts b/src/app/components/pab-profile-chart/pab-profile-chart.component.ts
index c8eff7ec5fbb007f2b11bf6f48ca87a94e3edab1..11aca1b4046fb5c4b0119cf4a5c87b4a56f41b20 100644
--- a/src/app/components/pab-profile-chart/pab-profile-chart.component.ts
+++ b/src/app/components/pab-profile-chart/pab-profile-chart.component.ts
@@ -6,7 +6,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { PabResults } from "../../results/pab-results";
 import { IYSeries } from "../../results/y-series";
-import { fv, longestVarParam } from "../../util";
+import { fv, longestVarParam } from "../../util/util";
 import { AppComponent } from "../../app.component";
 
 import { CloisonAval, Cloisons, LoiDebit } from "jalhyd";
@@ -268,7 +268,7 @@ export class PabProfileChartComponent extends ResultsComponentDirective implemen
                     ddSeries[sti] = [];
                 }
                 // orifices have no relevant ZDV
-                if (st.properties.getPropValue("loiDebit") !== LoiDebit.OrificeSubmerged) {
+                if (st.getPropValue("loiDebit") !== LoiDebit.OrificeSubmerged) {
                     // 2 points, to draw a segment
                     ddSeries[sti].push({
                         x: xs[i],
@@ -305,7 +305,7 @@ export class PabProfileChartComponent extends ResultsComponentDirective implemen
             }
             // orifices have no relevant ZDV; lift gate will be drawn later for each series
             if (! [ LoiDebit.OrificeSubmerged, LoiDebit.VanLevLarinier, LoiDebit.VanLevVillemonte ]
-                .includes(st.properties.getPropValue("loiDebit"))
+                .includes(st.getPropValue("loiDebit"))
             ) {
                 // 2 points, to draw a segment
                 ddSeries[sti].push({
@@ -403,7 +403,7 @@ export class PabProfileChartComponent extends ResultsComponentDirective implemen
                 // draw lift gate if any
                 if (isLastAbscissa) {
                     for (const st of dw.structures) {
-                        if ([ LoiDebit.VanLevLarinier, LoiDebit.VanLevVillemonte ].includes(st.properties.getPropValue("loiDebit"))) {
+                        if ([LoiDebit.VanLevLarinier, LoiDebit.VanLevVillemonte].includes(st.getPropValue("loiDebit"))) {
                             // skip a point to disjoin line
                             dataN.push({
                                 x: x,
diff --git a/src/app/components/pab-results/pab-results-table.component.ts b/src/app/components/pab-results/pab-results-table.component.ts
index b541cdc52e332f6cde19abc74c9c9730db870fd8..b2b36a8368d34de4fa186b699152db63e9602442 100644
--- a/src/app/components/pab-results/pab-results-table.component.ts
+++ b/src/app/components/pab-results/pab-results-table.component.ts
@@ -6,7 +6,7 @@ import { PabResults } from "../../results/pab-results";
 import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { AppComponent } from "../../app.component";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 
 @Component({
     selector: "pab-results-table",
diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts
index 8ef4e14c146d960d5b7d726f0fbb1c824a1d3da1..dc2470aef9334fdf718ddc095c103a460279dece 100644
--- a/src/app/components/pab-table/pab-table.component.ts
+++ b/src/app/components/pab-table/pab-table.component.ts
@@ -28,7 +28,7 @@ import { PabTable } from "../../formulaire/elements/pab-table";
 import { DialogEditPabComponent } from "../dialog-edit-pab/dialog-edit-pab.component";
 import { AppComponent } from "../../app.component";
 import { NgParameter, ParamRadioConfig } from "../../formulaire/elements/ngparam";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 
 /**
  * The big editable data grid for calculator type "Pab" (component)
@@ -594,7 +594,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
                     if (i === 0) { // 1st row
                         deviceParamRow.cells.push({
                             model: ouvrage,
-                            modelValue: ouvrage.properties.getPropValue("loiDebit"),
+                            modelValue: ouvrage.getPropValue("loiDebit"),
                             options: loisCloisons,
                             selectable: ouvrage
                         });
@@ -719,7 +719,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni
                 if (i === 0) { // 1st row
                     deviceParamRowDW.cells.push({
                         model: ouvrage,
-                        modelValue: ouvrage.properties.getPropValue("loiDebit"),
+                        modelValue: ouvrage.getPropValue("loiDebit"),
                         options: loisAval
                     });
                 }
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 641d9c4b026793c3f12900a2de31ee916068ff30..6cc879f86446cbcbdf0bf69e5e9bc0e6ec483072 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
@@ -41,6 +41,7 @@
                 <span fxHide.gt-xxs>C</span>
             </mat-button-toggle>
 
+            <!-- (click)="onRadioClick('link')" [checked]="isRadioLinkChecked" [disabled]="isRadioLinkDisabled"> -->
             <mat-button-toggle class="radio_link" value="radio_link" *ngIf="hasRadioLink()"
                 (click)="onRadioClick('link')" [checked]="isRadioLinkChecked" [disabled]="! canExitCalcMode()">
                 <span fxHide.xxs>{{ uitextParamLie }}</span>
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 a64528e1fcab24d304caf2763e13ada2708894ad..43cea6de730381b4fd523ce661b5ed94f4a0863a 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.service";
 import { NgParameter, ParamRadioConfig } from "../../formulaire/elements/ngparam";
 import { NgParamInputComponent } from "../ngparam-input/ngparam-input.component";
 import { ServiceFactory } from "../../services/service-factory";
-import { ParamValueMode, ParallelStructure, ParamCalculability, Pab } from "jalhyd";
+import { ParamValueMode, ParallelStructure, ParamCalculability, Pab, Session } from "jalhyd";
 import { FormulaireService } from "../../services/formulaire.service";
 import { ParamLinkComponent } from "../param-link/param-link.component";
 import { ParamValuesComponent } from "../param-values/param-values.component";
@@ -192,7 +192,7 @@ export class ParamFieldLineComponent implements OnChanges {
     * calcule la présence du radio "paramètre lié" (importé d'un autre module de calcul)
     */
     public hasRadioLink(): boolean {
-        if (this._formService.formulaires.length > 0) {
+        if (this._formService.formulaires.length > 0 && !this.isParameterLinkTarget()) {
             // au moins 2 modules de calcul ouverts
             if (this._formService.formulaires.length > 1) {
                 return this._formService.getLinkableValues(this.param).length > 0;
@@ -254,6 +254,20 @@ export class ParamFieldLineComponent implements OnChanges {
         return false;
     }
 
+    /**
+     * @returns true if parameter is a link target
+     */
+    private isParameterLinkTarget(): boolean {
+        return Session.getInstance().isParameterLinkTarget(this.param.paramDefinition);
+    }
+
+    /**
+     * compute link radio button "disabled" status
+     */
+    public get isRadioLinkDisabled(): boolean {
+        return !this.canExitCalcMode() || this.isParameterLinkTarget();
+    }
+
     public get radioCalTitle(): string {
         if (this.isRadioCalDisabled) {
             return this.intlService.localizeText("INFO_PARAMFIELD_CANNOT_CALC_LINKS_LOOP");
@@ -333,6 +347,10 @@ export class ParamFieldLineComponent implements OnChanges {
             case "cancelvar": // cancel button clicked in DialogEditParamValuesComponent
                 this.param.loadObjectRepresentation(this.paramBackup);
                 break;
+
+            case "okvar":
+                this.emitValidity();
+                break;
         }
     }
 
diff --git a/src/app/components/param-values/param-values.component.ts b/src/app/components/param-values/param-values.component.ts
index d39e310fecdaf7f8eb44affcae5ed76b9c24b9b5..b77eff7ade61558e17c7825455cb3a22f6b2b5f5 100644
--- a/src/app/components/param-values/param-values.component.ts
+++ b/src/app/components/param-values/param-values.component.ts
@@ -74,6 +74,10 @@ export class ParamValuesComponent implements AfterViewInit, Observer {
                 this.change.emit({
                     action: "cancelvar"
                 });
+            } else {
+                this.change.emit({
+                    action: "okvar"
+                });
             }
         });
     }
diff --git a/src/app/components/pb-results/pb-cloison-results.component.ts b/src/app/components/pb-results/pb-cloison-results.component.ts
index 0a59b6a8b1f0581e7ff07e706f24459e6e7df5ce..da3aae4ba32983e163a9096fdf1db300ea78aab3 100644
--- a/src/app/components/pb-results/pb-cloison-results.component.ts
+++ b/src/app/components/pb-results/pb-cloison-results.component.ts
@@ -2,7 +2,7 @@ import { Component, Input } from "@angular/core";
 
 import { FixedResultsComponent } from "../fixedvar-results/fixed-results.component";
 import { NgParameter } from "../../formulaire/elements/ngparam";
-import { getIthValue } from "../../util";
+import { getIthValue } from "../../util/util";
 import { PbCloisonResults } from "../../results/pb-cloison-results";
 
 import { Result, ResultElement } from "jalhyd";
diff --git a/src/app/components/pb-results/pb-results-table.component.ts b/src/app/components/pb-results/pb-results-table.component.ts
index 79084192dd3b7b5e0477157ea06f3e8a09b1e5c9..9e1315d86f6d1f8608f728389431494d1065d239 100644
--- a/src/app/components/pb-results/pb-results-table.component.ts
+++ b/src/app/components/pb-results/pb-results-table.component.ts
@@ -5,7 +5,7 @@ import { PreBarrage, PbBassin } from "jalhyd";
 import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { AppComponent } from "../../app.component";
-import { fv, getIthValue } from "../../util";
+import { fv, getIthValue } from "../../util/util";
 import { PrebarrageResults } from "../../results/prebarrage-results";
 
 @Component({
diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts
index 6de70eecd8ddf7c49d2c45a200a9ae6a6cf0a009..ef424b22fd24c3c88efd3680c3d0250f38ba1512 100644
--- a/src/app/components/pb-schema/pb-schema.component.ts
+++ b/src/app/components/pb-schema/pb-schema.component.ts
@@ -18,9 +18,9 @@ import { GenericCalculatorComponent } from "../generic-calculator/calculator.com
 import { FormulairePrebarrage } from "../../formulaire/definition/form-prebarrage";
 import { AppComponent } from "../../app.component";
 
-import { fv } from "app/util";
+import { fv } from "app/util/util";
 import { ServiceFactory } from "app/services/service-factory";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 import { PrebarrageService, PrebarrageServiceEvents } from "app/services/prebarrage.service";
 
 /**
diff --git a/src/app/components/remous-results/remous-results.component.ts b/src/app/components/remous-results/remous-results.component.ts
index 1f5ea4b720bfcb4bbf0ccbd85f42bde4dc582540..2fd73ae1e13f3ecb6b1b9444b07e0800ca6435f8 100644
--- a/src/app/components/remous-results/remous-results.component.ts
+++ b/src/app/components/remous-results/remous-results.component.ts
@@ -9,7 +9,7 @@ import { FormulaireService } from "../../services/formulaire.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { AppComponent } from "../../app.component";
 import { LineData, ChartData } from "./line-and-chart-data";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { VarResults } from "../../results/var-results";
 
 import { BaseChartDirective } from "ng2-charts";
diff --git a/src/app/components/results-chart/chart-type.component.ts b/src/app/components/results-chart/chart-type.component.ts
index 39839c3a96cb2feea1c220ebbf415e2855263880..123657c5173e1f09900f327f140c4cdd6eb3dc65 100644
--- a/src/app/components/results-chart/chart-type.component.ts
+++ b/src/app/components/results-chart/chart-type.component.ts
@@ -4,7 +4,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { ChartType } from "../../results/chart-type";
 import { SelectFieldChartType } from "app/formulaire/elements/select/select-field-charttype";
 import { SelectEntry } from "app/formulaire/elements/select/select-entry";
-import { decodeHtml } from "../../util";
+import { decodeHtml } from "../../util/util";
 
 @Component({
     selector: "chart-type",
diff --git a/src/app/components/results-chart/results-chart.component.ts b/src/app/components/results-chart/results-chart.component.ts
index 0b33967f3c45ae7766943a382b9db32fc3593ed7..a52719212a258e15957e4f8d27e586345f77474a 100644
--- a/src/app/components/results-chart/results-chart.component.ts
+++ b/src/app/components/results-chart/results-chart.component.ts
@@ -11,7 +11,7 @@ import { ChartType } from "../../results/chart-type";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { IYSeries } from "../../results/y-series";
 import { VarResults } from "../../results/var-results";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { AppComponent } from "../../app.component";
 
 import zoomPlugin from 'chartjs-plugin-zoom';
@@ -182,6 +182,10 @@ export class ResultsChartComponent extends ResultsComponentDirective implements
     }
 
     public ngOnChanges() {
+        if (this._results instanceof VarResults) {
+            this._results.updateCalculatedParameterHeader();
+        }
+
         // redessiner le graphique chaque fois qu'une entrée change
         this.drawChart();
     }
diff --git a/src/app/components/select-field-line/select-field-line.component.ts b/src/app/components/select-field-line/select-field-line.component.ts
index c6b2474a0f08e795ac58514bb58486d127e4ecdb..7c4b067efcece001ba65882f713c0e286d660f48 100644
--- a/src/app/components/select-field-line/select-field-line.component.ts
+++ b/src/app/components/select-field-line/select-field-line.component.ts
@@ -4,7 +4,7 @@ import { SelectField } from "../../formulaire/elements/select/select-field";
 import { SelectEntry } from "../../formulaire/elements/select/select-entry";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
-import { decodeHtml } from "../../util";
+import { decodeHtml } from "../../util/util";
 
 @Component({
     selector: "select-field-line",
diff --git a/src/app/components/select-section-details/select-section-details.component.ts b/src/app/components/select-section-details/select-section-details.component.ts
index 55c21b523cead45096bfc15f39d2d733043aad92..278ce4bdca3d2ead89a5354807f4266b1f4171a0 100644
--- a/src/app/components/select-section-details/select-section-details.component.ts
+++ b/src/app/components/select-section-details/select-section-details.component.ts
@@ -3,7 +3,7 @@ import { Router } from '@angular/router';
 import { FormulaireDefinition } from 'app/formulaire/definition/form-definition';
 import { FormulaireService } from 'app/services/formulaire.service';
 import { I18nService } from 'app/services/internationalisation.service';
-import { fv } from 'app/util';
+import { fv } from 'app/util/util';
 import { formattedValue } from 'jalhyd';
 
 /**
diff --git a/src/app/components/structure-fieldset-container/structure-fieldset-container.component.ts b/src/app/components/structure-fieldset-container/structure-fieldset-container.component.ts
deleted file mode 100644
index 34b9a3bbabc1b14e4ef87fcffa44b47e48d4efd6..0000000000000000000000000000000000000000
--- a/src/app/components/structure-fieldset-container/structure-fieldset-container.component.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Component } from "@angular/core";
-
-import { FieldSet } from "../../formulaire/elements/fieldset";
-import { I18nService } from "../../services/internationalisation.service";
-import { ApplicationSetupService } from "../../services/app-setup.service";
-import { FieldsetContainerComponent } from "../fieldset-container/fieldset-container.component";
-
-@Component({
-    selector: "structure-fieldset-container",
-    templateUrl: "../fieldset-container/fieldset-container.component.html",
-    styleUrls: [
-        "../fieldset-container/fieldset-container.component.scss"
-    ]
-})
-export class StructureFieldsetContainerComponent extends FieldsetContainerComponent {
-
-    constructor(i18nService: I18nService, appSetupService: ApplicationSetupService) {
-        super(i18nService, appSetupService);
-    }
-}
diff --git a/src/app/components/variable-results-selector/variable-results-selector.component.ts b/src/app/components/variable-results-selector/variable-results-selector.component.ts
index 0d68b44573e4177fb943019a8516944fdbc94162..50c4a0d968df136c66c0f25f9391c0c2a5688820 100644
--- a/src/app/components/variable-results-selector/variable-results-selector.component.ts
+++ b/src/app/components/variable-results-selector/variable-results-selector.component.ts
@@ -1,7 +1,7 @@
 import { Component, Input, OnChanges } from "@angular/core";
 
 import { I18nService } from "../../services/internationalisation.service";
-import { fv, longestVarParam } from "../../util";
+import { fv, longestVarParam } from "../../util/util";
 import { MultiDimensionResults } from "../../results/multidimension-results";
 import { VariatedDetails } from "jalhyd";
 import { CalculatorResults } from "../../results/calculator-results";
diff --git a/src/app/config.json b/src/app/config.json
index 97f3c60d6743c80c47231016f0666d26fb40e77d..1bc4882309f296358e3b051647ffbb6b8867896d 100644
--- a/src/app/config.json
+++ b/src/app/config.json
@@ -63,7 +63,7 @@
                 "path": "en-charge.jpg",
                 "credits": "Catherine Tailleux / Inrae"
             },
-            "calculators": [ 1, 0 ]
+            "calculators": [ 35, 0 ]
         },
         {
             "name": "LOIS_D_OUVRAGES",
diff --git a/src/app/formulaire/definition/form-courbe-remous.ts b/src/app/formulaire/definition/form-courbe-remous.ts
index 3762c846cb3fd3c5a11a21d850746f0a024c9074..62c8d3b3b20b6f9d962f626aa482b10cf27b1c2c 100644
--- a/src/app/formulaire/definition/form-courbe-remous.ts
+++ b/src/app/formulaire/definition/form-courbe-remous.ts
@@ -31,7 +31,7 @@ export class FormulaireCourbeRemous extends FormulaireSection {
         this._remousResults.parameters = prmCR;
 
         // variable supplémentaire à calculer
-        this._remousResults.extraParamSymbol = this.currentNub.properties.getPropValue("varCalc");
+        this._remousResults.extraParamSymbol = this.currentNub.getPropValue("varCalc");
 
         // calcul
         this._remousResults.result = cr.CalcSerie();
diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts
index 475c630c86e8664ce74bdfcef850275bd4f32a67..d7022502972c2cc748f6477791ce8e32ce60f977 100644
--- a/src/app/formulaire/definition/form-definition.ts
+++ b/src/app/formulaire/definition/form-definition.ts
@@ -25,6 +25,7 @@ import { ServiceFactory } from "../../services/service-factory";
 import { SelectEntry } from "../elements/select/select-entry";
 import { SelectField } from "../elements/select/select-field";
 import { DeepSelectFieldIterator } from "../form-iterator/deep-selectfield-iterator";
+import { ConfigParser } from "./config-parser";
 
 /**
  * classe de base pour tous les formulaires
@@ -61,6 +62,11 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     */
     protected _calcResults: CalculatorResults[];
 
+    /**
+     * Flag indiquant que le composant calculette est proche de la fin du 1er affichage (1er appel à AfterViewChecked)
+     */
+    private _firstDisplay: boolean = true;
+
     constructor(parent?: FormulaireNode) {
         super(parent);
         SessionSettings.instance.addObserver(this);
@@ -80,14 +86,26 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         return this._calculateDisabled;
     }
 
+    public get isFirstDisplay(): boolean {
+        return this._firstDisplay;
+    }
+
+    public setFirstDisplayCompleted() {
+        this._firstDisplay = false;
+    }
+
+    public getPropValue(key: string): any {
+        if (this._currentNub === undefined)
+            return this.defaultProperties[key];
+        return this._currentNub.getPropValue(key);
+    }
+
     public get calculatorType(): CalculatorType {
-        const props = this._currentNub === undefined ? this.defaultProperties : (this._currentNub.properties as Props).props;
-        return props["calcType"];
+        return this.getPropValue("calcType")
     }
 
     public get nodeType(): SectionType {
-        const props = this._currentNub === undefined ? this.defaultProperties : (this._currentNub.properties as Props).props;
-        return props["nodeType"];
+        return this.getPropValue("nodeType")
     }
 
     public get calculatorName() {
@@ -112,6 +130,27 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         return this._props;
     }
 
+    public parseConfigToProps(): Props {
+        const res: Props = new Props();
+
+        const jp = new ConfigParser(this._jsonConfig);
+        for (const fs of jp.forAll("fieldset")) {
+            const fsp = new ConfigParser(fs["fields"]);
+            for (const sel of fsp.forAll("select")) {
+                const p = sel["property"];
+                if (p !== undefined) { // if select has associated property
+                    const v = sel["default"];
+                    if (v !== undefined) { // if select has a default value for associated property
+                        const enumClass = Props.enumFromProperty[p];
+                        res.setPropValue(p, enumClass[v]);
+                    }
+                }
+            }
+        }
+
+        return res;
+    }
+
     /**
      * Creates a Nub from the given properties, and associates it to the current form
      */
@@ -126,7 +165,7 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     }
 
     public set currentNub(n: Nub) {
-        const nubCalcType = (n.properties as Props).getPropValue("calcType");
+        const nubCalcType = n.getPropValue("calcType");
         if (this._props["calcType"] !== nubCalcType) {
             throw new Error(
                 `Nub ${n.properties["calcType"]} incompatible avec le formulaire ${this._calculatorName} (${this._props["calcType"]})`
@@ -146,6 +185,24 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         Session.getInstance().deleteNub(sn);
     }
 
+    /**
+     * parse calculator JSON configuration for select default value
+     * @param selectId select id, ie. "id" field value
+     * @return select "default" field value
+     */
+    protected parseSelectDefaultValue(json: {}, selectId: string): string {
+        const jp = new ConfigParser(json);
+        for (const fs of jp.forAll("fieldset")) {
+            const fsp = new ConfigParser(fs["fields"]);
+            for (const sel of fsp.forAll("select")) {
+                if (sel["id"] === selectId) {
+                    return sel["default"];
+                }
+            }
+        }
+        return undefined;
+    }
+
     /**
      * prepare options parsing
      */
@@ -302,11 +359,10 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
     }
 
     /**
-     * Trouve le Ngparameter correspondant au symbole "symbol", parmi tous les
-     * éléments du formulaire
-     * @param symbol string
+     * Trouve le NgParameter correspondant au paramètre Nub parmi tous les éléments du formulaire
+     * @param param paramètre critère de recherche
      */
-    public getParamFromSymbol(param: ParamDefinition): NgParameter {
+    private getNgparamFromNubParam(param: ParamDefinition): NgParameter {
         for (const p of this.allFormElements) {
             if (p instanceof NgParameter) {
                 if (p.symbol === param.symbol && p.paramDefinition.parentNub.uid === param.parentNub.uid) {
@@ -316,6 +372,14 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         }
     }
 
+    public getOrCreateParam(nubParam: ParamDefinition, parent: FormulaireNode): NgParameter {
+        const res = this.getNgparamFromNubParam(nubParam);
+        if (res === undefined) {
+            return new NgParameter(nubParam, parent);
+        }
+        return res;
+    }
+
     public getFieldById(id: string): Field {
         const res = this.getFormulaireNodeById(id);
         if (res instanceof Field) {
@@ -488,14 +552,10 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
 
     protected getComputedParameter(): NgParameter {
         const cpd = this.currentNub.calculatedParam;
-        let ngparam: NgParameter;
         if (cpd !== undefined) {
-            ngparam = this.getParamFromSymbol(cpd);
-            if (ngparam === undefined) { // calculated parameter is not displayed on screen
-                ngparam = new NgParameter(cpd, this);
-            }
+            return this.getOrCreateParam(cpd, this);
         }
-        return ngparam;
+        return undefined;
     }
 
     /** find variated (possibly linked) parameters from model, and get their values at the same time */
@@ -508,7 +568,7 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         const fixedParams: ParamDefinition[] = this._currentNub.findFixedParams();
         let fnp: NgParameter[] = [];
         for (const fp of fixedParams) {
-            fnp.push(this.getParamFromSymbol(fp));
+            fnp.push(this.getNgparamFromNubParam(fp));
         }
         fnp = fnp.filter((e) => e !== undefined);
         return fnp;
diff --git a/src/app/formulaire/definition/form-espece.ts b/src/app/formulaire/definition/form-espece.ts
index 10ded3b83c1994fec8e6ff43cd5d5a684d13a79b..f0a36b93e1f5c9d4213438bd9095b25a7170292b 100644
--- a/src/app/formulaire/definition/form-espece.ts
+++ b/src/app/formulaire/definition/form-espece.ts
@@ -10,7 +10,7 @@ export class FormulaireEspece extends FormulaireFixedVar {
 
     protected completeParse(firstNotif: boolean = true) {
         super.completeParse(firstNotif);
-        this.updateDivingJetCriteriaInputs(this.currentNub.properties.getPropValue("divingJetSupported"));
+        this.updateDivingJetCriteriaInputs(this.currentNub.getPropValue("divingJetSupported"));
     }
 
     /**
diff --git a/src/app/formulaire/definition/form-fixedvar.ts b/src/app/formulaire/definition/form-fixedvar.ts
index 0a329f489308d59093da98d30da932a160e249fa..f20ce41fef290508de47d54f63fb72b826792e4d 100644
--- a/src/app/formulaire/definition/form-fixedvar.ts
+++ b/src/app/formulaire/definition/form-fixedvar.ts
@@ -63,7 +63,7 @@ export class FormulaireFixedVar extends FormulaireDefinition {
 
     public afterParseFieldset(fs: FieldSet) {
         // observe all Select fields @see this.update()
-        fs.properties.addObserver(this);
+        fs.addPropertiesObserver(this);
     }
 
     protected completeParse(firstNotif: boolean = true) {
@@ -82,8 +82,7 @@ export class FormulaireFixedVar extends FormulaireDefinition {
 
     protected compute() {
         this.runNubCalc(this.currentNub);
-        this.refreshFieldsets(); // important: before reaffectResultComponents() or it will break results components localization
-        // this.reaffectResultComponents();  // seems useless since called from runNubCalc()
+        this.reaffectResultComponents();
     }
 
     protected reaffectResultComponents() {
@@ -115,9 +114,9 @@ export class FormulaireFixedVar extends FormulaireDefinition {
     /**
      * Forces all fieldsets to update all their fields
      */
-    public refreshFieldsets() {
+    public refreshFieldsets(forceClear: boolean) {
         for (const fs of this.allFieldsets) {
-            fs.updateFields();
+            fs.updateFields(forceClear);
         }
         this.completeParse(false); // re-add observers that were destroyed by updateFields()
     }
@@ -130,7 +129,7 @@ export class FormulaireFixedVar extends FormulaireDefinition {
         if (data.action === "propertyChange") {
             this.reset();
             // reflect changes in GUI (who knows ?), for ex. show / hide dependent fields
-            this.refreshFieldsets();
+            this.refreshFieldsets(true);
         }
     }
 }
diff --git a/src/app/formulaire/definition/form-macrorugo-compound.ts b/src/app/formulaire/definition/form-macrorugo-compound.ts
index ec470e645ca81406ae5e353736467e77427508ba..5dd33e865c4dada32d3de547e0f9d7696423b6a3 100644
--- a/src/app/formulaire/definition/form-macrorugo-compound.ts
+++ b/src/app/formulaire/definition/form-macrorugo-compound.ts
@@ -56,7 +56,7 @@ export class FormulaireMacrorugoCompound extends FormulaireRepeatableFieldset {
         super.completeParse(firstNotif);
         this.fieldsetContainer.addObserver(this);
         if (firstNotif) {
-            this.updateApronState(this.currentNub.properties.getPropValue("inclinedApron"));
+            this.updateApronState(this.currentNub.getPropValue("inclinedApron"));
         }
     }
 
@@ -84,7 +84,7 @@ export class FormulaireMacrorugoCompound extends FormulaireRepeatableFieldset {
      */
     public updateApronState(inclined: MRCInclination) {
         // show / hide dependent fields (read from model)
-        this.refreshFieldsets();
+        this.refreshFieldsets(false);
         // show / hide children list (GUI only)
         for (const elt of this.allFormElements) {
             if (elt instanceof FieldsetContainer) {
diff --git a/src/app/formulaire/definition/form-pab.ts b/src/app/formulaire/definition/form-pab.ts
index 85f423501196014b5198c27d9e5a6a243524e796..d27a620927df68c68c2789250112d8889d7dbd93 100644
--- a/src/app/formulaire/definition/form-pab.ts
+++ b/src/app/formulaire/definition/form-pab.ts
@@ -3,7 +3,7 @@ import { Pab, Result, VariatedDetails } from "jalhyd";
 import { FormulaireDefinition } from "./form-definition";
 import { PabResults } from "../../results/pab-results";
 import { NgParameter } from "../elements/ngparam";
-import { longestVarParam } from "../../util";
+import { longestVarParam } from "../../util/util";
 import { PabTable } from "../elements/pab-table";
 
 /**
diff --git a/src/app/formulaire/definition/form-parallel-structures.ts b/src/app/formulaire/definition/form-parallel-structures.ts
index b47c4808b021bba832df9c201b65ebc4aadb76f5..a5de99edd14ca3a699a0e1f3c502e49879046d80 100644
--- a/src/app/formulaire/definition/form-parallel-structures.ts
+++ b/src/app/formulaire/definition/form-parallel-structures.ts
@@ -1,4 +1,4 @@
-import { Structure, Nub, ParallelStructure, StructureProperties, Props, Session, ParamDefinition, Prop_NullParameters } from "jalhyd";
+import { Structure, Nub, ParallelStructure, StructureProperties, Props, Session, ParamDefinition, Prop_NullParameters, IProperties } from "jalhyd";
 
 import { FieldsetContainer } from "../elements/fieldset-container";
 import { FieldSet } from "../elements/fieldset";
@@ -71,7 +71,7 @@ export class FormulaireParallelStructure extends FormulaireRepeatableFieldset {
      * and return it; does not store it in the Session (for Structures, not for Calculator Modules)
      * @param p properties for the new Nub
      */
-    protected createStructure(p: Props): Structure {
+    protected createStructure(p: IProperties): Structure {
         return Session.getInstance().createNub(p, this.currentNub as ParallelStructure) as Structure;
     }
 
@@ -81,9 +81,9 @@ export class FormulaireParallelStructure extends FormulaireRepeatableFieldset {
      * @param sn Structure to replace
      * @param params properties to build the new Nub (calcType, loiDebit...)
      */
-    protected replaceNub(sn: Structure, params: Props): Nub {
+    protected replaceNub(sn: Structure): Nub {
         const parent = (this.currentNub as ParallelStructure);
-        const newStructure = this.createStructure(params);
+        const newStructure = this.createStructure(sn);
         parent.replaceChildInplace(sn, newStructure);
         return newStructure;
     }
@@ -108,7 +108,7 @@ export class FormulaireParallelStructure extends FormulaireRepeatableFieldset {
      * @param name nom de la propriété qui vient de changer
      * @param val nouvelle valeur de la propriété
      */
-    protected adjustProperties(props: Props, name: string, val: any) {
+    protected adjustProperties(props: IProperties, name: string, val: any) {
         if (name === "structureType") {
             if (! StructureProperties.isCompatibleValues(
                 val, props.getPropValue("loiDebit"), this.currentNub as ParallelStructure
@@ -159,12 +159,12 @@ export class FormulaireParallelStructure extends FormulaireRepeatableFieldset {
         } else if (sender instanceof FieldSet && data.action === "propertyChange") {
             switch (sender.id) {
                 case "fs_ouvrage":
-                    const props = sender.properties;
                     // ensure loiDebit is set
-                    props.setPropValue("loiDebit", data.value);
-                    this.adjustProperties(props, data["name"], data["value"]);
+                    //props.setPropValue("loiDebit", data.value); // ?? et si la propriété modifiée n'est pas la loi de débit ?
+
+                    this.adjustProperties(sender, data["name"], data["value"]);
                     // replace Structure Nub
-                    const newNub = this.replaceNub((sender.nub as Structure), props);
+                    const newNub = this.replaceNub(sender.nub as Structure);
                     sender.setNub(newNub);
                     // treat the fieldset as new to re-subscribe to Nub properties change events
                     this.afterParseFieldset(sender);
diff --git a/src/app/formulaire/definition/form-pb-cloison.ts b/src/app/formulaire/definition/form-pb-cloison.ts
index 25910c87d6a1ed5681bd8d7af9e5a45092f876c0..145c03fe403d38338dc557da1f3cb41a2cf29c89 100644
--- a/src/app/formulaire/definition/form-pb-cloison.ts
+++ b/src/app/formulaire/definition/form-pb-cloison.ts
@@ -27,12 +27,12 @@ export class FormulairePbCloison extends FormulaireParallelStructure {
         } else if (sender instanceof FieldSet && data.action === "propertyChange") {
             switch (sender.id) {
                 case "fs_ouvrage":
-                    const props = sender.properties;
                     // ensure loiDebit is set
-                    props.setPropValue("loiDebit", data.value);
-                    this.adjustProperties(props, data["name"], data["value"]);
+                    //props.setPropValue("loiDebit", data.value); // ?? et si la propriété modifiée n'est pas la loi de débit ?
+
+                    this.adjustProperties(sender, data["name"], data["value"]);
                     // replace Structure Nub
-                    const newNub = this.replaceNub((sender.nub as Structure), props);
+                    const newNub = this.replaceNub(sender.nub as Structure);
                     sender.setNub(newNub);
                     // treat the fieldset as new to re-subscribe to Nub properties change events
                     this.afterParseFieldset(sender);
diff --git a/src/app/formulaire/definition/form-prebarrage.ts b/src/app/formulaire/definition/form-prebarrage.ts
index 95e5f65498b3ad929c00730266a3fa0ccb33d221..fbccdec66c8b788bb59508949aa2bd2b7c5413a5 100644
--- a/src/app/formulaire/definition/form-prebarrage.ts
+++ b/src/app/formulaire/definition/form-prebarrage.ts
@@ -9,7 +9,7 @@ import { FieldsetContainer } from "../elements/fieldset-container";
 import { CalculatorResults } from "../../results/calculator-results";
 import { PrebarrageResults } from "../../results/prebarrage-results";
 import { NgParameter } from "../elements/ngparam";
-import { longestVarParam } from "../../util";
+import { longestVarParam } from "../../util/util";
 import { FormulaireNode } from "../elements/formulaire-node";
 
 /**
@@ -239,7 +239,7 @@ export class FormulairePrebarrage extends FormulaireFixedVar {
 
     protected compute() {
         this.runNubCalc(this.currentNub);
-        this.refreshFieldsets(); // important: before reaffectResultComponents() or it will break results components localization
+        this.refreshFieldsets(false); // important: before reaffectResultComponents() or it will break results components localization
         // reset variable index to avoid trying to access an index > 0 when nothing varies
         this._pbResults.variableIndex = 0;
 
@@ -367,7 +367,7 @@ export class FormulairePrebarrage extends FormulaireFixedVar {
                     } catch (e) {
                         let res;
                         if (p.parentNub.calcType === CalculatorType.Structure) {
-                            res = p.parentNub.getParent().uid;
+                            res = p.parentNub.parent.uid;
                         } else {
                             res = p.parentNub.uid;
                         }
diff --git a/src/app/formulaire/definition/form-pressureloss.ts b/src/app/formulaire/definition/form-pressureloss.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4329b0879880205fdf3031303eb091c6be0094ff
--- /dev/null
+++ b/src/app/formulaire/definition/form-pressureloss.ts
@@ -0,0 +1,58 @@
+import { PressureLoss, Props, PressureLossType, Session, PressureLossLaw, CalculatorType, IObservable } from "jalhyd";
+import { FormulaireFixedVar } from "./form-fixedvar";
+import { FieldSet } from "../elements/fieldset";
+import { Prop_NullParameters } from "jalhyd";
+
+/**
+ * Formulaire pour la perte de charge
+ */
+export class FormulairePressureLoss extends FormulaireFixedVar {
+    public preparseConfig(json: {}) {
+        super.preparseConfig(json);
+
+        // get pressure loss law select default value
+        const dft: string = this.parseSelectDefaultValue(json, "select_pressurelosstype");
+        this.defaultProperties["pressureLossType"] = PressureLossType[dft];
+    }
+
+    public initNub(props?: Props) {
+        if (props === undefined) {
+            props = new Props();
+        }
+
+        // create pressure loss parent nub
+        const pll = this.defaultProperties["pressureLossType"];
+        props.setPropValue("calcType", this.calculatorType);
+        props.setPropValue("pressureLossType", pll);
+        super.initNub(props);
+
+        // create pressure loss law child nub
+        const propsLaw: Props = new Props();
+        propsLaw.setPropValue(Prop_NullParameters, props.getPropValue(Prop_NullParameters));
+        propsLaw.setPropValue("calcType", CalculatorType.PressureLossLaw);
+        const law = Session.getInstance().createNub(propsLaw) as PressureLossLaw;
+        const pl: PressureLoss = this.currentNub as PressureLoss;
+        pl.setLaw(law);
+    }
+
+    public update(sender: IObservable, data: any) {
+        // if (sender instanceof FieldSet && sender.id === "fs_pressureloss_law" && data.action === "propertyChange") {
+        if (data.action === "propertyChange") {
+            if (data.name === "pressureLossType") {
+                // changement de propriété du FieldSet contenant le select de choix du type de perte de charge
+
+                // replace underlying pressure loss law without replacing whole Nub
+                const newPLL = Session.getInstance().createPressureLossLaw(data.value, undefined, this.currentNub.getPropValue(Prop_NullParameters));
+                (this._currentNub as PressureLoss).setLaw(newPLL);
+                // show / hide dependent fields
+                this.refreshFieldsets(true);
+                this.reset();
+            }
+            else if (data.name === "material") {
+                // changement de propriété du FieldSet contenant le select de choix du type de matériau
+                this.refreshFieldsets(false);
+                this.reset();
+            }
+        }
+    }
+}
diff --git a/src/app/formulaire/definition/form-section-parametree.ts b/src/app/formulaire/definition/form-section-parametree.ts
index 7f91f2fca963c76ba3e5bae49cfa8adc511afa8b..7ddfdefaf792c204256e0ec9cfebfc86e9d9588a 100644
--- a/src/app/formulaire/definition/form-section-parametree.ts
+++ b/src/app/formulaire/definition/form-section-parametree.ts
@@ -23,10 +23,6 @@ export class FormulaireSectionParametree extends FormulaireSection {
         this.reaffectResultComponents();
     }
 
-    protected runNubCalc(nub: Nub, computedParam?: ParamDefinition): Result {
-        return nub.CalcSerie();
-    }
-
     protected reaffectResultComponents() {
         this.resetFormResults(); // to avoid adding fixed parameters more than once (see below)
         const sectNub: SectionParametree = this.currentNub as SectionParametree;
diff --git a/src/app/formulaire/definition/form-section.ts b/src/app/formulaire/definition/form-section.ts
index 7ef1534e104c6b2a3193dfcdca657222c03f016e..d2aa08b33f532607425315ddbf139662708d15bb 100644
--- a/src/app/formulaire/definition/form-section.ts
+++ b/src/app/formulaire/definition/form-section.ts
@@ -3,30 +3,18 @@ import { FieldSet } from "../elements/fieldset";
 import { ServiceFactory } from "../../services/service-factory";
 
 import { IObservable, Session, SectionNub, Props, CalculatorType, Prop_NullParameters, acSection } from "jalhyd";
-import { ConfigParser } from "./config-parser";
 import { SectionType } from "jalhyd";
 
 export class FormulaireSection extends FormulaireFixedVar {
 
-    /** for SectionNubs only */
-    protected _defaultSectionType;
-
     /**
      * determine default section type from select configuration
      */
     private parseDefaultSectionType() {
-        if (this._defaultSectionType === undefined) {
-            const jp = new ConfigParser(this._jsonConfig);
-            for (const fs of jp.forAll("fieldset")) {
-                const fsp = new ConfigParser(fs["fields"]);
-                for (const sel of fsp.forAll("select")) {
-                    if (sel["id"] === "select_section") {
-                        const st = sel["default"];
-                        this._defaultSectionType = SectionType[st];
-                        return;
-                    }
-                }
-            }
+        if (this.defaultProperties["nodeType"] === undefined) // still use "nodeType" for consistency, should be named "sectionType"
+        {
+            const def = this.parseSelectDefaultValue(this._jsonConfig, "select_section");
+            this.defaultProperties["nodeType"] = SectionType[def];
         }
     }
 
@@ -41,7 +29,7 @@ export class FormulaireSection extends FormulaireFixedVar {
             // add new Section as first child, from given nodeType
             const propsSection = new Props();
             propsSection.setPropValue("calcType", CalculatorType.Section);
-            propsSection.setPropValue("nodeType", this._defaultSectionType);
+            propsSection.setPropValue("nodeType", this.defaultProperties["nodeType"]);
             propsSection.setPropValue(Prop_NullParameters, ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit);
             const section = Session.getInstance().createNub(propsSection);
             this.currentNub.setSection(section as acSection);
@@ -59,12 +47,7 @@ export class FormulaireSection extends FormulaireFixedVar {
             const newSect = Session.getInstance().createSection(data.value);
             (this._currentNub as SectionNub).setSection(newSect);
             // reflect changes in GUI
-            for (const fs of this.allFieldsets) {
-                // show / hide dependent fields
-                fs.updateFields();
-            }
-            // show / hide dependent fields
-            this.refreshFieldsets();
+            this.refreshFieldsets(true);
             this.reset();
         }
     }
diff --git a/src/app/formulaire/definition/form-solveur.ts b/src/app/formulaire/definition/form-solveur.ts
index f658c98eefb762eece2bae95a7180273a8f307ae..08318dd8c33b6cab048e2f9538f2f562eeb2cd61 100644
--- a/src/app/formulaire/definition/form-solveur.ts
+++ b/src/app/formulaire/definition/form-solveur.ts
@@ -1,9 +1,10 @@
-import { IObservable, ParamDefinition, Nub } from "jalhyd";
+import { IObservable, ParamDefinition, Nub, Props, Solveur, SolveurParams } from "jalhyd";
 
 import { NgParameter } from "../elements/ngparam";
 import { FormulaireFixedVar } from "./form-fixedvar";
 import { SelectField } from "../elements/select/select-field";
 import { FieldSet } from "../elements/fieldset";
+import { ServiceFactory } from "app/services/service-factory";
 
 /**
  * Formulaire pour les Solveurs
@@ -34,6 +35,16 @@ export class FormulaireSolveur extends FormulaireFixedVar {
         }
     }
 
+    public initNub(props?: Props) {
+        super.initNub(props);
+
+        // subscribe to Ytarget to be able to reset it during form construction (with empty fields option set)
+        // cf. nghyd#601
+
+        const n: Solveur = this._currentNub as Solveur;
+        n.prms.Ytarget.addObserver(this);
+    }
+
     // interface Observer
 
     public update(sender: IObservable, data: any) {
@@ -47,23 +58,32 @@ export class FormulaireSolveur extends FormulaireFixedVar {
             }
         }
         // copied from FormFixedVar, to avoid calling super.update()
-        if (data.action === "propertyChange") {
+        else if (data.action === "propertyChange") {
             this.reset();
+            if (data.name === "nubToCalculate") {
+                const n = (this.currentNub as Solveur).nubToCalculate;
+                if (n !== undefined) {
+                    const prms: SolveurParams = this.currentNub.prms as SolveurParams;
+                    // update Ytarget
+                    if (prms.Ytarget.singleValue === undefined && !ServiceFactory.applicationSetupService.enableEmptyFieldsOnFormInit) {
+                        prms.Ytarget.singleValue = n.calculatedParam.singleValue;
+                    }
+                }
+            }
         }
-
-        if (sender instanceof SelectField) {
+        else if (sender instanceof SelectField) {
             if (sender.id === "select_target_nub" && data.action === "select") {
                 // update Solveur property: Nub to calculate
                 try {
                     // if searchedParam is set to a value that won't be available anymore
                     // once nubToCalculate is updated, setPropValue throws an error, but
                     // nubToCalculate is updated anyway; here, just inhibit the error
-                    this._currentNub.properties.setPropValue("nubToCalculate", data.value.value);
+                    this._currentNub.setPropValue("nubToCalculate", data.value.value);
                 } catch (e) { }
                 // refresh targetted result selector
                 const trSel = this.getFormulaireNodeById(this._targettedResultSelectId) as SelectField;
                 if (trSel) {
-                    (trSel.parent as FieldSet).updateFields();
+                    (trSel.parent as FieldSet).updateFields(true);
                     // trick to re-set observers
                     this.completeParse(false);
                 }
@@ -73,7 +93,7 @@ export class FormulaireSolveur extends FormulaireFixedVar {
                 // update Solveur property: searched Parameter
                 try {
                     const p: ParamDefinition = data.value.value;
-                    this._currentNub.properties.setPropValue(
+                    this._currentNub.setPropValue(
                         "searchedParameter",
                         p.nubUid + "/" + p.symbol
                     );
@@ -81,9 +101,7 @@ export class FormulaireSolveur extends FormulaireFixedVar {
                 // reflect changes in GUI
                 const inputXinit = this.getFormulaireNodeById("Xinit") as NgParameter;
                 inputXinit.notifyValueModified(this);
-            }
-        } else if (sender instanceof SelectField) {
-            if (sender.id === this._targettedResultSelectId) {
+            } else if (sender.id === this._targettedResultSelectId) {
                 // refresh parameters selector
                 this.refreshParameterEntries();
             }
diff --git a/src/app/formulaire/definition/form-verificateur.ts b/src/app/formulaire/definition/form-verificateur.ts
index 893754665a7cb997437bf7ab7aaa26b6643c9695..337bc789a44d1b06666e4133196d80f3aeb2736e 100644
--- a/src/app/formulaire/definition/form-verificateur.ts
+++ b/src/app/formulaire/definition/form-verificateur.ts
@@ -97,7 +97,7 @@ export class FormulaireVerificateur extends FormulaireFixedVar {
             this.reset(); // reset results
             if (sender.id === "select_target_pass" && data.action === "select") {
                 // update Verificateur property: Pass to check
-                this._currentNub.properties.setPropValue("nubToVerify", data.value ? data.value.value : undefined);
+                this._currentNub.setPropValue("nubToVerify", data.value ? data.value.value : undefined);
 
             } else if (sender.id === "select_species_list" && data.action === "select") {
                 // update Verificateur property: Species list (string[])
diff --git a/src/app/formulaire/elements/fieldset.ts b/src/app/formulaire/elements/fieldset.ts
index d0229afe762326464684558389b299439dd7f7d0..73e6b8535049442f73d2a8ac0dd748d32a53feeb 100644
--- a/src/app/formulaire/elements/fieldset.ts
+++ b/src/app/formulaire/elements/fieldset.ts
@@ -1,22 +1,23 @@
 import {
     CalculatorType,
     ParamDefinition,
-    Props,
+    IProperties,
     Observer,
     Nub,
-    enumValueFromString
+    enumValueFromString,
+    ParamValueMode
 } from "jalhyd";
 
 import { FormulaireElement } from "./formulaire-element";
 import { Field } from "./field";
 import { SelectField } from "./select/select-field";
-import { NgParameter, ParamRadioConfig } from "./ngparam";
+import { NgParameter } from "./ngparam";
 import { FieldsetContainer } from "./fieldset-container";
 import { SelectFieldFactory } from "./select/select-field-factory";
 import { FormulaireFixedVar } from "../definition/form-fixedvar";
 import { SelectEntry } from "./select/select-entry";
 
-export class FieldSet extends FormulaireElement implements Observer {
+export class FieldSet extends FormulaireElement implements IProperties {
 
     /** Nub associé */
     private _nub: Nub;
@@ -38,17 +39,18 @@ export class FieldSet extends FormulaireElement implements Observer {
 
     public setNub(sn: Nub, update: boolean = true) {
         this._nub = sn;
-        this.setParentNubForAllFields();
         if (update) {
-            this.updateFields();
+            this.updateFields(true);
         }
     }
 
     private addField(f: Field) {
-        if (! f) {
+        if (!f) {
             throw new Error("FieldSet.addField() : argument incorrect (undefined)");
         }
-        this.kids.push(f);
+        if (this.kids.indexOf(f) === -1) {
+            this.kids.push(f);
+        }
     }
 
     /**
@@ -106,48 +108,34 @@ export class FieldSet extends FormulaireElement implements Observer {
         }
     }
 
-    private parse_select(json: {}): SelectField {
-        const res: SelectField = SelectFieldFactory.newSelectField(json, this);
-        res.parseConfig(json);
-        res.afterParseConfig();
-        res.addObserver(this);
+    private getOrCreateSelect(json: {}): SelectField {
+        const id: string = json["id"];
+        const res = this.getFormulaireNodeById(id);
+        if (res === undefined || !(res instanceof SelectField)) {
+            return SelectFieldFactory.newSelectField(json, this);
+        }
         return res;
     }
 
-    public get properties(): Props {
-        return this.nub.properties;
-    }
-
-    private get sectionProperties(): Props {
-        const section = this.nub.getChildren()[0];
-        if (section) {
-            return section.properties;
-        } else {
-            return new Props();
+    private parse_select(json: {}): SelectField {
+        let ok: boolean = true;
+        // in case select is associated to a property, check nub has the property
+        const p: string = json["property"];
+        if (p !== undefined) {
+            ok = this.parentForm.getPropValue(p) !== undefined;
         }
-    }
-
-    /**
-     * get associated nub property value
-     * @param key property name
-     * @param inSection if true, will look for the required property in the Nub's section (children[0])
-     */
-    private getNubPropValue(key: string, inSection: boolean = false): any {
-        if (inSection) {
-            return this.sectionProperties.getPropValue(key);
-        } else {
-            return this.properties.getPropValue(key);
+        if (ok) {
+            const res: SelectField = this.getOrCreateSelect(json);
+            res.parseConfig(json);
+            res.afterParseConfig();
+            res.addObserver(this);
+            return res;
         }
+        return undefined;
     }
 
-    /**
-     * assign associated nub property
-     * @param key nub property name
-     * @param val value to assign with
-     * @returns true if property value has changed
-     */
-    public setNubPropValue(key: string, val: any): boolean {
-        return this.properties.setPropValue(key, val, this);
+    public addPropertiesObserver(o: Observer) {
+        this.nub.addPropertiesObserver(o);
     }
 
     private getNubParamFromSymbol(symbol: string): ParamDefinition {
@@ -158,9 +146,6 @@ export class FieldSet extends FormulaireElement implements Observer {
         return this._jsonConfig;
     }
 
-    private setParentNubForAllFields() {
-    }
-
     /**
      * crée un input
      * @param json definition de l'input, extrait du fichier de conf du module de calcul
@@ -174,7 +159,7 @@ export class FieldSet extends FormulaireElement implements Observer {
         try {
             nubParam = this.getNubParamFromSymbol(input_id);
             if (nubParam.visible) {
-                res = new NgParameter(nubParam, this);
+                res = this.parentForm.getOrCreateParam(nubParam, this);
                 res.parseConfig(json);
             }
         } catch (e) {
@@ -184,8 +169,6 @@ export class FieldSet extends FormulaireElement implements Observer {
     }
 
     private parseFields() {
-        // clear everything so that parseFields() is idempotent
-        this.clearFields();
         // parse children fields from config
         const fields = this._jsonConfig["fields"];
         for (const field_index in fields) {
@@ -211,7 +194,9 @@ export class FieldSet extends FormulaireElement implements Observer {
 
                 case "select":
                     param = this.parse_select(field);
-                    this.addField(param);
+                    if (param !== undefined) {
+                        this.addField(param);
+                    }
                     break;
             }
         }
@@ -228,8 +213,35 @@ export class FieldSet extends FormulaireElement implements Observer {
     /**
      * Reloads the model values and properties, and reloads localisation strings
      */
-    public updateFields() {
-        this.parseFields();
+    public updateFields(forceClear: boolean) {
+        if (forceClear) {
+            this.clearFields();
+            this.parseFields();
+        }
+        else {
+            const nub = this.parentForm.currentNub;
+            for (const e of this.parentForm.allFormElements) {
+                if (e instanceof NgParameter) {
+                    const p: ParamDefinition = nub.getParameter(e.symbol);
+                    switch (e.paramDefinition.valueMode) {
+                        case ParamValueMode.SINGLE:
+                            e.setValue(this, p.singleValue);
+                            break;
+
+                        case ParamValueMode.MINMAX:
+                            e.setMinValue(this, p.min);
+                            e.setMaxValue(this, p.max);
+                            e.setStepValue(this, p.step);
+                            break;
+
+                        case ParamValueMode.LISTE:
+                            e.setValueList(this, p.valueList);
+                            break;
+                    }
+                }
+            }
+        }
+
         this.updateLocalisation();
 
         // for all select fields known by the form, set selected value
@@ -237,7 +249,7 @@ export class FieldSet extends FormulaireElement implements Observer {
         if (this.parentForm instanceof FormulaireFixedVar) {
             for (const sel of this.parentForm.allSelectFields) {
                 if (sel.hasAssociatedNubProperty) {  // ie. if select is a standard select
-                    this.setSelectValueFromProperty(sel.id, (this._confId === "fs_section"));
+                    this.setSelectValueFromProperty(sel.id);
                 }
             }
         }
@@ -245,12 +257,11 @@ export class FieldSet extends FormulaireElement implements Observer {
 
     /**
      * Reflects a property value in the interface, through the value of a <select> field, if this select exists
-     * @param inSection if true, will look for the required property in the Nub's section (children[0])
      */
-    private setSelectValueFromProperty(selectId: string, inSection: boolean = false) {
+    private setSelectValueFromProperty(selectId: string) {
         const selectField: SelectField = this.getFormulaireNodeById(selectId) as SelectField;
         if (selectField) {
-            let propVal: any = this.getNubPropValue(selectField.associatedProperty, inSection);
+            let propVal: any = this.getPropValue(selectField.associatedProperty);
             if (propVal === undefined) {
                 propVal = ""; // clodo bullet-proof loading
             }
@@ -277,12 +288,13 @@ export class FieldSet extends FormulaireElement implements Observer {
         this._helpLink = json["help"];
 
         const ct: string = json["calcType"];
-        const currentCt = this.properties.getPropValue("calcType");
+        const currentCt = this.getPropValue("calcType");
         const calc_type: CalculatorType = currentCt ? currentCt : (ct ? CalculatorType[ct] : this.parentForm.calculatorType);
-        this.setNubPropValue("calcType", calc_type);
+        this.setPropValue("calcType", calc_type);
 
         // parse fields once, so that SelectField elements are present
         // when setting default properties below
+        this.clearFields();
         this.parseFields();
 
         // for all select fields known by the form, apply default value
@@ -293,41 +305,15 @@ export class FieldSet extends FormulaireElement implements Observer {
                     const prop = sel.associatedProperty;
                     const defaultValue = sel.configDefaultValue;
                     // Sets Nub default property, unless this property is already set
-                    const currentValue = this.properties.getPropValue(prop);
+                    const currentValue = this.getPropValue(prop);
                     if (defaultValue !== undefined && currentValue === undefined) {
-                        this.setNubPropValue(prop, enumValueFromString(prop, defaultValue));
+                        this.setPropValue(prop, enumValueFromString(prop, defaultValue));
                     }
                 }
             }
         }
     }
 
-    public getNodeParameter(symbol: string): NgParameter {
-        for (const p of this.kids) {
-            if (p instanceof NgParameter) {
-                if (p.isDisplayed && p.symbol === symbol) {
-                    return p;
-                }
-            }
-        }
-    }
-
-    public getNodeParameterValue(symbol: string): number {
-        const p = this.getNodeParameter(symbol);
-        if (! p) {
-            throw new Error(`FieldSet.getNodeParameterValue() : pas de paramètre ${symbol} trouvé`);
-        }
-
-        switch (p.radioState) {
-            case ParamRadioConfig.FIX:
-                return p.getValue();
-
-            case ParamRadioConfig.VAR:
-            case ParamRadioConfig.CAL:
-                return undefined;
-        }
-    }
-
     /**
      * retourne la valeur actuellement sélectionnée d'un SelectField (qui doit être affiché)
      * @param selectFieldId id du SelectField
@@ -336,7 +322,7 @@ export class FieldSet extends FormulaireElement implements Observer {
     public getSelectedValue(selectFieldId: string): string | string[] {
         for (const p of this.kids) {
             if (p instanceof SelectField && p.isDisplayed && p.id === selectFieldId) {
-                if (! p.multiple) {
+                if (!p.multiple) {
                     const value: string = (p.getValue() as SelectEntry).value;
                     return FormulaireElement.removePrefix(value, selectFieldId + "_");
                 } else {
@@ -359,7 +345,7 @@ export class FieldSet extends FormulaireElement implements Observer {
                     if (senderId === "select_section") {
                         // sections paramétrées, courbes de remous, régimes uniformes
                         // "nodeType" is a property of the section child, not of the parent
-                        const oldNodeType = this.nub.getChildren()[0].properties.getPropValue("nodeType");
+                        const oldNodeType = this.getPropValue("nodeType");
                         if (oldNodeType !== data.value.value) { // avoid infinite loops
                             // manually notify parent so that it replaces the child Nub @WARNING clodo trick
                             this.parentForm.update(this, {
@@ -368,6 +354,19 @@ export class FieldSet extends FormulaireElement implements Observer {
                                 value: data.value.value
                             });
                         }
+                    }
+                    else if (senderId === "select_pressurelosstype") {
+                        // lois de perte de charge
+                        // "pressureLossType" is a property of the child law, not of the parent
+                        const oldLaw = this.nub.getPropValue("pressureLossType");
+                        if (oldLaw !== data.value.value) { // avoid infinite loops
+                            // manually notify parent so that it replaces the child Nub @WARNING clodo trick
+                            this.parentForm.update(this, {
+                                action: "propertyChange",
+                                name: "pressureLossType",
+                                value: data.value.value
+                            });
+                        }
                     } else {
                         if (data.value !== undefined) {
                             if (this.parentForm instanceof FormulaireFixedVar) {
@@ -380,18 +379,50 @@ export class FieldSet extends FormulaireElement implements Observer {
                                             const prop = sel.associatedProperty;
                                             // for multiple select
                                             if (Array.isArray(data.value)) {
-                                                this.setNubPropValue(prop, data.value.map((v: any) => v.value));
+                                                this.setPropValue(prop, data.value.map((v: any) => v.value));
                                             } else {
-                                                this.setNubPropValue(prop, data.value.value);
+                                                this.setPropValue(prop, data.value.value);
                                             }
                                         }
                                     }
                                 }
                             }
+                            this.parentForm.resetResults();
                         }
                     }
                     break; // switch (data.action)
             }
         }
     }
+
+    // interface IProperties
+
+    public hasProperty(key: string): boolean {
+        return this._nub.hasProperty(key);
+    }
+
+    /**
+     * list of properties keys
+     */
+    public get keys(): string[] {
+        return this._nub.keys;
+    }
+
+    /**
+     * get associated nub property value
+     * @param key property name
+     */
+    public getPropValue(key: string): any {
+        return this.nub.getPropValue(key);
+    }
+
+    /**
+     * assign associated nub property
+     * @param key nub property name
+     * @param val value to assign with
+     * @returns true if property value has changed
+     */
+    public setPropValue(key: string, val: any): boolean {
+        return this.nub.setPropValue(key, val, this);
+    }
 }
diff --git a/src/app/formulaire/elements/formulaire-element.ts b/src/app/formulaire/elements/formulaire-element.ts
index 579b62648b34a662d8edba307d86920a98ff32d3..25b5943fa846c1790b151d0aa5d01cfec1aae4da 100644
--- a/src/app/formulaire/elements/formulaire-element.ts
+++ b/src/app/formulaire/elements/formulaire-element.ts
@@ -56,6 +56,10 @@ export abstract class FormulaireElement extends FormulaireNode {
      */
     public get parentForm(): FormulaireDefinition {
         let res = this.parent;
+        if (res === undefined) {
+            return undefined;
+        }
+
         // while (!(res instanceof FormulaireDefinition))
         while (!("calculatorName" in res)) {
             // pour éviter de faire référence au type FormulaireDefinition, supprimer l'import correspondant et
diff --git a/src/app/formulaire/elements/ngparam.ts b/src/app/formulaire/elements/ngparam.ts
index e4dc902cd4034702c761634d145c30781db189b2..a579f014146807fe4e091f7a7dd6b1d0050faa18 100644
--- a/src/app/formulaire/elements/ngparam.ts
+++ b/src/app/formulaire/elements/ngparam.ts
@@ -8,7 +8,7 @@ import { sprintf } from "sprintf-js";
 import { InputField } from "./input-field";
 import { ServiceFactory } from "../../services/service-factory";
 import { FormulaireNode } from "./formulaire-node";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 
 export enum ParamRadioConfig {
     /** pas de radio, paramètre modifiable à la main uniquement */
diff --git a/src/app/formulaire/elements/select/select-field-charttype.ts b/src/app/formulaire/elements/select/select-field-charttype.ts
index b6f35d3f932c8026a4dee3cc6297e81b1b14adf5..223dcaf39252a0cac32f7d3cf7ca9e52150047ba 100644
--- a/src/app/formulaire/elements/select/select-field-charttype.ts
+++ b/src/app/formulaire/elements/select/select-field-charttype.ts
@@ -20,7 +20,7 @@ export class SelectFieldChartType extends SelectField {
     protected populate() {
         for (const v of this._entryValues) {
             const id: string = ChartType[v];
-            this.addEntry(new SelectEntry(id, v));
+            this.addEntry(this.createOrGetEntry(id, v));
         }
     }
 
@@ -50,4 +50,4 @@ export class SelectFieldChartType extends SelectField {
 
         this._label = this.intlService.localizeText("INFO_PARAMFIELD_CHART_TYPE");
     }
-}
\ No newline at end of file
+}
diff --git a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
index 2c556ff0a54de8e207e0d6e077e53f3f198d9039..683adebaefa22e1b2a35176cdb90bc19889f8377 100644
--- a/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
+++ b/src/app/formulaire/elements/select/select-field-device-loi-debit.ts
@@ -34,7 +34,7 @@ export class SelectFieldDeviceLoiDebit extends SelectField {
      */
     private get loiDebit(): LoiDebit {
         const child = this.nub.getChildren()[this.parent.indexAsKid()];
-        return child.properties.getPropValue("loiDebit");
+        return child.getPropValue("loiDebit");
     }
 
     protected populate() {
@@ -47,7 +47,7 @@ export class SelectFieldDeviceLoiDebit extends SelectField {
         const stName = StructureType[stCode];
         if (la[stName] !== undefined) {
             for (const ld of la[stName]) {
-                const e: SelectEntry = new SelectEntry(this._entriesBaseId + LoiDebit[ld], ld);
+                const e: SelectEntry = this.createOrGetEntry(this._entriesBaseId + LoiDebit[ld], ld);
                 this.addEntry(e);
             }
         }
diff --git a/src/app/formulaire/elements/select/select-field-device-structure-type.ts b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
index aee9297606d7f0d756521e19b4d6c36ceecd8e85..0d1ce351d60caaf53c572e8f91f7bef415917c0d 100644
--- a/src/app/formulaire/elements/select/select-field-device-structure-type.ts
+++ b/src/app/formulaire/elements/select/select-field-device-structure-type.ts
@@ -21,15 +21,23 @@ export class SelectFieldDeviceStructureType extends SelectField {
         this._configDefaultValue = json["default"];
     }
 
+    /**
+    * get discharge law linked to this select's nub
+    */
+    private get structureType(): StructureType {
+        const child = this.nub.getChildren()[this.parent.indexAsKid()];
+        return child.getPropValue("structureType");
+    }
+
     protected populate() {
         // possible values depend on CalcType
         for (const st in (this.nub as ParallelStructure).getLoisAdmissibles()) {
-            const e: SelectEntry = new SelectEntry(this._entriesBaseId + st, StructureType[st]);
+            const e: SelectEntry = this.createOrGetEntry(this._entriesBaseId + st, StructureType[st]);
             this.addEntry(e);
         }
     }
 
     protected initSelectedValue() {
-        this.findAndSetDefaultValue();
+        this.findAndSetDefaultValue(StructureType[this.structureType]);
     }
 }
diff --git a/src/app/formulaire/elements/select/select-field-downstream-basin.ts b/src/app/formulaire/elements/select/select-field-downstream-basin.ts
index dd39ce85724ea830962b6abfa61d7564db7c72a5..9c34a16e8c8b147328ea5cd0430b0c514b485782 100644
--- a/src/app/formulaire/elements/select/select-field-downstream-basin.ts
+++ b/src/app/formulaire/elements/select/select-field-downstream-basin.ts
@@ -19,13 +19,13 @@ export class SelectFieldDownstreamBasin extends SelectField {
         for (const b of preBarrageD.bassins) {
             const pos = b.findPositionInParent();
             if (posUb === undefined || pos > posUb) {
-                const e = new SelectEntry(this._entriesBaseId + b.uid, b.uid);
+                const e = this.createOrGetEntry(this._entriesBaseId + b.uid, b.uid);
                 e.intlInfo = b.description;
                 this.addEntry(e);
             }
         }
         // river downstream
-        const e = new SelectEntry(this._entriesBaseId + "none", undefined);
+        const e = this.createOrGetEntry(this._entriesBaseId + "none", undefined);
         e.intlInfo = "INFO_LIB_AVAL";
         this.addEntry(e);
     }
diff --git a/src/app/formulaire/elements/select/select-field-factory.ts b/src/app/formulaire/elements/select/select-field-factory.ts
index 49fed0c91265f84c008fff570c024693adf85591..afb4246e01c12c991552a742e86f31ca9495954c 100644
--- a/src/app/formulaire/elements/select/select-field-factory.ts
+++ b/src/app/formulaire/elements/select/select-field-factory.ts
@@ -69,10 +69,11 @@ export class SelectFieldFactory {
             case "select_section":
             case "select_sppoperation":
             case "select_unit":
+            case "select_pressurelosstype":
                 return new SelectFieldNubProperty(parent);
 
             default:
-                throw new Error("unknown select id ${id}");
+                throw new Error(`unknown select id ${json["id"]}`);
         }
     }
 }
diff --git a/src/app/formulaire/elements/select/select-field-nub-prop.ts b/src/app/formulaire/elements/select/select-field-nub-prop.ts
index df62610a8a9c288d571618c59d954eec395d7546..233502db06a61c1133285507ce23046cf615f396 100644
--- a/src/app/formulaire/elements/select/select-field-nub-prop.ts
+++ b/src/app/formulaire/elements/select/select-field-nub-prop.ts
@@ -1,6 +1,5 @@
-import { Session } from "jalhyd";
+import { Props } from "jalhyd";
 import { FormulaireNode } from "../formulaire-node";
-import { SelectEntry } from "./select-entry";
 import { SelectField } from "./select-field";
 
 /*
@@ -27,11 +26,11 @@ export class SelectFieldNubProperty extends SelectField {
 
     protected populate() {
         // find enum associated to property
-        const enumClass = Session.enumFromProperty[this._associatedProperty];
+        const enumClass = Props.enumFromProperty[this._associatedProperty];
         if (enumClass !== undefined) {
             // add one select entry per enum entry, in the enum order
             for (let j = 0; j < Object.keys(enumClass).length / 2; j++) {
-                this.addEntry(new SelectEntry(this._entriesBaseId + j, j));
+                this.addEntry(this.createOrGetEntry(this._entriesBaseId + j, j));
             }
         }
     }
diff --git a/src/app/formulaire/elements/select/select-field-remous-target.ts b/src/app/formulaire/elements/select/select-field-remous-target.ts
index 20b40cc473bbe7395462953dd359638a1f9e217a..f6eb90e6ff1917981c2885e5afd618e235a357e6 100644
--- a/src/app/formulaire/elements/select/select-field-remous-target.ts
+++ b/src/app/formulaire/elements/select/select-field-remous-target.ts
@@ -19,9 +19,9 @@ export class SelectFieldRemousTarget extends SelectField {
 
     protected populate() {
         // driven by string[], not enum (easier for variable names)
-        this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
+        this.addEntry(this.createOrGetEntry(this._entriesBaseId + "none", ""));
         for (const at of CourbeRemous.availableTargets) {
-            const e: SelectEntry = new SelectEntry(this._entriesBaseId + at, at);
+            const e: SelectEntry = this.createOrGetEntry(this._entriesBaseId + at, at);
             this.addEntry(e);
         }
     }
diff --git a/src/app/formulaire/elements/select/select-field-searched-param.ts b/src/app/formulaire/elements/select/select-field-searched-param.ts
index 1342fb643e31b378ea799dc166f3277714d9b123..8811ef81f441628e39164f0f46404fa8cf1a1d4c 100644
--- a/src/app/formulaire/elements/select/select-field-searched-param.ts
+++ b/src/app/formulaire/elements/select/select-field-searched-param.ts
@@ -1,5 +1,5 @@
 import { ServiceFactory } from "app/services/service-factory";
-import { decodeHtml } from "app/util";
+import { decodeHtml } from "app/util/util";
 import { acSection, Nub, Solveur } from "jalhyd";
 import { SelectEntry } from "./select-entry";
 import { SelectField } from "./select-field";
@@ -28,7 +28,7 @@ export class SelectFieldSearchedParam extends SelectField {
                 const calc = fs.getFormulaireFromId(p.originNub.uid).calculatorName;
                 const varName = fs.expandVariableName(p.originNub.calcType, p.symbol);
                 const label = `${p.symbol} - ${varName} (${calc})`;
-                this.addEntry(new SelectEntry(this._entriesBaseId + p.getParentComputeNode(false).uid + "_" + p.symbol, p, decodeHtml(label)));
+                this.addEntry(this.createOrGetEntry(this._entriesBaseId + p.getParentComputeNode(false).uid + "_" + p.symbol, p, decodeHtml(label)));
             }
         }
     }
diff --git a/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts b/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
index a547bde3c6e1036f697281d8449d6b0df74b2fcf..36094a8dddfc808aea5ce6c8031de91bed17c1c9 100644
--- a/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
+++ b/src/app/formulaire/elements/select/select-field-solver-targeted-result.ts
@@ -24,12 +24,12 @@ export class SelectFieldSolverTargetedResult extends SelectField {
         // 1. calculated param
         const ntc = (this.nub as Solveur).nubToCalculate;
         if (ntc?.calculatedParam !== undefined) { // some nubs have no calculatedParam, for ex. SectionParam
-            this.addEntry(new SelectEntry(this._entriesBaseId + "none", ""));
+            this.addEntry(this.createOrGetEntry(this._entriesBaseId + "none", ""));
         }
         // 2. extra results
         if (ntc?.resultsFamilies !== undefined) {
             for (const er of Object.keys(ntc.resultsFamilies)) {
-                const e: SelectEntry = new SelectEntry(this._entriesBaseId + er, er);
+                const e: SelectEntry = this.createOrGetEntry(this._entriesBaseId + er, er);
                 this.addEntry(e);
             }
         }
diff --git a/src/app/formulaire/elements/select/select-field-solveur-target.ts b/src/app/formulaire/elements/select/select-field-solveur-target.ts
index aecd238b8d87d8836879bedf48ea707e21894e40..3d752aa4368a5c45c5b0c4a4d36375ff8cb0d8b2 100644
--- a/src/app/formulaire/elements/select/select-field-solveur-target.ts
+++ b/src/app/formulaire/elements/select/select-field-solveur-target.ts
@@ -5,7 +5,7 @@
 */
 
 import { ServiceFactory } from "app/services/service-factory";
-import { decodeHtml } from "../../../util";
+import { decodeHtml } from "../../../util/util";
 import { Session, Solveur } from "jalhyd";
 import { SelectEntry } from "./select-entry";
 import { SelectField } from "./select-field";
@@ -33,7 +33,7 @@ export class SelectFieldSolverTarget extends SelectField {
                     const varName = fs.expandVariableName(cn.calcType, cn.calculatedParam.symbol);
                     label += ` / ${varName} (${cn.calculatedParam.symbol})`;
                 }
-                this.addEntry(new SelectEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
+                this.addEntry(this.createOrGetEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
             } else {
                 // silent fail, this Solveur nub was probably loaded before all the candidate nubs are done loading
             }
diff --git a/src/app/formulaire/elements/select/select-field-species-list.ts b/src/app/formulaire/elements/select/select-field-species-list.ts
index 9401148c35ace8289cb150c5b2c8691f9dfc681e..7b94345aa073bbd77580775e01dfc5504d78fb64 100644
--- a/src/app/formulaire/elements/select/select-field-species-list.ts
+++ b/src/app/formulaire/elements/select/select-field-species-list.ts
@@ -24,14 +24,14 @@ export class SelectFieldSpeciesList extends SelectField {
         const especeNubs = Session.getInstance().getAllNubs().filter((element) => element.calcType === CalculatorType.Espece);
         for (const en of especeNubs) {
             const form = ServiceFactory.formulaireService.getFormulaireFromNubId(en.uid);
-            const e = new SelectEntry(this._entriesBaseId + en.uid, en.uid);
+            const e = this.createOrGetEntry(this._entriesBaseId + en.uid, en.uid);
             e.intlInfo = { "isnub": true, "code": "INFO_VERIFICATEUR_CUSTOM_SPECIES", "id": en.uid };
             this.addEntry(e);
         }
         // add all FishSpecies
         for (let j = 1; j < Object.keys(FishSpecies).length / 2; j++) { // exclude "0" (SPECIES_CUSTOM)
             const spgId = FishSpecies[j].substring(FishSpecies[j].lastIndexOf("_") + 1);
-            const e = new SelectEntry(this._entriesBaseId + spgId, FishSpecies[j]);
+            const e = this.createOrGetEntry(this._entriesBaseId + spgId, FishSpecies[j]);
             e.intlInfo = { "isnub": false, "code": FishSpecies[j] }
             this.addEntry(e);
         }
diff --git a/src/app/formulaire/elements/select/select-field-target-pass.ts b/src/app/formulaire/elements/select/select-field-target-pass.ts
index 10cd639a1b125fec2a7af633bfeb4cbde056f07d..8438e0a0fe2c50a5b5b1c8b937543249def5a423 100644
--- a/src/app/formulaire/elements/select/select-field-target-pass.ts
+++ b/src/app/formulaire/elements/select/select-field-target-pass.ts
@@ -1,5 +1,5 @@
 import { ServiceFactory } from "app/services/service-factory";
-import { decodeHtml } from "app/util";
+import { decodeHtml } from "app/util/util";
 import { CalculatorType, FishPass, Session, Verificateur } from "jalhyd";
 import { FormulaireElement } from "../formulaire-element";
 import { FormulaireNode } from "../formulaire-node";
@@ -33,7 +33,7 @@ export class SelectFieldTargetPass extends SelectField {
             const nub = fs.getFormulaireFromId(cn.uid);
             if (nub) {
                 const label = nub.calculatorName + " (" + fs.getLocalisedTitleFromCalculatorType(nub.calculatorType) + ")";
-                this.addEntry(new SelectEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
+                this.addEntry(this.createOrGetEntry(this._entriesBaseId + cn.uid, cn.uid, decodeHtml(label)));
             } else {
                 // silent fail, this Verificateur nub was probably loaded before all the candidate nubs are done loading
             }
diff --git a/src/app/formulaire/elements/select/select-field-upstream-basin.ts b/src/app/formulaire/elements/select/select-field-upstream-basin.ts
index d240d097c39d795647ab3cda6a4f0a78cac18b14..cc70f8a53860ce6c5d140c695c4121fd37d635e6 100644
--- a/src/app/formulaire/elements/select/select-field-upstream-basin.ts
+++ b/src/app/formulaire/elements/select/select-field-upstream-basin.ts
@@ -17,7 +17,7 @@ export class SelectFieldUpstreamBasin extends SelectField {
         const posDb = pbWallU.bassinAval?.findPositionInParent();
 
         // river upstream
-        const e = new SelectEntry(this._entriesBaseId + "none", undefined);
+        const e = this.createOrGetEntry(this._entriesBaseId + "none", undefined);
         e.intlInfo = "INFO_LIB_AMONT";
         this.addEntry(e);
 
@@ -25,7 +25,7 @@ export class SelectFieldUpstreamBasin extends SelectField {
         for (const b of preBarrageU.bassins) {
             const pos = b.findPositionInParent();
             if (posDb === undefined || pos < posDb) {
-                const e = new SelectEntry(this._entriesBaseId + b.uid, b.uid);
+                const e = this.createOrGetEntry(this._entriesBaseId + b.uid, b.uid);
                 e.intlInfo = b.description;
                 this.addEntry(e);
             }
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index 5e490c754d4246759a7ffd9e820b50be973188cd..b45f59c42d2581d536dc40659295cb86ed57cc62 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -1,6 +1,6 @@
 import { Field } from "../field";
 import { SelectEntry } from "./select-entry";
-import { arraysAreEqual } from "../../../util";
+import { arraysAreEqual } from "../../../util/util";
 import { FormulaireNode } from "../formulaire-node";
 import { ServiceFactory } from "app/services/service-factory";
 import { FormulaireDefinition } from "../../definition/form-definition";
@@ -36,6 +36,11 @@ export abstract class SelectField extends Field {
      */
     protected _messageWhenEmpty: string;
 
+    /**
+     * map id <-> select entry
+     */
+    protected _entryMap: { [key: string]: SelectEntry } = {};
+
     constructor(parent: FormulaireNode) {
         super(parent);
         this.clearEntries();
@@ -45,6 +50,10 @@ export abstract class SelectField extends Field {
      * associated nub
      */
     protected get nub(): Nub {
+        const parent = this.parentForm;
+        if (parent === undefined) {
+            return undefined;
+        }
         return (this.parentForm as FormulaireDefinition).currentNub;
     }
 
@@ -123,22 +132,39 @@ export abstract class SelectField extends Field {
     }
 
     /**
-     * try to find a default value to select
+     * Try to find a default value to select.
+     * Priority order :
+     *   - passed parameter
+     *   - nub value for associated property
+     *   - default value from configuration file
+     *   - first select entry
      */
     protected findAndSetDefaultValue(value?: string) {
         // default to first available entry if any
         if (this._entries.length > 0) {
             let val: SelectEntry;
+
             if (value !== undefined) {
                 val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, value));
-            } else if (this._configDefaultValue === undefined) {
-                val = this._entries[0];
+                if (val === undefined) {
+                    throw Error("invalid select default value " + value + " for " + this._associatedProperty + " property");
+                }
             } else {
-                val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, this._configDefaultValue));
+                if (this.nub !== undefined) {
+                    val = this.getEntryFromValue(this.nub.getPropValue(this._associatedProperty));
+                    // nub may not have "this._associatedProperty" as a property
+                }
+                if (val === undefined && this._configDefaultValue !== undefined) {
+                    val = this.getEntryFromValue(enumValueFromString(this._associatedProperty, this._configDefaultValue));
+                    if (val === undefined) {
+                        throw Error("invalid select default value " + this._configDefaultValue + " for " + this._associatedProperty + " property");
+                    }
+                }
                 if (val === undefined) {
-                    throw Error("invalid select default value " + this._configDefaultValue + " for " + this._associatedProperty + " property");
+                    val = this._entries[0];
                 }
             }
+
             if (this._multiple) {
                 this.setValue([val]);
             } else {
@@ -210,6 +236,15 @@ export abstract class SelectField extends Field {
         this._entries.push(e);
     }
 
+    protected createOrGetEntry(id: string, val: any, lbl?: string): SelectEntry {
+        let res: SelectEntry = this._entryMap[id];
+        if (res === undefined) {
+            res = new SelectEntry(id, val, lbl);
+            this._entryMap[id] = res;
+        }
+        return res;
+    }
+
     public getEntryFromValue(val: any): SelectEntry {
         for (const se of this._entries) {
             if (se.value === val) {
diff --git a/src/app/results/param-calc-results.ts b/src/app/results/param-calc-results.ts
index eaca087d32dee78357630328c0c72508f7b00e44..9e22d98b4928284d5b72a4712e233bb551649a7c 100644
--- a/src/app/results/param-calc-results.ts
+++ b/src/app/results/param-calc-results.ts
@@ -12,7 +12,7 @@ export abstract class CalculatedParamResults extends CalculatorResults {
     protected _calculatedParam: NgParameter;
 
     /** titre de la colonne du paramètre calculé */
-    public calculatedParameterHeader: string;
+    private _calculatedParameterHeader: string;
 
     /** résultat du calcul sur le paramètre calculé */
     public result: Result;
@@ -22,7 +22,7 @@ export abstract class CalculatedParamResults extends CalculatorResults {
 
     public reset() {
         this._calculatedParam = undefined;
-        this.calculatedParameterHeader = undefined;
+        this._calculatedParameterHeader = undefined;
         this.result = undefined;
     }
 
@@ -32,7 +32,17 @@ export abstract class CalculatedParamResults extends CalculatorResults {
 
     public set calculatedParameter(p: NgParameter) {
         this._calculatedParam = p;
-        this.calculatedParameterHeader = CalculatorResults.paramLabel(this._calculatedParam, true);
+        this.updateCalculatedParameterHeader();
+    }
+
+    public updateCalculatedParameterHeader() {
+        if (this._calculatedParam !== undefined) {
+            this._calculatedParameterHeader = CalculatorResults.paramLabel(this._calculatedParam, true);
+        }
+    }
+
+    public get calculatedParameterHeader(): string {
+        return this._calculatedParameterHeader;
     }
 
     public get hasResults(): boolean {
diff --git a/src/app/results/var-results.ts b/src/app/results/var-results.ts
index 37ff757b971936d696f0b040060cc6b54f2688f7..c8c814d89776af9760d03c5e524297446330f92a 100644
--- a/src/app/results/var-results.ts
+++ b/src/app/results/var-results.ts
@@ -2,7 +2,7 @@ import { CalculatedParamResults } from "./param-calc-results";
 import { ServiceFactory } from "../services/service-factory";
 import { PlottableData } from "./plottable-data";
 import { ChartType } from "./chart-type";
-import { longestVarParam } from "../util";
+import { longestVarParam } from "../util/util";
 import { FormulaireDefinition } from "../formulaire/definition/form-definition";
 
 import { sprintf } from "sprintf-js";
diff --git a/src/app/services/formulaire.service.ts b/src/app/services/formulaire.service.ts
index 5c29f442d1a79d93e9f4b143b76a2491492d7b70..a32a1aec446448eb849d26fcddc3c5d77b81497f 100644
--- a/src/app/services/formulaire.service.ts
+++ b/src/app/services/formulaire.service.ts
@@ -57,6 +57,8 @@ import { FormulaireVerificateur } from "../formulaire/definition/form-verificate
 import { FormulaireEspece } from "../formulaire/definition/form-espece";
 import { FormulairePrebarrage } from "../formulaire/definition/form-prebarrage";
 import { ServiceFactory } from "./service-factory";
+import { FormulairePressureLoss } from "app/formulaire/definition/form-pressureloss";
+import { getNubResultUnit } from "jalhyd";
 
 @Injectable()
 export class FormulaireService extends Observable {
@@ -193,7 +195,7 @@ export class FormulaireService extends Observable {
      * Returns variable name and unit from symbol
      * @param calcType
      * @param symbol
-     * @param forceUnit if given, will be used as nuit
+     * @param forceUnit if given, will be used as unit
      */
     public expandVariableNameAndUnit(calcType: CalculatorType, symbol: string, forceUnit?: string): string {
         let s = this.expandVariableName(calcType, symbol);
@@ -216,16 +218,8 @@ export class FormulaireService extends Observable {
         if (forceUnit) {
             unit = forceUnit;
         } else {
-            // create dummy Nub from CalcType just to get results units
-            let dummyNub: Nub;
-            try {
-                dummyNub = Session.getInstance().createNub(new Props({ "calcType": calcType }));
-            } catch (e) {
-                // silent fail
-            }
-            if (dummyNub?.resultsUnits && dummyNub.resultsUnits[symbol]) {
-                unit = dummyNub.resultsUnits[symbol];
-            } else {
+            unit = getNubResultUnit(calcType, symbol);
+            if (unit === undefined) {
                 // last chance: if unit cannot be read in model, use translation files
                 const unitKey = "UNIT_" + symbolBase;
                 if (langCache && langCache[unitKey] !== undefined) {
@@ -346,6 +340,10 @@ export class FormulaireService extends Observable {
                 f = new FormulairePrebarrage();
                 break;
 
+            case CalculatorType.PressureLoss:
+                f = new FormulairePressureLoss();
+                break;
+
             default:
                 f = new FormulaireFixedVar();
         }
@@ -373,7 +371,9 @@ export class FormulaireService extends Observable {
         if (nub) {
             f.currentNub = nub;
         } else {
-            f.initNub();
+            const confProps = f.parseConfigToProps();
+            confProps.setPropValue("calcType", ct);
+            f.initNub(confProps);
         }
 
         // Restaure le nom du module, sinon affecte le nom par défaut
diff --git a/src/app/services/internationalisation.service.ts b/src/app/services/internationalisation.service.ts
index af0f2d42e05abe4ee0c9d29a5c8fe528a7822baa..deee494fecb63836be93eeb253b8253ed708742f 100644
--- a/src/app/services/internationalisation.service.ts
+++ b/src/app/services/internationalisation.service.ts
@@ -5,7 +5,7 @@ import { Message, MessageCode, Observable, Observer, Nub, CalculatorType, PreBar
 import { StringMap } from "../stringmap";
 import { ApplicationSetupService } from "./app-setup.service";
 import { HttpService } from "./http.service";
-import { fv, decodeHtml } from "../util";
+import { fv, decodeHtml } from "../util/util";
 import { ServiceFactory } from "./service-factory";
 import { FormulaireService } from "./formulaire.service";
 
@@ -62,7 +62,8 @@ export class I18nService extends Observable implements Observer {
     public async setLanguage(code: string) {
         /** excluded calculators */
         const childCalculatorType: CalculatorType[] = [
-            CalculatorType.Section, CalculatorType.Structure, CalculatorType.CloisonAval, CalculatorType.YAXN
+            CalculatorType.Section, CalculatorType.Structure, CalculatorType.CloisonAval, CalculatorType.YAXN,
+            CalculatorType.LechaptCalmon, CalculatorType.PressureLossLaw
         ];
 
         // ensure 2-letter language code
diff --git a/src/app/services/service-factory.ts b/src/app/services/service-factory.ts
index e3ea55cae3c7ce504924688dac8ae1517f3268ba..668d5aea18b12ed1295f44b2885fdec97a1eeb56 100644
--- a/src/app/services/service-factory.ts
+++ b/src/app/services/service-factory.ts
@@ -4,6 +4,7 @@ import { I18nService } from "./internationalisation.service";
 import { HttpService } from "./http.service";
 import { NotificationsService } from "./notifications.service";
 import { PrebarrageService } from "./prebarrage.service";
+import { ServiceWorkerUpdateService } from "./service-worker-update.service";
 
 /**
  * A "Singleton" the TS way, that holds pointers to all services, to be accessed
@@ -17,11 +18,13 @@ export const ServiceFactory: {
     httpService: HttpService;
     notificationsService: NotificationsService;
     prebarrageService: PrebarrageService;
+    serviceWorkerUpdateService: ServiceWorkerUpdateService;
 } = {
     applicationSetupService: undefined,
     formulaireService: undefined,
     i18nService: undefined,
     httpService: undefined,
     notificationsService: undefined,
-    prebarrageService: undefined
+    prebarrageService: undefined,
+    serviceWorkerUpdateService: undefined
 };
diff --git a/src/app/services/service-worker-update.service.ts b/src/app/services/service-worker-update.service.ts
index 96b84426ac85dc1c4666d7d6b55ecc7e4aa46f29..163fd4a12e45f941abec56a389479a73c68b291c 100644
--- a/src/app/services/service-worker-update.service.ts
+++ b/src/app/services/service-worker-update.service.ts
@@ -1,36 +1,74 @@
-import { Injectable } from "@angular/core";
+import { Injectable, NgZone } from "@angular/core";
 import { SwUpdate } from '@angular/service-worker';
 import { I18nService } from "./internationalisation.service";
 import { NotificationsService } from "./notifications.service";
+import { UserConfirmationService } from "./user-confirmation.service";
+import { interval } from "rxjs";
 
 @Injectable()
 export class ServiceWorkerUpdateService {
     constructor(
         private swUpdate: SwUpdate,
         private notificationService: NotificationsService,
-        private i18nService: I18nService
+        private i18nService: I18nService,
+        private userConfirmationService: UserConfirmationService,
+        private ngZone: NgZone
     ) {
-        swUpdate.versionUpdates.subscribe(evt => {
+        if (this.swUpdate.isEnabled) {
+            this.ngZone.runOutsideAngular(() =>
+                interval(1000 * 60 * 60).subscribe(val => {
+                    console.log('ServiceWorkerUpdateService: checking for updates...')
+                    swUpdate.checkForUpdate().then(b => {
+                        console.log("ServiceWorkerUpdateService: " + (b ? "new version found" : "no new version"));
+                    });
+                })
+            );
+        } else {
+            console.log("ServiceWorkerUpdateService: SwUpdate is disabled");
+        }
+
+        this.swUpdate.versionUpdates.subscribe(evt => {
             switch (evt.type) {
                 case 'VERSION_DETECTED':
-                    let ver = evt.version.appData["version"];
-                    let msg = i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_DETECTED", { "ver": ver });
-                    notificationService.notify(msg, 10000);
+                    let ver = (evt as any).version?.appData?.version ?? "<NA>";
+                    console.log("ServiceWorkerUpdateService: VERSION_DETECTED", ver);
+                    notificationService.notify(i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_DETECTED", { "ver": ver }), 10000);
                     break;
 
                 case 'VERSION_READY':
-                    const newVer = evt.latestVersion.appData["version"];
-                    // const currVer = evt.currentVersion.appData["version"];
-                    msg = i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer });
-                    notificationService.notify(msg, 10000);
+                    const currVer = (evt as any).currentVersion?.appData?.version ?? "<NA>";
+                    const newVer = (evt as any).latestVersion?.appData?.version ?? "<NA>";
+                    console.log("ServiceWorkerUpdateService: VERSION_READY", currVer, "->", newVer);
+
+                    notificationService.notify(i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer }), 10000);
+                    // PLANTE si on stocke le message dans une variable !!!!
+                    // const msg = i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer });
+                    // notificationService.notify(msg, 10000);
+                    // -> ReferenceError: can't access lexical declaration 'xxx' before initialization
+                    // avec xxx qui varie d'une fois à l'autre !!!
+
+                    userConfirmationService.askUserConfirmation("Confirmation",
+                        i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer })).then(data => {
+                            if (data["confirm"]) {
+                                console.log("ServiceWorkerUpdateService: application update confirmed");
+                                window.location.reload();
+                            }
+                            else {
+                                console.log("ServiceWorkerUpdateService: application update canceled");
+                            }
+                        });
                     break;
 
                 case 'VERSION_INSTALLATION_FAILED':
-                    ver = evt.version.appData["version"];
-                    msg = i18nService.localizeText("ERROR_SERVICE_WORKER_INSTALL_FAILED", { "ver": ver });
-                    notificationService.notify(msg, 10000);
+                    ver = (evt as any).version?.appData?.version ?? "NA";
+                    console.log("ServiceWorkerUpdateService: VERSION_INSTALLATION_FAILED", ver);
+                    notificationService.notify(i18nService.localizeText("ERROR_SERVICE_WORKER_INSTALL_FAILED", { "ver": ver }), 10000);
                     break;
             }
         });
+        swUpdate.unrecoverable.subscribe(event => {
+            console.log("SwUpdate.unrecoverable reason", event.reason, "type", event.type);
+            notificationService.notify("SwUpdate: unrecoverable state. Reason=" + event.reason + ", type=" + event.type, 10000);
+        });
     }
 }
diff --git a/src/app/services/user-confirmation.service.ts b/src/app/services/user-confirmation.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d33beecbbcb3516620ad36170922baeeca54a8b3
--- /dev/null
+++ b/src/app/services/user-confirmation.service.ts
@@ -0,0 +1,69 @@
+import { Injectable } from "@angular/core";
+
+import { BidirectionalSubject } from "app/util/bidir_subject";
+import { Observer } from "rxjs";
+
+/**
+ * This service enables any class (even another service) to display a confirmation dialog on GUI ans get the user answer
+ */
+@Injectable()
+export class UserConfirmationService {
+
+    // used to communicate with UI component in charge of displaying confirmation dialog
+    // direction 0 : this
+    // direction 1 : UI
+    private _userConfirm: BidirectionalSubject<{}>;
+
+    public constructor() {
+        this._userConfirm = new BidirectionalSubject<{}>();
+
+        // we choose communication canal 0, UI will use 1
+        this._userConfirm.selectPostingChannel(this, 0);
+    }
+
+    /**
+     * add subscription from UI
+     * @param source 
+     */
+    public subscribe(source: any) {
+        this._userConfirm.selectPostingChannel(source, 1);
+    }
+
+    /**
+     * remove UI subscription
+     * @param source 
+     */
+    public unsubscribe(source: any) {
+        this._userConfirm.unselectPostingChannel(source);
+    }
+
+    /**
+     * add a handler (provided bu UI) to confirmation request
+     * @param source normally, UI component
+     * @param obs processing function
+     */
+    public addHandler(source: any, obs: Observer<boolean>) {
+        this._userConfirm.addHandler(source, obs)
+    }
+
+    /**
+     * forward user confirmation from UI to requesting object
+     * @param confirm user confirmation status
+     */
+    public postConfirmation(source: any, confirm: {}) {
+        this._userConfirm.post(source, confirm);
+    }
+
+    /**
+     * forward to UI a request from source to ask a user confirmation with a dialog
+     * @param source object requesting confirmation
+     * @param title confirmation dialog title
+     * @param text confirmation dialog body text
+     * @returns a Promise resolving to a boolean holding user confirmation status
+     */
+    public askUserConfirmation(title: string, text: string): Promise<{}> {
+        const ret = this._userConfirm.getReceivePromise(this);
+        this._userConfirm.post(this, { title: title, body: text }); // false or true, we don't care
+        return ret;
+    }
+}
diff --git a/src/app/util/bidir_subject.ts b/src/app/util/bidir_subject.ts
new file mode 100644
index 0000000000000000000000000000000000000000..069e004e83e7040dff2b273fad42ba9aef3ddd0c
--- /dev/null
+++ b/src/app/util/bidir_subject.ts
@@ -0,0 +1,131 @@
+import { Observer, Subject, firstValueFrom, lastValueFrom } from "rxjs";
+
+/**
+ * bi-directional subject (see RxJS Subject)
+ * Allows two objects to exchange messages in both directions. Each object has to choose a posting channel
+ * (messages will receive from the other one).
+ *
+ *    source1 ----post-----> | channel 0 | --subscribe--> source2
+ *            <--subscribe-- | channel 1 | <-----post----
+ * 
+ * EventEmitter is not used since it is reserved to properties in Angular component with @Output annotation
+ */
+export class BidirectionalSubject<T> {
+
+    // communication channels
+    private _channel0: Subject<T>;
+    private _channel1: Subject<T>;
+
+    // array of "who chose which posting channel"
+    private _channel0Posters: any[] = [];
+    private _channel1Posters: any[] = [];
+
+    constructor() {
+        this._channel0 = new Subject();
+        this._channel1 = new Subject();
+    }
+
+    /**
+     * get posting channel index
+     * @param source object that chose one of the channels
+     */
+    private getPostingChannelIndex(source: any) {
+        if (this._channel0Posters.indexOf(source) !== -1) {
+            return 0;
+        }
+        if (this._channel1Posters.indexOf(source) !== -1) {
+            return 1;
+        }
+        return -1;
+    }
+
+    /**
+     * choose a posting channel
+     * @param source object that chooses the channel
+     * @param chan channel number
+     */
+    public selectPostingChannel(source: any, chan: number) {
+        switch (chan) {
+            case 0:
+                if (this.getPostingChannelIndex(source) !== -1) {
+                    throw new Error("object already has a selected channel");
+                }
+                this._channel0Posters.push(source);
+                break;
+
+            case 1:
+                if (this.getPostingChannelIndex(source) !== -1) {
+                    throw new Error("object already has a selected channel");
+                }
+                this._channel1Posters.push(source);
+                break;
+
+            default:
+                throw new Error(`invalid channel number ${chan}`);
+        }
+    }
+
+    /**
+     * remove a source from its channel
+     */
+    public unselectPostingChannel(source: any) {
+        this._channel0Posters = this._channel0Posters.filter(o => o != source);
+        this._channel1Posters = this._channel1Posters.filter(o => o != source);
+    }
+
+    /**
+     * used by a source to post a message to communication channel
+     */
+    public post(source: any, msg: T) {
+        switch (this.getPostingChannelIndex(source)) {
+            case 0:
+                this._channel0.next(msg);
+                break;
+
+            case 1:
+                this._channel1.next(msg);
+                break;
+
+            case -1:
+                throw new Error("must select a channel first");
+        }
+    }
+
+    /**
+     * create a Promise representing a received message (when posted by another source)
+     * @param source object that will use the Promise
+     */
+    public getReceivePromise(source: any): Promise<T> {
+        switch (this.getPostingChannelIndex(source)) {
+            case 0:
+                return firstValueFrom(this._channel1);
+
+            case 1:
+                return firstValueFrom(this._channel0);
+
+            case -1:
+                throw new Error("must select a channel first");
+        }
+    }
+
+    /**
+     * Add a message handler (provided by source) to process received messages
+     * (alternative to getReceivePromise())
+     * @param source object providing handler
+     * @param handler message processing function
+     */
+    public addHandler(source: any, handler: Observer<T>) {
+        switch (this.getPostingChannelIndex(source)) {
+            case 0:
+                this._channel1.subscribe(handler);
+                break;
+
+            case 1:
+                this._channel0.subscribe(handler);
+                break;
+
+            case -1:
+                throw new Error("must select a channel first");
+        }
+    }
+}
diff --git a/src/app/definedvalue/definedboolean.ts b/src/app/util/definedvalue/definedboolean.ts
similarity index 100%
rename from src/app/definedvalue/definedboolean.ts
rename to src/app/util/definedvalue/definedboolean.ts
diff --git a/src/app/definedvalue/definedvalue.ts b/src/app/util/definedvalue/definedvalue.ts
similarity index 100%
rename from src/app/definedvalue/definedvalue.ts
rename to src/app/util/definedvalue/definedvalue.ts
diff --git a/src/app/util.ts b/src/app/util/util.ts
similarity index 97%
rename from src/app/util.ts
rename to src/app/util/util.ts
index 763a425cbebe0a5b95596c0ae707666bfea8df12..06db46788e2d96870913a5efd1faf8c49db487dd 100644
--- a/src/app/util.ts
+++ b/src/app/util/util.ts
@@ -1,5 +1,5 @@
-import { NgParameter } from "./formulaire/elements/ngparam";
-import { ServiceFactory } from "./services/service-factory";
+import { NgParameter } from "../formulaire/elements/ngparam";
+import { ServiceFactory } from "../services/service-factory";
 
 import { formattedValue, Nub, VariatedDetails, ParamDefinition, ParamValueMode, Result } from "jalhyd";
 
@@ -13,7 +13,7 @@ export function logObject(obj: {}, m?: string) {
 }
 
 export function isNumber(s: string): boolean {
-    return Number(s) !== NaN;
+    return !Number.isNaN(Number(s));
 }
 
 /**
diff --git a/src/index.html b/src/index.html
index 5a2324c1d7d449aa55ec7ce3dada7485ed3784c2..a1fbb0362dce2328d9bdf2db9ba4f1c31398e3c6 100644
--- a/src/index.html
+++ b/src/index.html
@@ -14,8 +14,8 @@
    <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#ffffff">
 
    <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="manifest" href="manifest.webmanifest">
-  <meta name="theme-color" content="#1976d2">
+   <link rel="manifest" href="manifest.webmanifest">
+   <meta name="theme-color" content="#1976d2">
 </head>
 
 <body>
@@ -163,4 +163,4 @@
   <noscript>Please enable JavaScript to continue using this application.</noscript>
 </body>
 
-</html>
\ No newline at end of file
+</html>
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index 6ac8ce559ee4a307c510e6ebdab1ed70a5fb7068..b7b836bd09864cbe81f26ff922b9d154c1a98a70 100755
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -233,6 +233,8 @@
     "INFO_CHILD_TYPE_SECTION": "section",
     "INFO_CHILD_TYPE_SECTION_PLUR": "sections",
     "INFO_CHILD_TYPE_SECTION_SHORT": "S",
+    "INFO_CHILD_TYPE_LECHAPTCALMON": "Lechapt-Calmon",
+    "INFO_CHILD_TYPE_STRICKLER": "Strickler",
     "INFO_DIALOG_PARSIM_DESC": "Choose a combination of values to generate the simulation",
     "INFO_FIELDSET_ADD": "Add",
     "INFO_FIELDSET_COPY": "Copy",
@@ -261,6 +263,9 @@
     "INFO_LECHAPTCALMON_DESCRIPTION": "pipe flow circular headloss",
     "INFO_LECHAPTCALMON_TITRE_COURT": "Lechapt-C.",
     "INFO_LECHAPTCALMON_TITRE": "Lechapt-Calmon",
+    "INFO_PRESSURELOSS_TITRE_DESCRIPTION": "Headloss in a pipe flow",
+    "INFO_PRESSURELOSS_TITRE_COURT": "Press. loss",
+    "INFO_PRESSURELOSS_TITRE": "Pressure loss",
     "INFO_LIB_ABSCISSE": "Abscissa (m)",
     "INFO_LIB_ALTITUDE": "Altitude (m)",
     "INFO_LIB_LENGTHS": "Every length",
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index 24135323659bf1aafcebc9ba8881f9ffe3e84e60..1b2892173b6f6383f61f07fac4766fbc1288b0b5 100755
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -233,6 +233,8 @@
     "INFO_CHILD_TYPE_SECTION": "section",
     "INFO_CHILD_TYPE_SECTION_PLUR": "sections",
     "INFO_CHILD_TYPE_SECTION_SHORT": "S",
+    "INFO_CHILD_TYPE_LECHAPTCALMON": "Lechapt-Calmon",
+    "INFO_CHILD_TYPE_STRICKLER": "Strickler",
     "INFO_DIALOG_PARSIM_DESC": "Choisir une combinaison de valeurs pour générer la simulation",
     "INFO_FIELDSET_ADD": "Ajouter",
     "INFO_FIELDSET_COPY": "Copier",
@@ -261,6 +263,9 @@
     "INFO_LECHAPTCALMON_DESCRIPTION": "Hydraulique en charge conduite colebrook",
     "INFO_LECHAPTCALMON_TITRE_COURT": "Lechapt-C.",
     "INFO_LECHAPTCALMON_TITRE": "Lechapt-Calmon",
+    "INFO_PRESSURELOSS_TITRE_DESCRIPTION": "Pertes dans une conduite en charge",
+    "INFO_PRESSURELOSS_TITRE_COURT": "Perte de ch.",
+    "INFO_PRESSURELOSS_TITRE": "Perte de charge",
     "INFO_LIB_ABSCISSE": "Abscisse (m)",
     "INFO_LIB_ALTITUDE": "Altitude (m)",
     "INFO_LIB_LENGTHS": "Toutes les longueurs",
@@ -769,4 +774,4 @@
     "ERROR_VERIF_PAB_WALL_NOT_CROSSABLE": "La cloison n°%N% n'est pas franchissable",
     "ERROR_VERIF_PAB_DW_NOT_CROSSABLE": "La cloison aval n'est pas franchissable",
     "WARNING_VERIF_PAR_SPECIES_GROUP": "Les groupes d'espèces 3a, 3b et 7b sont déconseillés pour ce type de passe"
-}
\ No newline at end of file
+}
diff --git a/src/main.ts b/src/main.ts
index 066830176864449a04f0ba173614afdd06bde81e..28da08b5bff46969e78ef8737919a9ee7b67fc3e 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -11,5 +11,9 @@ if (environment.production) {
   enableProdMode();
 }
 
-platformBrowserDynamic().bootstrapModule(AppModule)
-  .catch(err => console.log(err));
+platformBrowserDynamic().bootstrapModule(AppModule).then(() => {
+  if ('serviceWorker' in navigator && environment.production) {
+    console.log("Registering ngsw-worker.js...");
+    navigator.serviceWorker.register('ngsw-worker.js');
+  }
+}).catch(err => console.log(err));
diff --git a/src/manifest.webmanifest b/src/manifest.webmanifest
index 4d0eb567531bbbf0bd9842b500a40f5598aa3ce1..996eabf17c23c3cdc6e967fa128322766dc6ff70 100644
--- a/src/manifest.webmanifest
+++ b/src/manifest.webmanifest
@@ -11,49 +11,49 @@
       "src": "assets/icons/favicon-72x72.png",
       "sizes": "72x72",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     },
     {
       "src": "assets/icons/favicon-96x96.png",
       "sizes": "96x96",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     },
     {
       "src": "assets/icons/favicon-128x128.png",
       "sizes": "128x128",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     },
     {
       "src": "assets/icons/favicon-144x144.png",
       "sizes": "144x144",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     },
     {
       "src": "assets/icons/favicon-152x152.png",
       "sizes": "152x152",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     },
     {
       "src": "assets/icons/android-chrome-192x192.png",
       "sizes": "192x192",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     },
     {
       "src": "assets/icons/favicon-384x384.png",
       "sizes": "384x384",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     },
     {
       "src": "assets/icons/android-chrome-512x512.png",
       "sizes": "512x512",
       "type": "image/png",
-      "purpose": "maskable any"
+      "purpose": "any"
     }
   ]
 }