From b5432498c6d59d8403451d5786c7a9a0341cc575 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Thu, 20 Aug 2026 15:41:23 +0200 Subject: [PATCH] Ensemble: Add sequential runs with RUG variation into mage export. --- src/Model/Ensembles/Ensemble.py | 8 +- src/Model/Study.py | 13 +++ src/Solver/Mage.py | 62 +++++++++++++ src/View/Ensembles/Window.py | 1 + src/View/MainWindow.py | 25 ++++++ src/View/RunSolver/Window.py | 150 +++++++++++++++++++++++++++++++- 6 files changed, 256 insertions(+), 3 deletions(-) diff --git a/src/Model/Ensembles/Ensemble.py b/src/Model/Ensembles/Ensemble.py index 00d490fa..6e033321 100644 --- a/src/Model/Ensembles/Ensemble.py +++ b/src/Model/Ensembles/Ensemble.py @@ -20,6 +20,8 @@ import struct import logging import itertools +import numpy as np + from Model.Tools.PamhyrDB import SQLSubModel from Model.Scenario import Scenario from Model.Ensembles.Function import Uniform @@ -200,8 +202,10 @@ class Ensemble(SQLSubModel): return struct.unpack(data_format, bparams) def get_sample(self, nb): - return self._function.get_sample( - self._params, nb + return np.nditer( + self._function.get_sample( + self._params, nb + ) ) @property diff --git a/src/Model/Study.py b/src/Model/Study.py index 7718bbc0..ec64cc53 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -420,6 +420,19 @@ class Study(SQLModel): def _save(self, progress=None): progress = progress if progress is not None else lambda: None + if self.status.scenario.is_ensemble(): + self._save_submodel( + [ + self.scenarios, self._ens_functions, + self._river._ensembles + ], + data=progress + ) + progress() + self.commit() + + return + self.execute( "INSERT OR REPLACE INTO info VALUES ('study_release', " + f"'{self.status.version}')" diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 3ad490ae..b31c48c6 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -1027,6 +1027,68 @@ class Mage8(Mage): logger_exception(e) return False + def export_ensemble_variation(self, study, repertory, + qlog=None, name="0", + samples=None): + self._study = study + name = study.name.replace(" ", "_") + + self._export_ensemble_RUG( + study, repertory, + qlog, name=name, + samples=samples, + ) + + return True + + def _export_ensemble_RUG(self, study, repertory, + qlog, name="0", + samples={}): + files = [] + + for ens in samples: + if ens.target_data is None: + continue + + if ens.data_type == "strickler_minor": + ens.target_data.minor = next(samples[ens]) + elif ens.data_type == "strickler_medium": + ens.target_data.medium = next(samples[ens]) + + if qlog is not None: + qlog.put("Export variation of RUG file") + + # Write header + with mage_file_open(os.path.join(repertory, f"{name}.RUG"), "w+") as f: + files.append(f"{name}.RUG") + + edges = study.river.enable_edges() + + id = 1 + for edge in edges: + frictions = edge.frictions + + for friction in frictions.frictions: + if friction.begin_strickler is None: + continue + + if friction.is_deleted(): + continue + + num = f"{id:>3}" + brk = f"{friction.begin_rk:>10.3f}" + erk = f"{friction.end_rk:>10.3f}" + + strickler = friction.begin_strickler + coef_1 = f"{strickler.minor:>10.3f}" + coef_2 = f"{strickler.medium:>10.3f}" + + f.write(f"K{num} {brk}{erk}{coef_1}{coef_2}\n") + + id += 1 + + return files + ########### # RESULTS # ########### diff --git a/src/View/Ensembles/Window.py b/src/View/Ensembles/Window.py index a04d937e..7bb2d89f 100644 --- a/src/View/Ensembles/Window.py +++ b/src/View/Ensembles/Window.py @@ -199,6 +199,7 @@ class EnsemblesWindow(PamhyrWindow): logger_exception(e) return + class MinMaxDialog(PamhyrDialog): _pamhyr_ui = "MinMaxDialog" _pamhyr_name = "Minmax" diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index bbf46ed3..f40c6934 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -83,10 +83,13 @@ from View.GeoTIFF.Window import GeoTIFFListWindow from View.REPLines.Window import REPLineListWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import ( + EnsembleRunNumberDialog, SelectSolverWindow, SolverLogWindow, + SolverLogEnsWindow, ) + from View.Results.Window import ResultsWindow from View.Results.CompareDialog import ( CompareScenariosWindow @@ -1587,6 +1590,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): sol.show() def solver_log(self, solver): + if self._study.status.scenario.is_ensemble(): + return self._solver_log_ensemble(solver) + sol = SolverLogWindow( study=self._study, config=self.conf, @@ -1595,6 +1601,25 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) sol.show() + def _solver_log_ensemble(self, solver): + run_number = 1 + + dlg = EnsembleRunNumberDialog( + trad=self._trad, + parent=self + ) + if dlg.exec(): + run_number = dlg._number + + sol = SolverLogEnsWindow( + run_number=run_number, + study=self._study, + config=self.conf, + solver=solver, + parent=self + ) + sol.show() + def msg_box_results_continue(self): dlg = QMessageBox(self) diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 65e8546b..6488aed1 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, QComboBox, QVBoxLayout, QHeaderView, QTabWidget, - QTextEdit, + QTextEdit, QProgressBar, ) from View.RunSolver.Log.Window import SolverLogFileWindow @@ -57,6 +57,36 @@ _translate = QCoreApplication.translate logger = logging.getLogger() +class EnsembleRunNumberDialog(PamhyrDialog): + _pamhyr_ui = "EnsembleRunNumberDialog" + _pamhyr_name = "EnsembleRunNumber" + + def __init__(self, trad=None, parent=None): + super(EnsembleRunNumberDialog, self).__init__( + title=trad[self._pamhyr_name], + trad=trad, + options=[], + parent=parent + ) + + self._init_default_values() + + def _init_default_values(self): + self.set_spin_box("spinBox", 100) + + @property + def number(self): + return self._number + + def accept(self): + self._number = self.get_spin_box("spinBox") + + super().accept() + + def reject(self): + self.close() + + class SelectSolverWindow(PamhyrDialog): _pamhyr_ui = "SelectSolver" _pamhyr_name = "Select solver" @@ -425,3 +455,121 @@ class SolverLogWindow(PamhyrWindow): parent=self, ) log.show() + + +class SolverLogEnsWindow(SolverLogWindow): + _pamhyr_ui = "SolverLogEnsemble" + _pamhyr_name = "Solver Log Ensemble" + + def __init__(self, run_number=100, + study=None, config=None, + solver=None, parent=None): + self._run_number = run_number + self._last_run = 0 + + self._samples = {} + for ens in study.river._ensembles.lst: + if ens.function is None: + continue + + self._samples[ens] = ens.get_sample(self._run_number) + + super(SolverLogEnsWindow, self).__init__( + solver=solver, + study=study, + config=config, + parent=parent, + ) + + def setup_connections(self): + self.setup_progress_bar() + + super(SolverLogEnsWindow, self).setup_connections() + + def setup_progress_bar(self): + self._progress_bar = self.find(QProgressBar, "progressBar") + self._progress_bar.setRange(0, self._run_number) + self._progress_bar.setValue(0) + + def _log_int(self, int_code, color=None): + logger.info(f"solver: Returns {int_code}") + color = "blue" if int_code == 0 else "red" + + self.find(QTextEdit, "textEdit")\ + .append( + f"" + + f" *** Finished with code {int_code}" + + "" + ) + + self.statusbar.showMessage( + "Done" if int_code == 0 else "Failed", + 3000 + ) + + if self._last_run < self._run_number: + self.next() + else: + self._progress_bar.setValue(self._last_run) + + def next(self): + self._process = self.new_process(self._parent) + + self._log(f" *** Run #{self._last_run}", color="blue") + self._solver.export_ensemble_variation( + self._study, self._workdir, qlog=self._output, + samples=self._samples, + ) + + self._solver.run( + self._study, + process=self._process, + output_queue=self._output + ) + self._last_run += 1 + self._progress_bar.setValue(self._last_run - 1) + + def run(self): + self._log(f" *** Run solver {self._solver.name}", color="blue") + self.next() + + def start(self): + if self._solver.is_stoped(): + self._log(f" *** Export study {self._solver.name}", color="blue") + + ok = self._solver.export( + self._study, self._workdir, qlog=self._output + ) + + if not ok: + self._log(f" *** Failed to export", color="red") + self.update() + return + else: + self.update() + self._process = self.new_process(self._parent) + + self._log(" *** Start", color="blue") + self._results = None + + self.next() + + self.find(QAction, "action_start").setEnabled(False) + if _signal: + self.find(QAction, "action_pause").setEnabled(True) + else: + self.find(QAction, "action_pause").setEnabled(False) + self.find(QAction, "action_stop").setEnabled(True) + self.find(QAction, "action_log_file").setEnabled(False) + self.find(QAction, "action_results").setEnabled(False) + + def stop(self): + self._log(" *** Stop", color="blue") + self._solver.kill() + + self.find(QAction, "action_start").setEnabled(True) + self.find(QAction, "action_pause").setEnabled(False) + self.find(QAction, "action_stop").setEnabled(False) + self.find(QAction, "action_results").setEnabled(True) + if self._solver.log_file() != "": + self.find(QAction, "action_log_file").setEnabled(True)