diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 863629f7..dd8178d4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,8 +16,9 @@
stages:
- downloads
- - build
+ - configure
- test
+ - build
- package
- release
@@ -59,12 +60,12 @@ dl-mage-windows:
- mage-windows/mage_extraire.exe
- mage-windows/mailleurPF.exe
-#########
-# BUILD #
-#########
+#############
+# CONFIGURE #
+#############
set-version:
- stage: build
+ stage: configure
tags:
- linux
script:
@@ -75,7 +76,7 @@ set-version:
- VERSION
build-lang:
- stage: build
+ stage: configure
tags:
- linux
script:
@@ -85,6 +86,49 @@ build-lang:
paths:
- src/lang/*.qm
+#########
+# TESTS #
+#########
+
+unittest:
+ stage: test
+ tags:
+ - linux
+ needs:
+ - job: set-version
+ artifacts: true
+ script:
+ - python3 -m venv venv
+ - . venv/bin/activate
+ - pip3 install -U pip
+ - pip3 install -r ./full-requirements.txt
+ - pip3 install -U -r ./full-requirements.txt
+ - cd src
+ - python3 -m unittest discover -t .
+
+test-pep8:
+ stage: test
+ tags:
+ - linux
+ needs:
+ - job: set-version
+ artifacts: true
+ - job: unittest
+ script:
+ # Setup virtual env
+ - python3 -m venv venv
+ - . venv/bin/activate
+ - pip3 install -U pip
+ - pip3 install -r ./requirements.txt
+ - pip3 install -U -r ./requirements.txt
+ - pip3 install pycodestyle
+ - pycodestyle ./src
+ allow_failure: true
+
+#########
+# BUILD #
+#########
+
build-users-doc:
stage: build
tags:
@@ -242,56 +286,6 @@ build-windows:
paths:
- windows/pamhyr
-#########
-# TESTS #
-#########
-
-test-pep8:
- stage: test
- tags:
- - linux
- needs:
- - job: set-version
- artifacts: true
- script:
- - mkdir -p pep8
- - cd pep8
- # Setup virtual env
- - python3 -m venv venv
- - . venv/bin/activate
- - pip3 install -U pip
- - pip3 install -r ../requirements.txt
- - pip3 install -U -r ../requirements.txt
- - pip3 install pycodestyle
- - pycodestyle ../src
- allow_failure: true
-
-# test-windows:
-# stage: test
-# tags:
-# - wine
-# needs:
-# - job: set-version
-# artifacts: true
-# - job: build-windows
-# artifacts: true
-# script:
-# - cd windows\pamhyr
-# - pamhyr\pamhyr.exe hello
-
-# test-linux:
-# stage: test
-# tags:
-# - linux
-# needs:
-# - job: set-version
-# artifacts: true
-# - job: build-linux
-# artifacts: true
-# script:
-# - cd linux/pamhyr
-# - ./Pamhyr2 hello
-
############
# PACKAGES #
############
diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py
index 38cb9646..c997960f 100644
--- a/src/Model/InitialConditions/InitialConditions.py
+++ b/src/Model/InitialConditions/InitialConditions.py
@@ -174,7 +174,7 @@ class Data(SQLSubModel):
def _update_from_kp(self):
min = self._update_get_min()
- self._elevation = min - self._height
+ self._elevation = min + self._height
def _update_from_elevation(self):
min = self._update_get_min()
diff --git a/src/Model/Study.py b/src/Model/Study.py
index 1369e269..c6b482e2 100644
--- a/src/Model/Study.py
+++ b/src/Model/Study.py
@@ -290,3 +290,11 @@ class Study(SQLModel):
self._save_submodel([self._river])
self.commit()
+
+ def close(self):
+ """Close db connection
+
+ Returns:
+ Nothing.
+ """
+ self._close()
diff --git a/src/Model/__init__.py b/src/Model/__init__.py
new file mode 100644
index 00000000..89e3fbe8
--- /dev/null
+++ b/src/Model/__init__.py
@@ -0,0 +1,17 @@
+# __init__.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 .
+
+# -*- coding: utf-8 -*-
diff --git a/src/Model/test_Model.py b/src/Model/test_Model.py
new file mode 100644
index 00000000..2694366f
--- /dev/null
+++ b/src/Model/test_Model.py
@@ -0,0 +1,110 @@
+# test_Model.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 .
+
+# -*- coding: utf-8 -*-
+
+import os
+import unittest
+import tempfile
+
+from Model.Saved import SavedStatus
+from Model.Study import Study
+from Model.River import River
+
+
+class StudyTestCase(unittest.TestCase):
+ def test_create_study(self):
+ study = Study.new("foo", "bar")
+ self.assertEqual(study.name, "foo")
+ self.assertEqual(study.description, "bar")
+
+ def test_open_study(self):
+ study = Study.open("../tests_cases/Enlargement/Enlargement.pamhyr")
+ self.assertNotEqual(study, None)
+ self.assertEqual(study.name, "Enlargement")
+
+ def test_save_open_study(self):
+ study = Study.new("foo", "bar")
+ dir = tempfile.mkdtemp()
+ f = os.path.join(dir, "foo.pamhyr")
+
+ # Save study
+ study.filename = f
+ study.save()
+ study.close()
+
+ # Reopen study
+ study = Study.open(f)
+
+ # Check
+ self.assertNotEqual(study, None)
+ self.assertEqual(study.name, "foo")
+ self.assertEqual(study.description, "bar")
+
+ def test_create_study_river(self):
+ study = Study.new("foo", "bar")
+ self.assertNotEqual(study.river, None)
+
+
+class RiverTestCase(unittest.TestCase):
+ def test_create_river(self):
+ status = SavedStatus()
+ river = River(status=status)
+
+ self.assertNotEqual(river, None)
+
+ def test_create_river_nodes(self):
+ status = SavedStatus()
+ river = River(status=status)
+
+ self.assertNotEqual(river, None)
+
+ # Add nodes
+ n0 = river.add_node()
+ n1 = river.add_node(x=1.0, y=0.0)
+ n2 = river.add_node(x=0.0, y=1.0)
+
+ # Checks
+ self.assertEqual(river.nodes_counts(), 3)
+
+ nodes = river.nodes()
+ self.assertEqual(nodes[0], n0)
+ self.assertEqual(nodes[1], n1)
+ self.assertEqual(nodes[2], n2)
+
+ def test_create_river_edges(self):
+ status = SavedStatus()
+ river = River(status=status)
+
+ self.assertNotEqual(river, None)
+
+ # Add nodes
+ n0 = river.add_node()
+ n1 = river.add_node(x=1.0, y=0.0)
+ n2 = river.add_node(x=0.0, y=1.0)
+
+ self.assertEqual(river.nodes_counts(), 3)
+
+ # Add edges
+ e0 = river.add_edge(n0, n1)
+ e1 = river.add_edge(n1, n2)
+
+ # Checks
+ self.assertEqual(river.edges_counts(), 2)
+
+ edges = river.edges()
+ self.assertEqual(edges[0], e0)
+ self.assertEqual(edges[1], e1)
diff --git a/src/Scripts/ListSolver.py b/src/Scripts/ListSolver.py
new file mode 100644
index 00000000..fe50d8d9
--- /dev/null
+++ b/src/Scripts/ListSolver.py
@@ -0,0 +1,38 @@
+# ListSolver.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 .
+
+# -*- coding: utf-8 -*-
+
+import os
+import logging
+
+from Scripts.AScript import AScript
+
+logger = logging.getLogger()
+
+
+class ScriptListSolver(AScript):
+ name = "ListSolver"
+ description = "List configured solver(s) for Pamhyr2"
+
+ def usage(self):
+ logger.info(f"Usage : {self._args[0]} {self._args[1]}")
+
+ def run(self):
+ for solver in self._conf.solvers:
+ print(f"{solver.name:<16} ({solver.type}): {solver.description}")
+
+ return 0
diff --git a/src/Scripts/Run.py b/src/Scripts/Run.py
new file mode 100644
index 00000000..7b70a910
--- /dev/null
+++ b/src/Scripts/Run.py
@@ -0,0 +1,113 @@
+# Run.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 .
+
+# -*- coding: utf-8 -*-
+
+import os
+import logging
+
+from queue import Queue
+
+from PyQt5.QtCore import QProcess
+
+from Scripts.AScript import AScript
+from Model.Study import Study
+
+logger = logging.getLogger()
+
+
+class ScriptRun(AScript):
+ name = "Run"
+ description = "Run solver on Pamhyr2 a study"
+
+ def usage(self):
+ logger.info(
+ f"Usage : {self._args[0]} {self._args[1]} " +
+ " "
+ )
+
+ def run(self):
+ if len(self._args) < 4:
+ return 1
+
+ command = self._args[1]
+ solver_name = self._args[2]
+ study_file = os.path.abspath(
+ self._args[3]
+ )
+
+ try:
+ solver = next(
+ filter(
+ lambda solver: solver.name == solver_name,
+ self._conf.solvers
+ )
+ )
+ except Exception as e:
+ logger.error(f"No solver found: {e}")
+ return 2
+
+ study = Study.open(study_file)
+
+ self._solver = solver
+ self._study = study
+
+ logger.info(
+ f"Run {solver.name} ({solver.type}) " +
+ "on study '{study.name}' ({study_file})"
+ )
+
+ # Workdir
+ workdir = os.path.join(
+ os.path.dirname(study.filename),
+ "_PAMHYR_",
+ study.name.replace(" ", "_"),
+ solver.name.replace(" ", "_")
+ )
+ os.makedirs(workdir, exist_ok=True)
+ logger.info(f"Set working dir to {workdir}")
+
+ # Preparate process
+ p = QProcess(None)
+ p.setWorkingDirectory(workdir)
+
+ self._q = Queue()
+
+ # Export and Run
+ logger.info(f"~Export~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
+ solver.export(study, workdir, qlog=self._q)
+
+ while self._q.qsize() != 0:
+ s = self._q.get()
+ logger.info(s)
+
+ if command == "run":
+ logger.info(f"~Run~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
+ solver.run(
+ study,
+ process=p,
+ output_queue=self._q
+ )
+ p.waitForFinished()
+
+ logger.info(f"~End~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
+
+ return 0
+
+
+class ScriptExport(ScriptRun):
+ name = "Export"
+ description = "Export Pamhyr2 study for solver"
diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py
index 0603a5ab..c4fa286a 100644
--- a/src/Solver/ASolver.py
+++ b/src/Solver/ASolver.py
@@ -59,17 +59,6 @@ class AbstractSolver(object):
self._name = name
self._description = ""
- self._path_input = ""
- self._path_solver = ""
- self._path_output = ""
-
- self._cmd_input = ""
- self._cmd_solver = ""
- self._cmd_output = ""
-
- self._process = None
- self._output = None
-
# Last study running
self._study = None
@@ -92,7 +81,6 @@ class AbstractSolver(object):
("all_init_time", "000:00:00:00"),
("all_final_time", "999:99:00:00"),
("all_timestep", "300.0"),
- ("all_command_line_arguments", ""),
]
return lst
@@ -141,18 +129,6 @@ class AbstractSolver(object):
def description(self, description):
self._description = description
- def set_input(self, path, cmd):
- self._path_input = path
- self._cmd_input = cmd
-
- def set_solver(self, path, cmd):
- self._path_solver = path
- self._cmd_solver = cmd
-
- def set_output(self, path, cmd):
- self._path_output = path
- self._cmd_output = cmd
-
##########
# Export #
##########
@@ -160,41 +136,6 @@ class AbstractSolver(object):
def export(self, study, repertory, qlog=None):
raise NotImplementedMethodeError(self, self.export)
- def cmd_args(self, study):
- """Return solver command line arguments list
-
- Returns:
- Command line arguments list
- """
- params = study.river.get_params(self.type)
- args = params.get_by_key("all_command_line_arguments")
-
- return args.split(" ")
-
- def input_param(self):
- """Return input command line parameter(s)
-
- Returns:
- Returns input parameter(s) string
- """
- raise NotImplementedMethodeError(self, self.input_param)
-
- def output_param(self):
- """Return output command line parameter(s)
-
- Returns:
- Returns output parameter(s) string
- """
- raise NotImplementedMethodeError(self, self.output_param)
-
- def log_file(self):
- """Return log file name
-
- Returns:
- Returns log file name as string
- """
- raise NotImplementedMethodeError(self, self.log_file)
-
###########
# RESULTS #
###########
@@ -208,192 +149,17 @@ class AbstractSolver(object):
# Run #
#######
- def _install_dir(self):
- return os.path.abspath(
- os.path.join(
- os.path.dirname(__file__),
- "..", ".."
- )
- )
-
- def _format_command(self, study, cmd, path=""):
- """Format command line
-
- Args:
- cmd: The command line
- path: Optional path string (replace @path in cmd)
-
- Returns:
- The executable and list of arguments
- """
- # HACK: Works in most case... Trust me i'm an engineer
- cmd = cmd.replace("@install_dir", self._install_dir())
- cmd = cmd.replace("@path", path.replace(" ", "%20"))
- cmd = cmd.replace("@input", self.input_param())
- cmd = cmd.replace("@output", self.output_param())
- cmd = cmd.replace("@dir", self._process.workingDirectory())
- cmd = cmd.replace("@args", " ".join(self.cmd_args(study)))
-
- logger.debug(f"! {cmd}")
-
- if cmd[0] == "\"":
- # Command line executable path is between " char
- cmd = cmd.split("\"")
- exe = cmd[1].replace("%20", " ")
- args = list(
- filter(
- lambda s: s != "",
- "\"".join(cmd[2:]).split(" ")[1:]
- )
- )
- else:
- # We suppose the command line executable path as no space char
- cmd = cmd.replace("\\ ", "&_&").split(" ")
- exe = cmd[0].replace("&_&", " ")
- args = list(
- filter(
- lambda s: s != "",
- map(lambda s: s.replace("&_&", " "), cmd[1:])
- )
- )
-
- logger.info(f"! {exe} {args}")
- return exe, args
-
- def run_input_data_fomater(self, study):
- if self._cmd_input == "":
- self._run_next(study)
- return True
-
- cmd = self._cmd_input
- exe, args = self._format_command(study, cmd, self._path_input)
-
- if not os.path.exists(exe):
- error = f"[ERROR] Path {exe} do not exists"
- logger.info(error)
- return error
-
- self._process.start(
- exe, args,
- )
-
- return True
-
- def run_solver(self, study):
- if self._cmd_solver == "":
- self._run_next(study)
- return True
-
- cmd = self._cmd_solver
- exe, args = self._format_command(study, cmd, self._path_solver)
-
- if not os.path.exists(exe):
- error = f"[ERROR] Path {exe} do not exists"
- logger.info(error)
- return error
-
- self._process.start(
- exe, args,
- )
-
- self._status = STATUS.RUNNING
- return True
-
- def run_output_data_fomater(self, study):
- if self._cmd_output == "":
- self._run_next(study)
- return True
-
- cmd = self._cmd_output
- exe, args = self._format_command(study, cmd, self._path_output)
-
- if not os.path.exists(exe):
- error = f"[ERROR] Path {exe} do not exists"
- logger.info(error)
- return error
-
- self._process.start(
- exe, args,
- )
-
- return True
-
- def _data_ready(self):
- 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)
-
- self._run_next(study)
-
- def run(self, study, process=None, output_queue=None):
- if process is not None:
- self._process = process
- if output_queue is not None:
- self._output = output_queue
-
- self._process.readyRead.connect(self._data_ready)
- self._process.finished.connect(
- lambda c, s: self._finished(study, c, s))
-
- self._runs = [
- self.run_input_data_fomater,
- self.run_solver,
- self.run_output_data_fomater,
- ]
- self._step = 0
- # Run first step
- res = self._runs[0](study)
- if res is not True:
- self._output.put(res)
+ def run(self, study):
+ raise NotImplementedMethodeError(self, self.run)
def kill(self):
- if self._process is None:
- return True
-
- self._process.kill()
- self._status = STATUS.STOPED
- return True
+ raise NotImplementedMethodeError(self, self.kill)
def start(self, study, process=None):
- if _signal:
- # Solver is PAUSED, so continue execution
- if self._status == STATUS.PAUSED:
- os.kill(self._process.pid(), SIGCONT)
- self._status = STATUS.RUNNING
- return True
-
- self.run(study, process)
- return True
+ raise NotImplementedMethodeError(self, self.start)
def pause(self):
- if _signal:
- if self._process is None:
- return False
-
- # Send SIGSTOP to PAUSED solver
- os.kill(self._process.pid(), SIGSTOP)
- self._status = STATUS.PAUSED
- return True
- return False
+ raise NotImplementedMethodeError(self, self.pause)
def stop(self):
- if self._process is None:
- return False
-
- self._process.terminate()
- self._status = STATUS.STOPED
- return True
+ raise NotImplementedMethodeError(self, self.stop)
diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py
new file mode 100644
index 00000000..f7f5627c
--- /dev/null
+++ b/src/Solver/CommandLine.py
@@ -0,0 +1,309 @@
+# CommandLine.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 .
+
+# -*- coding: utf-8 -*-
+
+import os
+import logging
+
+from tools import timer, parse_command_line
+
+try:
+ # Installation allow Unix-like signal
+ from signal import SIGTERM, SIGSTOP, SIGCONT
+ _signal = True
+except Exception:
+ _signal = False
+
+from enum import Enum
+
+from Model.Except import NotImplementedMethodeError
+
+from Model.Results.Results import Results
+from Model.Results.River.River import River, Reach, Profile
+
+from Solver.ASolver import AbstractSolver, STATUS
+
+logger = logging.getLogger()
+
+
+class CommandLineSolver(AbstractSolver):
+ _type = ""
+
+ def __init__(self, name):
+ super(CommandLineSolver, self).__init__(name)
+
+ self._current_process = None
+ self._status = STATUS.NOT_LAUNCHED
+
+ self._path_input = ""
+ self._path_solver = ""
+ self._path_output = ""
+
+ self._cmd_input = ""
+ self._cmd_solver = ""
+ self._cmd_output = ""
+
+ self._process = None
+ self._output = None
+
+ # Last study running
+ self._study = None
+
+ @classmethod
+ def default_parameters(cls):
+ lst = super(CommandLineSolver, cls).default_parameters()
+
+ lst += [
+ ("all_command_line_arguments", ""),
+ ]
+
+ return lst
+
+ def set_input(self, path, cmd):
+ self._path_input = path
+ self._cmd_input = cmd
+
+ def set_solver(self, path, cmd):
+ self._path_solver = path
+ self._cmd_solver = cmd
+
+ def set_output(self, path, cmd):
+ self._path_output = path
+ self._cmd_output = cmd
+
+ ##########
+ # Export #
+ ##########
+
+ def cmd_args(self, study):
+ """Return solver command line arguments list
+
+ Returns:
+ Command line arguments list
+ """
+ params = study.river.get_params(self.type)
+ args = params.get_by_key("all_command_line_arguments")
+
+ return args.split(" ")
+
+ def input_param(self):
+ """Return input command line parameter(s)
+
+ Returns:
+ Returns input parameter(s) string
+ """
+ raise NotImplementedMethodeError(self, self.input_param)
+
+ def output_param(self):
+ """Return output command line parameter(s)
+
+ Returns:
+ Returns output parameter(s) string
+ """
+ raise NotImplementedMethodeError(self, self.output_param)
+
+ def log_file(self):
+ """Return log file name
+
+ Returns:
+ Returns log file name as string
+ """
+ raise NotImplementedMethodeError(self, self.log_file)
+
+ #######
+ # Run #
+ #######
+
+ def _install_dir(self):
+ return os.path.abspath(
+ os.path.join(
+ os.path.dirname(__file__),
+ "..", ".."
+ )
+ )
+
+ def _format_command(self, study, cmd, path=""):
+ """Format command line
+
+ Args:
+ cmd: The command line
+ path: Optional path string (replace @path in cmd)
+
+ Returns:
+ The executable and list of arguments
+ """
+ cmd = cmd.replace("@install_dir", self._install_dir())
+ cmd = cmd.replace("@path", "\"" + path + "\"")
+ cmd = cmd.replace("@input", self.input_param())
+ cmd = cmd.replace("@output", self.output_param())
+ cmd = cmd.replace("@dir", self._process.workingDirectory())
+ cmd = cmd.replace("@args", " ".join(self.cmd_args(study)))
+
+ logger.debug(f"! {cmd}")
+
+ words = parse_command_line(cmd)
+ exe = words[0]
+ args = words[1:]
+
+ logger.info(f"! {exe} {args}")
+ return exe, args
+
+ def run_input_data_fomater(self, study):
+ if self._cmd_input == "":
+ self._run_next(study)
+ return True
+
+ cmd = self._cmd_input
+ exe, args = self._format_command(study, cmd, self._path_input)
+
+ if not os.path.exists(exe):
+ error = f"[ERROR] Path {exe} do not exists"
+ logger.info(error)
+ return error
+
+ self._process.start(
+ exe, args,
+ )
+
+ return True
+
+ def run_solver(self, study):
+ if self._cmd_solver == "":
+ self._run_next(study)
+ return True
+
+ cmd = self._cmd_solver
+ exe, args = self._format_command(study, cmd, self._path_solver)
+
+ if not os.path.exists(exe):
+ error = f"[ERROR] Path {exe} do not exists"
+ logger.info(error)
+ return error
+
+ self._process.start(
+ exe, args,
+ )
+ self._process.waitForStarted()
+
+ self._status = STATUS.RUNNING
+ return True
+
+ def run_output_data_fomater(self, study):
+ if self._cmd_output == "":
+ self._run_next(study)
+ return True
+
+ cmd = self._cmd_output
+ exe, args = self._format_command(study, cmd, self._path_output)
+
+ if not os.path.exists(exe):
+ error = f"[ERROR] Path {exe} do not exists"
+ logger.info(error)
+ return error
+
+ self._process.start(
+ exe, args,
+ )
+
+ 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)
+
+ self._run_next(study)
+
+ def run(self, study, process=None, output_queue=None):
+ self._study = study
+
+ # Replace old values if needed
+ if process is not None:
+ self._process = process
+ if output_queue is not None:
+ self._output = output_queue
+
+ # Connect / reconnect signal
+ self._process.readyRead.connect(self._data_ready)
+ self._process.finished.connect(
+ lambda c, s: self._finished(study, c, s))
+
+ # Prepare running step
+ self._runs = [
+ self.run_input_data_fomater,
+ self.run_solver,
+ self.run_output_data_fomater,
+ ]
+ self._step = 0
+
+ # Run first step
+ res = self._runs[0](study)
+ if res is not True:
+ self._output.put(res)
+
+ def kill(self):
+ if self._process is None:
+ return True
+
+ self._process.kill()
+ self._status = STATUS.STOPED
+ return True
+
+ def start(self, study, process=None):
+ if _signal:
+ # Solver is PAUSED, so continue execution
+ if self._status == STATUS.PAUSED:
+ os.kill(self._process.pid(), SIGCONT)
+ self._status = STATUS.RUNNING
+ return True
+
+ self.run(study, process)
+ return True
+
+ def pause(self):
+ if _signal:
+ if self._process is None:
+ return False
+
+ # Send SIGSTOP to PAUSED solver
+ os.kill(self._process.pid(), SIGSTOP)
+ self._status = STATUS.PAUSED
+ return True
+ return False
+
+ def stop(self):
+ if self._process is None:
+ return False
+
+ self._process.terminate()
+ self._status = STATUS.STOPED
+ return True
diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py
index bc5a9f6b..f8ff31aa 100644
--- a/src/Solver/GenericSolver.py
+++ b/src/Solver/GenericSolver.py
@@ -16,12 +16,10 @@
# -*- coding: utf-8 -*-
-from Solver.ASolver import (
- AbstractSolver, STATUS
-)
+from Solver.CommandLine import CommandLineSolver
-class GenericSolver(AbstractSolver):
+class GenericSolver(CommandLineSolver):
_type = "generic"
def __init__(self, name):
diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py
index 4330736e..b6e586f8 100644
--- a/src/Solver/Mage.py
+++ b/src/Solver/Mage.py
@@ -22,7 +22,7 @@ import numpy as np
from tools import timer
-from Solver.ASolver import AbstractSolver
+from Solver.CommandLine import CommandLineSolver
from Checker.Mage import MageNetworkGraphChecker
from Model.Results.Results import Results
@@ -41,7 +41,7 @@ def mage_file_open(filepath, mode):
return f
-class Mage(AbstractSolver):
+class Mage(CommandLineSolver):
_type = "mage"
def __init__(self, name):
@@ -125,7 +125,6 @@ class Mage(AbstractSolver):
qlog.put("Export ST file")
os.makedirs(os.path.join(repertory, "net"), exist_ok=True)
- gra_file = f"{name}.GRA"
# Write header
edges = study.river.edges()
@@ -149,72 +148,83 @@ class Mage(AbstractSolver):
cnt_num = 1
for profile in edge.reach.profiles:
- num = f"{cnt_num:>6}"
- c1 = f"{profile.code1:>6}"
- c2 = f"{profile.code2:>6}"
- t = f"{len(profile.points):>6}"
- kp = f"{profile.kp:>12f}"[0:12]
- pname = profile.name
- if profile.name == "":
- # Generate name from profile id prefixed with
- # 'p' (and replace space char with '0' char)
- pname = f"p{profile.id:>3}".replace(" ", "0")
- name = f"{pname:<19}"
-
- # Generate sediment additional data if available
- sediment = ""
- if profile.sl is not None:
- if not any(filter(lambda f: ".GRA" in f, files)):
- files.append(gra_file)
-
- # Number of layers
- nl = len(profile.sl)
- sediment = f" {nl:>3}"
-
- # Layers data
- for layer in profile.sl.layers:
- sediment += (
- f" {layer.height:>10} {layer.d50:>10} " +
- f"{layer.sigma:>10} " +
- f"{layer.critical_constraint:>10}"
- )
-
- # Profile header line
- f.write(f"{num}{c1}{c2}{t} {kp} {name} {sediment}\n")
+ self._export_ST_profile_header(
+ f, files, profile, cnt_num
+ )
cnt_num += 1
# Points
for point in profile.points:
- x = f"{point.x:<12f}"[0:12]
- y = f"{point.y:<12f}"[0:12]
- z = f"{point.z:<12f}"[0:12]
- n = f"{point.name:<3}"
-
- # Generate sediment additional data if available
- sediment = ""
- prev = point.z
- if point.sl is not None:
- # Number of layers
- nl = len(point.sl)
- sediment = f"{nl:>3}"
-
- # Layers data
- for layer in point.sl.layers:
- prev = round(prev - layer.height, 5)
- sediment += (
- f" {prev:>10} {layer.d50:>10} " +
- f"{layer.sigma:>10} " +
- f"{layer.critical_constraint:>10}"
- )
-
- # Point line
- f.write(f"{x} {y} {z}{n} {sediment}\n")
+ self._export_ST_point_line(
+ f, files, point
+ )
# Profile last line
f.write(f" 999.9990 999.9990 999.9990\n")
return files
+ def _export_ST_profile_header(self, wfile, files,
+ profile, cnt):
+ num = f"{cnt:>6}"
+ c1 = f"{profile.code1:>6}"
+ c2 = f"{profile.code2:>6}"
+ t = f"{len(profile.points):>6}"
+ kp = f"{profile.kp:>12f}"[0:12]
+ pname = profile.name
+ if profile.name == "":
+ # Generate name from profile id prefixed with
+ # 'p' (and replace space char with '0' char)
+ pname = f"p{profile.id:>3}".replace(" ", "0")
+ name = f"{pname:<19}"
+
+ # Generate sediment additional data if available
+ sediment = ""
+ if profile.sl is not None:
+ if not any(filter(lambda f: ".GRA" in f, files)):
+ files.append(self._gra_file)
+
+ # Number of layers
+ nl = len(profile.sl)
+ sediment = f" {nl:>3}"
+
+ # Layers data
+ for layer in profile.sl.layers:
+ sediment += (
+ f" {layer.height:>10} {layer.d50:>10} " +
+ f"{layer.sigma:>10} " +
+ f"{layer.critical_constraint:>10}"
+ )
+
+ # Profile header line
+ wfile.write(f"{num}{c1}{c2}{t} {kp} {name} {sediment}\n")
+
+ def _export_ST_point_line(self, wfile, files, point):
+ x = f"{point.x:<12f}"[0:12]
+ y = f"{point.y:<12f}"[0:12]
+ z = f"{point.z:<12f}"[0:12]
+ n = f"{point.name:<3}"
+
+ # Generate sediment additional data if available
+ sediment = ""
+ prev = point.z
+ if point.sl is not None:
+ # Number of layers
+ nl = len(point.sl)
+ sediment = f"{nl:>3}"
+
+ # Layers data
+ for layer in point.sl.layers:
+ prev = round(prev - layer.height, 5)
+ sediment += (
+ f" {prev:>10} {layer.d50:>10} " +
+ f"{layer.sigma:>10} " +
+ f"{layer.critical_constraint:>10}"
+ )
+
+ # Point line
+ wfile.write(f"{x} {y} {z} {n} {sediment}\n")
+
@timer
def _export_BC(self, t, bounds, repertory, qlog, name="0"):
files = []
@@ -429,6 +439,10 @@ class Mage(AbstractSolver):
self._study = study
name = study.name.replace(" ", "_")
+ # Define GRA file name
+ self._gra_file = f"{name}.GRA"
+ self._bin_file = f"{name}.BIN"
+
self._export_ST(study, repertory, qlog, name=name)
return True
@@ -622,6 +636,11 @@ class Mage8(Mage):
self._study = study
name = study.name.replace(" ", "_")
+ # Define GRA file name
+ self._gra_file = f"{name}.GRA"
+ self._bin_file = f"{name}.BIN"
+
+ # Generate files
files = []
files = self._export_ST(study, repertory, qlog, name=name)
diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py
index 162459ed..b2d9e61f 100644
--- a/src/View/BoundaryCondition/Edit/Window.py
+++ b/src/View/BoundaryCondition/Edit/Window.py
@@ -124,6 +124,8 @@ class EditBoundaryConditionWindow(PamhyrWindow):
parent=parent
)
+ self._hash_data.append(data)
+
self.setup_table()
self.setup_plot()
self.setup_data()
diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py
index 9d30532e..4489a509 100644
--- a/src/View/BoundaryCondition/Window.py
+++ b/src/View/BoundaryCondition/Window.py
@@ -214,17 +214,17 @@ class BoundaryConditionWindow(PamhyrWindow):
tab = self.current_tab()
rows = self.index_selected_rows()
for row in rows:
- win = self.sub_win_filter_first(
- "Edit boundary condition",
- contain=[f"({self._bcs.get(tab, row).id})"]
- )
+ data = self._bcs.get(tab, row)
- if win is None:
- win = EditBoundaryConditionWindow(
- data=self._bcs.get(tab, row),
- study=self._study,
- parent=self
- )
- win.show()
- else:
- win.activateWindow()
+ if self.sub_window_exists(
+ EditBoundaryConditionWindow,
+ data=[self._study, None, data]
+ ):
+ continue
+
+ win = EditBoundaryConditionWindow(
+ data=data,
+ study=self._study,
+ parent=self
+ )
+ win.show()
diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py
index 0004f3e9..d96994a8 100644
--- a/src/View/CheckList/Window.py
+++ b/src/View/CheckList/Window.py
@@ -68,6 +68,9 @@ class CheckListWindow(PamhyrWindow):
parent=parent
)
+ # Add solver to hash computation data
+ self._hash_data.append(self._solver)
+
self._checker_list = (
self._study.checkers() +
self._solver.checkers()
diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py
index 2558ab43..95b41d8b 100644
--- a/src/View/Frictions/Window.py
+++ b/src/View/Frictions/Window.py
@@ -83,6 +83,9 @@ class FrictionsWindow(PamhyrWindow):
parent=parent
)
+ # Add reach to hash computation data
+ self._hash_data.append(self._reach)
+
self.setup_table()
self.setup_graph()
self.setup_connections()
@@ -237,17 +240,15 @@ class FrictionsWindow(PamhyrWindow):
self._table.redo()
def edit_stricklers(self):
- strick = self.sub_win_filter_first(
- "Stricklers",
- contain=[]
- )
+ if self.sub_window_exists(
+ StricklersWindow,
+ data=[self._study, self.parent.conf]
+ ):
+ return
- if strick is None:
- strick = StricklersWindow(
- study=self._study,
- config=self.parent.conf,
- parent=self
- )
- strick.show()
- else:
- strick.activateWindow()
+ strick = StricklersWindow(
+ study=self._study,
+ config=self.parent.conf,
+ parent=self
+ )
+ strick.show()
diff --git a/src/View/Geometry/PlotAC.py b/src/View/Geometry/PlotAC.py
index 1d7bf8d8..7cc75184 100644
--- a/src/View/Geometry/PlotAC.py
+++ b/src/View/Geometry/PlotAC.py
@@ -72,11 +72,11 @@ class PlotAC(PamhyrPlot):
self.canvas.axes.set_xlabel(
_translate("MainWindow_reach", "Transverse abscissa (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("MainWindow_reach", "Height (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.figure.tight_layout()
@@ -176,11 +176,11 @@ class PlotAC(PamhyrPlot):
self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
self.canvas.axes.set_xlabel(
_translate("MainWindow_reach", "Abscisse en travers (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("MainWindow_reach", "Cote (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.figure.tight_layout()
diff --git a/src/View/Geometry/PlotKPZ.py b/src/View/Geometry/PlotKPZ.py
index 41b08a8f..79fbfe05 100644
--- a/src/View/Geometry/PlotKPZ.py
+++ b/src/View/Geometry/PlotKPZ.py
@@ -64,11 +64,11 @@ class PlotKPZ(PamhyrPlot):
self.canvas.axes.set_xlabel(
_translate("MainWindow_reach", "Kp (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("MainWindow_reach", "Height (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
kp = self.data.get_kp()
diff --git a/src/View/Geometry/PlotXY.py b/src/View/Geometry/PlotXY.py
index a851cc82..fcd55ecc 100644
--- a/src/View/Geometry/PlotXY.py
+++ b/src/View/Geometry/PlotXY.py
@@ -65,11 +65,11 @@ class PlotXY(PamhyrPlot):
# Axes
self.canvas.axes.set_xlabel(
_translate("Geometry", "X (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("Geometry", "Y (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.axis("equal")
diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py
index dfce6d9d..9fd3f1aa 100644
--- a/src/View/Geometry/Profile/Window.py
+++ b/src/View/Geometry/Profile/Window.py
@@ -65,6 +65,8 @@ class ProfileWindow(PamhyrWindow):
parent=parent
)
+ self._hash_data.append(profile)
+
self.setup_table()
self.setup_plot()
self.setup_connections()
diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py
index eca958b4..3bc4ebd1 100644
--- a/src/View/Geometry/Window.py
+++ b/src/View/Geometry/Window.py
@@ -73,6 +73,9 @@ class GeometryWindow(PamhyrWindow):
parent=parent
)
+ # Add reach to hash computation data
+ self._hash_data.append(self._reach)
+
self._tablemodel = None
self._profile_window = []
@@ -225,20 +228,18 @@ class GeometryWindow(PamhyrWindow):
for row in rows:
profile = self._reach.profile(row)
- win = self.sub_win_filter_first(
- "Profile",
- contain=[self._reach.name, str(profile.kp)]
- )
+ if self.sub_window_exists(
+ ProfileWindow,
+ data=[None, None, profile]
+ ):
+ continue
- if win is None:
- win = ProfileWindow(
- profile=profile,
- parent=self,
- )
- self._profile_window.append(win)
- win.show()
- else:
- win.activateWindow()
+ win = ProfileWindow(
+ profile=profile,
+ parent=self,
+ )
+ self._profile_window.append(win)
+ win.show()
self.tableView.model().blockSignals(False)
diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py
index 6fb29ca2..a0911c4b 100644
--- a/src/View/InitialConditions/Window.py
+++ b/src/View/InitialConditions/Window.py
@@ -87,6 +87,9 @@ class InitialConditionsWindow(PamhyrWindow):
parent=parent
)
+ # Add reach to hash computation data
+ self._hash_data.append(self._reach)
+
self._ics = study.river.initial_conditions.get(self._reach)
self.setup_table()
diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py
index c813a3a9..72c59b4c 100644
--- a/src/View/LateralContribution/Edit/Window.py
+++ b/src/View/LateralContribution/Edit/Window.py
@@ -77,6 +77,8 @@ class EditLateralContributionWindow(PamhyrWindow):
parent=parent
)
+ self._hash_data.append(data)
+
self.setup_table()
self.setup_plot()
self.setup_connections()
diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py
index 72ca343d..8a9a2e8e 100644
--- a/src/View/LateralContribution/Window.py
+++ b/src/View/LateralContribution/Window.py
@@ -258,17 +258,17 @@ class LateralContributionWindow(PamhyrWindow):
tab = self.current_tab()
rows = self.index_selected_rows()
for row in rows:
- win = self.sub_win_filter_first(
- "Edit lateral contribution",
- contain=[f"({self._lcs.get(tab, row).id})"]
- )
+ data = self._lcs.get(tab, row)
- if win is None:
- win = EditLateralContributionWindow(
- data=self._lcs.get(tab, row),
- study=self._study,
- parent=self
- )
- win.show()
- else:
- win.activateWindow()
+ if self.sub_window_exists(
+ EditLateralContributionWindow,
+ data=[self._study, None, data]
+ ):
+ continue
+
+ win = EditLateralContributionWindow(
+ data=data,
+ study=self._study,
+ parent=self
+ )
+ win.show()
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 2c77ed71..92a3e8f5 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -488,6 +488,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
Returns:
Nothing
"""
+ if self.sub_window_exists(
+ ConfigureWindow,
+ data=[None, self.conf]
+ ):
+ return
+
self.config = ConfigureWindow(config=self.conf, parent=self)
self.config.show()
@@ -499,6 +505,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
Returns:
Nothing
"""
+ if self.sub_window_exists(
+ AboutWindow,
+ data=[None, None]
+ ):
+ return
+
self.about = AboutWindow(parent=self)
self.about.show()
@@ -527,6 +539,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
Nothing
"""
if self._study is None:
+ if self.sub_window_exists(
+ NewStudyWindow,
+ data=[None, None]
+ ):
+ return
+
self.new_study = NewStudyWindow(parent=self)
self.new_study.show()
@@ -537,6 +555,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
Nothing
"""
if self._study is not None:
+ if self.sub_window_exists(
+ NewStudyWindow,
+ data=[self._study, None]
+ ):
+ return
+
self.new_study = NewStudyWindow(study=self._study, parent=self)
self.new_study.show()
@@ -547,11 +571,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
Nothing
"""
if self._study is not None:
- if not self.sub_win_exists("River network"):
- self.network = NetworkWindow(study=self._study, parent=self)
- self.network.show()
- else:
- self.network.activateWindow()
+ if self.sub_window_exists(
+ NetworkWindow,
+ data=[self._study, None]
+ ):
+ return
+
+ self.network = NetworkWindow(study=self._study, parent=self)
+ self.network.show()
def open_geometry(self):
"""Open geometry window
@@ -560,115 +587,117 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
Nothing
"""
if (self._study is not None and self._study.river.has_current_reach()):
- geometry = self.sub_win_filter_first(
- "Geometry",
- contain=[self._study.river.current_reach().name]
- )
+ reach = self._study.river.current_reach().reach
- if geometry is None:
- geometry = GeometryWindow(
- study=self._study, config=self.conf, parent=self)
- geometry.show()
- else:
- geometry.activateWindow()
+ if self.sub_window_exists(
+ GeometryWindow,
+ data=[self._study, self.conf, reach]
+ ):
+ return
+
+ geometry = GeometryWindow(
+ study=self._study,
+ config=self.conf,
+ reach=reach,
+ parent=self
+ )
+ geometry.show()
else:
self.msg_select_reach()
def open_boundary_cond(self):
- bound = self.sub_win_filter_first(
- "Boundary conditions",
- contain=[]
- )
+ if self.sub_window_exists(
+ BoundaryConditionWindow,
+ data=[self._study, None]
+ ):
+ return
- if bound is None:
- bound = BoundaryConditionWindow(study=self._study, parent=self)
- bound.show()
- else:
- bound.activateWindow()
+ bound = BoundaryConditionWindow(study=self._study, parent=self)
+ bound.show()
def open_lateral_contrib(self):
- lateral = self.sub_win_filter_first(
- "Lateral contribution",
- contain=[]
- )
+ if self.sub_window_exists(
+ LateralContributionWindow,
+ data=[self._study, None]
+ ):
+ return
- if lateral is None:
- lateral = LateralContributionWindow(study=self._study, parent=self)
- lateral.show()
- else:
- lateral.activateWindow()
+ lateral = LateralContributionWindow(study=self._study, parent=self)
+ lateral.show()
def open_stricklers(self):
- strick = self.sub_win_filter_first(
- "Stricklers",
- contain=[]
- )
+ if self.sub_window_exists(
+ StricklersWindow,
+ data=[self._study, self.conf]
+ ):
+ return
- if strick is None:
- strick = StricklersWindow(
- study=self._study,
- config=self.conf,
- parent=self
- )
- strick.show()
- else:
- strick.activateWindow()
+ strick = StricklersWindow(
+ study=self._study,
+ config=self.conf,
+ parent=self
+ )
+ strick.show()
def open_frictions(self):
- if (self._study is not None and
- self._study.river.has_current_reach()):
+ if self._study is not None:
+ if self._study.river.has_current_reach():
+ reach = self._study.river.current_reach()
- frictions = self.sub_win_filter_first(
- "Frictions",
- contain=[self._study.river.current_reach().name]
- )
+ if self.sub_window_exists(
+ FrictionsWindow,
+ data=[self._study, None, reach]
+ ):
+ return
- if frictions is None:
frictions = FrictionsWindow(
study=self._study,
parent=self
)
frictions.show()
else:
- frictions.activateWindow()
- else:
- self.msg_select_reach()
+ self.msg_select_reach()
def open_initial_conditions(self):
if self._study.river.has_current_reach():
- initial = self.sub_win_filter_first(
- "Initial condition",
- contain=[self._study.river.current_reach().name]
- )
+ reach = self._study.river.current_reach()
- if initial is None:
- initial = InitialConditionsWindow(
- study=self._study,
- config=self.conf,
- parent=self
- )
- initial.show()
- else:
- initial.activateWindow()
+ if self.sub_window_exists(
+ InitialConditionsWindow,
+ data=[self._study, self.conf, reach]
+ ):
+ return
+
+ initial = InitialConditionsWindow(
+ study=self._study,
+ config=self.conf,
+ reach=reach,
+ parent=self
+ )
+ initial.show()
else:
self.msg_select_reach()
def open_solver_parameters(self):
- params = self.sub_win_filter_first(
- "Solver parameters",
- contain=[]
- )
+ if self.sub_window_exists(
+ SolverParametersWindow,
+ data=[self._study, None]
+ ):
+ return
- if params is None:
- params = SolverParametersWindow(
- study=self._study,
- parent=self
- )
- params.show()
- else:
- params.activateWindow()
+ params = SolverParametersWindow(
+ study=self._study,
+ parent=self
+ )
+ params.show()
def open_sediment_layers(self):
+ if self.sub_window_exists(
+ SedimentLayersWindow,
+ data=[self._study, None]
+ ):
+ return
+
sl = SedimentLayersWindow(
study=self._study,
parent=self
@@ -676,8 +705,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
sl.show()
def open_reach_sediment_layers(self):
+ reach = self._study.river.current_reach().reach
+
+ if self.sub_window_exists(
+ ReachSedimentLayersWindow,
+ data=[self._study, None, reach]
+ ):
+ return
+
sl = ReachSedimentLayersWindow(
study=self._study,
+ reach=reach,
parent=self
)
sl.show()
@@ -693,6 +731,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
if run.exec():
solver = run.solver
+
+ if self.sub_window_exists(
+ CheckListWindow,
+ data=[
+ self._study,
+ self.conf,
+ solver
+ ]
+ ):
+ return
+
check = CheckListWindow(
study=self._study,
config=self.conf,
@@ -720,21 +769,24 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
return
# Windows already opened
- res = self.sub_win_filter_first(
- "Results",
- contain=[solver.name, results.date]
- )
+ if self.sub_window_exists(
+ ResultsWindow,
+ data=[
+ self._study,
+ None, # No config
+ solver,
+ results
+ ]
+ ):
+ return
- if res is None:
- res = ResultsWindow(
- study=self._study,
- solver=solver,
- results=results,
- parent=self
- )
- res.show()
- else:
- res.activateWindow()
+ res = ResultsWindow(
+ study=self._study,
+ solver=solver,
+ results=results,
+ parent=self
+ )
+ res.show()
def open_last_results(self):
if self._last_solver is None or self._last_results is None:
diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py
index 9c24b4f0..ade0eaff 100644
--- a/src/View/Network/GraphWidget.py
+++ b/src/View/Network/GraphWidget.py
@@ -442,7 +442,7 @@ class GraphWidget(QGraphicsView):
Nothing
"""
for i in self.texts:
- if i is NodeItem:
+ if type(i) is NodeItem:
self.texts[i].rename()
def enable_edge(self, edge, prev):
@@ -699,11 +699,11 @@ class GraphWidget(QGraphicsView):
self._selected_new_edge_src_node = None
items = self.items(event.pos())
- if items and items[0] is EdgeItem:
+ if items and type(items[0]) is EdgeItem:
edge = items[0]
if edge:
self.set_current_edge(edge)
- elif items and items[0] is NodeItem:
+ elif items and type(items[0]) is NodeItem:
self._mouse_origin_x = pos.x()
self._mouse_origin_y = pos.y()
self._current_moved_node = items[0]
@@ -711,7 +711,7 @@ class GraphWidget(QGraphicsView):
# Add nodes and edges
elif self._state == "add":
items = self.items(event.pos())
- nodes = list(filter(lambda i: i is NodeItem, items))
+ nodes = list(filter(lambda i: type(i) is NodeItem, items))
if not nodes:
self.add_node(pos)
else:
@@ -725,15 +725,15 @@ class GraphWidget(QGraphicsView):
self._selected_new_edge_src_node = None
items = list(
filter(
- lambda i: i is NodeItem or i is EdgeItem,
+ lambda i: type(i) is NodeItem or type(i) is EdgeItem,
self.items(event.pos())
)
)
if len(items) > 0:
item = items[0]
- if item is NodeItem:
+ if type(item) is NodeItem:
self.del_node(item)
- elif item is EdgeItem:
+ elif type(item) is EdgeItem:
self.del_edge(item)
self.update()
@@ -767,7 +767,7 @@ class GraphWidget(QGraphicsView):
items = self.items(event.pos())
selectable_items = list(
filter(
- lambda i: (i is NodeItem or i is EdgeItem),
+ lambda i: (type(i) is NodeItem or type(i) is EdgeItem),
items
)
)
@@ -858,7 +858,7 @@ class GraphWidget(QGraphicsView):
menu = QMenu(self)
if len(items) == 0:
self._menu_default(event, pos, items, menu)
- elif items[0] is NodeItem:
+ elif type(items[0]) is NodeItem:
self._menu_node(event, pos, items, menu)
- elif items[0] is EdgeItem:
+ elif type(items[0]) is EdgeItem:
self._menu_edge(event, pos, items, menu)
diff --git a/src/View/Results/CustomPlot/CustomPlotValuesSelectionDialog.py b/src/View/Results/CustomPlot/CustomPlotValuesSelectionDialog.py
new file mode 100644
index 00000000..1ffd6732
--- /dev/null
+++ b/src/View/Results/CustomPlot/CustomPlotValuesSelectionDialog.py
@@ -0,0 +1,97 @@
+# CustomPlotValuesSelectionDialog.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 .
+
+# -*- coding: utf-8 -*-
+
+from View.Tools.PamhyrWindow import PamhyrDialog
+
+from PyQt5.QtWidgets import (
+ QRadioButton, QCheckBox, QVBoxLayout,
+)
+
+from View.Results.CustomPlot.Translate import CustomPlotTranslate
+
+
+class CustomPlotValuesSelectionDialog(PamhyrDialog):
+ _pamhyr_ui = "CustomPlotValuesSelectionDialog"
+ _pamhyr_name = "Custom Plot Selection"
+
+ def __init__(self, parent=None):
+ super(CustomPlotValuesSelectionDialog, self).__init__(
+ title=self._pamhyr_name,
+ options=[],
+ trad=CustomPlotTranslate(),
+ parent=parent
+ )
+
+ self._available_values_x = self._trad.get_dict("values_x")
+ self._available_values_y = self._trad.get_dict("values_y")
+
+ self.setup_radio_buttons()
+ self.setup_check_boxs()
+
+ self.value = None
+
+ def setup_radio_buttons(self):
+ self._radio = []
+ layout = self.find(QVBoxLayout, "verticalLayout_x")
+
+ for value in self._available_values_x:
+ btn = QRadioButton(
+ self._available_values_x[value],
+ parent=self
+ )
+ self._radio.append((value, btn))
+ layout.addWidget(btn)
+
+ self._radio[0][1].setChecked(True)
+ layout.addStretch()
+
+ def setup_check_boxs(self):
+ self._check = []
+ layout = self.find(QVBoxLayout, "verticalLayout_y")
+
+ for value in self._available_values_y:
+ btn = QCheckBox(
+ self._available_values_y[value],
+ parent=self
+ )
+ self._check.append((value, btn))
+ layout.addWidget(btn)
+
+ self._check[0][1].setChecked(True)
+ layout.addStretch()
+
+ def accept(self):
+ x = next(
+ filter(
+ lambda r: r[1].isChecked(),
+ self._radio
+ )
+ )[0]
+
+ y = list(
+ map(
+ lambda b: b[0],
+ filter(
+ lambda b: b[1].isChecked(),
+ self._check
+ )
+ )
+ )
+
+ self.value = x, y
+ super().accept()
diff --git a/src/View/Results/CustomPlot/Plot.py b/src/View/Results/CustomPlot/Plot.py
new file mode 100644
index 00000000..06f911a2
--- /dev/null
+++ b/src/View/Results/CustomPlot/Plot.py
@@ -0,0 +1,346 @@
+# Plot.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 .
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from functools import reduce
+from datetime import datetime
+
+from tools import timer
+from View.Tools.PamhyrPlot import PamhyrPlot
+
+from View.Results.CustomPlot.Translate import CustomPlotTranslate
+
+logger = logging.getLogger()
+
+unit = {
+ "elevation": "0-meter",
+ "water_elevation": "0-meter",
+ "discharge": "1-m3s",
+}
+
+
+class CustomPlot(PamhyrPlot):
+ def __init__(self, x, y, reach, profile, timestamp,
+ data=None, canvas=None, trad=None,
+ toolbar=None, parent=None):
+ super(CustomPlot, self).__init__(
+ canvas=canvas,
+ trad=CustomPlotTranslate(),
+ data=data,
+ toolbar=toolbar,
+ parent=parent
+ )
+
+ self._x = x
+ self._y = y
+ self._reach = reach
+ self._profile = profile
+ self._timestamp = timestamp
+
+ logger.debug(
+ "Create custom plot for: " +
+ f"{x} -> {','.join(y)}: " +
+ f"reach={reach}, profile={profile}, " +
+ f"timestamp={timestamp}"
+ )
+
+ self._y_axes = sorted(
+ set(
+ map(
+ lambda y: unit[y],
+ self._y
+ )
+ )
+ )
+
+ self._axes = {}
+
+ def _draw_kp(self):
+ results = self.data
+ reach = results.river.reach(self._reach)
+ kp = reach.geometry.get_kp()
+ z_min = reach.geometry.get_z_min()
+
+ self.canvas.axes.set_xlim(
+ left=min(kp), right=max(kp)
+ )
+
+ meter_axes = self.canvas.axes
+ m3S_axes = self.canvas.axes
+ if "0-meter" in self._y_axes and "1-m3s" in self._y_axes:
+ m3s_axes = self._axes["1-m3s"]
+
+ lines = {}
+ if "elevation" in self._y:
+ meter_axes.set_ylim(
+ bottom=min(0, min(z_min)),
+ top=max(z_min) + 1
+ )
+
+ line = meter_axes.plot(
+ kp, z_min,
+ color='grey', lw=1.,
+ )
+ lines["elevation"] = line
+
+ if "water_elevation" in self._y:
+ # Water elevation
+ water_z = list(
+ map(
+ lambda p: p.get_ts_key(self._timestamp, "Z"),
+ reach.profiles
+ )
+ )
+
+ meter_axes.set_ylim(
+ bottom=min(0, min(z_min)),
+ top=max(water_z) + 1
+ )
+
+ line = meter_axes.plot(
+ kp, water_z, lw=1.,
+ color='blue',
+ )
+ lines["water_elevation"] = line
+
+ if "elevation" in self._y:
+ meter_axes.fill_between(
+ kp, z_min, water_z,
+ color='blue', alpha=0.5, interpolate=True
+ )
+
+ if "discharge" in self._y:
+ q = list(
+ map(
+ lambda p: p.get_ts_key(self._timestamp, "Q"),
+ reach.profiles
+ )
+ )
+
+ m3s_axes.set_ylim(
+ bottom=min(0, min(q)),
+ top=max(q) + 1
+ )
+
+ line = m3s_axes.plot(
+ kp, q, lw=1.,
+ color='r',
+ )
+ lines["discharge"] = line
+
+ # Legend
+ lns = reduce(
+ lambda acc, line: acc + line,
+ map(lambda line: lines[line], lines),
+ []
+ )
+ labs = list(map(lambda line: self._trad[line], lines))
+ self.canvas.axes.legend(lns, labs, loc="lower left")
+
+ def _customize_x_axes_time(self, ts, mode="time"):
+ # Custom time display
+ nb = len(ts)
+ mod = int(nb / 5)
+ mod = mod if mod > 0 else nb
+
+ fx = list(
+ map(
+ lambda x: x[1],
+ filter(
+ lambda x: x[0] % mod == 0,
+ enumerate(ts)
+ )
+ )
+ )
+
+ if mode == "time":
+ t0 = datetime.fromtimestamp(0)
+ xt = list(
+ map(
+ lambda v: (
+ str(
+ datetime.fromtimestamp(v) - t0
+ ).split(",")[0]
+ .replace("days", self._trad["days"])
+ .replace("day", self._trad["day"])
+ ),
+ fx
+ )
+ )
+ else:
+ xt = list(
+ map(
+ lambda v: str(datetime.fromtimestamp(v).date()),
+ fx
+ )
+ )
+
+ self.canvas.axes.set_xticks(ticks=fx, labels=xt, rotation=45)
+
+ def _draw_time(self):
+ results = self.data
+ reach = results.river.reach(self._reach)
+ profile = reach.profile(self._profile)
+
+ meter_axes = self.canvas.axes
+ m3S_axes = self.canvas.axes
+ if "0-meter" in self._y_axes and "1-m3s" in self._y_axes:
+ m3s_axes = self._axes["1-m3s"]
+
+ ts = list(results.get("timestamps"))
+ ts.sort()
+
+ self.canvas.axes.set_xlim(
+ left=min(ts), right=max(ts)
+ )
+
+ x = ts
+ lines = {}
+ if "elevation" in self._y:
+ # Z min is constant in time
+ z_min = profile.geometry.z_min()
+ ts_z_min = list(
+ map(
+ lambda ts: z_min,
+ ts
+ )
+ )
+
+ line = meter_axes.plot(
+ ts, ts_z_min,
+ color='grey', lw=1.
+ )
+ lines["elevation"] = line
+
+ if "water_elevation" in self._y:
+ # Water elevation
+ z = profile.get_key("Z")
+
+ meter_axes.set_ylim(
+ bottom=min(0, min(z)),
+ top=max(z) + 1
+ )
+
+ line = meter_axes.plot(
+ ts, z, lw=1.,
+ color='b',
+ )
+ lines["water_elevation"] = line
+
+ if "elevation" in self._y:
+ z_min = profile.geometry.z_min()
+ ts_z_min = list(
+ map(
+ lambda ts: z_min,
+ ts
+ )
+ )
+
+ meter_axes.fill_between(
+ ts, ts_z_min, z,
+ color='blue', alpha=0.5, interpolate=True
+ )
+
+ if "discharge" in self._y:
+ q = profile.get_key("Q")
+
+ m3s_axes.set_ylim(
+ bottom=min(0, min(q)),
+ top=max(q) + 1
+ )
+
+ line = m3s_axes.plot(
+ ts, q, lw=1.,
+ color='r',
+ )
+ lines["discharge"] = line
+
+ self._customize_x_axes_time(ts)
+
+ # Legend
+ lns = reduce(
+ lambda acc, line: acc + line,
+ map(lambda line: lines[line], lines),
+ []
+ )
+ labs = list(map(lambda line: self._trad[line], lines))
+ self.canvas.axes.legend(lns, labs, loc="lower left")
+
+ @timer
+ def draw(self):
+ self.canvas.axes.cla()
+ self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
+
+ if self.data is None:
+ return
+
+ self.canvas.axes.set_xlabel(
+ self._trad[self._x],
+ color='green', fontsize=10
+ )
+
+ self.canvas.axes.set_ylabel(
+ self._trad[self._y_axes[0]],
+ color='green', fontsize=10
+ )
+
+ for axes in self._y_axes[1:]:
+ if axes in self._axes:
+ continue
+
+ ax_new = self.canvas.axes.twinx()
+ ax_new.set_ylabel(
+ self._trad[axes],
+ color='green', fontsize=10
+ )
+ self._axes[axes] = ax_new
+
+ if self._x == "kp":
+ self._draw_kp()
+ elif self._x == "time":
+ self._draw_time()
+
+ self.canvas.figure.tight_layout()
+ self.canvas.figure.canvas.draw_idle()
+ if self.toolbar is not None:
+ self.toolbar.update()
+
+ @timer
+ def update(self):
+ if not self._init:
+ self.draw()
+ return
+
+ def set_reach(self, reach_id):
+ self._reach = reach_id
+ self._profile = 0
+
+ self.update()
+
+ def set_profile(self, profile_id):
+ self._profile = profile_id
+
+ if self._x != "kp":
+ self.update()
+
+ def set_timestamp(self, timestamp):
+ self._timestamp = timestamp
+
+ if self._x != "time":
+ self.update()
diff --git a/src/View/Results/CustomPlot/Translate.py b/src/View/Results/CustomPlot/Translate.py
new file mode 100644
index 00000000..555950cd
--- /dev/null
+++ b/src/View/Results/CustomPlot/Translate.py
@@ -0,0 +1,67 @@
+# 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 .
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Results.translate import ResultsTranslate
+
+_translate = QCoreApplication.translate
+
+
+class CustomPlotTranslate(ResultsTranslate):
+ def __init__(self):
+ super(CustomPlotTranslate, self).__init__()
+
+ # Value type
+
+ self._dict['time'] = _translate(
+ "CustomPlot", "Time (sec)"
+ )
+ self._dict['kp'] = _translate(
+ "CustomPlot", "Kp (m)"
+ )
+ self._dict['elevation'] = _translate(
+ "CustomPlot", "Elevation (m)"
+ )
+ self._dict['water_elevation'] = _translate(
+ "CustomPlot", "Water elevation (m)"
+ )
+ self._dict['discharge'] = _translate(
+ "CustomPlot", "Discharge (m³/s)"
+ )
+
+ # Unit corresponding long name (plot axes display)
+
+ self._dict['0-meter'] = _translate(
+ "CustomPlot", "Elevation (m)"
+ )
+ self._dict['1-m3s'] = _translate(
+ "CustomPlot", "Discharge (m³/s)"
+ )
+
+ # SubDict
+
+ self._sub_dict["values_x"] = {
+ "kp": self._dict["kp"],
+ "time": self._dict["time"],
+ }
+ self._sub_dict["values_y"] = {
+ "elevation": self._dict["elevation"],
+ "water_elevation": self._dict["water_elevation"],
+ "discharge": self._dict["discharge"],
+ }
diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py
index f4f38e7c..a6184e21 100644
--- a/src/View/Results/PlotH.py
+++ b/src/View/Results/PlotH.py
@@ -82,11 +82,11 @@ class PlotH(PamhyrPlot):
# Axes
self.canvas.axes.set_xlabel(
_translate("Results", "Time (s)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("Results", "Discharge (m³/s)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
ts = list(self.results.get("timestamps"))
diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py
index 8d8f7b91..5d25b189 100644
--- a/src/View/Results/PlotSedProfile.py
+++ b/src/View/Results/PlotSedProfile.py
@@ -132,11 +132,11 @@ class PlotSedProfile(PamhyrPlot):
self.canvas.axes.set_xlabel(
_translate("MainWindow_reach", "X (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("MainWindow_reach", "Height (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
x = profile.geometry.get_station()
diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py
index e404f09c..fe7498d1 100644
--- a/src/View/Results/PlotSedReach.py
+++ b/src/View/Results/PlotSedReach.py
@@ -213,11 +213,11 @@ class PlotSedReach(PamhyrPlot):
self.canvas.axes.set_xlabel(
_translate("MainWindow_reach", "Kp (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("MainWindow_reach", "Height (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
kp = reach.geometry.get_kp()
diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py
index 78d4c66b..276ab600 100644
--- a/src/View/Results/PlotXY.py
+++ b/src/View/Results/PlotXY.py
@@ -83,11 +83,11 @@ class PlotXY(PamhyrPlot):
# Axes
self.canvas.axes.set_xlabel(
_translate("Results", "X (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("Results", "Y (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.axis("equal")
diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py
index d076fd42..d4115507 100644
--- a/src/View/Results/Window.py
+++ b/src/View/Results/Window.py
@@ -38,7 +38,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
- QSlider, QLabel,
+ QSlider, QLabel, QWidget, QGridLayout,
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
@@ -51,6 +51,11 @@ from View.Results.PlotH import PlotH
from View.Results.PlotSedReach import PlotSedReach
from View.Results.PlotSedProfile import PlotSedProfile
+from View.Results.CustomPlot.Plot import CustomPlot
+from View.Results.CustomPlot.CustomPlotValuesSelectionDialog import (
+ CustomPlotValuesSelectionDialog,
+)
+
from View.Results.Table import TableModel
from View.Results.translate import ResultsTranslate
from View.Stricklers.Window import StricklersWindow
@@ -86,6 +91,11 @@ class ResultsWindow(PamhyrWindow):
parent=parent
)
+ self._hash_data.append(self._solver)
+ self._hash_data.append(self._results)
+
+ self._additional_plot = {}
+
self.setup_table()
self.setup_plot()
self.setup_slider()
@@ -274,6 +284,7 @@ class ResultsWindow(PamhyrWindow):
# Action
actions = {
"action_reload": self._reload,
+ "action_add": self._add_custom_plot
}
for action in actions:
@@ -281,6 +292,7 @@ class ResultsWindow(PamhyrWindow):
actions[action]
)
+ # Table and Plot
fun = {
"reach": self._set_current_reach,
"profile": self._set_current_profile,
@@ -336,6 +348,9 @@ class ResultsWindow(PamhyrWindow):
self.plot_sed_reach.set_reach(reach_id)
self.plot_sed_profile.set_reach(reach_id)
+ for plot in self._additional_plot:
+ self._additional_plot[plot].set_reach(reach_id)
+
self.update_table_selection_reach(reach_id)
self.update_table_selection_profile(0)
@@ -349,7 +364,11 @@ class ResultsWindow(PamhyrWindow):
self.plot_sed_reach.set_profile(profile_id)
self.plot_sed_profile.set_profile(profile_id)
+ for plot in self._additional_plot:
+ self._additional_plot[plot].set_profile(profile_id)
+
self.update_table_selection_profile(profile_id)
+
if timestamp is not None:
self.plot_xy.set_timestamp(timestamp)
self.plot_ac.set_timestamp(timestamp)
@@ -360,17 +379,32 @@ class ResultsWindow(PamhyrWindow):
self.plot_sed_reach.set_timestamp(timestamp)
self.plot_sed_profile.set_timestamp(timestamp)
- self.plot_xy.draw()
- self.plot_ac.draw()
- self.plot_kpc.draw()
- self.plot_h.draw()
-
- if self._study.river.has_sediment():
- self.plot_sed_reach.draw()
- self.plot_sed_profile.draw()
+ for plot in self._additional_plot:
+ self._additional_plot[plot].set_timestamp(timestamp)
self.update_statusbar()
+ def _get_current_reach(self):
+ table = self.find(QTableView, f"tableView_reach")
+ indexes = table.selectedIndexes()
+ if len(indexes) == 0:
+ return 0
+
+ return indexes[0].row()
+
+ def _get_current_profile(self):
+ table = self.find(QTableView, f"tableView_profile")
+ indexes = table.selectedIndexes()
+ if len(indexes) == 0:
+ return 0
+
+ return indexes[0].row()
+
+ def _get_current_timestamp(self):
+ return self._timestamps[
+ self._slider_time.value()
+ ]
+
def _set_current_reach(self):
table = self.find(QTableView, f"tableView_reach")
indexes = table.selectedIndexes()
@@ -430,6 +464,56 @@ class ResultsWindow(PamhyrWindow):
self._reload_plots()
self._reload_slider()
+ def _add_custom_plot(self):
+ dlg = CustomPlotValuesSelectionDialog(parent=self)
+ if dlg.exec():
+ x, y = dlg.value
+ self.create_new_tab_custom_plot(x, y)
+
+ def create_new_tab_custom_plot(self, x: str, y: list):
+ name = f"{x}: {','.join(y)}"
+ wname = f"tab_custom_{x}_{y}"
+
+ tab_widget = self.find(QTabWidget, f"tabWidget")
+
+ # This plot already exists
+ if name in self._additional_plot:
+ tab_widget.setCurrentWidget(
+ tab_widget.findChild(QWidget, wname)
+ )
+ return
+
+ widget = QWidget()
+ grid = QGridLayout()
+
+ widget.setObjectName(wname)
+
+ canvas = MplCanvas(width=5, height=4, dpi=100)
+ canvas.setObjectName(f"canvas_{x}_{y}")
+ toolbar = PamhyrPlotToolbar(
+ canvas, self
+ )
+
+ plot = CustomPlot(
+ x, y,
+ self._get_current_reach(),
+ self._get_current_profile(),
+ self._get_current_timestamp(),
+ data=self._results,
+ canvas=canvas,
+ toolbar=toolbar,
+ parent=self,
+ )
+ plot.draw()
+
+ # Add plot to additional plot
+ self._additional_plot[name] = plot
+
+ grid.addWidget(toolbar, 0, 0)
+ grid.addWidget(canvas, 1, 0)
+ widget.setLayout(grid)
+ tab_widget.addTab(widget, name)
+
def _copy(self):
logger.info("TODO: copy")
diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py
index ae9dcad1..4de61cc7 100644
--- a/src/View/Results/translate.py
+++ b/src/View/Results/translate.py
@@ -27,6 +27,13 @@ class ResultsTranslate(PamhyrTranslate):
def __init__(self):
super(ResultsTranslate, self).__init__()
+ self._dict['day'] = _translate(
+ "Results", "day"
+ )
+ self._dict['days'] = _translate(
+ "Results", "days"
+ )
+
self._sub_dict["table_headers_reach"] = {
"name": _translate("Results", "Reach name"),
}
diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py
index 87cfe644..69e58f83 100644
--- a/src/View/SedimentLayers/Edit/Plot.py
+++ b/src/View/SedimentLayers/Edit/Plot.py
@@ -35,7 +35,7 @@ class Plot(PamhyrPlot):
self.canvas.axes.axes.get_xaxis().set_visible(False)
self.canvas.axes.set_ylabel(
self._trad["height"],
- color='green', fontsize=12
+ color='green', fontsize=10
)
if self.data is None:
diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py
index 3664081c..1bea651f 100644
--- a/src/View/SedimentLayers/Reach/Plot.py
+++ b/src/View/SedimentLayers/Reach/Plot.py
@@ -46,11 +46,11 @@ class Plot(PamhyrPlot):
self.canvas.axes.set_xlabel(
self._trad["kp"],
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
self._trad["height"],
- color='green', fontsize=12
+ color='green', fontsize=10
)
kp = self.data.get_kp()
diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py
index 38adb990..5000ca9b 100644
--- a/src/View/SedimentLayers/Reach/Profile/Plot.py
+++ b/src/View/SedimentLayers/Reach/Profile/Plot.py
@@ -46,11 +46,11 @@ class Plot(PamhyrPlot):
self.canvas.axes.set_xlabel(
_translate("MainWindow_reach", "X (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
self.canvas.axes.set_ylabel(
_translate("MainWindow_reach", "Height (m)"),
- color='green', fontsize=12
+ color='green', fontsize=10
)
x = self.data.get_station()
diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py
index 6c4cad96..854e06a5 100644
--- a/src/View/SedimentLayers/Reach/Window.py
+++ b/src/View/SedimentLayers/Reach/Window.py
@@ -46,9 +46,12 @@ class ReachSedimentLayersWindow(PamhyrWindow):
_pamhyr_ui = "ReachSedimentLayers"
_pamhyr_name = "Reach sediment layers"
- def __init__(self, study=None, config=None, parent=None):
+ def __init__(self, study=None, config=None, reach=None, parent=None):
self._sediment_layers = study.river.sediment_layers
- self._reach = study.river.current_reach().reach
+ if reach is None:
+ self._reach = study.river.current_reach().reach
+ else:
+ self._reach = reach
name = (
self._pamhyr_name + " - " +
@@ -64,6 +67,9 @@ class ReachSedimentLayersWindow(PamhyrWindow):
parent=parent
)
+ # Add reach to hash computation data
+ self._hash_data.append(self._reach)
+
self.setup_table()
self.setup_plot()
self.setup_connections()
diff --git a/src/View/Tools/ASubWindow.py b/src/View/Tools/ASubWindow.py
index a2e8fc88..0a3ca2ae 100644
--- a/src/View/Tools/ASubWindow.py
+++ b/src/View/Tools/ASubWindow.py
@@ -483,7 +483,7 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit):
def closeEvent(self, event):
if self.parent is not None:
- self.parent.sub_win_del(self.name)
+ self.parent.sub_win_del(self.hash())
def find(self, qtype, name):
"""Find an ui component
@@ -520,7 +520,7 @@ class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit):
def closeEvent(self, event):
if self.parent is not None:
- self.parent.sub_win_del(self.name)
+ self.parent.sub_win_del(self.hash())
def find(self, qtype, name):
"""Find an ui component
diff --git a/src/View/Tools/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py
index b548cb31..7fe21bb7 100644
--- a/src/View/Tools/ListedSubWindow.py
+++ b/src/View/Tools/ListedSubWindow.py
@@ -39,64 +39,62 @@ class ListedSubWindow(object):
self.sub_win_cnt += 1
try:
logger.info(
- f"Open window: {name}: {self.sub_win_cnt}: {win.hash()}")
+ f"Open window: {name}: {self.sub_win_cnt}")
except Exception:
logger.info(f"Open window: {name}: {self.sub_win_cnt}: X")
+ logger.warning(f"Sub window without hash method !")
- def sub_win_del(self, name):
+ def sub_win_del(self, h):
self.sub_win_list = list(
filter(
- lambda x: x[0] != name,
+ lambda x: x[1].hash() != h,
self.sub_win_list
)
)
self.sub_win_cnt = len(self.sub_win_list)
- logger.info(f"Close window: {name}: {self.sub_win_cnt}")
+ logger.info(f"Close window: ({h}) {self.sub_win_cnt}")
- def _sub_win_exists(self, name):
+ def _sub_win_exists(self, h):
return reduce(
- lambda acc, n: (acc or (n[0] == name)),
+ lambda acc, el: (acc or (h == (el[1].hash()))),
self.sub_win_list,
False
)
- def _sub_win_exists_with_contain(self, name, contain):
- return reduce(
- lambda acc, n: (
- acc or
- (
- (n[0] == name) and
- reduce(
- lambda acc, c: acc and (c in n[1]._title),
- contain,
- True
- )
- )
- ),
- self.sub_win_list,
- False
- )
+ def sub_win_exists(self, h):
+ return self._sub_win_exists(h)
- def sub_win_exists(self, name, contain=[]):
- if contain == []:
- return self._sub_win_exists(name)
- else:
- return self._sub_win_exists_with_contain(name, contain)
-
- def sub_win_filter_first(self, name, contain):
+ def get_sub_win(self, h):
try:
return next(
filter(
- lambda n: (
- (name in n[0]) and
- reduce(
- lambda acc, c: acc and (c in n[1]._title),
- contain,
- True
- )
- ),
+ lambda el: (h == el[1].hash()),
self.sub_win_list,
)
)[1]
except Exception:
return None
+
+ def sub_window_exists(self, cls,
+ data=None):
+ """Check if window already exists
+
+ Check if window already exists, used to deni window open
+ duplication
+
+ Args:
+ cls: Window class, must inerit to PamhyrWindow or
+ PamhyrDialog
+ data: Data used for hash computation of cls
+
+ Returns:
+ The window if hash already exists on sub window dictionary,
+ otherelse None
+ """
+ hash = cls._hash(data)
+ if self.sub_win_exists(hash):
+ win = self.get_sub_win(hash)
+ win.activateWindow()
+ return True
+ else:
+ return False
diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py
index a3c7d4c3..74756167 100644
--- a/src/View/Tools/PamhyrWindow.py
+++ b/src/View/Tools/PamhyrWindow.py
@@ -122,7 +122,6 @@ class PamhyrWindowTools(object):
hash_str += repr(el)
h = hash(hash_str)
- logger.debug(f"Compute hash = {h} for window {cls._pamhyr_name}")
return h
diff --git a/src/View/ui/CustomPlotValuesSelectionDialog.ui b/src/View/ui/CustomPlotValuesSelectionDialog.ui
new file mode 100644
index 00000000..88ca363c
--- /dev/null
+++ b/src/View/ui/CustomPlotValuesSelectionDialog.ui
@@ -0,0 +1,93 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 414
+ 482
+
+
+
+ Dialog
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
+ X axis:
+
+
+
+
+
+
+
+ -
+
+
+ Y axis:
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 00000000..89e3fbe8
--- /dev/null
+++ b/src/__init__.py
@@ -0,0 +1,17 @@
+# __init__.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 .
+
+# -*- coding: utf-8 -*-
diff --git a/src/pamhyr.py b/src/pamhyr.py
index 2381da01..5d274166 100755
--- a/src/pamhyr.py
+++ b/src/pamhyr.py
@@ -37,6 +37,8 @@ from Model.Study import Study
from Scripts.P3DST import Script3DST
from Scripts.Hello import ScriptHello
+from Scripts.ListSolver import ScriptListSolver
+from Scripts.Run import ScriptExport, ScriptRun
from init import legal_info, debug_info, setup_lang
@@ -44,6 +46,9 @@ logger = logging.getLogger()
scripts = {
"hello": ScriptHello,
+ "solvers": ScriptListSolver,
+ "export": ScriptExport,
+ "run": ScriptRun,
"3DST": Script3DST,
}
diff --git a/src/test_pamhyr.py b/src/test_pamhyr.py
new file mode 100644
index 00000000..7066a653
--- /dev/null
+++ b/src/test_pamhyr.py
@@ -0,0 +1,171 @@
+# test_pamhyr.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 .
+
+# -*- coding: utf-8 -*-
+
+import os
+import unittest
+import tempfile
+
+from tools import flatten, parse_command_line
+
+
+class FlattenTestCase(unittest.TestCase):
+ def test_flatten_0(self):
+ input = []
+ output = []
+
+ res = flatten(input)
+
+ self.assertEqual(len(res), len(output))
+ for i, o in enumerate(output):
+ self.assertEqual(res[i], o)
+
+ def test_flatten_1(self):
+ input = [['foo']]
+ output = ['foo']
+
+ res = flatten(input)
+
+ self.assertEqual(len(res), len(output))
+ for i, o in enumerate(output):
+ self.assertEqual(res[i], o)
+
+ def test_flatten_2(self):
+ input = [['foo', 'bar']]
+ output = ['foo', 'bar']
+
+ res = flatten(input)
+
+ self.assertEqual(len(res), len(output))
+ for i, o in enumerate(output):
+ self.assertEqual(res[i], o)
+
+ def test_flatten_3(self):
+ input = [['foo'], ['bar']]
+ output = ['foo', 'bar']
+
+ res = flatten(input)
+
+ self.assertEqual(len(res), len(output))
+ for i, o in enumerate(output):
+ self.assertEqual(res[i], o)
+
+ def test_flatten_4(self):
+ input = [['foo'], ['bar', 'baz'], ['bazz']]
+ output = ['foo', 'bar', 'baz', 'bazz']
+
+ res = flatten(input)
+
+ self.assertEqual(len(res), len(output))
+ for i, o in enumerate(output):
+ self.assertEqual(res[i], o)
+
+ def test_flatten_5(self):
+ input = [['foo'], ['bar', ['baz']], ['bazz']]
+ output = ['foo', 'bar', ['baz'], 'bazz']
+
+ res = flatten(input)
+
+ self.assertEqual(len(res), len(output))
+ for i, o in enumerate(output):
+ self.assertEqual(res[i], o)
+
+
+class ToolsCMDParserTestCase(unittest.TestCase):
+ def test_trivial(self):
+ cmd = "foo"
+ expect = ["foo"]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_unix_simple(self):
+ cmd = "/foo/bar a -b -c"
+ expect = ["/foo/bar", "a", '-b', "-c"]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_unix_quoted(self):
+ cmd = "\"/foo/bar\" -a -b -c"
+ expect = ["/foo/bar", "-a", '-b', "-c"]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_unix_quoted_with_space(self):
+ cmd = "\"/foo/bar baz\" -a -b -c"
+ expect = ["/foo/bar baz", "-a", '-b', "-c"]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_unix_quoted_args(self):
+ cmd = "/foo/bar -a -b -c=\"baz\""
+ expect = ["/foo/bar", "-a", '-b', "-c=\"baz\""]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_unix_quoted_args_with_space(self):
+ cmd = "/foo/bar -a -b -c=\"baz bazz\""
+ expect = ["/foo/bar", "-a", '-b', "-c=\"baz bazz\""]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_unix_escape_space(self):
+ cmd = r"/foo/bar\ baz -a -b -c"
+ expect = [r"/foo/bar\ baz", "-a", '-b', "-c"]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_windows_prog_files(self):
+ cmd = "\"C:\\Program Files (x86)\foo\bar\" a -b -c"
+ expect = ["C:\\Program Files (x86)\foo\bar", "a", '-b', "-c"]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
+
+ def test_windows_prog_files_args(self):
+ cmd = "\"C:\\Program Files (x86)\foo\bar\" a -b=\"baz bazz\" -c"
+ expect = [
+ "C:\\Program Files (x86)\foo\bar",
+ "a", '-b=\"baz bazz\"', "-c"
+ ]
+
+ res = parse_command_line(cmd)
+
+ for i, s in enumerate(expect):
+ self.assertEqual(res[i], s)
diff --git a/src/tools.py b/src/tools.py
index d286345a..cf4a0966 100644
--- a/src/tools.py
+++ b/src/tools.py
@@ -272,6 +272,10 @@ class SQL(object):
logger.debug("SQL - commit")
self._db.commit()
+ def _close(self):
+ self.commit()
+ self._db.close()
+
def _fetch_string(self, s):
return s.replace("'", "'")
@@ -337,3 +341,113 @@ class SQL(object):
def _load(self):
logger.warning("TODO: LOAD")
+
+
+#######################
+# COMMAND LINE PARSER #
+#######################
+
+parser_special_char = ["\"", "\'"]
+
+
+@timer
+def parse_command_line(cmd):
+ """Parse command line string and return list of string arguments
+
+ Parse command line string and returns the list of separate
+ arguments as string, this function take in consideration space
+ separator and quoted expression
+
+ Args:
+ cmd: The command line to parce
+
+ Returns:
+ List of arguments as string
+ """
+ words = []
+ rest = cmd
+
+ while True:
+ if len(rest) == 0:
+ break
+
+ word, rest = _parse_next_word(rest)
+ words.append(word)
+
+ return words
+
+
+def _parse_next_word(words):
+ """Parse the next word in words string
+
+ Args:
+ words: The words string
+
+ Returns:
+ the next word and rests of words
+ """
+ if len(words) == 1:
+ return words, ""
+
+ # Remove useless space
+ words = words.strip()
+
+ # Parse
+ if words[0] == "\"":
+ word, rest = _parse_word_up_to_next_sep(words, sep="\"")
+ elif words[0] == "\'":
+ word, rest = _parse_word_up_to_next_sep(words, sep="\'")
+ else:
+ word, rest = _parse_word_up_to_next_sep(words, sep=" ")
+
+ return word, rest
+
+
+def _parse_word_up_to_next_sep(words, sep=" "):
+ word = ""
+
+ i = 0 if sep == " " else 1
+ cur = words[i]
+ skip_next = False
+ while True:
+ # Exit conditions
+ if cur == "":
+ break
+
+ if cur == sep:
+ if not skip_next:
+ break
+
+ # Take in consideration escape char in case of \
+ if cur == "\\":
+ # If previous char is a escape char, cancel next char
+ # skiping:
+ # \ -> skip as separator
+ # \\ -> do not skip
+ skip_next = not skip_next
+ else:
+ skip_next = False
+
+ word += cur
+
+ # Current word contain a word with different separator,
+ # typicaly, the string '-c="foo bar"' with ' ' seperator must
+ # be parse as one word.
+ #
+ # Correct: '-c="foo bar" baz' -> '-c="foo bar"', 'baz'
+ # Not correct: '-c="foo bar" baz' -> '-c="foo', 'bar" baz'
+ if cur in parser_special_char:
+ # Recursive call to parse this word
+ sub_word, rest = _parse_word_up_to_next_sep(words[i:], sep=cur)
+ i += len(sub_word) + 1
+ word += sub_word + cur
+
+ # Get next symbol
+ i += 1
+ if i == len(words):
+ cur = ""
+ else:
+ cur = words[i]
+
+ rest = words[i+1:]
+ return word, rest
diff --git a/tests.sh b/tests.sh
new file mode 100755
index 00000000..06c8fd84
--- /dev/null
+++ b/tests.sh
@@ -0,0 +1,26 @@
+#! /bin/sh
+
+echo " Setup ENV"
+
+python3 -m venv venv
+. venv/bin/activate
+pip3 install -U pip
+pip3 install -r ./full-requirements.txt
+pip3 install -U -r ./full-requirements.txt
+
+echo " UNITTEST"
+
+cd src/
+python3 -m unittest discover -v -t .
+cd ..
+
+echo " PEP8"
+
+pycodestyle ./src
+
+if [ $? -eq 0 ]
+then
+ echo "OK"
+else
+ echo "WARNING"
+fi