Compare commits

..

9 Commits

26 changed files with 1445 additions and 70 deletions

View File

@ -352,11 +352,8 @@ class Scenario(SQLSubModel):
return None return None
def workdir(self): def workdir(self):
srep = "" current = "scenario_" + str(self.id)
if self.id != 0: if self.parent is None or self.parent.id == 0:
srep = os.path.join( return current
self.parent.workdir(),
"scenario_" + str(self.id)
)
return srep return os.path.join(self.parent.workdir(), current)

View File

@ -613,17 +613,24 @@ class WeatherParameters(SQLSubModel):
new_0 = self._default_0 new_0 = self._default_0
new_1 = self._default_1 new_1 = self._default_1
def convert(column, value, default):
if value.strip() == "":
return default
return self._types[column](value.replace(",", "."))
if len(header) != 0: if len(header) != 0:
for i in [0, 1]: for i in [0, 1]:
for j in range(len(header)): for j in range(len(header)):
if self._header[i] == header[j]: if self._header[i] == header[j] and j < len(data):
if i == 0: if i == 0:
new_0 = self._types[i](data[j].replace(",", ".")) new_0 = convert(i, data[j], new_0)
else: else:
new_1 = self._types[i](data[j].replace(",", ".")) new_1 = convert(i, data[j], new_1)
else: else:
new_0 = self._types[0](data[0].replace(",", ".")) if len(data) > 0:
new_1 = self._types[1](data[1].replace(",", ".")) new_0 = convert(0, data[0], new_0)
if len(data) > 1:
new_1 = convert(1, data[1], new_1)
return Data(new_0, new_1, status=self._status) return Data(new_0, new_1, status=self._status)

View File

@ -40,6 +40,17 @@ class WeatherParametersList(PamhyrModelList):
_default_types = [ _default_types = [
"AT", "SH", "GR", "RWS", "GT", "GFR", "ALB", "SC", "CCF" "AT", "SH", "GR", "RWS", "GT", "GFR", "ALB", "SC", "CCF"
] ]
_default_values = {
"AT": 20.0,
"SH": 0.001,
"GR": 10.0,
"RWS": 1.0,
"GT": 0.0,
"GFR": 0.0,
"ALB": 0.06,
"SC": 0.15,
"CCF": 0.5,
}
def __init__(self, status=None): def __init__(self, status=None):
super(WeatherParametersList, self).__init__(status=status) super(WeatherParametersList, self).__init__(status=status)
@ -109,7 +120,11 @@ class WeatherParametersList(PamhyrModelList):
for type in self._default_types: for type in self._default_types:
if type not in existing: if type not in existing:
self._default.append( self._default.append(
WeatherParametersDefault(type=type, status=self._status) WeatherParametersDefault(
type=type,
value=self._default_values[type],
status=self._status,
)
) )
self._sort_default_values() self._sort_default_values()

View File

@ -67,6 +67,10 @@ class StudyTestCase(unittest.TestCase):
defaults = study.river.weather_parameters.default defaults = study.river.weather_parameters.default
self.assertEqual(len(defaults), 9) self.assertEqual(len(defaults), 9)
self.assertEqual(
[default.value for default in defaults],
[20.0, 0.001, 10.0, 1.0, 0.0, 0.0, 0.06, 0.15, 0.5]
)
defaults[0].value = 12.5 defaults[0].value = 12.5
dir = tempfile.mkdtemp() dir = tempfile.mkdtemp()

View File

@ -75,7 +75,8 @@ class ScriptRun(AScript):
os.path.dirname(study.filename), os.path.dirname(study.filename),
"_PAMHYR_", "_PAMHYR_",
study.name.replace(" ", "_"), study.name.replace(" ", "_"),
solver.name.replace(" ", "_") solver.name.replace(" ", "_"),
study.status.scenario.workdir(),
) )
os.makedirs(workdir, exist_ok=True) os.makedirs(workdir, exist_ok=True)
logger.info(f"Set working dir to {workdir}") logger.info(f"Set working dir to {workdir}")

View File

@ -363,13 +363,19 @@ class AdisTS(CommandLineSolver):
if qlog is not None: if qlog is not None:
qlog.put("Export REP file") qlog.put("Export REP file")
mage_path = (mage_rep if os.path.isabs(mage_rep) else os.path.join(
os.path.abspath(os.path.join(repertory, os.pardir)),
mage_rep,
))
relative_mage_path = os.path.relpath(mage_path, repertory)
# Write header # Write header
with adists_file_open( with adists_file_open(
os.path.join( os.path.join(
repertory, f"{name}.REP" repertory, f"{name}.REP"
), "w+" ), "w+"
) as f: ) as f:
path = os.path.join("..", mage_rep, name) path = os.path.join(relative_mage_path, name)
f.write(f"NET {name}.NET\n") f.write(f"NET {name}.NET\n")
f.write(f"REP {path}.REP\n") f.write(f"REP {path}.REP\n")
@ -384,9 +390,7 @@ class AdisTS(CommandLineSolver):
# fake mage_fin.ini: # fake mage_fin.ini:
path_mage = os.path.join(os.path.abspath( self._export_fake_INI(study, mage_path,
os.path.join(repertory, os.pardir)), mage_rep)
self._export_fake_INI(study, path_mage,
qlog, name=name) qlog, name=name)
# path_mage_net = os.path.join(os.path.abspath( # path_mage_net = os.path.join(os.path.abspath(

View File

@ -330,13 +330,19 @@ class AdisTT(CommandLineSolver):
if qlog is not None: if qlog is not None:
qlog.put("Export REP file") qlog.put("Export REP file")
mage_path = (mage_rep if os.path.isabs(mage_rep) else os.path.join(
os.path.abspath(os.path.join(repertory, os.pardir)),
mage_rep,
))
relative_mage_path = os.path.relpath(mage_path, repertory)
# Write header # Write header
with adistt_file_open( with adistt_file_open(
os.path.join( os.path.join(
repertory, f"{name}.REP" repertory, f"{name}.REP"
), "w+" ), "w+"
) as f: ) as f:
path = os.path.join("..", mage_rep, name) path = os.path.join(relative_mage_path, name)
f.write(f"NET {path}.NET\n") f.write(f"NET {path}.NET\n")
f.write(f"REP {path}.REP\n") f.write(f"REP {path}.REP\n")
@ -348,9 +354,7 @@ class AdisTT(CommandLineSolver):
# fake mage_fin.ini: # fake mage_fin.ini:
path_mage = os.path.join(os.path.abspath( self._export_fake_INI(study, mage_path,
os.path.join(repertory, os.pardir)), mage_rep)
self._export_fake_INI(study, path_mage,
qlog, name=name) qlog, name=name)
# path_mage_net = os.path.join(os.path.abspath( # path_mage_net = os.path.join(os.path.abspath(

View File

