diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py
index 0c6414786ad8ba7efb6cc2bcf9e873887a717970..26cdefc920dd9f81877c561d792eda2595d7f655 100644
--- a/src/Model/Geometry/Reach.py
+++ b/src/Model/Geometry/Reach.py
@@ -42,7 +42,7 @@ class Reach(SQLSubModel):
     ]
 
     def __init__(self, status=None, parent=None):
-        self.id = parent.id
+        self.id = parent.id if parent is not None else 0
         self._status = status
         self._parent = parent
         self._profiles: List[Profile] = []
diff --git a/src/Scripts/Hello.py b/src/Scripts/Hello.py
index b1c27fe5d6d0625f0b6cdbd01e10aff1f4333bc6..c7333b8452c79933b62e92c5d4839ff25ba0e3c4 100644
--- a/src/Scripts/Hello.py
+++ b/src/Scripts/Hello.py
@@ -1,4 +1,4 @@
-# help.py -- Pamhyr
+# Hello.py -- Pamhyr
 # Copyright (C) 2023  INRAE
 #
 # This program is free software: you can redistribute it and/or modify
diff --git a/src/Scripts/P3DST.py b/src/Scripts/P3DST.py
new file mode 100644
index 0000000000000000000000000000000000000000..55a9992876927ea46647144612ddb393c770ed08
--- /dev/null
+++ b/src/Scripts/P3DST.py
@@ -0,0 +1,115 @@
+# P3DST.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 sys
+import logging
+
+from matplotlib import pyplot as plt
+from numpy import mean
+
+from Scripts.AScript import AScript
+
+from Model.Saved import SavedStatus
+from Model.Geometry.Reach import Reach
+
+logger = logging.getLogger()
+
+class Script3DST(AScript):
+    name = "3DST"
+    description = "Display a 3D plot of a river reach from ST file"
+
+    def usage(self):
+        logger.info(f"Usage : {self._args[0]} 3DST <INPUT_ST_FILE>")
+
+    def set_axes_equal(self, ax):
+        """Make axes of 3D plot have equal scale
+
+        Make axes of 3D plot have equal scale so that spheres appear
+        as spheres, cubes as cubes, etc.. This is one possible
+        solution to Matplotlib's ax.set_aspect('equal') and
+        ax.axis('equal') not working for 3D.
+
+        Args:
+            ax: a matplotlib axis, e.g., as output from plt.gca()
+
+        Returns:
+            The input axis
+        """
+        x_limits = ax.get_xlim3d()
+        y_limits = ax.get_ylim3d()
+        z_limits = ax.get_zlim3d()
+
+        x_range = abs(x_limits[1] - x_limits[0])
+        x_middle = mean(x_limits)
+        y_range = abs(y_limits[1] - y_limits[0])
+        y_middle = mean(y_limits)
+        z_range = abs(z_limits[1] - z_limits[0])
+        z_middle = mean(z_limits)
+
+        # The plot bounding box is a sphere in the sense of the infinity
+        # norm, hence I call half the max range the plot radius.
+        plot_radius = 0.5*max([x_range, y_range, z_range])
+
+        ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
+        ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
+        ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
+
+        return ax
+
+    def run(self):
+        try:
+            st_file = self._args[2]
+            logger.info(f"Use ST file: {st_file}")
+        except Exception as e:
+            logger.error(f"Arguments parcing: {str(e)}")
+            return 1
+
+        try:
+            status = SavedStatus()
+
+            my_reach = Reach(status = status)
+            my_reach.import_geometry(st_file)
+            my_reach.compute_guidelines()
+
+            ax = plt.figure().add_subplot(projection="3d")
+            logger.info(my_reach.get_x())
+            for x, y, z in zip(
+                    my_reach.get_x(),
+                    my_reach.get_y(),
+                    my_reach.get_z()
+            ):
+                ax.plot(x, y, z, color='r', lw=1.)
+
+            for x, y, z in zip(
+                    my_reach.get_guidelines_x(),
+                    my_reach.get_guidelines_y(),
+                    my_reach.get_guidelines_z()
+            ):
+                ax.plot(x, y, z, color='b', lw=1.)
+
+            ax.set_xlabel('X')
+            ax.set_ylabel('Y')
+            ax.set_zlabel('Z')
+            plt.tight_layout()
+            self.set_axes_equal(ax)
+            plt.show()
+
+            return 0
+        except Exception as e:
+            logger.error(str(e))
+            return 1
diff --git a/src/Scripts/plot_3DST.py b/src/Scripts/plot_3DST.py
deleted file mode 100644
index bcadf1f188ff0b922fa66fff282dc6f26035d073..0000000000000000000000000000000000000000
--- a/src/Scripts/plot_3DST.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# plot_3DST.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 -*-
-
-# a lancer depuis src
-import sys
-from matplotlib import pyplot as plt
-from Model.Geometry.Reach import Reach
-from numpy import mean
-
-def set_axes_equal(ax):
-    '''Make axes of 3D plot have equal scale so that spheres appear as spheres,
-    cubes as cubes, etc..  This is one possible solution to Matplotlib's
-    ax.set_aspect('equal') and ax.axis('equal') not working for 3D.
-
-    Input
-      ax: a matplotlib axis, e.g., as output from plt.gca().
-    '''
-
-    x_limits = ax.get_xlim3d()
-    y_limits = ax.get_ylim3d()
-    z_limits = ax.get_zlim3d()
-
-    x_range = abs(x_limits[1] - x_limits[0])
-    x_middle = mean(x_limits)
-    y_range = abs(y_limits[1] - y_limits[0])
-    y_middle = mean(y_limits)
-    z_range = abs(z_limits[1] - z_limits[0])
-    z_middle = mean(z_limits)
-
-    # The plot bounding box is a sphere in the sense of the infinity
-    # norm, hence I call half the max range the plot radius.
-    plot_radius = 0.5*max([x_range, y_range, z_range])
-
-    ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
-    ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
-    ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
-
-st_file = sys.argv[1]
-my_reach = Reach(None)
-my_reach.import_geometry(st_file)
-my_reach.compute_guidelines()
-
-ax = plt.figure().add_subplot(projection="3d")
-for x, y, z in zip(my_reach.get_x(), my_reach.get_y(), my_reach.get_z()):
-    ax.plot(x, y, z, color='r', lw=1.)
-for x, y, z in zip(my_reach.get_guidelines_x(), my_reach.get_guidelines_y(), my_reach.get_guidelines_z()):
-    ax.plot(x, y, z, color='b', lw=1.)
-ax.set_xlabel('X')
-ax.set_ylabel('Y')
-ax.set_zlabel('Z')
-plt.tight_layout()
-set_axes_equal(ax)
-plt.show()
diff --git a/src/pamhyr.py b/src/pamhyr.py
index b4bd401f0f75b5e67dc29d38e6409702c2d3dbe2..0d595df4bb3b9590770cc777df559f482d2eaef9 100755
--- a/src/pamhyr.py
+++ b/src/pamhyr.py
@@ -34,6 +34,7 @@ from tools import (
 from View.MainWindow import ApplicationWindow
 from Model.Study import Study
 
+from Scripts.P3DST import Script3DST
 from Scripts.Hello import ScriptHello
 
 from init import license, setup_lang
@@ -42,6 +43,7 @@ logger = logging.getLogger()
 
 scripts = {
     "hello": ScriptHello,
+    "3DST": Script3DST,
 }
 
 def usage(argv):