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 @@
+
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
+
+
+
+
+
+
+
+
+
+ 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")