@ -53,12 +53,13 @@ class CheckListWindowAdisTS(PamhyrWindow):
def __init__(self, autorun: bool = True, def __init__(self, autorun: bool = True,
study=None, config=None, study=None, config=None,
solver=None, parent=None, mage_rep=None): solver=None, parent=None, mage_rep=None, mode="adists"):
trad = CheckListTranslate() trad = CheckListTranslate()
self._autorun = autorun self._autorun = autorun
self._solver = solver self._solver = solver
self._mage_rep = mage_rep self._mage_rep = mage_rep
self._mode = mode
name = trad[self._pamhyr_name] + " - " + study.name name = trad[self._pamhyr_name] + " - " + study.name
@ -73,6 +74,8 @@ class CheckListWindowAdisTS(PamhyrWindow):
# Add solver to hash computation data # Add solver to hash computation data
self._hash_data.append(self._solver) self._hash_data.append(self._solver)
self._hash_data.append(self._mage_rep)
self._hash_data.append(self._mode)
self._checker_list = ( self._checker_list = (
self._study.checkers() + self._study.checkers() +
@ -169,7 +172,7 @@ class CheckListWindowAdisTS(PamhyrWindow):
if not errors: if not errors:
self.find(QPushButton, "pushButton_ok").setEnabled(True) self.find(QPushButton, "pushButton_ok").setEnabled(True)
if self._autorun: if self._autorun:
self._parent.solver_log_adists(self._solver, self._mage_rep) self.open_solver_log()
self.end() self.end()
self.update_statusbar() self.update_statusbar()
@ -196,5 +199,11 @@ class CheckListWindowAdisTS(PamhyrWindow):
self.end() self.end()
def accept(self): def accept(self):
self._parent.solver_log_adists(self._solver, self._mage_rep) self.open_solver_log()
# self.end() # self.end()
def open_solver_log(self):
if self._mode == "adistt":
self._parent.solver_log_adistt(self._solver, self._mage_rep)
else:
self._parent.solver_log_adists(self._solver, self._mage_rep)

View File

@ -103,6 +103,7 @@ from View.RunSolver.WindowAdisTS import (
from View.CheckList.Window import CheckListWindow from View.CheckList.Window import CheckListWindow
from View.CheckList.WindowAdisTS import CheckListWindowAdisTS from View.CheckList.WindowAdisTS import CheckListWindowAdisTS
from View.Results.WindowAdisTS import ResultsWindowAdisTS from View.Results.WindowAdisTS import ResultsWindowAdisTS
from View.Results.WindowAdisTT import ResultsWindowAdisTT
from View.Results.ReadingResultsDialog import ReadingResultsDialog from View.Results.ReadingResultsDialog import ReadingResultsDialog
from View.Debug.Window import ReplWindow, TimerWindow from View.Debug.Window import ReplWindow, TimerWindow
@ -148,7 +149,7 @@ model_action = [
"action_menu_close", "action_menu_edit", "action_menu_save", "action_menu_close", "action_menu_edit", "action_menu_save",
"action_menu_save_as", "action_toolBar_close", "action_toolBar_save", "action_menu_save_as", "action_toolBar_close", "action_toolBar_save",
"action_menu_numerical_parameter", "action_menu_open_results_from_file", "action_menu_numerical_parameter", "action_menu_open_results_from_file",
"action_menu_open_results_adists", "action_menu_open_results_adists", "action_menu_open_results_adistt",
] ]
other_model_action = [ other_model_action = [
@ -297,9 +298,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_d90": self.open_d90, "action_menu_d90": self.open_d90,
"action_menu_pollutants": self.open_pollutants, "action_menu_pollutants": self.open_pollutants,
"action_menu_run_adists": "action_menu_run_adists":
lambda: self.select_run_solver_adists(mode="adists"), self.select_run_solver_adists,
"action_menu_run_adistt": "action_menu_run_adistt":
lambda: self.select_run_solver_adists(mode="adistt"), self.select_run_solver_adistt,
"action_menu_output_rk": self.open_output_rk_adists, "action_menu_output_rk": self.open_output_rk_adists,
"action_menu_config": self.open_configure, "action_menu_config": self.open_configure,
"action_menu_new": self.open_new_study, "action_menu_new": self.open_new_study,
@ -334,6 +335,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_compare_scenarios_results": "action_menu_compare_scenarios_results":
self.compare_results_scenarios, self.compare_results_scenarios,
"action_menu_open_results_adists": self.open_results_adists, "action_menu_open_results_adists": self.open_results_adists,
"action_menu_open_results_adistt":
lambda: self.open_results_adists(mode="adistt"),
# Help # Help
"action_menu_pamhyr_users_wiki": self.open_doc_user, "action_menu_pamhyr_users_wiki": self.open_doc_user,
"action_menu_pamhyr_developers_pdf": "action_menu_pamhyr_developers_pdf":
@ -1081,7 +1084,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if self.sub_window_exists( if self.sub_window_exists(
BoundaryConditionsTemperatureWindow, BoundaryConditionsTemperatureWindow,
data=[self._study, None, bclist] data=[self._study, None]
): ):
return return
@ -1101,7 +1104,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if self.sub_window_exists( if self.sub_window_exists(
InitialConditionsTemperatureWindow, InitialConditionsTemperatureWindow,
data=[self._study, None, ic_default] data=[self._study, None]
): ):
return return
@ -1570,7 +1573,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if run.exec(): if run.exec():
self.run_solver(run.solver) self.run_solver(run.solver)
def select_run_solver_adists(self, mode="adists"): def select_run_solver_adists(self):
if self._study is None: if self._study is None:
return return
@ -1586,9 +1589,27 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
parent=self parent=self
) )
if run.exec(): if run.exec():
self.run_solver_adists(run.solver, run.mage_rep, mode=mode) self.run_solver_adists(run.solver, run.mage_rep)
def run_solver_adists(self, solver, mage_rep, mode="adists"): def select_run_solver_adistt(self):
if self._study is None:
return
run = SelectSolverWindowAdisTT(
study=self._study,
config=self.conf,
parent=self
)
if run.exec():
self.run_solver_adistt(run.solver, run.mage_rep)
def run_solver_adists(self, solver, mage_rep):
self._run_solver_adis(solver, mage_rep, mode="adists")
def run_solver_adistt(self, solver, mage_rep):
self._run_solver_adis(solver, mage_rep, mode="adistt")
def _run_solver_adis(self, solver, mage_rep, mode):
if self._study is None: if self._study is None:
return return
@ -1598,7 +1619,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self._study, self._study,
self.conf, self.conf,
solver, solver,
mage_rep mage_rep,
mode,
] ]
): ):
return return
@ -1609,6 +1631,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
solver=solver, solver=solver,
parent=self, parent=self,
mage_rep=mage_rep, mage_rep=mage_rep,
mode=mode,
) )
check.show() check.show()
@ -1644,6 +1667,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
) )
sol.show() sol.show()
def solver_log_adistt(self, solver, mage_rep):
sol = SolverLogWindowAdisTT(
study=self._study,
config=self.conf,
solver=solver,
parent=self,
mage_rep=mage_rep,
)
sol.show()
def solver_log(self, solver): def solver_log(self, solver):
sol = SolverLogWindow( sol = SolverLogWindow(
study=self._study, study=self._study,
@ -1763,6 +1796,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
) )
def open_solver_results_adists(self, solver, results=None): def open_solver_results_adists(self, solver, results=None):
self._open_solver_results_adis(
solver, ResultsWindowAdisTS, results=results
)
def open_solver_results_adistt(self, solver, results=None):
self._open_solver_results_adis(
solver, ResultsWindowAdisTT, results=results
)
def _open_solver_results_adis(self, solver, window_class, results=None):
def reading_fn(): def reading_fn():
self._tmp_results = results self._tmp_results = results
@ -1804,7 +1847,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# Windows already opened # Windows already opened
if self.sub_window_exists( if self.sub_window_exists(
ResultsWindowAdisTS, window_class,
data=[ data=[
self._study, self._study,
None, # No config None, # No config
@ -1815,7 +1858,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
): ):
return return
res = ResultsWindowAdisTS( res = window_class(
study=self._study, study=self._study,
solver=solver, solver=solver,
results=results, results=results,
@ -1842,6 +1885,23 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self._solver_workdir(solver, scenario) self._solver_workdir(solver, scenario)
) )
def _solver_from_results_path(self, results_path):
solvers_by_directory = {
solver.name.replace(" ", "_"): solver
for solver in self.conf.solvers
}
path = os.path.abspath(results_path)
while True:
directory = os.path.basename(path)
if directory in solvers_by_directory:
return solvers_by_directory[directory]
parent = os.path.dirname(path)
if parent == path:
return None
path = parent
def open_last_results(self): def open_last_results(self):
if self._last_solver is None: if self._last_solver is None:
return return
@ -2106,7 +2166,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
return [result4, result5, result3] return [result4, result5, result3]
def open_results_adists(self): def open_results_adists(self, mode="adists"):
if self._study is None: if self._study is None:
return return
@ -2143,10 +2203,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
text=self._trad["mb_results_bin_not_found"] text=self._trad["mb_results_bin_not_found"]
) )
return return
path = os.path.normpath(dir_path) solver_results = self._solver_from_results_path(dir_path)
solver_name = path.split(os.sep)[-2]
solver_results = next(filter(lambda x: x.name == solver_name,
self.conf.solvers), None)
if solver_results is None: if solver_results is None:
self.msg_results_warning( self.msg_results_warning(
info=self._trad["mb_results_not_found"], info=self._trad["mb_results_not_found"],
@ -2154,16 +2211,34 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
) )
return return
expected_type = "adistt" if mode == "adistt" else "adists"
solver_type = solver_results._type.lower()
if not solver_type.startswith(expected_type):
self.msg_results_warning(
info=self._trad["mb_results_not_found"],
text=self._trad["mb_results_solver_mode_mismatch"].format(
solver=solver_results.name,
mode=expected_type.upper(),
)
)
return
solver_results_adists = solver_results.results( solver_results_adists = solver_results.results(
self._study, self._study,
repertory=dir_path, qlog=None) # self._output) repertory=dir_path, qlog=None) # self._output)
logger.info(f"Select results: {dir_path}") logger.info(f"Select results: {dir_path}")
if len(bin_list) >= 2 and ("total_sediment.bin" in bin_list): if len(bin_list) >= 2 and ("total_sediment.bin" in bin_list):
self.open_solver_results_adists( if mode == "adistt":
solver_results, self.open_solver_results_adistt(
results=solver_results_adists, solver_results,
) results=solver_results_adists,
)
else:
self.open_solver_results_adists(
solver_results,
results=solver_results_adists,
)
else: else:
self.msg_results_warning( self.msg_results_warning(
info=self._trad["mb_results_not_found"], info=self._trad["mb_results_not_found"],

View File

@ -63,6 +63,9 @@ class PlotAdis(PamhyrPlot):
self.label["C"] = _translate("Results", "Concentration") self.label["C"] = _translate("Results", "Concentration")
self.label_max["C"] = _translate("Results", "Max Concentration") self.label_max["C"] = _translate("Results", "Max Concentration")
self.label_min["C"] = _translate("Results", "Min Concentration") self.label_min["C"] = _translate("Results", "Min Concentration")
self.label["T"] = _translate("Results", "Temperature")
self.label_max["T"] = _translate("Results", "Max Temperature")
self.label_min["T"] = _translate("Results", "Min Temperature")
if self._type_pol[pol_id[0]] == -1: # Total if self._type_pol[pol_id[0]] == -1: # Total
self.label["M"] = _translate("Results", "Thickness") self.label["M"] = _translate("Results", "Thickness")
self.label_max["M"] = _translate("Results", "Max Thickness") self.label_max["M"] = _translate("Results", "Max Thickness")
@ -75,6 +78,7 @@ class PlotAdis(PamhyrPlot):
self.label_y = self._available_values_y["unit_"+self._key] self.label_y = self._available_values_y["unit_"+self._key]
self.val_id = {} self.val_id = {}
self.val_id["C"] = 0 self.val_id["C"] = 0
self.val_id["T"] = 0
self.val_id["G"] = 1 self.val_id["G"] = 1
self.val_id["M"] = 2 self.val_id["M"] = 2
self.val_id["D"] = 3 self.val_id["D"] = 3
@ -205,9 +209,12 @@ class PlotAdis_dx(PlotAdis):
self._current_timestamp, "pols" self._current_timestamp, "pols"
)[pol_id][self.val_id[self._key]], )[pol_id][self.val_id[self._key]],
reach.profiles)) reach.profiles))
label = self.label[self._key]
if name != "TEM":
label += (" "+name)
self._lines.append(self.canvas.axes.plot( self._lines.append(self.canvas.axes.plot(
x, y, x, y,
label=self.label[self._key]+" "+name, label=label,
color=self.colors[pol_id % len(self.colors)], color=self.colors[pol_id % len(self.colors)],
**self.plot_default_kargs **self.plot_default_kargs
)[0]) )[0])

