From 0c7fecb1231941c70f03b645b06589792507d6ac Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Thu, 23 Jul 2026 10:10:56 +0200 Subject: [PATCH 1/8] InitialCond: data copy with correct reach affectation after split --- .../InitialConditions/InitialConditions.py | 25 ++++++ .../InitialConditionsDict.py | 25 ++++++ src/Model/River.py | 4 + src/Model/test_Model.py | 86 +++++++++++++++++++ 4 files changed, 140 insertions(+) diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index c295e24e..d0ca7160 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -256,6 +256,17 @@ class Data(SQLSubModel): ) return new + def cloned_for(self, reach, section): + return Data( + name=self._name, + comment=self._comment, + section=section, + discharge=self._discharge, + height=self._height, + reach=reach, + status=self._status, + ) + @property def name(self): return self._name @@ -505,6 +516,20 @@ class InitialConditions(SQLSubModel): for data in self._data: data.set_as_deleted() + def _splited_for(self, reach, sections): + new = InitialConditions(reach=reach, status=self._status) + + new._data = [ + data.cloned_for(reach, sections[data["section"]]) + for data in self.data + if data["section"] in sections + ] + + if len(new._data) != 0: + new.modified() + + return new + def generate_growing_constant_depth(self, height: float, compute_discharge: bool): profiles = self._reach.reach.profiles.copy() diff --git a/src/Model/InitialConditions/InitialConditionsDict.py b/src/Model/InitialConditions/InitialConditionsDict.py index 15403e90..5d66fc8d 100644 --- a/src/Model/InitialConditions/InitialConditionsDict.py +++ b/src/Model/InitialConditions/InitialConditionsDict.py @@ -88,3 +88,28 @@ class InitialConditionsDict(PamhyrModelDict): new = InitialConditions(reach=reach, status=self._status) self.set(reach, new) return new + + def split_reach(self, reach, profile, reach1, reach2): + if reach not in self._dict: + return + + profiles = reach.reach.profiles + split_index = profiles.index(profile) + + sections1 = dict(zip( + profiles[:split_index + 1], + reach1.reach.profiles + )) + sections2 = dict(zip( + profiles[split_index:], + reach2.reach.profiles + )) + + self.set( + reach1, + self._dict[reach]._splited_for(reach1, sections1) + ) + self.set( + reach2, + self._dict[reach]._splited_for(reach2, sections2) + ) diff --git a/src/Model/River.py b/src/Model/River.py index 882a844f..0e29be45 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -979,4 +979,8 @@ Last export at: @date.""" self._add_edge(r1) self._add_edge(r2) + self._initial_conditions.split_reach( + reach, profile, r1, r2 + ) + return r1, r2 diff --git a/src/Model/test_Model.py b/src/Model/test_Model.py index f09cbc8f..56a7728f 100644 --- a/src/Model/test_Model.py +++ b/src/Model/test_Model.py @@ -287,3 +287,89 @@ class RiverTestCase(unittest.TestCase): edges = river.edges() self.assertEqual(edges[0], e0) self.assertEqual(edges[1], e1) + + def test_split_reach_splits_initial_conditions(self): + status = StudyStatus() + river = River(status=status) + node1 = river.add_node() + node2 = river.add_node(x=40.0) + reach = river.add_edge(node1, node2) + + profiles = [] + for index in range(5): + profile = reach.reach.insert(index) + profile.rk = index * 10.0 + profile.insert(0) + profiles.append(profile) + + initial_conditions = river.initial_conditions.get(reach) + for index, profile in enumerate(profiles): + data = initial_conditions.new_from_data( + profile.rk, + discharge=100.0 + index, + elevation=10.0 + index, + ) + initial_conditions.insert(index, data) + + reach1, reach2 = river._split_reach(reach, profiles[2]) + conditions1 = river.initial_conditions.get(reach1).data + conditions2 = river.initial_conditions.get(reach2).data + + self.assertEqual( + [data["discharge"] for data in conditions1], + [100.0, 101.0, 102.0] + ) + self.assertEqual( + [data["discharge"] for data in conditions2], + [102.0, 103.0, 104.0] + ) + self.assertEqual( + [data["section"] for data in conditions1], + reach1.reach.profiles + ) + self.assertEqual( + [data["section"] for data in conditions2], + reach2.reach.profiles + ) + self.assertTrue(all( + data._reach is reach1 for data in conditions1 + )) + self.assertTrue(all( + data._reach is reach2 for data in conditions2 + )) + + def test_split_reach_keeps_partial_initial_conditions_in_scope(self): + status = StudyStatus() + river = River(status=status) + node1 = river.add_node() + node2 = river.add_node(x=40.0) + reach = river.add_edge(node1, node2) + + profiles = [] + for index in range(5): + profile = reach.reach.insert(index) + profile.rk = index * 10.0 + profile.insert(0) + profiles.append(profile) + + initial_conditions = river.initial_conditions.get(reach) + for index in (1, 3): + data = initial_conditions.new_from_data( + profiles[index].rk, + discharge=100.0 + index, + elevation=10.0 + index, + ) + initial_conditions.insert(len(initial_conditions.data), data) + + reach1, reach2 = river._split_reach(reach, profiles[2]) + + self.assertEqual( + [data["discharge"] + for data in river.initial_conditions.get(reach1).data], + [101.0] + ) + self.assertEqual( + [data["discharge"] + for data in river.initial_conditions.get(reach2).data], + [103.0] + ) From e3fbe5958562d2b0168221f955ff731583cda2c5 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Thu, 23 Jul 2026 10:12:01 +0200 Subject: [PATCH 2/8] SQLite debug: files with space in name can now be open --- src/View/MainWindow.py | 6 +----- src/View/test_MainWindow.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/View/test_MainWindow.py diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 38ca8770..dd3e9dee 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -2306,11 +2306,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): logger.debug("No study open for sql debuging...") return - # todo : gérer le cas où le dossier a un espace dans le nom - # (ne veut pas ouvrir sqlitebrowser) - file = self._study.filename _ = subprocess.Popen( - f"sqlitebrowser {file}", - shell=True + ["sqlitebrowser", file] ) diff --git a/src/View/test_MainWindow.py b/src/View/test_MainWindow.py new file mode 100644 index 00000000..338a6734 --- /dev/null +++ b/src/View/test_MainWindow.py @@ -0,0 +1,29 @@ +# test_MainWindow.py -- Pamhyr +# Copyright (C) 2026 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. + +# -*- coding: utf-8 -*- + +import unittest + +from types import SimpleNamespace +from unittest.mock import patch + +from View.MainWindow import ApplicationWindow + + +class MainWindowTestCase(unittest.TestCase): + @patch("View.MainWindow.subprocess.Popen") + def test_open_sqlite_with_spaces_in_study_path(self, popen): + path = "/tmp/a study with spaces/example study.pamhyr" + window = SimpleNamespace( + _study=SimpleNamespace(filename=path) + ) + + ApplicationWindow.open_sqlite(window) + + popen.assert_called_once_with(["sqlitebrowser", path]) From 698e300690415a3fbbb125c48fcf2056b7957fb1 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Thu, 23 Jul 2026 11:54:49 +0200 Subject: [PATCH 3/8] LateralContrib: split correctly reach and lateralContrib + display only enabled reach in tab --- .../LateralContribution.py | 16 ++++ .../LateralContributionList.py | 83 +++++++++++++++++++ src/Model/River.py | 3 + src/Model/test_Model.py | 1 + src/View/LateralContribution/Table.py | 28 ++++--- src/View/LateralContribution/Window.py | 9 +- 6 files changed, 126 insertions(+), 14 deletions(-) diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index 557f7374..5e9e5469 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -196,6 +196,13 @@ class Data(SQLSubModel): def __setitem__(self, key, value): self._data[key] = self._types[key](value) + def cloned(self): + return Data( + self[0], self[1], + types=self._types, + status=self._status + ) + class LateralContribution(SQLSubModel): _sub_classes = [Data] @@ -511,6 +518,15 @@ class LateralContribution(SQLSubModel): def has_reach(self): return self._reach is not None + def cloned_for(self, reach, begin_section, end_section): + new = type(self)(name=self._name, status=self._status) + new._reach = reach + new._begin_section = begin_section + new._end_section = end_section + new._data = [data.cloned() for data in self.data] + new.modified() + return new + @property def begin_rk(self): if self._begin_section is None: diff --git a/src/Model/LateralContribution/LateralContributionList.py b/src/Model/LateralContribution/LateralContributionList.py index 49bffef1..c6aa7162 100644 --- a/src/Model/LateralContribution/LateralContributionList.py +++ b/src/Model/LateralContribution/LateralContributionList.py @@ -71,6 +71,89 @@ class LateralContributionList(PamhyrModelListWithTab): self._status.modified() return n + def split_reach(self, reach, profile, reach1, reach2): + profiles = reach.reach.profiles + split_index = profiles.index(profile) + + parts = [ + ( + reach1, + 0, + split_index, + dict(zip( + profiles[:split_index + 1], + reach1.reach.profiles + )) + ), + ( + reach2, + split_index, + len(profiles) - 1, + dict(zip( + profiles[split_index:], + reach2.reach.profiles + )) + ), + ] + + for tab in self._tabs: + contributions = [ + contribution + for contribution in self.get_tab(tab) + if contribution.reach is reach + ] + + for contribution in contributions: + if (contribution.begin_section not in profiles or + contribution.end_section not in profiles): + continue + + begin = profiles.index(contribution.begin_section) + end = profiles.index(contribution.end_section) + lower, upper = sorted((begin, end)) + reverse = begin > end + clones = [] + + for new_reach, part_lower, part_upper, sections in parts: + clipped_lower = max(lower, part_lower) + clipped_upper = min(upper, part_upper) + if clipped_lower > clipped_upper: + continue + + begin_index = ( + clipped_upper if reverse else clipped_lower + ) + end_index = ( + clipped_lower if reverse else clipped_upper + ) + clones.append(contribution.cloned_for( + new_reach, + sections[profiles[begin_index]], + sections[profiles[end_index]] + )) + + if len(clones) == 0: + continue + + self._tabs[tab].extend(clones) + + self._status.modified() + + def get_tab_for_enabled_reaches(self, tab): + return [ + contribution + for contribution in self.get_tab(tab) + if (contribution.reach is None or + (not contribution.reach.is_deleted() and + contribution.reach.is_enable())) + ] + + def get_for_enabled_reaches(self, tab, index): + contributions = self.get_tab_for_enabled_reaches(tab) + if not 0 <= index < len(contributions): + return None + return contributions[index] + def __copy__(self): new = LateralContributionList() diff --git a/src/Model/River.py b/src/Model/River.py index 0e29be45..a6643c3e 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -982,5 +982,8 @@ Last export at: @date.""" self._initial_conditions.split_reach( reach, profile, r1, r2 ) + self._lateral_contribution.split_reach( + reach, profile, r1, r2 + ) return r1, r2 diff --git a/src/Model/test_Model.py b/src/Model/test_Model.py index 56a7728f..f42f2a67 100644 --- a/src/Model/test_Model.py +++ b/src/Model/test_Model.py @@ -23,6 +23,7 @@ import tempfile from Model.Status import StudyStatus from Model.Study import Study from Model.River import River +from Model.LateralContribution.LateralContributionTypes import LateralContrib class StudyTestCase(unittest.TestCase): diff --git a/src/View/LateralContribution/Table.py b/src/View/LateralContribution/Table.py index a7319cf0..f76bea3c 100644 --- a/src/View/LateralContribution/Table.py +++ b/src/View/LateralContribution/Table.py @@ -103,7 +103,10 @@ class ComboBoxDelegate(QItemDelegate): else: self.editor.addItems( [self._trad['not_associated']] + - self._data.edges_names() + [ + reach.name + for reach in self._data.enable_edges() + ] ) self.editor.setCurrentText(index.data(Qt.DisplayRole)) @@ -154,7 +157,9 @@ class TableModel(PamhyrTableModel): self._long_types = self._trad.get_dict("long_types") def get_true_data_row(self, row): - lc = self._lst.get(self._tab, row) + lc = self._lst.get_for_enabled_reaches(self._tab, row) + if lc is None: + return len(self._lst.get_tab(self._tab)) return next( map( @@ -167,7 +172,7 @@ class TableModel(PamhyrTableModel): ) def rowCount(self, parent): - return self._lst.len(self._tab) + return len(self._lst.get_tab_for_enabled_reaches(self._tab)) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -175,21 +180,22 @@ class TableModel(PamhyrTableModel): row = index.row() column = index.column() + contribution = self._lst.get_for_enabled_reaches(self._tab, row) if self._headers[column] == "name": - return self._lst.get(self._tab, row).name + return contribution.name elif self._headers[column] == "type": - t = self._lst.get(self._tab, row).lctype + t = contribution.lctype return self._long_types[t] elif self._headers[column] == "edge": - n = self._lst.get(self._tab, row).reach + n = contribution.reach if n is None: return self._trad['not_associated'] return n.name elif self._headers[column] == "begin_rk": - return str(self._lst.get(self._tab, row).begin_rk) + return str(contribution.begin_rk) elif self._headers[column] == "end_rk": - return str(self._lst.get(self._tab, row).end_rk) + return str(contribution.end_rk) return QVariant() @@ -202,6 +208,8 @@ class TableModel(PamhyrTableModel): row = index.row() column = index.column() + contribution = self._lst.get_for_enabled_reaches(self._tab, row) + row = self.get_true_data_row(row) try: if self._headers[column] == "name": @@ -225,7 +233,7 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "begin_rk": - _edge = self._lst.get(self._tab, row).reach + _edge = contribution.reach _begin_rk = next( p for p in _edge.reach.profiles if p.pamhyr_id == value @@ -236,7 +244,7 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "end_rk": - _edge = self._lst.get(self._tab, row).reach + _edge = contribution.reach _end_rk = next( p for p in _edge.reach.profiles if p.pamhyr_id == value diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index e69fd58e..2cc857d9 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -213,11 +213,11 @@ class LateralContributionWindow(PamhyrWindow): if len(rows) > 0: edge = self._study.river\ .lateral_contribution\ - .get(tab, rows[0])\ + .get_for_enabled_reaches(tab, rows[0])\ .reach if edge: data = edge.reach - lc = self._lcs.get(tab, rows[0]) + lc = self._lcs.get_for_enabled_reaches(tab, rows[0]) highlight = (lc.begin_rk, lc.end_rk) for delegate in self._delegate_rk: @@ -236,7 +236,8 @@ class LateralContributionWindow(PamhyrWindow): def add(self): tab = self.current_tab() rows = self.index_selected_rows() - if self._lcs.len(tab) == 0 or len(rows) == 0: + visible = self._lcs.get_tab_for_enabled_reaches(tab) + if len(visible) == 0 or len(rows) == 0: self._table[tab].add(0) else: self._table[tab].add(rows[0]) @@ -291,7 +292,7 @@ class LateralContributionWindow(PamhyrWindow): tab = self.current_tab() rows = self.index_selected_rows() for row in rows: - data = self._lcs.get(tab, row) + data = self._lcs.get_for_enabled_reaches(tab, row) if self.sub_window_exists( EditLateralContributionWindow, From 8e3d784e926c4a6aef6fe8ff09860a4b01d8f293 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Thu, 23 Jul 2026 13:54:25 +0200 Subject: [PATCH 4/8] HydraulicStructures: split correctly reach and Hydraulic Structures + display only enabled reach in tab --- .../Basic/HydraulicStructures.py | 7 ++ src/Model/HydraulicStructures/Basic/Value.py | 8 +++ .../HydraulicStructures.py | 12 ++++ .../HydraulicStructuresList.py | 65 +++++++++++++++++++ src/Model/River.py | 3 + src/View/HydraulicStructures/Table.py | 22 ++++--- src/View/HydraulicStructures/Window.py | 15 +++-- 7 files changed, 117 insertions(+), 15 deletions(-) diff --git a/src/Model/HydraulicStructures/Basic/HydraulicStructures.py b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py index fcffeddf..b84f22b5 100644 --- a/src/Model/HydraulicStructures/Basic/HydraulicStructures.py +++ b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py @@ -277,3 +277,10 @@ class BasicHS(SQLSubModel): def convert(self, new_type): return new_type(id=self.id, name=self.name, status=self._status) + + def cloned(self): + new = type(self)(name=self._name, status=self._status) + new._enabled = self._enabled + new._data = [value.cloned() for value in self._data] + new.modified() + return new diff --git a/src/Model/HydraulicStructures/Basic/Value.py b/src/Model/HydraulicStructures/Basic/Value.py index bdf03de5..907a81c7 100644 --- a/src/Model/HydraulicStructures/Basic/Value.py +++ b/src/Model/HydraulicStructures/Basic/Value.py @@ -208,3 +208,11 @@ class BHSValue(SQLSubModel): def value(self, value): self._value = self._type(value) self.modified() + + def cloned(self): + return BHSValue( + name=self._name, + type=self._type, + value=self._value, + status=self._status + ) diff --git a/src/Model/HydraulicStructures/HydraulicStructures.py b/src/Model/HydraulicStructures/HydraulicStructures.py index f07150c5..e3ed5155 100644 --- a/src/Model/HydraulicStructures/HydraulicStructures.py +++ b/src/Model/HydraulicStructures/HydraulicStructures.py @@ -427,6 +427,18 @@ class HydraulicStructure(SQLSubModel): def basic_structures(self): return self.lst.copy() + def cloned_for(self, input_reach, input_section, + output_reach, output_section): + new = HydraulicStructure(name=self._name, status=self._status) + new._enabled = self._enabled + new._input_reach = input_reach + new._input_section = input_section + new._output_reach = output_reach + new._output_section = output_section + new._data = [structure.cloned() for structure in self.lst] + new.modified() + return new + def basic_structure(self, index: int): if len(self._data) == 0: return None diff --git a/src/Model/HydraulicStructures/HydraulicStructuresList.py b/src/Model/HydraulicStructures/HydraulicStructuresList.py index 1c66dabf..2d1b347c 100644 --- a/src/Model/HydraulicStructures/HydraulicStructuresList.py +++ b/src/Model/HydraulicStructures/HydraulicStructuresList.py @@ -61,6 +61,71 @@ class HydraulicStructureList(PamhyrModelList): self.modified() return n + def split_reach(self, reach, profile, reach1, reach2): + profiles = reach.reach.profiles + split_index = profiles.index(profile) + sections1 = dict(zip( + profiles[:split_index + 1], + reach1.reach.profiles + )) + sections2 = dict(zip( + profiles[split_index:], + reach2.reach.profiles + )) + + def split_endpoint(endpoint_reach, section): + if endpoint_reach is not reach: + return endpoint_reach, section + if section not in profiles: + return endpoint_reach, section + + index = profiles.index(section) + if index < split_index: + return reach1, sections1[section] + return reach2, sections2[section] + + structures = [ + structure + for structure in self.lst + if (structure.input_reach is reach or + structure.output_reach is reach) + ] + + for structure in structures: + input_reach, input_section = split_endpoint( + structure.input_reach, + structure.input_section + ) + output_reach, output_section = split_endpoint( + structure.output_reach, + structure.output_section + ) + self._lst.append(structure.cloned_for( + input_reach, + input_section, + output_reach, + output_section + )) + + if len(structures) != 0: + self.modified() + + @property + def enabled_reaches_list(self): + return [ + structure + for structure in self.lst + if (structure.input_reach is None or + (not structure.input_reach.is_deleted() and + structure.input_reach.is_enable())) + ] + + def get_for_enabled_reaches(self, index): + structures = self.enabled_reaches_list + if not 0 <= index < len(structures): + return None + return structures[index] + def __copy__(self): new = HydraulicStructureList() diff --git a/src/Model/River.py b/src/Model/River.py index a6643c3e..abfc2be5 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -985,5 +985,8 @@ Last export at: @date.""" self._lateral_contribution.split_reach( reach, profile, r1, r2 ) + self._hydraulic_structures.split_reach( + reach, profile, r1, r2 + ) return r1, r2 diff --git a/src/View/HydraulicStructures/Table.py b/src/View/HydraulicStructures/Table.py index 5947b9b0..13a3057e 100644 --- a/src/View/HydraulicStructures/Table.py +++ b/src/View/HydraulicStructures/Table.py @@ -61,7 +61,7 @@ class ComboBoxDelegate(QItemDelegate): val = [] if self._mode == "rk": reach = self._data.hydraulic_structures\ - .get(index.row())\ + .get_for_enabled_reaches(index.row())\ .input_reach if reach is not None: val = list( @@ -73,7 +73,7 @@ class ComboBoxDelegate(QItemDelegate): else: val = list( map( - lambda n: n.name, self._data.edges() + lambda n: n.name, self._data.enable_edges() ) ) @@ -94,7 +94,7 @@ class ComboBoxDelegate(QItemDelegate): if self._mode == "rk": reach = self._data.hydraulic_structures\ - .get(index.row())\ + .get_for_enabled_reaches(index.row())\ .input_reach profiles = list( filter( @@ -128,20 +128,22 @@ class TableModel(PamhyrTableModel): self._lst = self._data._hydraulic_structures def get_true_data_row(self, row): - hs = self._lst.get(row) + hs = self._lst.get_for_enabled_reaches(row) + if hs is None: + return len(self._lst.lst) return next( map( lambda e: e[0], filter( lambda e: e[1] == hs, - enumerate(self._lst._lst) + enumerate(self._lst.lst) ) ), 0 ) def rowCount(self, parent): - return len(self._lst) + return len(self._lst.enabled_reaches_list) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -149,16 +151,17 @@ class TableModel(PamhyrTableModel): row = index.row() column = index.column() + structure = self._lst.get_for_enabled_reaches(row) if self._headers[column] == "name": - return self._lst.get(row).name + return structure.name elif self._headers[column] == "reach": - n = self._lst.get(row).input_reach + n = structure.input_reach if n is None: return self._trad['not_associated'] return n.name elif self._headers[column] == "rk": - n = self._lst.get(row).input_section + n = structure.input_section if n is None: return self._trad['not_associated'] return n.display_name() @@ -174,6 +177,7 @@ class TableModel(PamhyrTableModel): row = index.row() column = index.column() + row = self.get_true_data_row(row) na = self._trad['not_associated'] try: diff --git a/src/View/HydraulicStructures/Window.py b/src/View/HydraulicStructures/Window.py index 3cb4a126..734b1a65 100644 --- a/src/View/HydraulicStructures/Window.py +++ b/src/View/HydraulicStructures/Window.py @@ -220,7 +220,7 @@ class HydraulicStructuresWindow(PamhyrWindow): def add(self): rows = self.index_selected_rows() - if len(self._hs_lst) == 0 or len(rows) == 0: + if len(self._hs_lst.enabled_reaches_list) == 0 or len(rows) == 0: self._table.add(0) else: self._table.add(rows[0]) @@ -247,7 +247,7 @@ class HydraulicStructuresWindow(PamhyrWindow): def edit(self): rows = self.index_selected_rows() for row in rows: - data = self._hs_lst.get(row) + data = self._hs_lst.get_for_enabled_reaches(row) if self.sub_window_exists( BasicHydraulicStructuresWindow, @@ -269,7 +269,8 @@ class HydraulicStructuresWindow(PamhyrWindow): self._checkbox.setChecked(True) else: self._checkbox.setEnabled(True) - self._checkbox.setChecked(self._hs_lst.get(row).enabled) + structure = self._hs_lst.get_for_enabled_reaches(row) + self._checkbox.setChecked(structure.enabled) def _set_structure_state(self): rows = self.index_selected_rows() @@ -288,16 +289,18 @@ class HydraulicStructuresWindow(PamhyrWindow): def _update_clear_plot(self): rows = self.index_selected_rows() - if len(rows) == 0 or len(self._hs_lst) == 0: + if (len(rows) == 0 or + len(self._hs_lst.enabled_reaches_list) == 0): self._update_clear_all() return - reach = self._hs_lst.get(rows[0]).input_reach + structure = self._hs_lst.get_for_enabled_reaches(rows[0]) + reach = structure.input_reach if reach is not None: self.plot_rkc.set_reach(reach) self.plot_ac.set_reach(reach) - profile = self._hs_lst.get(rows[0]).input_section + profile = structure.input_section if profile is not None: profiles = reach.reach\ .get_profiles_from_rk( From 6175e8a03ead3f7e1fc74862851ca8e56d25f00a Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Thu, 23 Jul 2026 15:39:07 +0200 Subject: [PATCH 5/8] Reach sediments: avoid crash when trying to open Edit_reach_sediment_layer without any reach selected --- src/Model/test_Model.py | 1 + src/View/MainWindow.py | 10 ++++++++++ src/View/test_MainWindow.py | 29 ----------------------------- 3 files changed, 11 insertions(+), 29 deletions(-) delete mode 100644 src/View/test_MainWindow.py diff --git a/src/Model/test_Model.py b/src/Model/test_Model.py index f42f2a67..598cfe60 100644 --- a/src/Model/test_Model.py +++ b/src/Model/test_Model.py @@ -24,6 +24,7 @@ from Model.Status import StudyStatus from Model.Study import Study from Model.River import River from Model.LateralContribution.LateralContributionTypes import LateralContrib +from Model.HydraulicStructures.Basic.Types import DischargeWeir class StudyTestCase(unittest.TestCase): diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index dd3e9dee..5ae5da9d 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -1525,6 +1525,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): sl.show() def open_reach_sediment_layers(self): + if self._study is None: + logger.debug( + "No study open for reach sediment layers edition..." + ) + return + + if not self._study.river.has_current_reach(): + self.msg_select_reach() + return + reach = self._study.river.current_reach().reach if self.sub_window_exists( diff --git a/src/View/test_MainWindow.py b/src/View/test_MainWindow.py deleted file mode 100644 index 338a6734..00000000 --- a/src/View/test_MainWindow.py +++ /dev/null @@ -1,29 +0,0 @@ -# test_MainWindow.py -- Pamhyr -# Copyright (C) 2026 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. - -# -*- coding: utf-8 -*- - -import unittest - -from types import SimpleNamespace -from unittest.mock import patch - -from View.MainWindow import ApplicationWindow - - -class MainWindowTestCase(unittest.TestCase): - @patch("View.MainWindow.subprocess.Popen") - def test_open_sqlite_with_spaces_in_study_path(self, popen): - path = "/tmp/a study with spaces/example study.pamhyr" - window = SimpleNamespace( - _study=SimpleNamespace(filename=path) - ) - - ApplicationWindow.open_sqlite(window) - - popen.assert_called_once_with(["sqlitebrowser", path]) From 5e95c286a9a2e5fccc81b6de0b1b961cc790e238 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Thu, 23 Jul 2026 15:57:50 +0200 Subject: [PATCH 6/8] InitialConditionsAdisTS: split correctly reach and InitialConditionsAdisTS + display only enabled reach in tab --- .../InitialConditionsAdisTS.py | 57 +++++++++++++++++++ .../InitialConditionsAdisTSList.py | 6 ++ .../InitialConditionsAdisTSSpec.py | 44 +++++++++----- src/Model/River.py | 3 + src/View/InitialConditionsAdisTS/Table.py | 23 +++++--- src/View/InitialConditionsAdisTS/Window.py | 9 ++- 6 files changed, 117 insertions(+), 25 deletions(-) diff --git a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTS.py b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTS.py index 5dac7d8d..65a72e90 100644 --- a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTS.py +++ b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTS.py @@ -359,3 +359,60 @@ class InitialConditionsAdisTS(SQLSubModel): x.set_as_not_deleted() self.modified() + + def split_reach(self, reach, profile, reach1, reach2): + split_rk = profile.rk + reach_rks = reach.reach.get_rk() + if len(reach_rks) == 0: + return + + lower_reach_rk = min(reach_rks) + upper_reach_rk = max(reach_rks) + parts = [ + (reach1.id, lower_reach_rk, split_rk), + (reach2.id, split_rk, upper_reach_rk), + ] + specifications = [ + specification + for specification in self._data + if (not specification.is_deleted() and + specification.reach == reach.id) + ] + + for specification in specifications: + begin = specification.start_rk + end = specification.end_rk + if begin is None or end is None: + continue + + lower, upper = sorted((begin, end)) + reverse = begin > end + + for new_reach, part_lower, part_upper in parts: + clipped_lower = max(lower, part_lower) + clipped_upper = min(upper, part_upper) + if clipped_lower > clipped_upper: + continue + + start_rk = clipped_upper if reverse else clipped_lower + end_rk = clipped_lower if reverse else clipped_upper + self._data.append(specification.cloned_for( + new_reach, start_rk, end_rk + )) + + if len(specifications) != 0: + self.modified() + + def get_specs_for_enabled_reaches(self, reaches): + enabled_reaches = { + reach.id + for reach in reaches + if not reach.is_deleted() and reach.is_enable() + } + return [ + specification + for specification in self._data + if (not specification.is_deleted() and + (specification.reach in (None, -1) or + specification.reach in enabled_reaches)) + ] diff --git a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py index 1d5c6726..274a22f1 100644 --- a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py +++ b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py @@ -62,6 +62,12 @@ class InitialConditionsAdisTSList(PamhyrModelList): self._status.modified() return n + def split_reach(self, reach, profile, reach1, reach2): + for initial_condition in self.lst: + initial_condition.split_reach( + reach, profile, reach1, reach2 + ) + @property def Initial_Conditions_List(self): return self.lst diff --git a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py index 8cd963d1..89277bee 100644 --- a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py +++ b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py @@ -31,10 +31,12 @@ class ICAdisTSSpec(SQLSubModel): _sub_classes = [] def __init__(self, id: int = -1, name: str = "", - status=None, owner_scenario=None): - super(ICAdisTSSpec, self).__init__() - - self._status = status + status=None, owner_scenario=-1): + super(ICAdisTSSpec, self).__init__( + id=id, + status=status, + owner_scenario=owner_scenario + ) self._name_section = name self._reach = None @@ -234,7 +236,7 @@ class ICAdisTSSpec(SQLSubModel): @name.setter def name(self, name): self._name_section = name - self._status.modified() + self.modified() @property def reach(self): @@ -243,7 +245,7 @@ class ICAdisTSSpec(SQLSubModel): @reach.setter def reach(self, reach): self._reach = reach - self._status.modified() + self.modified() @property def start_rk(self): @@ -252,7 +254,7 @@ class ICAdisTSSpec(SQLSubModel): @start_rk.setter def start_rk(self, start_rk): self._start_rk = start_rk - self._status.modified() + self.modified() @property def end_rk(self): @@ -261,7 +263,7 @@ class ICAdisTSSpec(SQLSubModel): @end_rk.setter def end_rk(self, end_rk): self._end_rk = end_rk - self._status.modified() + self.modified() @property def concentration(self): @@ -270,7 +272,7 @@ class ICAdisTSSpec(SQLSubModel): @concentration.setter def concentration(self, concentration): self._concentration = concentration - self._status.modified() + self.modified() @property def eg(self): @@ -279,7 +281,7 @@ class ICAdisTSSpec(SQLSubModel): @eg.setter def eg(self, eg): self._eg = eg - self._status.modified() + self.modified() @property def em(self): @@ -288,7 +290,7 @@ class ICAdisTSSpec(SQLSubModel): @em.setter def em(self, em): self._em = em - self._status.modified() + self.modified() @property def ed(self): @@ -297,7 +299,7 @@ class ICAdisTSSpec(SQLSubModel): @ed.setter def ed(self, ed): self._ed = ed - self._status.modified() + self.modified() @property def rate(self): @@ -306,7 +308,7 @@ class ICAdisTSSpec(SQLSubModel): @rate.setter def rate(self, rate): self._rate = rate - self._status.modified() + self.modified() @property def enabled(self): @@ -315,4 +317,18 @@ class ICAdisTSSpec(SQLSubModel): @enabled.setter def enabled(self, enabled): self._enabled = enabled - self._status.modified() + self.modified() + + def cloned_for(self, reach, start_rk, end_rk): + new = ICAdisTSSpec(name=self._name_section, status=self._status) + new._reach = reach + new._start_rk = start_rk + new._end_rk = end_rk + new._concentration = self._concentration + new._eg = self._eg + new._em = self._em + new._ed = self._ed + new._rate = self._rate + new._enabled = self._enabled + new.modified() + return new diff --git a/src/Model/River.py b/src/Model/River.py index abfc2be5..286aeef5 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -988,5 +988,8 @@ Last export at: @date.""" self._hydraulic_structures.split_reach( reach, profile, r1, r2 ) + self._InitialConditionsAdisTS.split_reach( + reach, profile, r1, r2 + ) return r1, r2 diff --git a/src/View/InitialConditionsAdisTS/Table.py b/src/View/InitialConditionsAdisTS/Table.py index 70316e20..83864d57 100644 --- a/src/View/InitialConditionsAdisTS/Table.py +++ b/src/View/InitialConditionsAdisTS/Table.py @@ -55,17 +55,27 @@ class ComboBoxDelegate(QItemDelegate): self._trad = trad self._ic_spec_lst = ic_spec_lst + def _specifications(self): + if hasattr( + self._ic_spec_lst, + "get_specs_for_enabled_reaches" + ): + return self._ic_spec_lst.get_specs_for_enabled_reaches( + self._data.edges() + ) + return self._ic_spec_lst + def createEditor(self, parent, option, index): self.editor = QComboBox(parent) val = [] if self._mode == "rk": - reach_id = self._ic_spec_lst[index.row()].reach + reach_id = self._specifications()[index.row()].reach reach = next(filter(lambda edge: edge.id == reach_id, self._data.edges()), None) - if reach_id is not None: + if reach is not None: val = list( map( lambda rk: str(rk), reach.reach.get_rk() @@ -74,7 +84,7 @@ class ComboBoxDelegate(QItemDelegate): else: val = list( map( - lambda n: n.name, self._data.edges() + lambda n: n.name, self._data.enable_edges() ) ) @@ -117,11 +127,8 @@ class InitialConditionTableModel(PamhyrTableModel): self._data = data def _setup_lst(self): - self._lst = list( - filter( - lambda ica: ica._deleted is False, - self._data._data - ) + self._lst = self._data.get_specs_for_enabled_reaches( + self._river.edges() ) def rowCount(self, parent): diff --git a/src/View/InitialConditionsAdisTS/Window.py b/src/View/InitialConditionsAdisTS/Window.py index c4938d2b..9c11d0c9 100644 --- a/src/View/InitialConditionsAdisTS/Window.py +++ b/src/View/InitialConditionsAdisTS/Window.py @@ -152,14 +152,14 @@ class InitialConditionsAdisTSWindow(PamhyrWindow): self._delegate_reach = ComboBoxDelegate( trad=self._trad, data=self._study.river, - ic_spec_lst=self._data[0]._data, + ic_spec_lst=self._data[0], parent=self, mode="reaches" ) self._delegate_rk = ComboBoxDelegate( trad=self._trad, data=self._study.river, - ic_spec_lst=self._data[0]._data, + ic_spec_lst=self._data[0], parent=self, mode="rk" ) @@ -308,7 +308,10 @@ class InitialConditionsAdisTSWindow(PamhyrWindow): def add(self): rows = self.index_selected_rows() - if len(self._data[0]._data) == 0 or len(rows) == 0: + visible = self._data[0].get_specs_for_enabled_reaches( + self._study.river.edges() + ) + if len(visible) == 0 or len(rows) == 0: self._table_spec.add(0) else: self._table_spec.add(rows[0]) From bbdd18e8eff4e3b8bc330e4a215b1a05487609a3 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 24 Jul 2026 15:35:12 +0200 Subject: [PATCH 7/8] LateralContributions: propagate LAT when splitting reach + display only enabled reach in combobox --- .../LateralContributionAdisTS.py | 19 ++++++++ .../LateralContributionsAdisTSList.py | 40 ++++++++++++++++ src/Model/River.py | 3 ++ src/Solver/AdisTS.py | 2 +- src/View/D90AdisTS/Table.py | 2 +- src/View/DIFAdisTS/Table.py | 2 +- .../InitialConditionsTemperature/Table.py | 2 +- src/View/LateralContributionsAdisTS/Table.py | 47 +++++++++++++++++-- .../LateralContributionsAdisTS/UndoCommand.py | 16 +++++-- src/View/LateralContributionsAdisTS/Window.py | 30 ++++++++---- src/View/WeatherParameters/Table.py | 5 +- 11 files changed, 148 insertions(+), 20 deletions(-) diff --git a/src/Model/LateralContributionsAdisTS/LateralContributionAdisTS.py b/src/Model/LateralContributionsAdisTS/LateralContributionAdisTS.py index b0deda44..2b3faa6f 100644 --- a/src/Model/LateralContributionsAdisTS/LateralContributionAdisTS.py +++ b/src/Model/LateralContributionsAdisTS/LateralContributionAdisTS.py @@ -192,6 +192,13 @@ class Data(SQLSubModel): def __setitem__(self, key, value): self._data[key] = self._types[key](value) + def cloned(self): + return Data( + self[0], self[1], + types=self._types, + status=self._status + ) + class LateralContributionAdisTS(SQLSubModel): _sub_classes = [Data] @@ -477,6 +484,18 @@ class LateralContributionAdisTS(SQLSubModel): self._end_rk = end_rk self.modified() + def cloned_for(self, reach, begin_rk, end_rk): + new = LateralContributionAdisTS( + pollutant=self._pollutant, + status=self._status + ) + new._reach = reach + new._begin_rk = begin_rk + new._end_rk = end_rk + new._data = [data.cloned() for data in self.data] + new.modified() + return new + @property def _default_0(self): return self._types[0](0) diff --git a/src/Model/LateralContributionsAdisTS/LateralContributionsAdisTSList.py b/src/Model/LateralContributionsAdisTS/LateralContributionsAdisTSList.py index 2baad522..ad9d2783 100644 --- a/src/Model/LateralContributionsAdisTS/LateralContributionsAdisTSList.py +++ b/src/Model/LateralContributionsAdisTS/LateralContributionsAdisTSList.py @@ -68,6 +68,46 @@ class LateralContributionsAdisTSList(PamhyrModelList): self._status.modified() return n + def split_reach(self, reach, profile, reach1, reach2): + parts = [] + for new_reach in (reach1, reach2): + rks = new_reach.reach.get_rk() + if len(rks) == 0: + continue + parts.append((new_reach.id, min(rks), max(rks))) + + contributions = [ + contribution + for contribution in self.lst + if contribution.reach == reach.id + ] + clones = [] + + for contribution in contributions: + begin = contribution.begin_rk + end = contribution.end_rk + if begin is None or end is None: + continue + + lower, upper = sorted((begin, end)) + reverse = begin > end + + for new_reach, part_lower, part_upper in parts: + clipped_lower = max(lower, part_lower) + clipped_upper = min(upper, part_upper) + if clipped_lower > clipped_upper: + continue + + begin_rk = clipped_upper if reverse else clipped_lower + end_rk = clipped_lower if reverse else clipped_upper + clones.append(contribution.cloned_for( + new_reach, begin_rk, end_rk + )) + + if len(clones) != 0: + self._lst.extend(clones) + self._status.modified() + @property def Lat_Cont_List(self): return self.lst diff --git a/src/Model/River.py b/src/Model/River.py index 286aeef5..00a0a799 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -991,5 +991,8 @@ Last export at: @date.""" self._InitialConditionsAdisTS.split_reach( reach, profile, r1, r2 ) + self._LateralContributionsAdisTS.split_reach( + reach, profile, r1, r2 + ) return r1, r2 diff --git a/src/Solver/AdisTS.py b/src/Solver/AdisTS.py index 2ab237dc..4c0c11e7 100644 --- a/src/Solver/AdisTS.py +++ b/src/Solver/AdisTS.py @@ -549,7 +549,7 @@ class AdisTSwc(AdisTS): for LC in POL_LC: reach = next(( edge for edge in study.river.enable_edges() - if edge.id == LC.edge + if edge.id == LC.reach ), None) if reach is None: continue diff --git a/src/View/D90AdisTS/Table.py b/src/View/D90AdisTS/Table.py index 7435ddad..0e2b21d0 100644 --- a/src/View/D90AdisTS/Table.py +++ b/src/View/D90AdisTS/Table.py @@ -78,7 +78,7 @@ class ComboBoxDelegate(QItemDelegate): else: val = list( map( - lambda n: n.name, self._data.edges() + lambda n: n.name, self._data.enable_edges() ) ) diff --git a/src/View/DIFAdisTS/Table.py b/src/View/DIFAdisTS/Table.py index 5f00da7d..2a8fce12 100644 --- a/src/View/DIFAdisTS/Table.py +++ b/src/View/DIFAdisTS/Table.py @@ -80,7 +80,7 @@ class ComboBoxDelegate(QItemDelegate): else: val = list( map( - lambda n: n.name, self._data.edges() + lambda n: n.name, self._data.enable_edges() ) ) diff --git a/src/View/InitialConditionsTemperature/Table.py b/src/View/InitialConditionsTemperature/Table.py index 037d0d27..1acafd00 100644 --- a/src/View/InitialConditionsTemperature/Table.py +++ b/src/View/InitialConditionsTemperature/Table.py @@ -75,7 +75,7 @@ class ComboBoxDelegate(QItemDelegate): else: val = list( map( - lambda n: n.name, self._data.edges() + lambda n: n.name, self._data.enable_edges() ) ) diff --git a/src/View/LateralContributionsAdisTS/Table.py b/src/View/LateralContributionsAdisTS/Table.py index 952458b3..01e7d428 100644 --- a/src/View/LateralContributionsAdisTS/Table.py +++ b/src/View/LateralContributionsAdisTS/Table.py @@ -81,7 +81,10 @@ class ComboBoxDelegate(QItemDelegate): else: self.editor.addItems( [self._trad['not_associated']] + - self._data.edges_names() + [ + reach.name + for reach in self._data.enable_edges() + ] ) self.editor.setCurrentText(index.data(Qt.DisplayRole)) @@ -119,6 +122,11 @@ class TableModel(PamhyrTableModel): self._setup_lst() def _setup_lst(self): + enabled_reach_ids = { + reach.id + for reach in self._data.enable_edges() + } + if self._lcs_list is not None: self._lcs_pol_list = [ lcs for lcs in self._lcs_list._lst @@ -127,7 +135,13 @@ class TableModel(PamhyrTableModel): self._lst = list( filter( - lambda x: x._deleted is False, + lambda x: ( + x._deleted is False and + ( + x.reach in (None, -1) or + x.reach in enabled_reach_ids + ) + ), self._lcs_pol_list ) ) @@ -137,6 +151,18 @@ class TableModel(PamhyrTableModel): def rowCount(self, parent): return len(self._lst) + def get(self, row): + if 0 <= row < len(self._lst): + return self._lst[row] + return None + + def refresh(self): + self.beginResetModel() + try: + self._setup_lst() + finally: + self.endResetModel() + def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() @@ -170,11 +196,18 @@ class TableModel(PamhyrTableModel): try: if self._headers[column] == "reach": + reach = self._data.reach(value) + rks = reach.reach.get_rk() + if len(rks) == 0: + return False + self._undo.push( SetReachCommand( self._lcs_list, self._lst, row, - self._data.reach(value).id + reach.id, + min(rks), + max(rks) ) ) elif self._headers[column] == "begin_rk": @@ -193,7 +226,13 @@ class TableModel(PamhyrTableModel): logger.info(e) logger.debug(traceback.format_exc()) - self.dataChanged.emit(index, index) + if self._headers[column] == "reach": + end_index = self.index( + row, self._headers.index("end_rk") + ) + self.dataChanged.emit(index, end_index) + else: + self.dataChanged.emit(index, index) return True def add(self, row, parent=QModelIndex()): diff --git a/src/View/LateralContributionsAdisTS/UndoCommand.py b/src/View/LateralContributionsAdisTS/UndoCommand.py index a7bef5be..83b8116f 100644 --- a/src/View/LateralContributionsAdisTS/UndoCommand.py +++ b/src/View/LateralContributionsAdisTS/UndoCommand.py @@ -64,20 +64,30 @@ class SetEndCommand(QUndoCommand): class SetReachCommand(QUndoCommand): - def __init__(self, lcs, lcs_lst, index, reach): + def __init__(self, lcs, lcs_lst, index, reach, begin_rk, end_rk): QUndoCommand.__init__(self) self._lcs = lcs self._lcs_lst = lcs_lst self._index = index self._old = self._lcs_lst[self._index].reach + self._old_begin_rk = self._lcs_lst[self._index].begin_rk + self._old_end_rk = self._lcs_lst[self._index].end_rk self._new = reach + self._new_begin_rk = begin_rk + self._new_end_rk = end_rk def undo(self): - self._lcs_lst[self._index].reach = self._old + contribution = self._lcs_lst[self._index] + contribution.reach = self._old + contribution.begin_rk = self._old_begin_rk + contribution.end_rk = self._old_end_rk def redo(self): - self._lcs_lst[self._index].reach = self._new + contribution = self._lcs_lst[self._index] + contribution.reach = self._new + contribution.begin_rk = self._new_begin_rk + contribution.end_rk = self._new_end_rk class AddCommand(QUndoCommand): diff --git a/src/View/LateralContributionsAdisTS/Window.py b/src/View/LateralContributionsAdisTS/Window.py index 56a7b777..92038bb3 100644 --- a/src/View/LateralContributionsAdisTS/Window.py +++ b/src/View/LateralContributionsAdisTS/Window.py @@ -20,6 +20,7 @@ import logging from tools import trace, timer +from Modules import Modules from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( @@ -187,12 +188,13 @@ class LateralContributionAdisTSWindow(PamhyrWindow): data = None highlight = None - tab = "liquid" - if len(rows) > 0: - reach_id = self._study.river\ - .lateral_contributions_adists.lst[rows[0]]\ - .reach + contribution = self._table.get(rows[0]) + reach_id = ( + contribution.reach + if contribution is not None + else None + ) if reach_id: reach = next( @@ -201,8 +203,10 @@ class LateralContributionAdisTSWindow(PamhyrWindow): self._study.river.reachs())) data = reach.reach - lc = self._lcs.lst[rows[0]] - highlight = (lc.begin_rk, lc.end_rk) + highlight = ( + contribution.begin_rk, + contribution.end_rk + ) for delegate in self._delegate_rk: delegate.data = reach @@ -254,13 +258,23 @@ class LateralContributionAdisTSWindow(PamhyrWindow): self._table.redo() self._set_current_reach() + def _propagated_update(self, key=Modules(0)): + if Modules.NETWORK not in key: + return + + self._table.refresh() + self.find(QTableView, "tableView").clearSelection() + self._set_current_reach() + def edit(self): rows = self.index_selected_rows() if not rows: return for row in rows: - data = self._lcs.lst[row] + data = self._table.get(row) + if data is None: + continue if self.sub_window_exists( EditLateralContributionAdisTSWindow, diff --git a/src/View/WeatherParameters/Table.py b/src/View/WeatherParameters/Table.py index 7ce653d8..8b394248 100644 --- a/src/View/WeatherParameters/Table.py +++ b/src/View/WeatherParameters/Table.py @@ -87,7 +87,10 @@ class ComboBoxDelegate(QItemDelegate): else: self.editor.addItems( [self._trad['not_associated']] + - self._data.edges_names() + [ + reach.name + for reach in self._data.enable_edges() + ] ) self.editor.setCurrentText(str(index.data(Qt.DisplayRole))) From 0076faf03a5c436e9f6478ee29404fb12371da66 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 27 Jul 2026 09:31:29 +0200 Subject: [PATCH 8/8] Temperature: propagate TEM when splitting reach --- .../InitialConditionsTemperature.py | 39 +++++++++++++ .../InitialConditionsTemperatureList.py | 6 ++ .../InitialConditionsTemperatureSpec.py | 12 ++++ src/Model/River.py | 6 ++ .../WeatherParameters/WeatherParameters.py | 16 +++++ .../WeatherParametersList.py | 58 +++++++++++++++++++ src/Solver/AdisTT.py | 3 +- .../InitialConditionsTemperature/Table.py | 40 +++++++++++-- .../UndoCommand.py | 12 +++- .../InitialConditionsTemperature/Window.py | 7 +++ src/View/WeatherParameters/Table.py | 35 ++++++++++- src/View/WeatherParameters/UndoCommand.py | 18 +++++- src/View/WeatherParameters/Window.py | 7 +++ 13 files changed, 245 insertions(+), 14 deletions(-) diff --git a/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py b/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py index 350970c6..33e2c99e 100644 --- a/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py +++ b/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py @@ -227,3 +227,42 @@ class InitialConditionsTemperature(SQLSubModel): x.set_as_not_deleted() self.modified() + + def split_reach(self, reach, profile, reach1, reach2): + parts = [] + for new_reach in (reach1, reach2): + rks = new_reach.reach.get_rk() + if rks: + parts.append((new_reach.id, min(rks), max(rks))) + + specifications = [ + specification + for specification in self._data + if (not specification.is_deleted() and + specification.reach == reach.id) + ] + clones = [] + + for specification in specifications: + begin = specification.start_rk + end = specification.end_rk + if begin is None or end is None: + continue + + lower, upper = sorted((begin, end)) + reverse = begin > end + for new_reach, part_lower, part_upper in parts: + clipped_lower = max(lower, part_lower) + clipped_upper = min(upper, part_upper) + if clipped_lower > clipped_upper: + continue + + start_rk = clipped_upper if reverse else clipped_lower + end_rk = clipped_lower if reverse else clipped_upper + clones.append(specification.cloned_for( + new_reach, start_rk, end_rk + )) + + if clones: + self._data.extend(clones) + self.modified() diff --git a/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureList.py b/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureList.py index 2d60bc79..4dcdc9fd 100644 --- a/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureList.py +++ b/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureList.py @@ -62,6 +62,12 @@ class InitialConditionsTemperatureList(PamhyrModelList): self._status.modified() return n + def split_reach(self, reach, profile, reach1, reach2): + for initial_condition in self.lst: + initial_condition.split_reach( + reach, profile, reach1, reach2 + ) + @property def Initial_Conditions_List(self): return self.lst diff --git a/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py b/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py index 3682b275..431093fc 100644 --- a/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py +++ b/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py @@ -200,3 +200,15 @@ class ICTemperatureSpec(SQLSubModel): def temperature(self, temperature): self._temperature = temperature self._status.modified() + + def cloned_for(self, reach, start_rk, end_rk): + new = ICTemperatureSpec( + name=self._name_section, + status=self._status + ) + new._reach = reach + new._start_rk = start_rk + new._end_rk = end_rk + new._temperature = self._temperature + new.modified() + return new diff --git a/src/Model/River.py b/src/Model/River.py index 00a0a799..35e4416f 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -994,5 +994,11 @@ Last export at: @date.""" self._LateralContributionsAdisTS.split_reach( reach, profile, r1, r2 ) + self._InitialConditionsTemperature.split_reach( + reach, profile, r1, r2 + ) + self._WeatherParameters.split_reach( + reach, profile, r1, r2 + ) return r1, r2 diff --git a/src/Model/WeatherParameters/WeatherParameters.py b/src/Model/WeatherParameters/WeatherParameters.py index b3637636..a440173a 100644 --- a/src/Model/WeatherParameters/WeatherParameters.py +++ b/src/Model/WeatherParameters/WeatherParameters.py @@ -153,6 +153,13 @@ class Data(SQLSubModel): def __setitem__(self, key, value): self._data[key] = self._types[key](value) + def cloned(self): + return Data( + self[0], self[1], + types=self._types, + status=self._status + ) + class WeatherParametersDefault(SQLSubModel): def __init__(self, type: str, value: float = 0.0, @@ -580,6 +587,15 @@ class WeatherParameters(SQLSubModel): self._end_section = section self.modified() + def cloned_for(self, reach, begin_section, end_section): + new = type(self)(name=self._name, status=self._status) + new._reach = reach + new._begin_section = begin_section + new._end_section = end_section + new._data = [data.cloned() for data in self.data] + new.modified() + return new + @property def header(self): return self._header.copy() diff --git a/src/Model/WeatherParameters/WeatherParametersList.py b/src/Model/WeatherParameters/WeatherParametersList.py index 135e7fde..399308ff 100644 --- a/src/Model/WeatherParameters/WeatherParametersList.py +++ b/src/Model/WeatherParameters/WeatherParametersList.py @@ -161,6 +161,64 @@ class WeatherParametersList(PamhyrModelList): self._status.modified() return n + def split_reach(self, reach, profile, reach1, reach2): + profiles = reach.reach.profiles + split_index = profiles.index(profile) + parts = [ + ( + reach1, + 0, + split_index, + dict(zip( + profiles[:split_index + 1], + reach1.reach.profiles + )) + ), + ( + reach2, + split_index, + len(profiles) - 1, + dict(zip( + profiles[split_index:], + reach2.reach.profiles + )) + ), + ] + weather_parameters = [ + weather_parameter + for weather_parameter in self.lst + if weather_parameter.reach is reach + ] + clones = [] + + for weather_parameter in weather_parameters: + if (weather_parameter.begin_section not in profiles or + weather_parameter.end_section not in profiles): + continue + + begin = profiles.index(weather_parameter.begin_section) + end = profiles.index(weather_parameter.end_section) + lower, upper = sorted((begin, end)) + reverse = begin > end + + for new_reach, part_lower, part_upper, sections in parts: + clipped_lower = max(lower, part_lower) + clipped_upper = min(upper, part_upper) + if clipped_lower > clipped_upper: + continue + + begin_index = clipped_upper if reverse else clipped_lower + end_index = clipped_lower if reverse else clipped_upper + clones.append(weather_parameter.cloned_for( + new_reach, + sections[profiles[begin_index]], + sections[profiles[end_index]] + )) + + if clones: + self._lst.extend(clones) + self._status.modified() + @property def Weather_Parameters_List(self): return self.lst diff --git a/src/Solver/AdisTT.py b/src/Solver/AdisTT.py index 9433230c..418a845d 100644 --- a/src/Solver/AdisTT.py +++ b/src/Solver/AdisTT.py @@ -935,7 +935,8 @@ class AdisTTwc(AdisTT): for weather_parameter in study.river.weather_parameters.lst: config = self._weather_files.get(weather_parameter.type) if (config is None or weather_parameter.reach is None - or weather_parameter.reach.is_deleted()): + or weather_parameter.reach.is_deleted() + or not weather_parameter.reach.is_enable()): continue _, extension = config exported = self._export_weather_file( diff --git a/src/View/InitialConditionsTemperature/Table.py b/src/View/InitialConditionsTemperature/Table.py index 1acafd00..c2a6fca8 100644 --- a/src/View/InitialConditionsTemperature/Table.py +++ b/src/View/InitialConditionsTemperature/Table.py @@ -140,13 +140,30 @@ class InitialConditionTableModel(PamhyrTableModel): self._data = data def _setup_lst(self): + enabled_reach_ids = { + reach.id + for reach in self._river.enable_edges() + } self._lst = list( filter( - lambda ica: ica._deleted is False, + lambda ica: ( + ica._deleted is False and + ( + ica.reach in (None, -1) or + ica.reach in enabled_reach_ids + ) + ), self._data._data ) ) + def refresh(self): + self.beginResetModel() + try: + self._setup_lst() + finally: + self.endResetModel() + def rowCount(self, parent): return len(self._lst) @@ -221,20 +238,31 @@ class InitialConditionTableModel(PamhyrTableModel): ) ) else: + new_value = value + if self._headers[column] == "reach": + reach = self._river.edge(value) + rks = reach.reach.get_rk() + if not rks: + return False + new_value = (reach.id, min(rks)) + self._undo.push( SetCommandSpec( self._lst, row, self._headers[column], - (self._river.edge(value).id - if self._headers[column] == "reach" - else value - ) + new_value ) ) except Exception as e: logger.info(e) logger.debug(traceback.format_exc()) - self.dataChanged.emit(index, index) + if self._headers[column] == "reach": + rk_index = self.index( + row, self._headers.index("rk") + ) + self.dataChanged.emit(index, rk_index) + else: + self.dataChanged.emit(index, index) return True def add(self, row, parent=QModelIndex()): diff --git a/src/View/InitialConditionsTemperature/UndoCommand.py b/src/View/InitialConditionsTemperature/UndoCommand.py index 83f93265..12de80a1 100644 --- a/src/View/InitialConditionsTemperature/UndoCommand.py +++ b/src/View/InitialConditionsTemperature/UndoCommand.py @@ -73,6 +73,8 @@ class SetCommandSpec(QUndoCommand): self._old = self._data[self._row].name elif self._column == "reach": self._old = self._data[self._row].reach + self._old_start_rk = self._data[self._row].start_rk + self._old_end_rk = self._data[self._row].end_rk elif self._column == "rk": self._old = self._data[self._row].start_rk elif self._column == "temperature": @@ -84,13 +86,19 @@ class SetCommandSpec(QUndoCommand): elif column == "reach": _type = int - self._new = _type(new_value) + if column == "reach": + self._new = _type(new_value[0]) + self._new_rk = float(new_value[1]) + else: + self._new = _type(new_value) def undo(self): if self._column == "name": self._data[self._row].name = self._old elif self._column == "reach": self._data[self._row].reach = self._old + self._data[self._row].start_rk = self._old_start_rk + self._data[self._row].end_rk = self._old_end_rk elif self._column == "rk": self._data[self._row].start_rk = self._old self._data[self._row].end_rk = self._old @@ -102,6 +110,8 @@ class SetCommandSpec(QUndoCommand): self._data[self._row].name = self._new elif self._column == "reach": self._data[self._row].reach = self._new + self._data[self._row].start_rk = self._new_rk + self._data[self._row].end_rk = self._new_rk elif self._column == "rk": self._data[self._row].start_rk = self._new self._data[self._row].end_rk = self._new diff --git a/src/View/InitialConditionsTemperature/Window.py b/src/View/InitialConditionsTemperature/Window.py index 53f45fe6..bdc7a0db 100644 --- a/src/View/InitialConditionsTemperature/Window.py +++ b/src/View/InitialConditionsTemperature/Window.py @@ -309,3 +309,10 @@ class InitialConditionsTemperatureWindow(PamhyrWindow): if len(rows) == 0: return self._table_spec.delete(rows) + + def _propagated_update(self, key=Modules(0)): + if Modules.NETWORK not in key: + return + + self._table_spec.refresh() + self.table_spec.clearSelection() diff --git a/src/View/WeatherParameters/Table.py b/src/View/WeatherParameters/Table.py index 8b394248..b3e05398 100644 --- a/src/View/WeatherParameters/Table.py +++ b/src/View/WeatherParameters/Table.py @@ -147,8 +147,28 @@ class WeatherParametersTableModel(PamhyrTableModel): if self._type == "ND" or self._data is None: self._lst = [] return + enabled_reach_ids = { + reach.id + for reach in self._river.enable_edges() + } self._lst = self._data.lst - self._lst = list(filter(lambda wp: wp.type == self._type, self._lst)) + self._lst = list(filter( + lambda wp: ( + wp.type == self._type and + ( + wp.reach is None or + wp.reach.id in enabled_reach_ids + ) + ), + self._lst + )) + + def refresh(self): + self.beginResetModel() + try: + self._setup_lst() + finally: + self.endResetModel() def update_tab_spec(self, type="ND", enabled=True): if enabled: @@ -242,16 +262,25 @@ class WeatherParametersTableModel(PamhyrTableModel): ) ) elif self._headers[column] == "reach": + edge = self._river.edge(value) + if not edge.reach.profiles: + return False self._undo.push( SetEdgeCommand( - self._data, global_row, self._river.edge(value) + self._data, global_row, edge ) ) except Exception as e: logger.info(e) logger.debug(traceback.format_exc()) - self.dataChanged.emit(index, index) + if self._headers[column] == "reach": + end_index = self.index( + row, self._headers.index("end_rk") + ) + self.dataChanged.emit(index, end_index) + else: + self.dataChanged.emit(index, index) return True def add(self, row, parent=QModelIndex()): diff --git a/src/View/WeatherParameters/UndoCommand.py b/src/View/WeatherParameters/UndoCommand.py index 18727d99..fe86f9b8 100644 --- a/src/View/WeatherParameters/UndoCommand.py +++ b/src/View/WeatherParameters/UndoCommand.py @@ -125,14 +125,26 @@ class SetEdgeCommand(QUndoCommand): self._wps = wps self._index = index - self._old = self._wps.get(self._index).reach + weather_parameter = self._wps.get(self._index) + self._old = weather_parameter.reach + self._old_begin = weather_parameter.begin_section + self._old_end = weather_parameter.end_section self._new = edge + profiles = edge.reach.profiles + self._new_begin = min(profiles, key=lambda profile: profile.rk) + self._new_end = max(profiles, key=lambda profile: profile.rk) def undo(self): - self._wps.get(self._index).reach = self._old + weather_parameter = self._wps.get(self._index) + weather_parameter.reach = self._old + weather_parameter.begin_section = self._old_begin + weather_parameter.end_section = self._old_end def redo(self): - self._wps.get(self._index).reach = self._new + weather_parameter = self._wps.get(self._index) + weather_parameter.reach = self._new + weather_parameter.begin_section = self._new_begin + weather_parameter.end_section = self._new_end class AddCommand(QUndoCommand): diff --git a/src/View/WeatherParameters/Window.py b/src/View/WeatherParameters/Window.py index 4730f7e5..5617eb5b 100644 --- a/src/View/WeatherParameters/Window.py +++ b/src/View/WeatherParameters/Window.py @@ -439,3 +439,10 @@ class WeatherParametersWindow(PamhyrWindow): parent=self ) win.show() + + def _propagated_update(self, key=Modules(0)): + if Modules.NETWORK not in key: + return + + self._table_spec.refresh() + self.table_spec.clearSelection()