Repository: snek5000/snek5000 (language: python)

Issue:
Saving (and plot) of an estimation of remaining_clock_time
From my experience, the figure obtained in fluidsim with `sim.output.print_stdout.plot_clock_times()` is VERY useful.

The related code is in https://foss.heptapod.net/fluiddyn/fluidsim/-/blob/branch/default/fluidsim/base/output/print_stdout.py (methods `_evaluate_duration_left` and of course `plot_clock_times`).

The logic of these methods should also applies for Nek5000.

So we could move some code in `fluidsim_core` and use it for snek.

Note that we don't need to write that for each time step and we don't need to write that in stdout (it can be another file with for some time steps `it, time, dt, remaining_clock_time`).

Related to #81 and #121


Discussion / hints:
Also this detail while deciding end of sim: https://github.com/snek5000/snek5000/pull/121#issuecomment-995789318
@ashwinvis it would be great if you could work on the Fortran/Nek5000 part of this one (because I would struggle to do it :-)). Then, I will be able to work on the Python/fluidsim-core part.

It seems to me that 

- It can first be implemented in our tgv solver.
- We can use some `remaining-clock-time.csv` files in the session directories.
- A reasonable condition to print a new line in these files could be based on the real clock time, for example every 10 s, and I tend to think that we don't really need a user parameter for this (?).
I'm going to work on this one so that @ashwinvis you can focus on #179!

Fix patch:
diff --git a/docs/examples/snek5000-tgv/src/snek5000_tgv/output.py b/docs/examples/snek5000-tgv/src/snek5000_tgv/output.py
index 596eb23f..33c8c51d 100644
--- a/docs/examples/snek5000-tgv/src/snek5000_tgv/output.py
+++ b/docs/examples/snek5000-tgv/src/snek5000_tgv/output.py
@@ -30,5 +30,13 @@ def _set_info_solver_classes(cls, classes):
             ),
         )
 
+        classes._set_child(
+            "RemainingClockTime",
+            dict(
+                module_name="snek5000.output.remaining_clock_time",
+                class_name="RemainingClockTime",
+            ),
+        )
+
 
 Output = OutputTGV
diff --git a/docs/examples/snek5000-tgv/src/snek5000_tgv/tgv.usr.f b/docs/examples/snek5000-tgv/src/snek5000_tgv/tgv.usr.f
index f774a7ca..fcdeb657 100644
--- a/docs/examples/snek5000-tgv/src/snek5000_tgv/tgv.usr.f
+++ b/docs/examples/snek5000-tgv/src/snek5000_tgv/tgv.usr.f
@@ -100,16 +100,24 @@ subroutine userchk()
       include 'SIZE'
       include 'TOTAL'
 
-      character*80 fnames(3)
-      character*128 file_name
+c      character*80 fnames(3)
+      ! for spatial_means
+      character*28 file_name
       logical exist
       real e1, e2
       real period_save, t_last_save
       save t_last_save
+      ! for remaining_clock_time
+      character*28 rct_file_name
+      real rct_seconds_between_save
+      real remaining_clock_time
 
       period_save = real(UPARAM(11))
 
+      rct_seconds_between_save = real(UPARAM(12))
+
       file_name = 'spatial_means.csv'
+      rct_file_name = 'remaining_clock_time.csv'
 
       if (istep.eq.0) then
          inquire(file=file_name, exist=exist)
@@ -119,7 +127,15 @@ subroutine userchk()
                write(10,'(a)') 'time,energy,enstrophy'
                close(10)
             endif
-          endif
+         endif
+         inquire(file=rct_file_name, exist=exist)
+         if (.not. exist) then
+            if (nid.eq.0) then
+               open(11, File=rct_file_name)
+               write(11,'(a)') 'it,time,dt,remaining_clock_time'
+               close(11)
+            endif
+         endif
       endif
 
 c      if (.false.) then
@@ -133,19 +149,26 @@ subroutine userchk()
 c      iostep_full = iostep
 c      call full_restart_save(iostep_full)
 
-      if ((istep.eq.1) .or. (istep.eq.lastep) .or. (floor(time
-     &/period_save)).gt.(floor(t_last_save/period_save))) then
+      if ((nid .eq. 0) .and. (istep > 0)) then
+         call compute_remaining_clock_time(
+     &       rct_seconds_between_save, remaining_clock_time)
+         if (remaining_clock_time >= 0.0) then
+            open(10, File=rct_file_name, position='append')
+            write(10,'(I12,A,g14.8,A,g14.8,A,g14.8)')
+     &          istep,',',time,',',dt,',',remaining_clock_time
+            close(10)
+         endif
+      endif
 
+      if ((istep.eq.1) .or. (lastep .eq. 1) .or. (floor(time
+     &/period_save)).gt.(floor(t_last_save/period_save))) then
          t_last_save = time
-
          call compute_energy_enstrophy(e1,e2,vx,vy,vz)
-
          if (nid.eq.0) then
             open(10, File=file_name, position='append')
             write(10,'(g14.8,A,g14.8,A,g14.8)') time,',',e1,',',e2
             close(10)
          endif
