From e64c45b621a4cf6b8f83276b0669a6e3facc2951 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Aug 2026 15:15:47 +0200 Subject: [PATCH] Ensemble: Create new ensemble scenario. --- src/Model/Scenario.py | 41 ++++++++++++++++++++++--------- src/Model/Scenarios.py | 7 ++++-- src/Model/Study.py | 16 ++++++++++++ src/View/Ensembles/UndoCommand.py | 2 ++ src/View/Scenarios/ContextMenu.py | 11 +++++++++ src/View/Scenarios/GraphWidget.py | 20 +++++++++++++++ src/View/Scenarios/UndoCommand.py | 19 ++++++++++++++ src/View/Scenarios/translate.py | 3 +++ 8 files changed, 105 insertions(+), 14 deletions(-) diff --git a/src/Model/Scenario.py b/src/Model/Scenario.py index cc4c01f4..bc061caa 100644 --- a/src/Model/Scenario.py +++ b/src/Model/Scenario.py @@ -53,6 +53,7 @@ class Scenario(SQLSubModel): "geometry_pointXYZ", "geometry_profileXYZ", "river_reach", "river_node", "geotiff", "reservoir", "reservoir_data", + "ensemble", ] related_tables = tables_with_deleted_column + [ @@ -67,6 +68,7 @@ class Scenario(SQLSubModel): name: str = "", description: str = "", x: int = 1000.0, y: int = 1000.0, + is_ensemble: bool = False, revision: int = 0, parent=None): super(Scenario, self).__init__() @@ -76,6 +78,7 @@ class Scenario(SQLSubModel): self._x, self._y = x, y self._name = name self._description = description + self._is_ensemble = False self._revision = revision self._parent = parent @@ -98,6 +101,7 @@ class Scenario(SQLSubModel): y REAL NOT NULL DEFAULT 1000, name TEXT NOT NULL, description TEXT NOT NULL, + is_ensemble BOOL DEFAULT FALSE, revision INTEGER NOT NULL, parent_id INTEGER REFERENCES scenario(id) ) @@ -133,6 +137,13 @@ class Scenario(SQLSubModel): cls._db_create(execute) cls._db_add_default(execute) + if major == "0" and int(minor) == 2: + if int(release) < 8: + execute( + f"ALTER TABLE scenario " + + "ADD COLUMN is_ensemble BOOL DEFAULT FALSE" + ) + return True @classmethod @@ -153,7 +164,8 @@ class Scenario(SQLSubModel): scenarios = {} table = execute( - "SELECT id, x, y, name, description, revision, parent_id " + + "SELECT id, x, y, name, description, " + + " is_ensemble, revision, parent_id " + "FROM scenario " + "ORDER BY id ASC" ) @@ -165,6 +177,7 @@ class Scenario(SQLSubModel): x, y = next(it), next(it) name = next(it) desc = next(it) + is_ens = next(it) revi = next(it) parent = next(it) @@ -174,7 +187,9 @@ class Scenario(SQLSubModel): new = cls( id=id, x=x, y=y, name=name, description=desc, - revision=revi, parent=parent + is_ensemble=is_ens, + revision=revi, + parent=parent ) scenarios[id] = new @@ -184,21 +199,18 @@ class Scenario(SQLSubModel): if self.is_deleted(): return self.drop_all(execute) - parent = 'NULL' + parent = None if self.parent is not None: parent = self.parent._id execute( "INSERT OR REPLACE INTO " + - "scenario(id, x, y, name, description, revision, parent_id) " + - "VALUES (" + - f"{self._id}, " + - f"{self.x}, {self.y}, " + - f"'{self._db_format(self.name)}', " + - f"'{self._db_format(self.description)}', " + - f"{self._revision}, " + - f"{parent}" + - ")" + "scenario(id, x, y, name, description, " + + " is_ensemble, revision, parent_id) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + self._id, self.x, self.y, + self.name, self.description, self._is_ensemble, + self._revision, parent ) return True @@ -323,6 +335,9 @@ class Scenario(SQLSubModel): def parent(self): return self._parent + def is_ensemble(self): + return self._is_ensemble + def set_pos(self, x, y): self._x = x self._y = y @@ -340,6 +355,8 @@ class Scenario(SQLSubModel): return self.name if key == "description": return self.description + if key == "is_ensemble": + return self.is_ensemble() if key == "parent": return self.parent diff --git a/src/Model/Scenarios.py b/src/Model/Scenarios.py index e37a504e..5a688cd4 100644 --- a/src/Model/Scenarios.py +++ b/src/Model/Scenarios.py @@ -57,8 +57,11 @@ class Scenarios(PamhyrModelDict): return None - def new(self, parent): - new = Scenario(parent=parent) + def new(self, parent, is_ensemble=False): + new = Scenario( + parent=parent, + is_ensemble=is_ensemble + ) self.set(new._id, new) return new diff --git a/src/Model/Study.py b/src/Model/Study.py index 2fe1eb44..6355d102 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -674,6 +674,22 @@ class Study(SQLModel): self.status.set_as_editable() return new + def create_ensemble_scenario(self, switch=True): + new = self.scenarios.new( + self.status.scenario, + is_ensemble=True + ) + + new.name = self.status.scenario.name + " (ensemble)" + new.set_pos(self.status.scenario.x + 100, + self.status.scenario.y + 100) + + if switch: + self.status.scenario = new + + self.status.set_as_editable() + return new + @property def results(self): return self._river.results diff --git a/src/View/Ensembles/UndoCommand.py b/src/View/Ensembles/UndoCommand.py index a4bd0f3a..ff78c8ce 100644 --- a/src/View/Ensembles/UndoCommand.py +++ b/src/View/Ensembles/UndoCommand.py @@ -74,6 +74,7 @@ class SetDataCommand(QUndoCommand): def redo(self): self._ensembles.get(self._index).target_data = self._new + class SetFunctionCommand(QUndoCommand): def __init__(self, ensembles, index, edge): QUndoCommand.__init__(self) @@ -89,6 +90,7 @@ class SetFunctionCommand(QUndoCommand): def redo(self): self._ensembles.get(self._index).function = self._new + class AddCommand(QUndoCommand): def __init__(self, ensembles, index, reach): QUndoCommand.__init__(self) diff --git a/src/View/Scenarios/ContextMenu.py b/src/View/Scenarios/ContextMenu.py index 1a733082..adf430b2 100644 --- a/src/View/Scenarios/ContextMenu.py +++ b/src/View/Scenarios/ContextMenu.py @@ -59,11 +59,15 @@ class ScenarioMenu(AbstractMenu): item = self._items[0] scenarios = item.graph._study.scenarios + if len(scenarios) == 1: + return + current_scenario = item.graph._study.status.scenario.id select = self._menu.addAction(self._trad["menu_select_scenario"]) duplicate = None delete = None + ensemble = None if item.scenario.id != 0: if scenarios.is_leaf(item.scenario): @@ -74,6 +78,11 @@ class ScenarioMenu(AbstractMenu): self._trad["menu_dup_scenario"] ) + if item.scenario.id == current_scenario: + ensemble = self._menu.addAction( + self._trad["menu_ens_scenario"] + ) + action = self._exec() if action is None: return @@ -83,3 +92,5 @@ class ScenarioMenu(AbstractMenu): self._parent.delete_scenario(item) elif action == duplicate: self._parent.duplicate_scenario(item) + elif action == ensemble: + self._parent.create_ensemble_scenario(item) diff --git a/src/View/Scenarios/GraphWidget.py b/src/View/Scenarios/GraphWidget.py index 9756a8f0..6271f301 100644 --- a/src/View/Scenarios/GraphWidget.py +++ b/src/View/Scenarios/GraphWidget.py @@ -526,6 +526,26 @@ class GraphWidget(QGraphicsView): self.exec_with_waiting_window(fn, "duplicate_scenario") self.changeScenario.emit(self.sender()) + def create_ensemble_scenario(self, item): + must_save = self.dialog_save() + if must_save == "Cancel": + return + + def fn(): + self._close_other_window() + + if must_save == "Save": + self._study.save() + + self._undo.push( + CreateEnsembleScenarioCommand( + self._study, + ) + ) + + self.exec_with_waiting_window(fn, "create_ensemble_scenario") + self.changeScenario.emit(self.sender()) + def _close_other_window(self): self.parent\ .parent\ diff --git a/src/View/Scenarios/UndoCommand.py b/src/View/Scenarios/UndoCommand.py index dd6631e7..cbe05fbc 100644 --- a/src/View/Scenarios/UndoCommand.py +++ b/src/View/Scenarios/UndoCommand.py @@ -98,6 +98,25 @@ class DuplicateScenariosCommand(QUndoCommand): self._study.reload_from_scenario(self._new) +class CreateEnsembleScenarioCommand(QUndoCommand): + def __init__(self, study): + QUndoCommand.__init__(self) + + self._study = study + self._new = None + + def undo(self): + self._study.scenarios.delete(self._new.id) + self._study.reload_from_scenario(self._new.parent) + + def redo(self): + if self._new is None: + self._new = self._study.create_ensemble_scenario() + else: + self._new.set_as_not_deleted() + self._study.reload_from_scenario(self._new) + + class SetCommand(QUndoCommand): def __init__(self, scenario, column, new_value): QUndoCommand.__init__(self) diff --git a/src/View/Scenarios/translate.py b/src/View/Scenarios/translate.py index 8c7f0c9d..6661f64b 100644 --- a/src/View/Scenarios/translate.py +++ b/src/View/Scenarios/translate.py @@ -43,6 +43,9 @@ class ScenariosTranslate(MainTranslate): self._dict["menu_dup_scenario"] = _translate( "Scenarios", "Duplicate this scenario" ) + self._dict["menu_ens_scenario"] = _translate( + "Scenarios", "Create ensemble scenario from this scenario" + ) self._sub_dict["table_headers_scenarios"] = { # "id": self._dict['id'],