View File

@ -0,0 +1,128 @@
# TableAdisTT.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import timer, trace
from PyQt5.QtGui import (
QKeySequence, QColor
)
from PyQt5.QtCore import (
Qt, QAbstractTableModel, QModelIndex,
QVariant, pyqtSlot, QCoreApplication,
)
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
QStyledItemDelegate, QLineEdit, QAbstractItemView,
QComboBox,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.Results.translate import *
logger = logging.getLogger()
_translate = QCoreApplication.translate
class TableModel(PamhyrTableModel):
def _setup_lst(self):
_river = self._data.river
self._timestamp = min(list(self._data.get("timestamps")))
if self._opt_data == "reach":
self._lst = _river.reachs
elif self._opt_data == "profile":
self._lst = _river.reach(0).profiles
elif self._opt_data == "raw_data":
self._lst = _river.reach(0).profiles
def __init__(self, type_pol, **kwargs):
self._type_pol = type_pol
super(TableModel, self).__init__(**kwargs)
def data(self, index, role=Qt.DisplayRole):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
if self._opt_data == "reach":
if self._headers[column] == "name":
v = self._lst[row].name
return str(v)
elif self._opt_data == "profile":
if self._headers[column] == "name":
v = self._lst[row].name
return str(v)
elif self._headers[column] == "rk":
v = self._lst[row].rk
return f"{v:.4f}"
elif self._opt_data == "raw_data":
p = self._lst[row]
if self._headers[column] == "name":
if p.name == "":
return f"{p.rk:.4f}"
return f"{p.name}"
tmp_list = self._data.pollutants_list.copy()
tmp_list.remove("total_sediment")
tmp_list2 = self._data.pollutants_list.copy()
for pol in tmp_list:
pol_index = tmp_list2.index(pol)
if self._headers[column] == pol + " Concentration":
v = self._lst[row].get_ts_key(
self._timestamp, "pols")[pol_index][0]
return f"{v:.4f}"
if self._headers[column] == pol + " Temperature":
v = self._lst[row].get_ts_key(
self._timestamp, "pols")[pol_index][0]
return f"{v:.4f}"
if self._headers[column] == pol + " Mass":
if self._type_pol[pol_index] == 7:
m1 = self._lst[row].get_ts_key(
self._timestamp, "pols")[pol_index][1]
m2 = self._lst[row].get_ts_key(
self._timestamp, "pols")[pol_index][2]
m3 = self._lst[row].get_ts_key(
self._timestamp, "pols")[pol_index][3]
v = m1 + m2 + m3
return f"{v:.4f}"
else:
return ""
return QVariant()
def update(self, reach):
_river = self._data.river
if self._opt_data == "reach":
self._lst = _river.reachs
elif self._opt_data == "profile" or self._opt_data == "raw_data":
self._lst = _river.reach(reach).profiles
self.layoutChanged.emit()
@property
def timestamp(self):
return self._timestamp
def set_timestamp(self, timestamp):
self._timestamp = timestamp
self.layoutChanged.emit()