-
       endif
 
       return
@@ -217,3 +240,55 @@ subroutine compute_energy_enstrophy(e1,e2,u,v,w)
       return
       end
 c-----------------------------------------------------------------------
+      subroutine compute_remaining_clock_time(
+     &    seconds_between_save, remaining_clock_time)
+      implicit none
+
+      include 'SIZE'
+      include 'TOTAL'
+
+      real seconds_between_save
+      real remaining_clock_time
+      real remaining_eq_time
+
+      real clock_time, delta_clock_time, delta_eq_time
+
+      real clock_time_last, eq_time_last
+      save clock_time_last, eq_time_last
+
+      integer istep_next, istep_last
+      save istep_next
+
+      remaining_clock_time = -1.0
+      if (istep .eq. 1) then
+        call cpu_time(clock_time_last)
+        eq_time_last = time
+        istep_last = 1
+        istep_next = 2
+      elseif ((istep .eq. istep_next) .or. (lastep .eq. 1)) then
+        call cpu_time(clock_time)
+        delta_clock_time = clock_time - clock_time_last
+        clock_time_last = clock_time
+
+        delta_eq_time = time - eq_time_last
+        eq_time_last = time
+
+        if (fintim > 0.0) then
+          remaining_eq_time = fintim - time
+        else
+          remaining_eq_time = (nsteps - istep) * dt
+        endif
+        istep_next = istep + int((istep - istep_last) *
+     &      seconds_between_save / delta_clock_time)
+        if (istep_next .eq. istep) then
+          istep_next = istep_next + 1
+        endif
+        istep_last = istep
+        remaining_clock_time = remaining_eq_time / delta_eq_time *
+     &      delta_clock_time
+        if (remaining_clock_time < 0.0) then
+          remaining_clock_time = 0.0
+        endif
+      endif
+
+      end subroutine
diff --git a/src/snek5000/output/remaining_clock_time.py b/src/snek5000/output/remaining_clock_time.py
new file mode 100644
index 00000000..8e44c6e6
--- /dev/null
+++ b/src/snek5000/output/remaining_clock_time.py
@@ -0,0 +1,12 @@
+"""Plot remaining clock time
+
+"""
+
+from .simple_csv import OutputWithCsvFileAndParam
+
+
+class RemainingClockTime(OutputWithCsvFileAndParam):
+    INDEX_USERPARAM = 12
+    _tag = "remaining_clock_time"
+    _param_name = "period_save_in_seconds"
+    _param_default_value = 5.0
diff --git a/src/snek5000/output/simple_csv.py b/src/snek5000/output/simple_csv.py
new file mode 100644
index 00000000..b18617f2
--- /dev/null
+++ b/src/snek5000/output/simple_csv.py
@@ -0,0 +1,30 @@
+"""Simple specific output with a csv file and a user parameter."""
+
+from abc import ABCMeta
+from pathlib import Path
+
+import pandas as pd
+
+
+class OutputWithCsvFileAndParam(metaclass=ABCMeta):
+    INDEX_USERPARAM: int
+    _tag: str
+    _param_name: str
+    _param_default_value: object
+
+    def __init__(self, output=None) -> None:
+        self.output = output
+        self.path_file = Path(output.path_run) / (self._tag + ".csv")
+
+    @classmethod
+    def _complete_params_with_default(cls, params):
+        params.output._set_child(
+            cls._tag,
+            attribs={cls._param_name: cls._param_default_value},
+        )
+        params.output[cls._tag]._record_nek_user_params(
+            {cls._param_name: cls.INDEX_USERPARAM}
+        )
+
+    def load(self):
+        return pd.read_csv(self.path_file)
diff --git a/src/snek5000/output/spatial_means.py b/src/snek5000/output/spatial_means.py
index f56a7e72..f6c13556 100644
--- a/src/snek5000/output/spatial_means.py
+++ b/src/snek5000/output/spatial_means.py
@@ -1,30 +1,13 @@
 """Load and parse spatial means data."""
 
-from pathlib import Path
+from .simple_csv import OutputWithCsvFileAndParam
 
-import pandas as pd
 
-
-class SpatialMeans:
-    INDEX_USERPARAM_SPATIAL_MEANS = 11
+class SpatialMeans(OutputWithCsvFileAndParam):
+    INDEX_USERPARAM = 11
     _tag = "spatial_means"
-
-    def __init__(self, output=None) -> None:
-        self.output = output
-        self.path_file = Path(output.path_run) / (self._tag + ".csv")
-
-    @classmethod
-    def _complete_params_with_default(cls, params):
-        params.output._set_child(
-            cls._tag,
-            attribs={"write_interval": 1.0},
-        )
-        params.output.spatial_means._record_nek_user_params(
-            {"write_interval": cls.INDEX_USERPARAM_SPATIAL_MEANS}
-        )
-
-    def load(self):
-        return pd.read_csv(self.path_file)
+    _param_name = "write_interval"
+    _param_default_value = 1.0
 
     def plot(self, **kwargs):
         df = self.load()


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index af8c189a..58d534ad 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -49,6 +49,11 @@ def pytest_collection_modifyitems(config, items):
             item.add_marker(skip_slow)
 
 
