From 51c912f3a896272941490a0664d334f493c2bd15 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby <pierre-antoine.rouby@inrae.fr> Date: Fri, 27 Oct 2023 09:33:32 +0200 Subject: [PATCH] Solver: Code refactoring. --- src/Solver/ASolver.py | 249 +-------------------------- src/Solver/CommandLine.py | 327 ++++++++++++++++++++++++++++++++++++ src/Solver/GenericSolver.py | 7 +- src/Solver/Mage.py | 4 +- 4 files changed, 337 insertions(+), 250 deletions(-) create mode 100644 src/Solver/CommandLine.py diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index b461d602..c4fa286a 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -59,17 +59,6 @@ class AbstractSolver(object): self._name = name self._description = "" - self._path_input = "" - self._path_solver = "" - self._path_output = "" - - self._cmd_input = "" - self._cmd_solver = "" - self._cmd_output = "" - - self._process = None - self._output = None - # Last study running self._study = None @@ -92,7 +81,6 @@ class AbstractSolver(object): ("all_init_time", "000:00:00:00"), ("all_final_time", "999:99:00:00"), ("all_timestep", "300.0"), - ("all_command_line_arguments", ""), ] return lst @@ -141,18 +129,6 @@ class AbstractSolver(object): def description(self, description): self._description = description - def set_input(self, path, cmd): - self._path_input = path - self._cmd_input = cmd - - def set_solver(self, path, cmd): - self._path_solver = path - self._cmd_solver = cmd - - def set_output(self, path, cmd): - self._path_output = path - self._cmd_output = cmd - ########## # Export # ########## @@ -160,41 +136,6 @@ class AbstractSolver(object): def export(self, study, repertory, qlog=None): raise NotImplementedMethodeError(self, self.export) - def cmd_args(self, study): - """Return solver command line arguments list - - Returns: - Command line arguments list - """ - params = study.river.get_params(self.type) - args = params.get_by_key("all_command_line_arguments") - - return args.split(" ") - - def input_param(self): - """Return input command line parameter(s) - - Returns: - Returns input parameter(s) string - """ - raise NotImplementedMethodeError(self, self.input_param) - - def output_param(self): - """Return output command line parameter(s) - - Returns: - Returns output parameter(s) string - """ - raise NotImplementedMethodeError(self, self.output_param) - - def log_file(self): - """Return log file name - - Returns: - Returns log file name as string - """ - raise NotImplementedMethodeError(self, self.log_file) - ########### # RESULTS # ########### @@ -208,195 +149,17 @@ class AbstractSolver(object): # Run # ####### - def _install_dir(self): - return os.path.abspath( - os.path.join( - os.path.dirname(__file__), - "..", ".." - ) - ) - - def _format_command(self, study, cmd, path=""): - """Format command line - - Args: - cmd: The command line - path: Optional path string (replace @path in cmd) - - Returns: - The executable and list of arguments - """ - # HACK: Works in most case... Trust me i'm an engineer - cmd = cmd.replace("@install_dir", self._install_dir()) - cmd = cmd.replace("@path", path.replace(" ", "%20")) - cmd = cmd.replace("@input", self.input_param()) - cmd = cmd.replace("@output", self.output_param()) - cmd = cmd.replace("@dir", self._process.workingDirectory()) - cmd = cmd.replace("@args", " ".join(self.cmd_args(study))) - - logger.debug(f"! {cmd}") - - if cmd[0] == "\"": - # Command line executable path is between " char - cmd = cmd.split("\"") - exe = cmd[1].replace("%20", " ") - args = list( - filter( - lambda s: s != "", - "\"".join(cmd[2:]).split(" ")[1:] - ) - ) - else: - # We suppose the command line executable path as no space char - cmd = cmd.replace("\\ ", "&_&").split(" ") - exe = cmd[0].replace("&_&", " ") - args = list( - filter( - lambda s: s != "", - map(lambda s: s.replace("&_&", " "), cmd[1:]) - ) - ) - - logger.info(f"! {exe} {args}") - return exe, args - - def run_input_data_fomater(self, study): - if self._cmd_input == "": - self._run_next(study) - return True - - cmd = self._cmd_input - exe, args = self._format_command(study, cmd, self._path_input) - - if not os.path.exists(exe): - error = f"[ERROR] Path {exe} do not exists" - logger.info(error) - return error - - self._process.start( - exe, args, - ) - - return True - - def run_solver(self, study): - if self._cmd_solver == "": - self._run_next(study) - return True - - cmd = self._cmd_solver - exe, args = self._format_command(study, cmd, self._path_solver) - - if not os.path.exists(exe): - error = f"[ERROR] Path {exe} do not exists" - logger.info(error) - return error - - self._process.start( - exe, args, - ) - self._process.waitForStarted() - - self._status = STATUS.RUNNING - return True - - def run_output_data_fomater(self, study): - if self._cmd_output == "": - self._run_next(study) - return True - - cmd = self._cmd_output - exe, args = self._format_command(study, cmd, self._path_output) - - if not os.path.exists(exe): - error = f"[ERROR] Path {exe} do not exists" - logger.info(error) - return error - - self._process.start( - exe, args, - ) - - return True - - def _data_ready(self): - s = self._process.readAll().data().decode() - if self._output is not None: - for x in s.split('\n'): - self._output.put(x) - - def _run_next(self, study): - self._step += 1 - if self._step < len(self._runs): - res = self._runs[self._step](study) - if res is not True: - self._output.put(res) - else: - self._status = STATUS.STOPED - - def _finished(self, study, exit_code, exit_status): - if self._output is not None: - self._output.put(exit_code) - - self._run_next(study) - - def run(self, study, process=None, output_queue=None): - self._study = study - - if process is not None: - self._process = process - if output_queue is not None: - self._output = output_queue - - self._process.readyRead.connect(self._data_ready) - self._process.finished.connect( - lambda c, s: self._finished(study, c, s)) - - self._runs = [ - self.run_input_data_fomater, - self.run_solver, - self.run_output_data_fomater, - ] - self._step = 0 - # Run first step - res = self._runs[0](study) - if res is not True: - self._output.put(res) + def run(self, study): + raise NotImplementedMethodeError(self, self.run) def kill(self): - if self._process is None: - return True - - self._process.kill() - self._status = STATUS.STOPED - return True + raise NotImplementedMethodeError(self, self.kill) def start(self, study, process=None): - if _signal: - # Solver is PAUSED, so continue execution - if self._status == STATUS.PAUSED: - os.kill(self._process.pid(), SIGCONT) - self._status = STATUS.RUNNING - return True - - self.run(study, process) - return True + raise NotImplementedMethodeError(self, self.start) def pause(self): - if _signal: - if self._process is None: - return False - - # Send SIGSTOP to PAUSED solver - os.kill(self._process.pid(), SIGSTOP) - self._status = STATUS.PAUSED - return True - return False + raise NotImplementedMethodeError(self, self.pause) def stop(self): - if self._process is None: - return False - - self._process.terminate() - self._status = STATUS.STOPED - return True + raise NotImplementedMethodeError(self, self.stop) diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py new file mode 100644 index 00000000..c922b86a --- /dev/null +++ b/src/Solver/CommandLine.py @@ -0,0 +1,327 @@ +# CommandLine.py -- Pamhyr +# Copyright (C) 2023 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 -*- + +import os +import logging + +from tools import timer + +try: + # Installation allow Unix-like signal + from signal import SIGTERM, SIGSTOP, SIGCONT + _signal = True +except Exception: + _signal = False + +from enum import Enum + +from Model.Except import NotImplementedMethodeError + +from Model.Results.Results import Results +from Model.Results.River.River import River, Reach, Profile + +from Solver.ASolver import AbstractSolver, STATUS + +logger = logging.getLogger() + + +class CommandLineSolver(AbstractSolver): + _type = "" + + def __init__(self, name): + super(CommandLineSolver, self).__init__(name) + + self._current_process = None + self._status = STATUS.NOT_LAUNCHED + + self._path_input = "" + self._path_solver = "" + self._path_output = "" + + self._cmd_input = "" + self._cmd_solver = "" + self._cmd_output = "" + + self._process = None + self._output = None + + # Last study running + self._study = None + + @classmethod + def default_parameters(cls): + lst = super(CommandLineSolver, cls).default_parameters() + + lst += [ + ("all_command_line_arguments", ""), + ] + + return lst + + def set_input(self, path, cmd): + self._path_input = path + self._cmd_input = cmd + + def set_solver(self, path, cmd): + self._path_solver = path + self._cmd_solver = cmd + + def set_output(self, path, cmd): + self._path_output = path + self._cmd_output = cmd + + ########## + # Export # + ########## + + def cmd_args(self, study): + """Return solver command line arguments list + + Returns: + Command line arguments list + """ + params = study.river.get_params(self.type) + args = params.get_by_key("all_command_line_arguments") + + return args.split(" ") + + def input_param(self): + """Return input command line parameter(s) + + Returns: + Returns input parameter(s) string + """ + raise NotImplementedMethodeError(self, self.input_param) + + def output_param(self): + """Return output command line parameter(s) + + Returns: + Returns output parameter(s) string + """ + raise NotImplementedMethodeError(self, self.output_param) + + def log_file(self): + """Return log file name + + Returns: + Returns log file name as string + """ + raise NotImplementedMethodeError(self, self.log_file) + + ####### + # Run # + ####### + + def _install_dir(self): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", ".." + ) + ) + + def _format_command(self, study, cmd, path=""): + """Format command line + + Args: + cmd: The command line + path: Optional path string (replace @path in cmd) + + Returns: + The executable and list of arguments + """ + # HACK: Works in most case... Trust me i'm an engineer + cmd = cmd.replace("@install_dir", self._install_dir()) + cmd = cmd.replace("@path", path.replace(" ", "%20")) + cmd = cmd.replace("@input", self.input_param()) + cmd = cmd.replace("@output", self.output_param()) + cmd = cmd.replace("@dir", self._process.workingDirectory()) + cmd = cmd.replace("@args", " ".join(self.cmd_args(study))) + + logger.debug(f"! {cmd}") + + if cmd[0] == "\"": + # Command line executable path is between " char + cmd = cmd.split("\"") + exe = cmd[1].replace("%20", " ") + args = list( + filter( + lambda s: s != "", + "\"".join(cmd[2:]).split(" ")[1:] + ) + ) + else: + # We suppose the command line executable path as no space char + cmd = cmd.replace("\\ ", "&_&").split(" ") + exe = cmd[0].replace("&_&", " ") + args = list( + filter( + lambda s: s != "", + map(lambda s: s.replace("&_&", " "), cmd[1:]) + ) + ) + + logger.info(f"! {exe} {args}") + return exe, args + + def run_input_data_fomater(self, study): + if self._cmd_input == "": + self._run_next(study) + return True + + cmd = self._cmd_input + exe, args = self._format_command(study, cmd, self._path_input) + + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.info(error) + return error + + self._process.start( + exe, args, + ) + + return True + + def run_solver(self, study): + if self._cmd_solver == "": + self._run_next(study) + return True + + cmd = self._cmd_solver + exe, args = self._format_command(study, cmd, self._path_solver) + + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.info(error) + return error + + self._process.start( + exe, args, + ) + self._process.waitForStarted() + + self._status = STATUS.RUNNING + return True + + def run_output_data_fomater(self, study): + if self._cmd_output == "": + self._run_next(study) + return True + + cmd = self._cmd_output + exe, args = self._format_command(study, cmd, self._path_output) + + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.info(error) + return error + + self._process.start( + exe, args, + ) + + return True + + def _data_ready(self): + # Read process output and put lines in queue + s = self._process.readAll().data().decode() + if self._output is not None: + for x in s.split('\n'): + self._output.put(x) + + def _run_next(self, study): + self._step += 1 + if self._step < len(self._runs): + res = self._runs[self._step](study) + if res is not True: + self._output.put(res) + else: + self._status = STATUS.STOPED + + def _finished(self, study, exit_code, exit_status): + if self._output is not None: + self._output.put(exit_code) + + self._run_next(study) + + def run(self, study, process=None, output_queue=None): + self._study = study + + # Replace old values if needed + if process is not None: + self._process = process + if output_queue is not None: + self._output = output_queue + + # Connect / reconnect signal + self._process.readyRead.connect(self._data_ready) + self._process.finished.connect( + lambda c, s: self._finished(study, c, s)) + + # Prepare running step + self._runs = [ + self.run_input_data_fomater, + self.run_solver, + self.run_output_data_fomater, + ] + self._step = 0 + + # Run first step + res = self._runs[0](study) + if res is not True: + self._output.put(res) + + def kill(self): + if self._process is None: + return True + + self._process.kill() + self._status = STATUS.STOPED + return True + + def start(self, study, process=None): + if _signal: + # Solver is PAUSED, so continue execution + if self._status == STATUS.PAUSED: + os.kill(self._process.pid(), SIGCONT) + self._status = STATUS.RUNNING + return True + + self.run(study, process) + return True + + def pause(self): + if _signal: + if self._process is None: + return False + + # Send SIGSTOP to PAUSED solver + os.kill(self._process.pid(), SIGSTOP) + self._status = STATUS.PAUSED + return True + return False + + def stop(self): + if self._process is None: + return False + + self._process.terminate() + self._status = STATUS.STOPED + return True diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py index bc5a9f6b..9fb272e4 100644 --- a/src/Solver/GenericSolver.py +++ b/src/Solver/GenericSolver.py @@ -16,12 +16,9 @@ # -*- coding: utf-8 -*- -from Solver.ASolver import ( - AbstractSolver, STATUS -) +from Solver.CommandLine import CommandLineSolver - -class GenericSolver(AbstractSolver): +class GenericSolver(CommandLineSolver): _type = "generic" def __init__(self, name): diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index baf23a9b..52efc071 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -22,7 +22,7 @@ import numpy as np from tools import timer -from Solver.ASolver import AbstractSolver +from Solver.CommandLine import CommandLineSolver from Checker.Mage import MageNetworkGraphChecker from Model.Results.Results import Results @@ -41,7 +41,7 @@ def mage_file_open(filepath, mode): return f -class Mage(AbstractSolver): +class Mage(CommandLineSolver): _type = "mage" def __init__(self, name): -- GitLab