View File

@ -527,14 +527,16 @@ class ResultsWindow(PamhyrWindow):
# self._additional_plot[plot].set_result(solver_id) # self._additional_plot[plot].set_result(solver_id)
if timestamp is not None: if timestamp is not None:
self.plot_xy.set_timestamp(timestamp) self.plot_xy.set_timestamp_preserve_view(timestamp)
self.plot_ac.set_timestamp(timestamp) self.plot_ac.set_timestamp_preserve_view(timestamp)
self.plot_rkc.set_timestamp(timestamp) self.plot_rkc.set_timestamp_preserve_view(timestamp)
if tab_index == 2: if tab_index == 2:
self.plot_h.set_timestamp(timestamp) self.plot_h.set_timestamp_preserve_view(timestamp)
if tab_widget.currentIndex() > 2: if tab_widget.currentIndex() > 2:
self._additional_plot[name].set_timestamp(timestamp) self._additional_plot[name].set_timestamp_preserve_view(
timestamp
)
# for plot in self._additional_plot: # for plot in self._additional_plot:
# self._additional_plot[plot].set_timestamp(timestamp) # self._additional_plot[plot].set_timestamp(timestamp)

View File

@ -532,12 +532,12 @@ class ResultsWindowAdisTS(PamhyrWindow):
self.plot_mdt.set_pollutant(self._current_pol_id) self.plot_mdt.set_pollutant(self._current_pol_id)
if timestamp is not None: if timestamp is not None:
self.plot_cdt.set_timestamp(timestamp) self.plot_cdt.set_timestamp_preserve_view(timestamp)
self.plot_cdx.set_timestamp(timestamp) self.plot_cdx.set_timestamp_preserve_view(timestamp)
self.plot_mdx.set_timestamp(timestamp) self.plot_mdx.set_timestamp_preserve_view(timestamp)
self.plot_mdt.set_timestamp(timestamp) self.plot_mdt.set_timestamp_preserve_view(timestamp)
self.plot_tdt.set_timestamp(timestamp) self.plot_tdt.set_timestamp_preserve_view(timestamp)
self.plot_tdx.set_timestamp(timestamp) self.plot_tdx.set_timestamp_preserve_view(timestamp)
self._table["raw_data"].set_timestamp(timestamp) self._table["raw_data"].set_timestamp(timestamp)

View File