+def modif_test_params(params):
+    params.output.sub_directory = "tests_snek5000"
+    params.oper.nproc_min = 2
+
+
 @pytest.fixture(scope="session")
 def jinja_env():
     import jinja2
@@ -65,7 +70,7 @@ def sim():
     from phill.solver import Simul
 
     params = Simul.create_default_params()
-    params.output.sub_directory = "test_snek5000"
+    modif_test_params(params)
 
     params.nek.general.stop_at = "numSteps"
     params.nek.general.num_steps = 9
@@ -117,8 +122,7 @@ def sim2d():
     from phill.solver import Simul
 
     params = Simul.create_default_params()
-    params.output.sub_directory = "test_snek5000"
-
+    modif_test_params(params)
     set_params_oper2d(params)
     return Simul(params)
 
@@ -128,8 +132,7 @@ def sim_canonical():
     from snek5000_canonical.solver import Simul
 
     params = Simul.create_default_params()
-    params.output.sub_directory = "test_snek5000"
-
+    modif_test_params(params)
     return Simul(params)
 
 
@@ -138,13 +141,12 @@ def sim_executed():
     from phill.solver import Simul
 
     params = Simul.create_default_params()
-    params.output.sub_directory = "test_snek5000"
+    modif_test_params(params)
 
     params.nek.general.stop_at = "endTime"
     params.nek.general.end_time = 10 * abs(params.nek.general.dt)
     params.nek.general.write_interval = 5
 
-    params.oper.nproc_min = 2
     params.oper.nproc_max = 12
     params.oper.nx = params.oper.ny = params.oper.nz = 3
     params.oper.elem.order = params.oper.elem.order_out = 8
@@ -172,15 +174,14 @@ def sim_cbox_executed():
     from snek5000_cbox.solver import Simul
 
     params = Simul.create_default_params()
+    modif_test_params(params)
     params.Ra_side = 1.83e08
-    params.output.sub_directory = "test_snek5000"
 
     params.nek.general.stop_at = "numSteps"
     params.nek.general.dt = 1e-3
     params.nek.general.num_steps = 12
     params.nek.general.write_interval = 3
 
-    params.oper.nproc_min = 2
     params.oper.nproc_max = 12
     params.oper.dim = 2
     params.oper.nx = params.oper.ny = 8
@@ -234,11 +235,10 @@ def sim_tgv_executed():
     from snek5000_tgv.solver import Simul
 
     params = Simul.create_default_params()
-    params.output.sub_directory = "examples_snek/tuto"
+    modif_test_params(params)
 
     params.oper.nx = params.oper.ny = params.oper.nz = 6
     params.oper.elem.order = params.oper.elem.order_out = 6
-    params.oper.nproc_min = 2
 
     params.nek.velocity.residual_tol = 1e-07
     params.nek.pressure.residual_tol = 1e-05
@@ -252,6 +252,7 @@ def sim_tgv_executed():
     params.nek.general.write_interval = 0.5
 
     params.output.spatial_means.write_interval = 0.5
+    params.output.remaining_clock_time.period_save_in_seconds = 1.0
 
     sim = Simul(params)
     if not sim.make.exec("run_fg", nproc=2):
diff --git a/tests/test_oper/test_size_template.f b/tests/test_oper/test_size_template.f
index 5f59df9d..b5bde36d 100644
--- a/tests/test_oper/test_size_template.f
+++ b/tests/test_oper/test_size_template.f
@@ -20,7 +20,7 @@
       parameter (lx2=4)                 ! GLL points for pressure (lx1 or lx1-2)
 
       parameter (lelg=8192)             ! max total number of elements
-      parameter (lpmin=8)               ! min MPI ranks
+      parameter (lpmin=2)               ! min MPI ranks
       parameter (lpmax=32)              ! max MPI ranks
       parameter (ldimt=1)               ! max auxiliary fields (temperature + scalars)
 
@@ -40,7 +40,7 @@
       parameter (lelx=32)               ! global tensor mesh dimensions
       parameter (lely=16)
       parameter (lelz=32)
-      parameter (lelt=1024)             ! max number of local elements per MPI rank
+      parameter (lelt=4096)             ! max number of local elements per MPI rank
       parameter (lbelt=1)               ! set to lelt for mhd
       parameter (lpelt=1)               ! set to lelt for linear stability
       parameter (lcvelt=1)              ! set to lelt for cvode
diff --git a/tests/test_tgv.py b/tests/test_tgv.py
index d95e9522..d0808c42 100644
--- a/tests/test_tgv.py
+++ b/tests/test_tgv.py
@@ -10,3 +10,7 @@ def test_tgv(sim_tgv_executed):
     assert isinstance(df, pd.DataFrame)
     assert len(df) > 2
     sim.output.spatial_means.plot()
+
+    df = sim.output.remaining_clock_time.load()
+    assert isinstance(df, pd.DataFrame)
+    assert len(df) >= 2