diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index dd8178d486ebd843149fe4a82ea639f659495178..61a4ab6ffc2839ead886b7c591bfe3dfb34cf098 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -122,7 +122,7 @@ test-pep8:
     - pip3 install -r ./requirements.txt
     - pip3 install -U -r ./requirements.txt
     - pip3 install pycodestyle
-    - pycodestyle ./src
+    - pycodestyle --exclude="*_to_*.py" ./src
   allow_failure: true
 
 #########
diff --git a/src/Model/Friction/Friction.py b/src/Model/Friction/Friction.py
index db1a60db772c318dd57fdfdf3fe8ab8b5c9ecaac..d3be3cf5980379be21df9ac4201d955279f4919c 100644
--- a/src/Model/Friction/Friction.py
+++ b/src/Model/Friction/Friction.py
@@ -16,10 +16,14 @@
 
 # -*- coding: utf-8 -*-
 
+import logging
+
 from tools import trace, timer
 
 from Model.Tools.PamhyrDB import SQLSubModel
 
+logger = logging.getLogger()
+
 
 class Friction(SQLSubModel):
     def __init__(self, name: str = "", status=None):
@@ -60,7 +64,8 @@ class Friction(SQLSubModel):
     @classmethod
     def _db_load(cls, execute, data=None):
         new = []
-        reach = data["parent"]  # Reach object
+
+        reach = data["reach"]
         status = data["status"]
         stricklers = data["stricklers"].stricklers
 
@@ -69,9 +74,6 @@ class Friction(SQLSubModel):
             f"FROM friction WHERE reach = {reach.id}"
         )
 
-        for _ in table:
-            new.append(None)
-
         for row in table:
             ind = row[0]
             # Get stricklers
@@ -86,7 +88,9 @@ class Friction(SQLSubModel):
             sec.begin_strickler = bs
             sec.end_strickler = es
 
-            yield ind, sec
+            new.append((ind, sec))
+
+        return new
 
     def _db_save(self, execute, data=None):
         ind = data["ind"]
@@ -116,6 +120,10 @@ class Friction(SQLSubModel):
     def edge(self):
         return self._edge
 
+    @property
+    def reach(self):
+        return self._edge
+
     @edge.setter
     def edge(self, edge):
         self._edge = edge
diff --git a/src/Model/Friction/FrictionList.py b/src/Model/Friction/FrictionList.py
index 20c3319764400f516972ad33f4349ebb578b1040..6a5a6c091b63cce40896c52987b9d8f21c1a7c6c 100644
--- a/src/Model/Friction/FrictionList.py
+++ b/src/Model/Friction/FrictionList.py
@@ -50,10 +50,12 @@ class FrictionList(PamhyrModelList):
     def _db_load(cls, execute, data=None):
         new = cls(status=data['status'])
 