@ -0,0 +1,756 @@
# WindowAdisTT.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import os
import csv
import logging
from datetime import datetime
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5.QtGui import (
QKeySequence, QIcon, QPixmap,
)
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QItemSelectionModel, QTimer,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QSlider, QLabel, QWidget, QGridLayout,
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.Results.PlotSedAdis import PlotAdis_dx, PlotAdis_dt
from View.Results.CustomPlot.Plot import CustomPlot
from View.Results.CustomExport.CustomExportAdis import (
CustomExportAdisDialog,
)
from View.Results.TableAdisTT import TableModel
from View.Results.translate import ResultsTranslate
_translate = QCoreApplication.translate
logger = logging.getLogger()
class ResultsWindowAdisTT(PamhyrWindow):
_pamhyr_ui = "ResultsAdisTT"
_pamhyr_name = "Results"
def _path_file(self, filename):
return os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"..", "ui", "ressources", filename
)
)
def __init__(self, study=None, config=None,
solver=None, results=None,
parent=None):
self._solver = solver
self._results = results
self.pollutant_label = ["None"]
pollutants_headers = self._results.pollutants_list.copy()
trad = ResultsTranslate(pollutants_headers)
name = (
trad[self._pamhyr_name] + " - "
+ study.name + " - "
+ self._solver.name
)
super(ResultsWindowAdisTT, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._hash_data.append(self._solver)
self._hash_data.append(self._results._repertory)
self._hash_data.append(self._results._name)
self._additional_plot = {}
self._current_pol_id = [1]
self._reach_id = 0
self._profile_id = 0
try:
self._timestamps = sorted(list(self._results.get("timestamps")))
self.set_type_pol()
self.setup_slider()
self.setup_table()
self.setup_plots()
self.setup_statusbar()
self.setup_connections()
self.update_table_selection_reach(self._reach_id)
self.update_table_selection_profile(self._profile_id)
except Exception as e:
logger_exception(e)
return
def set_type_pol(self):
self._type_pol = []
self._pol_id_dict = {}
tmp_list = self._results.river.reach(0).profiles
for pol_index in range(self._results.nb_pollutants):
pol_name = self._results.pollutants_list[pol_index]
if pol_name == "total_sediment":
self._type_pol.append(-1)
else:
self._type_pol.append(len(
tmp_list[0].get_ts_key(
self._timestamps[0], "pols")[pol_index])
)
self._pol_id_dict[pol_name] = pol_index
def setup_table(self):
self._table = {}
for t in ["reach", "profile", "raw_data"]:
table = self.find(QTableView, f"tableView_{t}")
self._table[t] = TableModel(
self._type_pol,
table_view=table,
table_headers=self._trad.get_dict(f"table_headers_{t}"),
data=self._results,
undo=self._undo_stack,
opt_data=t,
)
self._table[t]._timestamp = self._timestamps[
self._slider_time.value()]
def setup_slider(self):
self._slider_time = self.find(QSlider, f"horizontalSlider_time")
self._slider_time.setMaximum(len(self._timestamps) - 1)
self._slider_time.setValue(len(self._timestamps) - 1)
self._icon_start = QIcon()
self._icon_start.addPixmap(
QPixmap(self._path_file("media-playback-start.png"))
)
self._icon_pause = QIcon()
self._icon_pause.addPixmap(
QPixmap(self._path_file("media-playback-pause.png"))
)
self._button_play = self.find(QPushButton, f"playButton")
self._button_play.setIcon(self._icon_start)
self._button_back = self.find(QPushButton, f"backButton")
self._button_next = self.find(QPushButton, f"nextButton")
self._button_first = self.find(QPushButton, f"firstButton")
self._button_last = self.find(QPushButton, f"lastButton")
self._timer = QTimer(self)
def setup_plots(self):
self.canvas_cdt = MplCanvas(width=5, height=4, dpi=100)
self.canvas_cdt.setObjectName("canvas_cdt")
self.toolbar_cdt = PamhyrPlotToolbar(
self.canvas_cdt, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_cdt = self.find(
QVBoxLayout, "verticalLayout_concentration_dt")
self.plot_layout_cdt.addWidget(self.toolbar_cdt)
self.plot_layout_cdt.addWidget(self.canvas_cdt)
self.plot_cdt = PlotAdis_dt(
canvas=self.canvas_cdt,
results=self._results,
reach_id=self._reach_id,
profile_id=self._profile_id,
pol_id=self._current_pol_id,
key="T",
type_pol=self._type_pol,
trad=self._trad,
toolbar=self.toolbar_cdt
)
self.plot_cdt.draw()
self.canvas_cdx = MplCanvas(width=5, height=4, dpi=100)
self.canvas_cdx.setObjectName("canvas_cdx")
self.toolbar_cdx = PamhyrPlotToolbar(
self.canvas_cdx, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_cdx = self.find(
QVBoxLayout, "verticalLayout_concentration_dx")
self.plot_layout_cdx.addWidget(self.toolbar_cdx)
self.plot_layout_cdx.addWidget(self.canvas_cdx)
self.plot_cdx = PlotAdis_dx(
canvas=self.canvas_cdx,
results=self._results,
reach_id=self._reach_id,
profile_id=self._profile_id,
pol_id=self._current_pol_id,
key="T",
type_pol=self._type_pol,
trad=self._trad,
toolbar=self.toolbar_cdx
)
self.plot_cdx.draw()
# The AdisTT window only displays temperature plots. The code below
# belongs to the sediment/pollutant result window and its layouts are
# intentionally absent from ResultsAdisTT.ui.
return
self.canvas_mdx = MplCanvas(width=5, height=4, dpi=100)
self.canvas_mdx.setObjectName("canvas_mdx")
self.toolbar_mdx = PamhyrPlotToolbar(
self.canvas_mdx, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_mdx = self.find(
QVBoxLayout, "verticalLayout_mass_dx2")
self.plot_layout_mdx.addWidget(self.toolbar_mdx)
self.plot_layout_mdx.addWidget(self.canvas_mdx)
self.plot_mdx = PlotAdis_dx(
canvas=self.canvas_mdx,
results=self._results,
reach_id=self._reach_id,
profile_id=self._profile_id,
pol_id=self._current_pol_id,
key="M",
type_pol=self._type_pol,
trad=self._trad,
toolbar=self.toolbar_mdx
)
self.plot_mdx.draw()
self.canvas_mdt = MplCanvas(width=5, height=4, dpi=100)
self.canvas_mdt.setObjectName("canvas_mdt")
self.toolbar_mdt = PamhyrPlotToolbar(
self.canvas_mdt, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_mdt = self.find(
QVBoxLayout, "verticalLayout_mass_dt2")
self.plot_layout_mdt.addWidget(self.toolbar_mdt)
self.plot_layout_mdt.addWidget(self.canvas_mdt)
self.plot_mdt = PlotAdis_dt(
canvas=self.canvas_mdt,
results=self._results,
reach_id=self._reach_id,
profile_id=self._profile_id,
pol_id=self._current_pol_id,
key="M",
type_pol=self._type_pol,
trad=self._trad,
toolbar=self.toolbar_mdt
)
self.plot_mdt.draw()
# Thickness
self.canvas_tdx = MplCanvas(width=5, height=4, dpi=100)
self.canvas_tdx.setObjectName("canvas_tdx")
self.toolbar_tdx = PamhyrPlotToolbar(
self.canvas_tdx, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_tdx = self.find(
QVBoxLayout, "verticalLayout_tdx")
self.plot_layout_tdx.addWidget(self.toolbar_tdx)
self.plot_layout_tdx.addWidget(self.canvas_tdx)
self.plot_tdx = PlotAdis_dx(
canvas=self.canvas_tdx,
results=self._results,
reach_id=self._reach_id,
profile_id=self._profile_id,
pol_id=[0],
key="M",
type_pol=self._type_pol,
trad=self._trad,
toolbar=self.toolbar_tdx
)
self.plot_tdx.draw()
self.canvas_tdt = MplCanvas(width=5, height=4, dpi=100)
self.canvas_tdt.setObjectName("canvas_tdt")
self.toolbar_tdt = PamhyrPlotToolbar(
self.canvas_tdt, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_tdt = self.find(
QVBoxLayout, "verticalLayout_tdt")
self.plot_layout_tdt.addWidget(self.toolbar_tdt)
self.plot_layout_tdt.addWidget(self.canvas_tdt)
self.plot_tdt = PlotAdis_dt(
canvas=self.canvas_tdt,
results=self._results,
reach_id=self._reach_id,
profile_id=self._profile_id,
pol_id=[0],
key="M",
type_pol=self._type_pol,
trad=self._trad,
toolbar=self.toolbar_tdt
)
self.plot_tdt.draw()
def closeEvent(self, event):
try:
self._timer.stop()
except Exception as e:
logger_exception(e)
super(ResultsWindowAdisTT, self).closeEvent(event)
def _compute_status_label(self):
return (self.text_bief() + " | " +
self.text_profile() + " | " +
self.text_time())
def text_bief(self):
# Reach
table = self.find(QTableView, f"tableView_reach")
indexes = table.selectedIndexes()
if len(indexes) == 0:
reach = self._study.river.enable_edges()[0]
else:
reach = self._study.river.enable_edges()[indexes[0].row()]
return f"{self._trad['reach']}: {reach.name}"
def text_profile(self):
# Reach
table = self.find(QTableView, f"tableView_reach")
indexes = table.selectedIndexes()
if len(indexes) == 0:
reach = self._study.river.enable_edges()[0]
else:
reach = self._study.river.enable_edges()[indexes[0].row()]
# Profile
table = self.find(QTableView, f"tableView_profile")
indexes = table.selectedIndexes()
if len(indexes) == 0:
profile = reach.reach.profile(0)
else:
profile = reach.reach.profile(indexes[0].row())
pname = profile.name if profile.name != "" else profile.rk
return f"{self._trad['cross_section']}: {pname}"
def text_time(self):
# Timestamp
ts = self._timestamps[self._slider_time.value()]
t0 = datetime.fromtimestamp(0)
fts = str(
datetime.fromtimestamp(ts) - t0
)
fts = str(
datetime.fromtimestamp(ts) - t0
)
fts.replace("days", _translate("Results", "days"))\
.replace("day", _translate("Results", "day"))
return f"{self._trad['time']} : {fts} ({ts} sec)"
def text_pollutant(self):
# Pollutant
table = self.find(QTableView, f"tableView_pollutants")
indexes = table.selectedIndexes()
if len(indexes) != 0:
self.pollutant_label = [
self._results.pollutants_list[i.row()+1] for i in indexes
]
return (f"Pollutant: {', '.join(self.pollutant_label)}")
def setup_statusbar(self):
txt = self._compute_status_label()
self._status_label = QLabel(txt)
self.statusbar.addPermanentWidget(self._status_label)
def update_statusbar(self):
txt = self._compute_status_label()
self._status_label.setText(txt)
def setup_connections(self):
# Action
actions = {
"action_reload": self._reload,
# TODO "action_add": self._add_custom_plot,
"action_export": self.export,
}
self.find(QAction, "action_add").setEnabled(False)
for action in actions:
self.find(QAction, action).triggered.connect(
actions[action]
)
# Table and Plot
fun = {
"reach": self._set_current_reach,
"profile": self._set_current_profile,
"raw_data": self._set_current_profile_raw_data,
}
for t in ["reach", "profile"]:
table = self.find(QTableView, f"tableView_{t}")
table.selectionModel()\
.selectionChanged\
.connect(fun[t])
self._table[t].dataChanged.connect(fun[t])
self._slider_time.valueChanged.connect(self._set_current_timestamp)
self._button_play.setChecked(False)
self._button_play.clicked.connect(self._pause)
self._button_back.clicked.connect(self._back)
self._button_next.clicked.connect(self._next)
self._button_first.clicked.connect(self._first)
self._button_last.clicked.connect(self._last)
self._timer.timeout.connect(self._update_slider)
def update_table_selection_reach(self, ind):
table = self.find(QTableView, f"tableView_reach")
selectionModel = table.selectionModel()
index = table.model().index(ind, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
self._table["profile"].update(ind)
self._table["raw_data"].update(ind)
def update_table_selection_profile(self, ind):
for t in ["profile"]:
table = self.find(QTableView, f"tableView_{t}")
selectionModel = table.selectionModel()
index = table.model().index(ind, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
def update_table_selection_pol(self, ind):
for t in ["pollutants"]:
table = self.find(QTableView, f"tableView_{t}")
selectionModel = table.selectionModel()
index = table.model().index(ind[0] - 1, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
def update(self, reach_id=None, profile_id=None,
pol_id=None, timestamp=None):
if reach_id is not None:
self._reach_id = reach_id
self.plot_cdt.set_reach(reach_id)
self.plot_cdx.set_reach(reach_id)
self.update_table_selection_reach(reach_id)
self.update_table_selection_profile(0)
if profile_id is not None:
self._profile_id = profile_id
self.plot_cdt.set_profile(profile_id)
self.plot_cdx.set_profile(profile_id)
self.update_table_selection_profile(profile_id)
if pol_id is not None:
self._current_pol_id = [p+1 for p in pol_id] # rm total_sediment
self.plot_cdt.set_pollutant(self._current_pol_id)
self.plot_cdx.set_pollutant(self._current_pol_id)
if timestamp is not None:
self.plot_cdt.set_timestamp_preserve_view(timestamp)
self.plot_cdx.set_timestamp_preserve_view(timestamp)
self._table["raw_data"].set_timestamp(timestamp)
self.update_statusbar()
def _get_current_timestamp(self):
return self._timestamps[
self._slider_time.value()
]
def _set_current_reach(self):
table = self.find(QTableView, f"tableView_reach")
indexes = table.selectedIndexes()
if len(indexes) == 0:
return
self.update(reach_id=indexes[0].row())
def _set_current_profile(self):
table = self.find(QTableView, f"tableView_profile")
indexes = table.selectedIndexes()
if len(indexes) == 0:
return
self.update(profile_id=indexes[0].row())
def _set_current_pol(self):
table = self.find(QTableView, f"tableView_pollutants")
indexes = table.selectedIndexes()
rows = [i.row() for i in indexes]
if len(indexes) == 0:
return
self.update(pol_id=rows)
def _set_current_profile_raw_data(self):
return
def _set_current_timestamp(self):
timestamp = self._timestamps[self._slider_time.value()]
self.update(timestamp=timestamp)
def _reload_plots(self):
self.plot_cdt.results = self._results
self.plot_cdx.results = self._results
self.plot_cdt.draw()
self.plot_cdx.draw()
def _reload_slider(self):
self._slider_time = self.find(QSlider, f"horizontalSlider_time")
self._slider_time.setMaximum(len(self._timestamps) - 1)
self._slider_time.setValue(len(self._timestamps) - 1)
def _reload(self):
logger.debug("Reload results...")
self._results = self._results.reload()
self._timestamps = sorted(list(self._results.get("timestamps")))
self._reload_plots()
self._reload_slider()
def _copy(self):
# focus on raw data table
if self.find(QTabWidget, f"tabWidget").currentIndex() == 0:
table = self.find(QTableView, f"tableView_raw_data")
model = self._table["raw_data"]
coltext = []
for index in sorted(table.selectionModel().selectedRows()):
row = index.row()
rowtext = []
for column in range(model.columnCount()):
i = model.createIndex(row, column)
try:
rowtext.append(str(model.data(i)))
except AttributeError:
rowtext.append("")
coltext.append(rowtext)
self.copyTableIntoClipboard(coltext)
else:
logger.info("TODO: copy")
def _paste(self):
logger.info("TODO: paste")
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
# play / pause buttons
def _update_slider(self):
if self._slider_time.value() == self._slider_time.maximum():
self._slider_time.setValue(self._slider_time.minimum())
else:
self._slider_time.setValue(self._slider_time.value()+1)
def _next(self):
self._slider_time.setValue(self._slider_time.value()+1)
def _back(self):
self._slider_time.setValue(self._slider_time.value()-1)
def _first(self):
self._slider_time.setValue(self._slider_time.minimum())
def _last(self):
self._slider_time.setValue(self._slider_time.maximum())
def _pause(self):
if self._button_play.isChecked():
self._button_next.setEnabled(False)
self._button_back.setEnabled(False)
self._button_first.setEnabled(False)
self._button_last.setEnabled(False)
self._timer.start(100)
self._button_play.setIcon(self._icon_pause)
else:
self._timer.stop()
self._button_next.setEnabled(True)
self._button_back.setEnabled(True)
self._button_first.setEnabled(True)
self._button_last.setEnabled(True)
self._button_play.setIcon(self._icon_start)
def export(self):
pols = self._results.pollutants_list.copy()
dlg = CustomExportAdisDialog(pollutants=pols,
parent=self)
if dlg.exec():
x, y, pol = dlg.value
else:
return
self.file_dialog(
select_file="AnyFile",
callback=lambda f: self.export_to(f[0], x, y, pol),
default_suffix=".csv",
file_filter=["CSV (*.csv)"],
)
def export_to(self, filename, x, y, pol):
timestamps = sorted(self._results.get("timestamps"))
reach = self._results.river.reachs[self._reach_id]
first_line = [f"Study: {self._results.study.name}",
f"Reach: {reach.name}"]
if x == "rk":
timestamp = self._get_current_timestamp()
first_line.append(f"Time: {timestamp}s")
val_dict = self._export_rk(timestamp, y, pol)
elif x == "time":
profile = reach.profile(self._profile_id)
pname = profile.name if profile.name != "" else profile.rk
first_line.append(f"Profile: {pname}")
val_dict = self._export_time(self._profile_id, y, pol)
with open(filename, 'w', newline='') as csvfile:
writer = csv.writer(csvfile, delimiter=',',
quotechar='|', quoting=csv.QUOTE_MINIMAL)
dict_x = self._trad.get_dict("values_x")
header = []
writer.writerow(first_line)
for text in val_dict.keys():
header.append(text)
writer.writerow(header)
for row in range(len(val_dict[dict_x[x]])):
line = []
for var in val_dict.keys():
line.append(val_dict[var][row])
writer.writerow(line)
def _export_rk(self, timestamp, y, pol):
reach = self._results.river.reachs[self._reach_id]
dict_x = self._trad.get_dict("values_x")
dict_y = self._trad.get_dict("values_y_pol")
my_dict = {}
my_dict[dict_x["rk"]] = reach.geometry.get_rk()
val_id = {}
val_id["unit_C"] = 0
val_id["unit_M"] = 2
val_id["unit_thickness"] = 2
for unit in y:
if unit == "unit_thickness":
pol_id = 0
else:
pol_id = self._pol_id_dict[pol]
if unit == "unit_M" and self._type_pol[pol_id] == 1:
my_dict[dict_y[unit]] = [0.0]*len(reach.profiles)
else:
my_dict[dict_y[unit]] = list(map(lambda p:
p.get_ts_key(
timestamp, "pols"
)[pol_id][val_id[unit]],
reach.profiles))
return my_dict
def _export_time(self, profile, y, pol):
reach = self._results.river.reachs[self._reach_id]
profile = reach.profile(profile)
ts = list(self._results.get("timestamps"))
ts.sort()
dict_x = self._trad.get_dict("values_x")
dict_y = self._trad.get_dict("values_y_pol")
my_dict = {}
my_dict[dict_x["time"]] = ts
val_id = {}
val_id["unit_C"] = 0
val_id["unit_M"] = 2
val_id["unit_thickness"] = 2
for unit in y:
if unit == "unit_thickness":
pol_id = 0
else:
pol_id = self._pol_id_dict[pol]
if unit == "unit_M" and self._type_pol[pol_id] == 1:
my_dict[dict_y[unit]] = [0.0]*len(ts)
else:
my_dict[dict_y[unit]] = list(map(lambda data_el:
data_el[pol_id][val_id[unit]],
profile.get_key("pols")
))
return my_dict

View File

@ -121,8 +121,11 @@ class ResultsTranslate(MainTranslate):
} }
tmp = self._sub_dict["table_headers_raw_data"] tmp = self._sub_dict["table_headers_raw_data"]
for pol in self.pollutants: for pol in self.pollutants:
tmp[pol + " Concentration"] = pol + "\n Concentration" if pol == "TEM":
tmp[pol + " Mass"] = pol + "\n Mass" tmp[pol + " Temperature"] = "Temperature (°C)"
else:
tmp[pol + " Concentration"] = pol + "\n Concentration"
tmp[pol + " Mass"] = pol + "\n Mass"
else: else:
self._sub_dict["table_headers_raw_data"] = { self._sub_dict["table_headers_raw_data"] = {
"name": _translate("Results", "Profile"), "name": _translate("Results", "Profile"),
@ -146,6 +149,7 @@ class ResultsTranslate(MainTranslate):
self._sub_dict["values_y_pol"] = { self._sub_dict["values_y_pol"] = {
"unit_C": self._dict["unit_concentration"], "unit_C": self._dict["unit_concentration"],
"unit_T": self._dict["unit_temperature"],
"unit_M": self._dict["unit_mass"], "unit_M": self._dict["unit_mass"],
"unit_thickness": self._dict["unit_thickness"], "unit_thickness": self._dict["unit_thickness"],
} }

View File

@ -83,7 +83,7 @@ class SelectSolverWindowAdisTS(PamhyrDialog):
# solvers = self._config.solvers # solvers = self._config.solvers
# solvers mage # solvers mage
solvers = list(filter( solvers = list(filter(
lambda x: "mage" not in x._type and "rubar" not in x._type, lambda x: "adists" in x._type.lower(),
self._config.solvers self._config.solvers
)) ))
solvers_name = list( solvers_name = list(
@ -94,7 +94,8 @@ class SelectSolverWindowAdisTS(PamhyrDialog):
) )
solvers_mage = list(filter( solvers_mage = list(filter(
lambda x: "mage" or "rubar" in x._type.lower(), lambda x: ("mage" in x._type.lower() or
"rubar" in x._type.lower()),
self._config.solvers self._config.solvers
)) ))
solvers_mage_names = list(map(lambda x: x._name, solvers_mage)) solvers_mage_names = list(map(lambda x: x._name, solvers_mage))

View File

@ -83,7 +83,7 @@ class SelectSolverWindowAdisTT(PamhyrDialog):
# solvers = self._config.solvers # solvers = self._config.solvers
# solvers mage # solvers mage
solvers = list(filter( solvers = list(filter(
lambda x: "mage" not in x._type and "rubar" not in x._type, lambda x: "adistt" in x._type.lower(),
self._config.solvers self._config.solvers
)) ))
solvers_name = list( solvers_name = list(
@ -94,7 +94,8 @@ class SelectSolverWindowAdisTT(PamhyrDialog):
) )
solvers_mage = list(filter( solvers_mage = list(filter(
lambda x: "mage" or "rubar" in x._type.lower(), lambda x: ("mage" in x._type.lower() or
"rubar" in x._type.lower()),
self._config.solvers self._config.solvers
)) ))
solvers_mage_names = list(map(lambda x: x._name, solvers_mage)) solvers_mage_names = list(map(lambda x: x._name, solvers_mage))

View File

@ -118,6 +118,18 @@ class PamhyrPlot(APlot):
def table(self): def table(self):
return self._table return self._table
def set_timestamp_preserve_view(self, timestamp):
"""Change result frame without resetting the current zoom."""
x_limits = self.canvas.axes.get_xlim()
y_limits = self.canvas.axes.get_ylim()
try:
self.set_timestamp(timestamp)
finally:
self.canvas.axes.set_xlim(x_limits)
self.canvas.axes.set_ylim(y_limits)
self.canvas.draw_idle()
@property @property
def label_x(self): def label_x(self):
"""Plot X axes label""" """Plot X axes label"""

View File

@ -26,6 +26,7 @@ class MplCanvas(FigureCanvasQTAgg):
layout='constrained', layout='constrained',
) )
super(MplCanvas, self).__init__(fig) super(MplCanvas, self).__init__(fig)
self.setMinimumSize(1, 1)
self.axes = fig.add_subplot(111) self.axes = fig.add_subplot(111)
self.axes.format_coord = lambda x, y: ( self.axes.format_coord = lambda x, y: (
@ -38,6 +39,13 @@ class MplCanvas(FigureCanvasQTAgg):
self.axes.spines[['top', 'right']].set_color('none') self.axes.spines[['top', 'right']].set_color('none')
self.add_arrows() self.add_arrows()
def resizeEvent(self, event):
"""Ignore transient invalid sizes produced by hidden Qt tabs."""
if self.width() <= 0 or self.height() <= 0:
return
super(MplCanvas, self).resizeEvent(event)
def add_arrows(self): def add_arrows(self):
al = 8. al = 8.
arrowprops = dict( arrowprops = dict(

View File

@ -294,6 +294,11 @@ class MainTranslate(UnitTranslate):
"MainWindow", "MainWindow",
"Results folder does not correspond to a Pamhyr2 solver name" "Results folder does not correspond to a Pamhyr2 solver name"
) )
self._dict["mb_results_solver_mode_mismatch"] = _translate(
"MainWindow",
"The folder belongs to solver '{solver}',\nwhich is not an "
"{mode} solver"
)
self._dict["mb_results_bin_not_found"] = _translate( self._dict["mb_results_bin_not_found"] = _translate(
"MainWindow", "Results binary files not found" "MainWindow", "Results binary files not found"
) )

View File

@ -56,16 +56,22 @@ class EditWeatherParametersWindow(PamhyrWindow):
self._data = data self._data = data
trad = WPETranslate() trad = WPETranslate()
name = trad[self._pamhyr_name] # name = trad[self._pamhyr_name]
if self._data is not None: if self._data is not None:
edge_name = (self._data.reach.name if self._data.reach is not None edge_name = (self._data.reach.name if self._data.reach is not None
else trad['not_associated']) else trad['not_associated'])
name += ( parameter_type = trad.get_dict("long_types").get(
f" - {study.name} " + self._data.type,
f" - {self._data.name} ({self._data.id}) " + self._data.type,
# f"({trad.get_dict('long_types')[self._data.type]} - " +
f"{edge_name})"
) )
begin_rk = (self._format_rk(self._data.begin_rk)
if self._data.begin_section is not None
else trad['not_associated'])
end_rk = (self._format_rk(self._data.end_rk)
if self._data.end_section is not None
else trad['not_associated'])
name = (f"{parameter_type} - {edge_name} - "
f"PK {begin_rk}{end_rk}")
super(EditWeatherParametersWindow, self).__init__( super(EditWeatherParametersWindow, self).__init__(
title=name, title=name,
@ -81,6 +87,12 @@ class EditWeatherParametersWindow(PamhyrWindow):
self.setup_plot() self.setup_plot()
self.setup_connections() self.setup_connections()
@staticmethod
def _format_rk(rk):
if isinstance(rk, (int, float)):
return f"{rk:g}"
return str(rk)
def setup_table(self): def setup_table(self):
self._delegate_time = PamhyrExTimeDelegate( self._delegate_time = PamhyrExTimeDelegate(
data=self._data, data=self._data,

View File

@ -67,6 +67,16 @@ class WeatherParametersTableDefaultModel(PamhyrTableModel):
return QVariant() return QVariant()
def headerData(self, section, orientation, role):
if (orientation == Qt.Orientation.Horizontal and
self._headers[section] == "value" and
role == Qt.ItemDataRole.ToolTipRole):
return self._trad["default_values_tooltip"]
return super(WeatherParametersTableDefaultModel, self).headerData(
section, orientation, role
)
def setData(self, index, value, role=Qt.EditRole): def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole: if not index.isValid() or role != Qt.EditRole:
return False return False

View File

@ -128,9 +128,19 @@ class WeatherParametersWindow(PamhyrWindow):
toolBar.addAction(self.action_delete) toolBar.addAction(self.action_delete)
toolBar.addAction(self.action_edit) toolBar.addAction(self.action_edit)
self.time_series_info = QLabel(
self._trad["time_series_unavailable"]
)
self.time_series_info.setWordWrap(True)
layout.addWidget(self.time_series_info)
layout.addWidget(self.table_spec_title) layout.addWidget(self.table_spec_title)
layout.addWidget(self.table_spec) layout.addWidget(self.table_spec)
selected_type = self._wp_acronyms[
self._selected_default_row_index
]
self.update_time_series_info(selected_type)
def setup_table(self): def setup_table(self):
self.table_default = self.find(QTableView, f"tableView") self.table_default = self.find(QTableView, f"tableView")
@ -249,6 +259,7 @@ class WeatherParametersWindow(PamhyrWindow):
self.enable_actions(tab_enabled) self.enable_actions(tab_enabled)
self.update_table_spec_title(enabled=tab_enabled) self.update_table_spec_title(enabled=tab_enabled)
self.update_time_series_info(type_selected)
self._table_spec.update_tab_spec( self._table_spec.update_tab_spec(
type=type_selected, enabled=tab_enabled type=type_selected, enabled=tab_enabled
@ -256,6 +267,12 @@ class WeatherParametersWindow(PamhyrWindow):
return self._selected_default_row_index return self._selected_default_row_index
def update_time_series_info(self, parameter_type):
if hasattr(self, "time_series_info"):
self.time_series_info.setVisible(
parameter_type in {"ALB", "SC", "CCF"}
)
def enable_actions(self, enabled): def enable_actions(self, enabled):
if hasattr(self, "action_add"): if hasattr(self, "action_add"):
self.action_add.setEnabled(enabled) self.action_add.setEnabled(enabled)

View File

@ -29,12 +29,24 @@ class WeatherParametersTranslate(MainTranslate):
self._dict["Weather parameters"] = _translate( self._dict["Weather parameters"] = _translate(
"WeatherParameters", "Weather parameters") "WeatherParameters", "Weather parameters")
self._dict["time_series_unavailable"] = _translate(
"WeatherParameters",
"Time series are not available for this parameter type."
)
self._dict["default_values"] = _translate(
"WeatherParameters", "Default values"
)
self._dict["default_values_tooltip"] = _translate(
"WeatherParameters",
"These values are applied to sections that have not been "
"defined using time series."
)
self._dict["rk"] = self._dict["unit_rk"] self._dict["rk"] = self._dict["unit_rk"]
self._sub_dict["table_headers"] = { self._sub_dict["table_headers"] = {
"name": self._dict["name"], "name": self._dict["name"],
"value": self._dict["value"] "value": self._dict["default_values"]
} }
self._sub_dict["table_headers_spec"] = { self._sub_dict["table_headers_spec"] = {
@ -62,3 +74,13 @@ class WeatherParametersTranslate(MainTranslate):
"cloud_cover_fraction": _translate("WeatherParameters", "cloud_cover_fraction": _translate("WeatherParameters",
"Cloud Cover Fraction"), "Cloud Cover Fraction"),
} }
weather_parameters = self._sub_dict["weather_parameters"]
self._sub_dict["long_types"] = {
"AT": weather_parameters["air_temperature"],
"SH": weather_parameters["specific_humidity"],
"GR": weather_parameters["global_radiation"],
"RWS": weather_parameters["reference_wind_speed"],
"GT": weather_parameters["surface_temperature"],
"GFR": weather_parameters["surface_flow_rate"]
}

View File

@ -162,6 +162,7 @@
<addaction name="action_menu_open_results_from_file"/> <addaction name="action_menu_open_results_from_file"/>
<addaction name="action_menu_compare_scenarios_results"/> <addaction name="action_menu_compare_scenarios_results"/>
<addaction name="action_menu_open_results_adists"/> <addaction name="action_menu_open_results_adists"/>
<addaction name="action_menu_open_results_adistt"/>
</widget> </widget>
<widget class="QMenu" name="menu_help"> <widget class="QMenu" name="menu_help">
<property name="title"> <property name="title">
@ -863,6 +864,14 @@
<string>Run AdisTT</string> <string>Run AdisTT</string>
</property> </property>
</action> </action>
<action name="action_menu_open_results_adistt">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Open results temperature</string>
</property>
</action>
</widget> </widget>
<resources/> <resources/>
<connections> <connections>

View File

@ -0,0 +1,265 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1410</width>
<height>821</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="locale">
<locale language="English" country="Europe"/>
</property>
<widget class="QWidget" name="centralwidget">
<property name="minimumSize">
<size>
<width>874</width>
<height>0</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QSplitter" name="splitter_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QSplitter" name="splitter_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QTableView" name="tableView_reach"/>
<widget class="QWidget" name="layoutWidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTableView" name="tableView_profile"/>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QGridLayout" name="gridLayout_2">
<item row="4" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="firstButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-skip-backward.png</normaloff>ressources/media-skip-backward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="backButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-seek-backward.png</normaloff>ressources/media-seek-backward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="playButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/player_play.png</normaloff>
<normalon>ressources/player_pause.png</normalon>ressources/player_play.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="nextButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-seek-forward.png</normaloff>ressources/media-seek-forward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="lastButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-skip-forward.png</normaloff>ressources/media-skip-forward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="horizontalSlider_time">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="enabled">
<bool>true</bool>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>Raw data</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4"/>
</item>
<item>
<widget class="QTableView" name="tableView_raw_data"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="Temperature">
<property name="enabled">
<bool>true</bool>
</property>
<attribute name="title">
<string>Temperature</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QTabWidget" name="tabWidget_c">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_cdx">
<attribute name="title">
<string>Temperature dx</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_17">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_concentration_dx"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_cdt">
<attribute name="title">
<string>Temperature dt</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_18">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_concentration_dt"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1410</width>
<height>20</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_export"/>
<addaction name="action_reload"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add custom visualization</string>
</property>
</action>
<action name="action_reload">
<property name="icon">
<iconset>
<normaloff>ressources/reload.png</normaloff>ressources/reload.png</iconset>
</property>
<property name="text">
<string>Reload</string>
</property>
</action>
<action name="action_export">
<property name="icon">
<iconset>
<normaloff>ressources/export.png</normaloff>ressources/export.png</iconset>
</property>
<property name="text">
<string>Export</string>
</property>
<property name="toolTip">
<string>Export raw data</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
</action>
</widget>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="buttonGroup"/>
<buttongroup name="buttonGroup_2"/>
</buttongroups>
</ui>