diff --git a/src/Model/Geometry/Geometry.py b/src/Model/Geometry/Geometry.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce977efa1dfad3f89db070466b241e657ae98b58
--- /dev/null
+++ b/src/Model/Geometry/Geometry.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+
+import numpy as np
+
+from time import time
+from typing import List
+from copy import deepcopy
+from operator import itemgetter
+
+from Model.Geometry.PointXYZ import PointXYZ
+from Model.Geometry.ProfileXYZ import ProfileXYZ
+
+# TypeProfileXYZ = List[ProfileXYZ]
+
+
+class Geometry:
+    """
+    Reach geometry
+    """
+    def __init__(self, parent=None):
+        self.parent = parent
+        self._list_profiles: List[ProfileXYZ] = []
+        self.file_st = ""
+        self.__list_copied_profiles = []
+
+    def __repr__(self):
+        return f"\n===== Début liste des profils ======> {np.array(self._list_profiles)}" \
+               f"\n<====== Fin liste des profils ====="
+
+    @property
+    def number_profiles(self):
+        """
+        Returns:
+            Number of profiles
+        """
+        return len(self._list_profiles)
+
+    def get_geometry(self) -> List[ProfileXYZ]:
+        """
+        Returns:
+            The profiles list.
+        """
+        return self._list_profiles
+
+    def get_profile_i(self, i: int) -> ProfileXYZ:
+        """
+        Args:
+            i: Index
+        Returns:
+            The profile at index i.
+        """
+        try:
+            return self._list_profiles[i]
+        except IndexError:
+            raise IndexError(f"Le bief a moins de {i} profil(s)")
+
+    def import_geometry(self, file_path_name: str):
+        """Import a geometry from file (.ST or .st)
+
+        Args:
+            file_path_name: The absolute path of geometry file (.ST or .st) to import.
+
+        Returns:
+            Nothing.
+        """
+        self.file_st = str(file_path_name)
+        list_profile, list_header = self.read_file_st()
+       # print(list_profile, "\n", list_header)
+        if list_profile and list_header:
+            for ind, profile in enumerate(list_profile):
+                prof = ProfileXYZ(*list_header[ind])
+                prof.import_points(profile)
+                self._list_profiles.append(prof)
+            self._update_profile_numbers()
+
+        else:
+            print("Fichier introuvable ou non conforme !")
+
+    def add(self):
+        """Add a new profile at the end of profiles list
+
+        Returns:
+            Nothing.
+        """
+        nb_profile = self.number_profiles
+        profile = ProfileXYZ()
+        profile.num = nb_profile + 1
+        self._list_profiles.append(profile)
+
+    def _update_profile_numbers(self):
+        """Update profiles index
+
+        Returns:
+            Nothing.
+        """
+        for ind, profile in enumerate(self.get_geometry()):
+            profile.num = ind + 1
+
+    def insert(self, index: int):
+        """Insert new profile in list
+
+        Args:
+            index: The position of the new profile.
+
+        Returns:
+            Nothing.
+        """
+        profile = ProfileXYZ()
+        self._list_profiles.insert(index, profile)
+        self._update_profile_numbers()
+
+    def delete(self, list_index: list):
+        """Delete some elements in profile list
+
+        Args:
+            list_index: The list of element index
+
+        Returns:
+            Nothing.
+        """
+        try:
+            if list_index:
+                indices = sorted(list(set(list_index)), reverse=True)
+                for idx in indices:
+                    # if idx < len(self._list_profiles) :
+                    try:
+                        self._list_profiles.pop(idx)
+                        self._update_profile_numbers()
+                    except IndexError:
+                        print("Liste vide, rien à supprimer !")
+        except TypeError:
+            if isinstance(list_index, int):
+                self._list_profiles.pop(list_index)
+                self._update_profile_numbers()
+                print(f"\nSuppression --> attention !!!!\nL'argument {list_index} doit être une liste!\n")
+            else:
+                raise TypeError(f"L'argument {list_index} fourni est de type incorrect")
+
+    def _sort(self, is_reversed: bool = False):
+        self._list_profiles = sorted(
+            self._list_profiles,
+            key=lambda profile: profile.pk,
+            reverse=is_reversed
+        )
+
+    def sort_ascending(self):
+        """Sort profiles by increasing KP
+
+        Returns:
+            Nothing.
+        """
+        self._sort(is_reversed=False)
+
+    def sort_descending(self):
+        """Sort profiles by decreasing KP
+
+        Returns:
+            Nothing.
+        """
+        self._sort(is_reversed=True)
+
+    def copy(self, index_list: List[int]):
+        self.__list_copied_profiles.clear()
+        index_list = list(set(index_list))  # delete duplicate index
+        for index in index_list:
+            try:
+                self.__list_copied_profiles.append(deepcopy(self.get_profile_i(index)))
+            except IndexError:
+                raise IndexError(f"Echec de la copie, l'indice {index} n'existe pas !")
+
+    def paste(self):
+        if self.__list_copied_profiles:
+            for profile in self.__list_copied_profiles:
+                self._list_profiles.append(profile)
+        print("self.__list_copied_profiles", self.__list_copied_profiles, "\n *****")
+
+    def read_file_st(self):
+        """Read the ST file
+
+        Returns:
+            List of profiles and list of headers.
+        """
+        t0 = time()
+        line_is_header = True
+        list_point_profile = []
+        list_profile = []
+        list_header = []
+        stop_code = "999.999"
+        try:
+            with open(self.file_st, encoding="utf-8") as file_st:
+                for line in file_st:
+                    if not (line.startswith("#") or line.startswith("*")):
+                        line = line.split()
+                        if line_is_header:
+                            if len(line) >= 6:
+                                list_header.append(line[:6])
+                            elif len(line) == 5:
+                                line.append("")
+                                list_header.append(line)
+                            else:
+                                print(f"Point {line} invalide ==> pas pris en compte")
+                            line_is_header = False
+                        else:
+                            # Read until "999.9990     999.9990" as found
+                            if len(line) == 3:
+                                x, y, z = line
+                                if stop_code in x and stop_code in y:
+                                    line_is_header = True
+                                    list_profile.append(list_point_profile)
+                                    list_point_profile = []
+                                else:
+                                    line.append("")
+                                    list_point_profile.append(line)
+                            elif len(line) == 4:
+                                x, y, z, ld = line
+                                if stop_code in x and stop_code in y:
+                                    list_profile.append(list_point_profile)
+                                    list_point_profile = []
+                                    line_is_header = True
+                                else:
+                                    list_point_profile.append(line)
+                            else:
+                                pass
+
+        except FileNotFoundError:
+            print(f"\n \n %%%%%% Fichier : {self.file_st} introuvable !! %%%%%%")
+        print("******  Fichier {} lu et traité en {} secondes *******".format(self.file_st, time() - t0))
+        return list_profile, list_header
diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py
new file mode 100644
index 0000000000000000000000000000000000000000..14c29bebc55c968d3bd3040e1798b5e402420994
--- /dev/null
+++ b/src/Model/Geometry/PointXYZ.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+
+from math import dist
+from pandas import isna as pd_is_na
+
+class PointXYZ:
+    def __init__(self, x: float, y: float, z: float, point_name: str = ""):
+        self.x = float(x)
+        self.y = float(y)
+        self.z = float(z)
+        self.name = point_name
+        self.points = [self.x, self.y, self.z, self.name]
+
+    def __repr__(self):
+        point_xyz_name = f"({self.x}, {self.y},{self.z}, {self.name})"
+        return point_xyz_name
+
+    @property
+    def x(self):
+        return self._x
+
+    @x.setter
+    def x(self, value):
+        self._x = float(value)
+
+    @property
+    def y(self):
+        return self._y
+
+    @y.setter
+    def y(self, value):
+        self._y = float(value)
+
+    # self.points[1] = self._y
+
+    @property
+    def z(self):
+        return self._z
+
+    @z.setter
+    def z(self, value):
+        self._z = float(value)
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, point_name):
+        self._name = point_name
+
+    def point_is_named(self):
+        """
+        Returns:
+            True if the point is named.
+        """
+        return len(self.name.strip()) != 0
+
+    @property
+    def is_nan(self):
+        """
+        Returns:
+            True if at least one coordinate is as np.nan
+        """
+        return pd_is_na(self.x) or pd_is_na(self.y) or pd_is_na(self.z)
+
+    @staticmethod
+    def distance(p1, p2):
+        """Euclidean distance between p1 and p2.
+
+        Args:
+            p1: A XYZ Point
+            p2: A XYZ Point
+
+        Returns:
+            Euclidean distance between the two points
+        """
+        return dist((p1.x, p1.y, p1.z), (p2.x, p2.y, p2.z))
diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3fc6883b61e8d3f85f392f8fed2901e77f8f1f6
--- /dev/null
+++ b/src/Model/Geometry/ProfileXYZ.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import pandas as pd
+from typing import List
+
+from Model.Geometry.PointXYZ import PointXYZ
+
+class ProfileXYZ:
+    def __init__(self, num: int = 0, code1: int = 0, code2: int = 0,
+                 nb_points: int = 0, kp: float = 0., name: str = ""):
+        """ProfileXYZ constructor
+
+        Args:
+            num: The number of this profile
+            code1: The interpolation code 1
+            code2: The interpolation code 2
+            nb_points: Number of points
+            kp: Kilometer point
+            name: The name of profile
+
+        Returns:
+            Nothing.
+        """
+        self._num = int(num)
+        self._code1 = int(code1)
+        self._code2 = int(code2)
+        self._nb_points = int(nb_points)
+        self._kp = float(kp)
+        self._name = str(name)
+        self._list_points: List[PointXYZ] = []
+
+    def __repr__(self):
+        df = pd.DataFrame(columns=["X", "Y", "Z", "Name"],
+                          data=[[p.x, p.y, p.z, p.name] for p in self._list_points])
+        return f"\n{self.header}\n{df}"
+
+    @property
+    def num(self):
+        """
+        Returns:
+            Number of profile.
+        """
+        return self._num
+
+    @num.setter
+    def num(self, value: int):
+        self._num = int(value)
+
+    @property
+    def code1(self):
+        """
+        Returns:
+            Interpolation code 1.
+        """
+        return self._code1
+
+    @code1.setter
+    def code1(self, value: int):
+        self._code1 = int(value)
+
+    @property
+    def code2(self):
+        """
+        Returns:
+            Interpolation code 2.
+        """
+        return self._code2
+
+    @code2.setter
+    def code2(self, value: int):
+        self._code2 = int(value)
+
+    @property
+    def nb_points(self):
+        return self._nb_points
+
+    # @nb_points.setter
+    # def nb_points(self, nb: int):
+    #     self._nb_points = int(nb)
+
+    @property
+    def kp(self):
+        """
+        Returns:
+            Kilometer point.
+        """
+        return self._kp
+
+    @kp.setter
+    def kp(self, value: float):
+        self._kp = float(value)
+
+    @property
+    def name(self):
+        """
+        Returns:
+            Profile name.
+        """
+        return self._name
+
+    @name.setter
+    def name(self, other: str):
+        self._name = other
+
+    @property
+    def header(self):
+        """
+        Returns:
+            Profile header.
+        """
+        return np.array([self._num, self._code1, self._code2, self._nb_points, self.kp, self._name])
+
+    def import_points(self, list_points: list):
+        """Import a list of points to profile
+
+        Args:
+            list_points: Liste of PointXYZ
+
+        Returns:
+            Nothing.
+        """
+        for point in list_points:
+            pt = PointXYZ(*point)
+            self._list_points.append(pt)
+
+    def get_point_i(self, index: int) -> PointXYZ:
+        """Get point at index.
+
+        Args:
+            index: Index of point.
+
+        Returns:
+            The point.
+        """
+        try:
+            return self._list_points[index]
+        except IndexError:
+            raise IndexError(f"Le profil a moins de {index} points !")
+
+    def add(self):
+        """Add a new PointXYZ to profile.
+
+        Returns:
+            Nothing.
+        """
+        point_xyz = PointXYZ(0., 0., 0.)
+        self._list_points.append(point_xyz)
+
+    def delete(self, index: int):
+        """Delete the point at index
+
+        Args:
+            index: Index of point.
+
+        Returns:
+            Nothing.
+        """
+        try:
+            self._list_points.pop(index)
+        except IndexError:
+            raise IndexError(f"Suppression échouée, l'indice {index} n'existe pas !")
+
+    def insert(self, index: int):
+        """Insert a new profile at index.
+
+        Args:
+            index: The index of new profile.
+
+        Returns:
+            Nothing.
+        """
+        profile = ProfileXYZ()
+        self._list_points.insert(index, profile)
+
+    def delete1(self, list_index: list):
+        """Delete a list of points
+
+        Args:
+            list_index: Indexes list.
+
+        Returns:
+            Nothing.
+        """
+        try:
+            if list_index:
+                indices = sorted(list(set(list_index)), reverse=True)
+                for idx in indices:
+                    # if idx < len(self._list_profiles) :
+                    try:
+                        self._list_points.pop(idx)
+                    except IndexError:
+                        print("Liste vide, rien à supprimer !")
+        except TypeError:
+            if isinstance(list_index, int):
+                self._list_points.pop(list_index)
+                print(f"\nSuppression --> attention !!!!\nL'argument {list_index} doit être une liste!\n")
+            else:
+                raise TypeError(f"L'argument {list_index} fourni est de type incorrect")
diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fe06ce5318bced623331de884aa66318c90e891
--- /dev/null
+++ b/src/Model/Geometry/Reach.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+
+from Model.Geometry.Geometry import Geometry
+
+class Reach:
+    def __init__(self, name: str = "",
+                 upstream_node: str = None,
+                 downstream_node: str = None,
+                 parent=None):
+        self._name = name
+        self._name_upstream_node = name_upstream_node
+        self._name_downstream_node = name_downstream_node
+        self.parent = parent
+        self._geometry = Geometry(parent=self)
+
+    def __repr__(self):
+        return f"Bief : {self._name}\n Nb de sections : {self._geometry.number_profiles}"
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def name_upstream_node(self):
+        return self._name_upstream_node
+
+    @property
+    def name_downstream_node(self):
+        return self._name_downstream_node
+
+    @property
+    def geometry(self):
+        return self._geometry