From 78eed7af134bd923638fe87b4a62bb2ed5c142e9 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Wed, 22 Jul 2026 15:40:32 +0200 Subject: [PATCH 1/7] SolverParams: quickfix for solverparams copy on scenario creation --- src/Model/SolverParameters/SolverParametersList.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index a0054d8b..d61787d7 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -216,6 +216,9 @@ class Parameter(SQLSubModel): return new def _db_save(self, execute, data=None): + if not self.must_be_saved(): + return True + ind = data["ind"] solver = data["solver"] From 53732744f428e189095fc2f78b4a965f73e80829 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 24 Jul 2026 10:40:52 +0200 Subject: [PATCH 2/7] Meshing: unsigned distance to be able to mesh reach with start_rk > end_rk --- src/Meshing/Internal.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Meshing/Internal.py b/src/Meshing/Internal.py index e597a339..48a84a94 100644 --- a/src/Meshing/Internal.py +++ b/src/Meshing/Internal.py @@ -57,6 +57,13 @@ class InternalMeshing(AMeshingTool): # we never modify original profiles, we work on the copies m_profiles = self.st_to_m(profiles, guide_list) + + # Adapt the spacing to the kilometre-point direction. The value from + # the dialog is a distance, so only its magnitude is significant. + step = self.signed_step( + step, m_profiles[0].rk, m_profiles[-1].rk + ) + new_profiles = self.interpolate_transversal_step(m_profiles, step) for new_profiles2 in new_profiles: @@ -65,6 +72,12 @@ class InternalMeshing(AMeshingTool): return new_profiles + @staticmethod + def signed_step(step, begin_rk, end_rk): + if end_rk < begin_rk: + return -abs(step) + return abs(step) + def st_to_m(self, profiles, guide_list): guide_list = ["un"] + guide_list + ["np"] max_values = [0] * (len(guide_list) - 1) From 1c44590defad850c4d577480e5bdd0189ee72a11 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 24 Jul 2026 10:40:52 +0200 Subject: [PATCH 3/7] Meshing: unsigned distance to be able to mesh reach with start_rk > end_rk --- src/Meshing/Internal.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Meshing/Internal.py b/src/Meshing/Internal.py index e597a339..48a84a94 100644 --- a/src/Meshing/Internal.py +++ b/src/Meshing/Internal.py @@ -57,6 +57,13 @@ class InternalMeshing(AMeshingTool): # we never modify original profiles, we work on the copies m_profiles = self.st_to_m(profiles, guide_list) + + # Adapt the spacing to the kilometre-point direction. The value from + # the dialog is a distance, so only its magnitude is significant. + step = self.signed_step( + step, m_profiles[0].rk, m_profiles[-1].rk + ) + new_profiles = self.interpolate_transversal_step(m_profiles, step) for new_profiles2 in new_profiles: @@ -65,6 +72,12 @@ class InternalMeshing(AMeshingTool): return new_profiles + @staticmethod + def signed_step(step, begin_rk, end_rk): + if end_rk < begin_rk: + return -abs(step) + return abs(step) + def st_to_m(self, profiles, guide_list): guide_list = ["un"] + guide_list + ["np"] max_values = [0] * (len(guide_list) - 1) From 5cf0a0ec7643e550fb33262167fb4815d5410455 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 24 Jul 2026 11:20:33 +0200 Subject: [PATCH 4/7] Mesh: correction to make meshing thread-safe with tab display --- src/View/Geometry/Table.py | 3 +++ src/View/Geometry/Window.py | 12 ++++++++++-- src/View/WaitingDialog.py | 18 ++++++++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index a27dd360..847722d9 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -259,6 +259,9 @@ class GeometryReachTableModel(PamhyrTableModel): **data ) + self.apply_meshing(new_profiles, data, tableView) + + def apply_meshing(self, new_profiles, data, tableView): if new_profiles is None: return diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 987245a4..0827a094 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -350,14 +350,22 @@ class GeometryWindow(PamhyrWindow): tableview = self.tableView def fn(): - self._table.meshing(mesher, data, tableview) - # self._table.meshing(mesher, data, tableview) + return mesher.meshing(self._reach, **data) + dlg2 = WaitingDialog( payload_fn=fn, title="waiting_mesh", parent=self ) dlg2.exec_() + + if dlg2.worker_exception is not None: + raise dlg2.worker_exception + + # Models, undo stacks and selections belong to the GUI thread. + self._table.apply_meshing( + dlg2.worker_result, data, tableview + ) self._table.update() except Exception as e: diff --git a/src/View/WaitingDialog.py b/src/View/WaitingDialog.py index 581d8b2e..4167380e 100644 --- a/src/View/WaitingDialog.py +++ b/src/View/WaitingDialog.py @@ -47,10 +47,16 @@ class Worker(QObject): super(self.__class__, self).__init__(parent) self._payload_fn = payload_fn + self.result = None + self.exception = None def process(self): - self._payload_fn() - self.signalStatus.emit('end') + try: + self.result = self._payload_fn() + except Exception as exception: + self.exception = exception + finally: + self.signalStatus.emit('end') class WaitingDialog(PamhyrDialog): @@ -123,6 +129,14 @@ class WaitingDialog(PamhyrDialog): self._timer = QTimer(self) self._timer.timeout.connect(self.update_spinner) + @property + def worker_result(self): + return self._worker.result + + @property + def worker_exception(self): + return self._worker.exception + def update_spinner(self): self._spinner_step += 1 self._spinner_step %= len(self._spin) From defcb3af84a031918ba29adcd652f33eae1e1a1f Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 24 Jul 2026 13:17:55 +0200 Subject: [PATCH 5/7] BoundaryConditionsData: fix multidelete + relative undo/redo --- src/View/BoundaryCondition/Edit/Table.py | 42 ++++++++++++++----- .../BoundaryCondition/Edit/UndoCommand.py | 31 ++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 5a73fd78..363aa19f 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -143,21 +143,43 @@ class TableModel(PamhyrTableModel): self.update() def delete(self, rows, parent=QModelIndex()): - self.beginRemoveRows(parent, rows[0], rows[-1]) + if not rows: + return - rows = list(map( - lambda r: self.get_true_data_row(r), - rows)) + rows = sorted(set(rows)) + rows = [ + self.get_true_data_row(row) + for row in rows + ] - self._undo.push( - DelCommand( - self._data, rows + # A multiple selection may contain non-contiguous rows. A model reset + # accurately represents this atomic undo command, unlike one + # beginRemoveRows() call spanning rows that are not all removed. + self.beginResetModel() + try: + self._undo.push( + DelCommand( + self._data, rows + ) ) - ) - - self.endRemoveRows() + finally: + self.endResetModel() self.update() + def undo(self): + self.beginResetModel() + try: + self._undo.undo() + finally: + self.endResetModel() + + def redo(self): + self.beginResetModel() + try: + self._undo.redo() + finally: + self.endResetModel() + def sort(self, _reverse, parent=QModelIndex()): self.layoutAboutToBeChanged.emit() diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py index 06b598a1..82c2bca5 100644 --- a/src/View/BoundaryCondition/Edit/UndoCommand.py +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -97,12 +97,11 @@ class DelCommand(QUndoCommand): QUndoCommand.__init__(self) self._data = data - self._rows = rows - - self._bc = [] - for row in rows: - self._bc.append(self._data.get_i(row)) - self._bc.sort() + self._rows = sorted(set(rows)) + self._bc = [ + self._data.get_i(row) + for row in self._rows + ] def undo(self): for el in self._bc: @@ -120,13 +119,17 @@ class SortCommand(QUndoCommand): self._data = data self._reverse = _reverse - self._old = self._data.data - self._indexes = None + # Keep the order of the complete underlying list, including soft + # deleted items. ``data`` only exposes visible items and therefore + # cannot reliably restore a sort after a delete/undelete operation. + self._old_positions = { + id(item): index + for index, item in enumerate(self._data._data) + } def undo(self): - ll = self._data.data self._data.sort( - key=lambda x: self._indexes[ll.index(x)] + key=lambda item: self._old_positions[id(item)] ) def redo(self): @@ -134,14 +137,6 @@ class SortCommand(QUndoCommand): _reverse=self._reverse, key=lambda x: x[0] ) - if self._indexes is None: - self._indexes = list( - map( - lambda p: self._old.index(p), - self._data.data - ) - ) - self._old = None class MoveCommand(QUndoCommand): From 7b2c276643cb7f624138cc2f6fd91bbd594999fa Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 24 Jul 2026 13:53:30 +0200 Subject: [PATCH 6/7] Mage: reset node name registry for each export + display only Mage/Rubar solver in hydraulic solvers list --- src/Solver/Mage.py | 11 ++++++++--- src/View/RunSolver/Window.py | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 1428f583..a7f5740d 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -65,6 +65,8 @@ class Mage(CommandLineSolver): self._cmd_solver = "@path @input -o @output" self._cmd_output = "" + self._reset_node_names() + @classmethod def default_parameters(cls): lst = super(Mage, cls).default_parameters() @@ -125,9 +127,11 @@ class Mage(CommandLineSolver): _l_alph = len(_alph) - _nodes_cnt = 0 - _nodes_names = {} - _nodes_views = set() + def _reset_node_names(self): + """Reset the node-name registry for a new export.""" + self._nodes_cnt = 0 + self._nodes_names = {} + self._nodes_views = set() def get_reach_name(self, reach): index = self._study.river.get_edge_id(reach) + 1 @@ -1005,6 +1009,7 @@ class Mage8(Mage): @timer def export(self, study, repertory, qlog=None, name="0"): self._study = study + self._reset_node_names() name = study.name.replace(" ", "_") # Define GRA file name diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 65e8546b..fe4b2b3f 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -82,7 +82,9 @@ class SelectSolverWindow(PamhyrDialog): # solvers = self._config.solvers # solvers mage solvers = list(filter( - lambda x: "adists" not in x._type, self._config.solvers + lambda x: "adists" not in x._type + and "adistt" not in x._type, + self._config.solvers )) solvers_name = list( map( From ce04f5fd1edd272fd88796521d8719ac80444824 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 24 Jul 2026 14:39:39 +0200 Subject: [PATCH 7/7] PEP8 --- src/View/RunSolver/Window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index fe4b2b3f..48654128 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -83,7 +83,7 @@ class SelectSolverWindow(PamhyrDialog): # solvers mage solvers = list(filter( lambda x: "adists" not in x._type - and "adistt" not in x._type, + and "adistt" not in x._type, self._config.solvers )) solvers_name = list(