From bb2e83b6ce958f29e218f3e297963a17eec22555 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Thu, 16 Jul 2026 10:44:01 +0200 Subject: [PATCH] AdisTT: Stable version of files writer for adisTT solver --- src/Solver/AdisTT.py | 1133 ++++++++++++++++++++++++++++ src/Solver/Solvers.py | 3 + src/View/MainWindow.py | 18 +- src/View/RunSolver/WindowAdisTT.py | 505 +++++++++++++ src/View/ui/MainWindow.ui | 10 + src/View/ui/SelectSolverAdisTT.ui | 79 ++ src/View/ui/SolverLogAdisTT.ui | 122 +++ src/config.py | 31 +- 8 files changed, 1895 insertions(+), 6 deletions(-) create mode 100644 src/Solver/AdisTT.py create mode 100644 src/View/RunSolver/WindowAdisTT.py create mode 100644 src/View/ui/SelectSolverAdisTT.ui create mode 100644 src/View/ui/SolverLogAdisTT.ui diff --git a/src/Solver/AdisTT.py b/src/Solver/AdisTT.py new file mode 100644 index 00000000..b840bcfb --- /dev/null +++ b/src/Solver/AdisTT.py @@ -0,0 +1,1133 @@ +# AdisTT.py -- Pamhyr +# Copyright (C) 2023-2025 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 . + +# -*- coding: utf-8 -*- + +import os +import logging + +import numpy as np + +import shutil + +from tools import ( + trace, timer, logger_exception, + timestamp_to_old_pamhyr_date, + old_pamhyr_date_to_timestamp, + timestamp_to_old_pamhyr_date_adists +) + +from Solver.CommandLine import CommandLineSolver + +from Model.Results.ResultsAdisTS import Results +from Model.Results.River.River import River, Reach, Profile + +from itertools import chain + +logger = logging.getLogger() + + +def adistt_file_open(filepath, mode): + f = open(filepath, mode) + + if "w" in mode: + # Write header + comment = "*" + if ".ST" in filepath: + comment = "#" + + f.write( + f"{comment} " + + "This file is generated by PAMHYR, please don't modify\n" + ) + + return f + + +class AdisTT(CommandLineSolver): + _type = "adistt" + + def __init__(self, name): + super(AdisTT, self).__init__(name) + + self._type = "adistt" + + self._cmd_input = "" + self._cmd_solver = "@path @input -o @output" + self._cmd_output = "" + + @classmethod + def default_parameters(cls): + lst = super(AdisTT, cls).default_parameters() + + lst += [ + ("adistt_implicitation_parameter", "0.5"), + ("adistt_timestep_screen", "60"), + ("adistt_timestep_bin", "60"), + ("adistt_timestep_csv", "60"), + ("adistt_timestep_mage", "60"), + ("adistt_initial_concentration", "60"), + ] + + return lst + + @classmethod + def checkers(cls): + lst = [ + ] + + return lst + + ########## + # Export # + ########## + + _alph = list( + map( + chr, + chain( + range(48, 58), # 0..9 + range(65, 91), # A..Z + range(97, 123) # a..z + ) + ) + ) + + _l_alph = len(_alph) + + _nodes_cnt = 0 + _nodes_names = {} + _nodes_views = set() + + def get_reach_name(self, reach): + return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0") + + def get_node_name(self, node): + """Generate a 3 char name for node + + Args: + node: The node + + Returns: + A 3 char name string + """ + n = node.pamhyr_id + + if n in self._nodes_names: + return self._nodes_names[n] + + name = "" + + checked_new = False + while not checked_new: + self._nodes_cnt += 1 + nc = self._nodes_cnt + + name = "".join( + map( + lambda i: self._alph[i % self._l_alph], + [ + int(nc / (self._l_alph * self._l_alph)), + int(nc / self._l_alph), + nc + ] + ) + ) + + checked_new = name not in self._nodes_views + + self._nodes_views.add(name) + self._nodes_names[n] = name + + return name + + def cmd_args(self, study): + lst = super(AdisTT, self).cmd_args(study) + return lst + + def input_param(self): + name = self._study.name.replace(" ", "_") + return f"{name}.REP" + + def log_file(self): + name = self._study.name.replace(" ", "_") + return f"{name}.TRA" + + def _export_ST(self, study, repertory, qlog, name="0"): + files = [] + + if qlog is not None: + qlog.put("Export ST file") + + os.makedirs(os.path.join(repertory, "net"), exist_ok=True) + + # Write header + edges = study.river.enable_edges() + for edge in edges: + name = f"Reach_{edge.id + 1:>3}".replace(" ", "0") + + with adistt_file_open( + os.path.join(repertory, "net", f"{name}.ST"), + "w+" + ) as f: + files.append(str(os.path.join("net", f"{name}.ST"))) + + cnt_num = 1 + for profile in edge.reach.profiles: + self._export_ST_profile_header( + f, files, profile, cnt_num + ) + cnt_num += 1 + + # Points + for point in profile.points: + self._export_ST_point_line( + f, files, point + ) + + # Profile last line + f.write(f" 999.9990 999.9990 999.9990\n") + + def _export_ST_profile_header(self, wfile, files, + profile, cnt): + num = f"{cnt:>6}" + c1 = f"{profile.code1:>6}" + c2 = f"{profile.code2:>6}" + t = f"{len(profile.points):>6}" + rk = f"{profile.rk:>12f}"[0:12] + pname = profile.name + if profile.name == "": + # Generate name from profile id prefixed with + # 'p' (and replace space char with '0' char) + pname = f"p{profile.id:>3}".replace(" ", "0") + name = f"{pname:<19}" + + # Profile header line + # AdisTT only reads the hydraulic geometry. Mage sediment-layer + # fields are deliberately omitted because AdisTT interprets them + # with a different grammar. + wfile.write(f"{num}{c1}{c2}{t} {rk} {pname}\n") + + def _export_ST_point_line(self, wfile, files, point): + x = f"{point.x:<12.4f}"[0:12] + y = f"{point.y:<12.4f}"[0:12] + z = f"{point.z:<12.4f}"[0:12] + n = f"{point.name:<3}" + + # Point line + wfile.write(f"{x} {y} {z} {n}\n") + + def _export_NET(self, study, repertory, qlog=None, name="0"): + + if qlog is not None: + qlog.put("Export NET file") + + with adistt_file_open( + os.path.join(repertory, f"{name}.NET"), "w+") as f: + edges = study.river.enable_edges() + + for e in edges: + name = f"Reach_{e.id + 1:>3}".replace(" ", "0") + id = name + + n1 = f"{self.get_node_name(e.node1):3}".replace(" ", "x") + n2 = f"{self.get_node_name(e.node2):3}".replace(" ", "x") + file = os.path.join("net", name + ".ST") + + f.write(f"{id} {n1} {n2} {file}\n") + + def _export_fake_INI(self, study, repertory, qlog=None, name="0"): + if qlog is not None: + qlog.put("Export fake INI file") + + with adistt_file_open( + os.path.join( + repertory, "Mage_fin.ini" + ), "w+" + ) as f: + edges = study.river.enable_edges() + for i, edge in enumerate(edges): + lst = list(filter( + lambda f: f.is_full_defined(), + edge.frictions.frictions + )) + rk_min = 9999999.9 + rk_max = -9999999.9 + coeff_min = -1.0 + coeff_max = -1.0 + for s in lst: # TODO optimise ? + if s.begin_rk > rk_max: + rk_max = s.begin_rk + coeff_max = s.begin_strickler + if s.begin_rk < rk_min: + rk_min = s.begin_rk + coeff_min = s.begin_strickler + if s.end_rk > rk_max: + rk_max = s.end_rk + coeff_max = s.end_strickler + if s.end_rk < rk_min: + rk_min = s.end_rk + coeff_min = s.end_strickler + + print("min max", rk_min, rk_max) + + def get_stricklers_from_rk(rk, lst): + print("rk", rk) + + coeff = None + if rk > rk_max: + coeff = coeff_max + elif rk < rk_min: + coeff = coeff_min + else: + for s in lst: + if (rk >= s.begin_rk and rk <= s.end_rk or + rk <= s.begin_rk and rk >= s.end_rk): + coeff = s.begin_strickler # TODO: inerpolate + break + + # TODO interpolation if rk is not in frictons + + if coeff is None: + logger.error( + "Study frictions are not fully defined" + ) + return None + + return coeff.minor, coeff.medium + + for j, profile in enumerate(edge.reach.profiles): + coef_min, coef_moy = get_stricklers_from_rk(profile.rk, + lst) + f.write( + f" {i+1:3}{j+1:4}{' '*116}{coef_min:9}{coef_moy:9}\n") + + def _export_REP_additional_lines(self, study, rep_file): + lines = filter( + lambda line: line.is_enabled(), + study.river.rep_lines.lines + ) + + for line in lines: + rep_file.write(line.line) + + def _export_REP(self, study, repertory, mage_rep, files, qlog, name="0"): + + if qlog is not None: + qlog.put("Export REP file") + + # Write header + with adistt_file_open( + os.path.join( + repertory, f"{name}.REP" + ), "w+" + ) as f: + path = os.path.join("..", mage_rep, name) + f.write(f"NET {path}.NET\n") + f.write(f"REP {path}.REP\n") + + for file in files: + EXT = file.split('.')[1] + f.write(f"{EXT} {file}\n") + + self._export_REP_additional_lines(study, f) + + # fake mage_fin.ini: + + path_mage = os.path.join(os.path.abspath( + os.path.join(repertory, os.pardir)), mage_rep) + self._export_fake_INI(study, path_mage, + qlog, name=name) + + # path_mage_net = os.path.join(os.path.abspath( + # os.path.join(repertory, os.pardir) + # ), os.path.join(mage_rep, "net")) + # path_adistt_net = os.path.join(repertory, "net") + + # if os.path.exists(path_mage_net): + # shutil.copytree(path_mage_net, + # path_adistt_net, + # dirs_exist_ok=True) + + @timer + def export(self, study, repertory, qlog=None): + self._study = study + name = study.name.replace(" ", "_") + + self.export_additional_files(study, repertory, qlog, name=name) + + return True + + ########### + # RESULTS # + ########### + + def read_bin(self, study, repertory, results, qlog=None, name="0"): + return + + @timer + def results(self, study, repertory, qlog=None, name="0", type_pol=None): + results = Results( + study=study, + solver=self, + repertory=repertory, + name=name, + type_pol=type_pol, + ) + self.read_bin(study, repertory, results, qlog, name=name) + + return results + + def output_param(self): + name = "" + return f"{name}" + + _alph = list( + map( + chr, + chain( + range(48, 58), # 0..9 + range(65, 91), # A..Z + range(97, 123) # a..z + ) + ) + ) + + _l_alph = len(_alph) + + _nodes_cnt = 0 + _nodes_names = {} + _nodes_views = set() + + def get_reach_name(self, reach): + return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0") + +################################# +# Adis-TT in weak coupling mode # +################################# + + +class AdisTTwc(AdisTT): + _type = "adisttwc" + + def __init__(self, name): + super(AdisTTwc, self).__init__(name) + + self._type = "adisttwc" + + @classmethod + def default_parameters(cls): + lst = super(AdisTTwc, cls).default_parameters() + + # Insert new parameters at specific position + names = list(map(lambda t: t[0], lst)) + + return lst + + ########## + # Export # + ########## + + def cmd_args(self, study): + lst = super(AdisTTwc, self).cmd_args(study) + + return lst + + def _export_TEMs(self, study, repertory, qlog=None, name="0"): + + files = [] + + if qlog is not None: + qlog.put("Export TEMs files") + + name = "TEM" + with adistt_file_open( + os.path.join(repertory, f"{name}.POL"), "w+" + ) as f: + + TEM_ICs = ( + study.river._InitialConditionsTemperature.Initial_Conditions_List[0] + ) + + if TEM_ICs._temperature is not None: + f.write(f"file_ini = {name}.INI\n") + self._export_ICs_AdisTT( + study, repertory, TEM_ICs, qlog, name + ) + + TEM_BCs =( + study.river._BoundaryConditionsTemperature.BCs_Temperature_List + ) + + if len(TEM_BCs) != 0: + f.write(f"file_cl = {name}.CDT\n") + self._export_BCs_AdisTT( + study, repertory, TEM_BCs, qlog, name + ) + + return files + + def _export_BCs_AdisTT(self, study, repertory, TEM_BC, qlog, TEM_name): + + if qlog is not None: + qlog.put("Export TEM BCs files") + + with adistt_file_open(os.path.join( + repertory, f"{TEM_name}.CDT"), "w+" + ) as f: + for BC in TEM_BC: + node = next(filter( + lambda x: x.id == BC.node, study.river._nodes + )) + f.write(f"${self.get_node_name(node)}\n") + + f.write(f"*temps |temperature\n") + f.write(f"*JJ:HH:MM | (°C)\n") + f.write(f"*---------++++++++++\n") + + for BC_data in BC._data: + tmp = timestamp_to_old_pamhyr_date_adists(int(BC_data[0])) + f.write(" ".join((f"{tmp}", + f"{BC_data[1]}\n"))) + f.write(f"*\n") + + return True + + def _export_ICs_AdisTT(self, study, repertory, + TEM_IC_default, qlog, TEM_name): + + if qlog is not None: + qlog.put("Export TEM ICs files") + + with adistt_file_open(os.path.join( + repertory, f"{TEM_name}.INI" + ), "w+") as f: + f.write(f"*État initial pour le polluant {TEM_name}\n") + f.write(" ".join(("DEFAULT =", + f"{TEM_IC_default.temperature}\n"))) + + if len(TEM_IC_default._data) != 0: + self._export_ICs_AdisTT_Spec( + study, TEM_IC_default._data, f, qlog + ) + + def _export_ICs_AdisTT_Spec(self, study, tem_ics_spec_data, + f, qlog, name="0"): + + edges = study.river.enable_edges() + for ic_spec in tem_ics_spec_data: + id_reach = ic_spec.reach + reach = next((x for x in edges if x.id == id_reach), None) + if reach is None: + continue + + f.write(" ".join((f"{ic_spec.name}", + "=", + f"{study.river.get_edge_id(reach)+1}", + f"{ic_spec.start_rk}", + f"{ic_spec.end_rk}", + f"{ic_spec.temperature}\n"))) + + return True + + # def _export_D90(self, study, repertory, qlog=None, name="0"): + + # files = [] + + # if qlog is not None: + # qlog.put("Export D90 file") + + # with adistt_file_open( + # os.path.join(repertory, f"{name}.D90"), "w+" + # ) as f: + # files.append(f"{name}.D90") + + # f.write(f"*Diamètres caractéristiques du fond stable\n") + + # d90AdisTT = study.river.d90_adistt.D90_AdisTT_List + + # f.write(f"DEFAULT = {d90AdisTT[0].d90}\n") + + # self._export_d90_spec(study, d90AdisTT[0]._data, f, qlog) + + # return files + + # def _export_d90_spec(self, study, d90_spec_data, f, qlog, name="0"): + + # for d90_spec in d90_spec_data: + # if (d90_spec.name is None + # or d90_spec.reach is None + # or d90_spec.start_rk is None + # or d90_spec.end_rk is None + # or d90_spec.d90 is None): + # return + + # edges = study.river.enable_edges() + + # id_edges = list(map(lambda x: x.id, edges)) + + # id_reach = d90_spec.reach + # reach = next((x for x in edges if x.id == id_reach), None) + + # if reach is None: + # return + + # f.write(" ".join((f"{d90_spec.name}", + # "=", + # f"{study.river.get_edge_id(reach)+1}", + # f"{d90_spec.start_rk}", + # f"{d90_spec.end_rk}", + # f"{d90_spec.d90}\n"))) + + # def _export_DIF(self, study, repertory, qlog=None, name="0"): + + # files = [] + + # if qlog is not None: + # qlog.put("Export DIF file") + + # with adistt_file_open( + # os.path.join(repertory, f"{name}.DIF"), "w+" + # ) as f: + # files.append(f"{name}.DIF") + + # f.write(f"*Définition des paramètres des fonctions de calcul du\n") + # f.write(f"*coefficient de diffusion\n") + + # difAdisTT = study.river.dif_adistt.DIF_AdisTT_List + + # if difAdisTT[0].method != "generique": + # f.write(" ".join((f"defaut = ", + # f"{difAdisTT[0].method}", + # f"{difAdisTT[0].dif}\n"))) + # else: + # f.write(" ".join((f"defaut =" + # f"{difAdisTT[0].method}", + # f"{difAdisTT[0].dif}", + # f"{difAdisTT[0].b}", + # f"{difAdisTT[0].c}\n"))) + + # self._export_dif_spec(study, difAdisTT[0]._data, f, qlog) + + # return files + + # def _export_dif_spec(self, study, dif_spec_data, f, qlog, name="0"): + + # for dif_spec in dif_spec_data: + # if (dif_spec.reach is None + # or dif_spec.start_rk is None + # or dif_spec.end_rk is None + # or dif_spec.dif is None + # or dif_spec.b is None + # or dif_spec.c is None): + # return + + # edges = study.river.enable_edges() + + # id_reach = dif_spec.reach + # reach = next((x for x in edges if x.id == id_reach), None) + + # if reach is None: + # return + + # if dif_spec.method != "generique": + # f.write(" ".join((f"{dif_spec.method}", + # "=", + # f"{study.river.get_edge_id(reach)+1}", + # f"{dif_spec.start_rk}", + # f"{dif_spec.end_rk}", + # f"{dif_spec.dif}\n"))) + # else: + # f.write(" ".join((f"{dif_spec.method}", + # f"=" f"{study.river.get_edge_id(reach)+1}", + # f"{dif_spec.start_rk}", + # f"{dif_spec.end_rk}", + # f"{dif_spec.dif}", + # f"{dif_spec.b}", + # f"{dif_spec.c}\n"))) + + def _export_NUM(self, study, repertory, qlog=None, name="0"): + + dict_names = {"init_time": "start_date", + "final_time": "end_date", + "timestep": "dt0", + "implicitation_parameter": "theta", + "timestep_screen": "dtscr", + "timestep_bin": "dtbin", + "timestep_csv": "dtcsv", + "timestep_mage": "dtMage", + "initial_concentration": "c_initiale"} + files = [] + + if qlog is not None: + qlog.put("Export NUM file") + + with adistt_file_open( + os.path.join(repertory, f"{name}.NUM"), "w+" + ) as f: + files.append(f"{name}.NUM") + + params = study.river.get_params(self.type).parameters + for p in params: + name = p.name\ + .replace("all_", "")\ + .replace("adistt_", "") + value = p.value + + logger.debug( + f"export: NUM: {name}: {value} ({p.value})" + ) + + if name != "command_line_arguments": + f.write(f"{dict_names[name]} = {value}\n") + + return files + + @timer + def read_bin(self, study, repertory, results, qlog=None, name="0"): + + filelist = [f for f in os.listdir(repertory) + if os.path.isfile(os.path.join(repertory, f)) + ] + files_bin_names = [f for f in filelist if f[-4:] == ".bin"] + files_bin_names.insert(0, files_bin_names.pop( + files_bin_names.index("total_sediment.bin")) + ) + + ifilename = os.path.join(repertory, files_bin_names[0]) + + logger.info(f"read_bin: Start reading '{ifilename}' ...") + + with open(ifilename, 'rb') as f: + # header + # first line + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + data = np.fromfile(f, dtype=np.int32, count=3) + ibmax = data[0] # number of reaches + ismax = data[1] # total number of cross sections + kbl = data[2] * -1 # block size for .BIN header + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # second line + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + ibu = np.fromfile(f, dtype=np.int32, count=ibmax) + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # third line + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + data = np.fromfile(f, dtype=np.int32, count=2 * ibmax) + is1 = np.zeros(ibmax, dtype=np.int32) + is2 = np.zeros(ibmax, dtype=np.int32) + + logger.debug(f"read_bin: nb_reach = {ibmax}") + logger.debug(f"read_bin: nb_profile = {ismax}") + + results.set("nb_reach", f"{ibmax}") + results.set("nb_profile", f"{ismax}") + + reachs = [] + iprofiles = {} + reach_offset = {} + + for i in range(ibmax): + # Add results reach to reach list + r = results.river.add(i) + reachs.append(r) + + is1[i] = data[2 * i] - 1 # first section of reach i + is2[i] = data[2 * i + 1] - 1 # last section of reach i + + key = (is1[i], is2[i]) + iprofiles[key] = r + + reach_offset[r] = is1[i] + + logger.debug(f"read_bin: iprofiles = {iprofiles}") + + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # fourth line + pk = np.zeros(ismax, dtype=np.float32) + for k in range(0, ismax, kbl): + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + pk[k:min(k + kbl, ismax)] = np.fromfile(f, + dtype=np.float32, + count=min( + k + kbl, ismax + ) - k) + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + + # fifth line (useless) + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + zmin_OLD = np.fromfile(f, dtype=np.float32, count=1)[0] + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # sixth line + zf = np.zeros(ismax, dtype=np.float32) + z = np.zeros(ismax * 3, dtype=np.float32) + for k in range(0, ismax, kbl): + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + z[3 * k:3 * min(k + kbl, ismax)] = \ + np.fromfile(f, + dtype=np.float32, + count=3 * (min(k + kbl, ismax) - k) + ) + # z[i*3+1] and z[i*3+2] are useless + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + zf = [z[i * 3] for i in range(ismax)] + # seventh line (useless) + for k in range(0, ismax, kbl): + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + zero = np.fromfile(f, dtype=np.int32, count=ismax) + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # end header + + def ip_to_r(i): + return iprofiles[ + next( + filter( + lambda k: k[0] <= i <= k[1], + iprofiles + ) + ) + ] + + def ip_to_ri(r, i): return i - reach_offset[r] + + path_files = map(lambda file: os.path.join( + repertory, file), files_bin_names) + + data_tmp = {} + + for file_bin in path_files: + key_pol = os.path.basename(file_bin)[0:-4] + data_tmp[key_pol] = {} + logger.info(f"read_bin: Start reading '{file_bin}' ...") + with open(file_bin, 'rb') as f: + # header + # first line + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + data = np.fromfile(f, dtype=np.int32, count=3) + ibmax = data[0] # number of reaches + ismax = data[1] # total number of cross sections + kbl = data[2] * -1 # block size for .BIN header + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # second line + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + ibu = np.fromfile(f, dtype=np.int32, count=ibmax) + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # third line + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + data = np.fromfile(f, dtype=np.int32, count=2 * ibmax) + is1 = np.zeros(ibmax, dtype=np.int32) + is2 = np.zeros(ibmax, dtype=np.int32) + for i in range(ibmax): + # first section of reach i (FORTRAN numbering) + is1[i] = data[2 * i] + # last section of reach i (FORTRAN numbering) + is2[i] = data[2 * i + 1] + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # fourth line + pk = np.zeros(ismax, dtype=np.float32) + for k in range(0, ismax, kbl): + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + pk[k:min(k + kbl, ismax)] = np.fromfile( + f, dtype=np.float32, count=min(k + kbl, ismax) - k + ) + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # fifth line (useless) + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + zmin_OLD = np.fromfile(f, dtype=np.float32, count=1)[0] + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # sixth line + zf = np.zeros(ismax, dtype=np.float32) + z = np.zeros(ismax * 3, dtype=np.float32) + for k in range(0, ismax, kbl): + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + z[3 * k:3 * min(k + kbl, ismax)] = np.fromfile( + f, dtype=np.float32, + count=3 * (min(k + kbl, ismax) - k) + ) + # z[i*3+1] and z[i*3+2] are useless + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + zf = [z[i * 3] for i in range(ismax)] + # seventh line (useless) + for k in range(0, ismax, kbl): + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + zero = np.fromfile( + f, dtype=np.int32, count=min(k + kbl, ismax) - k + ) + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + # end header + # data + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + while data.size > 0: + ismax = np.fromfile(f, dtype=np.int32, count=1)[0] + t = np.fromfile(f, dtype=np.float64, count=1)[0] + if t not in data_tmp[key_pol]: + data_tmp[key_pol][t] = {} + c = np.fromfile(f, dtype=np.byte, count=1) + # possible values : + # sediment : C, G, M, D, L, N, R + # polutant : C + phys_var = bytearray(c).decode() + data_tmp[key_pol][t][phys_var] = {} + real_data = np.fromfile(f, dtype=np.float32, count=ismax) + data = np.fromfile(f, dtype=np.int32, count=1) # (end) + data_tmp[key_pol][t][phys_var] = real_data + data = np.fromfile(f, dtype=np.int32, count=1) # (start) + # end data + + pollutants_keys = list(data_tmp.keys()) + timestamps_keys = list(data_tmp[pollutants_keys[0]].keys()) + phys_data_names = list(data_tmp[pollutants_keys[0]] + [timestamps_keys[0]].keys()) + type_pol_index = len(phys_data_names) + + for r, reach in enumerate(reachs): + for i in range(is1[r]-1, is2[r]): + p_i = ip_to_ri(reach, i) + for t_data in timestamps_keys: + pol_view = [] + for pol in pollutants_keys: + pol_view.append(tuple(list(map( + lambda data_el: data_el[i], + list(data_tmp[pol][t_data].values()) + )))) + + reach.set(p_i, t_data, "pols", pol_view) + + results.set("timestamps", set(timestamps_keys)) + + @timer + def results(self, study, repertory, qlog=None, name=None): + self._study = study + if name is None: + name = study.name.replace(" ", "_") + + results = super(AdisTTwc, self).results(study, + repertory, + qlog, + name=name) + + return results + + _weather_files = { + "AT": ("tda0", "TDA"), + "SH": ("hsa0", "HSA"), + "GR": ("ray0", "RAY"), + "RWS": ("ven0", "VEN"), + "GT": ("tna0", "TNA"), + "GFR": ("qna0", "QNA"), + "ALB": ("alb0", "ALB"), + "SC": ("SF0", "SF"), + "CCF": ("cld0", "CLD"), + } + + _required_weather_extensions = ( + "TDA", "HSA", "RAY", "VEN", "TNA", "QNA", + ) + + def _export_temperature_initial_conditions(self, study, repertory): + initial_conditions = study.river.ic_temperature.Initial_Conditions_List + filename = "TEM.INI" + with adistt_file_open(os.path.join(repertory, filename), "w+") as f: + f.write("* Initial temperature\n") + initial = initial_conditions[0] if initial_conditions else None + temperature = initial.temperature if initial is not None else 0.0 + if temperature is None: + temperature = 0.0 + f.write(f"DEFAULT = {temperature} 0.0 0.0 0.0\n") + + edges = study.river.enable_edges() + specifications = initial._data if initial is not None else [] + for spec in specifications: + if spec.is_deleted(): + continue + reach = next((edge for edge in edges + if edge.id == spec.reach), None) + if reach is None or spec.temperature is None: + continue + reach_id = study.river.get_edge_id(reach) + 1 + f.write( + f"{spec.name} = {reach_id} {spec.start_rk} " + f"{spec.end_rk} {spec.temperature} 0.0 0.0 0.0 0.0\n" + ) + return filename + + def _export_temperature_boundary_conditions(self, study, repertory): + boundary_conditions = ( + study.river.boundary_conditions_temperature + .BCs_Temperature_List + ) + filename = "TEM.CDT" + with adistt_file_open(os.path.join(repertory, filename), "w+") as f: + for boundary_condition in boundary_conditions: + node = next((node for node in study.river._nodes + if node.id == boundary_condition.node), None) + if node is None: + continue + f.write(f"${self.get_node_name(node)}\n") + f.write("*temps |temperature (degC)\n") + f.write("*---------++++++++++\n") + for value in boundary_condition.data: + date = timestamp_to_old_pamhyr_date_adists(int(value[0])) + f.write(f"{date} {value[1]}\n") + f.write("*\n") + return filename + + def _export_weather_file(self, study, repertory, weather_parameter, + extension): + filename = f"TEM.{extension}" + path = os.path.join(repertory, filename) + mode = "a" if os.path.exists(path) else "w+" + with adistt_file_open(path, mode) as f: + reach = weather_parameter.reach + if reach is None: + return None + reach_name = ( + f"Reach_{study.river.get_edge_id(reach) + 1:>3}" + .replace(" ", "0") + ) + f.write( + f"${reach_name} {weather_parameter.begin_rk} " + f"{weather_parameter.end_rk}\n" + ) + f.write("*temps |value\n") + f.write("*---------++++++++++\n") + for value in weather_parameter.data: + date = timestamp_to_old_pamhyr_date_adists(int(value[0])) + f.write(f"{date} {value[1]}\n") + f.write("*\n") + return filename + + def _export_TEM(self, study, repertory, qlog=None, name="0"): + if qlog is not None: + qlog.put("Export TEM files") + + # Weather files are appended range by range below. Remove files from + # a previous export first so rerunning an unchanged study is stable. + for _, extension in self._weather_files.values(): + path = os.path.join(repertory, f"TEM.{extension}") + if os.path.exists(path): + os.remove(path) + + # These files are mandatory for the AdisTT master file. They are + # created even when no spatial time series is defined in the study. + for extension in self._required_weather_extensions: + adistt_file_open( + os.path.join(repertory, f"TEM.{extension}"), "w+" + ).close() + + initial_file = self._export_temperature_initial_conditions( + study, repertory + ) + boundary_file = self._export_temperature_boundary_conditions( + study, repertory + ) + + weather_files = {} + 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: + continue + _, extension = config + exported = self._export_weather_file( + study, repertory, weather_parameter, extension + ) + if exported is not None: + weather_files[extension] = exported + + filename = f"{name}.TEM" + with adistt_file_open(os.path.join(repertory, filename), "w+") as f: + defaults = study.river.weather_parameters + for type_, (parameter, _) in self._weather_files.items(): + default = defaults.default_for_type(type_) + if default is not None: + f.write(f"{parameter} = {default.value}\n") + + f.write(f"file_ini = {initial_file}\n") + f.write(f"file_cl = {boundary_file}\n") + for extension in self._required_weather_extensions: + f.write( + f"file_{extension.lower()} = TEM.{extension}\n" + ) + for _, extension in self._weather_files.values(): + if (extension in weather_files + and extension not in self._required_weather_extensions): + f.write( + f"file_{extension.lower()} = " + f"{weather_files[extension]}\n" + ) + + return [filename] + + def export_func_dict(self): + return [ + self._export_NUM, + self._export_TEM, + self._export_TEMs + ] + + def rm_previous_results(self, study, repertory, qlog): + + if "resultats" in os.listdir(repertory): + repertory_results = os.path.join(repertory, "resultats") + else: + repertory_results = os.path.normpath(repertory) + + if not os.path.isdir(repertory_results): + return + + filelist = [f for f in os.listdir(repertory_results) + if os.path.isfile(os.path.join(repertory_results, f)) + ] + files_bin_names = [f for f in filelist if f[-4:] == ".bin"] + if len(files_bin_names) < 1: + return + + for el in files_bin_names: + os.remove(os.path.join(repertory_results, el)) + + @timer + def export(self, study, repertory, mage_rep, qlog=None, name="0"): + logger.debug(f"cmd solver adisttwc : {self._cmd_solver}") + self._study = study + name = study.name.replace(" ", "_") + + # Node names must be deterministic and identical in NET and TEM.CDT. + self._nodes_cnt = 0 + self._nodes_names = {} + self._nodes_views = set() + for edge in study.river.enable_edges(): + self.get_node_name(edge.node1) + self.get_node_name(edge.node2) + + # Generate files + files = [] + + self.rm_previous_results(study, repertory, qlog) + + try: + for func in self.export_func_dict(): + files = files + func(study, repertory, qlog, name=name) + + self.export_additional_files(study, repertory, qlog, name=name) + self._export_REP(study, repertory, mage_rep, + files, qlog, name=name) + + return True + except Exception as e: + logger.error(f"Exception occurred during export: {e}") + logger.error(f"Failed to export study to {self._type}") + logger_exception(e) + return False diff --git a/src/Solver/Solvers.py b/src/Solver/Solvers.py index f6fb93b6..05e3493f 100644 --- a/src/Solver/Solvers.py +++ b/src/Solver/Solvers.py @@ -23,6 +23,7 @@ from Solver.Mage import ( Mage7, Mage8, MageFake7, ) from Solver.AdisTS import AdisTSwc +from Solver.AdisTT import AdisTTwc from Solver.RubarBE import Rubar3, RubarBE _translate = QCoreApplication.translate @@ -33,6 +34,7 @@ solver_long_name = { "mage8": "Mage v8", # "mage_fake7": "Mage fake v7", "adistswc": "Adis-TS_WC", + "adisttwc": "Adis-TT_WC", # "rubarbe": "RubarBE", "rubar3": "Rubar3", } @@ -43,6 +45,7 @@ solver_type_list = { "mage8": Mage8, # "mage_fake7": MageFake7, "adistswc": AdisTSwc, + "adisttwc": AdisTTwc, # "rubarbe": RubarBE, "rubar3": Rubar3, } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 4105fd2b..33e4c74f 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -91,6 +91,11 @@ from View.Results.CompareDialog import ( CompareScenariosWindow ) +from View.RunSolver.WindowAdisTT import ( + SelectSolverWindowAdisTT, + SolverLogWindowAdisTT +) + from View.RunSolver.WindowAdisTS import ( SelectSolverWindowAdisTS, SolverLogWindowAdisTS @@ -172,7 +177,7 @@ define_model_action = [ "action_menu_d90", "action_menu_dif", "action_menu_edit_geotiff", "action_menu_boundary_conditions_temperature", "action_menu_initial_conditions_temperature", - "action_menu_weather_parameters", + "action_menu_weather_parameters", "action_menu_run_adistt", ] action = ( @@ -291,7 +296,10 @@ 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.select_run_solver_adists, + "action_menu_run_adists": + lambda: self.select_run_solver_adists(mode="adists"), + "action_menu_run_adistt": + lambda: self.select_run_solver_adists(mode="adistt"), "action_menu_output_rk": self.open_output_rk_adists, "action_menu_config": self.open_configure, "action_menu_new": self.open_new_study, @@ -1555,7 +1563,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if run.exec(): self.run_solver(run.solver) - def select_run_solver_adists(self): + def select_run_solver_adists(self, mode="adists"): if self._study is None: return @@ -1571,9 +1579,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): parent=self ) if run.exec(): - self.run_solver_adists(run.solver, run.mage_rep) + self.run_solver_adists(run.solver, run.mage_rep, mode=mode) - def run_solver_adists(self, solver, mage_rep): + def run_solver_adists(self, solver, mage_rep, mode="adists"): if self._study is None: return diff --git a/src/View/RunSolver/WindowAdisTT.py b/src/View/RunSolver/WindowAdisTT.py new file mode 100644 index 00000000..4fc15c82 --- /dev/null +++ b/src/View/RunSolver/WindowAdisTT.py @@ -0,0 +1,505 @@ +# WindowAdisTS.py -- Pamhyr +# Copyright (C) 2023-2025 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 . + +# -*- coding: utf-8 -*- + +import os +import logging +import tempfile + +from queue import Queue +from tools import trace, timer, logger_exception + +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, QTimer, QProcess, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, + QTextEdit, +) + +from View.RunSolver.Log.Window import SolverLogFileWindow +from View.Results.ReadingResultsDialog import ReadingResultsDialog +from View.WaitingDialog import WaitingDialog + +try: + from signal import SIGTERM, SIGSTOP, SIGCONT + _signal = True +except Exception: + _signal = False + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class SelectSolverWindowAdisTT(PamhyrDialog): + _pamhyr_ui = "SelectSolverAdisTT" + _pamhyr_name = "Select solver" + + def __init__(self, study=None, config=None, + parent=None): + self._solver = None + + name = _translate("Solver", "Select solver") + super(SelectSolverWindowAdisTT, self).__init__( + title=name, + study=study, + config=config, + options=[], + parent=parent + ) + + self.setup_combobox() + self.setup_connections() + self.select_last_solver() + + def setup_combobox(self): + # solvers = self._config.solvers + # solvers mage + solvers = list(filter( + lambda x: "mage" not in x._type and "rubar" not in x._type, + self._config.solvers + )) + solvers_name = list( + map( + self._format_solver_name, + solvers + ) + ) + + solvers_mage = list(filter( + lambda x: "mage" or "rubar" in x._type.lower(), + 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) + self.find(QPushButton, "pushButton_cancel") \ + .clicked.connect(self.reject) + + def select_last_solver(self): + solvers = self._config.solvers + last = self._config.last_solver_name + + solver = list( + filter( + lambda s: s.name == last, + solvers + ) + ) + + if len(solver) != 0: + self.set_combobox_text( + "comboBox", + self._format_solver_name(solver[0]) + ) + + def _format_solver_name(self, solver): + return f"{solver.name} - ({solver._type})" + + @property + 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( + filter( + lambda s: s.name == solver_name, + self._config.solvers + ) + ) + + super(SelectSolverWindowAdisTT, self).accept() + + +class SolverLogWindowAdisTT(PamhyrWindow): + _pamhyr_ui = "SolverLogAdisTT" + _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._results_mage = None + self._mage_rep = mage_rep + + name = _translate("Solver", "Select log") + super(SolverLogWindowAdisTT, self).__init__( + title=name, + study=study, + config=config, + options=[], + parent=parent + ) + + self._workdir = "" + self._workdir_mage = "" + + 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.find(QAction, "action_results_Mage").triggered.connect( + self.resultsMage + ) + + self._alarm.timeout.connect(self.update) + + def setup_workdir(self): + scenario = self._study.status.scenario + srep = scenario.workdir() + + if self._study.filename == "": + self._workdir = os.path.join( + tempfile.TemporaryDirectory(), + srep + ) + else: + self._workdir = os.path.join( + os.path.dirname(self._study.filename), + "_PAMHYR_", + self._study.name.replace(" ", "_"), + self._solver.name.replace(" ", "_"), + srep + ) + + logger.info(f"Create workdir: '{self._workdir}'") + os.makedirs(self._workdir, exist_ok=True) + + self.setup_mage_workdir() + + def setup_mage_workdir(self): + scenario = self._study.status.scenario + srep = scenario.workdir() + + if self._study.filename == "": + self._workdir_mage = os.path.join( + tempfile.TemporaryDirectory(), + srep + ) + else: + self._workdir_mage = os.path.join( + os.path.dirname(self._study.filename), + "_PAMHYR_", + self._study.name.replace(" ", "_"), + self._mage_rep.replace(" ", "_"), + srep + ) + + logger.info(f"Create workdir: '{self._workdir_mage}'") + + 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._workdir_mage, + qlog=self._output + ) + self.update() + + return ok + + def closeEvent(self, event): + self._alarm.stop() + super(SolverLogWindowAdisTT, 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"" + msg + "" + + 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"" + + f" *** Finished with code {int_code}" + + "" + ) + + 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._workdir_mage, + 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: + dir_list = os.listdir(self._workdir) + if "resultats" in dir_list: + dir_path = os.path.join(self._workdir, "resultats") + else: + dir_path = self._workdir + + def reading_fn(): + self._results = self._solver.results( + self._study, dir_path, qlog=self._output + ) + + dlg = WaitingDialog(payload_fn=reading_fn, + title="waiting_result", + parent=self) + dlg.exec_() + + self._parent.set_results(self._solver, self._results) + self._parent.open_solver_results_adistt(self._solver, self._results) + + self._solver.has_results_loaded() + + def resultsMage(self): + mage_solver = next(filter( + lambda x: x._name == self._mage_rep, self._config.solvers + )) + if self._results_mage is None: + workdir_mage = self._workdir_mage + + def reading_fn(): + self._results_mage = mage_solver.results( + self._study, workdir_mage, qlog=self._output + ) + + dlg = ReadingResultsDialog(reading_fn=reading_fn, parent=self) + dlg.exec_() + + self._parent.set_results(mage_solver, self._results_mage) + self._parent.open_solver_results(mage_solver, self._results_mage) + + mage_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/MainWindow.ui b/src/View/ui/MainWindow.ui index 93d72519..1eaf9ad7 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -138,6 +138,7 @@ + @@ -853,6 +854,15 @@ Lateral contributions ? + + + + ressources/run.pngressources/run.png + + + Run AdisTT + + diff --git a/src/View/ui/SelectSolverAdisTT.ui b/src/View/ui/SelectSolverAdisTT.ui new file mode 100644 index 00000000..2c8867e9 --- /dev/null +++ b/src/View/ui/SelectSolverAdisTT.ui @@ -0,0 +1,79 @@ + + + Dialog + + + + 0 + 0 + 384 + 107 + + + + Dialog + + + + + + + + + AdisTS Solver: + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run + + + + ressources/run.pngressources/run.png + + + + + + + Cancel + + + + + + + + + + + + Mage Repertory: + + + + + + + + diff --git a/src/View/ui/SolverLogAdisTT.ui b/src/View/ui/SolverLogAdisTT.ui new file mode 100644 index 00000000..3677152a --- /dev/null +++ b/src/View/ui/SolverLogAdisTT.ui @@ -0,0 +1,122 @@ + + + MainWindow + + + + 0 + 0 + 1152 + 648 + + + + MainWindow + + + + + + + + Monospace + 10 + + + + + + + + + + false + + + true + + + + + + + + + 0 + 0 + 1152 + 22 + + + + + + + true + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + ressources/close.pngressources/close.png + + + Stop + + + + + + ressources/run.pngressources/run.png + + + Start + + + + + + ressources/player_pause.pngressources/player_pause.png + + + Pause + + + + + + ressources/zoom.pngressources/zoom.png + + + LogFile + + + + + results + + + + + results_Mage + + + + + + diff --git a/src/config.py b/src/config.py index d94b39ca..c1dccc65 100644 --- a/src/config.py +++ b/src/config.py @@ -37,7 +37,7 @@ logger = logging.getLogger() class Config(SQL): def __init__(self): - self._version = '0.0.7' + self._version = '0.0.8' self.filename = Config.filename() self.set_default_value() @@ -194,6 +194,27 @@ class Config(SQL): "WHERE name='default-Rubar3'" ) + if major == "0" and minor <= "2": + if int(release) < 8: + posix = os.name == 'posix' + ext = "" if posix else ".exe" + path = os.path.join("@install_dir", + "adists", f"adists{ext}") + + self.execute(f""" + INSERT OR IGNORE INTO solver VALUES ( + 'adisttwc', + 'default-AdisTT', + 'Default Pamhyr2 AdisTT version', + + '', '', '', + + '', + '{path} @args @input', + '' + ) + """) + self.execute( f"UPDATE info SET value='{self._version}' " + "WHERE key='version'" @@ -368,6 +389,14 @@ class Config(SQL): new._cmd_solver = f""""{path}{ext}" @args @input""" self._solvers.append(new) + # AdisTT + ctor = solver_type_list["adisttwc"] + new = ctor("default-AdisTT") + new._description = "Default Pamhyr2 AdisTT version" + path = os.path.join("@install_dir", "adists", "adists") + new._cmd_solver = f"\"{path}{ext}\" @args @input" + self._solvers.append(new) + # Rubar3 ctor = solver_type_list["rubar3"] new = ctor("default-Rubar3")