diff --git a/src/Solver/AdisTS.py b/src/Solver/AdisTS.py index 5219c85114fee676819f3d7e47ebd91df556f090..942afc166b3ccf86fbbdf89597d9e90dbe680d5b 100644 --- a/src/Solver/AdisTS.py +++ b/src/Solver/AdisTS.py @@ -112,7 +112,7 @@ class AdisTS(CommandLineSolver): for line in lines: rep_file.write(line.line) - def _export_REP(self, study, repertory, files, qlog, name="0"): + def _export_REP(self, study, repertory, mage_rep, files, qlog, name="0"): if qlog is not None: qlog.put("Export REP file") @@ -124,8 +124,8 @@ class AdisTS(CommandLineSolver): ), "w+" ) as f: - f.write(f"NET ../default-mage/{name}.NET\n") - f.write(f"REP ../default-mage/{name}.REP\n") + f.write(f"NET ../{mage_rep}/{name}.NET\n") + f.write(f"REP ../{mage_rep}/{name}.REP\n") for file in files: EXT = file.split('.')[1] @@ -133,7 +133,7 @@ class AdisTS(CommandLineSolver): self._export_REP_additional_lines(study, f) - path_mage_net = os.path.join(os.path.abspath(os.path.join(repertory, os.pardir)), f"default-mage/net") + path_mage_net = os.path.join(os.path.abspath(os.path.join(repertory, os.pardir)), f"{mage_rep}/net") path_adists_net = os.path.join(repertory, "net") if os.path.exists(path_mage_net): @@ -466,7 +466,7 @@ class AdisTSlc(AdisTS): ] @timer - def export(self, study, repertory, qlog=None, name="0"): + def export(self, study, repertory, mage_rep, qlog=None, name="0"): print("cmd solver adistslc : ", self._cmd_solver) self._study = study name = study.name.replace(" ", "_") @@ -479,7 +479,7 @@ class AdisTSlc(AdisTS): files = files + func(study, repertory, qlog, name=name) self.export_additional_files(study, repertory, qlog, name=name) - self._export_REP(study, repertory, files, qlog, name=name) + self._export_REP(study, repertory, mage_rep, files, qlog, name=name) return True except Exception as e: diff --git a/src/View/CheckList/WindowAdisTS.py b/src/View/CheckList/WindowAdisTS.py new file mode 100644 index 0000000000000000000000000000000000000000..40bba4b4dcbb16983b6621a5a2991bd99befde69 --- /dev/null +++ b/src/View/CheckList/WindowAdisTS.py @@ -0,0 +1,200 @@ +# WindowAdisTS.py -- Pamhyr +# Copyright (C) 2023-2024 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, QRect, QThread, + pyqtSlot, pyqtSignal, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, + QProgressBar, QLabel, +) + +from View.CheckList.Table import TableModel +from View.CheckList.Worker import Worker +from View.CheckList.Translate import CheckListTranslate + +_translate = QCoreApplication.translate + + +class CheckListWindowAdisTS(PamhyrWindow): + _pamhyr_ui = "CheckList" + _pamhyr_name = "Check list" + + signalStatus = pyqtSignal(str) + + def __init__(self, autorun: bool = True, + study=None, config=None, + solver=None, parent=None, mage_rep=None): + trad = CheckListTranslate() + + self._autorun = autorun + self._solver = solver + self._mage_rep = mage_rep + + name = trad[self._pamhyr_name] + " - " + study.name + + super(CheckListWindowAdisTS, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + options=[], + parent=parent + ) + + # Add solver to hash computation data + self._hash_data.append(self._solver) + + self._checker_list = ( + self._study.checkers() + + self._solver.checkers() + ) + + self.setup_table() + self.setup_progress_bar() + self.setup_connections() + self.setup_thread() + self.setup_statusbar() + + def setup_table(self): + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + data=self._checker_list, + ) + + def setup_progress_bar(self): + self._progress = self.find(QProgressBar, f"progressBar") + self._p = 0 # Progress current step + + self._progress.setRange(0, len(self._checker_list)) + self._progress.setValue(self._p) + + def setup_connections(self): + self.find(QPushButton, "pushButton_ok").clicked.connect(self.accept) + self.find(QPushButton, "pushButton_retry").clicked.connect(self.retry) + self.find(QPushButton, "pushButton_cancel")\ + .clicked.connect(self.reject) + + def setup_thread(self): + self._worker = Worker(self._study, self._checker_list) + self._worker_thread = QThread() + self._worker.moveToThread(self._worker_thread) + + # Connect any worker signals + self._worker.signalStatus.connect(self.update) + self._worker_thread.started.connect(self._worker.process) + + self._worker_thread.start() + + def retry(self): + self._worker_thread.terminate() + self._worker_thread.wait() + + self.find(QPushButton, "pushButton_retry").setEnabled(False) + self.find(QPushButton, "pushButton_ok").setEnabled(False) + + self.setup_thread() + + def _compute_status(self): + ok = len(list(filter(lambda c: c.is_ok(), self._checker_list))) + warning = len( + list(filter(lambda c: c.is_warning(), self._checker_list))) + error = len(list(filter(lambda c: c.is_error(), self._checker_list))) + + return ok, warning, error + + def _compute_status_label(self): + ok, warning, error = self._compute_status() + return (f"<font color=\"Green\">Ok: {ok} </font> |" + + f"<font color=\"Orange\">Warning: {warning} </font> |" + + f"<font color=\"Red\">Error: {error}</font>") + + def setup_statusbar(self): + txt = self._compute_status_label() + self._status_label = QLabel(txt) + self.statusbar.addPermanentWidget(self._status_label) + + def update_statusbar(self): + txt = self._compute_status_label() + self._status_label.setText(txt) + + def progress(self): + self._p += 1 + self._progress.setValue(self._p) + self._table.update() + + def start_compute(self): + self._p = 0 + self._progress.setValue(self._p) + + def info_compute(self, str): + self.statusbar.showMessage(str, 3000) + + def end_compute(self): + self._table.layoutChanged.emit() + self.find(QPushButton, "pushButton_retry").setEnabled(True) + + errors = any(filter(lambda c: c.is_error(), self._checker_list)) + if not errors: + self.find(QPushButton, "pushButton_ok").setEnabled(True) + if self._autorun: + self._parent.solver_log_adists(self._solver, self._mage_rep) + self.end() + + self.update_statusbar() + + def update(self, key: str): + if key == "start": + self.start_compute() + self.info_compute("Starting ...") + elif key == "end": + self.info_compute("Finish") + self.end_compute() + elif key == "progress": + self.progress() + else: + self.info_compute(key) + + self.update_statusbar() + + def end(self): + # self._worker.join()b + self.close() + + def reject(self): + self.end() + + def accept(self): + self._parent.solver_log_adists(self._solver, self._mage_rep) + # self.end() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index aeaac94c7a40c386450637e3996ab522fb21d334..f7fdcce68048d219d0c782b8c6ae3bd347761cdf 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -71,8 +71,9 @@ from View.AdditionalFiles.Window import AddFileListWindow from View.REPLines.Window import REPLineListWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -from View.RunSolver.WindowAdisTS import SelectSolverWindowAdisTS +from View.RunSolver.WindowAdisTS import SelectSolverWindowAdisTS, SolverLogWindowAdisTS from View.CheckList.Window import CheckListWindow +from View.CheckList.WindowAdisTS import CheckListWindowAdisTS from View.Results.Window import ResultsWindow from View.Results.ReadingResultsDialog import ReadingResultsDialog from View.Debug.Window import ReplWindow @@ -242,7 +243,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_dif": self.open_dif, "action_menu_d90": self.open_d90, "action_menu_pollutants": self.open_pollutants, - "action_menu_run_adists":self.run_solver_adists, + "action_menu_run_adists":self.select_run_solver_adists, "action_menu_output_kp": self.open_output_kp_adists, "action_menu_config": self.open_configure, "action_menu_new": self.open_new_study, @@ -1258,7 +1259,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if run.exec(): self.run_solver(run.solver) - def run_solver_adists(self): + def select_run_solver_adists(self): if self._study is None: return @@ -1273,7 +1274,31 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): parent=self ) if run.exec(): - self.run_solver(run.solver) + self.run_solver_adists(run.solver, run.mage_rep) + + def run_solver_adists(self, solver, mage_rep): + if self._study is None: + return + + if self.sub_window_exists( + CheckListWindowAdisTS, + data=[ + self._study, + self.conf, + solver, + mage_rep + ] + ): + return + + check = CheckListWindowAdisTS( + study=self._study, + config=self.conf, + solver=solver, + parent=self, + mage_rep=mage_rep, + ) + check.show() def run_solver(self, solver): if self._study is None: @@ -1297,6 +1322,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) check.show() + def solver_log_adists(self, solver, mage_rep): + sol = SolverLogWindowAdisTS( + study=self._study, + config=self.conf, + solver=solver, + parent=self, + mage_rep=mage_rep, + ) + sol.show() + def solver_log(self, solver): sol = SolverLogWindow( study=self._study, diff --git a/src/View/RunSolver/WindowAdisTS.py b/src/View/RunSolver/WindowAdisTS.py index fd10c6d482e4d67328423e1e8af77fd0d87ab740..52e31dd5b4379903148a146fa9f6ada26618f26b 100644 --- a/src/View/RunSolver/WindowAdisTS.py +++ b/src/View/RunSolver/WindowAdisTS.py @@ -58,7 +58,7 @@ logger = logging.getLogger() class SelectSolverWindowAdisTS(PamhyrDialog): - _pamhyr_ui = "SelectSolver" + _pamhyr_ui = "SelectSolverAdisTS" _pamhyr_name = "Select solver" def __init__(self, study=None, config=None, @@ -89,7 +89,21 @@ class SelectSolverWindowAdisTS(PamhyrDialog): ) ) + solvers_mage = list(filter(lambda x: "mage" in x._type, self._config.solvers)) + solvers_mage_names = list(map(lambda x: x._name, solvers_mage)) + + solvers_dir = os.path.join( + os.path.dirname(self._study.filename), + "_PAMHYR_", + self._study.name.replace(" ", "_"), + ) + + dir_solvers_List = os.listdir(solvers_dir) + + display_mage_names = list(filter(lambda x: x in solvers_mage_names, dir_solvers_List)) + self.combobox_add_items("comboBox", solvers_name) + self.combobox_add_items("comboBoxRepMage", display_mage_names) def setup_connections(self): self.find(QPushButton, "pushButton_run").clicked.connect(self.accept) @@ -120,10 +134,16 @@ class SelectSolverWindowAdisTS(PamhyrDialog): def solver(self): return self._solver + @property + def mage_rep(self): + return self._mage_result_rep + def accept(self): solver_name = self.get_combobox_text("comboBox") solver_name = solver_name.rsplit(" - ", 1)[0] + self._mage_result_rep = self.get_combobox_text("comboBoxRepMage") + self._config.update_last_solver_used(solver_name) self._solver = next( @@ -135,3 +155,273 @@ class SelectSolverWindowAdisTS(PamhyrDialog): super(SelectSolverWindowAdisTS, self).accept() +class SolverLogWindowAdisTS(PamhyrWindow): + _pamhyr_ui = "SolverLog" + _pamhyr_name = "Solver Log" + + def __init__(self, study=None, config=None, + solver=None, parent=None, mage_rep=None): + self._solver = solver + self._results = None + self._mage_rep = mage_rep + + name = _translate("Solver", "Select log") + super(SolverLogWindowAdisTS, self).__init__( + title=name, + study=study, + config=config, + options=[], + parent=parent + ) + + self.setup_action() + self.setup_alarm() + self.setup_connections() + self.setup_workdir() + self.setup_process() + + ok = self.export() + if ok: + self.run() + else: + self._log( + f" *** Failed to export study to {self._solver._type}", + color="red" + ) + + def setup_action(self): + 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 setup_alarm(self): + self._alarm = QTimer() + + def setup_connections(self): + self.find(QAction, "action_start").triggered.connect(self.start) + self.find(QAction, "action_pause").triggered.connect(self.pause) + self.find(QAction, "action_stop").triggered.connect(self.stop) + self.find(QAction, "action_log_file").triggered.connect(self.log_file) + self.find(QAction, "action_results").triggered.connect(self.results) + + self._alarm.timeout.connect(self.update) + + def setup_workdir(self): + self._workdir = "" + if self._study.filename == "": + self._workdir = tempfile.TemporaryDirectory() + else: + self._workdir = os.path.join( + os.path.dirname(self._study.filename), + "_PAMHYR_", + self._study.name.replace(" ", "_"), + self._solver.name.replace(" ", "_"), + ) + os.makedirs(self._workdir, exist_ok=True) + + def setup_process(self): + self._alarm.start(100) + self._output = Queue() + self._process = self.new_process(self._parent) + + def new_process(self, parent): + new = QProcess(parent) + new.setWorkingDirectory(self._workdir) + new.setProcessChannelMode(QProcess.MergedChannels) + return new + + def export(self): + self._log(f" *** Export study {self._solver.name}", color="blue") + ok = self._solver.export(self._study, self._workdir, self._mage_rep, qlog=self._output) + self.update() + + return ok + + def closeEvent(self, event): + self._alarm.stop() + super(SolverLogWindowAdisTS, self).closeEvent(event) + + def _copy(self): + self.find(QTextEdit, "textEdit").copy() + + ####### + # LOG # + ####### + + def _log(self, msg, color=None): + if type(msg) is str: + self._log_str(msg, color) + elif type(msg) is int: + self._log_int(msg, color) + + def _log_str(self, msg, color=None): + if msg == "": + return + + logger.info(f"solver: {msg}") + msg = msg.rsplit('\n', 1)[0] + + if color is not None: + msg = f"<font color=\"{color}\">" + msg + "</font>" + + self.find(QTextEdit, "textEdit").append(msg) + + 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"<font color=\"{color}\">" + + f" *** Finished with code {int_code}" + + "</font>" + ) + + self.statusbar.showMessage( + "Done" if int_code == 0 else "Failed", + 3000 + ) + + ########## + # UPDATE # + ########## + + def update(self): + if self._solver.is_stoped(): + 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) + + self._update_logs_all() + # self._update_get_results() + + self._update_logs_all() + + def _update_get_results(self): + if self._results is None: + def reading_fn(): + try: + self._results = self._solver.results( + self._study, self._workdir, qlog=self._output + ) + self._parent.set_results(self._solver, self._results) + except Exception as e: + logger.error(f"Failed to open results") + logger_exception(e) + + dlg = ReadingResultsDialog(reading_fn=reading_fn, parent=self) + dlg.exec_() + + def _update_logs_all(self): + while self._output.qsize() != 0: + s = self._output.get() + + try: + if type(s) is str and "[ERROR]" in s: + self._log(s.encode("utf-8"), color="red") + else: + self._log(s) + except Exception as e: + logger_exception(e) + + #################### + # Process controle # + #################### + + def run(self): + self._log(f" *** Run solver {self._solver.name}", color="blue") + self._solver.run( + self._study, + process=self._process, + output_queue=self._output + ) + + 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, self._mage_rep, 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._solver.start(self._study, process=self._process) + + 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 pause(self): + self._log(" *** Pause", color="blue") + self._solver.pause() + + self.find(QAction, "action_start").setEnabled(True) + self.find(QAction, "action_pause").setEnabled(False) + self.find(QAction, "action_stop").setEnabled(True) + 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) + + ########### + # Results # + ########### + + def results(self): + if self._results is None: + def reading_fn(): + self._results = self._solver.results( + self._study, self._workdir, qlog=self._output + ) + + dlg = ReadingResultsDialog(reading_fn=reading_fn, parent=self) + dlg.exec_() + + self._parent.set_results(self._solver, self._results) + self._parent.open_solver_results(self._solver, self._results) + + self._solver.has_results_loaded() + + def log_file(self): + file_name = os.path.join(self._workdir, self._solver.log_file()) + log = SolverLogFileWindow( + file_name=file_name, + study=self._study, + config=self._config, + solver=self._solver, + parent=self, + ) + log.show() + diff --git a/src/View/ui/SelectSolverAdisTS.ui b/src/View/ui/SelectSolverAdisTS.ui new file mode 100644 index 0000000000000000000000000000000000000000..a014586567cd51116f377c5c2874f93c864e9a2f --- /dev/null +++ b/src/View/ui/SelectSolverAdisTS.ui @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Dialog</class> + <widget class="QDialog" name="Dialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>384</width> + <height>107</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <property name="locale"> + <locale language="English" country="Europe"/> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QComboBox" name="comboBox"/> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="pushButton_run"> + <property name="text"> + <string>Run</string> + </property> + <property name="icon"> + <iconset> + <normaloff>ressources/run.png</normaloff>ressources/run.png</iconset> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pushButton_cancel"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QComboBox" name="comboBoxRepMage"/> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>