-        new._lst = Friction._db_load(
+        ilst = Friction._db_load(
             execute, data
         )
 
+        new._lst = list(map(lambda x: x[1], sorted(ilst)))
+
         return new
 
     def _db_save(self, execute, data=None):
diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py
index 06a6ccf61ecd97e8f888363e5514282c432a3c7f..57c13a373d66dfd29a5a110a91de162e4f9a5b0d 100644
--- a/src/Model/Geometry/PointXYZ.py
+++ b/src/Model/Geometry/PointXYZ.py
@@ -116,7 +116,7 @@ class PointXYZ(Point, SQLSubModel):
         sl = self._sl.id if self._sl is not None else -1
 
         sql = (
-            "INSERT OR REPLACE INTO " +
+            "INSERT INTO " +
             "geometry_pointXYZ(ind, name, x, y, z, profile, sl) " +
             "VALUES (" +
             f"{ind}, '{self._db_format(self._name)}', " +
diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py
index ee662308f33def332297ed0d450d552d74b30669..a04e1a8f15b5b540fa113892e8d08360552435e6 100644
--- a/src/Model/Geometry/Profile.py
+++ b/src/Model/Geometry/Profile.py
@@ -214,31 +214,17 @@ class Profile(object):
         self.points.insert(index, point)
         self._status.modified()
 
-    def delete(self, indexes: int):
-        """Delete points at index
-
-        Args:
-            indexes: List of index of points.
-
-        Returns:
-            Nothing.
-        """
-        points = set(
+    def delete_i(self, indexes: list):
+        self._points = list(
             map(
                 lambda e: e[1],
                 filter(
-                    lambda e: e[0] in indexes,
+                    lambda e: e[0] not in indexes,
                     enumerate(self.points)
                 )
             )
         )
 
-        self.points = list(
-            filter(
-                lambda p: p not in points,
-                self.points
-            )
-        )
         self._status.modified()
 
     def delete_points(self, points):
@@ -250,7 +236,7 @@ class Profile(object):
         Returns:
             Nothing.
         """
-        self.points = list(
+        self._points = list(
             filter(
                 lambda p: p not in points,
                 self.points
@@ -286,7 +272,7 @@ class Profile(object):
         elif column == 'z':
             def predicate(p): return p.z
 
-        self.points = sorted(
+        self._points = sorted(
             self.points,
             key=predicate,
             reverse=is_reversed
@@ -298,7 +284,7 @@ class Profile(object):
         if len(self.points) != len(indexes):
             logger.critical("Indexes list do not correspond to point list")
 
-        self.points = list(
+        self._points = list(
             map(
                 lambda x: x[1],
                 sorted(
diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py
index ad3ebb5ea61e9a23b8c8b2c75efe3bdc52f12599..384d930e40537afc0abd4ca3b69949cfd8d8bdfb 100644
--- a/src/Model/Geometry/ProfileXYZ.py
+++ b/src/Model/Geometry/ProfileXYZ.py
@@ -115,9 +115,6 @@ class ProfileXYZ(Profile, SQLSubModel):
             f"WHERE reach = {reach.id}"
         )
 
-        for _ in table:
-            profiles.append(None)
-
         for row in table:
             id = row[0]
             ind = row[1]
@@ -132,7 +129,7 @@ class ProfileXYZ(Profile, SQLSubModel):
                 id=id, num=num,
                 name=name, kp=kp,
                 code1=code1, code2=code2,
-                reach=data["parent"],
+                reach=reach,
                 status=status
             )
 
@@ -151,10 +148,6 @@ class ProfileXYZ(Profile, SQLSubModel):
 
             yield ind, new
 
-        #     profiles[ind] = new
-
-        # return profiles
-
     def _db_save(self, execute, data=None):
         ok = True
         ind = data["ind"]
@@ -191,11 +184,20 @@ class ProfileXYZ(Profile, SQLSubModel):
         profile = None
         try:
             if len(header) == 0:
+                name = data[0]
+                kp = data[1]
+                reach = data[2]
+                status = data[3]
+
                 profile = cls(
-                    *data
+                    id=-1,
+                    name=name,
+                    kp=kp,
+                    reach=reach,
+                    status=status
                 )
             else:
-                valid_header = {'name', 'reach', 'kp'}
+                valid_header = {'name', 'reach', 'kp', 'status'}
                 d = {}
                 for i, v in enumerate(data):
                     h = header[i].strip().lower().split(' ')[0]
@@ -204,6 +206,7 @@ class ProfileXYZ(Profile, SQLSubModel):
 
                 profile = cls(**d)
         except Exception as e:
+            logger.error(e)
             raise ClipboardFormatError(header, data)
 
         return profile
@@ -444,42 +447,57 @@ class ProfileXYZ(Profile, SQLSubModel):
         return abs(rg.dist(rd))
 
     def get_water_limits(self, z):
-#==============================================================================
-#   détermination des points limites RG et RD pour un niveau d'eau donné
-#
-#   irg et ird sont les premiers indices en partant des rives gauche et
-#   droite qui correspondent à des points sous la surface de l'eau
-#   ptX et ptY sont les points interpolés où le plan d'eau intersecte le profil
-#   known_level est le niveau d'eau pour lequel on a obtenu irg, ird, ptX et ptY
-#==============================================================================
+        # ====================================================================
+        #  détermination des points limites RG et RD pour un niveau
+        #  d'eau donné
+        #
+        #  irg et ird sont les premiers indices en partant des rives
+        #  gauche et droite qui correspondent à des points sous la
+        #  surface de l'eau ptX et ptY sont les points interpolés où
+        #  le plan d'eau intersecte le profil known_level est le
+        #  niveau d'eau pour lequel on a obtenu irg, ird, ptX et ptY
+        # ====================================================================
+
         # initialisation
-        irg = -1  ;  ird = -1
+        irg = -1
+        ird = -1
+
         for i in range(self.number_points):
             if self.point(i).z <= z:
                 irg = i
+
         for i in reversed(range(self.number_points)):
             if self.point(i).z <= z:
                 ird = i
+
         # interpolation des points ptX et ptY
-        if (irg < self.number_points-1):
-            x=np.interp(z,
-                        [self.point(irg).z,self.point(irg+1).z],
-                        [self.point(irg).x,self.point(irg+1).x])
-            y=np.interp(z,
-                        [self.point(irg).z,self.point(irg+1).z],
-                        [self.point(irg).y,self.point(irg+1).y])
-            ptX=PointXYZ(x,y,z)
+        if (irg < self.number_points - 1):
+            x = np.interp(
+                z,
+                [self.point(irg).z, self.point(irg + 1).z],
+                [self.point(irg).x, self.point(irg + 1).x]
+            )
+            y = np.interp(
+                z,
+                [self.point(irg).z, self.point(irg + 1).z],
+                [self.point(irg).y, self.point(irg + 1).y]
+            )
+            ptX = PointXYZ(x, y, z)
         else:
             ptX = self.point(0)
         if (ird > 0):
-            x=np.interp(z,
-                        [self.point(ird-1).z,self.point(ird).z],
-                        [self.point(ird-1).x,self.point(ird).x])
-            y=np.interp(z,
-                        [self.point(ird).z,self.point(ird-1).z],
-                        [self.point(ird).y,self.point(ird-1).y])
-            ptY=PointXYZ(x,y,z)
+            x = np.interp(
+                z,
+                [self.point(ird-1).z, self.point(ird).z],
+                [self.point(ird-1).x, self.point(ird).x]
+            )
+            y = np.interp(
+                z,
+                [self.point(ird).z, self.point(ird - 1).z],
+                [self.point(ird).y, self.point(ird - 1).y]
+            )
+            ptY = PointXYZ(x, y, z)
         else:
-            ptY = self.point(self.number_points-1)
+            ptY = self.point(self.number_points - 1)
 
-        return ptX,ptY
+        return ptX, ptY
diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py
index 5351a9cbbabeb1dd7cf9a62012ccb8542b779168..3c02f16a68541a78b4fd79dfdd09d2c5aeb3f63b 100644
--- a/src/Model/Geometry/Reach.py
+++ b/src/Model/Geometry/Reach.py
@@ -61,7 +61,7 @@ class Reach(SQLSubModel):
 
     @classmethod
     def _db_load(cls, execute, data=None):
-        new = cls(status=data["status"], parent=data["parent"])
+        new = cls(status=data["status"], parent=data["reach"])
 
         new._profiles = ProfileXYZ._db_load(
             execute,
diff --git a/src/Model/HydraulicStructures/Basic/HydraulicStructures.py b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py
new file mode 100644
index 0000000000000000000000000000000000000000..e7a5af5fa7c383624009fbd421a33675beea86a7
--- /dev/null
+++ b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py
@@ -0,0 +1,191 @@
+# HydraulicStructures.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import trace, timer
+
+from Model.Tools.PamhyrDB import SQLSubModel
+from Model.Except import NotImplementedMethodeError
+
+from Model.HydraulicStructures.Basic.Value import (
+    BHSValue
+)
+
+logger = logging.getLogger()
+
+
+class BasicHS(SQLSubModel):
+    _sub_classes = [
+        BHSValue,
+    ]
+    _id_cnt = 0
+
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(BasicHS, self).__init__()
+
+        self._status = status
+
+        if id == -1:
+            self.id = BasicHS._id_cnt
+        else:
+            self.id = id
+
+        self._name = name
+        self._type = ""
+        self._enabled = True
+        self._data = []
+
+        BasicHS._id_cnt = max(BasicHS._id_cnt + 1, self.id)
+
+    @classmethod
+    def _db_create(cls, execute):
+        execute("""
+          CREATE TABLE hydraulic_structures_basic(
+            id INTEGER NOT NULL PRIMARY KEY,
+            name TEXT NOT NULL,
+            type TEXT NOT NULL,
+            enabled BOOLEAN NOT NULL,
+            hs INTEGER,
+            FOREIGN KEY(hs) REFERENCES hydraulic_structures(id)
+          )
+        """)
+
+        return cls._create_submodel(execute)
+
+    @classmethod
+    def _db_update(cls, execute, version):
+        major, minor, release = version.strip().split(".")
+        if major == minor == "0":
+            if int(release) < 6:
+                cls._db_create(execute)
+
+        return True
+
+    @classmethod
+    def _get_ctor_from_type(cls, t):
+        from Model.HydraulicStructures.Basic.Types import (
+            BHS_types, NotDefined,
+        )
+
+        res = NotDefined
+
+        if t in BHS_types.keys():
+            res = BHS_types[t]
+
+        return res
+
+    @classmethod
+    def _db_load(cls, execute, data=None):
+        new = []
+
+        table = execute(
+            "SELECT id, name, type, enabled, hs " +
+            "FROM hydraulic_structures_basic " +
+            f"WHERE hs = {data['hs_id']} "
+        )
+
+        for row in table:
+            bhs_id = row[0]
+            name = row[1]
+            type = row[2]
+            enabled = (row[3] == 1)
+            hs_id = row[4]
+
+            ctor = cls._get_ctor_from_type(type)
+            bhs = ctor(
+                id=bhs_id,
+                name=name,
+                status=data['status']
+            )
+
+            bhs.enabled = enabled
+
+            data['bhs_id'] = bhs_id
+            bhs._data = BHSValue._db_load(
+                execute, data
+            )
+
+            new.append(bhs)
+
+        return new
+
+    def _db_save(self, execute, data=None):
+        hs_id = data['hs_id']
+
+        sql = (
+            "INSERT INTO " +
+            "hydraulic_structures_basic(id, name, type, enabled, hs) " +
+            "VALUES (" +
+            f"{self.id}, " +
+            f"'{self._db_format(self._name)}', " +
+            f"'{self._db_format(self._type)}', " +
+            f"{self._db_format(self.enabled)}, " +
+            f"{hs_id} " +
+            ")"
+        )
+        execute(sql)
+
+        data['bhs_id'] = self.id
+        execute(
+            "DELETE FROM hydraulic_structures_basic_value " +
+            f"WHERE bhs = {self.id}"
+        )
+
+        for values in self._data:
+            values._db_save(execute, data)
+
+        return True
+
+    def __len__(self):
+        return len(self._data)
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, name):
+        self._name = name
+        self._status.modified()
+
+    @property
+    def type(self):
+        return self._type
+
+    @type.setter
+    def type(self, type):
+        self._type = type
+        self._status.modified()
+
+    @property
+    def enabled(self):
+        return self._enabled
+
+    @enabled.setter
+    def enabled(self, enabled):
+        self._enabled = enabled
+        self._status.modified()
+
+    @property
+    def parameters(self):
+        return self._data
+
+    def convert(self, new_type):
+        return new_type(id=self.id, name=self.name, status=self._status)
diff --git a/src/Model/HydraulicStructures/Basic/Types.py b/src/Model/HydraulicStructures/Basic/Types.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d1d0930b9159485e5a0976cc5c326e14ba36791
--- /dev/null
+++ b/src/Model/HydraulicStructures/Basic/Types.py
@@ -0,0 +1,228 @@
+# Types.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from Model.HydraulicStructures.Basic.HydraulicStructures import (
+    BasicHS
+)
+
+from Model.HydraulicStructures.Basic.Value import (
+    BHSValue
+)
+
+
+class NotDefined(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(NotDefined, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "ND"
+        self._data = []
+
+
+class SeuilDeversoir(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(SeuilDeversoir, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "S1"
+        self._data = [
+            BHSValue("Largeur", float, 1.0, status=status),
+            BHSValue("Cote", float, 1.0, status=status),
+            BHSValue("Coefficient de debit", float, 0.4, status=status),
+        ]
+
+
+class SeuilTrapezoidal(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(SeuilTrapezoidal, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "S2"
+        self._data = [
+            BHSValue("Largeur", float, 1.0, status=status),
+            BHSValue("Cote", float, 1.0, status=status),
+            BHSValue("Cote de mise en charge", float, 9999.999, status=status),
+            BHSValue("Coefficient de debit", float, 0.4, status=status),
+            BHSValue("Tangeante du demi angle", float, 0.0, status=status),
+        ]
+
+
+class SeuilTriangulaire(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(SeuilTriangulaire, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "S3"
+        self._data = [
+            BHSValue("Cote", float, 1.0, status=status),
+            BHSValue("Cote de mise en charge", float, 9999.999, status=status),
+            BHSValue("Coefficient de debit", float, 0.4, status=status),
+            BHSValue("Tangeante du demi angle", float, 0.0, status=status),
+        ]
+
+
+class OrificeRectangulaire(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(OrificeRectangulaire, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "OR"
+        self._data = [
+            BHSValue("Largeur", float, 0.0, status=status),
+            BHSValue("Cote", float, 0.0, status=status),
+            BHSValue("Cote de mise en charge", float, 9999.999,
+                     status=status),
+            BHSValue("Coefficient de debit", float, 0.4, status=status),
+            BHSValue("Cote de mise en charge maximale", float, 9999.999,
+                     status=status),
+        ]
+
+
+class OrificeCirculaire(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(OrificeCirculaire, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "OC"
+        self._data = [
+            BHSValue("Diametre", float, 0.0, status=status),
+            BHSValue("Cote", float, 0.0, status=status),
+            BHSValue("hauteur envasement", float, 0.0, status=status),
+            BHSValue("Coefficient de debit", float, 0.4, status=status),
+        ]
+
+
+# class OrificeVoute(BasicHS):
+#     def __init__(self, id: int = -1, name: str = "",
+#                  status=None):
+#         super(OrificeVoute, self).__init__(
+#             id=id, name=name,
+#             status=status
+#         )
+
+#         self._type = "OV"
+#         self._data = [
+#             BHSValue("Cote", float, 1.0, status=status),
+#             BHSValue("Largeur", float, 1.0, status=status),
+#             BHSValue("Haut de la voute", float, 0.0, status=status),
+#             BHSValue("Bas de la voute", float, 0.0, status=status),
+#             BHSValue("Coefficient de debit", float, 0.4, status=status),
+#         ]
+
+
+class VanneRectangulaire(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(VanneRectangulaire, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "V1"
+        self._data = [
+            BHSValue("Largeur", float, 1.0, status=status),
+            BHSValue("Cote", float, 0.0, status=status),
+            BHSValue("Coefficient de debit", float, 0.4, status=status),
+            BHSValue("Ouverture", float, 1.0, status=status),
+            BHSValue("Ouverture maximale", float, 1.0, status=status),
+        ]
+
+
+class VanneRectangulaireSimplifiee(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(VanneRectangulaireSimplifiee, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "V2"
+        self._data = [
+            BHSValue("Largeur", float, 1.0, status=status),
+            BHSValue("Cote", float, 0.0, status=status),
+            BHSValue("Coefficient de debit", float, 0.4, status=status),
+            BHSValue("Ouverture", float, 1.0, status=status),
+            BHSValue("Ouverture maximale", float, 1.0, status=status),
+        ]
+
+
+class Borda(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(Borda, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "BO"
+        self._data = [
+            BHSValue("Pas espace", float, 0.1, status=status),
+            BHSValue("Seuil", float, 0.15, status=status),
+            BHSValue("Coefficient", float, 0.4, status=status),
+        ]
+
+
+class UserDefined(BasicHS):
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(UserDefined, self).__init__(
+            id=id, name=name,
+            status=status
+        )
+
+        self._type = "UD"
+        self._data = [
+            BHSValue("Parameter 1", float, 0.0, status=status),
+            BHSValue("Parameter 2", float, 0.0, status=status),
+            BHSValue("Parameter 3", float, 0.0, status=status),
+            BHSValue("Parameter 4", float, 0.0, status=status),
+            BHSValue("Parameter 5", float, 0.0, status=status),
+        ]
+
+
+BHS_types = {
+    "ND": NotDefined,
+    "S1": SeuilDeversoir,
+    "S2": SeuilTrapezoidal,
+    "S3": SeuilTriangulaire,
+    "OR": OrificeRectangulaire,
+    "OC": OrificeCirculaire,
+    # "OV": OrificeVoute,
+    "V1": VanneRectangulaire,
+    "V2": VanneRectangulaireSimplifiee,
+    "BO": Borda,
+    "UD": UserDefined
+}
diff --git a/src/Model/HydraulicStructures/Basic/Value.py b/src/Model/HydraulicStructures/Basic/Value.py
new file mode 100644
index 0000000000000000000000000000000000000000..ebf4744eda3199dbe89b1e4baa993d528a9e9fab
--- /dev/null
+++ b/src/Model/HydraulicStructures/Basic/Value.py
@@ -0,0 +1,145 @@
+# Value.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from Model.Tools.PamhyrDB import SQLSubModel
+
+
+class BHSValue(SQLSubModel):
+    _sub_classes = []
+    _id_cnt = 0
+
+    def __init__(self, name: str = "", type=float, value=0.0,
+                 status=None):
+        super(BHSValue, self).__init__()
+
+        self._status = status
+
+        self._name = name
+        self._type = type
+        self._value = type(value)
+
+    @classmethod
+    def _db_create(cls, execute):
+        execute("""
+          CREATE TABLE hydraulic_structures_basic_value(
+            id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+            name TEXT NOT NULL,
+            type TEXT NOT NULL,
+            value TEXT NOT NULL,
+            bhs INTEGER,
+            FOREIGN KEY(bhs) REFERENCES hydraulic_structures_basic(id)
+          )
+        """)
+
+        return cls._create_submodel(execute)
+
+    @classmethod
+    def _db_update(cls, execute, version):
+        major, minor, release = version.strip().split(".")
+        if major == minor == "0":
+            if int(release) < 6:
+                cls._db_create(execute)
+
+        return True
+
+    @classmethod
+    def _str_to_type(cls, type):
+        res = str
+
+        if type == "float":
+            res = float
+        elif type == "int":
+            res = int
+        elif type == "bool":
+            res = bool
+
+        return res
+
+    @classmethod
+    def _type_to_str(cls, type):
+        res = "str"
+
+        if type == float:
+            res = "float"
+        elif type == int:
+            res = "int"
+        elif type == bool:
+            res = "bool"
+
+        return res
+
+    @classmethod
+    def _db_load(cls, execute, data=None):
+        new = []
+        bhs_id = data["bhs_id"]
+
+        table = execute(
+            "SELECT name, type, value " +
+            "FROM hydraulic_structures_basic_value " +
+            f"WHERE bhs = '{bhs_id}'"
+        )
+
+        for row in table:
+            name = row[0]
+            type = cls._str_to_type(row[1])
+            value = row[2]
+
+            val = cls(
+                name=name,
+                type=type,
+                value=value,
+                status=data['status']
+            )
+
+            new.append(val)
+
+        return new
+
+    def _db_save(self, execute, data=None):
+        bhs_id = data["bhs_id"]
+
+        sql = (
+            "INSERT INTO " +
+            "hydraulic_structures_basic_value(name, type, value, bhs) " +
+            "VALUES (" +
+            f"'{self._db_format(self._name)}', " +
+            f"'{self._db_format(self._type_to_str(self._type))}', " +
+            f"'{self._db_format(self._value)}', " +
+            f"{bhs_id}" +
+            ")"
+        )
+        execute(sql)
+
+        return True
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def type(self):
+        return self._type
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        self._value = self._type(value)
+        self._status.modified()
diff --git a/src/Model/HydraulicStructures/HydraulicStructures.py b/src/Model/HydraulicStructures/HydraulicStructures.py
new file mode 100644
index 0000000000000000000000000000000000000000..c301439b891216e058011ceb21f402f9bc4ca415
--- /dev/null
+++ b/src/Model/HydraulicStructures/HydraulicStructures.py
@@ -0,0 +1,285 @@
+# HydraulicStructures.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+from functools import reduce
+
+from tools import trace, timer, old_pamhyr_date_to_timestamp
+
+from Model.Tools.PamhyrDB import SQLSubModel
+from Model.Except import NotImplementedMethodeError
+
+from Model.HydraulicStructures.Basic.HydraulicStructures import BasicHS
+from Model.HydraulicStructures.Basic.Types import (
+    NotDefined,
+)
+
+logger = logging.getLogger()
+
+
+class HydraulicStructure(SQLSubModel):
+    _sub_classes = [
+        BasicHS,
+    ]
+    _id_cnt = 0
+
+    def __init__(self, id: int = -1, name: str = "",
+                 status=None):
+        super(HydraulicStructure, self).__init__()
+
+        self._status = status
+
+        if id == -1:
+            self.id = HydraulicStructure._id_cnt
+        else:
+            self.id = id
+
+        self._name = name
+        self._input_kp = None
+        self._output_kp = None
+        self._input_reach = None
+        self._output_reach = None
+        self._enabled = True
+        self._data = []
+
+        HydraulicStructure._id_cnt = max(
+            HydraulicStructure._id_cnt + 1,
+            self.id
+        )
+
+    @classmethod
+    def _db_create(cls, execute):
+        execute("""
+          CREATE TABLE hydraulic_structures(
+            id INTEGER NOT NULL PRIMARY KEY,
+            name TEXT NOT NULL,
+            enabled BOOLEAN NOT NULL,
+            input_kp REAL NOT NULL,
+            output_kp REAL NOT NULL,
+            input_reach INTEGER,
+            output_reach INTEGER,
+            FOREIGN KEY(input_reach) REFERENCES river_reach(id),
+            FOREIGN KEY(output_reach) REFERENCES river_reach(id)
+          )
+        """)
+
+        return cls._create_submodel(execute)
+
+    @classmethod
+    def _db_update(cls, execute, version):
+        major, minor, release = version.strip().split(".")
+        if major == minor == "0":
+            if int(release) < 6:
+                cls._db_create(execute)
+
+        return True
+
+    @classmethod
+    def _db_load(cls, execute, data=None):
+        new = []
+
+        table = execute(
+            "SELECT id, name, enabled, " +
+            "input_kp, output_kp, " +
+            "input_reach, output_reach " +
+            "FROM hydraulic_structures "
+        )
+
+        for row in table:
+            it = iter(row)
+
+            hs_id = next(it)
+            name = next(it)
+            enabled = (next(it) == 1)
+            input_kp = next(it)
+            output_kp = next(it)
+            input_reach_id = next(it)
+            output_reach_id = next(it)
+
+            hs = cls(
+                id=hs_id,
+                name=name,
+                status=data['status']
+            )
+
+            hs.enabled = enabled
+            hs.input_kp = input_kp if input_kp != -1 else None
+            hs.output_kp = output_kp if output_kp != -1 else None
+
+            hs.input_reach, hs.output_reach = reduce(
+                lambda acc, n: (
+                    n if n.id == input_reach_id else acc[0],
+                    n if n.id == output_reach_id else acc[1]
+                ),
+                data["edges"],
+                [None, None]
+            )
+
+            data['hs_id'] = hs_id
+            hs._data = BasicHS._db_load(execute, data)
+
+            new.append(hs)
+
+        return new
+
+    def _db_save(self, execute, data=None):
+        execute(f"DELETE FROM hydraulic_structures WHERE id = {self.id}")
+
+        input_reach_id = -1
+        if self._input_reach is not None:
+            input_reach_id = self._input_reach.id
+
+        output_reach_id = -1
+        if self._output_reach is not None:
+            output_reach_id = self._output_reach.id
+
+        input_kp = -1
+        if self.input_kp is not None:
+            input_kp = self.input_kp
+
+        output_kp = -1
+        if self.output_kp is not None:
+            output_kp = self.output_kp
+
+        sql = (
+            "INSERT INTO " +
+            "hydraulic_structures(" +
+            "  id, name, enabled, input_kp, output_kp, " +
+            "  input_reach, output_reach" +
+            ") " +
+            "VALUES (" +
+            f"{self.id}, '{self._db_format(self._name)}', " +
+            f"{self._db_format(self.enabled)}, " +
+            f"{input_kp}, {output_kp}, " +
+            f"{input_reach_id}, {output_reach_id}" +
+            ")"
+        )
+        execute(sql)
+
+        data['hs_id'] = self.id
+        execute(
+            "DELETE FROM hydraulic_structures_basic " +
+            f"WHERE hs = {self.id}"
+        )
+
+        for basic in self._data:
+            basic._db_save(execute, data)
+
+        return True
+
+    def __len__(self):
+        return len(self._data)
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, name):
+        self._name = name
+        self._status.modified()
+
+    @property
+    def input_kp(self):
+        return self._input_kp
+
+    @input_kp.setter
+    def input_kp(self, input_kp):
+        self._input_kp = input_kp
+        self._status.modified()
+
+    @property
+    def output_kp(self):
+        return self._output_kp
+
+    @output_kp.setter
+    def output_kp(self, output_kp):
+        self._output_kp = output_kp
+        self._status.modified()
+
+    @property
+    def enabled(self):
+        return self._enabled
+
+    @enabled.setter
+    def enabled(self, enabled):
+        self._enabled = enabled
+        self._status.modified()
+
+    @property
+    def input_reach(self):
+        return self._input_reach
+
+    @input_reach.setter
+    def input_reach(self, input_reach):
+        self._input_reach = input_reach
+        self._status.modified()
+
+    @property
+    def output_reach(self):
+        return self._output_reach
+
+    @output_reach.setter
+    def output_reach(self, output_reach):
+        self._output_reach = output_reach
+        self._status.modified()
+
+    @property
+    def basic_structures(self):
+        return self._data.copy()
+
+    def basic_structure(self, index: int):
+        return self._data[index]
+
+    def add(self, index: int):
+        value = NotDefined(status=self._status)
+        self._data.insert(index, value)
+        self._status.modified()
+        return value
+
+    def insert(self, index: int, value: BasicHS):
+        self._data.insert(index, value)
+        self._status.modified()
+
+    def delete_i(self, indexes):
+        self._data = list(
+            map(
+                lambda e: e[1],
+                filter(
+                    lambda e: e[0] not in indexes,
+                    enumerate(self._data)
+                )
+            )
+        )
+        self._status.modified()
+
+    def delete(self, els):
+        self._data = list(
+            filter(
+                lambda e: e not in els,
+                self._data
+            )
+        )
+        self._status.modified()
+
+    def sort(self, _reverse=False, key=None):
+        if key is None:
+            self._data.sort(reverse=_reverse)
+        else:
+            self._data.sort(reverse=_reverse, key=key)
+        self._status.modified()
diff --git a/src/Model/HydraulicStructures/HydraulicStructuresList.py b/src/Model/HydraulicStructures/HydraulicStructuresList.py
new file mode 100644
index 0000000000000000000000000000000000000000..549d80745b13099575cc4145d0a1c5a45bb0e773
--- /dev/null
+++ b/src/Model/HydraulicStructures/HydraulicStructuresList.py
@@ -0,0 +1,78 @@
+# HydraulicStructuresList.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from copy import copy
+from tools import trace, timer
+
+from Model.Tools.PamhyrList import PamhyrModelList
+from Model.HydraulicStructures.HydraulicStructures import HydraulicStructure
+
+
+class HydraulicStructureList(PamhyrModelList):
+    _sub_classes = [
+        HydraulicStructure,
+    ]
+
+    @classmethod
+    def _db_load(cls, execute, data=None):
+        new = cls(status=data['status'])
+
+        if data is None:
+            data = {}
+
+        new._lst = HydraulicStructure._db_load(
+            execute, data
+        )
+
+        return new
+
+    def _db_save(self, execute, data=None):
+        execute("DELETE FROM hydraulic_structures")
+
+        if data is None:
+            data = {}
+
+        for hs in self._lst:
+            hs._db_save(execute, data=data)
+
+        return True
+
+    def new(self, lst, index):
+        n = HydraulicStructure(status=self._status)
+        self._lst.insert(index, n)
+        self._status.modified()
+        return n
+
+    def __copy__(self):
+        new = HydraulicStructureList()
+
+        for lst in self._tabs:
+            new.tabs[lst] = self._tabs[lst].copy()
+
+        return new
+
+    def __deepcopy__(self):
+        new = HydraulicStructureList()
+
+        for lst in self._tabs:
+            new.tabs[lst] = self._tabs[lst].deepcopy()
+
+        return new
+
+    def copy(self):
+        return copy(self)
diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py
index 439445fa3887ad85087d59f7efd19a88576d2a95..2d88ea688d1571dd06e0437e6249f982099af8d6 100644
--- a/src/Model/InitialConditions/InitialConditions.py
+++ b/src/Model/InitialConditions/InitialConditions.py
@@ -250,7 +250,7 @@ class InitialConditions(SQLSubModel):
         )
 
         if new._data is not None:
-            yield new
+            return new
 
     def _db_save(self, execute, data=None):
         ok = True
@@ -376,7 +376,7 @@ class InitialConditions(SQLSubModel):
                  * (abs(incline) ** (0.5)))
             )
 
-            elevation= max(
+            elevation = max(
                 profile.z_min() + height,
                 previous_elevation
             )
@@ -422,7 +422,7 @@ class InitialConditions(SQLSubModel):
                 ((width * 0.8) * strickler * (abs(incline) ** (0.5)))
             ) ** (0.6)
 
-            elevation= max(
+            elevation = max(
                 profile.z_min() + height,
                 previous_elevation
             )
diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py
index d617ba77039b09e21d65925dca13fac464856dfe..c96345b1242d6911ffb2de58b6151e6ccb7bfc09 100644
--- a/src/Model/Network/Graph.py
+++ b/src/Model/Network/Graph.py
@@ -212,3 +212,16 @@ class Graph(object):
 
     def is_enable_edge(self, edge):
         return edge._enable
+
+    # def get_edge_id(self, reach):
+    #    for i, e in enumerate(self.enable_edges):
+    #        if e.id == reach.id:
+    #            return i
+
+    def get_edge_id(self, reach):
+        return next(
+            filter(
+                lambda e: e[1].id == reach.id,
+                enumerate(self.enable_edges())
+            )
+        )[0]
diff --git a/src/Model/Reservoir/Reservoir.py b/src/Model/Reservoir/Reservoir.py
index 97a6311318e23e56d52c931f5272ad037ad376d2..ebf7e24c883445ac085244dbd22e98c3d7825555 100644
--- a/src/Model/Reservoir/Reservoir.py
+++ b/src/Model/Reservoir/Reservoir.py
@@ -94,7 +94,11 @@ class Reservoir(SQLSubModel):
 
             new_reservoir._node = None
             if node_id != -1:
-                new_reservoir._node = next(filter(lambda n: n.id == node_id, data["nodes"]))
+                new_reservoir._node = next(
+                    filter(
+                        lambda n: n.id == node_id, data["nodes"]
+                    )
+                )
 
             new_data = []
             table = execute(
diff --git a/src/Model/River.py b/src/Model/River.py
index 84836625f83f2d7e69031a744d07b3a6aa5803e6..99cf8d2116be0757151a08a9e6b142e0b7a9fde7 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -37,6 +37,9 @@ from Model.Friction.FrictionList import FrictionList
 from Model.SolverParameters.SolverParametersList import SolverParametersList
 from Model.SedimentLayer.SedimentLayerList import SedimentLayerList
 from Model.Reservoir.ReservoirList import ReservoirList
+from Model.HydraulicStructures.HydraulicStructuresList import (
+    HydraulicStructureList,
+)
 
 from Solver.Solvers import solver_type_list
 
@@ -154,7 +157,9 @@ class RiverReach(Edge, SQLSubModel):
             data = {}
 
         table = execute(
-            "SELECT id, name, enable, node1, node2 FROM river_reach")
+            "SELECT id, name, enable, node1, node2 FROM river_reach"
+        )
+
         for row in table:
             # Update id counter
             cls._id_cnt = max(cls._id_cnt, row[0])
@@ -169,10 +174,9 @@ class RiverReach(Edge, SQLSubModel):
             new = cls(id, name, node1, node2, status=data["status"])
             new.enable(enable=enable)
 
-            data["reach"] = id
-            data["parent"] = new
-            new._reach = Reach._db_load(execute, data)
+            data["reach"] = new
 
+            new._reach = Reach._db_load(execute, data)
             new._frictions = FrictionList._db_load(execute, data)
 
             reachs.append(new)
@@ -219,6 +223,7 @@ class River(Graph, SQLSubModel):
         SolverParametersList,
         SedimentLayerList,
         ReservoirList,
+        HydraulicStructureList,
     ]
 
     def __init__(self, status=None):
@@ -237,6 +242,9 @@ class River(Graph, SQLSubModel):
         self._parameters = {}
         self._sediment_layers = SedimentLayerList(status=self._status)
         self._reservoir = ReservoirList(status=self._status)
+        self._hydraulic_structures = HydraulicStructureList(
+            status=self._status
+        )
 
     @classmethod
     def _db_create(cls, execute):
@@ -303,6 +311,12 @@ class River(Graph, SQLSubModel):
             data
         )
 
+        # Hydraulic Structures
+        new._hydraulic_structures = HydraulicStructureList._db_load(
+            execute,
+            data
+        )
+
         # Parameters
         new._parameters = SolverParametersList._db_load(
             execute,
@@ -319,6 +333,7 @@ class River(Graph, SQLSubModel):
         objs.append(self._sediment_layers)
         objs.append(self._stricklers)
         objs.append(self._reservoir)
+        objs.append(self._hydraulic_structures)
 
         for solver in self._parameters:
             objs.append(self._parameters[solver])
@@ -363,6 +378,10 @@ class River(Graph, SQLSubModel):
     def reservoir(self):
         return self._reservoir
 
+    @property
+    def hydraulic_structures(self):
+        return self._hydraulic_structures
+
     @property
     def parameters(self):
         return self._parameters
diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py
index 070c31ea7e28d40b8bd238ba21f0ffe075fc427d..bf1ca817ca3a3f52d612da579dea82729bba037e 100644
--- a/src/Model/SolverParameters/SolverParametersList.py
+++ b/src/Model/SolverParameters/SolverParametersList.py
@@ -142,6 +142,10 @@ class SolverParametersList(PamhyrModelList):
                         insert(v[0], v[1], ind)
                         ind += 1
 
+                    if int(release) < 7:
+                        insert("mage_init_internal", "N", ind)
+                        ind += 1
+
                     new = [
                         ("mage_sediment_masse_volumique", "2650.0"),
                         ("mage_sediment_angle_repos", "40.0"),
diff --git a/src/Model/Study.py b/src/Model/Study.py
index b163088b8d93312297d38c7ae469f800233269df..033f886747445153e90f65f2241f044905f71825 100644
--- a/src/Model/Study.py
+++ b/src/Model/Study.py
@@ -41,7 +41,7 @@ class Study(SQLModel):
 
     def __init__(self, filename=None, init_new=True):
         # Metadata
-        self._version = "0.0.5"
+        self._version = "0.0.7"
         self.creation_date = datetime.now()
         self.last_modification_date = datetime.now()
         self.last_save_date = datetime.now()
@@ -86,7 +86,7 @@ class Study(SQLModel):
     def is_saved(self):
         return self.status.is_saved()
 
-    def save(self):
+    def save(self, progress=None):
         # Save a copy of database
         fdir, fname = os.path.split(self.filename)
 
@@ -110,7 +110,7 @@ class Study(SQLModel):
 
         # Save
         self.last_save_date = datetime.now()
-        self._save()
+        self._save(progress=progress)
         self.status.save()
 
     @property
@@ -266,38 +266,54 @@ class Study(SQLModel):
 
         return new
 
-    def _save(self):
+    def _save(self, progress=None):
+        progress = progress if progress is not None else lambda: None
+
         self.execute(
             f"UPDATE info SET " +
             f"value='{self._db_format(self.name)}' WHERE key='name'"
         )
+        progress()
         self.execute(
             f"UPDATE info SET " +
             f"value='{self._db_format(self.description)}' " +
             "WHERE key='description'"
         )
+        progress()
         self.execute(
             f"UPDATE info SET " +
             f"value='{self._time_system}' WHERE key='time_system'"
         )
+        progress()
         self.execute(
             f"UPDATE info SET " +
             f"value='{timestamp(self._date)}' WHERE key='date'"
         )
+        progress()
         self.execute(
             f"UPDATE info SET " +
             f"value='{timestamp(self.creation_date)}' " +
             "WHERE key='creation_date'"
         )
+        progress()
         self.execute(
             f"UPDATE info SET " +
             f"value='{timestamp(self.last_save_date)}' " +
             "WHERE key='last_save_date'"
         )
+        progress()
 
-        self._save_submodel([self._river])
+        self._save_submodel([self._river], data=progress)
         self.commit()
 
+    def sql_save_request_count(self):
+        return self._count()
+
+    def _count(self):
+        cnt = self._save_count([self._river])
+        logger.debug(cnt)
+        return cnt + 6
+
     def close(self):
         """Close db connection
 
diff --git a/src/Model/Tools/PamhyrDB.py b/src/Model/Tools/PamhyrDB.py
index 8ee134fc97f7c725e0cbe1ebdc6fedb94ecb942f..44065709db86ccabe2aaa1e340e3ec673259be46 100644
--- a/src/Model/Tools/PamhyrDB.py
+++ b/src/Model/Tools/PamhyrDB.py
@@ -21,6 +21,7 @@ import sqlite3
 import logging
 
 from pathlib import Path
+from functools import reduce
 
 from tools import SQL
 from Model.Except import NotImplementedMethodeError
@@ -87,11 +88,16 @@ class SQLModel(SQL):
         raise NotImplementedMethodeError(self, self._update)
 
     def _save_submodel(self, objs, data=None):
-        def fn(sql): return self.execute(
-            sql,
-            fetch_one=False,
-            commit=False
-        )
+        progress = data if data is not None else lambda: None
+
+        def fn(sql):
+            res = self.execute(
+                sql,
+                fetch_one=False,
+                commit=False
+            )
+            progress()
+            return res
 
         ok = True
         for obj in objs:
@@ -100,9 +106,43 @@ class SQLModel(SQL):
         self.commit()
         return ok
 
-    def _save(self):
+    def _save(self, progress=None):
         raise NotImplementedMethodeError(self, self._save)
 
+    def _count(self):
+        raise NotImplementedMethodeError(self, self._count)
+
+    def _save_count(self, objs, data=None):
+        counter = {
+            "insert": 0,
+            "update": 0,
+            "delete": 0,
+            "other": 0,
+        }
+
+        def fn(sql):
+            if "insert" in sql.lower():
+                counter["insert"] = counter["insert"] + 1
+            elif "update" in sql.lower():
+                counter["update"] = counter["update"] + 1
+            elif "delete" in sql.lower():
+                counter["delete"] = counter["delete"] + 1
+            else:
+                counter["other"] = counter["other"] + 1
+            return []
+
+        ok = True
+        for obj in objs:
+            ok &= obj._db_save(fn)
+
+        logger.debug(counter)
+
+        return reduce(
+            lambda acc, k: acc + counter[k],
+            counter,
+            0
+        )
+
     @classmethod
     def _load(cls, filename=None):
         raise NotImplementedMethodeError(cls, cls._load)
diff --git a/src/Model/Tools/PamhyrDict.py b/src/Model/Tools/PamhyrDict.py
index c8cab70a1b0572c4092aa729aee5487e70a612b8..31bc12a6bc9df306df64b719ed977153371830f8 100644
--- a/src/Model/Tools/PamhyrDict.py
+++ b/src/Model/Tools/PamhyrDict.py
@@ -67,7 +67,7 @@ class PamhyrModelDict(SQLSubModel):
         if key in self._dict:
             v = self._dict[key]
 
-            if type(v) == types.GeneratorType:
+            if type(v) is types.GeneratorType:
                 return list(v)
 
             return v
diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py
index c4fa286ae9c1bd8ddd938d145b52ad51caef78f1..5ff5f6dcee3e36ecb892b56a7eb89ef6836ff2e2 100644
--- a/src/Solver/ASolver.py
+++ b/src/Solver/ASolver.py
@@ -121,6 +121,9 @@ class AbstractSolver(object):
     def is_stoped(self):
         return self._status == STATUS.STOPED
 
+    def has_results_loaded(self):
+        self._status = STATUS.NOT_LAUNCHED
+
     @name.setter
     def name(self, name):
         self._name = name
diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py
index f7f5627c58f2bb38a18a1d5a81ad550d1763e51c..b3614ca3538e92134743772dd6ae8c2d2ed83768 100644
--- a/src/Solver/CommandLine.py
+++ b/src/Solver/CommandLine.py
@@ -172,12 +172,13 @@ class CommandLineSolver(AbstractSolver):
 
         if not os.path.exists(exe):
             error = f"[ERROR] Path {exe} do not exists"
-            logger.info(error)
+            logger.warning(error)
             return error
 
         self._process.start(
             exe, args,
         )
+        self._process.waitForStarted()
 
         return True
 
@@ -191,7 +192,7 @@ class CommandLineSolver(AbstractSolver):
 
         if not os.path.exists(exe):
             error = f"[ERROR] Path {exe} do not exists"
-            logger.info(error)
+            logger.warning(error)
             return error
 
         self._process.start(
@@ -212,37 +213,48 @@ class CommandLineSolver(AbstractSolver):
 
         if not os.path.exists(exe):
             error = f"[ERROR] Path {exe} do not exists"
-            logger.info(error)
+            logger.warning(error)
             return error
 
         self._process.start(
             exe, args,
         )
+        self._process.waitForStarted()
 
         return True
 
     def _data_ready(self):
         # Read process output and put lines in queue
         s = self._process.readAll().data().decode()
+
         if self._output is not None:
             for x in s.split('\n'):
                 self._output.put(x)
 
-    def _run_next(self, study):
-        self._step += 1
-        if self._step < len(self._runs):
-            res = self._runs[self._step](study)
-            if res is not True:
-                self._output.put(res)
-        else:
-            self._status = STATUS.STOPED
-
     def _finished(self, study, exit_code, exit_status):
         if self._output is not None:
             self._output.put(exit_code)
 
+        logger.debug(
+            "Process finished with " +
+            f"code: {exit_code}, status: {exit_status}"
+        )
+
         self._run_next(study)
 
+    def _run_next(self, study):
+        self._step += 1
+
+        if self._step >= len(self._runs):
+            self._status = STATUS.STOPED
+            return
+
+        fn = self._runs[self._step]
+        res = fn(study)
+
+        if res is not True:
+            self._output.put(res)
+
     def run(self, study, process=None, output_queue=None):
         self._study = study
 
@@ -255,7 +267,8 @@ class CommandLineSolver(AbstractSolver):
         # Connect / reconnect signal
         self._process.readyRead.connect(self._data_ready)
         self._process.finished.connect(
-            lambda c, s: self._finished(study, c, s))
+            lambda c, s: self._finished(study, c, s)
+        )
 
         # Prepare running step
         self._runs = [
@@ -265,6 +278,8 @@ class CommandLineSolver(AbstractSolver):
         ]
         self._step = 0
 
+        self._status = STATUS.RUNNING
+
         # Run first step
         res = self._runs[0](study)
         if res is not True:
diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py
index 22349870d0e3da5b1ae7f644ff4e984d551deae1..00d440984da8bbabe5ce00ebe304a395f9398d34 100644
--- a/src/Solver/Mage.py
+++ b/src/Solver/Mage.py
@@ -20,7 +20,7 @@ import os
 import logging
 import numpy as np
 
-from tools import timer
+from tools import timer, trace
 
 from Solver.CommandLine import CommandLineSolver
 from Checker.Mage import MageNetworkGraphChecker
@@ -36,7 +36,7 @@ def mage_file_open(filepath, mode):
 
     if "w" in mode:
         # Write header
-        f.write("* This file is generate by PAMHYR, please don't modify\n")
+        f.write("* This file is generated by PAMHYR, please don't modify\n")
 
     return f
 
@@ -81,6 +81,7 @@ class Mage(CommandLineSolver):
             ("mage_compute_reach_volume_balance", "y"),
             ("mage_max_reach_volume_balance", "0.001"),
             ("mage_min_reach_volume_to_check", "1000.0"),
+            ("mage_init_internal", " "),
         ]
 
         return lst
@@ -374,13 +375,7 @@ class Mage(CommandLineSolver):
         with mage_file_open(os.path.join(repertory, f"{name}.INI"), "w+") as f:
             has_ini = False
             id = 1
-            reachs = study.river.edges()
-            reachs = list(
-                filter(
-                    lambda e: e.is_enable(),
-                    reachs
-                )
-            )
+            reachs = study.river.enable_edges()
 
             # TODO put real date...
             f.write(f"$ date en minutes :       0.00\n")
@@ -442,13 +437,149 @@ class Mage(CommandLineSolver):
 
         return files
 
+    @timer
+    def _export_SIN(self, study, repertory, qlog, name="0"):
+        files = []
+
+        sin_dict = {
+            "ND": "*",
+            "S1": "D", "S2": "T", "S3": "T",
+            "OR": "O", "OC": "B", "OV": "F",
+            "V1": "V", "V2": "W",
+            "BO": "A",
+            "UD": "X",
+            "PO": "P",
+        }
+
+        hydraulic_structures = study.river.hydraulic_structures.lst
+        if len(hydraulic_structures) == 0:
+            return files
+
+        if qlog is not None:
+            qlog.put("Export SIN file")
+
+        with mage_file_open(os.path.join(repertory, f"{name}.SIN"), "w+") as f:
+            files.append(f"{name}.SIN")
+
+            for hs in hydraulic_structures:
+                if not hs.input_reach.is_enable():
+                    continue
+
+                f.write(
+                    '* ouvrage au pk ' +
+                    f"{hs.input_kp:>12.1f}" + ' ' +
+                    hs.name + '\n'
+                )
+
+                for bhs in hs.basic_structures:
+                    reach_id = study.river.get_edge_id(hs.input_reach) + 1
+                    param_str = ' '.join(
+                        [
+                            f'{p:>10.3f}'
+                            for p in self._export_SIN_parameters(bhs)
+                        ]
+                    )
+
+                    f.write(
+                        f"{sin_dict[bhs._type]} " +
+                        f"{reach_id} {hs.input_kp:>12.3f} {param_str} " +
+                        f"{bhs.name}\n"
+                    )
+
+        return files
+
+    def _export_SIN_parameters(self, bhs):
+        res = [9999.999] * 5
+
+        if len(bhs) == 5:
+            res = self._export_SIN_parameters_5(bhs)
+        elif len(bhs) == 4:
+            res = self._export_SIN_parameters_4(bhs)
+        elif len(bhs) == 3:
+            res = self._export_SIN_parameters_3(bhs)
+
+        return res
+
+    def _export_SIN_parameters_5(self, bhs):
+        # S2, OR, V1, V2, UD
+        return [
+            bhs._data[0].value,
+            bhs._data[1].value,
+            bhs._data[2].value,
+            bhs._data[3].value,
+            bhs._data[4].value,
+        ]
+
+    def _export_SIN_parameters_4(self, bhs):
+        # S3, OC
+        res = [
+            bhs._data[0].value,
+            bhs._data[1].value,
+            bhs._data[2].value,
+            bhs._data[3].value,
+            0.0,
+        ]
+
+        if bhs._type == "T":    # S3
+            res = [0.0] + res[:-1]
+
+        return res
+
+    def _export_SIN_parameters_3(self, bhs):
+        # S1, BO
+        if bhs._type == "S1":
+            res = [
+                bhs._data[0].value,
+                bhs._data[1].value,
+                0.0,
+                bhs._data[2].value,
+                9999.99,
+            ]
+        else:
+            res = [
+                bhs._data[0].value,
+                bhs._data[1].value,
+                bhs._data[2].value,
+                0.0,
+                0.0,
+            ]
+
+        return res
+
+    @timer
+    def _export_DEV(self, study, repertory, qlog, name="0"):
+        files = []
+
+        if qlog is not None:
+            qlog.put("Export DEV file")
+
+        with mage_file_open(
+                os.path.join(
+                    repertory, f"{name}.DEV"
+                ), "w+"
+        ) as f:
+            reachs = study.river.enable_edges()
+
+            id = 1
+            for reach in reachs:
+                f.write(f"YD{id:3}\n")
+                f.write(f"YG{id:3}\n")
+                id += 1
+            files.append(f"{name}.DEV")
+
+        return files
+
     @timer
     def _export_REP(self, study, repertory, files, qlog, name="0"):
         if qlog is not None:
             qlog.put("Export REP file")
 
         # Write header
-        with mage_file_open(os.path.join(repertory, f"{name}.REP"), "w+") as f:
+        with mage_file_open(
+                os.path.join(
+                    repertory, f"{name}.REP"
+                ), "w+"
+        ) as f:
             f.write("confirmation=non\n")
 
             for file in files:
@@ -588,8 +719,12 @@ class Mage8(Mage):
                 if name in ["command_line_arguments"]:
                     continue
 
-                if name == "mage_compute_reach_volume_balance":
-                    value = "O" if value == "y" else "N"
+                if name == "compute_reach_volume_balance":
+                    value = "O" if value.lower() == "y" else "N"
+
+                if name == "init_internal":
+                    value = ("p" if value.lower() in ["y", "yes", "true"]
+                             else "")
 
                 f.write(f"{name} {value}\n")
 
@@ -683,7 +818,9 @@ class Mage8(Mage):
             self._export_bound_cond(study, repertory, qlog, name=name)
         files = files + self._export_RUG(study, repertory, qlog, name=name)
         files = files + self._export_INI(study, repertory, qlog, name=name)
+        files = files + self._export_SIN(study, repertory, qlog, name=name)
         files = files + self._export_CAS(study, repertory, qlog, name=name)
+        files = files + self._export_DEV(study, repertory, qlog, name=name)
         self._export_REP(study, repertory, files, qlog, name=name)
 
         return True
@@ -814,12 +951,13 @@ class Mage8(Mage):
                         # Set data for profile RI
                         reach.set(ri, timestamp, key, d)
                         if key == "Z":
-                            profile = study.river.current_reach().reach.profile(ri)
-                            ptX,ptY = profile.get_water_limits(d)
+                            profile = study.river\
+                                           .current_reach()\
+                                           .reach.profile(ri)
+                            ptX, ptY = profile.get_water_limits(d)
                             reach.set(ri, timestamp, "ptX", ptX)
                             reach.set(ri, timestamp, "ptY", ptY)
 
-
                 endline()
                 end = newline().size <= 0
 
@@ -861,7 +999,7 @@ class Mage8(Mage):
             logger.debug(f"read_gra: nb_profile = {nb_profile}")
             logger.debug(f"read_gra: mage_version = {mage_version}")
 
-            if mage_version <= 80:
+            if mage_version < 80:
                 msg = (
                     "Read GRA files: " +
                     f"Possible incompatible mage version '{mage_version}', " +
@@ -929,60 +1067,71 @@ class Mage8(Mage):
             def ip_to_ri(r, i): return i - reach_offset[r]
 
             ts = set()
-            ind = 0
             end = False
 
             newline()
             while not end:
                 n = read_int(1)[0]
                 timestamp = read_float64(1)[0]
+                with_bedload = read_int(1)[0]
 
-                logger.debug(f"read_gra: timestamp = {timestamp} sec")
-                ts.add(timestamp)
+                logger.debug(f"read_gra: Number of cross section: {n}")
+                logger.debug(f"read_gra: Timestamp: {timestamp}")
+                logger.debug(f"read_gra: Type of bedload: {with_bedload}")
 
                 endline()
 
-                for i in range(n):
+                npts = [1] * n
+                if with_bedload == 1:
                     newline()
-                    nsl = read_int(1)[0]
+                    npts = read_int(n)
                     endline()
+                sum_npts = sum(npts)
+                logger.debug(f"read_gra: Number of points: {sum_npts}")
 
-                    # Get current profile id
-                    reach = ip_to_r(i)
-                    ri = ip_to_ri(reach, i)
+                newline()
+                nsl = read_int(sum_npts)
+                logger.debug(f"read_gra: Number of sedimentary layers: {nsl}")
+                endline()
 
-                    if nsl > 1:
-                        logger.warning(
-                            "read_gra: " +
-                            "Multiple sediment layers for one profile " +
-                            "is not implemented yet..."
-                        )
+                newline()
+                data = read_float64(sum(nsl) * 3)
+                endline()
 
-                    for j in range(nsl):
-                        newline()
-                        nl = read_int(1)[0]
-                        endline()
+                ts.add(timestamp)
 
-                        sl = []
+                i_pts = 0
+                i_data = 0
+                # Loop on cross section
+                for i in range(n):
+                    sec_sl = []
+                    reach = ip_to_r(i)
+                    p_i = ip_to_ri(reach, i)
 
-                        for k in range(nl):
-                            newline()
-                            data = read_float64(3)
-                            endline()
+                    # Loop on cross section points
+                    for j in range(npts[i]):
+                        sl = []
 
-                            h = data[0]
-                            d50 = data[1]
-                            sigma = data[2]
+                        # Loop on sediment layers
+                        for k in range(nsl[i_pts]):
+                            h = data[i_data]
+                            d50 = data[i_data + 1]
+                            sigma = data[i_data + 2]
 
                             sl.append((h, d50, sigma))
+                            i_data += 3
+
+                        i_pts += 1
+                        sec_sl.append(sl)
 
-                        reach.set(ri, timestamp, "sl", sl)
+                    reach.set(p_i, timestamp, "sl", sec_sl)
 
-                ind += 1
+                logger.debug(
+                    f"read_gra: data size = {len(data)} ({i_data} readed)"
+                )
                 end = newline().size <= 0
 
-            logger.debug(reachs[0].profiles[0]._data)
-            results.set("timestamps", ts)
+            results.set("sediment_timestamps", ts)
             logger.info(f"read_gra: ... end with {len(ts)} timestamp read")
 
     @timer
diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py
index ccec087baf3dd644adbb199619b2a4e94fcadd01..4bdbb5ae72ab145418a4e73fe20d5a6b7d0842b6 100644
--- a/src/View/BoundaryCondition/Edit/Window.py
+++ b/src/View/BoundaryCondition/Edit/Window.py
@@ -160,7 +160,7 @@ class EditBoundaryConditionWindow(PamhyrWindow):
             table_headers=headers,
             editable_headers=self._data.header,
             delegates={
-                #"time": self._delegate_time,
+                # "time": self._delegate_time,
             },
             data=self._data,
             undo=self._undo_stack,
diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py
index 024dc2889f44a8374ea8c0604d0ccc1b06dc460f..aad78fc35c80eb3ee65c7c519759594152f01e2f 100644
--- a/src/View/BoundaryCondition/Table.py
+++ b/src/View/BoundaryCondition/Table.py
@@ -115,13 +115,13 @@ class ComboBoxDelegate(QItemDelegate):
 
 
 class TableModel(PamhyrTableModel):
-    def __init__(self, trad = None, **kwargs):
+    def __init__(self, trad=None, **kwargs):
         self._trad = trad
         self._long_types = {}
         if self._trad is not None:
             self._long_types = self._trad.get_dict("long_types")
 
-        super(TableModel, self).__init__(trad = trad, **kwargs)
+        super(TableModel, self).__init__(trad=trad, **kwargs)
 
     def _setup_lst(self):
         self._lst = self._data.boundary_condition
diff --git a/src/View/Geometry/Profile/UndoCommand.py b/src/View/Geometry/Profile/UndoCommand.py
index c1122a688b7889f563b504a69b0c3e005f9a9ca6..e1bf3ae8593aee207874bd7b8436fa402672af6f 100644
--- a/src/View/Geometry/Profile/UndoCommand.py
+++ b/src/View/Geometry/Profile/UndoCommand.py
@@ -93,7 +93,7 @@ class AddCommand(QUndoCommand):
         self._point = None
 
     def undo(self):
-        self._profile.delete([self._index])
+        self._profile.delete_i([self._index])
 
     def redo(self):
         if self._point is None:
@@ -119,7 +119,7 @@ class DelCommand(QUndoCommand):
             self._profile.insert_point(row, point)
 
     def redo(self):
-        self._profile.delete(self._rows)
+        self._profile.delete_i(self._rows)
 
 
 class SortCommand(QUndoCommand):
@@ -181,7 +181,7 @@ class PasteCommand(QUndoCommand):
 
     def undo(self):
         for ind in range(len(self._points)):
-            self._profile.delete([self._row])
+            self._profile.delete_i([self._row])
 
     def redo(self):
         for point in self._points:
diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py
index 9fd3f1aa76a8b55562a20082fd456ec23c29a848..0b241d9497a71578ec3f9736b695500e1a280c98 100644
--- a/src/View/Geometry/Profile/Window.py
+++ b/src/View/Geometry/Profile/Window.py
@@ -135,16 +135,13 @@ class ProfileWindow(PamhyrWindow):
         self._tablemodel.blockSignals(False)
 
     def index_selected_row(self):
-        rows = self._tablemodel\
-            .selectionModel()\
-            .selectedRows()
+        table = self.find(QTableView, "tableView")
+        rows = table.selectionModel()\
+                    .selectedRows()
         if len(rows) == 0:
             return 0
 
-        return self._tablemodel\
-            .selectionModel()\
-            .selectedRows()[0]\
-            .row()
+        return rows[0].row()
 
     def add(self):
         table = self.find(QTableView, "tableView")
@@ -213,24 +210,24 @@ class ProfileWindow(PamhyrWindow):
 
         self.update_plot()
 
-    def copy(self):
-        rows = self._tablemodel\
-            .selectionModel()\
-            .selectedRows()
-        table = []
-        table.append(["x", "y", "z", "name"])
+    def _copy(self):
+        table = self.find(QTableView, "tableView")
+        rows = table.selectionModel().selectedRows()
+
+        data = []
+        data.append(["x", "y", "z", "name"])
 
         for row in rows:
             point = self._profile.point(row.row())
-            table.append(
+            data.append(
                 [
                     point.x, point.y, point.z, point.name
                 ]
             )
 
-        self.copyTableIntoClipboard(table)
+        self.copyTableIntoClipboard(data)
 
-    def paste(self):
+    def _paste(self):
         header, data = self.parseClipboardTable()
 
         if len(data) == 0:
@@ -245,10 +242,10 @@ class ProfileWindow(PamhyrWindow):
         self._tablemodel.paste(row, header, data)
         self.update_plot()
 
-    def undo(self):
+    def _undo(self):
         self._tablemodel.undo()
         self.update_plot()
 
-    def redo(self):
+    def _redo(self):
         self._tablemodel.redo()
         self.update_plot()
diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py
index 3bc4ebd15c737fe4cccb1e091980b43340270c87..4f1e0b22de4202b0c5f2b4ff811932a3ace047ad 100644
--- a/src/View/Geometry/Window.py
+++ b/src/View/Geometry/Window.py
@@ -455,7 +455,7 @@ class GeometryWindow(PamhyrWindow):
                    .selectedRows()
 
         table = []
-        table.append(["name", "kp"])
+        # table.append(["name", "kp"])
 
         for row in rows:
             profile = self._reach.profile(row.row())
@@ -471,13 +471,17 @@ class GeometryWindow(PamhyrWindow):
         if len(data) == 0:
             return
 
-        if len(header) != 0:
-            header.append("reach")
+        # if len(header) != 0:
+        #     header.append("reach")
+        #     header.append("status")
+
         for row in data:
             row.append(self._reach)
+            row.append(self._study.river._status)
 
         row = self.index_selected_row()
-        self._tablemodel.paste(row, header, data)
+        # self._tablemodel.paste(row, header, data)
+        self._tablemodel.paste(row, [], data)
         self.select_current_profile()
 
     def _undo(self):
diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed81c13166ed87864d90f72026efbea6f0833789
--- /dev/null
+++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py
@@ -0,0 +1,275 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from tools import trace, timer
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QComboBox, QMessageBox,
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.HydraulicStructures.BasicHydraulicStructures.UndoCommand import (
+    SetNameCommand, SetTypeCommand,
+    SetEnabledCommand, AddCommand, DelCommand,
+    SetValueCommand,
+)
+from Model.HydraulicStructures.Basic.Types import BHS_types
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+    def __init__(self, data=None, trad=None, parent=None):
+        super(ComboBoxDelegate, self).__init__(parent)
+
+        self._data = data
+        self._trad = trad
+
+        self._long_types = {}
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+    def createEditor(self, parent, option, index):
+        self.editor = QComboBox(parent)
+
+        lst = list(
+            map(
+                lambda k: self._long_types[k],
+                BHS_types.keys()
+            )
+        )
+        self.editor.addItems(lst)
+
+        self.editor.setCurrentText(index.data(Qt.DisplayRole))
+        return self.editor
+
+    def setEditorData(self, editor, index):
+        value = index.data(Qt.DisplayRole)
+        self.editor.currentTextChanged.connect(self.currentItemChanged)
+
+    def setModelData(self, editor, model, index):
+        text = str(editor.currentText())
+        model.setData(index, text)
+        editor.close()
+        editor.deleteLater()
+
+    def updateEditorGeometry(self, editor, option, index):
+        r = QRect(option.rect)
+        if self.editor.windowFlags() & Qt.Popup:
+            if editor.parent() is not None:
+                r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
+        editor.setGeometry(r)
+
+    @pyqtSlot()
+    def currentItemChanged(self):
+        self.commitData.emit(self.sender())
+
+
+class TableModel(PamhyrTableModel):
+    def __init__(self, trad=None, **kwargs):
+        self._trad = trad
+        self._long_types = {}
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+        super(TableModel, self).__init__(trad=trad, **kwargs)
+
+    def rowCount(self, parent):
+        return len(self._lst)
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        if self._headers[column] == "name":
+            return self._data.basic_structure(row).name
+        elif self._headers[column] == "type":
+            return self._long_types[self._data.basic_structure(row).type]
+
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        row = index.row()
+        column = index.column()
+
+        try:
+            if self._headers[column] == "name":
+                self._undo.push(
+                    SetNameCommand(
+                        self._data, row, value
+                    )
+                )
+            elif self._headers[column] == "type":
+                if self._question_set_type():
+                    key = next(
+                        k for k, v in self._long_types.items()
+                        if v == value
+                    )
+
+                    self._undo.push(
+                        SetTypeCommand(
+                            self._data, row, BHS_types[key]
+                        )
+                    )
+        except Exception as e:
+            logger.error(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def _question_set_type(self):
+        question = QMessageBox(self._parent)
+
+        question.setWindowTitle(self._trad['msg_type_change_title'])
+        question.setText(self._trad['msg_type_change_text'])
+        question.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
+        question.setIcon(QMessageBox.Question)
+
+        res = question.exec()
+        return res == QMessageBox.Ok
+
+    def add(self, row, parent=QModelIndex()):
+        self.beginInsertRows(parent, row, row - 1)
+
+        self._undo.push(
+            AddCommand(
+                self._data, row
+            )
+        )
+
+        self.endInsertRows()
+        self.layoutChanged.emit()
+
+    def delete(self, rows, parent=QModelIndex()):
+        self.beginRemoveRows(parent, rows[0], rows[-1])
+
+        self._undo.push(
+            DelCommand(
+                self._data, rows
+            )
+        )
+
+        self.endRemoveRows()
+        self.layoutChanged.emit()
+
+    def enabled(self, row, enabled, parent=QModelIndex()):
+        self._undo.push(
+            SetEnabledCommand(
+                self._lst, row, enabled
+            )
+        )
+        self.layoutChanged.emit()
+
+    def undo(self):
+        self._undo.undo()
+        self.layoutChanged.emit()
+
+    def redo(self):
+        self._undo.redo()
+        self.layoutChanged.emit()
+
+
+class ParametersTableModel(PamhyrTableModel):
+    def __init__(self, trad=None, **kwargs):
+        self._trad = trad
+        self._long_types = {}
+
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+        self._hs_index = None
+
+        super(ParametersTableModel, self).__init__(trad=trad, **kwargs)
+
+    def rowCount(self, parent):
+        if self._hs_index is None:
+            return 0
+
+        return len(
+            self._data.basic_structure(self._hs_index)
+        )
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        if self._hs_index is None:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        hs = self._data.basic_structure(self._hs_index)
+
+        if self._headers[column] == "name":
+            return hs.parameters[row].name
+        elif self._headers[column] == "value":
+            return str(hs.parameters[row].value)
+
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        if self._hs_index is None:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        try:
+            if self._headers[column] == "value":
+                self._undo.push(
+                    SetValueCommand(
+                        self._data.basic_structure(self._hs_index),
+                        row, value
+                    )
+                )
+        except Exception as e:
+            logger.error(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def update_hs_index(self, index):
+        self._hs_index = index
+        self.layoutChanged.emit()
diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..6681219e8dab5d517e579f805d31b99d5480015e
--- /dev/null
+++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py
@@ -0,0 +1,85 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Tools.PamhyrTranslate import PamhyrTranslate
+
+_translate = QCoreApplication.translate
+
+
+class BasicHydraulicStructuresTranslate(PamhyrTranslate):
+    def __init__(self):
+        super(BasicHydraulicStructuresTranslate, self).__init__()
+
+        self._dict['msg_type_change_title'] = _translate(
+            "BasicHydraulicStructures",
+            "Change hydraulic structure type"
+        )
+
+        self._dict['msg_type_change_text'] = _translate(
+            "BasicHydraulicStructures",
+            "Do you want to change the hydraulic structure type and reset \
+hydraulic structure values?"
+        )
+
+        self._sub_dict["long_types"] = {
+            "ND": _translate(
+                "BasicHydraulicStructures", "Not defined"
+            ),
+            "S1": _translate(
+                "BasicHydraulicStructures", "Seuil déversoir"
+            ),
+            "S2": _translate(
+                "BasicHydraulicStructures", "Seuil trapezoidal"
+            ),
+            "S3": _translate(
+                "BasicHydraulicStructures", "Seuil triangulaire"
+            ),
+            "OR": _translate(
+                "BasicHydraulicStructures", "Orifice rectangulaire"
+            ),
+            "OC": _translate(
+                "BasicHydraulicStructures", "Orifice circulaire"
+            ),
+            "OV": _translate(
+                "BasicHydraulicStructures", "Orifice voute"
+            ),
+            "V1": _translate(
+                "BasicHydraulicStructures", "Vanne rectangulaire"
+            ),
+            "V2": _translate(
+                "BasicHydraulicStructures", "Vanne rectangulaire simplifiée"
+            ),
+            "BO": _translate(
+                "BasicHydraulicStructures", "Perte de charge à la Borda"
+            ),
+            "UD": _translate(
+                "BasicHydraulicStructures", "User defined"
+            ),
+        }
+
+        self._sub_dict["table_headers"] = {
+            "name": _translate("BasicHydraulicStructures", "Name"),
+            "type": _translate("BasicHydraulicStructures", "Type"),
+        }
+
+        self._sub_dict["table_headers_parameters"] = {
+            "name": _translate("BasicHydraulicStructures", "Name"),
+            "value": _translate("BasicHydraulicStructures", "Value"),
+        }
diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py b/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..124a446ca9c548498a43acfb0efb57b5d0ceaf3d
--- /dev/null
+++ b/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py
@@ -0,0 +1,153 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+
+class SetNameCommand(QUndoCommand):
+    def __init__(self, hs, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+        self._index = index
+        self._old = self._hs.basic_structure(self._index).name
+        self._new = str(new_value)
+
+    def undo(self):
+        self._hs.basic_structure(self._index).name = self._old
+
+    def redo(self):
+        self._hs.basic_structure(self._index).name = self._new
+
+
+class SetTypeCommand(QUndoCommand):
+    def __init__(self, hs, index, new_type):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+        self._index = index
+        self._type = new_type
+        self._old = self._hs.basic_structure(self._index)
+        self._new = self._hs.basic_structure(self._index)\
+            .convert(self._type)
+
+    def undo(self):
+        self._hs.delete_i([self._index])
+        self._hs.insert(self._index, self._old)
+
+    def redo(self):
+        self._hs.delete_i([self._index])
+        self._hs.insert(self._index, self._new)
+
+
+class SetEnabledCommand(QUndoCommand):
+    def __init__(self, hs, index, enabled):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+        self._index = index
+        self._old = not enabled
+        self._new = enabled
+
+    def undo(self):
+        self._hs.basic_structure(self._index).enabled = self._old
+
+    def redo(self):
+        self._hs.basic_structure(self._index).enabled = self._new
+
+
+class AddCommand(QUndoCommand):
+    def __init__(self, hs, index):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._hs.delete_i([self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._hs.add(self._index)
+        else:
+            self._hs.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+    def __init__(self, hs, rows):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+
+        self._rows = rows
+
+        self._bhs = []
+        for row in rows:
+            self._bhs.append((row, self._hs.basic_structure(row)))
+
+    def undo(self):
+        for row, el in self._bhs:
+            self._hs.insert(row, el)
+
+    def redo(self):
+        self._hs.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, hs, row, h_s):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+
+        self._row = row
+        self._bhs = deepcopy(h_s)
+        self._bhs.reverse()
+
+    def undo(self):
+        self._hs.delete_i(range(self._row, self._row + len(self._bhs)))
+
+    def redo(self):
+        for r in self._bhs:
+            self._hs.insert(self._row, r)
+
+####################################
+# Basic hydraulic structure values #
+####################################
+
+
+class SetValueCommand(QUndoCommand):
+    def __init__(self, bhs, index, value):
+        QUndoCommand.__init__(self)
+
+        self._bhs = bhs
+        self._index = index
+        self._old = self._bhs.parameters[self._index].value
+        self._new = self._bhs.parameters[self._index].type(value)
+
+    def undo(self):
+        self._bhs.parameters[self._index].value = self._old
+
+    def redo(self):
+        self._bhs.parameters[self._index].value = self._new
diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca4b26800ea639e752c28419725829ae5e82781c
--- /dev/null
+++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py
@@ -0,0 +1,265 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import timer, trace
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel, QCoreApplication,
+    pyqtSlot, pyqtSignal, QItemSelectionModel,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.HydraulicStructures.PlotAC import PlotAC
+
+from View.HydraulicStructures.BasicHydraulicStructures.Table import (
+    ComboBoxDelegate, TableModel, ParametersTableModel,
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.HydraulicStructures.BasicHydraulicStructures.Translate import (
+    BasicHydraulicStructuresTranslate
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class BasicHydraulicStructuresWindow(PamhyrWindow):
+    _pamhyr_ui = "BasicHydraulicStructures"
+    _pamhyr_name = "Basic Hydraulic Structures"
+
+    def __init__(self, data=None, study=None, config=None, parent=None):
+        name = self._pamhyr_name + " - " + study.name
+
+        super(BasicHydraulicStructuresWindow, self).__init__(
+            title=name,
+            study=study,
+            config=config,
+            trad=BasicHydraulicStructuresTranslate(),
+            parent=parent
+        )
+
+        self._hash_data.append(data)
+
+        self._hs = data
+
+        self.setup_table()
+        self.setup_checkbox()
+        self.setup_plot()
+        self.setup_connections()
+
+        self.update()
+
+    def setup_table(self):
+        self.setup_table_bhs()
+        self.setup_table_bhs_parameters()
+
+    def setup_table_bhs(self):
+        self._table = None
+
+        self._delegate_type = ComboBoxDelegate(
+            trad=self._trad,
+            parent=self
+        )
+
+        table = self.find(QTableView, f"tableView")
+        self._table = TableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers"),
+            editable_headers=["name", "type"],
+            delegates={
+                "type": self._delegate_type,
+            },
+            trad=self._trad,
+            data=self._hs,
+            undo=self._undo_stack,
+            parent=self,
+        )
+
+        selectionModel = table.selectionModel()
+        index = table.model().index(0, 0)
+
+        selectionModel.select(
+            index,
+            QItemSelectionModel.Rows |
+            QItemSelectionModel.ClearAndSelect |
+            QItemSelectionModel.Select
+        )
+        table.scrollTo(index)
+
+    def setup_table_bhs_parameters(self):
+        self._table_parameters = None
+
+        table = self.find(QTableView, f"tableView_2")
+        self._table_parameters = ParametersTableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers_parameters"),
+            editable_headers=["value"],
+            delegates={},
+            trad=self._trad,
+            data=self._hs,
+            undo=self._undo_stack,
+            parent=self,
+        )
+
+    def setup_checkbox(self):
+        self._checkbox = self.find(QCheckBox, f"checkBox")
+        self._set_checkbox_state()
+
+    def setup_plot(self):
+        self.canvas = MplCanvas(width=5, height=4, dpi=100)
+        self.canvas.setObjectName("canvas")
+        self.toolbar = PamhyrPlotToolbar(
+            self.canvas, self
+        )
+        self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
+        self.plot_layout.addWidget(self.toolbar)
+        self.plot_layout.addWidget(self.canvas)
+
+        reach = self._hs.input_reach
+        profile_kp = self._hs.input_kp
+        if profile_kp is not None:
+            profiles = reach.reach.get_profiles_from_kp(float(profile_kp))
+        else:
+            profiles = None
+        if profiles is not None:
+            profile = profiles[0]
+        else:
+            profile = None
+
+        self.plot_ac = PlotAC(
+            canvas=self.canvas,
+            river=self._study.river,
+            reach=self._hs.input_reach,
+            profile=profile,
+            toolbar=self.toolbar
+        )
+        self.plot_ac.draw()
+
+    def setup_connections(self):
+        self.find(QAction, "action_add").triggered.connect(self.add)
+        self.find(QAction, "action_delete").triggered.connect(self.delete)
+        self._checkbox.clicked.connect(self._set_basic_structure_state)
+
+        table = self.find(QTableView, "tableView")
+        table.selectionModel()\
+             .selectionChanged\
+             .connect(self.update)
+
+        self._table.dataChanged.connect(self.update)
+        self._table.layoutChanged.connect(self.update)
+
+    def index_selected(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0]
+        else:
+            return None
+
+    def index_selected_row(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0].row()
+        else:
+            return None
+
+    def index_selected_rows(self):
+        table = self.find(QTableView, "tableView")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        rows = self.index_selected_rows()
+
+        if len(self._hs) == 0 or len(rows) == 0:
+            self._table.add(0)
+        else:
+            self._table.add(rows[0])
+
+    def delete(self):
+        rows = self.index_selected_rows()
+
+        if len(rows) == 0:
+            return
+
+        self._table.delete(rows)
+
+    def _copy(self):
+        logger.info("TODO: copy")
+
+    def _paste(self):
+        logger.info("TODO: paste")
+
+    def _undo(self):
+        self._table.undo()
+
+    def _redo(self):
+        self._table.redo()
+
+    def _set_checkbox_state(self):
+        row = self.index_selected_row()
+
+        if row is None:
+            self._checkbox.setEnabled(False)
+            self._checkbox.setChecked(True)
+        else:
+            self._checkbox.setEnabled(True)
+            self._checkbox.setChecked(self._hs.basic_structure(row).enabled)
+
+    def _set_basic_structure_state(self):
+        row = self.index_selected_row()
+
+        if row is not None:
+            self._table.enabled(
+                row,
+                self._checkbox.isChecked()
+            )
+
+    def update(self):
+        self._set_checkbox_state()
+        self._update_parameters_table()
+
+    def _update_parameters_table(self):
+        row = self.index_selected_row()
+        self._table_parameters.update_hs_index(row)
diff --git a/src/View/HydraulicStructures/PlotAC.py b/src/View/HydraulicStructures/PlotAC.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d171968945997795e09919d1ee70e8cf338146a
--- /dev/null
+++ b/src/View/HydraulicStructures/PlotAC.py
@@ -0,0 +1,133 @@
+# PlotAC.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from tools import timer
+from View.Tools.PamhyrPlot import PamhyrPlot
+from matplotlib import pyplot as plt
+
+from PyQt5.QtCore import (
+    QCoreApplication
+)
+
+_translate = QCoreApplication.translate
+
+
+class PlotAC(PamhyrPlot):
+    def __init__(self, canvas=None, trad=None, toolbar=None,
+                 river=None, reach=None, profile=None,
+                 parent=None):
+        super(PlotAC, self).__init__(
+            canvas=canvas,
+            trad=trad,
+            data=river,
+            toolbar=toolbar,
+            parent=parent
+        )
+
+        self._current_reach = reach
+        self._current_profile = profile
+
+    @property
+    def river(self):
+        return self.data
+
+    @river.setter
+    def river(self, river):
+        self.data = river
+
+    @timer
+    def draw(self, highlight=None):
+        self.canvas.axes.cla()
+        self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
+
+        if self.data is None:
+            self.line_kp = None
+            return
+
+        if self._current_reach is None:
+            self.line_kp = None
+            return
+
+        reach = self._current_reach
+
+        self.canvas.axes.set_xlabel(
+            _translate("MainWindow_reach", "X (m)"),
+            color='black', fontsize=11
+        )
+        self.canvas.axes.set_ylabel(
+            _translate("MainWindow_reach", "Elevation (m)"),
+            color='black', fontsize=11
+        )
+
+        if self._current_profile is None:
+            self.line_kp = None
+        else:
+            profile = self._current_profile
+            x = profile.get_station()
+            z = profile.z()
+
+            self.line_kp, = self.canvas.axes.plot(
+                x, z,
+                linestyle="solid",
+                lw=1.8,
+                color='grey',
+            )
+
+        self.canvas.axes.relim()
+        self.canvas.axes.autoscale_view()
+
+        self.canvas.figure.tight_layout()
+        self.canvas.figure.canvas.draw_idle()
+        if self.toolbar is not None:
+            self.toolbar.update()
+
+    def set_reach(self, reach):
+        self._current_reach = reach
+        self.update()
+
+    def set_profile(self, profile):
+        self._current_profile = profile
+        self.update()
+
+    def update(self):
+        if self.line_kp is None:
+            self.draw()
+            return
+
+        if self._current_reach is None or self._current_profile is None:
+            self.clear()
+            return
+
+        profile = self._current_profile
+        x = profile.get_station()
+        z = profile.z()
+
+        self.line_kp.set_data(x, z)
+
+        self.canvas.axes.relim()
+        self.canvas.axes.autoscale_view()
+
+        self.canvas.figure.tight_layout()
+        self.canvas.figure.canvas.draw_idle()
+
+    def clear(self):
+        if self.line_kp is not None:
+            self.line_kp.set_data([], [])
+
+        self.canvas.figure.tight_layout()
+        self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/HydraulicStructures/PlotKPC.py b/src/View/HydraulicStructures/PlotKPC.py
new file mode 100644
index 0000000000000000000000000000000000000000..f72cf7fc1d41cdbe15d22ac761bbb6d232a07c0c
--- /dev/null
+++ b/src/View/HydraulicStructures/PlotKPC.py
@@ -0,0 +1,161 @@
+# PlotKPC.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from tools import timer
+from View.Tools.PamhyrPlot import PamhyrPlot
+
+from PyQt5.QtCore import (
+    QCoreApplication
+)
+
+from matplotlib.collections import LineCollection
+
+_translate = QCoreApplication.translate
+
+
+class PlotKPC(PamhyrPlot):
+    def __init__(self, canvas=None, trad=None, toolbar=None,
+                 river=None, reach=None, profile=None,
+                 parent=None):
+        super(PlotKPC, self).__init__(
+            canvas=canvas,
+            trad=trad,
+            data=river,
+            toolbar=toolbar,
+            parent=parent
+        )
+
+        self._current_reach = reach
+        self._current_profile = profile
+
+    @property
+    def river(self):
+        return self.data
+
+    @river.setter
+    def river(self, river):
+        self.data = river
+
+    @timer
+    def draw(self, highlight=None):
+        self.canvas.axes.cla()
+        self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
+
+        if self.data is None:
+            self.profile = None
+            self.line_kp_zmin_zmax = None
+            self.line_kp_zmin = None
+            return
+
+        if self._current_reach is None:
+            self.profile = None
+            self.line_kp_zmin_zmax = None
+            self.line_kp_zmin = None
+            return
+
+        reach = self._current_reach
+
+        self.canvas.axes.set_ylabel(
+            _translate("MainWindow_reach", "Elevation (m)"),
+            color='black', fontsize=11
+        )
+        self.canvas.axes.set_xlabel(
+            _translate("MainWindow_reach", "KP (m)"),
+            color='black', fontsize=11
+        )
+
+        kp = reach.reach.get_kp()
+        z_min = reach.reach.get_z_min()
+        z_max = reach.reach.get_z_max()
+
+        self.line_kp_zmin, = self.canvas.axes.plot(
+            kp, z_min,
+            color='grey', lw=1.
+        )
+
+        if len(kp) != 0:
+            self.line_kp_zmin_zmax = self.canvas.axes.vlines(
+                x=kp,
+                ymin=z_min, ymax=z_max,
+                color='b',
+                lw=1.
+            )
+
+        if self._current_profile is None:
+            self.profile = None
+        else:
+            self.profile, = self.canvas.axes.plot(
+                [self._current_profile.kp, self._current_profile.kp],
+                [self._current_profile.z_min(), self._current_profile.z_max()],
+                color='red', lw=1.
+            )
+
+        self.canvas.axes.relim()
+        self.canvas.figure.tight_layout()
+        self.canvas.figure.canvas.draw_idle()
+        if self.toolbar is not None:
+            self.toolbar.update()
+
+    def set_reach(self, reach):
+        self._current_reach = reach
+        self._current_profile = None
+        self.update()
+
+    def set_profile(self, profile):
+        self._current_profile = profile
+        self.update_profil()
+
+    def update(self):
+        self.draw()
+
+    def update_profil(self):
+        reach = self._current_reach
+        kp = reach.reach.get_kp()
+        z_min = reach.reach.get_z_min()
+        z_max = reach.reach.get_z_max()
+
+        if self.profile is None:
+            self.draw()
+        else:
+            self.profile.set_data(
+                [self._current_profile.kp, self._current_profile.kp],
+                [self._current_profile.z_min(), self._current_profile.z_max()],
+            )
+
+            self.canvas.axes.relim()
+            self.canvas.axes.autoscale_view()
+            self.canvas.figure.canvas.draw_idle()
+
+    def clear(self):
+        if self.profile is not None:
+            self.profile.set_data([], [])
+
+        if self.line_kp_zmin_zmax is not None:
+            self.line_kp_zmin_zmax.remove()
+            self.line_kp_zmin_zmax = None
+
+        if self.line_kp_zmin is not None:
+            self.line_kp_zmin.set_data([], [])
+
+        self.canvas.figure.canvas.draw_idle()
+
+    def clear_profile(self):
+        if self.profile is not None:
+            self.profile.set_data([], [])
+
+        self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/HydraulicStructures/Table.py b/src/View/HydraulicStructures/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..210cf1e8ec7d6d0d9c4de8bc8170e8d637167cf5
--- /dev/null
+++ b/src/View/HydraulicStructures/Table.py
@@ -0,0 +1,215 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from tools import trace, timer
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QComboBox,
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.HydraulicStructures.UndoCommand import (
+    SetNameCommand, SetReachCommand, SetKpCommand,
+    SetEnabledCommand, AddCommand, DelCommand,
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+    def __init__(self, data=None, trad=None, parent=None, mode="reaches"):
+        super(ComboBoxDelegate, self).__init__(parent)
+
+        self._data = data
+        self._trad = trad
+        self._mode = mode
+
+    def createEditor(self, parent, option, index):
+        self.editor = QComboBox(parent)
+
+        val = []
+        if self._mode == "kp":
+            reach = self._data.hydraulic_structures\
+                              .get(index.row())\
+                              .input_reach
+            if reach is not None:
+                val = list(
+                    map(
+                        lambda kp: str(kp), reach.reach.get_kp()
+                    )
+                )
+        else:
+            val = list(
+                map(
+                    lambda n: n.name, self._data.edges()
+                )
+            )
+
+        self.editor.addItems(
+            [_translate("Hydraulic structure", "Not associated")] +
+            val
+        )
+
+        self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
+        return self.editor
+
+    def setEditorData(self, editor, index):
+        value = index.data(Qt.DisplayRole)
+        self.editor.currentTextChanged.connect(self.currentItemChanged)
+
+    def setModelData(self, editor, model, index):
+        text = str(editor.currentText())
+        model.setData(index, text)
+        editor.close()
+        editor.deleteLater()
+
+    def updateEditorGeometry(self, editor, option, index):
+        r = QRect(option.rect)
+        if self.editor.windowFlags() & Qt.Popup:
+            if editor.parent() is not None:
+                r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
+        editor.setGeometry(r)
+
+    @pyqtSlot()
+    def currentItemChanged(self):
+        self.commitData.emit(self.sender())
+
+
+class TableModel(PamhyrTableModel):
+    def _setup_lst(self):
+        self._lst = self._data._hydraulic_structures
+
+    def rowCount(self, parent):
+        return len(self._lst)
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        if self._headers[column] == "name":
+            return self._lst.get(row).name
+        elif self._headers[column] == "reach":
+            n = self._lst.get(row).input_reach
+            if n is None:
+                return _translate("Hydraulic structure", "Not associated")
+            return n.name
+        elif self._headers[column] == "kp":
+            n = self._lst.get(row).input_kp
+            if n is None:
+                return _translate("Hydraulic structure", "Not associated")
+            return n
+
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        row = index.row()
+        column = index.column()
+        na = _translate("Hydraulic structure", "Not associated")
+
+        try:
+            if self._headers[column] == "name":
+                self._undo.push(
+                    SetNameCommand(
+                        self._lst, row, value
+                    )
+                )
+            elif self._headers[column] == "reach":
+                if value == na:
+                    value = None
+
+                self._undo.push(
+                    SetReachCommand(
+                        self._lst, row, self._data.edge(value)
+                    )
+                )
+            elif self._headers[column] == "kp":
+                if value == na:
+                    value = None
+
+                self._undo.push(
+                    SetKpCommand(
+                        self._lst, row, value
+                    )
+                )
+        except Exception as e:
+            logger.info(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def add(self, row, parent=QModelIndex()):
+        self.beginInsertRows(parent, row, row - 1)
+
+        self._undo.push(
+            AddCommand(
+                self._lst, row
+            )
+        )
+
+        self.endInsertRows()
+        self.layoutChanged.emit()
+
+    def delete(self, rows, parent=QModelIndex()):
+        self.beginRemoveRows(parent, rows[0], rows[-1])
+
+        self._undo.push(
+            DelCommand(
+                self._lst, rows
+            )
+        )
+
+        self.endRemoveRows()
+        self.layoutChanged.emit()
+
+    def enabled(self, row, enabled, parent=QModelIndex()):
+        self._undo.push(
+            SetEnabledCommand(
+                self._lst, row, enabled
+            )
+        )
+        self.layoutChanged.emit()
+
+    def undo(self):
+        self._undo.undo()
+        self.layoutChanged.emit()
+
+    def redo(self):
+        self._undo.redo()
+        self.layoutChanged.emit()
diff --git a/src/View/HydraulicStructures/Translate.py b/src/View/HydraulicStructures/Translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..1db3faeca39e54c5d391af1f631598809e3c3d52
--- /dev/null
+++ b/src/View/HydraulicStructures/Translate.py
@@ -0,0 +1,34 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Tools.PamhyrTranslate import PamhyrTranslate
+
+_translate = QCoreApplication.translate
+
+
+class HydraulicStructuresTranslate(PamhyrTranslate):
+    def __init__(self):
+        super(HydraulicStructuresTranslate, self).__init__()
+
+        self._sub_dict["table_headers"] = {
+            "name": _translate("HydraulicStructures", "Name"),
+            "reach": _translate("HydraulicStructures", "Reach"),
+            "kp": _translate("HydraulicStructures", "Kp"),
+        }
diff --git a/src/View/HydraulicStructures/UndoCommand.py b/src/View/HydraulicStructures/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c47ccf12256fb436862ef1316db9814ddfd52a2
--- /dev/null
+++ b/src/View/HydraulicStructures/UndoCommand.py
@@ -0,0 +1,159 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+logger = logging.getLogger()
+
+
+class SetNameCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = self._h_s_lst.get(self._index).name
+        self._new = str(new_value)
+
+    def undo(self):
+        self._h_s_lst.get(self._index).name = self._old
+
+    def redo(self):
+        self._h_s_lst.get(self._index).name = self._new
+
+
+class SetReachCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, reach):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = self._h_s_lst.get(self._index).input_reach
+        self._new = reach
+        self._old_kp = self._h_s_lst.get(self._index).input_kp
+        self._new_kp = None
+
+    def undo(self):
+        i = self._h_s_lst.get(self._index)
+        i.input_reach = self._old
+        i.input_kp = self._old_kp
+
+    def redo(self):
+        i = self._h_s_lst.get(self._index)
+        i.input_reach = self._new
+        i.input_kp = self._new_kp
+
+
+class SetKpCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, kp):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = self._h_s_lst.get(self._index).input_kp
+        self._new = kp
+
+    def undo(self):
+        self._h_s_lst.get(self._index).input_kp = self._old
+
+    def redo(self):
+        self._h_s_lst.get(self._index).input_kp = self._new
+
+
+class SetEnabledCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, enabled):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = not enabled
+        self._new = enabled
+
+    def undo(self):
+        self._h_s_lst.get(self._index).enabled = self._old
+
+    def redo(self):
+        self._h_s_lst.get(self._index).enabled = self._new
+
+
+class AddCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._h_s_lst.delete_i([self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._h_s_lst.new(self._h_s_lst, self._index)
+        else:
+            self._h_s_lst.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+    def __init__(self, h_s_lst, rows):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+
+        self._rows = rows
+
+        self._h_s = []
+        for row in rows:
+            self._h_s.append((row, self._h_s_lst.get(row)))
+        self._h_s.sort()
+
+    def undo(self):
+        for row, el in self._h_s:
+            self._h_s_lst.insert(row, el)
+
+    def redo(self):
+        self._h_s_lst.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, h_s_lst, row, h_s):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+
+        self._row = row
+        self._h_s = deepcopy(h_s)
+        self._h_s.reverse()
+
+    def undo(self):
+        self._h_s_lst.delete_i(
+            self._tab,
+            range(self._row, self._row + len(self._h_s))
+        )
+
+    def redo(self):
+        for r in self._h_s:
+            self._h_s_lst.insert(self._row, r)
diff --git a/src/View/HydraulicStructures/Window.py b/src/View/HydraulicStructures/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0b80bf9ec2c6b55cb87395fe0bff80d5ffd9cfd
--- /dev/null
+++ b/src/View/HydraulicStructures/Window.py
@@ -0,0 +1,313 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import timer, trace
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel, QCoreApplication,
+    pyqtSlot, pyqtSignal, QItemSelectionModel,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.HydraulicStructures.PlotAC import PlotAC
+from View.HydraulicStructures.PlotKPC import PlotKPC
+
+from View.HydraulicStructures.Table import (
+    TableModel, ComboBoxDelegate
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.HydraulicStructures.Translate import HydraulicStructuresTranslate
+
+from View.HydraulicStructures.BasicHydraulicStructures.Window import (
+    BasicHydraulicStructuresWindow
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class HydraulicStructuresWindow(PamhyrWindow):
+    _pamhyr_ui = "HydraulicStructures"
+    _pamhyr_name = "Hydraulic Structures"
+
+    def __init__(self, study=None, config=None, parent=None):
+        name = self._pamhyr_name + " - " + study.name
+
+        super(HydraulicStructuresWindow, self).__init__(
+            title=name,
+            study=study,
+            config=config,
+            trad=HydraulicStructuresTranslate(),
+            parent=parent
+        )
+
+        self._hs_lst = self._study.river._hydraulic_structures
+
+        self.setup_table()
+        self.setup_checkbox()
+        self.setup_plots()
+        self.setup_connections()
+
+        self.update()
+
+    def setup_table(self):
+        self._table = None
+
+        self._delegate_reach = ComboBoxDelegate(
+            trad=self._trad,
+            data=self._study.river,
+            parent=self,
+            mode="reaches"
+        )
+        self._delegate_kp = ComboBoxDelegate(
+            trad=self._trad,
+            data=self._study.river,
+            parent=self,
+            mode="kp"
+        )
+
+        table = self.find(QTableView, f"tableView")
+        self._table = TableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers"),
+            editable_headers=["name", "reach", "kp"],
+            delegates={
+                "reach": self._delegate_reach,
+                "kp": self._delegate_kp,
+            },
+            trad=self._trad,
+            data=self._study.river,
+            undo=self._undo_stack,
+        )
+
+        selectionModel = table.selectionModel()
+        index = table.model().index(0, 0)
+
+        selectionModel.select(
+            index,
+            QItemSelectionModel.Rows |
+            QItemSelectionModel.ClearAndSelect |
+            QItemSelectionModel.Select
+        )
+        table.scrollTo(index)
+
+    def setup_checkbox(self):
+        self._checkbox = self.find(QCheckBox, f"checkBox")
+        self._set_checkbox_state()
+
+    def setup_plots(self):
+        self.canvas = MplCanvas(width=5, height=4, dpi=100)
+        self.canvas.setObjectName("canvas")
+        self.toolbar = PamhyrPlotToolbar(
+            self.canvas, self
+        )
+        self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
+        self.plot_layout.addWidget(self.toolbar)
+        self.plot_layout.addWidget(self.canvas)
+
+        self.plot_kpc = PlotKPC(
+            canvas=self.canvas,
+            river=self._study.river,
+            reach=None,
+            profile=None,
+            toolbar=self.toolbar
+        )
+        self.plot_kpc.draw()
+
+        self.canvas_2 = MplCanvas(width=5, height=4, dpi=100)
+        self.canvas_2.setObjectName("canvas_2")
+        self.toolbar_2 = PamhyrPlotToolbar(
+            self.canvas_2, self
+        )
+        self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2")
+        self.plot_layout_2.addWidget(self.toolbar_2)
+        self.plot_layout_2.addWidget(self.canvas_2)
+
+        self.plot_ac = PlotAC(
+            canvas=self.canvas_2,
+            river=self._study.river,
+            reach=None,
+            profile=None,
+            toolbar=self.toolbar_2
+        )
+        self.plot_ac.draw()
+
+    def setup_connections(self):
+        self.find(QAction, "action_add").triggered.connect(self.add)
+        self.find(QAction, "action_delete").triggered.connect(self.delete)
+        self.find(QAction, "action_edit").triggered.connect(self.edit)
+        self._checkbox.clicked.connect(self._set_structure_state)
+
+        table = self.find(QTableView, "tableView")
+        table.selectionModel()\
+             .selectionChanged\
+             .connect(self.update)
+
+        self._table.dataChanged.connect(self.update)
+        self._table.layoutChanged.connect(self.update)
+
+    def index_selected(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0]
+        else:
+            return None
+
+    def index_selected_row(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0].row()
+        else:
+            return None
+
+    def index_selected_rows(self):
+        table = self.find(QTableView, "tableView")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        rows = self.index_selected_rows()
+        if len(self._hs_lst) == 0 or len(rows) == 0:
+            self._table.add(0)
+        else:
+            self._table.add(rows[0])
+
+    def delete(self):
+        rows = self.index_selected_rows()
+        if len(rows) == 0:
+            return
+
+        self._table.delete(rows)
+
+    def _copy(self):
+        logger.info("TODO: copy")
+
+    def _paste(self):
+        logger.info("TODO: paste")
+
+    def _undo(self):
+        self._table.undo()
+
+    def _redo(self):
+        self._table.redo()
+
+    def edit(self):
+        rows = self.index_selected_rows()
+        for row in rows:
+            data = self._hs_lst.get(row)
+            print(row)
+            print(data)
+            if self.sub_window_exists(
+                BasicHydraulicStructuresWindow,
+                data=[self._study, None, data]
+            ):
+                continue
+
+            win = BasicHydraulicStructuresWindow(
+                data=data,
+                study=self._study,
+                parent=self
+            )
+            win.show()
+
+    def _set_checkbox_state(self):
+        row = self.index_selected_row()
+        if row is None:
+            self._checkbox.setEnabled(False)
+            self._checkbox.setChecked(True)
+        else:
+            self._checkbox.setEnabled(True)
+            self._checkbox.setChecked(self._hs_lst.get(row).enabled)
+
+    def _set_structure_state(self):
+        row = self.index_selected_row()
+        if row is not None:
+            self._table.enabled(
+                row,
+                self._checkbox.isChecked()
+            )
+
+    def update(self):
+        self._set_checkbox_state()
+        self._update_clear_plot()
+
+    def _update_clear_plot(self):
+        rows = self.index_selected_rows()
+
+        if len(rows) == 0 or len(self._hs_lst) == 0:
+            self._update_clear_all()
+            return
+
+        reach = self._hs_lst.get(rows[0]).input_reach
+        if reach is not None:
+            self.plot_kpc.set_reach(reach)
+            self.plot_ac.set_reach(reach)
+
+            profile_kp = self._hs_lst.get(rows[0]).input_kp
+            if profile_kp is not None:
+                profiles = reach.reach\
+                                .get_profiles_from_kp(
+                                    float(profile_kp)
+                                )
+
+                if profiles is not None:
+                    profile = profiles[0]
+
+                    self.plot_kpc.set_profile(profile)
+                    self.plot_ac.set_profile(profile)
+                else:
+                    self._update_clear_profile()
+            else:
+                self._update_clear_profile()
+        else:
+            self._update_clear_all()
+
+    def _update_clear_all(self):
+        self.plot_kpc.clear()
+        self.plot_ac.clear()
+
+    def _update_clear_profile(self):
+        self.plot_ac.clear()
+        self.plot_kpc.clear_profile()
diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py
index 745fd15e143e13320ab67ed85df45dfb7dfe1436..897703a469c147761234d2ff7d2003775d95d342 100644
--- a/src/View/LateralContribution/Edit/Window.py
+++ b/src/View/LateralContribution/Edit/Window.py
@@ -101,7 +101,7 @@ class EditLateralContributionWindow(PamhyrWindow):
             table_headers=headers,
             editable_headers=self._data.header,
             delegates={
-                #"time": self._delegate_time,
+                # "time": self._delegate_time,
             },
             data=self._data,
             undo=self._undo_stack,
diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py
index 8a9a2e8e740c48a37e08d9662c9a49defd77142b..a6f04af5a494680de1dba05b0a73761459f45691 100644
--- a/src/View/LateralContribution/Window.py
+++ b/src/View/LateralContribution/Window.py
@@ -107,7 +107,7 @@ class LateralContributionWindow(PamhyrWindow):
             self._table[t] = TableModel(
                 table_view=table,
                 table_headers=self._trad.get_dict("table_headers"),
-                editable_headers=True,
+                editable_headers=self._trad.get_dict("table_headers"),
                 delegates={
                     "type": self._delegate_type,
                     "edge": self._delegate_edge,
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 04ed5f94267aad9aad4e2419b6aea1fab59b47f1..4ce865db72b513fb371b577034cf67677894c8c2 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -28,12 +28,12 @@ from PyQt5.QtGui import (
 )
 
 from PyQt5.QtCore import (
-    QTranslator, QEvent, QUrl,
+    Qt, QTranslator, QEvent, QUrl,
 )
 from PyQt5.QtWidgets import (
     QMainWindow, QApplication, QAction,
     QFileDialog, QShortcut, QMenu, QToolBar,
-    QMessageBox,
+    QMessageBox, QProgressDialog,
 )
 from PyQt5.uic import loadUi
 
@@ -48,6 +48,7 @@ from View.Network.Window import NetworkWindow
 from View.Geometry.Window import GeometryWindow
 from View.BoundaryCondition.Window import BoundaryConditionWindow
 from View.Reservoir.Window import ReservoirWindow
+from View.HydraulicStructures.Window import HydraulicStructuresWindow
 from View.LateralContribution.Window import LateralContributionWindow
 from View.InitialConditions.Window import InitialConditionsWindow
 from View.Stricklers.Window import StricklersWindow
@@ -102,7 +103,8 @@ define_model_action = [
     "action_menu_boundary_conditions", "action_menu_initial_conditions",
     "action_menu_edit_friction", "action_menu_edit_lateral_contribution",
     "action_menu_run_solver", "action_menu_sediment_layers",
-    "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs"
+    "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs",
+    "action_menu_edit_hydraulic_structures"
 ]
 
 action = (
@@ -193,6 +195,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
             "action_menu_edit_geometry": self.open_geometry,
             "action_menu_boundary_conditions": self.open_boundary_cond,
             "action_menu_edit_reservoirs": self.open_reservoir,
+            "action_menu_edit_hydraulic_structures":
+            self.open_hydraulic_structures,
             "action_menu_initial_conditions": self.open_initial_conditions,
             "action_menu_edit_friction": self.open_frictions,
             "action_menu_edit_lateral_contribution": self.open_lateral_contrib,
@@ -279,8 +283,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
             Nothing
         """
         self.update_enable_action()
-        # Maximise window
-        #self.showMaximized()
+        # self.showMaximized()
 
     def set_debug_lvl(self, debug=True):
         if debug:
@@ -403,8 +406,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
         if self._study.is_saved:
             return
 
+        sql_request_count = self._study.sql_save_request_count()
+        progress = QProgressDialog(
+            "Saving...", None,
+            0, sql_request_count,
+            parent=self
+        )
+        progress.setWindowModality(Qt.WindowModal)
+        progress.setValue(0)
+
         logger.info("Save...")
-        self._study.save()
+        self._study.save(
+            progress=lambda: progress.setValue(progress.value() + 1)
+        )
+        logger.info("Done")
 
     def save_as_study(self):
         """Save current study as new file
@@ -425,7 +440,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
         else:
             self._study.filename = file_name + ".pamhyr"
 
-        self._study.save()
+        sql_request_count = self._study.sql_save_request_count()
+        progress = QProgressDialog(
+            "Saving...", None,
+            0, sql_request_count,
+            parent=self
+        )
+        progress.setWindowModality(Qt.WindowModal)
+        progress.setValue(0)
+
+        logger.info("Save...")
+        self._study.save(
+            progress=lambda: progress.setValue(progress.value() + 1)
+        )
+        logger.info("Done")
 
     ##################
     # MSG AND DIALOG #
@@ -628,6 +656,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
         reservoir = ReservoirWindow(study=self._study, parent=self)
         reservoir.show()
 
+    def open_hydraulic_structures(self):
+        if self.sub_window_exists(
+            HydraulicStructuresWindow,
+            data=[self._study, None]
+        ):
+            return
+
+        hydraulic_structures = HydraulicStructuresWindow(
+            study=self._study,
+            parent=self
+        )
+        hydraulic_structures.show()
+
     def open_lateral_contrib(self):
         if self.sub_window_exists(
             LateralContributionWindow,
@@ -635,7 +676,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
         ):
             return
 
-        lateral = LateralContributionWindow(study=self._study, parent=self)
+        lateral = LateralContributionWindow(
+            study=self._study,
+            parent=self
+        )
         lateral.show()
 
     def open_stricklers(self):
diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py
index ade0eaffd326c3c07dc0ecfbe63afa2bacc871e7..a045a1fa60ab1cf46fc2d6ff51961c411c297371 100644
--- a/src/View/Network/GraphWidget.py
+++ b/src/View/Network/GraphWidget.py
@@ -178,12 +178,15 @@ class EdgeItem(QGraphicsItem):
         if self.graph.selected_item() == self:
             color = Qt.red
         elif self.graph.current_edge() == self:
-            color = Qt.black
+            color = Qt.blue
         elif not self.graph.graph.is_enable_edge(self.edge):
             color = Qt.darkGray
 
-        painter.setPen(QPen(color, 2, Qt.SolidLine, Qt.RoundCap,
-                            Qt.RoundJoin))
+        painter.setPen(
+            QPen(
+                color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin
+            )
+        )
         # Draw the line
         painter.drawLine(line)
 
diff --git a/src/View/Reservoir/Edit/Plot.py b/src/View/Reservoir/Edit/Plot.py
index 8f8f49b858228c34d3b143ec17bea797afa9f146..8c75a19971d85a4284fd4b234c4f76063721b636 100644
--- a/src/View/Reservoir/Edit/Plot.py
+++ b/src/View/Reservoir/Edit/Plot.py
@@ -69,7 +69,7 @@ class Plot(PamhyrPlot):
         )
 
         # Plot label
-        #header = self.data.header
+        # header = self.data.header
         self.canvas.axes.set_xlabel(
             self._table_headers["z"], color='black', fontsize=10
         )
diff --git a/src/View/Reservoir/Edit/Window.py b/src/View/Reservoir/Edit/Window.py
index 34724bea63ab44e4e37aec5557fd02a32a2bd9f5..865ae6c89e1eb109bbf570a3afe9c937aa2c329e 100644
--- a/src/View/Reservoir/Edit/Window.py
+++ b/src/View/Reservoir/Edit/Window.py
@@ -88,18 +88,13 @@ class EditReservoirWindow(PamhyrWindow):
     def setup_table(self):
         headers = {}
         table_headers = self._trad.get_dict("table_headers")
-        #for h in self._data.header:
-            #headers[h] = table_headers[h]
 
         table = self.find(QTableView, "tableView")
         self._table = TableModel(
             table_view=table,
             table_headers=table_headers,
             editable_headers=table_headers,
-            #editable_headers=self._data.header,
-            delegates={
-                #"time": self._delegate_time,
-            },
+            delegates={},
             data=self._data,
             undo=self._undo_stack,
             opt_data=self._study.time_system
@@ -181,7 +176,7 @@ class EditReservoirWindow(PamhyrWindow):
         rows = self.index_selected_rows()
 
         table = []
-        #table.append(self._data.header)
+        # table.append(self._data.header)
         table.append(self._trad.get_dict("table_headers"))
 
         data = self._data.data
diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py
index 5709efb4fa903d8616100decea36f2f62acf9e7e..f236bf8f3f7871004d56fe72a6202711eb11ef7e 100644
--- a/src/View/Results/PlotH.py
+++ b/src/View/Results/PlotH.py
@@ -92,31 +92,24 @@ class PlotH(PamhyrPlot):
         self.ts = list(self.results.get("timestamps"))
         self.ts.sort()
 
-        self.canvas.axes.set_xlim(
-            left=min(self.ts), right=max(self.ts)
-        )
-
         # Draw discharge for each timestamp
         x = self.ts
         y = profile.get_key("Q")
 
-        if len(self.ts) != len(x):
-            logger.warning(
-                "Results as less Q data ({len(x)}) " +
-                "than timestamps ({len(self.ts)}) " +
-                "for profile {self._current_profile_id}"
-            )
-            return
+        self._line, = self.canvas.axes.plot(
+            x, y, lw=1.,
+            color='r',
+            markersize=3, marker='+'
+        )
 
-        self.canvas.axes.set_ylim(
-            [min(min(y), 0), max(y) + 10]
+        self._current, = self.canvas.axes.plot(
+            self._current_timestamp,
+            y[self.ts.index(self._current_timestamp)],
+            lw=1., color='b',
+            markersize=3, marker='+'
         )
 
-        self._line, = self.canvas.axes.plot(
-                x, y, lw=1.,
-                color='r',
-                markersize=3, marker='+'
-            )
+        self.canvas.axes.relim()
 
         # Custom time display
         nb = len(x)
@@ -175,12 +168,21 @@ class PlotH(PamhyrPlot):
 
     def set_timestamp(self, timestamp):
         self._current_timestamp = timestamp
-        #self.update()
+        self.update()
 
     def update(self):
         reach = self.results.river.reach(self._current_reach_id)
         profile = reach.profile(self._current_profile_id)
+
         x = self.ts
         y = profile.get_key("Q")
-        self._line.set_data(x,y)
+
+        self._line.set_data(x, y)
+
+        self._current.set_data(
+            self._current_timestamp,
+            y[self.ts.index(self._current_timestamp)]
+        )
+
+        self.canvas.axes.relim()
         self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py
index 93f91c9f4282527c8fb360d7f0566ba465a62f89..34ed91f9b28c1a4730dcb0b379579aa08ecbb22d 100644
--- a/src/View/Results/PlotKPC.py
+++ b/src/View/Results/PlotKPC.py
@@ -105,8 +105,14 @@ class PlotKPC(PamhyrPlot):
             )
 
         self.profile, = self.canvas.axes.plot(
-            [kp[self._current_profile_id], kp[self._current_profile_id]],
-            [z_max[self._current_profile_id],z_min[self._current_profile_id]],
+            [
+                kp[self._current_profile_id],
+                kp[self._current_profile_id]
+            ],
+            [
+                z_max[self._current_profile_id],
+                z_min[self._current_profile_id]
+            ],
             color='red', lw=1.
         )
 
@@ -137,7 +143,13 @@ class PlotKPC(PamhyrPlot):
         z_min = reach.geometry.get_z_min()
         z_max = reach.geometry.get_z_max()
         self.profile.set_data(
-            [kp[self._current_profile_id], kp[self._current_profile_id]],
-            [z_max[self._current_profile_id],z_min[self._current_profile_id]]
+            [
+                kp[self._current_profile_id],
+                kp[self._current_profile_id]
+            ],
+            [
+                z_max[self._current_profile_id],
+                z_min[self._current_profile_id]
+            ]
         )
         self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py
index 895df719633913c52947433160ed2280e6bad16b..1db320f91c80c4bff5f786b677029be1fab020e3 100644
--- a/src/View/Results/PlotSedProfile.py
+++ b/src/View/Results/PlotSedProfile.py
@@ -51,13 +51,13 @@ class PlotSedProfile(PamhyrPlot):
         profiles_sl = list(
             map(
                 lambda sl: sl[0],
-                profile.get_ts_key(self._current_timestamp, "sl")
+                profile.get_ts_key(self._current_timestamp, "sl")[0]
             )
         )
         profiles_sl_0 = list(
             map(
                 lambda sl: sl[0],
-                profile.get_ts_key(0.0, "sl")
+                profile.get_ts_key(0.0, "sl")[0]
             )
         )
 
diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py
index 3978930d2fb265ed60da62ce5b91bde6e2ccd2a9..b7f229c8c113a4425e96b4174e4a4957cad63b97 100644
--- a/src/View/Results/PlotSedReach.py
+++ b/src/View/Results/PlotSedReach.py
@@ -124,14 +124,14 @@ class PlotSedReach(PamhyrPlot):
         profiles_sl_0 = list(
             map(
                 # Get SL list for profile p at time 0 (initial data)
-                lambda p: p.get_ts_key(0.0, "sl"),
+                lambda p: p.get_ts_key(0.0, "sl")[0],
                 reach.profiles
             )
         )
         profiles_sl = list(
             map(
                 # Get SL list for profile p at current time
-                lambda p: p.get_ts_key(self._current_timestamp, "sl"),
+                lambda p: p.get_ts_key(self._current_timestamp, "sl")[0],
                 reach.profiles
             )
         )
@@ -163,7 +163,10 @@ class PlotSedReach(PamhyrPlot):
         z_sl = reduce(
             lambda acc, v: acc + [
                 list(
-                    map(lambda x, y: y - x, v, acc[-1])
+                    map(
+                        lambda x, y: y - x,
+                        v, acc[-1]
+                    )
                 )
             ],
             sl_0,
diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py
index 559fd9a6c05b4797a2cd59ffc110166416edd309..3d0e22810c68749e59835a45a45c1b42f581a76d 100644
--- a/src/View/Results/PlotXY.py
+++ b/src/View/Results/PlotXY.py
@@ -136,10 +136,14 @@ class PlotXY(PamhyrPlot):
         poly_x = [0]
         poly_y = [0]
 
-        self.fill = self.canvas.axes.fill(poly_x, poly_y, color='skyblue', alpha=0.7)
+        self.fill = self.canvas.axes.fill(
+            poly_x, poly_y,
+            color='skyblue',
+            alpha=0.7
+        )
 
-        #self.canvas.axes.autoscale_view(True, True, True)
-        #self.canvas.axes.autoscale()
+        # self.canvas.axes.autoscale_view(True, True, True)
+        # self.canvas.axes.autoscale()
         self.canvas.figure.tight_layout()
         self.canvas.figure.canvas.draw_idle()
         if self.toolbar is not None:
@@ -166,7 +170,7 @@ class PlotXY(PamhyrPlot):
             # Current profile
             profile = reach.profile(self._current_profile_id).geometry
 
-            self.plot_selected.set_data(profile.x(),profile.y())
+            self.plot_selected.set_data(profile.x(), profile.y())
             self.plot_selected.set_visible(True)
         self.canvas.draw_idle()
 
@@ -196,12 +200,12 @@ class PlotXY(PamhyrPlot):
             poly_r_x.append(ptY.x)
             poly_r_y.append(ptY.y)
 
-            #self.canvas.axes.plot(
-                #x, y, lw=1.,
-                #color='b',
-                #markersize=1,
-                #marker='o'
-            #)
+            # self.canvas.axes.plot(
+            #     x, y, lw=1.,
+            #     color='b',
+            #     markersize=1,
+            #     marker='o'
+            # )
 
         poly_x = poly_l_x + list(reversed(poly_r_x))
         poly_y = poly_l_y + list(reversed(poly_r_y))
diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py
index 75682ed557915fd4945b304e4e4e0e4b639b470a..75e95f2181feef09739553d0f8db434c98dcc2e9 100644
--- a/src/View/Results/Window.py
+++ b/src/View/Results/Window.py
@@ -97,7 +97,7 @@ class ResultsWindow(PamhyrWindow):
         self._additional_plot = {}
 
         self.setup_table()
-        self.setup_plot()
+        self.setup_plots()
         self.setup_slider()
         self.setup_statusbar()
         self.setup_connections()
@@ -125,9 +125,13 @@ class ResultsWindow(PamhyrWindow):
         self._slider_time.setValue(len(self._timestamps) - 1)
 
         self._icon_start = QIcon()
-        self._icon_start.addPixmap(QPixmap('./src/View/ui/ressources/media-playback-start.png'))
+        self._icon_start.addPixmap(
+            QPixmap('./src/View/ui/ressources/media-playback-start.png')
+        )
         self._icon_pause = QIcon()
-        self._icon_pause.addPixmap(QPixmap('./src/View/ui/ressources/media-playback-pause.png'))
+        self._icon_pause.addPixmap(
+            QPixmap('./src/View/ui/ressources/media-playback-pause.png')
+        )
         self._button_play = self.find(QPushButton, f"playButton")
         self._button_play.setIcon(self._icon_start)
         self._button_back = self.find(QPushButton, f"backButton")
@@ -136,7 +140,7 @@ class ResultsWindow(PamhyrWindow):
         self._button_last = self.find(QPushButton, f"lastButton")
         self._timer = QTimer(self)
 
-    def setup_plot(self):
+    def setup_plots(self):
         self.canvas = MplCanvas(width=5, height=4, dpi=100)
         self.canvas.setObjectName("canvas")
         self.toolbar = PamhyrPlotToolbar(
@@ -392,7 +396,7 @@ class ResultsWindow(PamhyrWindow):
             self.plot_xy.set_timestamp(timestamp)
             self.plot_ac.set_timestamp(timestamp)
             self.plot_kpc.set_timestamp(timestamp)
-            # self.plot_h.set_timestamp(timestamp)
+            self.plot_h.set_timestamp(timestamp)
 
             if self._study.river.has_sediment():
                 self.plot_sed_reach.set_timestamp(timestamp)
diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py
index 96ada16e0053d08a4bc0cd72621dc3f7c3432504..6067f5e12b9e21011b897dec7bdb13c151d7e710 100644
--- a/src/View/RunSolver/Window.py
+++ b/src/View/RunSolver/Window.py
@@ -74,10 +74,16 @@ class SelectSolverWindow(PamhyrDialog):
 
         self.setup_combobox()
         self.setup_connections()
+        self.select_last_solver()
 
     def setup_combobox(self):
         solvers = self._config.solvers
-        solvers_name = list(map(lambda s: s.name + f" - ({s._type})", solvers))
+        solvers_name = list(
+            map(
+                self._format_solver_name,
+                solvers
+            )
+        )
 
         self.combobox_add_items("comboBox", solvers_name)
 
@@ -86,6 +92,26 @@ class SelectSolverWindow(PamhyrDialog):
         self.find(QPushButton, "pushButton_cancel")\
             .clicked.connect(self.reject)
 
+    def select_last_solver(self):
+        solvers = self._config.solvers
+        last = self._config.last_solver_name
+
+        solver = list(
+            filter(
+                lambda s: s.name == last,
+                solvers
+            )
+        )
+
+        if len(solver) != 0:
+            self.set_combobox_text(
+                "comboBox",
+                self._format_solver_name(solver[0])
+            )
+
+    def _format_solver_name(self, solver):
+        return f"{solver.name} - ({solver._type})"
+
     @property
     def solver(self):
         return self._solver
@@ -94,6 +120,8 @@ class SelectSolverWindow(PamhyrDialog):
         solver_name = self.get_combobox_text("comboBox")
         solver_name = solver_name.rsplit(" - ", 1)[0]
 
+        self._config.update_last_solver_used(solver_name)
+
         self._solver = next(
             filter(
                 lambda s: s.name == solver_name,
@@ -124,40 +152,11 @@ class SolverLogWindow(PamhyrWindow):
         self.setup_action()
         self.setup_alarm()
         self.setup_connections()
+        self.setup_workdir()
+        self.setup_process()
 
-        self._workdir = ""
-        if self._study.filename == "":
-            self._workdir = tempfile.TemporaryDirectory()
-        else:
-            self._workdir = os.path.join(
-                os.path.dirname(self._study.filename),
-                "_PAMHYR_",
-                self._study.name.replace(" ", "_"),
-                self._solver.name.replace(" ", "_"),
-            )
-            os.makedirs(self._workdir, exist_ok=True)
-
-        self._alarm.start(500)
-        self._output = Queue()
-        self._process = self.new_process(parent)
-
-        self._log(f" *** Export study {self._solver.name}", color="blue")
-        self._solver.export(self._study, self._workdir, qlog=self._output)
-
-        self.update()
-
-        self._log(f" *** Run solver {self._solver.name}", color="blue")
-        self._solver.run(
-            study,
-            process=self._process,
-            output_queue=self._output
-        )
-
-    def new_process(self, parent):
-        new = QProcess(parent)
-        new.setWorkingDirectory(self._workdir)
-        new.setProcessChannelMode(QProcess.MergedChannels)
-        return new
+        self.export()
+        self.run()
 
     def setup_action(self):
         self.find(QAction, "action_start").setEnabled(False)
@@ -182,30 +181,74 @@ class SolverLogWindow(PamhyrWindow):
 
         self._alarm.timeout.connect(self.update)
 
+    def setup_workdir(self):
+        self._workdir = ""
+        if self._study.filename == "":
+            self._workdir = tempfile.TemporaryDirectory()
+        else:
+            self._workdir = os.path.join(
+                os.path.dirname(self._study.filename),
+                "_PAMHYR_",
+                self._study.name.replace(" ", "_"),
+                self._solver.name.replace(" ", "_"),
+            )
+            os.makedirs(self._workdir, exist_ok=True)
+
+    def setup_process(self):
+        self._alarm.start(500)
+        self._output = Queue()
+        self._process = self.new_process(self._parent)
+
+    def new_process(self, parent):
+        new = QProcess(parent)
+        new.setWorkingDirectory(self._workdir)
+        new.setProcessChannelMode(QProcess.MergedChannels)
+        return new
+
+    def export(self):
+        self._log(f" *** Export study {self._solver.name}", color="blue")
+        self._solver.export(self._study, self._workdir, qlog=self._output)
+        self.update()
+
+    #######
+    # LOG #
+    #######
+
     def _log(self, msg, color=None):
         if type(msg) is str:
-            logger.info(f"solver: {msg}")
+            self._log_str(msg, color)
+        elif type(msg) is int:
+            self._log_int(msg, color)
 
-            msg = msg.rsplit('\n')[0]
+    def _log_str(self, msg, color=None):
+        logger.info(f"solver: {msg}")
+        msg = msg.rsplit('\n')[0]
 
-            if color is not None:
-                msg = f"<font color=\"{color}\">" + msg + "</font>"
+        if color is not None:
+            msg = f"<font color=\"{color}\">" + msg + "</font>"
 
-            self.find(QTextEdit, "textEdit").append(msg)
-        elif type(msg) is int:
-            logger.info(f"solver: Returns {msg}")
+        self.find(QTextEdit, "textEdit").append(msg)
 
-            color = "blue" if msg == 0 else "red"
-            self.find(QTextEdit, "textEdit")\
-                .append(f"<font color=\"{color}\">" +
-                        f" *** Finished with code {msg}" +
-                        "</font>")
+    def _log_int(self, int_code, color=None):
+        logger.info(f"solver: Returns {int_code}")
+        color = "blue" if int_code == 0 else "red"
 
-            self.statusbar.showMessage(
-                "Done" if msg == 0 else "Failed",
-                3000
+        self.find(QTextEdit, "textEdit")\
+            .append(
+                f"<font color=\"{color}\">" +
+                f" *** Finished with code {int_code}" +
+                "</font>"
             )
 
+        self.statusbar.showMessage(
+            "Done" if int_code == 0 else "Failed",
+            3000
+        )
+
+    ##########
+    # UPDATE #
+    ##########
+
     def update(self):
         if self._solver.is_stoped():
             self.find(QAction, "action_start").setEnabled(True)
@@ -230,11 +273,24 @@ class SolverLogWindow(PamhyrWindow):
     def _update_logs_all(self):
         while self._output.qsize() != 0:
             s = self._output.get()
+
             if type(s) is str and "[ERROR]" in s:
                 self._log(s, color="red")
             else:
                 self._log(s)
 
+    ####################
+    # Process controle #
+    ####################
+
+    def run(self):
+        self._log(f" *** Run solver {self._solver.name}", color="blue")
+        self._solver.run(
+            self._study,
+            process=self._process,
+            output_queue=self._output
+        )
+
     def start(self):
         if self._solver.is_stoped():
             self._log(f" *** Export study {self._solver.name}", color="blue")
@@ -276,6 +332,10 @@ class SolverLogWindow(PamhyrWindow):
         if self._solver.log_file() != "":
             self.find(QAction, "action_log_file").setEnabled(True)
 
+    ###########
+    # Results #
+    ###########
+
     def results(self):
         if self._results is None:
             self._results = self._solver.results(
@@ -284,6 +344,8 @@ class SolverLogWindow(PamhyrWindow):
         self._parent.set_results(self._solver, self._results)
         self._parent.open_solver_results(self._solver, self._results)
 
+        self._solver.has_results_loaded()
+
     def log_file(self):
         file_name = os.path.join(self._workdir, self._solver.log_file())
         log = SolverLogFileWindow(
diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py
index 642fdc5760742eb3400c9ed818494249952e587e..5521402c2d6c9ccdec013bfaae088ceb53a03cba 100644
--- a/src/View/SolverParameters/translate.py
+++ b/src/View/SolverParameters/translate.py
@@ -129,4 +129,7 @@ class ParamTranslate(PamhyrTranslate):
             "mage_min_reach_volume_to_check":
             _translate("SolverParameters",
                        "Minimum reach volume to check"),
+            "mage_init_internal":
+            _translate("SolverParameters",
+                       "Use Mage internal initialization (Y/N)"),
         }
diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py
index e8b9c7f83791ae14e05671d78958a78771669461..bfb615d1ecb99fe853ffbc9f5565c04d38e9a55e 100644
--- a/src/View/Study/Window.py
+++ b/src/View/Study/Window.py
@@ -101,4 +101,5 @@ class NewStudyWindow(PamhyrDialog):
                 self._study.use_date(date)
             else:
                 self._study.use_time()
+
         self.done(True)
diff --git a/src/View/Tools/ASubWindow.py b/src/View/Tools/ASubWindow.py
index 8530c15c9e5849053c51ab0166fa9cec5b8fffa2..7ef5f964b341ec5b229ea179f48d441892bf1485 100644
--- a/src/View/Tools/ASubWindow.py
+++ b/src/View/Tools/ASubWindow.py
@@ -81,7 +81,7 @@ class WindowToolKit(object):
                 header = row.copy()
                 continue
 
-            values.append(list(filter(lambda s: s != '', row)))
+            values.append(row)
 
         return header, values
 
diff --git a/src/View/Tools/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py
index 7fe21bb7bfab7cf0b1a40d3b5fac61ca8fff8792..d0b833b8efa46a7179f86e746b66c6bcc0c1f98b 100644
--- a/src/View/Tools/ListedSubWindow.py
+++ b/src/View/Tools/ListedSubWindow.py
@@ -55,11 +55,12 @@ class ListedSubWindow(object):
         logger.info(f"Close window: ({h}) {self.sub_win_cnt}")
 
     def _sub_win_exists(self, h):
-        return reduce(
+        res = reduce(
             lambda acc, el: (acc or (h == (el[1].hash()))),
             self.sub_win_list,
             False
         )
+        return res
 
     def sub_win_exists(self, h):
         return self._sub_win_exists(h)
diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py
index b77e26b9ceab0afe487e1b83caaf28fd1a5af4dd..803e58421fd97a95bf68a03183df2e4e5281b9c8 100644
--- a/src/View/Tools/PamhyrTable.py
+++ b/src/View/Tools/PamhyrTable.py
@@ -83,7 +83,8 @@ class PamhyrTableModel(QAbstractTableModel):
                  trad=None,
                  data=None,
                  undo=None,
-                 opt_data=None):
+                 opt_data=None,
+                 parent=None):
         super(PamhyrTableModel, self).__init__()
 
         self._table_view = table_view
@@ -93,6 +94,7 @@ class PamhyrTableModel(QAbstractTableModel):
         self._editable_headers = editable_headers
         self._delegates = delegates
         self._trad = trad
+        self._parent = parent
 
         self._data = data
         self._opt_data = opt_data
@@ -119,8 +121,7 @@ class PamhyrTableModel(QAbstractTableModel):
 
         options = Qt.ItemIsEnabled | Qt.ItemIsSelectable
 
-        if (self._editable_headers or
-                self._headers[column] in self._editable_headers):
+        if self._headers[column] in self._editable_headers:
             options |= Qt.ItemIsEditable
 
         return options
diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py
index 74756167b65b01ebefb02a9f996095d28ced6923..87829b8dad058c0933a2285c5f2a273d950bf5e7 100644
--- a/src/View/Tools/PamhyrWindow.py
+++ b/src/View/Tools/PamhyrWindow.py
@@ -186,3 +186,8 @@ class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools):
         self._hash_data.append(self._config)
 
         self._set_title()
+
+    def done(self, result):
+        if self.parent is not None:
+            self.parent.sub_win_del(self.hash())
+        super(PamhyrDialog, self).done(result)
diff --git a/src/View/ui/BasicHydraulicStructures.ui b/src/View/ui/BasicHydraulicStructures.ui
new file mode 100644
index 0000000000000000000000000000000000000000..add59b7c8c5d4de7cdde19a8eaec49f1789c0e3a
--- /dev/null
+++ b/src/View/ui/BasicHydraulicStructures.ui
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Basic hydraulic structures</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0">
+     <widget class="QSplitter" name="splitter_2">
+      <property name="orientation">
+       <enum>Qt::Horizontal</enum>
+      </property>
+      <widget class="QWidget" name="verticalLayoutWidget_3">
+       <layout class="QVBoxLayout" name="verticalLayout_3">
+        <item>
+         <widget class="QTableView" name="tableView">
+          <property name="minimumSize">
+           <size>
+            <width>300</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="baseSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout">
+          <property name="leftMargin">
+           <number>5</number>
+          </property>
+          <item>
+           <widget class="QCheckBox" name="checkBox">
+            <property name="text">
+             <string>Enable / Disable basic hydraulic structure</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QSplitter" name="splitter">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <widget class="QWidget" name="verticalLayoutWidget">
+        <layout class="QVBoxLayout" name="verticalLayout"/>
+       </widget>
+       <widget class="QWidget" name="verticalLayoutWidget_2">
+        <layout class="QVBoxLayout" name="verticalLayout_2">
+         <item>
+          <widget class="QTableView" name="tableView_2"/>
+         </item>
+        </layout>
+       </widget>
+      </widget>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>800</width>
+     <height>22</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+  <widget class="QToolBar" name="toolBar">
+   <property name="windowTitle">
+    <string>toolBar</string>
+   </property>
+   <attribute name="toolBarArea">
+    <enum>TopToolBarArea</enum>
+   </attribute>
+   <attribute name="toolBarBreak">
+    <bool>false</bool>
+   </attribute>
+   <addaction name="action_add"/>
+   <addaction name="action_delete"/>
+  </widget>
+  <action name="action_add">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset>
+   </property>
+   <property name="text">
+    <string>Add</string>
+   </property>
+   <property name="toolTip">
+    <string>Add a new point</string>
+   </property>
+  </action>
+  <action name="action_delete">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset>
+   </property>
+   <property name="text">
+    <string>Delete</string>
+   </property>
+   <property name="toolTip">
+    <string>Delete points</string>
+   </property>
+  </action>
+  <action name="action_edit">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
+   </property>
+   <property name="text">
+    <string>Edit</string>
+   </property>
+   <property name="toolTip">
+    <string>Edit selected hydraulic structure</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/View/ui/GeometryReach.ui b/src/View/ui/GeometryReach.ui
index a11a2279caabc72fba0a586e430b2d86a9a60899..d18d0e14e34cb335b9b9f5ae31a99e023241bcb8 100644
--- a/src/View/ui/GeometryReach.ui
+++ b/src/View/ui/GeometryReach.ui
@@ -6,7 +6,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>868</width>
+    <width>1280</width>
     <height>720</height>
    </rect>
   </property>
@@ -101,7 +101,7 @@
     <rect>
      <x>0</x>
      <y>0</y>
-     <width>868</width>
+     <width>1280</width>
      <height>22</height>
     </rect>
    </property>
diff --git a/src/View/ui/HydraulicStructures.ui b/src/View/ui/HydraulicStructures.ui
new file mode 100644
index 0000000000000000000000000000000000000000..813da5b5f48ef45a2e14e4c473eb3263ed35f1b4
--- /dev/null
+++ b/src/View/ui/HydraulicStructures.ui
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Hydraulic structures</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0">
+     <widget class="QSplitter" name="splitter_2">
+      <property name="orientation">
+       <enum>Qt::Horizontal</enum>
+      </property>
+      <widget class="QWidget" name="verticalLayoutWidget_3">
+       <layout class="QVBoxLayout" name="verticalLayout_3">
+        <item>
+         <widget class="QTableView" name="tableView">
+          <property name="minimumSize">
+           <size>
+            <width>300</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="baseSize">
+           <size>
+            <width>0</width>
+            <height>0</height>
+           </size>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout">
+          <property name="leftMargin">
+           <number>5</number>
+          </property>
+          <item>
+           <widget class="QCheckBox" name="checkBox">
+            <property name="text">
+             <string>Enable / Disable hydraulic structure</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QSplitter" name="splitter">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <widget class="QWidget" name="verticalLayoutWidget">
+        <layout class="QVBoxLayout" name="verticalLayout"/>
+       </widget>
+       <widget class="QWidget" name="verticalLayoutWidget_2">
+        <layout class="QVBoxLayout" name="verticalLayout_2"/>
+       </widget>
+      </widget>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>800</width>
+     <height>22</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+  <widget class="QToolBar" name="toolBar">
+   <property name="windowTitle">
+    <string>toolBar</string>
+   </property>
+   <attribute name="toolBarArea">
+    <enum>TopToolBarArea</enum>
+   </attribute>
+   <attribute name="toolBarBreak">
+    <bool>false</bool>
+   </attribute>
+   <addaction name="action_add"/>
+   <addaction name="action_delete"/>
+   <addaction name="action_edit"/>
+  </widget>
+  <action name="action_add">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset>
+   </property>
+   <property name="text">
+    <string>Add</string>
+   </property>
+   <property name="toolTip">
+    <string>Add a new point</string>
+   </property>
+  </action>
+  <action name="action_delete">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset>
+   </property>
+   <property name="text">
+    <string>Delete</string>
+   </property>
+   <property name="toolTip">
+    <string>Delete points</string>
+   </property>
+  </action>
+  <action name="action_edit">
+   <property name="icon">
+    <iconset>
+     <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
+   </property>
+   <property name="text">
+    <string>Edit</string>
+   </property>
+   <property name="toolTip">
+    <string>Edit selected hydraulic structure</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui
index e4d00954ccfff566746cfb1c98d94be09c52d6b1..bf73c3d3cb232511420f01a2fd72ad73438b235e 100644
--- a/src/View/ui/MainWindow.ui
+++ b/src/View/ui/MainWindow.ui
@@ -132,6 +132,7 @@
     <addaction name="action_menu_edit_friction"/>
     <addaction name="action_menu_edit_lateral_contribution"/>
     <addaction name="action_menu_edit_reservoirs"/>
+    <addaction name="action_menu_edit_hydraulic_structures"/>
    </widget>
    <widget class="QMenu" name="menu_results">
     <property name="title">
@@ -934,6 +935,14 @@
     <string>Edit reservoirs</string>
    </property>
   </action>
+  <action name="action_menu_edit_hydraulic_structures">
+   <property name="text">
+    <string>Hydraulic structures</string>
+   </property>
+   <property name="toolTip">
+    <string>Edit hydraulic structures</string>
+   </property>
+  </action>
  </widget>
  <resources/>
  <connections>
diff --git a/src/config.py b/src/config.py
index c8613a42323b307d0a0395d698644ca0b9d3b905..e5c314c3b96a0654507d0e640433c9be66a2cba7 100644
--- a/src/config.py
+++ b/src/config.py
@@ -35,7 +35,7 @@ logger = logging.getLogger()
 
 class Config(SQL):
     def __init__(self):
-        self._version = '0.0.3'
+        self._version = '0.0.4'
         self.filename = Config.filename()
         self.set_default_value()
 
@@ -113,11 +113,17 @@ class Config(SQL):
                         ''
                       )
                     """)
+
                 if int(release) < 3:
                     self.execute(f"INSERT INTO data VALUES ('last_study', '')")
                     self.execute(
                         f"INSERT INTO data VALUES ('close_correctly', 'True')")
 
+                if int(release) < 4:
+                    self.execute(
+                        f"INSERT INTO data VALUES ('last_solver_name', '')"
+                    )
+
             self.commit()
 
     def _load_solver(self):
@@ -195,6 +201,10 @@ class Config(SQL):
         v = self.execute("SELECT value FROM data WHERE key='close_correctly'")
         self.close_correctly = v[0] == "True"
 
+        # Last Solver
+        v = self.execute("SELECT value FROM data WHERE key='last_solver_name'")
+        self.last_solver_name = v[0]
+
         # Debug
         v = self.execute("SELECT value FROM data WHERE key='debug'")
         self.debug = v[0] == "True"
@@ -250,6 +260,7 @@ class Config(SQL):
             "last_study": self.last_study,
             "close_correctly": self.close_correctly,
             "debug": self.debug,
+            "last_solver_name": self.last_solver_name,
         }
 
         for key in data:
@@ -306,6 +317,9 @@ class Config(SQL):
         self.last_study = ""
         self.close_correctly = False
 
+        # Last Solver
+        self.last_solver_name = ""
+
         # Debug
         self.debug = False
 
@@ -332,6 +346,14 @@ class Config(SQL):
         )
         self.commit()
 
+    def update_last_solver_used(self, solver_name):
+        self.last_solver_name = solver_name
+        self.execute(
+            "UPDATE data SET " +
+            f"value='{self._db_format(self.last_solver_name)}' " +
+            "WHERE key='last_solver_name'"
+        )
+
     @classmethod
     def filename(cls):
         file = ""
diff --git a/src/tools.py b/src/tools.py
index b6f78857a90de16178ce21d680047c6015e70b99..1ad8c382ec46c63e5881573f43eafe014bfbc30c 100644
--- a/src/tools.py
+++ b/src/tools.py
@@ -67,6 +67,7 @@ def logger_color_reset():
         return f"{Style.RESET_ALL}"
     return ""
 
+
 def logger_exception(exception):
     logger.error(
         f"[{Fore.RED}ERROR{Style.RESET_ALL}] " +
diff --git a/tests.sh b/tests.sh
index 06c8fd846e45c5ffa156fde542901a4387b12e23..ff235027a698f756112e4ffbdf28d8a90505a863 100755
--- a/tests.sh
+++ b/tests.sh
@@ -16,7 +16,7 @@ cd ..
 
 echo " PEP8"
 
-pycodestyle ./src
+pycodestyle --exclude="*_to_*.py" ./src
 
 if [ $? -eq 0 ]
 then