From 819ae41c00f8abf0b92e2262811fa6a840bc49b5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Thu, 23 Oct 2025 11:23:44 +0200 Subject: [PATCH 01/12] Results: Add posibility to save multiple results for different solver. --- src/Model/Results/Results.py | 38 ++++++++++++++++++++++---------- src/Model/Results/River/River.py | 2 +- src/Model/River.py | 15 ++++++++----- src/View/MainWindow.py | 6 ++++- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 3ea6e45a..b036f37a 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -52,6 +52,9 @@ class Results(SQLSubModel): "study_revision": study.status.version, } + if solver is not None: + self.set("solver_type", solver._type) + @property def date(self): date = self._meta_data["creation_date"] @@ -147,7 +150,7 @@ class Results(SQLSubModel): ) if table is None: - yield new + yield if len(table) > 1: logger.warning("Multiple results for this scenario") @@ -178,23 +181,32 @@ class Results(SQLSubModel): data["timestamps"] = sorted(ts) new_results._river = River._db_load(execute, data) - new = new_results + yield (solver_type, new_results) - yield new + def _db_save_clear(self, execute, solver_type, data=None): + old_pid = execute( + "SELECT pamhyr_id FROM results " + + f"WHERE scenario = {self._owner_scenario} " + + f"AND solver_type = '{solver_type}'" + ) + if len(old_pid) != 0: + old_pid = old_pid[0] + + execute( + "DELETE FROM results " + + f"WHERE scenario = {self._owner_scenario} " + + f"AND solver_type = '{solver_type}'" + ) + execute( + "DELETE FROM results_data " + + f"WHERE scenario = {self._owner_scenario} " + + f"AND results = '{old_pid}'" + ) def _db_save(self, execute, data=None): if self._status.scenario.id != self._owner_scenario: return - execute( - "DELETE FROM results " + - f"WHERE scenario = {self._owner_scenario}" - ) - execute( - "DELETE FROM results_data " + - f"WHERE scenario = {self._owner_scenario}" - ) - pid = self._pamhyr_id if self._solver is None: solver_name = self.get("solver_name") @@ -203,6 +215,8 @@ class Results(SQLSubModel): solver_name = self._solver._name solver_type = self._solver._type + self._db_save_clear(execute, solver_type, data=data) + ts = sorted(self.get("timestamps")) sf = ">" + ''.join(itertools.repeat("d", len(ts))) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index b94b06d6..d111bf9f 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -227,7 +227,7 @@ class Profile(SQLSubModel): values = list(map(float, values)) sf = ">" + ''.join(itertools.repeat("f", len(values))) len_values = len(values) - elif key is "sl": + elif key == "sl": # HACK: Some dirty code to transforme list of list of # tuple to list of values and ensure the values is # float type... diff --git a/src/Model/River.py b/src/Model/River.py index 2671b12b..e0c4602b 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -505,7 +505,7 @@ class River(Graph): self._D90AdisTS = D90AdisTSList(status=self._status) self._DIFAdisTS = DIFAdisTSList(status=self._status) - self._results = None + self._results = {} @classmethod def _db_create(cls, execute): @@ -620,7 +620,10 @@ class River(Graph): return new def _db_load_results(self, execute, data=None): - self._results = Results._db_load(execute, data) + results_lst = Results._db_load(execute, data) + + for solv_type, results in results_lst: + self._results[solv_type] = results def _db_save(self, execute, data=None): self._db_save_delete_artefact(execute, data) @@ -647,8 +650,8 @@ class River(Graph): objs.append(self._D90AdisTS) objs.append(self._DIFAdisTS) - if self.results is not None: - objs.append(self.results) + for solv_type in self.results: + objs.append(self.results[solv_type]) self._save_submodel(execute, objs, data) return True @@ -902,7 +905,9 @@ Last export at: @date.""" @results.setter def results(self, results): - self._results = results + solv_type = results.get("solver_type") + + self._results[solv_type] = results def _split_reach(self, reach, profile): node1 = reach.node1 diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 0821cd62..dba69b56 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -596,7 +596,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self._study is None: return None - return self._study.results + results = self._study.results + if self._last_solver._type in results: + return self._study.results[self._last_solver._type] + + return None @last_results.setter def last_results(self, results): From ee01beb8449f9f80026e08042ada28b6374624e4 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 24 Oct 2025 11:12:44 +0200 Subject: [PATCH 02/12] Results: Compare: Simplify compare method and add message box on error. --- src/View/MainWindow.py | 121 ++++++++++++++++++++--------------------- src/View/Translate.py | 12 ++++ 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index dba69b56..57b8fe48 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -1710,12 +1710,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): config=self.conf, parent=self ) - if run.exec(): - results = self.diff_results(run.solver1, - run.solver2) - else: + if not run.exec(): return + results = self.diff_results( + run.solver1, run.solver2 + ) + # At least one result not available if results is None: return @@ -1737,97 +1738,95 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) res.show() + def msg_diff_results_param(self): + self.message_box( + window_title=self._trad["Error"], + text=self._trad["mb_diff_results_title"], + informative_text=self._trad["mb_diff_results_param_msg"] + ) + + def msg_diff_results_compatibility(self): + self.message_box( + window_title=self._trad["Error"], + text=self._trad["mb_diff_results_title"], + informative_text=self._trad["mb_diff_results_compatibility_msg"] + ) + def diff_results(self, solver1, solver2): - if solver1 is None: - # TODO message - return None - if solver2 is None: - # TODO message + if solver1 is None or solver2 is None: + self.msg_diff_results_param() return None - solver3 = GenericSolver(solver1.name+" - "+solver2.name) - solver4 = GenericSolver(solver1.name) - solver5 = GenericSolver(solver2.name) + solver3 = GenericSolver(solver1.name + " - " + solver2.name) result1 = solver1.results( - self._study, - self._solver_workdir(solver1), - ) - - if result1 is None: - # TODO message - return None + self._study, + self._solver_workdir(solver1), + ) result2 = solver2.results( - self._study, - self._solver_workdir(solver2), - ) + self._study, + self._solver_workdir(solver2), + ) - if result2 is None: - # TODO message + if result1 is None or result2 is None: + self.msg_diff_results_param() return None if result2.get("nb_reach") != result1.get("nb_reach"): - # TODO message + self.msg_diff_results_compatibility() return None if result2.get("nb_profile") != result1.get("nb_profile"): - # TODO message + self.msg_diff_results_compatibility() return None - # return [result1, result2] + return self._diff_results( + solver1, solver2, solver3, + result1, result2, + ) + + def _diff_results(self, solver1, solver2, solver3, result1, result2): + result3 = Results(study=self._study, solver=solver3) + ts = sorted( + list( + result1.get("timestamps")\ + .intersection(result2.get("timestamps")) + ) + ) - result3 = Results(self._study, solver3) - result4 = Results(self._study, solver1) - result5 = Results(self._study, solver2) result3.set("nb_reach", result1.get("nb_reach")) - result4.set("nb_reach", result1.get("nb_reach")) - result5.set("nb_reach", result1.get("nb_reach")) result3.set("nb_profile", result1.get("nb_profile")) - result4.set("nb_profile", result1.get("nb_profile")) - result5.set("nb_profile", result1.get("nb_profile")) - ts = sorted(list(result1.get("timestamps").intersection( - result2.get("timestamps")))) result3.set("timestamps", ts) - result4.set("timestamps", ts) - result5.set("timestamps", ts) for i in range(int(result1.get("nb_reach"))): - # Add reach to results reach list r = result3.river.add(i) - r = result4.river.add(i) - r = result5.river.add(i) for timestamp in result3.get("timestamps"): for r in range(int(result1.get("nb_reach"))): reach1 = result1.river.reach(r) reach2 = result2.river.reach(r) reach3 = result3.river.reach(r) - reach4 = result4.river.reach(r) - reach5 = result5.river.reach(r) - for p, (profile1, profile2) in enumerate(zip( - reach1.profiles, reach2.profiles)): + + for p, (profile1, profile2) in enumerate( + zip(reach1.profiles, + reach2.profiles)): for key in ["Z", "Q", "V"]: d1 = profile1.get_ts_key(timestamp, key) d2 = profile2.get_ts_key(timestamp, key) d = d1-d2 - reach3.set(p, timestamp, key, d) - reach4.set(p, timestamp, key, d1) - reach5.set(p, timestamp, key, d2) - limits = reach3.profile(p).geometry.get_water_limits( - reach3.profile(p).get_ts_key(timestamp, "Z") - ) - reach3.set( - p, timestamp, - "water_limits", - limits - ) - limits = profile1.get_ts_key(timestamp, "water_limits") - reach4.set(p, timestamp, "water_limits", limits) - limits = profile2.get_ts_key(timestamp, "water_limits") - reach5.set(p, timestamp, "water_limits", limits) - return [result4, result5, result3] + reach3.set(p, timestamp, key, d) + + limits = reach3.profile(p)\ + .geometry\ + .get_water_limits( + reach3.profile(p)\ + .get_ts_key(timestamp, "Z") + ) + reach3.set(p, timestamp, "water_limits", limits) + + return [result1, result2, result3] def open_results_adists(self): if self._study is None: diff --git a/src/View/Translate.py b/src/View/Translate.py index e9a24a4d..30434d4f 100644 --- a/src/View/Translate.py +++ b/src/View/Translate.py @@ -228,6 +228,18 @@ class MainTranslate(UnitTranslate): "Do you still want to open those results?" ) + self._dict["mb_diff_results_title"] = _translate( + "MainWindow", "Results compare" + ) + self._dict["mb_diff_results_param_msg"] = _translate( + "MainWindow", "Results comparison parameters is invalid" + ) + self._dict["mb_diff_results_compatibility_msg"] = _translate( + "MainWindow", + "Results comparison with two " + "incompatible study version" + ) + self._dict["x"] = _translate("MainWindow", "X (m)") self._dict["y"] = _translate("MainWindow", "Y (m)") self._dict["Yes"] = _translate("MainWindow", "Yes") From dc7dfb2414a36f51f5c194e8a58f37add0c7eb40 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 24 Oct 2025 15:02:53 +0200 Subject: [PATCH 03/12] Results: Compare: Minor change. --- src/View/MainWindow.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 57b8fe48..f60d2ca1 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -32,7 +32,7 @@ from platformdirs import user_cache_dir from Solver.AdisTS import AdisTS from Solver.Mage import Mage8 from Solver.RubarBE import Rubar3 -from tools import logger_exception, pamhyr_db_need_update +from tools import logger_exception, pamhyr_db_need_update, timer from PyQt5 import QtGui from PyQt5.QtGui import ( @@ -1752,12 +1752,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): informative_text=self._trad["mb_diff_results_compatibility_msg"] ) + @timer def diff_results(self, solver1, solver2): if solver1 is None or solver2 is None: self.msg_diff_results_param() return None - solver3 = GenericSolver(solver1.name + " - " + solver2.name) + solver3 = GenericSolver(solver1.name + " <> " + solver2.name) result1 = solver1.results( self._study, @@ -1773,11 +1774,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.msg_diff_results_param() return None - if result2.get("nb_reach") != result1.get("nb_reach"): + if result1.get("nb_reach") != result2.get("nb_reach"): self.msg_diff_results_compatibility() return None - if result2.get("nb_profile") != result1.get("nb_profile"): + if result1.get("nb_profile") != result2.get("nb_profile"): self.msg_diff_results_compatibility() return None @@ -1786,6 +1787,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): result1, result2, ) + @timer def _diff_results(self, solver1, solver2, solver3, result1, result2): result3 = Results(study=self._study, solver=solver3) ts = sorted( @@ -1808,23 +1810,22 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): reach2 = result2.river.reach(r) reach3 = result3.river.reach(r) - for p, (profile1, profile2) in enumerate( - zip(reach1.profiles, - reach2.profiles)): + for profile1, profile2, profile3 in zip( + reach1.profiles, + reach2.profiles, + reach3.profiles): for key in ["Z", "Q", "V"]: d1 = profile1.get_ts_key(timestamp, key) d2 = profile2.get_ts_key(timestamp, key) - d = d1-d2 + d = d1 - d2 - reach3.set(p, timestamp, key, d) + profile3.set(timestamp, key, d) - limits = reach3.profile(p)\ - .geometry\ - .get_water_limits( - reach3.profile(p)\ - .get_ts_key(timestamp, "Z") - ) - reach3.set(p, timestamp, "water_limits", limits) + limits = profile3.geometry\ + .get_water_limits( + profile3.get_ts_key(timestamp, "Z") + ) + profile3.set(timestamp, "water_limits", limits) return [result1, result2, result3] From 002ebc5928119ba124a4e8a31904be419a6bc7da Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 24 Oct 2025 15:18:31 +0200 Subject: [PATCH 04/12] Solver: Mage: Fix path name with whitespaces. --- src/Solver/Mage.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index d3e1068f..7501cb4f 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -177,15 +177,15 @@ class Mage(CommandLineSolver): return lst def input_param(self): - name = self._study.name + name = self._study.name.replace(" ", "_") return f"{name}.REP" def output_param(self): - name = self._study.name + name = self._study.name.replace(" ", "_") return f"{name}.BIN" def log_file(self): - name = self._study.name + name = self._study.name.replace(" ", "_") return f"{name}.TRA" def _export_REP_additional_lines(self, study, rep_file): @@ -819,7 +819,8 @@ class Mage(CommandLineSolver): repertory=repertory, name=name, ) - fname = os.path.join(repertory, f"{name}.BIN") + + fname = os.path.join(repertory, f"{name}.BIN".replace(" ", "_")) if not os.path.isfile(fname): logger.info(f"Result file {name}.BIN does not exist") return None From 05e039011afeb75886113d0d516a7d2565e5fd55 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 27 Oct 2025 09:29:33 +0100 Subject: [PATCH 05/12] Scenario: Fix workdir path for last result opening. --- src/Model/Scenario.py | 2 +- src/Solver/Mage.py | 1 + src/View/MainWindow.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Model/Scenario.py b/src/Model/Scenario.py index 8ceb8161..b66faa64 100644 --- a/src/Model/Scenario.py +++ b/src/Model/Scenario.py @@ -276,7 +276,7 @@ class Scenario(SQLSubModel): if self.id != 0: srep = os.path.join( self.parent.workdir(), - "senario_" + str(self.id) + "scenario_" + str(self.id) ) return srep diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 7501cb4f..95f642bf 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -1375,6 +1375,7 @@ class Mage8(Mage): results = super(Mage8, self).results(study, repertory, qlog, name=name) if results is None: return None + if with_gra: self.read_gra(study, repertory, results, qlog, name=name) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f60d2ca1..6306e77e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -1511,7 +1511,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): return False def open_solver_results(self, solver, results=None): - logger.info(f"{solver} {results}") + logger.info(f"Open results for '{solver}' ({results})") def reading_fn(): self._tmp_results = results @@ -1647,6 +1647,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "_PAMHYR_", self._study.name.replace(" ", "_"), solver.name.replace(" ", "_"), + self._study.status.scenario.workdir(), ) return workdir @@ -1870,6 +1871,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): solver_results_adists = solver_results.results( self._study, repertory=dir_path, qlog=None) # self._output) + logger.info(f"Select results: {dir_path}") if len(bin_list) >= 2 and ("total_sediment.bin" in bin_list): self.open_solver_results_adists( From bc08d06bb13e10cb8837940921f3fd8c1755bdf1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 27 Oct 2025 14:49:18 +0100 Subject: [PATCH 06/12] Study: Add copy method to prepare scenario comparison. --- src/Model/Study.py | 106 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/src/Model/Study.py b/src/Model/Study.py index a89416f8..df00579b 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -44,7 +44,10 @@ class Study(SQLModel): River, ] - def __init__(self, filename=None, init_new=True): + def __init__(self, filename=None, init_new=True, copy=False): + if copy: + return + # Metadata self.creation_date = datetime.now() self.last_modification_date = datetime.now() @@ -476,31 +479,8 @@ class Study(SQLModel): return new def reload_from_scenario(self, scenario): - if scenario in self._river_scenario_cache: - self._river = self._river_scenario_cache[scenario] - self.status.scenario = scenario - else: - def sql_exec(sql): - return self.execute( - sql, fetch_one=False, commit=True - ) - - self.status.scenario = scenario - data = { - "status": self.status, - "loaded_pid": set(), - "scenario": scenario - } - - # Reload river data - river = River._db_load( - sql_exec, data=data - ) - data["study"] = self - river._db_load_results(sql_exec, data=data) - - self._river_scenario_cache[scenario] = river - self._river = river + river = self.load_scenario(scenario) + self._river = river if reduce( lambda a, s: a or (s.parent is scenario), @@ -511,6 +491,80 @@ class Study(SQLModel): else: self.status.set_as_editable() + def load_scenario(self, scenario): + if scenario in self._river_scenario_cache: + return self._river_scenario_cache[scenario] + + def sql_exec(sql): + return self.execute( + sql, fetch_one=False, commit=True + ) + + old_scenario = self.status.scenario + self.status.scenario = scenario + + data = { + "status": self.status, + "loaded_pid": set(), + "scenario": scenario + } + + # Load river data + river = River._db_load( + sql_exec, data=data + ) + data["study"] = self + river._db_load_results(sql_exec, data=data) + + self._river_scenario_cache[scenario] = river + self.status.scenario = old_scenario + + return river + + def copy_from_scenario(self, scenario): + new = self._copy() + + new._river = self.load_scenario(scenario) + + def set_status(obj): + obj._status = new.status + + new.river._data_traversal( + modifier=lambda obj, data: set_status(obj), + ) + + return new + + def _copy(self): + """ + This method make a copy of current study. This copy is like an + empty shell, it's not fully functional. Study object use + SQLite connection to file, this copy as no valid connection. + + /!\ Please use this copy as read only object! + """ + new = Study(copy=True) + + new._filename = "" + new.creation_date = self.creation_date + new.last_modification_date = self.last_modification_date + new.last_save_date = self.last_save_date + + new._name = self._name + new.description = self.description + + new._time_system = self._time_system + new._date = self._date + + new.status = StudyStatus() + new.scenarios = self.scenarios + new.status.scenario = self.status.scenario + + new._river = self._river + new._river_scenario_cache = self._river_scenario_cache + + return new + def duplicate_current_scenario(self): source = self.status.scenario_id new = self.scenarios.new( From 188486f33a51173d3f776577083b7c87c0e6e245 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 27 Oct 2025 17:14:29 +0100 Subject: [PATCH 07/12] Results: Compare scenarios. --- src/View/MainWindow.py | 93 +++++++++-- src/View/Results/CompareDialog.py | 253 ++++++++++++++++++++++++++++++ src/View/RunSolver/Window.py | 101 ------------ src/View/ui/CompareScenarios.ui | 79 ++++++++++ src/View/ui/MainWindow.ui | 24 ++- 5 files changed, 426 insertions(+), 124 deletions(-) create mode 100644 src/View/Results/CompareDialog.py create mode 100644 src/View/ui/CompareScenarios.ui diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 6306e77e..ae401e89 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -84,10 +84,12 @@ from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import ( SelectSolverWindow, SolverLogWindow, - CompareSolversWindow ) from View.Results.Window import ResultsWindow +from View.Results.CompareDialog import ( + CompareSolversWindow, CompareScenariosWindow +) from View.RunSolver.WindowAdisTS import ( SelectSolverWindowAdisTS, @@ -128,8 +130,8 @@ no_model_action = [ model_action = [ "action_menu_close", "action_menu_edit", "action_menu_save", "action_menu_save_as", "action_toolBar_close", "action_toolBar_save", - "action_menu_numerical_parameter", "action_open_results_from_file", - "action_open_results_adists", + "action_menu_numerical_parameter", "action_menu_open_results_from_file", + "action_menu_open_results_adists", ] other_model_action = [ @@ -150,8 +152,9 @@ define_model_action = [ "action_menu_run_solver", "action_menu_sediment_layers", "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs", "action_menu_edit_hydraulic_structures", "action_menu_additional_file", - "action_menu_results_last", "action_open_results_from_file", - "action_compare_results", "action_menu_boundary_conditions_sediment", + "action_menu_results_last", "action_menu_open_results_from_file", + "action_menu_compare_results", "action_menu_compare_scenarios_results", + "action_menu_boundary_conditions_sediment", "action_menu_rep_additional_lines", "action_menu_output_rk", "action_menu_run_adists", "action_menu_pollutants", "action_menu_d90", "action_menu_dif", @@ -296,9 +299,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_rep_additional_lines": self.open_rep_lines, "action_menu_close": self.close_model, "action_menu_results_last": self.open_last_results, - "action_open_results_from_file": self.open_results_from_file, - "action_compare_results": self.compare_results, - "action_open_results_adists": self.open_results_adists, + "action_menu_open_results_from_file": self.open_results_from_file, + "action_menu_compare_results": self.compare_results, + "action_menu_compare_scenarios_results": self.compare_results_scenarios, + "action_menu_open_results_adists": self.open_results_adists, # Help "action_menu_pamhyr_users_wiki": self.open_doc_user, "action_menu_pamhyr_developers_pdf": @@ -1641,13 +1645,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) res.show() - def _solver_workdir(self, solver): + def _solver_workdir(self, solver, scenario=None): + if scenario is None: + scenario = self._study.status.scenario + workdir = os.path.join( os.path.dirname(self._study.filename), "_PAMHYR_", self._study.name.replace(" ", "_"), solver.name.replace(" ", "_"), - self._study.status.scenario.workdir(), + scenario.workdir(), ) return workdir @@ -1724,7 +1731,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Windows already opened if self.sub_window_exists( - ResultsWindow, + CompareSolversWindow, data=[self._study, None] + [r._solver for r in results] + [r._repertory for r in results] + @@ -1739,6 +1746,46 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) res.show() + def compare_results_scenarios(self): + if self._study is None: + return + + dlg = CompareScenariosWindow( + study=self._study, + config=self.conf, + parent=self + ) + if not dlg.exec(): + return + + results = self.diff_results( + dlg.solver1, dlg.solver2, + scenario1=dlg.scenario1, + scenario2=dlg.scenario2 + ) + + # At least one result not available + if results is None: + return + + # Windows already opened + if self.sub_window_exists( + CompareScenariosWindow, + data=[self._study, None] + + [r._solver for r in results] + + [r._repertory for r in results] + + [r._name for r in results] + + [dlg.scenario1, dlg.scenario2] + ): + return + + res = ResultsWindow( + study=self._study, + results=results, + parent=self + ) + res.show() + def msg_diff_results_param(self): self.message_box( window_title=self._trad["Error"], @@ -1754,21 +1801,33 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) @timer - def diff_results(self, solver1, solver2): + def diff_results(self, solver1, solver2, + scenario1=None, scenario2=None): if solver1 is None or solver2 is None: self.msg_diff_results_param() return None - solver3 = GenericSolver(solver1.name + " <> " + solver2.name) + study1 = self._study + study2 = self._study + + s3_name = solver1.name + " <> " + solver2.name + + if not (scenario2 is None or scenario2 is None): + study1 = self._study.copy_from_scenario(scenario1) + study2 = self._study.copy_from_scenario(scenario2) + + s3_name = scenario1.name + " <> " + scenario2.name + + solver3 = GenericSolver(s3_name) result1 = solver1.results( - self._study, - self._solver_workdir(solver1), + study1, + self._solver_workdir(solver1, scenario=scenario1), ) result2 = solver2.results( - self._study, - self._solver_workdir(solver2), + study2, + self._solver_workdir(solver2, scenario=scenario2), ) if result1 is None or result2 is None: diff --git a/src/View/Results/CompareDialog.py b/src/View/Results/CompareDialog.py new file mode 100644 index 00000000..d4d0f6b4 --- /dev/null +++ b/src/View/Results/CompareDialog.py @@ -0,0 +1,253 @@ +import os +import logging +import tempfile + +from queue import Queue +from tools import trace, timer, logger_exception + +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, QTimer, QProcess, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, + QTextEdit, +) + +from View.WaitingDialog import WaitingDialog + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + + +class CompareSolversWindow(PamhyrDialog): + _pamhyr_ui = "CompareSolvers" + _pamhyr_name = "Compare solvers" + + def __init__(self, study=None, config=None, + parent=None): + self._solver1 = None + self._solver2 = None + + name = _translate("Solver", "Compare solvers") + super(CompareSolversWindow, self).__init__( + title=name, + study=study, + config=config, + options=[], + parent=parent + ) + + self.setup_solvers() + self.setup_connections() + self.select_last_solver() + + def setup_solvers(self): + solvers = self._config.solvers + solvers_name = list( + map( + self._format_solver_name, + solvers + ) + ) + + self.combobox_add_items("comboBox1", solvers_name) + self.combobox_add_items("comboBox2", solvers_name) + + def setup_connections(self): + self.find(QPushButton, "pushButton_ok").clicked.connect(self.accept) + self.find(QPushButton, "pushButton_cancel")\ + .clicked.connect(self.reject) + + def select_last_solver(self): + solvers = self._config.solvers + last = self._config.last_solver_name + + solver = list( + filter( + lambda s: s.name == last, + solvers + ) + ) + + if len(solver) != 0: + self.set_combobox_text( + "comboBox1", + self._format_solver_name(solver[0]) + ) + + def _format_solver_name(self, solver): + return f"{solver.name} - ({solver._type})" + + @property + def solver1(self): + return self._solver1 + + @property + def solver2(self): + return self._solver2 + + def accept(self): + solver_name1 = self.get_combobox_text("comboBox1") + solver_name1 = solver_name1.rsplit(" - ", 1)[0] + + self._solver1 = next( + filter( + lambda s: s.name == solver_name1, + self._config.solvers + ) + ) + solver_name2 = self.get_combobox_text("comboBox2") + solver_name2 = solver_name2.rsplit(" - ", 1)[0] + + self._solver2 = next( + filter( + lambda s: s.name == solver_name2, + self._config.solvers + ) + ) + + super(CompareSolversWindow, self).accept() + + +class CompareScenariosWindow(PamhyrDialog): + _pamhyr_ui = "CompareScenarios" + _pamhyr_name = "Compare scenarios" + + def __init__(self, study=None, config=None, + parent=None): + self._solver1 = None + self._solver2 = None + self._scenario1 = None + self._scenario2 = None + + name = _translate("Solver", "Compare solvers") + super(CompareScenariosWindow, self).__init__( + title=name, + study=study, + config=config, + options=[], + parent=parent + ) + + self.setup_solvers() + self.setup_scenarios() + self.setup_connections() + self.select_last_solver() + + def setup_solvers(self): + solvers = self._config.solvers + solvers_name = list( + map( + self._format_solver_name, solvers + ) + ) + + self.combobox_add_items("comboBoxSolver1", solvers_name) + self.combobox_add_items("comboBoxSolver2", solvers_name) + + def setup_scenarios(self): + scenarios = self._study.scenarios.lst + scenarios_name = list( + map( + lambda s : s.name, scenarios + ) + ) + + self.combobox_add_items("comboBoxScenario1", scenarios_name) + self.combobox_add_items("comboBoxScenario2", scenarios_name) + + def setup_connections(self): + self.find(QPushButton, "pushButton_ok").clicked.connect(self.accept) + self.find(QPushButton, "pushButton_cancel")\ + .clicked.connect(self.reject) + + def select_last_solver(self): + solvers = self._config.solvers + last = self._config.last_solver_name + + solver = list( + filter( + lambda s: s.name == last, + solvers + ) + ) + + if len(solver) != 0: + self.set_combobox_text( + "comboBoxSolver1", + self._format_solver_name(solver[0]) + ) + self.set_combobox_text( + "comboBoxSolver2", + self._format_solver_name(solver[0]) + ) + + def _format_solver_name(self, solver): + return f"{solver.name} - ({solver._type})" + + @property + def solver1(self): + return self._solver1 + + @property + def solver2(self): + return self._solver2 + + @property + def scenario1(self): + return self._scenario1 + + @property + def scenario2(self): + return self._scenario2 + + def accept(self): + solver_name1 = self.get_combobox_text("comboBoxSolver1") + solver_name1 = solver_name1.rsplit(" - ", 1)[0] + self._solver1 = next( + filter( + lambda s: s.name == solver_name1, + self._config.solvers + ) + ) + + solver_name2 = self.get_combobox_text("comboBoxSolver2") + solver_name2 = solver_name2.rsplit(" - ", 1)[0] + self._solver2 = next( + filter( + lambda s: s.name == solver_name2, + self._config.solvers + ) + ) + + scenario_name1 = self.get_combobox_text("comboBoxScenario1") + self._scenario1 = next( + filter( + lambda s: s.name == scenario_name1, + self._study.scenarios + ) + ) + + scenario_name2 = self.get_combobox_text("comboBoxScenario2") + self._scenario2 = next( + filter( + lambda s: s.name == scenario_name2, + self._study.scenarios + ) + ) + + super(CompareScenariosWindow, self).accept() diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 22b24bbc..64134c7d 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -138,107 +138,6 @@ class SelectSolverWindow(PamhyrDialog): super(SelectSolverWindow, self).accept() -class CompareSolversWindow(PamhyrDialog): - _pamhyr_ui = "CompareSolvers" - _pamhyr_name = "Compare solvers" - - def __init__(self, study=None, config=None, - parent=None): - self._solver1 = None - self._solver2 = None - - name = _translate("Solver", "Compare solvers") - super(CompareSolversWindow, self).__init__( - title=name, - study=study, - config=config, - options=[], - parent=parent - ) - - self.setup_combobox1() - self.setup_combobox2() - self.setup_connections() - self.select_last_solver() - - def setup_combobox1(self): - solvers = self._config.solvers - solvers_name = list( - map( - self._format_solver_name, - solvers - ) - ) - - self.combobox_add_items("comboBox1", solvers_name) - - def setup_combobox2(self): - solvers = self._config.solvers - solvers_name = list( - map( - self._format_solver_name, - solvers - ) - ) - - self.combobox_add_items("comboBox2", solvers_name) - - def setup_connections(self): - self.find(QPushButton, "pushButton_ok").clicked.connect(self.accept) - self.find(QPushButton, "pushButton_cancel")\ - .clicked.connect(self.reject) - - def select_last_solver(self): - solvers = self._config.solvers - last = self._config.last_solver_name - - solver = list( - filter( - lambda s: s.name == last, - solvers - ) - ) - - if len(solver) != 0: - self.set_combobox_text( - "comboBox1", - self._format_solver_name(solver[0]) - ) - - def _format_solver_name(self, solver): - return f"{solver.name} - ({solver._type})" - - @property - def solver1(self): - return self._solver1 - - @property - def solver2(self): - return self._solver2 - - def accept(self): - solver_name1 = self.get_combobox_text("comboBox1") - solver_name1 = solver_name1.rsplit(" - ", 1)[0] - - self._solver1 = next( - filter( - lambda s: s.name == solver_name1, - self._config.solvers - ) - ) - solver_name2 = self.get_combobox_text("comboBox2") - solver_name2 = solver_name2.rsplit(" - ", 1)[0] - - self._solver2 = next( - filter( - lambda s: s.name == solver_name2, - self._config.solvers - ) - ) - - super(CompareSolversWindow, self).accept() - - class SolverLogWindow(PamhyrWindow): _pamhyr_ui = "SolverLog" _pamhyr_name = "Solver Log" diff --git a/src/View/ui/CompareScenarios.ui b/src/View/ui/CompareScenarios.ui new file mode 100644 index 00000000..8f1b18a5 --- /dev/null +++ b/src/View/ui/CompareScenarios.ui @@ -0,0 +1,79 @@ + + + Dialog + + + + 0 + 0 + 488 + 117 + + + + Dialog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + OK + + + + + + + + + + diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 22d1c542..9f1f3b79 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -160,9 +160,10 @@ &Results - - - + + + + @@ -726,7 +727,7 @@ Edit hydraulic structures - + false @@ -787,12 +788,15 @@ DIF - + + + false + Compare results - + false @@ -800,6 +804,14 @@ Open results AdisTS + + + false + + + Compare scenarios results + + From 5ca763ac8c78b1c6cdf231a0514ad426af9c16d2 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 28 Oct 2025 11:09:03 +0100 Subject: [PATCH 08/12] Results: Disable simple compare between solvers to solvers/scenarios. --- src/View/MainWindow.py | 67 ++++++++++++++++++++------------------- src/View/ui/MainWindow.ui | 3 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index ae401e89..c7d65b83 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -153,7 +153,8 @@ define_model_action = [ "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs", "action_menu_edit_hydraulic_structures", "action_menu_additional_file", "action_menu_results_last", "action_menu_open_results_from_file", - "action_menu_compare_results", "action_menu_compare_scenarios_results", + # "action_menu_compare_results", + "action_menu_compare_scenarios_results", "action_menu_boundary_conditions_sediment", "action_menu_rep_additional_lines", "action_menu_output_rk", "action_menu_run_adists", "action_menu_pollutants", @@ -300,7 +301,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_close": self.close_model, "action_menu_results_last": self.open_last_results, "action_menu_open_results_from_file": self.open_results_from_file, - "action_menu_compare_results": self.compare_results, + # "action_menu_compare_results": self.compare_results, "action_menu_compare_scenarios_results": self.compare_results_scenarios, "action_menu_open_results_adists": self.open_results_adists, # Help @@ -1709,42 +1710,42 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): results=file_name[0] ) - def compare_results(self): - if self._study is None: - return + # def compare_results(self): + # if self._study is None: + # return - run = CompareSolversWindow( - study=self._study, - config=self.conf, - parent=self - ) - if not run.exec(): - return + # run = CompareSolversWindow( + # study=self._study, + # config=self.conf, + # parent=self + # ) + # if not run.exec(): + # return - results = self.diff_results( - run.solver1, run.solver2 - ) + # results = self.diff_results( + # run.solver1, run.solver2 + # ) - # At least one result not available - if results is None: - return + # # At least one result not available + # if results is None: + # return - # Windows already opened - if self.sub_window_exists( - CompareSolversWindow, - data=[self._study, None] + - [r._solver for r in results] + - [r._repertory for r in results] + - [r._name for r in results] - ): - return + # # Windows already opened + # if self.sub_window_exists( + # CompareSolversWindow, + # data=[self._study, None] + + # [r._solver for r in results] + + # [r._repertory for r in results] + + # [r._name for r in results] + # ): + # return - res = ResultsWindow( - study=self._study, - results=results, - parent=self - ) - res.show() + # res = ResultsWindow( + # study=self._study, + # results=results, + # parent=self + # ) + # res.show() def compare_results_scenarios(self): if self._study is None: diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 9f1f3b79..96661c4f 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -161,7 +161,6 @@ - @@ -809,7 +808,7 @@ false - Compare scenarios results + Compare results From de180b36359815a991fe1a3f7dfc6e6b5545bf97 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 28 Oct 2025 11:12:45 +0100 Subject: [PATCH 09/12] Pamhyr2: Fix pep8. --- src/Model/Study.py | 2 +- src/View/MainWindow.py | 8 +++++--- src/View/Results/CompareDialog.py | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Model/Study.py b/src/Model/Study.py index df00579b..4e054c24 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -541,7 +541,7 @@ class Study(SQLModel): empty shell, it's not fully functional. Study object use SQLite connection to file, this copy as no valid connection. - /!\ Please use this copy as read only object! + (!) Please use this copy as read only object! """ new = Study(copy=True) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index c7d65b83..db4ec59f 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -302,7 +302,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_results_last": self.open_last_results, "action_menu_open_results_from_file": self.open_results_from_file, # "action_menu_compare_results": self.compare_results, - "action_menu_compare_scenarios_results": self.compare_results_scenarios, + "action_menu_compare_scenarios_results": + self.compare_results_scenarios, "action_menu_open_results_adists": self.open_results_adists, # Help "action_menu_pamhyr_users_wiki": self.open_doc_user, @@ -1853,8 +1854,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): result3 = Results(study=self._study, solver=solver3) ts = sorted( list( - result1.get("timestamps")\ - .intersection(result2.get("timestamps")) + result1.get("timestamps").intersection( + result2.get("timestamps") + ) ) ) diff --git a/src/View/Results/CompareDialog.py b/src/View/Results/CompareDialog.py index d4d0f6b4..b77a133d 100644 --- a/src/View/Results/CompareDialog.py +++ b/src/View/Results/CompareDialog.py @@ -32,7 +32,6 @@ _translate = QCoreApplication.translate logger = logging.getLogger() - class CompareSolversWindow(PamhyrDialog): _pamhyr_ui = "CompareSolvers" _pamhyr_name = "Compare solvers" @@ -163,7 +162,7 @@ class CompareScenariosWindow(PamhyrDialog): scenarios = self._study.scenarios.lst scenarios_name = list( map( - lambda s : s.name, scenarios + lambda s: s.name, scenarios ) ) From 1e6a5788909262a7bfc8d58db0801becd2114970 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 28 Oct 2025 17:01:37 +0100 Subject: [PATCH 10/12] Results: Minor fix. --- src/Model/Results/Results.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index b036f37a..0e0ce377 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -115,7 +115,10 @@ class Results(SQLSubModel): ) """) - return True + if ext != "": + return True + + return cls._create_submodel(execute) @classmethod def _db_update(cls, execute, version, data=None): From de7f3e63b1afc4a9e6ca42850b655de68501c374 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Thu, 30 Oct 2025 09:57:05 +0100 Subject: [PATCH 11/12] BC: Fix BC undo command. --- src/Model/BoundaryCondition/BoundaryCondition.py | 4 ++-- src/View/BoundaryCondition/Edit/Table.py | 10 +++++++--- src/View/BoundaryCondition/Edit/UndoCommand.py | 13 +++++++------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index 14ac5db4..86f2cb6e 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -582,10 +582,10 @@ class BoundaryCondition(SQLSubModel): self._data.index(bc) def get_i(self, index): - if len(self.data) == 0: + if len(self._data) == 0: return None - return self.data[index] + return self._data[index] def get_range(self, _range): lst = [] diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 601a09ac..c0cfa1ed 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -58,7 +58,7 @@ logger = logging.getLogger() class TableModel(PamhyrTableModel): def get_true_data_row(self, row): - bc = self._data.get_i(row) + bc = self._data.data[row] return next( map( @@ -83,6 +83,7 @@ class TableModel(PamhyrTableModel): value = QVariant() if 0 <= column < 2: + row = self.get_true_data_row(row) v = self._data.get_i(row)[column] if self._data.get_type_column(column) == float: if type(v) is str: @@ -106,13 +107,16 @@ class TableModel(PamhyrTableModel): column = index.column() try: + row = self.get_true_data_row(row) + self._undo.push( SetDataCommand( - self._data, row, column, value + self._data, row, + column, value ) ) except Exception as e: - logger.info(e) + logger.warning(e) logger.debug(traceback.format_exc()) self.update() diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py index 5289ef22..986b4b73 100644 --- a/src/View/BoundaryCondition/Edit/UndoCommand.py +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -83,13 +83,13 @@ class AddCommand(QUndoCommand): self._new = None def undo(self): - self._data.delete_i([self._index]) + self._new.set_as_deleted() def redo(self): if self._new is None: self._new = self._data.add(self._index) else: - self._data.insert(self._index, self._new) + self._new.set_as_not_deleted() class DelCommand(QUndoCommand): @@ -101,15 +101,16 @@ class DelCommand(QUndoCommand): self._bc = [] for row in rows: - self._bc.append((row, self._data.get_i(row))) + self._bc.append(self._data.get_i(row)) self._bc.sort() def undo(self): - for row, el in self._bc: - self._data.insert(row, el) + for el in self._bc: + el.set_as_not_deleted() def redo(self): - self._data.delete_i(self._rows) + for el in self._bc: + el.set_as_deleted() class SortCommand(QUndoCommand): From ff9d25412c1f9cf923add893c99834289ec2e264 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Thu, 30 Oct 2025 14:52:09 +0100 Subject: [PATCH 12/12] Results: Fix results_data delete. --- src/Model/Results/Results.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 0e0ce377..3b352bec 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -193,18 +193,19 @@ class Results(SQLSubModel): f"AND solver_type = '{solver_type}'" ) if len(old_pid) != 0: - old_pid = old_pid[0] + for pid in old_pid: + pid = pid[0] - execute( - "DELETE FROM results " + - f"WHERE scenario = {self._owner_scenario} " + - f"AND solver_type = '{solver_type}'" - ) - execute( - "DELETE FROM results_data " + - f"WHERE scenario = {self._owner_scenario} " + - f"AND results = '{old_pid}'" - ) + execute( + "DELETE FROM results " + + f"WHERE scenario = {self._owner_scenario} " + + f"AND solver_type = '{solver_type}'" + ) + execute( + "DELETE FROM results_data " + + f"WHERE scenario = {self._owner_scenario} " + + f"AND result = {pid}" + ) def _db_save(self, execute, data=None): if self._status.scenario.id != self._owner_scenario: