Compare commits

...
93 Commits
Author SHA1 Message Date
Dylan Jeannin da28bae6c5 Fix pep8 2026-08-24 17:08:37 +02:00
Dylan Jeannin 944f99f14b Disable profiles: security to prevent the disable of a profile associated with data (initial conditions, structures, lateral conditions, weather params, init. cond. temperature) 2026-08-24 17:01:16 +02:00
Dylan Jeannin 8978cc3482 Disabled profiles: the reach split is impossible on a disabled profile to avoid any problem 2026-08-24 15:37:03 +02:00
Dylan Jeannin f08d4b1a37 Disable profiles: results only take into account enabled profiles to display results for Mage 2026-08-24 14:17:39 +02:00
Dylan Jeannin 824fbe506e Profiles: disabled profiles now affects results for mage, AdisTS and AdisTT 2026-08-21 17:30:38 +02:00
Dylan Jeannin deb84ca331 Profiles: can now disable some profiles, they still appears in the list, but they are marked as disabled 2026-08-21 16:35:40 +02:00
Dylan Jeannin 5b778af745 Profiles: fix misconnexion between add/remove buttons and tab in actual profile window 2026-08-21 11:30:26 +02:00
JeaDylan 776d9ab610 Profiles: keep security to not open the same profile twice 2026-08-20 23:22:06 +02:00
JeaDylan 587e5412d5 Profiles: new functionnality, navigate between differents profiles (next and previous one) directly in the edit profile window 2026-08-20 22:17:49 +02:00
JeaDylan ef5430ae2c Mesh: give choice to export only handwritten points in geometry, or all points with interpolated ones 2026-08-20 00:01:55 +02:00
JeaDylan 059d7203a9 Mesh: activate button in toolbar only when a study is selected 2026-08-19 23:42:17 +02:00
Dylan Jeannin c170dbc46c Mesh tab: modification of the mesh layout to better integrate it into the modeling process 2026-08-18 16:56:34 +02:00
Dylan Jeannin 890dbd9d32 Network: correct a crashing bug that happened when we tried to right click on the canvas of network after selecting a first point to create an edge 2026-08-18 13:40:02 +02:00
Dylan Jeannin f17b395c4c Bedload: Dynamic display for the riverbed over time in results window + correction of import of .ST files in geometry tab when we import a bedload file 2026-08-17 17:34:32 +02:00
Dylan Jeannin cdbcb2b539 Merge branch 'split_reach' into dev_dylan 2026-08-11 11:17:16 +02:00
Dylan Jeannin 4cad251e57 Friction: switch coefficients types or edit the list of application coefficients now send a notify the Friction window to update the list and plot, even if the window is opened already 2026-07-28 10:08:44 +02:00
Dylan Jeannin 48e9c14525 Strickler: dont display duplicated strickler from application database into study data tab 2026-07-27 17:13:16 +02:00
Dylan Jeannin db62af6f71 Friction: application's strickler now appears in combobox to pick a strickler, and create a copy of it in the study 2026-07-27 16:28:14 +02:00
Dylan Jeannin b468a1eb70 Friction: application's stricklers are now read-only in the friction window 2026-07-27 16:04:36 +02:00
Dylan Jeannin b9ab202e3f Application strickler: check that the name is not already in use before committing the change to the talbe, as the name is used as database identifier 2026-07-27 15:38:06 +02:00
Dylan Jeannin 129dde1c57 Strickler: quickfix to correctly save Strickler's name into app model 2026-07-27 15:12:09 +02:00
Dylan Jeannin ff2d3926b1 Strickler: can now add general stricklers relative to application and not only study 2026-07-27 14:41:03 +02:00
Dylan Jeannin fefc6ebc4f Manning: Add parameter to setup manning coef instead of strickler, and switch the value on the view only, but keeps strickler value in the model for the solvers 2026-07-27 13:22:32 +02:00
Dylan Jeannin 0076faf03a Temperature: propagate TEM when splitting reach 2026-07-27 09:31:29 +02:00
Dylan Jeannin bbdd18e8ef LateralContributions: propagate LAT when splitting reach + display only enabled reach in combobox 2026-07-24 15:35:12 +02:00
Dylan Jeannin 87d31c134e Merge branch 'dev_dylan' into split_reach 2026-07-24 14:40:29 +02:00
Dylan Jeannin ce04f5fd1e PEP8 2026-07-24 14:39:39 +02:00
Dylan Jeannin 435ccd1f2d Merge branch 'temperature_test' into dev_dylan 2026-07-24 14:34:50 +02:00
Dylan Jeannin 74c28d40a6 Merge branch 'master' into dev_dylan 2026-07-24 14:33:01 +02:00
Dylan Jeannin 7b2c276643 Mage: reset node name registry for each export + display only Mage/Rubar solver in hydraulic solvers list 2026-07-24 13:53:30 +02:00
Dylan Jeannin 0e1264ca4f Merge branch 'master' into temperature_test 2026-07-24 13:27:07 +02:00
Dylan Jeannin defcb3af84 BoundaryConditionsData: fix multidelete + relative undo/redo 2026-07-24 13:17:55 +02:00
Dylan Jeannin 5cf0a0ec76 Mesh: correction to make meshing thread-safe with tab display 2026-07-24 11:20:33 +02:00
Dylan Jeannin 1c44590def Meshing: unsigned distance to be able to mesh reach with start_rk > end_rk 2026-07-24 10:44:04 +02:00
Dylan Jeannin 53732744f4 Meshing: unsigned distance to be able to mesh reach with start_rk > end_rk 2026-07-24 10:40:52 +02:00
Dylan Jeannin 5e95c286a9 InitialConditionsAdisTS: split correctly reach and InitialConditionsAdisTS + display only enabled reach in tab 2026-07-23 15:57:50 +02:00
Dylan Jeannin 6175e8a03e Reach sediments: avoid crash when trying to open Edit_reach_sediment_layer without any reach selected 2026-07-23 15:39:07 +02:00
Dylan Jeannin 8e3d784e92 HydraulicStructures: split correctly reach and Hydraulic Structures + display only enabled reach in tab 2026-07-23 13:54:25 +02:00
Dylan Jeannin 698e300690 LateralContrib: split correctly reach and lateralContrib + display only enabled reach in tab 2026-07-23 11:54:49 +02:00
Dylan Jeannin e3fbe59585 SQLite debug: files with space in name can now be open 2026-07-23 10:12:01 +02:00
Dylan Jeannin 0c7fecb123 InitialCond: data copy with correct reach affectation after split 2026-07-23 10:10:56 +02:00
Dylan Jeannin 78eed7af13 SolverParams: quickfix for solverparams copy on scenario creation 2026-07-22 15:40:32 +02:00
Dylan Jeannin 6f5743a030 Merge dictionary 2026-07-22 13:31:59 +02:00
Dylan Jeannin d027946fb2 OutputRK: delete all references to this useless feature 2026-07-22 12:54:50 +02:00
Dylan Jeannin 37ea83d42a Lang: new words in french dictionnary 2026-07-22 11:26:30 +02:00
Dylan Jeannin ae39f33131 PEP8 2026-07-21 17:18:59 +02:00
Dylan Jeannin 291f80e904 Results AdisTT: zoom not reset when changing timestamp 2026-07-21 17:16:35 +02:00
Dylan Jeannin 61baaeeffb Scenario structure: adapting better structure for results with scenario mode + default data for weather parameters 2026-07-21 16:58:30 +02:00
Dylan Jeannin 68aabb343d WeatherParameters: informations for default values + no timeseries for ALB, SF and CLD 2026-07-21 15:54:06 +02:00
Dylan Jeannin f2d6e07931 WeatherParameters: better window names to be more clear for user 2026-07-21 15:26:38 +02:00
Dylan Jeannin 123259c2ef Duplicate window: avoid opening duplicate windows for boundary and initial conditions ADISTT 2026-07-21 15:06:44 +02:00
Dylan Jeannin 816d7663b5 AdisTT: new button on interface to open AdisTT results from file 2026-07-21 14:57:30 +02:00
Dylan Jeannin 9b9202c739 AdisTT: new results window, adapted from adists results 2026-07-21 14:00:59 +02:00
Dylan Jeannin 57f82415bd AdisTT: Avoid crash when pasting something unexpected in time series 2026-07-21 11:09:46 +02:00
Dylan Jeannin 36c93a0c93 AdisTT: cleaning code, delete useless functions 2026-07-20 16:22:49 +02:00
Dylan Jeannin 3cd1dbab49 AdisTT: default parameters update to fit better 2026-07-20 16:10:39 +02:00
Dylan Jeannin 4e3da95b1d Plot: Update plots after changing each data in tab 2026-07-20 15:51:05 +02:00
Dylan Jeannin 0d0c4f913e Merge branch 'dev_dylan' into temperature_test 2026-07-20 15:16:41 +02:00
Dylan Jeannin 600f86aeb4 Friction: combobox in tab for rk 2026-07-20 14:48:19 +02:00
Dylan Jeannin c9bb222bff WeatherParameters: Debug crash pamhyr when select a reach without geometry 2026-07-20 13:47:19 +02:00
Dylan Jeannin 456559ffe2 AdisTS & AdisTT: deleted data no more exported in solver files 2026-07-20 12:34:49 +02:00
Dylan Jeannin 6d58c09104 AdisTT: short correction on paste function in edit windows for WeatherParam 2026-07-17 17:47:33 +02:00
Dylan Jeannin bc87d6add1 AdisTT: corrections on writing TEM files 2026-07-17 17:21:52 +02:00
Dylan Jeannin 3fe49b70d0 Merge branch 'monopk-ic-adistt' into temperature_test 2026-07-17 17:04:37 +02:00
Dylan Jeannin 5287f30636 AdisTT: Handling cases where parameters do not have time series 2026-07-17 15:05:21 +02:00
Dylan Jeannin 67433ae325 AdisTS: delete duplicate function 2026-07-17 13:51:18 +02:00
Dylan Jeannin d27ce9d1d8 AdisTT: rewriting reach names in parser + copy the net folder from mage 2026-07-17 13:45:40 +02:00
Dylan Jeannin c8ea9bd774 Merge branch 'temperature_test' into monopk-ic-adistt 2026-07-17 10:12:30 +02:00
Dylan Jeannin 885c8ea502 Merge branch 'master' into temperature_test 2026-07-17 10:09:52 +02:00
Dylan Jeannin c8b32f9877 Merge branch 'master' into temperature_test 2026-07-17 10:05:42 +02:00
Dylan Jeannin cf61fd8d09 IC_temp: temperature set on a single RK rather than a segment 2026-07-17 10:03:46 +02:00
Dylan Jeannin cac20d4f24 AdisTT: user-friendly fixes on interface 2026-07-17 09:31:03 +02:00
Dylan Jeannin 965443a8f9 AdisTT: Managing tab of time series when it's disabled 2026-07-16 16:34:37 +02:00
Dylan Jeannin 02be084faf AdisTT: Fake 0.0 in files to match AdisTS pollutants description 2026-07-16 12:56:46 +02:00
Dylan Jeannin bb2e83b6ce AdisTT: Stable version of files writer for adisTT solver 2026-07-16 10:44:01 +02:00
parouby c09c62f077 Merge: Results reading optimisation. 2026-07-10 10:18:58 +02:00
parouby 7a2e93a995 Results: Custom plot: Minor change. 2026-07-10 09:16:05 +02:00
JeaDylan 4a94c946d1 WeatherParameters: add name of parameter in spec tab 2026-07-09 18:40:27 +02:00
JeaDylan 2dca5d2905 WeatherParameters: automatic selection when we open the window + monoselection in default tab 2026-07-09 15:44:32 +02:00
JeaDylan b5ae352af5 WeatherParameters: deleted variable not useful in default params 2026-07-09 13:47:23 +02:00
JeaDylan 868215705c WeatherParameters: New data in model+db to setup default parameters 2026-07-09 11:43:16 +02:00
Dylan Jeannin 6d16d9dff4 Fix PEP8 2026-07-06 15:45:47 +02:00
Dylan Jeannin b60ab881b1 Weather parameters: time series for each parameter, add/del/sort functions working 2026-07-06 15:37:45 +02:00
Dylan Jeannin f2d76a0d67 WeatherParameters: button connections in another function to clear code 2026-07-06 12:31:01 +02:00
Dylan Jeannin 5dcc82d81e WeatherParameters: Correct add and implement del function on tab_spec 2026-07-06 11:47:37 +02:00
Dylan Jeannin a01e47f205 Weather Parameters: Display relative parameters in spec tab 2026-07-06 10:26:49 +02:00
Dylan Jeannin c87563b7ed Cleaning some code 2026-07-06 09:57:05 +02:00
Dylan Jeannin 246b411114 Weather params: Dual tab for parameters with add functionnality 2026-07-03 17:07:34 +02:00
Dylan Jeannin cfbc2d96a1 Mage: Fix parsing for .LAT files 2026-07-03 13:07:10 +02:00
JeaDylan b2ca1c2ab7 WeatherParameters: Add first parameters view + model 2026-06-30 08:49:35 +02:00
Dylan Jeannin 2cbfaec0c2 Boundary Conditions: interface for temperature's boundary conditions 2026-06-26 19:02:49 +02:00
Dylan Jeannin 95dd648b90 Temperature: Interface for temperature's initial conditions 2026-06-26 17:36:42 +02:00
Dylan Jeannin 333a8a0970 Temperature: New tab in UI 2026-06-25 17:13:02 +02:00
137 changed files with 12394 additions and 1451 deletions

No files matched your search

+5 -2
View File
@@ -229,7 +229,11 @@ for each reach"
gls = []
for edge in edges:
comp, incomp = edge.reach.compute_guidelines()
profiles = edge.reach.enabled_profiles
comp, incomp = edge.reach.compute_guidelines(
profiles=profiles,
update_cache=False
)
if len(incomp) != 0:
self._status = STATUS.WARNING
self._summary = "incomplete_guideline"
@@ -237,7 +241,6 @@ for each reach"
gls.append(comp)
profiles = edge.reach.profiles
for profile in profiles:
if not profile.has_standard_named_points():
self._status = STATUS.WARNING
+7 -3
View File
@@ -87,7 +87,7 @@ class StudyGeometryChecker(AbstractModelChecker):
return False
for edge in edges:
if len(edge.reach.profiles) < 2:
if len(edge.reach.enabled_profiles) < 2:
summary = "no_geometry_defined"
status = STATUS.ERROR
ok = False
@@ -132,8 +132,12 @@ class StudyInitialConditionsChecker(AbstractModelChecker):
return ok
ic = river.initial_conditions[reach]
len_ic = len(ic)
len_reach = len(reach)
enabled_profiles = set(reach.enabled_profiles)
len_ic = sum(
data["section"] in enabled_profiles
for data in ic.data
)
len_reach = len(enabled_profiles)
if len_ic < len_reach:
self._summary = "initial_condition_missing_profile"
+13
View File
@@ -57,6 +57,13 @@ class InternalMeshing(AMeshingTool):
# we never modify original profiles, we work on the copies
m_profiles = self.st_to_m(profiles, guide_list)
# Adapt the spacing to the kilometre-point direction. The value from
# the dialog is a distance, so only its magnitude is significant.
step = self.signed_step(
step, m_profiles[0].rk, m_profiles[-1].rk
)
new_profiles = self.interpolate_transversal_step(m_profiles, step)
for new_profiles2 in new_profiles:
@@ -65,6 +72,12 @@ class InternalMeshing(AMeshingTool):
return new_profiles
@staticmethod
def signed_step(step, begin_rk, end_rk):
if end_rk < begin_rk:
return -abs(step)
return abs(step)
def st_to_m(self, profiles, guide_list):
guide_list = ["un"] + guide_list + ["np"]
max_values = [0] * (len(guide_list) - 1)
@@ -0,0 +1,478 @@
# BoundaryConditionsTemperature.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class Data(SQLSubModel):
_sub_classes = []
def __init__(self,
data0, data1,
id: int = -1,
types=[float, float],
status=None,
owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._types = types
self._data = [data0, data1]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_data_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
bca INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(bca)
REFERENCES boundary_condition_temperature(pamhyr_id)
)
""")
return True
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and minor == "0":
if int(release) < 11:
cls._db_create(execute)
created = True
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 7)):
if not cls.is_table_exists(execute,
"boundary_condition_data_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
bca = data["bca"]
table = execute(
"SELECT pamhyr_id, deleted, data0, data1, scenario FROM " +
"boundary_condition_data_temperature " +
f"WHERE scenario = {scenario.id} " +
# f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
f"AND bca = {bca.id}"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
data0 = next(it)
data1 = next(it)
owner_scenario = next(it)
bc = cls(
data0, data1,
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
bca = data["bca"]
data0 = self._db_format(self._data[0])
data1 = self._db_format(self._data[1])
execute(
"INSERT INTO " +
"boundary_condition_data_temperature(" +
"pamhyr_id, deleted, data0, data1, bca, scenario) " +
f"VALUES ({self.id}, {self._db_format(self.is_deleted())}, " +
f"'{data0}', {data1}, {bca.id}, " +
f"{self._status.scenario_id})"
)
return True
def __getitem__(self, key):
return self._types[key](self._data[key])
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
class BoundaryConditionTemperature(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1, status=None, owner_scenario=-1):
super(BoundaryConditionTemperature, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = ""
self._node = None
self._data = []
self._header = []
self._types = [self.time_convert, float]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
node INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and (int(minor) < 2 or (
int(minor) == 2 and int(release) < 7)):
if not cls.is_table_exists(execute,
"boundary_condition_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
nodes = data['nodes']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
sql = (
"SELECT pamhyr_id, deleted, name, node, scenario "
"FROM boundary_condition_temperature " +
f"WHERE scenario = {scenario.id} "
)
table = execute(sql)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
bc_name = next(it)
node = next(it)
owner_scenario = next(it)
bc = cls(
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
bc.name = bc_name
bc.node = None
if node != -1:
tmp = next(
filter(
lambda n: n.id == node, nodes
),
None
)
if tmp is not None:
bc.node = tmp.id
else:
bc.node = -1
data["bca"] = bc
bc._data = Data._db_load(execute, data=data)
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM boundary_condition_temperature " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE bca = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
node = -1
if self._node is not None:
node = self._node
execute(
"INSERT INTO " +
"boundary_condition_temperature(" +
"pamhyr_id, deleted, name, " +
"node, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', {node}, " +
f"{self._status.scenario_id}" +
")"
)
data["bca"] = self
ind = 0
for d in self._data:
data["ind"] = ind
d._db_save(execute, data)
ind += 1
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for d in self._data:
d._data_traversal(predicate, modifier, data)
def __len__(self):
return len(
list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
)
@classmethod
def time_convert(cls, data):
if type(data) is str:
if data.count("-") == 2:
return date_iso_to_timestamp(data)
if data.count("/") == 2:
return date_dmy_to_timestamp(data)
if data.count(":") == 3:
return old_pamhyr_date_to_timestamp(data)
if data.count(":") == 2:
return old_pamhyr_date_to_timestamp("00:" + data)
if data.count(".") == 1:
return round(float(data))
return int(data)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def node(self):
return self._node
@node.setter
def node(self, node):
self._node = node
self.modified()
@property
def header(self):
return self._header.copy()
@header.setter
def header(self, header):
self._header = header
self.modified()
@property
def data(self):
return list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property
def _default_0(self):
return self._types[0](0)
@property
def _default_1(self):
return self._types[1](0.0)
def new_from_data(self, header, data):
new_0 = self._default_0
new_1 = self._default_1
if len(header) != 0:
for i in [0, 1]:
for j in range(len(header)):
if self._header[i] == header[j]:
if i == 0:
new_0 = self._types[i](data[j].replace(",", "."))
else:
new_1 = self._types[i](data[j].replace(",", "."))
else:
new_0 = self._types[0](data[0].replace(",", "."))
new_1 = self._types[1](data[1].replace(",", "."))
return (new_0, new_1)
def add(self, index: int):
value = Data(
self._default_0, self._default_1,
status=self._status
)
self._data.insert(index, value)
self.modified()
return value
def insert(self, index: int, value):
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self.data)
)
)
)
self.modified()
def sort(self, _reverse=False, key=None):
if key is None:
self._data.sort(reverse=_reverse)
else:
self._data.sort(reverse=_reverse, key=key)
self.modified()
def index(self, bc):
self._data.index(bc)
def get_i(self, index):
return self.data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index, column, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)
@@ -1,5 +1,5 @@
# OutputRKListAdists.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
# BoundaryConditionsTemperatureList.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
@@ -16,44 +16,56 @@
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.OutputRKAdists.OutputRKAdists import OutputRKAdists
from Model.Except import NotImplementedMethodeError
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
class OutputRKAdistsList(PamhyrModelList):
_sub_classes = [OutputRKAdists]
class BoundaryConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
BoundaryConditionTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"])
new = cls(status=data['status'])
new._lst = OutputRKAdists._db_load(execute, data)
if data is None:
data = {}
new._lst = BoundaryConditionTemperature._db_load(execute, data)
return new
def _db_save(self, execute, data=None):
ok = True
# Delete previous data
execute(
"DELETE FROM output_rk_adists " +
"DELETE FROM boundary_condition_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
for sl in self._lst:
ok &= sl._db_save(execute, data)
if data is None:
data = {}
return ok
for bca in self._lst:
bca._db_save(execute, data=data)
@property
def OutputRK_List(self):
return self.lst
return True
def new(self, lst, index):
n = OutputRKAdists(status=self._status)
def new(self, index):
n = BoundaryConditionTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
@property
def BCs_Temperature_List(self):
return self.lst
+37 -8
View File
@@ -58,7 +58,8 @@ class ProfileXYZ(Profile, SQLSubModel):
num=0,
nb_point: int = 0,
code1: int = 0, code2: int = 0,
status=None, owner_scenario=-1):
status=None, owner_scenario=-1,
enabled=True):
"""ProfileXYZ constructor
Args:
@@ -90,6 +91,7 @@ class ProfileXYZ(Profile, SQLSubModel):
self.time_l = 0.0
self._station = []
self.station_up_to_date = False
self._enabled = bool(enabled)
self._get_water_limits_cache = {}
self._get_water_limits_ac_cache = {}
@@ -108,6 +110,7 @@ class ProfileXYZ(Profile, SQLSubModel):
code1 INTEGER NOT NULL,
code2 INTEGER NOT NULL,
sl INTEGER,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
@@ -152,6 +155,13 @@ class ProfileXYZ(Profile, SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and int(minor) <= 2:
if int(release) < 9:
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE"
)
return cls._update_submodel(execute, version, data)
@classmethod
@@ -237,7 +247,7 @@ class ProfileXYZ(Profile, SQLSubModel):
table = execute(
"SELECT pamhyr_id, ind, deleted, name, rk, num, " +
"code1, code2, sl, scenario " +
"code1, code2, sl, scenario, enabled " +
"FROM geometry_profileXYZ " +
f"WHERE reach = {reach.id} " +
f"AND scenario = {scenario.id} " +
@@ -258,6 +268,7 @@ class ProfileXYZ(Profile, SQLSubModel):
code2 = next(it)
sl = next(it)
owner_scenario = next(it)
enabled = (next(it) == 1)
profile = cls(
id=pid, ind=ind, num=num,
@@ -265,7 +276,8 @@ class ProfileXYZ(Profile, SQLSubModel):
code1=code1, code2=code2,
reach=reach,
status=status,
owner_scenario=owner_scenario
owner_scenario=owner_scenario,
enabled=enabled
)
if deleted:
profile.set_as_deleted()
@@ -318,12 +330,13 @@ class ProfileXYZ(Profile, SQLSubModel):
execute(
"INSERT OR REPLACE INTO " +
"geometry_profileXYZ(pamhyr_id, deleted, ind, name, reach, " +
"rk, num, code1, code2, sl, scenario) " +
"rk, num, code1, code2, sl, scenario, enabled) " +
"VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(self._name)}', " +
f"{self.reach.pamhyr_id}, {self.rk}, {self.num}, " +
f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}" +
f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, " +
f"{self._db_format(self._enabled)}" +
")"
)
@@ -390,7 +403,8 @@ class ProfileXYZ(Profile, SQLSubModel):
name=self.name,
rk=self.rk,
reach=self.reach,
status=self._status
status=self._status,
enabled=self.is_enabled
)
if self.is_deleted():
new_p.set_as_deleted()
@@ -410,7 +424,8 @@ class ProfileXYZ(Profile, SQLSubModel):
name=self.name,
rk=self.rk,
reach=new_reach,
status=self._status
status=self._status,
enabled=self.is_enabled
)
if self.is_deleted():
new_p.set_as_deleted()
@@ -1131,7 +1146,8 @@ class ProfileXYZ(Profile, SQLSubModel):
p = ProfileXYZ(name=self.name,
rk=self.rk,
reach=self.reach,
status=self._status)
status=self._status,
enabled=self.is_enabled)
for i, k in enumerate(self.points):
p.insert_point(i, k.copy())
@@ -1165,3 +1181,16 @@ class ProfileXYZ(Profile, SQLSubModel):
break
self._points = points
self.modified()
@property
def is_enabled(self):
return self._enabled
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, value):
self._enabled = bool(value)
self.modified()
+62 -8
View File
@@ -157,6 +157,14 @@ class Reach(SQLSubModel):
def profiles(self, profiles):
self._profiles = profiles
@property
def enabled_profiles(self):
return list(filter(lambda p: p.is_enabled, self.profiles))
@property
def number_enabled_profiles(self):
return len(self.enabled_profiles)
def get_profiles_from_rk(self, rk):
return list(
filter(
@@ -367,6 +375,28 @@ class Reach(SQLSubModel):
)
)
def get_z_min_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_min(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_z_max_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_max(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk(self):
"""List of profiles rk
@@ -375,6 +405,17 @@ class Reach(SQLSubModel):
"""
return [profile.rk for profile in self.profiles]
def get_rk_enabled_profiles(self):
return list(
map(
lambda profile: profile.rk,
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk_complete_profiles(self):
return list(
map(
@@ -486,14 +527,22 @@ class Reach(SQLSubModel):
)
@timer
def compute_guidelines(self):
def compute_guidelines(self, profiles=None, update_cache=True):
"""Compute reach guidelines
Args:
profiles: Profiles used to compute the guidelines. If omitted,
all non-deleted profiles of the reach are used.
update_cache: Whether to update the reach guidelines cache.
Returns:
Tuple of complete and incomplete guidelines name.
"""
if profiles is None:
profiles = self.profiles
# Get all point contained into a guideline
named_points = [profile.named_points() for profile in self.profiles]
named_points = [profile.named_points() for profile in profiles]
points_name = list(
map(
lambda lst: list(map(lambda p: p.name, lst)),
@@ -524,11 +573,13 @@ class Reach(SQLSubModel):
complete = guide_set - incomplete
# Compute guideline and put data in cache
self._compute_guidelines_cache(guide_set, named_points,
complete, incomplete)
if update_cache:
# Compute guideline and put data in cache
self._compute_guidelines_cache(
guide_set, named_points, complete, incomplete
)
self.modified()
self.modified()
return (complete, incomplete)
def _map_guidelines_points(self, func, full=False):
@@ -752,8 +803,11 @@ class Reach(SQLSubModel):
list_profile.append(list_point_profile)
list_point_profile = []
else:
line.append("")
list_point_profile.append(line[:3])
# MAGE-8 ST files can append hydraulic data
# after the regular x, y, z and point-name
# fields. ProfileXYZ still expects exactly
# those first four fields.
list_point_profile.append(line[:4])
else:
pass
@@ -277,3 +277,10 @@ class BasicHS(SQLSubModel):
def convert(self, new_type):
return new_type(id=self.id, name=self.name, status=self._status)
def cloned(self):
new = type(self)(name=self._name, status=self._status)
new._enabled = self._enabled
new._data = [value.cloned() for value in self._data]
new.modified()
return new
@@ -208,3 +208,11 @@ class BHSValue(SQLSubModel):
def value(self, value):
self._value = self._type(value)
self.modified()
def cloned(self):
return BHSValue(
name=self._name,
type=self._type,
value=self._value,
status=self._status
)
@@ -427,6 +427,18 @@ class HydraulicStructure(SQLSubModel):
def basic_structures(self):
return self.lst.copy()
def cloned_for(self, input_reach, input_section,
output_reach, output_section):
new = HydraulicStructure(name=self._name, status=self._status)
new._enabled = self._enabled
new._input_reach = input_reach
new._input_section = input_section
new._output_reach = output_reach
new._output_section = output_section
new._data = [structure.cloned() for structure in self.lst]
new.modified()
return new
def basic_structure(self, index: int):
if len(self._data) == 0:
return None
@@ -61,6 +61,71 @@ class HydraulicStructureList(PamhyrModelList):
self.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
sections1 = dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
sections2 = dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
def split_endpoint(endpoint_reach, section):
if endpoint_reach is not reach:
return endpoint_reach, section
if section not in profiles:
return endpoint_reach, section
index = profiles.index(section)
if index < split_index:
return reach1, sections1[section]
return reach2, sections2[section]
structures = [
structure
for structure in self.lst
if (structure.input_reach is reach or
structure.output_reach is reach)
]
for structure in structures:
input_reach, input_section = split_endpoint(
structure.input_reach,
structure.input_section
)
output_reach, output_section = split_endpoint(
structure.output_reach,
structure.output_section
)
self._lst.append(structure.cloned_for(
input_reach,
input_section,
output_reach,
output_section
))
if len(structures) != 0:
self.modified()
@property
def enabled_reaches_list(self):
return [
structure
for structure in self.lst
if (structure.input_reach is None or
(not structure.input_reach.is_deleted() and
structure.input_reach.is_enable()))
]
def get_for_enabled_reaches(self, index):
structures = self.enabled_reaches_list
if not 0 <= index < len(structures):
return None
return structures[index]
def __copy__(self):
new = HydraulicStructureList()
@@ -256,6 +256,17 @@ class Data(SQLSubModel):
)
return new
def cloned_for(self, reach, section):
return Data(
name=self._name,
comment=self._comment,
section=section,
discharge=self._discharge,
height=self._height,
reach=reach,
status=self._status,
)
@property
def name(self):
return self._name
@@ -505,6 +516,20 @@ class InitialConditions(SQLSubModel):
for data in self._data:
data.set_as_deleted()
def _splited_for(self, reach, sections):
new = InitialConditions(reach=reach, status=self._status)
new._data = [
data.cloned_for(reach, sections[data["section"]])
for data in self.data
if data["section"] in sections
]
if len(new._data) != 0:
new.modified()
return new
def generate_growing_constant_depth(self, height: float,
compute_discharge: bool):
profiles = self._reach.reach.profiles.copy()
@@ -88,3 +88,28 @@ class InitialConditionsDict(PamhyrModelDict):
new = InitialConditions(reach=reach, status=self._status)
self.set(reach, new)
return new
def split_reach(self, reach, profile, reach1, reach2):
if reach not in self._dict:
return
profiles = reach.reach.profiles
split_index = profiles.index(profile)
sections1 = dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
sections2 = dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
self.set(
reach1,
self._dict[reach]._splited_for(reach1, sections1)
)
self.set(
reach2,
self._dict[reach]._splited_for(reach2, sections2)
)
@@ -359,3 +359,60 @@ class InitialConditionsAdisTS(SQLSubModel):
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
lower_reach_rk = min(reach_rks)
upper_reach_rk = max(reach_rks)
parts = [
(reach1.id, lower_reach_rk, split_rk),
(reach2.id, split_rk, upper_reach_rk),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
def get_specs_for_enabled_reaches(self, reaches):
enabled_reaches = {
reach.id
for reach in reaches
if not reach.is_deleted() and reach.is_enable()
}
return [
specification
for specification in self._data
if (not specification.is_deleted() and
(specification.reach in (None, -1) or
specification.reach in enabled_reaches))
]
@@ -62,6 +62,12 @@ class InitialConditionsAdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for initial_condition in self.lst:
initial_condition.split_reach(
reach, profile, reach1, reach2
)
@property
def Initial_Conditions_List(self):
return self.lst
@@ -31,10 +31,12 @@ class ICAdisTSSpec(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=None):
super(ICAdisTSSpec, self).__init__()
self._status = status
status=None, owner_scenario=-1):
super(ICAdisTSSpec, self).__init__(
id=id,
status=status,
owner_scenario=owner_scenario
)
self._name_section = name
self._reach = None
@@ -234,7 +236,7 @@ class ICAdisTSSpec(SQLSubModel):
@name.setter
def name(self, name):
self._name_section = name
self._status.modified()
self.modified()
@property
def reach(self):
@@ -243,7 +245,7 @@ class ICAdisTSSpec(SQLSubModel):
@reach.setter
def reach(self, reach):
self._reach = reach
self._status.modified()
self.modified()
@property
def start_rk(self):
@@ -252,7 +254,7 @@ class ICAdisTSSpec(SQLSubModel):
@start_rk.setter
def start_rk(self, start_rk):
self._start_rk = start_rk
self._status.modified()
self.modified()
@property
def end_rk(self):
@@ -261,7 +263,7 @@ class ICAdisTSSpec(SQLSubModel):
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self._status.modified()
self.modified()
@property
def concentration(self):
@@ -270,7 +272,7 @@ class ICAdisTSSpec(SQLSubModel):
@concentration.setter
def concentration(self, concentration):
self._concentration = concentration
self._status.modified()
self.modified()
@property
def eg(self):
@@ -279,7 +281,7 @@ class ICAdisTSSpec(SQLSubModel):
@eg.setter
def eg(self, eg):
self._eg = eg
self._status.modified()
self.modified()
@property
def em(self):
@@ -288,7 +290,7 @@ class ICAdisTSSpec(SQLSubModel):
@em.setter
def em(self, em):
self._em = em
self._status.modified()
self.modified()
@property
def ed(self):
@@ -297,7 +299,7 @@ class ICAdisTSSpec(SQLSubModel):
@ed.setter
def ed(self, ed):
self._ed = ed
self._status.modified()
self.modified()
@property
def rate(self):
@@ -306,7 +308,7 @@ class ICAdisTSSpec(SQLSubModel):
@rate.setter
def rate(self, rate):
self._rate = rate
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -315,4 +317,18 @@ class ICAdisTSSpec(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = ICAdisTSSpec(name=self._name_section, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._concentration = self._concentration
new._eg = self._eg
new._em = self._em
new._ed = self._ed
new._rate = self._rate
new._enabled = self._enabled
new.modified()
return new
@@ -0,0 +1,272 @@
# InitialConditionsTemperature.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from functools import reduce
from tools import trace, timer, old_pamhyr_date_to_timestamp
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
from Model.InitialConditionsTemperature.InitialConditionsTemperatureSpec \
import ICTemperatureSpec
logger = logging.getLogger()
class InitialConditionsTemperature(SQLSubModel):
_sub_classes = [
ICTemperatureSpec,
]
def __init__(self, id: int = -1, name: str = "default",
status=None, owner_scenario=-1):
super(InitialConditionsTemperature, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = name
self._temperature = None
self._data = []
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
temperature REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) < 7):
table_name = "initial_conditions_temperature"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, " +
"name, temperature, scenario " +
"FROM initial_conditions_temperature " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
temperature = next(it)
owner_scenario = next(it)
ic = cls(
id=pid,
name=name,
status=status,
owner_scenario=owner_scenario
)
if deleted:
ic.set_as_deleted()
ic.temperature = temperature
data['ic_default_id'] = pid
ic._data = ICTemperatureSpec._db_load(execute, data)
loaded.add(pid)
new.append(ic)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM initial_conditions_temperature " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
temperature = self._temperature if \
self._temperature is not None else 0.0
sql = (
"INSERT INTO " +
"initial_conditions_temperature(" +
"pamhyr_id, deleted, name, temperature, " +
"scenario" +
") " +
"VALUES (" +
f"{self.id}, {self.is_deleted()}, " +
f"'{self._db_format(self._name)}', " +
f"{temperature}, {self._status.scenario_id}" +
")"
)
execute(sql)
data['ic_default_id'] = self.id
execute(
"DELETE FROM initial_conditions_temperature_spec " +
f"WHERE ic_default = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
for ic_spec in self._data:
ic_spec._db_save(execute, data)
return True
def __len__(self):
return len(self.data)
@property
def data(self):
return list(filter(lambda item: not item.is_deleted(), self._data))
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temperature):
self._temperature = temperature
self.modified()
def new(self, index):
n = ICTemperatureSpec(status=self._status)
self._data.insert(index, n)
self.modified()
return n
def delete(self, data):
list(
map(
lambda x: x.set_as_deleted(),
data
)
)
self.modified()
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self._data)
)
)
)
self.modified()
def insert(self, index, data):
if data in self._data:
self.undelete([data])
else:
self._data.insert(index, data)
self.modified()
def undelete(self, lst):
for x in lst:
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
parts = []
for new_reach in (reach1, reach2):
rks = new_reach.reach.get_rk()
if rks:
parts.append((new_reach.id, min(rks), max(rks)))
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
clones = []
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
clones.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if clones:
self._data.extend(clones)
self.modified()
@@ -0,0 +1,73 @@
# InitialConditionsTemperatureList.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.InitialConditionsTemperature.InitialConditionsTemperature \
import InitialConditionsTemperature
class InitialConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
InitialConditionsTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = InitialConditionsTemperature._db_load(
execute, data
)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM initial_conditions_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for ic in self._lst:
ic._db_save(execute, data=data)
return True
def new(self, index):
n = InitialConditionsTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for initial_condition in self.lst:
initial_condition.split_reach(
reach, profile, reach1, reach2
)
@property
def Initial_Conditions_List(self):
return self.lst
@@ -0,0 +1,214 @@
# InitialConditionsAdisTSSpec.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class ICTemperatureSpec(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=None):
super(ICTemperatureSpec, self).__init__()
self._status = status
self._name_section = name
self._reach = None
self._start_rk = None
self._end_rk = None
self._temperature = None
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_temperature_spec{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ic_default INTEGER NOT NULL,
name TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
temperature REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(ic_default)
REFERENCES initial_conditions_temperature(pamhyr_id),
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) < 7):
table_name = "initial_conditions_temperature_spec"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
reachs = data["edges"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, name, reach, start_rk, end_rk, " +
"temperature, scenario " +
"FROM initial_conditions_temperature_spec " +
f"WHERE ic_default = {data['ic_default_id']} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
if table is not None:
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
reach = next(it)
start_rk = next(it)
end_rk = next(it)
temperature = next(it)
owner_scenario = next(it)
new_spec = cls(
id=id, name=name,
status=status,
owner_scenario=owner_scenario
)
if deleted:
new_spec.set_as_deleted()
new_spec.reach = reach
new_spec.start_rk = start_rk
new_spec.end_rk = end_rk
new_spec.temperature = temperature
# loaded.add(pid)
new.append(new_spec)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ic_default = data['ic_default_id']
sql = (
"INSERT INTO " +
"initial_conditions_temperature_spec(pamhyr_id, deleted, " +
"ic_default, name, reach, " +
"start_rk, end_rk, temperature, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{ic_default}, " +
f"'{self._db_format(self._name_section)}', " +
f"{self._reach}, " +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._temperature}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
@property
def name(self):
return self._name_section
@name.setter
def name(self, name):
self._name_section = name
self._status.modified()
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach):
self._reach = reach
self._status.modified()
@property
def start_rk(self):
return self._start_rk
@start_rk.setter
def start_rk(self, start_rk):
self._start_rk = start_rk
self._status.modified()
@property
def end_rk(self):
return self._end_rk
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self._status.modified()
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temperature):
self._temperature = temperature
self._status.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = ICTemperatureSpec(
name=self._name_section,
status=self._status
)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._temperature = self._temperature
new.modified()
return new
@@ -196,6 +196,13 @@ class Data(SQLSubModel):
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class LateralContribution(SQLSubModel):
_sub_classes = [Data]
@@ -511,6 +518,15 @@ class LateralContribution(SQLSubModel):
def has_reach(self):
return self._reach is not None
def cloned_for(self, reach, begin_section, end_section):
new = type(self)(name=self._name, status=self._status)
new._reach = reach
new._begin_section = begin_section
new._end_section = end_section
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def begin_rk(self):
if self._begin_section is None:
@@ -71,6 +71,89 @@ class LateralContributionList(PamhyrModelListWithTab):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
parts = [
(
reach1,
0,
split_index,
dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
),
(
reach2,
split_index,
len(profiles) - 1,
dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
),
]
for tab in self._tabs:
contributions = [
contribution
for contribution in self.get_tab(tab)
if contribution.reach is reach
]
for contribution in contributions:
if (contribution.begin_section not in profiles or
contribution.end_section not in profiles):
continue
begin = profiles.index(contribution.begin_section)
end = profiles.index(contribution.end_section)
lower, upper = sorted((begin, end))
reverse = begin > end
clones = []
for new_reach, part_lower, part_upper, sections in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
continue
begin_index = (
clipped_upper if reverse else clipped_lower
)
end_index = (
clipped_lower if reverse else clipped_upper
)
clones.append(contribution.cloned_for(
new_reach,
sections[profiles[begin_index]],
sections[profiles[end_index]]
))
if len(clones) == 0:
continue
self._tabs[tab].extend(clones)
self._status.modified()
def get_tab_for_enabled_reaches(self, tab):
return [
contribution
for contribution in self.get_tab(tab)
if (contribution.reach is None or
(not contribution.reach.is_deleted() and
contribution.reach.is_enable()))
]
def get_for_enabled_reaches(self, tab, index):
contributions = self.get_tab_for_enabled_reaches(tab)
if not 0 <= index < len(contributions):
return None
return contributions[index]
def __copy__(self):
new = LateralContributionList()
@@ -192,6 +192,13 @@ class Data(SQLSubModel):
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class LateralContributionAdisTS(SQLSubModel):
_sub_classes = [Data]
@@ -477,6 +484,18 @@ class LateralContributionAdisTS(SQLSubModel):
self._end_rk = end_rk
self.modified()
def cloned_for(self, reach, begin_rk, end_rk):
new = LateralContributionAdisTS(
pollutant=self._pollutant,
status=self._status
)
new._reach = reach
new._begin_rk = begin_rk
new._end_rk = end_rk
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def _default_0(self):
return self._types[0](0)
@@ -68,6 +68,46 @@ class LateralContributionsAdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
parts = []
for new_reach in (reach1, reach2):
rks = new_reach.reach.get_rk()
if len(rks) == 0:
continue
parts.append((new_reach.id, min(rks), max(rks)))
contributions = [
contribution
for contribution in self.lst
if contribution.reach == reach.id
]
clones = []
for contribution in contributions:
begin = contribution.begin_rk
end = contribution.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
continue
begin_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
clones.append(contribution.cloned_for(
new_reach, begin_rk, end_rk
))
if len(clones) != 0:
self._lst.extend(clones)
self._status.modified()
@property
def Lat_Cont_List(self):
return self.lst
-208
View File
@@ -1,208 +0,0 @@
# OutputRKAdists.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
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class OutputRKAdists(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, reach=None,
rk=None, title: str = "", status=None,
owner_scenario=-1):
super(OutputRKAdists, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._reach = reach
self._rk = rk
self._title = str(title)
self._enabled = True
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach_id):
self._reach = reach_id
self._status.modified()
@property
def rk(self):
return self._rk
@rk.setter
def rk(self, profile_id):
self._rk = profile_id
self._status.modified()
@property
def title(self):
return self._title
@title.setter
def title(self, title):
self._title = title
self._status.modified()
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE output_rk_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
reach INTEGER NOT NULL,
rk REAL NOT NULL,
title TEXT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "OutputRKAdists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "OutputRKAdists"
table_new = "output_rk_adists"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table_new}_tmp " +
"(pamhyr_id, reach, rk, title, scenario) " +
"SELECT pamhyr_id, reach, rk, title, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table_new, reachs)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
# reach = data["reach"]
# profile = data["profile"]
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, reach, rk, title, scenario " +
f"FROM output_rk_adists"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
id_reach = next(it)
id_rk = next(it)
title = next(it)
owner_scenario = next(it)
new_output = cls(
id=pid, reach=id_reach,
rk=id_rk, title=title,
status=status,
owner_scenario=owner_scenario,
)
if deleted:
new_output.set_as_deleted()
loaded.add(pid)
new.append(new_output)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM output_rk_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"INSERT INTO " +
"output_rk_adists(pamhyr_id, deleted, " +
"reach, rk, title, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._reach}, {self._rk}, " +
f"'{self._db_format(self._title)}', " +
f"{self._status.scenario_id}" +
")"
)
return True
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
+1 -1
View File
@@ -527,7 +527,7 @@ class Results(SQLSubModel):
i = 0
for reach in new_results._river.reachs:
nb = len(reach.profiles)
reach.set_global_index(range(i, i + nb + 1))
reach.set_global_index(range(i, i + nb))
i += nb
if "Z" in table_data:
+21 -8
View File
@@ -154,6 +154,7 @@ class Profile(SQLSubModel):
"len_data, data, scenario " +
"FROM results_data " +
f"WHERE scenario = {scenario.id} " +
f"AND result = {data['result_pid']} " +
f"AND reach = {reach.pamhyr_id} " +
f"AND section = {profile.pamhyr_id}"
)
@@ -276,7 +277,7 @@ class Profile(SQLSubModel):
class Reach(SQLSubModel):
_sub_classes = [Profile]
def __init__(self, reach, study, parent, with_init=True):
def __init__(self, reach, study, parent, with_init=True, profiles=None):
super(Reach, self).__init__(
id=-1, status=study.status,
owner_scenario=study.status.scenario.id
@@ -287,10 +288,12 @@ class Reach(SQLSubModel):
self._reach = reach # Source reach in the study
self._profiles = []
if with_init:
if profiles is None:
profiles = reach.profiles
self._profiles = list(
map(
lambda p: Profile(p, self._study, self._parent),
reach.profiles
profiles
)
)
@@ -379,7 +382,14 @@ class Reach(SQLSubModel):
for i, profile in enumerate(reach.profiles):
data["profile"] = profile
new_reach._profiles += [Profile._db_load(execute, data)]
result_profile = Profile._db_load(execute, data)
if len(result_profile) > 0:
new_reach._profiles.append(result_profile)
new_reach._profile_mask = [
profile.name[0:8] != 'interpol'
for profile in new_reach._profiles
]
return new_reach
@@ -418,10 +428,13 @@ class River(SQLSubModel):
def has_reach(self, id):
return 0 <= id < len(self._reachs)
def add(self, reach_id):
def add(self, reach_id, profiles=None):
reachs = self._study.river.enable_edges()
new = Reach(reachs[reach_id].reach, self._study, self._parent)
new = Reach(
reachs[reach_id].reach, self._study, self._parent,
profiles=profiles
)
self._reachs.append(new)
return new
@@ -460,9 +473,9 @@ class River(SQLSubModel):
for reach in study.river.reachs():
data["reach"] = reach.reach
new_river._reachs.append(
Reach._db_load(execute, data)
)
result_reach = Reach._db_load(execute, data)
if len(result_reach) > 0:
new_river._reachs.append(result_reach)
return new_river
+126 -13
View File
@@ -49,7 +49,6 @@ from Model.REPLine.REPLineList import REPLineList
from Solver.Solvers import solver_type_list
from Model.OutputRKAdists.OutputRKListAdists import OutputRKAdistsList
from Model.Pollutants.PollutantsList import PollutantsList
from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList \
import InitialConditionsAdisTSList
@@ -59,6 +58,16 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
import LateralContributionsAdisTSList
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
import InitialConditionsTemperatureList
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
from Model.WeatherParameters.WeatherParametersList \
import WeatherParametersList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Results.Results import Results
@@ -468,13 +477,15 @@ class River(Graph):
HydraulicStructureList,
AddFileList,
REPLineList,
OutputRKAdistsList,
PollutantsList,
InitialConditionsAdisTSList,
BoundaryConditionsAdisTSList,
LateralContributionsAdisTSList,
D90AdisTSList,
DIFAdisTSList,
InitialConditionsTemperatureList,
BoundaryConditionsTemperatureList,
WeatherParametersList,
GeoTIFFList,
Results
]
@@ -502,7 +513,6 @@ class River(Graph):
)
self._additional_files = AddFileList(status=self._status)
self._rep_lines = REPLineList(status=self._status)
self._Output_rk_adists = OutputRKAdistsList(status=self._status)
self._Pollutants = PollutantsList(status=self._status)
self._InitialConditionsAdisTS = InitialConditionsAdisTSList(
status=self._status)
@@ -512,6 +522,13 @@ class River(Graph):
status=self._status)
self._D90AdisTS = D90AdisTSList(status=self._status)
self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._InitialConditionsTemperature = (
InitialConditionsTemperatureList(status=self._status)
)
self._BoundaryConditionsTemperature = (
BoundaryConditionsTemperatureList(status=self._status)
)
self._WeatherParameters = (WeatherParametersList(status=self._status))
self._geotiff = GeoTIFFList(status=self._status)
@@ -610,10 +627,6 @@ class River(Graph):
new._Pollutants = PollutantsList._db_load(execute, data)
new._Output_rk_adists = OutputRKAdistsList._db_load(
execute, data
)
new._InitialConditionsAdisTS = InitialConditionsAdisTSList._db_load(
execute, data)
@@ -627,6 +640,14 @@ class River(Graph):
new._DIFAdisTS = DIFAdisTSList._db_load(execute, data)
new._InitialConditionsTemperature = \
InitialConditionsTemperatureList._db_load(execute, data)
new._BoundaryConditionsTemperature = \
BoundaryConditionsTemperatureList._db_load(execute, data)
new._WeatherParameters = WeatherParametersList._db_load(execute, data)
new._geotiff = GeoTIFFList._db_load(execute, data)
return new
@@ -654,7 +675,6 @@ class River(Graph):
for solver in self._parameters:
objs.append(self._parameters[solver])
objs.append(self._Output_rk_adists)
objs.append(self._Pollutants)
objs.append(self._InitialConditionsAdisTS)
objs.append(self._BoundaryConditionsAdisTS)
@@ -662,6 +682,10 @@ class River(Graph):
objs.append(self._D90AdisTS)
objs.append(self._DIFAdisTS)
objs.append(self._InitialConditionsTemperature)
objs.append(self._BoundaryConditionsTemperature)
objs.append(self._WeatherParameters)
objs.append(self._geotiff)
for solv_type in self.results:
@@ -734,12 +758,14 @@ class River(Graph):
self._hydraulic_structures,
self._additional_files,
self._rep_lines,
self._Output_rk_adists,
self._Pollutants,
self._InitialConditionsAdisTS,
self._BoundaryConditionsAdisTS,
self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS,
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
self._WeatherParameters,
self._geotiff,
]
@@ -845,10 +871,6 @@ Last export at: @date."""
def parameters(self):
return self._parameters
@property
def Output_rk_adists(self):
return self._Output_rk_adists
@property
def Pollutants(self):
return self._Pollutants
@@ -873,6 +895,75 @@ Last export at: @date."""
def dif_adists(self):
return self._DIFAdisTS
@property
def ic_temperature(self):
return self._InitialConditionsTemperature
@property
def boundary_conditions_temperature(self):
return self._BoundaryConditionsTemperature
@property
def weather_parameters(self):
return self._WeatherParameters
def profile_dependencies(self, profile):
"""Return model data which directly reference *profile*.
PK-based intervals are deliberately not included: only explicit
references to the profile object prevent it from being disabled.
"""
dependencies = []
def add(kind, item, association):
dependencies.append({
"kind": kind,
"item": item,
"association": association,
})
for reach in list(self._initial_conditions._dict):
initial_conditions = self._initial_conditions.get(reach)
for data in initial_conditions.data:
if data["section"] is profile:
add("initial_condition", data, "section")
for structure in self._hydraulic_structures.lst:
if structure.input_section is profile:
add("hydraulic_structure", structure, "input_section")
if structure.output_section is profile:
add("hydraulic_structure", structure, "output_section")
for tab in self._lateral_contribution._tabs_list:
for contribution in self._lateral_contribution.get_tab(tab):
if contribution.begin_section is profile:
add("lateral_contribution", contribution, "begin_section")
if contribution.end_section is profile:
add("lateral_contribution", contribution, "end_section")
for parameter in self._WeatherParameters.lst:
if parameter.begin_section is profile:
add("weather_parameter", parameter, "begin_section")
if parameter.end_section is profile:
add("weather_parameter", parameter, "end_section")
reach_id = profile.reach.pamhyr_id
for initial_condition in self._InitialConditionsTemperature.lst:
for specification in initial_condition.data:
same_reach = specification.reach == reach_id
same_rk = (
specification.start_rk == profile.rk or
specification.end_rk == profile.rk
)
if same_reach and same_rk:
add(
"initial_condition_temperature",
specification,
"cross_section_pk",
)
return dependencies
def get_params(self, solver):
if solver in self._parameters:
return self._parameters[solver]
@@ -945,4 +1036,26 @@ Last export at: @date."""
self._add_edge(r1)
self._add_edge(r2)
self._initial_conditions.split_reach(
reach, profile, r1, r2
)
self._lateral_contribution.split_reach(
reach, profile, r1, r2
)
self._hydraulic_structures.split_reach(
reach, profile, r1, r2
)
self._InitialConditionsAdisTS.split_reach(
reach, profile, r1, r2
)
self._LateralContributionsAdisTS.split_reach(
reach, profile, r1, r2
)
self._InitialConditionsTemperature.split_reach(
reach, profile, r1, r2
)
self._WeatherParameters.split_reach(
reach, profile, r1, r2
)
return r1, r2
+10 -8
View File
@@ -32,7 +32,6 @@ class Scenario(SQLSubModel):
tables_with_deleted_column = [
# Adists
"output_rk_adists",
"boundary_condition_adists", "boundary_condition_data_adists",
"lateral_contribution_adists", "lateral_contribution_data_adists",
"initial_conditions_adists", "initial_conditions_adists_spec",
@@ -53,6 +52,12 @@ class Scenario(SQLSubModel):
"geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node",
"geotiff", "reservoir", "reservoir_data",
# Temperature
"boundary_condition_temperature",
"boundary_condition_data_temperature",
"initial_conditions_temperature",
"initial_conditions_temperature_spec",
"weather_parameters", "weather_parameters_data",
]
related_tables = tables_with_deleted_column + [
@@ -346,11 +351,8 @@ class Scenario(SQLSubModel):
return None
def workdir(self):
srep = ""
if self.id != 0:
srep = os.path.join(
self.parent.workdir(),
"scenario_" + str(self.id)
)
current = "scenario_" + str(self.id)
if self.parent is None or self.parent.id == 0:
return current
return srep
return os.path.join(self.parent.workdir(), current)
@@ -216,6 +216,9 @@ class Parameter(SQLSubModel):
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ind = data["ind"]
solver = data["solver"]
+4 -1
View File
@@ -60,9 +60,12 @@ class StricklersList(PamhyrModelList):
return self.lst
def new(self, index):
owner_scenario = (
self._status.scenario_id if self._status is not None else -1
)
s = Stricklers(
status=self._status,
owner_scenario=self._status.scenario_id
owner_scenario=owner_scenario
)
self.insert(index, s)
self.modified()
+1 -1
View File
@@ -46,7 +46,7 @@ logger = logging.getLogger()
class Study(SQLModel):
_version = "0.2.7"
_version = "0.2.9"
_sub_classes = [
Scenario,
@@ -0,0 +1,756 @@
# WeatherParameters.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class Data(SQLSubModel):
_sub_classes = []
def __init__(self,
data0, data1,
id: int = -1,
types=[float, float],
status=None,
owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._types = types
self._data = [data0, data1]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE weather_parameters_data{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
wp INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(wp) REFERENCES weather_parameters(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 7)):
if not cls.is_table_exists(execute, "air_temperature_data"):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
wp = data["wp"]
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
values = execute(
"SELECT pamhyr_id, deleted, " +
"data0, data1, scenario " +
"FROM weather_parameters_data " +
f"WHERE wp = {wp._pamhyr_id} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
"ORDER BY ind ASC"
)
for v in values:
it = iter(v)
pid = next(it)
deleted = next(it)
data0 = wp._types[0](next(it))
data1 = wp._types[1](next(it))
owner_scenario = next(it)
nd = cls(
data0, data1,
id=pid,
types=wp._types,
status=status,
owner_scenario=owner_scenario
)
if deleted:
nd.set_as_deleted()
loaded.add(pid)
new.append(nd)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
pid = self._pamhyr_id
ind = data["ind"]
data0 = self._db_format(str(self[0]))
data1 = self._db_format(str(self[1]))
wp = data["wp"]
execute(
"INSERT INTO " +
"weather_parameters_data (pamhyr_id, deleted, ind, " +
"data0, data1, wp, scenario) " +
f"VALUES ({pid}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(data0)}', {self._db_format(data1)}, " +
f"{wp._pamhyr_id}, {self._status.scenario_id}" +
")"
)
return True
def __getitem__(self, key):
return self._types[key](self._data[key])
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class WeatherParametersDefault(SQLSubModel):
def __init__(self, type: str, value: float = 0.0,
id: int = -1, status=None, owner_scenario=-1):
super(WeatherParametersDefault, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._type = type
self._value = float(value)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, type, value, scenario " +
"FROM weather_parameters_default " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
for row in table:
it = iter(row)
pid = next(it)
type = next(it)
value = next(it)
owner_scenario = next(it)
wp_default = cls(
type=type,
value=value,
id=pid,
status=status,
owner_scenario=owner_scenario
)
loaded.add(pid)
new.append(wp_default)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"weather_parameters_default(" +
"pamhyr_id, type, value, scenario) " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._type)}', {self._value}, " +
f"{self._status.scenario_id}" +
")"
)
return True
@property
def type(self):
return self._type
@property
def value(self):
return self._value
@value.setter
def value(self, value):
self._value = float(value)
self.modified()
class WeatherParameters(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1,
name: str = "",
status=None, owner_scenario=-1):
super(WeatherParameters, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._type = ""
self._reach = None
self._begin_section = None
self._end_section = None
self._data = []
self._header = []
self._types = [float, float]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE weather_parameters{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type TEXT NOT NULL,
reach INTEGER,
begin_section INTEGER,
end_section INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(begin_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
FOREIGN KEY(end_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
execute(f"""
CREATE TABLE weather_parameters_default{ext}(
{cls.create_db_add_pamhyr_id()},
type TEXT NOT NULL,
value FLOAT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
primary KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _get_ctor_from_type(cls, t):
from Model.WeatherParameters.WeatherParametersTypes import (
NotDefined, WeatherParam, AirTemperature, SpecificHumidity,
GlobalRadiation, ReferenceWindSpeed, GroundwaterTemperature,
GroundwaterFlowRate, Albedo, ShadingCoefficient, CloudCoverFraction
)
res = NotDefined
if t == "WP":
res = WeatherParam
elif t == "AT":
res = AirTemperature
elif t == "SH":
res = SpecificHumidity
elif t == "GR":
res = GlobalRadiation
elif t == "RWS":
res = ReferenceWindSpeed
elif t == "GT":
res = GroundwaterTemperature
elif t == "GFR":
res = GroundwaterFlowRate
elif t == "ALB":
res = Albedo
elif t == "SC":
res = ShadingCoefficient
elif t == "CCF":
res = CloudCoverFraction
else:
res = NotDefined
return res
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 7)):
if not cls.is_table_exists(execute, "air_temperature"):
cls._db_create(execute)
created = True
if not cls.is_table_exists(execute, "weather_parameters_default"):
execute(f"""
CREATE TABLE weather_parameters_default(
{cls.create_db_add_pamhyr_id()},
type TEXT NOT NULL,
value FLOAT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
primary KEY(pamhyr_id, scenario)
)
""")
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
edges = data["edges"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"reach, begin_section, end_section, scenario " +
"FROM weather_parameters " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
for row in table:
it = iter(row)
pid = next(it)
deleted = next(it)
name = next(it)
t = next(it)
reach = next(it)
b_section = next(it)
e_section = next(it)
owner_scenario = next(it)
ctor = cls._get_ctor_from_type(t)
wp = ctor(
id=pid, name=name,
status=status,
owner_scenario=owner_scenario
)
wp.reach = None
wp._begin_section = None
wp._end_section = None
if reach not in (-1, None):
wp.reach = next(
(edge for edge in edges if edge.id == reach), None
)
profiles = (
wp.reach.reach.profiles
if wp.reach is not None else []
)
wp._begin_section = next(
(profile for profile in profiles
if profile.id == b_section), None
)
wp._end_section = next(
(profile for profile in profiles
if profile.id == e_section), None
)
data["wp"] = wp
wp._data = Data._db_load(execute, data=data)
loaded.add(pid)
new.append(wp)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
data["wp"] = self
execute(
"DELETE FROM weather_parameters_data " +
f"WHERE wp = {self._pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
reach = None
begin_section = None
end_section = None
if self._reach is not None:
reach = self._reach._pamhyr_id
begin_section = self._begin_section._pamhyr_id
end_section = self._end_section._pamhyr_id
execute(
"INSERT INTO " +
"weather_parameters(" +
"pamhyr_id, deleted, name, type, " +
"reach, begin_section, end_section, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', {reach}, " +
f"{begin_section}, {end_section}, " +
f"{self._status.scenario_id}" +
")"
)
ind = 0
for d in self._data:
data["ind"] = ind
d._db_save(execute, data)
ind += 1
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for d in self._data:
d._data_traversal(predicate, modifier, data)
def __len__(self):
return len(
list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
)
@classmethod
def compatibility(cls):
return ["liquid", "solid", "suspenssion"]
@classmethod
def time_convert(cls, data):
if type(data) is str:
if data.count("-") == 2:
return date_iso_to_timestamp(data)
if data.count("/") == 2:
return date_dmy_to_timestamp(data)
if data.count(":") == 3:
return old_pamhyr_date_to_timestamp(data)
if data.count(":") == 2:
return old_pamhyr_date_to_timestamp("00:" + data)
if data.count(".") == 1:
return round(float(data))
return int(data)
@property
def name(self):
if self._name == "":
return f"LC #{self.id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def type(self):
return self._type
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach):
self._reach = reach
self._begin_section = None
self._end_section = None
if reach is not None:
profiles = reach.reach.profiles
if profiles:
self._begin_section = profiles[0]
self._end_section = profiles[-1]
self.modified()
def has_reach(self):
return self._reach is not None
@property
def begin_rk(self):
if self._begin_section is None:
return 0
return self._begin_section.rk
@property
def begin_section(self):
return self._begin_section
@begin_section.setter
def begin_section(self, section):
self._begin_section = section
self.modified()
@property
def end_rk(self):
if self._end_section is None:
return 0
return self._end_section.rk
@property
def end_section(self):
return self._end_section
@end_section.setter
def end_section(self, section):
self._end_section = section
self.modified()
def cloned_for(self, reach, begin_section, end_section):
new = type(self)(name=self._name, status=self._status)
new._reach = reach
new._begin_section = begin_section
new._end_section = end_section
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def header(self):
return self._header.copy()
@property
def data(self):
return list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
def get_type_column(self, column):
if 0 <= column < 2:
return self._types[column]
return None
@property
def _default_0(self):
return self._types[0](0)
@property
def _default_1(self):
return self._types[1](0.0)
def is_define(self):
return self._data is not None
def new_from_data(self, header, data):
new_0 = self._default_0
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:
for i in [0, 1]:
for j in range(len(header)):
if self._header[i] == header[j] and j < len(data):
if i == 0:
new_0 = convert(i, data[j], new_0)
else:
new_1 = convert(i, data[j], new_1)
else:
if len(data) > 0:
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)
def add(self, index: int):
value = Data(self._default_0, self._default_1, status=self._status)
self._data.insert(index, value)
self.modified()
return value
def insert(self, index: int, value):
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
self._data = list(
map(
lambda e: e[1],
filter(
lambda e: e[0] not in indexes,
enumerate(self.data)
)
)
)
self.modified()
def set_as_deleted_i(self, indexes):
for i in indexes:
self._data[i].set_as_deleted()
self.modified()
def set_as_not_deleted_i(self, indexes):
for i in indexes:
self._data[i].set_as_not_deleted()
self.modified()
def delete(self, els):
self._data = list(
filter(
lambda e: e not in els,
self.data
)
)
self.modified()
def sort(self, _reverse=False, key=None):
if key is None:
self._data.sort(reverse=_reverse)
else:
self._data.sort(reverse=_reverse, key=key)
self.modified()
def get_i(self, index):
return self.data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index, column, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)
@timer
def convert(self, cls):
new = cls(name=self.name, status=self._status)
new.reach = self.reach
new.begin_section = self.begin_section
new.end_section = self.end_section
for i, _ in enumerate(self.data):
new.add(i)
for i in [0, 1]:
for j in [0, 1]:
if self._header[i] == new.header[j]:
for ind, v in self.data:
try:
new._set_i_c_v(ind, j, v[i])
except Exception as e:
logger.info(e)
self.modified()
return new
def move_up(self, index):
if index < len(self):
next = index - 1
d = self._data
d[index], d[next] = d[next], d[index]
self.modified()
def move_down(self, index):
if index >= 0:
prev = index + 1
d = self._data
d[index], d[prev] = d[prev], d[index]
self.modified()
@@ -0,0 +1,234 @@
# WeatherParametersList.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Except import NotImplementedMethodeError
from Model.WeatherParameters.WeatherParameters import (
WeatherParameters,
WeatherParametersDefault,
)
from Model.WeatherParameters.WeatherParametersTypes import (
NotDefined, WeatherParam, AirTemperature, SpecificHumidity,
GlobalRadiation, ReferenceWindSpeed, GroundwaterTemperature,
GroundwaterFlowRate, Albedo, ShadingCoefficient, CloudCoverFraction
)
class WeatherParametersList(PamhyrModelList):
_sub_classes = [
WeatherParameters,
]
_default_types = [
"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):
super(WeatherParametersList, self).__init__(status=status)
self._default = []
self._init_missing_default_values()
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = WeatherParameters._db_load(
execute, data
)
data['loaded_pid'] = set()
new._default = WeatherParametersDefault._db_load(
execute, data
)
new._init_missing_default_values()
new._sort_default_values()
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM weather_parameters " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM weather_parameters_data " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM weather_parameters_default " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for wp in self._lst:
wp._db_save(execute, data=data)
for wp_default in self._default:
wp_default._db_save(execute, data=data)
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for el in self._lst:
el._data_traversal(predicate, modifier, data)
for el in self._default:
el._data_traversal(predicate, modifier, data)
def _init_missing_default_values(self):
existing = [wp_default.type for wp_default in self.default]
for type in self._default_types:
if type not in existing:
self._default.append(
WeatherParametersDefault(
type=type,
value=self._default_values[type],
status=self._status,
)
)
self._sort_default_values()
def _sort_default_values(self):
self._default.sort(
key=lambda wp_default: self._default_types.index(wp_default.type)
if wp_default.type in self._default_types
else len(self._default_types)
)
def new(self, index, type="ND"):
if type == "AT":
n = AirTemperature(status=self._status)
elif type == "SH":
n = SpecificHumidity(status=self._status)
elif type == "GR":
n = GlobalRadiation(status=self._status)
elif type == "RWS":
n = ReferenceWindSpeed(status=self._status)
elif type == "GT":
n = GroundwaterTemperature(status=self._status)
elif type == "GFR":
n = GroundwaterFlowRate(status=self._status)
elif type == "ALB":
n = Albedo(status=self._status)
elif type == "SC":
n = ShadingCoefficient(status=self._status)
elif type == "CCF":
n = CloudCoverFraction(status=self._status)
else:
n = NotDefined(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
parts = [
(
reach1,
0,
split_index,
dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
),
(
reach2,
split_index,
len(profiles) - 1,
dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
),
]
weather_parameters = [
weather_parameter
for weather_parameter in self.lst
if weather_parameter.reach is reach
]
clones = []
for weather_parameter in weather_parameters:
if (weather_parameter.begin_section not in profiles or
weather_parameter.end_section not in profiles):
continue
begin = profiles.index(weather_parameter.begin_section)
end = profiles.index(weather_parameter.end_section)
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper, sections in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower > clipped_upper:
continue
begin_index = clipped_upper if reverse else clipped_lower
end_index = clipped_lower if reverse else clipped_upper
clones.append(weather_parameter.cloned_for(
new_reach,
sections[profiles[begin_index]],
sections[profiles[end_index]]
))
if clones:
self._lst.extend(clones)
self._status.modified()
@property
def Weather_Parameters_List(self):
return self.lst
@property
def default(self):
return self._default.copy()
def default_for_type(self, type):
for wp_default in self.default:
if wp_default.type == type:
return wp_default
return None
@@ -0,0 +1,171 @@
# LateralContributionTypes.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from Model.Except import NotImplementedMethodeError
from Model.WeatherParameters.WeatherParameters import WeatherParameters
class NotDefined(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(NotDefined, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "ND"
self._header = ["x", "y"]
@property
def _default_0(self):
return 0.0
class WeatherParam(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(WeatherParam, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "WP"
self._header = ["time", "value"]
self._types = [WeatherParam.time_convert, float]
@classmethod
def compatibility(cls):
return ["liquid"]
class AirTemperature(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(AirTemperature, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "AT"
self._header = ["time", "temperature"]
self._types = [AirTemperature.time_convert, float]
class SpecificHumidity(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(SpecificHumidity, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "SH"
self._header = ["time", "humidity"]
self._types = [SpecificHumidity.time_convert, float]
class GlobalRadiation(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(GlobalRadiation, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "GR"
self._header = ["time", "radiation"]
self._types = [GlobalRadiation.time_convert, float]
class ReferenceWindSpeed(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(ReferenceWindSpeed, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "RWS"
self._header = ["time", "wind speed"]
self._types = [ReferenceWindSpeed.time_convert, float]
class GroundwaterTemperature(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(GroundwaterTemperature, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "GT"
self._header = ["time", "temperature"]
self._types = [GroundwaterTemperature.time_convert, float]
class GroundwaterFlowRate(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(GroundwaterFlowRate, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "GFR"
self._header = ["time", "discharge"]
self._types = [GroundwaterFlowRate.time_convert, float]
class Albedo(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(Albedo, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "ALB"
self._header = ["time", "albedo"]
self._types = [Albedo.time_convert, float]
class ShadingCoefficient(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(ShadingCoefficient, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "SC"
self._header = ["time", "shading coefficient"]
self._types = [ShadingCoefficient.time_convert, float]
class CloudCoverFraction(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(CloudCoverFraction, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "CCF"
self._header = ["time", "cloud cover fraction"]
self._types = [CloudCoverFraction.time_convert, float]
+113
View File
@@ -23,6 +23,8 @@ import tempfile
from Model.Status import StudyStatus
from Model.Study import Study
from Model.River import River
from Model.LateralContribution.LateralContributionTypes import LateralContrib
from Model.HydraulicStructures.Basic.Types import DischargeWeir
class StudyTestCase(unittest.TestCase):
@@ -62,6 +64,31 @@ class StudyTestCase(unittest.TestCase):
study = Study.new("foo", "bar")
self.assertNotEqual(study.river, None)
def test_save_open_study_weather_parameters_default(self):
study = Study.new("foo", "bar")
defaults = study.river.weather_parameters.default
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
dir = tempfile.mkdtemp()
f = os.path.join(dir, "foo.pamhyr")
study.filename = f
study.save()
study.close()
study = Study.open(f)
defaults = study.river.weather_parameters.default
self.assertEqual(len(defaults), 9)
self.assertEqual(defaults[0].type, "AT")
self.assertEqual(defaults[0].value, 12.5)
class StudyScenarioTestCase(unittest.TestCase):
def test_create_study(self):
@@ -262,3 +289,89 @@ class RiverTestCase(unittest.TestCase):
edges = river.edges()
self.assertEqual(edges[0], e0)
self.assertEqual(edges[1], e1)
def test_split_reach_splits_initial_conditions(self):
status = StudyStatus()
river = River(status=status)
node1 = river.add_node()
node2 = river.add_node(x=40.0)
reach = river.add_edge(node1, node2)
profiles = []
for index in range(5):
profile = reach.reach.insert(index)
profile.rk = index * 10.0
profile.insert(0)
profiles.append(profile)
initial_conditions = river.initial_conditions.get(reach)
for index, profile in enumerate(profiles):
data = initial_conditions.new_from_data(
profile.rk,
discharge=100.0 + index,
elevation=10.0 + index,
)
initial_conditions.insert(index, data)
reach1, reach2 = river._split_reach(reach, profiles[2])
conditions1 = river.initial_conditions.get(reach1).data
conditions2 = river.initial_conditions.get(reach2).data
self.assertEqual(
[data["discharge"] for data in conditions1],
[100.0, 101.0, 102.0]
)
self.assertEqual(
[data["discharge"] for data in conditions2],
[102.0, 103.0, 104.0]
)
self.assertEqual(
[data["section"] for data in conditions1],
reach1.reach.profiles
)
self.assertEqual(
[data["section"] for data in conditions2],
reach2.reach.profiles
)
self.assertTrue(all(
data._reach is reach1 for data in conditions1
))
self.assertTrue(all(
data._reach is reach2 for data in conditions2
))
def test_split_reach_keeps_partial_initial_conditions_in_scope(self):
status = StudyStatus()
river = River(status=status)
node1 = river.add_node()
node2 = river.add_node(x=40.0)
reach = river.add_edge(node1, node2)
profiles = []
for index in range(5):
profile = reach.reach.insert(index)
profile.rk = index * 10.0
profile.insert(0)
profiles.append(profile)
initial_conditions = river.initial_conditions.get(reach)
for index in (1, 3):
data = initial_conditions.new_from_data(
profiles[index].rk,
discharge=100.0 + index,
elevation=10.0 + index,
)
initial_conditions.insert(len(initial_conditions.data), data)
reach1, reach2 = river._split_reach(reach, profiles[2])
self.assertEqual(
[data["discharge"]
for data in river.initial_conditions.get(reach1).data],
[101.0]
)
self.assertEqual(
[data["discharge"]
for data in river.initial_conditions.get(reach2).data],
[103.0]
)
-2
View File
@@ -55,7 +55,6 @@ class Modules(IterableFlag):
RESERVOIR = auto()
SEDIMENT_LAYER = auto()
ADDITIONAL_FILES = auto()
OUTPUT_RK = auto()
GEOTIFF = auto()
# Results
@@ -81,7 +80,6 @@ class Modules(IterableFlag):
cls.ADDITIONAL_FILES,
cls.RESULTS,
cls.WINDOW_LIST,
cls.OUTPUT_RK,
cls.GEOTIFF
]
+2 -1
View File
@@ -75,7 +75,8 @@ class ScriptRun(AScript):
os.path.dirname(study.filename),
"_PAMHYR_",
study.name.replace(" ", "_"),
solver.name.replace(" ", "_")
solver.name.replace(" ", "_"),
study.status.scenario.workdir(),
)
os.makedirs(workdir, exist_ok=True)
logger.info(f"Set working dir to {workdir}")
+1 -1
View File
@@ -79,7 +79,7 @@ class AbstractSolver(object):
def default_parameters(cls):
lst = [
("all_init_time", "000:00:00:00"),
("all_final_time", "999:99:00:00"),
("all_final_time", "10:00:00:00"),
("all_timestep", "300.0"),
]
+48 -60
View File
@@ -35,10 +35,6 @@ from Solver.CommandLine import CommandLineSolver
from Model.Results.ResultsAdisTS import Results
from Model.Results.River.River import River, Reach, Profile
from Checker.Adists import (
AdistsOutputRKChecker,
)
from itertools import chain
logger = logging.getLogger()
@@ -90,11 +86,7 @@ class AdisTS(CommandLineSolver):
@classmethod
def checkers(cls):
lst = [
AdistsOutputRKChecker(),
]
return lst
return []
##########
# Export #
@@ -117,9 +109,6 @@ class AdisTS(CommandLineSolver):
_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
@@ -182,7 +171,7 @@ class AdisTS(CommandLineSolver):
# Write header
edges = study.river.enable_edges()
for edge in edges:
name = f"Reach_{edge.id + 1:>3}".replace(" ", "0")
name = self.get_reach_name(edge)
with adists_file_open(
os.path.join(repertory, "net", f"{name}.ST"),
@@ -191,7 +180,7 @@ class AdisTS(CommandLineSolver):
files.append(str(os.path.join("net", f"{name}.ST")))
cnt_num = 1
for profile in edge.reach.profiles:
for profile in edge.reach.enabled_profiles:
self._export_ST_profile_header(
f, files, profile, cnt_num
)
@@ -277,7 +266,7 @@ class AdisTS(CommandLineSolver):
edges = study.river.enable_edges()
for e in edges:
name = f"Reach_{e.id + 1:>3}".replace(" ", "0")
name = self.get_reach_name(e)
id = name
n1 = f"{self.get_node_name(e.node1):3}".replace(" ", "x")
@@ -346,7 +335,7 @@ class AdisTS(CommandLineSolver):
return coeff.minor, coeff.medium
for j, profile in enumerate(edge.reach.profiles):
for j, profile in enumerate(edge.reach.enabled_profiles):
coef_min, coef_moy = get_stricklers_from_rk(profile.rk,
lst)
f.write(
@@ -366,13 +355,19 @@ class AdisTS(CommandLineSolver):
if qlog is not None:
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
with adists_file_open(
os.path.join(
repertory, f"{name}.REP"
), "w+"
) 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"REP {path}.REP\n")
@@ -387,9 +382,7 @@ class AdisTS(CommandLineSolver):
# 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,
self._export_fake_INI(study, mage_path,
qlog, name=name)
# path_mage_net = os.path.join(os.path.abspath(
@@ -453,7 +446,8 @@ class AdisTS(CommandLineSolver):
_nodes_views = set()
def get_reach_name(self, reach):
return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0")
index = self._study.river.get_edge_id(reach) + 1
return f"Reach_{index:03d}"
#################################
# Adis-TS in weak coupling mode #
@@ -507,12 +501,13 @@ class AdisTSwc(AdisTS):
study, pollutant._data, f, qlog
)
POL_ICs = next(filter(
lambda ic: ic.pollutant == pollutant.id,
POL_ICs = next((
ic for ic in
study.river.ic_adists.Initial_Conditions_List
))
if ic.pollutant == pollutant.id
), None)
if POL_ICs.concentration is not None:
if POL_ICs is not None and POL_ICs.concentration is not None:
f.write(f"file_ini = {name}.INI\n")
self._export_ICs_AdisTS(
study, repertory, POL_ICs, qlog, name
@@ -552,15 +547,18 @@ class AdisTSwc(AdisTS):
os.path.join(repertory, f"{POL_name}.ALD"), "w+"
) as f:
for LC in POL_LC:
reach = next(filter(
lambda edge: edge.id == LC.edge, study.river.enable_edges()
)) # .name
reach = next((
edge for edge in study.river.enable_edges()
if edge.id == LC.reach
), None)
if reach is None:
continue
reach_name = self.get_reach_name(reach)
f.write(f"${reach_name} {LC.begin_rk} {LC.end_rk}\n")
f.write(f"*temps |débit massique (kg/s)\n")
f.write(f"*---------++++++++++\n")
for LC_data in LC._data:
for LC_data in LC.data:
tmp = timestamp_to_old_pamhyr_date_adists(int(LC_data[0]))
f.write(" ".join((f"{tmp}",
f"{LC_data[1]}\n")))
@@ -577,9 +575,10 @@ class AdisTSwc(AdisTS):
repertory, f"{POL_name}.CDT"), "w+"
) as f:
for BC in POL_BC:
node = next(filter(
lambda x: x.id == BC.node, study.river._nodes
))
node = next((node for node in study.river.nodes()
if node.id == BC.node), None)
if node is None:
continue
f.write(f"${self.get_node_name(node)}\n")
if BC.type == "Concentration":
@@ -591,7 +590,7 @@ class AdisTSwc(AdisTS):
f.write(f"*JJ:HH:MM | (kg/s)\n")
f.write(f"*---------++++++++++\n")
for BC_data in BC._data:
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")))
@@ -625,6 +624,9 @@ class AdisTSwc(AdisTS):
edges = study.river.enable_edges()
for ic_spec in pol_ics_spec_data:
if ic_spec.is_deleted():
continue
id_reach = ic_spec.reach
reach = next((x for x in edges if x.id == id_reach), None)
if reach is None:
@@ -648,7 +650,9 @@ class AdisTSwc(AdisTS):
list_characteristics = ["type", "diametre", "rho", "porosity",
"cdc_riv", "cdc_cas", "apd", "ac", "bc"]
if len(list_characteristics) <= (len(pol_data[0])):
pol_data = [data for data in pol_data if not data.is_deleted()]
if pol_data and len(list_characteristics) <= len(pol_data[0]):
for i in range(len(list_characteristics)):
f.write(f"{list_characteristics[i]} = {pol_data[0][i]}\n")
@@ -677,6 +681,9 @@ class AdisTSwc(AdisTS):
def _export_d90_spec(self, study, d90_spec_data, f, qlog, name="0"):
for d90_spec in d90_spec_data:
if d90_spec.is_deleted():
continue
if (d90_spec.name is None
or d90_spec.reach is None
or d90_spec.start_rk is None
@@ -736,6 +743,9 @@ class AdisTSwc(AdisTS):
def _export_dif_spec(self, study, dif_spec_data, f, qlog, name="0"):
for dif_spec in dif_spec_data:
if dif_spec.is_deleted():
continue
if (dif_spec.reach is None
or dif_spec.start_rk is None
or dif_spec.end_rk is None
@@ -803,33 +813,8 @@ class AdisTSwc(AdisTS):
if name != "command_line_arguments":
f.write(f"{dict_names[name]} = {value}\n")
outputrks = study.river.Output_rk_adists.OutputRK_List
for outputrk in outputrks:
self._export_outputrk(study, outputrk, f, qlog)
return files
def _export_outputrk(self, study, outputrk, f, qlog, name="0"):
if (outputrk.reach is None
or outputrk.rk is None
or outputrk.title is None):
return
edges = study.river.enable_edges()
id_edges = list(map(lambda x: x.id, edges))
id_reach = outputrk.reach
reach = next((x for x in edges if x.id == id_reach), None)
rk = outputrk.rk
title = outputrk.title
if reach is None:
return
f.write(f"output = {study.river.get_edge_id(reach)+1} {rk} {title}\n")
@timer
def read_bin(self, study, repertory, results, qlog=None, name="0"):
@@ -876,7 +861,10 @@ class AdisTSwc(AdisTS):
for i in range(ibmax):
# Add results reach to reach list
r = results.river.add(i)
geometry_reach = study.river.enable_edges()[i].reach
r = results.river.add(
i, profiles=geometry_reach.enabled_profiles
)
reachs.append(r)
is1[i] = data[2 * i] - 1 # first section of reach i
+1042
View File
File diff suppressed because it is too large. Load diff
+27 -13
View File
@@ -65,6 +65,8 @@ class Mage(CommandLineSolver):
self._cmd_solver = "@path @input -o @output"
self._cmd_output = ""
self._reset_node_names()
@classmethod
def default_parameters(cls):
lst = super(Mage, cls).default_parameters()
@@ -125,12 +127,15 @@ class Mage(CommandLineSolver):
_l_alph = len(_alph)
_nodes_cnt = 0
_nodes_names = {}
_nodes_views = set()
def _reset_node_names(self):
"""Reset the node-name registry for a new export."""
self._nodes_cnt = 0
self._nodes_names = {}
self._nodes_views = set()
def get_reach_name(self, reach):
return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0")
index = self._study.river.get_edge_id(reach) + 1
return f"Reach_{index:03d}"
def get_node_name(self, node):
"""Generate a 3 char name for node
@@ -210,7 +215,7 @@ class Mage(CommandLineSolver):
# Write header
edges = study.river.enable_edges()
for edge in edges:
name = f"Reach_{edge.pamhyr_id:>3}".replace(" ", "0")
name = self.get_reach_name(edge)
with mage_file_open(
os.path.join(repertory, "net", f"{name}.ST"),
@@ -219,7 +224,7 @@ class Mage(CommandLineSolver):
files.append(str(os.path.join("net", f"{name}.ST")))
cnt_num = 1
for profile in edge.reach.profiles:
for profile in edge.reach.enabled_profiles:
self._export_ST_profile_header(
f, files, profile, cnt_num
)
@@ -407,7 +412,7 @@ class Mage(CommandLineSolver):
# name = f"Reach_{lateral.reach.id + 1:>3}".replace(" ", "0")
f.write(f"* {lateral.reach.name} ({name}) {lateral.lctype}\n")
f.write(
f"${name} " +
f"${name} " +
f"{lateral.begin_rk:>10.4f} {lateral.end_rk:>10.4f}\n"
)
header = lateral.header
@@ -484,7 +489,12 @@ class Mage(CommandLineSolver):
if cond.is_deleted():
continue
data = cond.data
enabled_profiles = set(reach.reach.enabled_profiles)
data = [
item for item in cond.data
if not item.is_deleted()
and item["section"] in enabled_profiles
]
if len(data) == 0:
continue
@@ -492,9 +502,6 @@ class Mage(CommandLineSolver):
id_sec = 1
for d in data:
if d.is_deleted():
continue
IR = f"{id}"
IS = f"{id_sec}"
discharge = f"{d['discharge']:>10.5f}"
@@ -1004,6 +1011,7 @@ class Mage8(Mage):
@timer
def export(self, study, repertory, qlog=None, name="0"):
self._study = study
self._reset_node_names()
name = study.name.replace(" ", "_")
# Define GRA file name
@@ -1091,7 +1099,10 @@ class Mage8(Mage):
for i in range(nb_reach):
# Add results reach to reach list
r = results.river.add(i)
geometry_reach = study.river.enable_edges()[i].reach
r = results.river.add(
i, profiles=geometry_reach.enabled_profiles
)
reachs.append(r)
# ID of first and last reach profiles
@@ -1396,7 +1407,10 @@ class Mage8(Mage):
zfd_lst = []
for r in reachs:
z_min = r.geometry.get_z_min()
z_min = [
profile.geometry.z_min()
for profile in r.profiles
]
sls = map(
lambda p: p.get_ts_key(ts_list[0], "sl")[0],
r.profiles
+3
View File
@@ -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,
}
+1 -1
View File
@@ -54,7 +54,7 @@ class Plot(PamhyrPlot):
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
self._autoscale_update = True
def custom_ticks(self):
if self.data.header[0] != "time":
+32 -10
View File
@@ -143,21 +143,43 @@ class TableModel(PamhyrTableModel):
self.update()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
if not rows:
return
rows = list(map(
lambda r: self.get_true_data_row(r),
rows))
rows = sorted(set(rows))
rows = [
self.get_true_data_row(row)
for row in rows
]
self._undo.push(
DelCommand(
self._data, rows
# A multiple selection may contain non-contiguous rows. A model reset
# accurately represents this atomic undo command, unlike one
# beginRemoveRows() call spanning rows that are not all removed.
self.beginResetModel()
try:
self._undo.push(
DelCommand(
self._data, rows
)
)
)
self.endRemoveRows()
finally:
self.endResetModel()
self.update()
def undo(self):
self.beginResetModel()
try:
self._undo.undo()
finally:
self.endResetModel()
def redo(self):
self.beginResetModel()
try:
self._undo.redo()
finally:
self.endResetModel()
def sort(self, _reverse, parent=QModelIndex()):
self.layoutAboutToBeChanged.emit()
+13 -18
View File
@@ -97,12 +97,11 @@ class DelCommand(QUndoCommand):
QUndoCommand.__init__(self)
self._data = data
self._rows = rows
self._bc = []
for row in rows:
self._bc.append(self._data.get_i(row))
self._bc.sort()
self._rows = sorted(set(rows))
self._bc = [
self._data.get_i(row)
for row in self._rows
]
def undo(self):
for el in self._bc:
@@ -120,13 +119,17 @@ class SortCommand(QUndoCommand):
self._data = data
self._reverse = _reverse
self._old = self._data.data
self._indexes = None
# Keep the order of the complete underlying list, including soft
# deleted items. ``data`` only exposes visible items and therefore
# cannot reliably restore a sort after a delete/undelete operation.
self._old_positions = {
id(item): index
for index, item in enumerate(self._data._data)
}
def undo(self):
ll = self._data.data
self._data.sort(
key=lambda x: self._indexes[ll.index(x)]
key=lambda item: self._old_positions[id(item)]
)
def redo(self):
@@ -134,14 +137,6 @@ class SortCommand(QUndoCommand):
_reverse=self._reverse,
key=lambda x: x[0]
)
if self._indexes is None:
self._indexes = list(
map(
lambda p: self._old.index(p),
self._data.data
)
)
self._old = None
class MoveCommand(QUndoCommand):
@@ -155,6 +155,7 @@ class EditBoundaryConditionWindow(PamhyrWindow):
def update(self):
self.plot.update()
self.plot.fit_to_data()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
@@ -0,0 +1,105 @@
# Plot.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from datetime import datetime
from tools import timer, trace
from View.Tools.PamhyrPlot import PamhyrPlot
from PyQt5.QtCore import (
QCoreApplication
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class Plot(PamhyrPlot):
def __init__(self, mode="time", data=None,
trad=None, canvas=None, toolbar=None,
parent=None):
super(Plot, self).__init__(
canvas=canvas,
trad=trad,
data=data,
toolbar=toolbar,
parent=parent
)
self._table_headers = self._trad.get_dict("table_headers")
header = self.data.header
self.label_x = self._table_headers[header[0]]
self.label_y = self._table_headers[header[1]]
self._mode = mode
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
def custom_ticks(self):
if self.data.header[0] != "time":
return
self.set_ticks_time_formater()
@timer
def draw(self):
self.init_axes()
if len(self.data) == 0:
self._init = False
return
self.draw_data()
self.custom_ticks()
self.idle()
self._init = True
def draw_data(self):
# Plot data
x = list(map(lambda v: v[0], self.data.data))
y = list(map(lambda v: v[1], self.data.data))
self._line, = self.canvas.axes.plot(
x, y,
color=self.color_plot,
**self.plot_default_kargs
)
@timer
def update(self, ind=None):
if not self._init:
self.draw()
return
self.update_data()
self.update_idle()
def update_data(self):
x = list(map(lambda v: v[0], self.data.data))
y = list(map(lambda v: v[1], self.data.data))
self._line.set_data(x, y)
@@ -0,0 +1,185 @@
# Table.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
)
from View.Tools.PamhyrTable import PamhyrTableModel
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QTime, QDateTime,
)
from PyQt5.QtWidgets import (
QTableView, QAbstractItemView, QSpinBox,
QTimeEdit, QDateTimeEdit, QItemDelegate,
)
from View.BoundaryConditionsTemperature.Edit.UndoCommand import (
AddCommand, DelCommand, SetDataCommand, PasteCommand, SortCommand,
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class TableModel(PamhyrTableModel):
def get_true_data_row(self, row):
if len(self._data.data) > 0:
bc = self._data.data[row]
else:
return 0
return next(
map(
lambda e: e[0],
filter(
lambda e: e[1] == bc,
enumerate(self._data._data)
)
), 0
)
def data(self, index, role):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter | Qt.AlignVCenter
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
value = QVariant()
if 0 <= column < 2:
row = self.get_true_data_row(row)
v = self._data._data[row][column]
if self._data._types[column] == float:
value = f"{v:.2f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = str(datetime.fromtimestamp(v))
else:
value = f"{v}"
return value
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
try:
row = self.get_true_data_row(row)
self._undo.push(
SetDataCommand(
self._data, row, column, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
self._undo.push(
AddCommand(
self._data, row
)
)
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
_rows = list(
map(
lambda r: self.get_true_data_row(r),
rows
)
)
self._undo.push(
DelCommand(
self._data, _rows
)
)
self.endRemoveRows()
self.layoutChanged.emit()
def sort(self, _reverse, parent=QModelIndex()):
self.layoutAboutToBeChanged.emit()
self._undo.push(
SortCommand(
self._data, _reverse
)
)
self.layoutAboutToBeChanged.emit()
self.update()
def paste(self, row, header, data):
if len(data) == 0:
return
self.layoutAboutToBeChanged.emit()
self._undo.push(
PasteCommand(
self._data, row,
list(
map(
lambda d: self._data.new_from_data(header, d),
data
)
)
)
)
self.layoutAboutToBeChanged.emit()
self.update()
def update(self):
# self.auto_sort()
self.layoutChanged.emit()
@@ -0,0 +1,137 @@
# UndoCommand.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
logger = logging.getLogger()
class SetDataCommand(QUndoCommand):
def __init__(self, data, index, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._index = index
self._column = column
self._old = self._data.get_i(self._index)[self._column]
self._new = new_value
def undo(self):
self._data._set_i_c_v(self._index, self._column, self._old)
def redo(self):
self._data._set_i_c_v(self._index, self._column, self._new)
class AddCommand(QUndoCommand):
def __init__(self, data, index):
QUndoCommand.__init__(self)
self._data = data
self._index = index
self._new = None
def undo(self):
del self._data._data[self._index]
def redo(self):
if self._new is None:
self._new = self._data.add(self._index)
else:
self._data.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, data, rows):
QUndoCommand.__init__(self)
self._data = data
self._rows = rows
self._bc = []
for row in rows:
self._bc.append((row, self._data._data[row]))
self._bc.sort()
def undo(self):
for row, el in self._bc:
el.set_as_not_deleted()
def redo(self):
for row, el in self._bc:
el.set_as_deleted()
class PasteCommand(QUndoCommand):
def __init__(self, data, row, bcs):
QUndoCommand.__init__(self)
self._data = data
self._row = row
self._bcs = bcs
self._bcs.reverse()
def undo(self):
self._data.delete_i(
range(self._row, self._row + len(self._bcs))
)
def redo(self):
for bc in self._bcs:
self._data.insert(self._row, bc)
class SortCommand(QUndoCommand):
def __init__(self, data, _reverse):
QUndoCommand.__init__(self)
self._data = data
self._reverse = _reverse
self._old = self._data.data
self._indexes = None
def undo(self):
ll = self._data.data
self._data.sort(
key=lambda x: self._indexes[ll.index(x)]
)
def redo(self):
self._data.sort(
_reverse=self._reverse,
key=lambda x: x[0]
)
if self._indexes is None:
self._indexes = list(
map(
lambda p: self._old.index(p),
self._data.data
)
)
self._old = None
@@ -0,0 +1,230 @@
# Window.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import timer, trace
from View.Tools.PamhyrWindow import PamhyrWindow
from View.Tools.PamhyrWidget import PamhyrWidget
from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtCore
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel, QCoreApplication,
pyqtSlot, pyqtSignal,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QHeaderView, QDoubleSpinBox, QVBoxLayout,
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.BoundaryConditionsTemperature.Edit.translate import BCETranslate
from View.BoundaryConditionsTemperature.Edit.Table import TableModel
from View.BoundaryConditionsTemperature.Edit.Plot import Plot
_translate = QCoreApplication.translate
logger = logging.getLogger()
class EditBoundaryConditionWindow(PamhyrWindow):
_pamhyr_ui = "EditBoundaryConditionsTemperature"
_pamhyr_name = "Edit Boundary Conditions Temperature"
def __init__(self, data=None, study=None, config=None, parent=None):
self._data = data
trad = BCETranslate()
name = trad[self._pamhyr_name]
if self._data is not None:
n = self._data.node
node_name = next(filter(
lambda x: x.id == n, study.river._nodes
)).name
name += (
f" - {study.name} " +
f"({node_name})"
)
super(EditBoundaryConditionWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._hash_data.append(data)
self.setup_table()
self.setup_plot()
self.setup_connections()
def setup_table(self):
table_headers = self._trad.get_dict("table_headers")
if self._study.is_editable():
editable_headers = self._trad.get_dict("table_headers")
else:
editable_headers = []
self._data.header = ["time", "temperature"]
headers = {}
for h in self._data.header:
headers[h] = table_headers[h]
self._delegate_time = PamhyrExTimeDelegate(
data=self._data,
mode=self._study.time_system,
parent=self
)
table = self.find(QTableView, "tableView")
self._table = TableModel(
table_view=table,
table_headers=headers,
editable_headers=editable_headers,
delegates={
# "time": self._delegate_time,
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_plot(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self
)
self.verticalLayout.addWidget(self.toolbar)
self.verticalLayout.addWidget(self.canvas)
self.plot = Plot(
canvas=self.canvas,
data=self._data,
mode=self._study.time_system,
trad=self._trad,
toolbar=self.toolbar,
)
self.plot.draw()
def setup_connections(self):
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_sort").triggered.connect(self.sort)
self._table.dataChanged.connect(self.update)
def update(self):
self.plot.update()
self.plot.fit_to_data()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
return table.selectionModel()\
.selectedRows()[0]\
.row()
def index_selected_rows(self):
table = self.find(QTableView, "tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._data) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
self.plot.update()
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
self.plot.update()
def sort(self):
self._table.sort(False)
self.plot.update()
def _copy(self):
rows = self.index_selected_rows()
table = []
table.append(self._data.header)
data = self._data.data
for row in rows:
table.append(list(data[row]))
self.copyTableIntoClipboard(table)
def _paste(self):
header, data = self.parseClipboardTable()
logger.debug(f"paste: h:{header}, d:{data}")
if len(data) == 0:
return
row = 0
rows = self.index_selected_rows()
if len(rows) != 0:
row = rows[0]
self._table.paste(row, header, data)
self.plot.update()
def _undo(self):
self._table.undo()
self.plot.update()
def _redo(self):
self._table.redo()
self.plot.update()
@@ -1,5 +1,5 @@
# translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
# 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
@@ -20,21 +20,21 @@ from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
from View.BoundaryCondition.translate import BCTranslate
_translate = QCoreApplication.translate
class OutputRKAdisTSTranslate(MainTranslate):
class BCETranslate(BCTranslate):
def __init__(self):
super(OutputRKAdisTSTranslate, self).__init__()
super(BCETranslate, self).__init__()
self._dict["Output RK"] = _translate(
"OutputRKAdisTS", "Output RK"
self._dict["Edit Boundary Conditions Temperature"] = _translate(
"BoundaryConditionsTemperature",
"Edit boundary conditions Temperature"
)
self._dict["x"] = _translate("OutputRKAdisTS", "X (m)")
self._sub_dict["table_headers"] = {
"title": self._dict["title"],
"reach": self._dict["reach"],
"rk": self._dict["unit_rk"],
"time": self._dict["time"],
"temperature": self._dict["unit_temperature"],
}
@@ -1,5 +1,5 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
# 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
@@ -36,60 +36,36 @@ from PyQt5.QtWidgets import (
from View.Tools.PamhyrTable import PamhyrTableModel
from View.OutputRKAdisTS.UndoCommand import (
SetTitleCommand, SetReachCommand, SetRKCommand,
SetEnabledCommand, AddCommand, DelCommand,
from View.BoundaryConditionsTemperature.UndoCommand import (
SetNodeCommand, SetNameCommand,
AddCommand, DelCommand
)
from functools import reduce
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, trad=None, parent=None, mode="reaches"):
def __init__(self, data=None, mode="node", tab="",
trad=None, parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._trad = trad
self._mode = mode
self._tab = tab
self._trad = trad
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
val = []
if self._mode == "rk":
reach_id = self._data.Output_rk_adists \
.get(index.row()) \
.reach
if reach_id is not None:
reach = next(
filter(
lambda edge: edge.id == reach_id, self._data.edges()
)
)
val = list(
map(
lambda rk: str(rk), reach.reach.get_rk()
)
)
else:
val = list(
map(
lambda n: n.name, self._data.edges()
)
if self._mode == "node":
self.editor.addItems(
[self._trad["not_associated"]] +
self._data.nodes_names()
)
self.editor.addItems(
[self._trad['not_associated']] +
val
)
self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
self.editor.setCurrentText(index.data(Qt.DisplayRole))
return self.editor
def setEditorData(self, editor, index):
@@ -115,10 +91,27 @@ class ComboBoxDelegate(QItemDelegate):
class TableModel(PamhyrTableModel):
def _setup_lst(self):
self._lst = self._data._Output_rk_adists
def __init__(self, bc_list=None, trad=None, **kwargs):
self._trad = trad
self._bc_list = bc_list
def rowCount(self, parent):
super(TableModel, self).__init__(trad=trad, **kwargs)
def _setup_lst(self):
self._lst = self._bc_list.lst
def get(self, row):
if row < 0 or row >= len(self._lst):
return None
return self._lst[row]
def _global_row(self, row):
bc = self.get(row)
if bc is None:
return None
return self._bc_list.index(bc)
def rowCount(self, parent=QModelIndex()):
return len(self._lst)
def data(self, index, role):
@@ -128,20 +121,20 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
if self._headers[column] == "title":
return self._lst.get(row).title
elif self._headers[column] == "reach":
n = self._lst.get(row).reach
if n is None:
return self._trad['not_associated']
return next(filter(
lambda edge: edge.id == n, self._data.edges())
).name
elif self._headers[column] == "rk":
n = self._lst.get(row).rk
if n is None:
return self._trad['not_associated']
if self._headers[column] == "name":
n = self._lst[row].name
if n is None or n == "":
return self._trad["default"]
return n
elif self._headers[column] == "node":
n = self._lst[row].node
if n is None:
return self._trad["not_associated"]
tmp = next(filter(lambda x: x.id == n, self._data._nodes), None)
if tmp is not None:
return tmp.name
else:
return self._trad["not_associated"]
return QVariant()
@@ -154,76 +147,79 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
na = self._trad['not_associated']
try:
if self._headers[column] == "title":
if self._headers[column] == "name":
global_row = self._global_row(row)
self._undo.push(
SetTitleCommand(
self._lst, row, value
SetNameCommand(
self._bc_list, global_row, value
)
)
elif self._headers[column] == "node":
global_row = self._global_row(row)
node = next(
filter(lambda n: n.name == value, self._data.nodes()),
None
)
self._undo.push(
SetNodeCommand(
self._bc_list, global_row, node
)
)
elif self._headers[column] == "reach":
if value == na:
value = None
self._undo.push(
SetReachCommand(
self._lst, row, self._data.edge(value)
)
)
elif self._headers[column] == "rk":
if value == na:
value = None
self._undo.push(
SetRKCommand(
self._lst, row, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
if self._headers[column] == "node" and self.rowCount() > 0:
self.dataChanged.emit(
self.index(0, column),
self.index(self.rowCount() - 1, column)
)
else:
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
row = len(self._lst)
self.beginInsertRows(parent, row, row)
self._undo.push(
AddCommand(
self._lst, row
self._bc_list, len(self._bc_list)
)
)
self._setup_lst()
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
row_by_bc = {
id(bc): row for row, bc in enumerate(self._bc_list._lst)
}
global_rows = [
row_by_bc[id(self._lst[row])]
for row in rows
if 0 <= row < len(self._lst)
]
self._undo.push(
DelCommand(
self._lst, rows
self._bc_list, global_rows
)
)
self._setup_lst()
self.endRemoveRows()
self.layoutChanged.emit()
def enabled(self, row, enabled, parent=QModelIndex()):
self._undo.push(
SetEnabledCommand(
self._lst, row, enabled
)
)
self.layoutChanged.emit()
def undo(self):
self._undo.undo()
self._setup_lst()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self._setup_lst()
self.layoutChanged.emit()
@@ -0,0 +1,111 @@
# UndoCommand.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
class SetNodeCommand(QUndoCommand):
def __init__(self, bcs, index, node):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._old = self._bcs.get(self._index).node
self._new = node.id if node is not None else None
self._previous = next(
(
bc for i, bc in enumerate(self._bcs.lst)
if i != self._index and bc.node == self._new
),
None
)
def undo(self):
self._bcs.get(self._index).node = self._old
if self._previous is not None:
self._previous.node = self._new
def redo(self):
self._bcs.get(self._index).node = self._new
if self._previous is not None:
self._previous.node = None
class SetNameCommand(QUndoCommand):
def __init__(self, bcs, index, name):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._name = name
self._old = self._bcs.get(self._index).name
self._new = self._name
def undo(self):
self._bcs.get(self._index).name = self._old
def redo(self):
self._bcs.get(self._index).name = self._new
class AddCommand(QUndoCommand):
def __init__(self, bcs, index):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._new = None
def undo(self):
self._bcs.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._bcs.new(self._index)
else:
self._bcs.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, bcs, rows):
QUndoCommand.__init__(self)
self._bcs = bcs
self._rows = rows
self._bc = []
for row in rows:
self._bc.append((row, self._bcs._lst[row]))
self._bc.sort()
def undo(self):
for row, el in self._bc:
self._bcs.insert(row, el)
def redo(self):
self._bcs.delete_i(self._rows)
@@ -0,0 +1,192 @@
# Window.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QWidget,
)
from View.BoundaryConditionsTemperature.Table import (
TableModel, ComboBoxDelegate
)
from View.Network.GraphWidget import GraphWidget
from View.BoundaryConditionsTemperature.translate import BCTemperatureTranslate
from View.BoundaryConditionsTemperature.Edit.Window \
import EditBoundaryConditionWindow
_translate = QCoreApplication.translate
logger = logging.getLogger()
class BoundaryConditionsTemperatureWindow(PamhyrWindow):
_pamhyr_ui = "BoundaryConditionsTemperature"
_pamhyr_name = "Boundary conditions temperature"
def __init__(self, data=None, study=None,
config=None, parent=None):
self._data = data
trad = BCTemperatureTranslate()
name = (
trad[self._pamhyr_name] +
" - " + study.name
)
super(BoundaryConditionsTemperatureWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._bcs = self._study.river.boundary_conditions_temperature
self.setup_graph()
self.setup_table()
self.setup_connections()
def setup_table(self):
self._delegate_node = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
mode="node",
parent=self
)
if self._study.is_editable():
editable_headers = self._trad.get_dict("table_headers")
else:
editable_headers = []
table = self.find(QTableView, f"tableView")
self._table = TableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates={
"node": self._delegate_node,
},
trad=self._trad,
bc_list=self._study.river.boundary_conditions_temperature,
undo=self._undo_stack,
data=self._study.river
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_graph(self):
self.graph_widget = GraphWidget(
self._study.river,
min_size=None, size=(200, 200),
only_display=True,
parent=self
)
self.graph_layout = self.find(QVBoxLayout, "verticalLayout")
self.graph_layout.addWidget(self.graph_widget)
def setup_connections(self):
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
def index_selected_row(self):
table = self.find(QTableView, f"tableView")
return table.selectionModel()\
.selectedRows()[0]\
.row()
def index_selected_rows(self):
table = self.find(QTableView, f"tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
self._table.add(self._table.rowCount())
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _copy(self):
logger.info("TODO: copy")
def _paste(self):
logger.info("TODO: paste")
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def edit(self):
rows = self.index_selected_rows()
for row in rows:
data = self._table.get(row)
if data.node is None:
continue
if self.sub_window_exists(
EditBoundaryConditionWindow,
data=[self._study, None, data]
):
continue
win = EditBoundaryConditionWindow(
data=data,
study=self._study,
parent=self
)
win.show()
@@ -1,5 +1,5 @@
# Adists.py -- Pamhyr study checkers
# Copyright (C) 2023-2026 INRAE
# translate.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
@@ -16,37 +16,25 @@
# -*- coding: utf-8 -*-
import time
import logging
from functools import reduce
from PyQt5.QtCore import QCoreApplication
from Modules import Modules
from Checker.Checker import AbstractModelChecker, STATUS
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
logger = logging.getLogger()
class AdistsOutputRKChecker(AbstractModelChecker):
class BCTemperatureTranslate(MainTranslate):
def __init__(self):
super(AdistsOutputRKChecker, self).__init__()
super(BCTemperatureTranslate, self).__init__()
self._name = _translate("Checker", "AdisTS output RK checker")
self._description = _translate(
"Checker", "Check output RK"
self._dict["Boundary conditions temperature"] = _translate(
"BoundaryConditionsTemperature", "Boundary conditions temperature"
)
self._dict["default"] = _translate(
"BoundaryConditionsTemperature", "default"
)
self._modules = Modules.OUTPUT_RK
def run(self, study):
ok = True
nerror = 0
self._summary = "ok"
self._status = STATUS.OK
if not self.basic_check(study):
return False
return ok
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"node": _translate("BoundaryCondition", "Node"),
}
+12 -3
View File
@@ -53,12 +53,13 @@ class CheckListWindowAdisTS(PamhyrWindow):
def __init__(self, autorun: bool = True,
study=None, config=None,
solver=None, parent=None, mage_rep=None):
solver=None, parent=None, mage_rep=None, mode="adists"):
trad = CheckListTranslate()
self._autorun = autorun
self._solver = solver
self._mage_rep = mage_rep
self._mode = mode
name = trad[self._pamhyr_name] + " - " + study.name
@@ -73,6 +74,8 @@ class CheckListWindowAdisTS(PamhyrWindow):
# Add solver to hash computation data
self._hash_data.append(self._solver)
self._hash_data.append(self._mage_rep)
self._hash_data.append(self._mode)
self._checker_list = (
self._study.checkers() +
@@ -169,7 +172,7 @@ class CheckListWindowAdisTS(PamhyrWindow):
if not errors:
self.find(QPushButton, "pushButton_ok").setEnabled(True)
if self._autorun:
self._parent.solver_log_adists(self._solver, self._mage_rep)
self.open_solver_log()
self.end()
self.update_statusbar()
@@ -196,5 +199,11 @@ class CheckListWindowAdisTS(PamhyrWindow):
self.end()
def accept(self):
self._parent.solver_log_adists(self._solver, self._mage_rep)
self.open_solver_log()
# 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)
+20
View File
@@ -43,6 +43,7 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QComboBox, QUndoStack, QShortcut, QHeaderView,
QCheckBox,
)
from Modules import Modules
@@ -119,11 +120,17 @@ class ConfigureWindow(PamhyrDialog):
editable_headers=["name", "comment", "minor", "medium"],
data=self._stricklers,
undo=self._undo_stack,
trad=self._trad_stricklers,
display_manning=self._config.display_manning,
unique_names=True,
)
def setup_data(self):
# Backup
self.set_check_box("checkBox_backup", self._config.backup_enable)
self.set_check_box(
"checkBox_display_manning", self._config.display_manning
)
# self.set_line_edit_text("lineEdit_backup_path",
# self._config.backup_path)
self.set_time_edit("timeEdit_backup_frequence",
@@ -167,6 +174,10 @@ class ConfigureWindow(PamhyrDialog):
for button in buttons:
self.find(QPushButton, button).clicked.connect(buttons[button])
self.find(QCheckBox, "checkBox_display_manning").stateChanged.connect(
self.update_stricklers_display
)
def accept(self):
# Solvers
self._config.solvers = self.solver_table_model._data.copy()
@@ -178,6 +189,9 @@ class ConfigureWindow(PamhyrDialog):
self._config.backup_frequence = self.get_time_edit(
"timeEdit_backup_frequence"
)
self._config.display_manning = self.get_check_box(
"checkBox_display_manning"
)
# self._config.backup_max = self.get_spin_box("spinBox_backup_max")
# Stricklers
@@ -237,6 +251,12 @@ class ConfigureWindow(PamhyrDialog):
# Stricklers
def update_stricklers_display(self):
self._stricklers_table.display_manning = self.get_check_box(
"checkBox_display_manning"
)
self._stricklers_table.layoutChanged.emit()
def index_selected_rows_strikclers(self):
table = self.find(QTableView, f"tableView_stricklers")
return list(
+1 -1
View File
@@ -78,7 +78,7 @@ class ComboBoxDelegate(QItemDelegate):
else:
val = list(
map(
lambda n: n.name, self._data.edges()
lambda n: n.name, self._data.enable_edges()
)
)
+1 -1
View File
@@ -80,7 +80,7 @@ class ComboBoxDelegate(QItemDelegate):
else:
val = list(
map(
lambda n: n.name, self._data.edges()
lambda n: n.name, self._data.enable_edges()
)
)
+13 -4
View File
@@ -20,6 +20,7 @@ import logging
from tools import timer, flatten
from View.Tools.PamhyrPlot import PamhyrPlot
from View.Tools.CoefficientDisplay import display_coefficient
from PyQt5.QtCore import (
QCoreApplication
@@ -32,7 +33,7 @@ _translate = QCoreApplication.translate
class PlotStricklers(PamhyrPlot):
def __init__(self, data=None, canvas=None, trad=None,
toolbar=None, parent=None):
toolbar=None, display_manning=True, parent=None):
super(PlotStricklers, self).__init__(
canvas=canvas,
trad=trad,
@@ -42,7 +43,11 @@ class PlotStricklers(PamhyrPlot):
)
self.label_x = self._trad["rk"]
self.label_y = self._trad["strickler_plot"]
self._display_manning = display_manning
self.label_y = (
self._trad["manning_plot"] if display_manning
else self._trad["strickler_plot"]
)
self.line_rk_elevation = [None, None]
@@ -113,8 +118,12 @@ class PlotStricklers(PamhyrPlot):
)
)
coef_minor = list(map(lambda s: s.minor, coef))
coef_medium = list(map(lambda s: s.medium, coef))
coef_minor = [
display_coefficient(s.minor, self._display_manning) for s in coef
]
coef_medium = [
display_coefficient(s.medium, self._display_manning) for s in coef
]
self.line_rk_elevation[0] = self.canvas.axes.plot(
rk, coef_minor,
+81 -16
View File
@@ -42,6 +42,7 @@ from View.Frictions.UndoCommand import (
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.Tools.CoefficientDisplay import display_strickler
from View.Frictions.translate import *
@@ -51,30 +52,64 @@ _translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, study=None,
mode="stricklers", trad=None, parent=None):
def __init__(self, data=None, study=None, application_stricklers=None,
mode="stricklers", trad=None, display_manning=True,
parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._study = study
self._application_stricklers = application_stricklers or []
self._trad = trad
self._mode = mode
self._display_manning = display_manning
@staticmethod
def same_strickler(first, second):
return (
first.name == second.name
and first.comment == second.comment
and first.minor == second.minor
and first.medium == second.medium
)
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
if self._mode == "stricklers":
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda s: str(s),
self._study.river.stricklers.stricklers
)
self.editor.addItem(self._trad["not_defined"], None)
study_stricklers = self._study.river.stricklers.stricklers
for strickler in study_stricklers:
self.editor.addItem(
display_strickler(strickler, self._display_manning),
strickler
)
application_stricklers = [
application_strickler
for application_strickler in self._application_stricklers
if not any(
self.same_strickler(
application_strickler, study_strickler
)
for study_strickler in study_stricklers
)
]
if application_stricklers:
self.editor.insertSeparator(self.editor.count())
for strickler in application_stricklers:
self.editor.addItem(
display_strickler(
strickler, self._display_manning
),
strickler
)
elif self._mode == "rk":
self.editor.addItems(
list(map(str, self._data.reach.get_rk()))
)
self.editor.setCurrentText(index.data(Qt.DisplayRole))
self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
return self.editor
def setEditorData(self, editor, index):
@@ -82,8 +117,9 @@ class ComboBoxDelegate(QItemDelegate):
self.editor.currentTextChanged.connect(self.currentItemChanged)
def setModelData(self, editor, model, index):
text = str(editor.currentText())
model.setData(index, text)
value = editor.currentData() if self._mode == "stricklers" else \
str(editor.currentText())
model.setData(index, value)
editor.close()
editor.deleteLater()
@@ -100,10 +136,33 @@ class ComboBoxDelegate(QItemDelegate):
class FrictionTableModel(PamhyrTableModel):
def __init__(self, *args, display_manning=False, **kwargs):
self.display_manning = display_manning
super(FrictionTableModel, self).__init__(*args, **kwargs)
def _setup_lst(self):
self._lst = self._data.frictions
self._study = self._opt_data
def study_strickler(self, strickler):
if strickler is None:
return None
study_stricklers = self._study.river.stricklers
if any(s is strickler for s in study_stricklers.stricklers):
return strickler
for current in study_stricklers.stricklers:
if ComboBoxDelegate.same_strickler(current, strickler):
return current
copied = study_stricklers.new(len(study_stricklers))
copied.name = strickler.name
copied.comment = strickler.comment
copied.minor = strickler.minor
copied.medium = strickler.medium
return copied
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
@@ -121,12 +180,12 @@ class FrictionTableModel(PamhyrTableModel):
value = self._lst.get(row).begin_strickler
if value is None:
return self._trad["not_defined"]
return str(value)
return display_strickler(value, self.display_manning)
elif self._headers[column] == "end_strickler":
value = self._lst.get(row).end_strickler
if value is None:
return self._trad["not_defined"]
return str(value)
return display_strickler(value, self.display_manning)
return QVariant()
@@ -160,15 +219,21 @@ class FrictionTableModel(PamhyrTableModel):
)
)
elif self._headers[column] == "begin_strickler":
strickler = value if hasattr(value, "minor") else \
self._study.river.strickler(value)
strickler = self.study_strickler(strickler)
self._undo.push(
SetBeginStricklerCommand(
self._lst, row, self._study.river.strickler(value)
self._lst, row, strickler
)
)
elif self._headers[column] == "end_strickler":
strickler = value if hasattr(value, "minor") else \
self._study.river.strickler(value)
strickler = self.study_strickler(strickler)
self._undo.push(
SetEndStricklerCommand(
self._lst, row, self._study.river.strickler(value)
self._lst, row, strickler
)
)
except Exception as e:
+65 -8
View File
@@ -67,6 +67,9 @@ class FrictionsWindow(PamhyrWindow):
def __init__(self, reach=None, study=None, config=None, parent=None):
trad = FrictionsTranslate()
if config is None and parent is not None:
config = getattr(parent, "conf", None)
if reach is not None:
self._reach = reach
else:
@@ -101,8 +104,20 @@ class FrictionsWindow(PamhyrWindow):
self._delegate_stricklers = ComboBoxDelegate(
data=self._reach,
study=self._study,
application_stricklers=(
self._config.stricklers.stricklers
if self._config is not None else []
),
mode="stricklers",
trad=self._trad,
display_manning=getattr(self._config, "display_manning", False),
parent=self
)
self._delegate_rk = ComboBoxDelegate(
data=self._reach,
study=self._study,
mode="rk",
trad=self._trad,
parent=self
)
@@ -115,18 +130,27 @@ class FrictionsWindow(PamhyrWindow):
editable_headers = []
table = self.find(QTableView, f"tableView")
table_headers = self._trad.get_dict("table_headers").copy()
table_headers["begin_strickler"] = (
self._trad["manning"]
if getattr(self._config, "display_manning", False)
else self._trad["strickler"]
)
self._table = FrictionTableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
table_headers=table_headers,
editable_headers=editable_headers,
delegates={
"begin_rk": self._delegate_rk,
"end_rk": self._delegate_rk,
"begin_strickler": self._delegate_stricklers,
"end_strickler": self._delegate_stricklers,
# "end_strickler": self._delegate_stricklers,
},
data=self._reach,
trad=self._trad,
undo=self._undo_stack,
opt_data=self._study
opt_data=self._study,
display_manning=getattr(self._config, "display_manning", False)
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
@@ -158,7 +182,8 @@ class FrictionsWindow(PamhyrWindow):
canvas=self.canvas_2,
data=self._reach,
trad=self._trad,
toolbar=None
toolbar=None,
display_manning=getattr(self._config, "display_manning", False)
)
self.plot_2.draw()
@@ -198,9 +223,41 @@ class FrictionsWindow(PamhyrWindow):
# self._propagate_update(key=Modules.FRICTION)
def _propagated_update(self, key=Modules(0)):
if Modules.GEOMETRY not in key:
return
if Modules.CONFIG in key:
self.update_config()
if Modules.GEOMETRY in key:
self.update_plot()
for _, window in self.sub_win_list:
window._propagated_update(key=key)
def update_config(self):
display_manning = getattr(
self._config, "display_manning", False
)
self._delegate_stricklers._application_stricklers = (
self._config.stricklers.stricklers
if self._config is not None else []
)
self._delegate_stricklers._display_manning = display_manning
self._table.display_manning = display_manning
self._table._table_headers["begin_strickler"] = (
self._trad["manning"] if display_manning
else self._trad["strickler"]
)
self._table.headerDataChanged.emit(
Qt.Horizontal, 0, self._table.columnCount() - 1
)
self._table.layoutChanged.emit()
self.plot_2._display_manning = display_manning
self.plot_2.label_y = (
self._trad["manning_plot"] if display_manning
else self._trad["strickler_plot"]
)
self.update_plot()
def _set_current_reach(self):
@@ -265,13 +322,13 @@ class FrictionsWindow(PamhyrWindow):
def edit_stricklers(self):
if self.sub_window_exists(
StricklersWindow,
data=[self._study, self.parent.conf]
data=[self._study, self._config]
):
return
strick = StricklersWindow(
study=self._study,
config=self.parent.conf,
config=self._config,
parent=self
)
strick.show()
+9
View File
@@ -31,9 +31,18 @@ class FrictionsTranslate(MainTranslate):
self._dict["strickler_plot"] = _translate(
"Frictions", "Strickler ($m^{1/3}/s$)"
)
self._dict["manning_plot"] = _translate(
"Frictions", "Manning ($s/m^{1/3}$)"
)
self._dict["stricklers"] = _translate(
"Frictions", "Stricklers"
)
self._dict["strickler"] = _translate(
"Frictions", "Strickler"
)
self._dict["manning"] = _translate(
"Frictions", "Manning"
)
self._dict["Edit frictions"] = _translate(
"Frictions", "Edit frictions"
+77
View File
@@ -128,6 +128,15 @@ class ProfileWindow(PamhyrWindow):
self._tablemodel.blockSignals(False)
def setup_connections(self):
navigation_actions = {
"action_previous_profile": self.previous_profile,
"action_next_profile": self.next_profile,
}
for action in navigation_actions:
self.find(QAction, action)\
.triggered.connect(navigation_actions[action])
if self._study.is_read_only():
actions = {}
else:
@@ -152,6 +161,74 @@ class ProfileWindow(PamhyrWindow):
.connect(self.update_points_selection)
self._tablemodel.dataChanged.connect(self.update)
self.update_navigation_actions()
def previous_profile(self):
self.navigate_profile(-1)
def next_profile(self):
self.navigate_profile(1)
def profiles(self):
"""Return profiles from the geometry model owning this profile."""
return self._profile.reach.reach.profiles
def navigate_profile(self, offset):
profiles = self.profiles()
try:
index = profiles.index(self._profile)
except ValueError:
self.update_navigation_actions()
return
target_index = index + offset
if not 0 <= target_index < len(profiles):
self.update_navigation_actions()
return
target = profiles[target_index]
if self._parent is not None and self._parent.sub_window_exists(
ProfileWindow,
data=[self._study, self._config, target]
):
self.close()
return
self.set_profile(target)
def set_profile(self, profile):
self._profile = profile
self._hash_data[-1] = profile
self._undo_stack.clear()
self._tablemodel.beginResetModel()
self._tablemodel._data = profile
self._tablemodel._setup_lst()
self._tablemodel.endResetModel()
self._plot.data = profile
self._plot.highlight = None
self._plot.draw()
self._title = (
self._trad[self._pamhyr_name] +
f" - {profile.name} {profile.rk}"
)
self._set_title()
self.update_navigation_actions()
def update_navigation_actions(self):
profiles = self.profiles()
try:
index = profiles.index(self._profile)
except ValueError:
index = -1
self.find(QAction, "action_previous_profile")\
.setEnabled(index > 0)
self.find(QAction, "action_next_profile")\
.setEnabled(0 <= index < len(profiles) - 1)
def update_points_selection(self):
rows = self.index_selected_rows()
+19 -3
View File
@@ -64,15 +64,20 @@ class GeometryReachTableModel(PamhyrTableModel):
if not index.isValid():
return QVariant()
profile = self._data.profile(index.row())
if role == Qt.BackgroundRole and not profile.is_enabled:
return QColor(190, 190, 190)
if role == Qt.DisplayRole and index.column() == 0:
return self._data.profile(index.row()).name
return profile.name
if role == Qt.DisplayRole and index.column() == 1:
rk = self._data.profile(index.row()).rk
rk = profile.rk
return f"{rk:.4f}"
if role == Qt.DisplayRole and index.column() == 2:
return str(self._data.profile(index.row()).nb_points)
return str(profile.nb_points)
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter | Qt.AlignVCenter
@@ -155,6 +160,14 @@ class GeometryReachTableModel(PamhyrTableModel):
self.endRemoveRows()
self.layoutChanged.emit()
def enabled(self, rows, enabled):
self._undo.push(
SetEnabledCommand(
self._data, rows, enabled
)
)
self.layoutChanged.emit()
def sort_profiles(self, _reverse):
self.layoutAboutToBeChanged.emit()
@@ -259,6 +272,9 @@ class GeometryReachTableModel(PamhyrTableModel):
**data
)
self.apply_meshing(new_profiles, data, tableView)
def apply_meshing(self, new_profiles, data, tableView):
if new_profiles is None:
return
+70
View File
@@ -19,6 +19,7 @@
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
from View.WeatherParameters.translate import WeatherParametersTranslate
_translate = QCoreApplication.translate
@@ -44,6 +45,66 @@ class GeometryTranslate(MainTranslate):
self._dict["cross_sections"] = _translate("Geometry", "cross-sections")
self._dict["profile"] = _translate("Geometry", "cross-section")
self._dict["profiles"] = _translate("Geometry", "cross-sections")
self._dict["cross_section_enabled_count"] = _translate(
"Geometry", "Cross-section enabled ({count} disabled)"
)
self._dict["profile_disable_blocked"] = _translate(
"Geometry", "The cross-section cannot be disabled."
)
self._dict["profile_disable_blocked_info"] = _translate(
"Geometry",
"It is referenced by the following data:\n{dependencies}\n\n"
"Change or remove these associations before disabling it."
)
self._dict["profile_dependency"] = _translate(
"Geometry", "{kind} “{name}” ({association})"
)
self._dict["initial_condition"] = _translate(
"Geometry", "Initial condition"
)
self._dict["initial_condition_temperature"] = _translate(
"Geometry", "Initial condition temperature"
)
self._dict["hydraulic_structure"] = _translate(
"Geometry", "Hydraulic structure"
)
self._dict["lateral_contribution"] = _translate(
"Geometry", "Lateral contribution"
)
self._dict["weather_parameter"] = _translate(
"Geometry", "Weather parameter"
)
self._dict["section"] = _translate("Geometry", "cross-section")
self._dict["input_section"] = _translate(
"Geometry", "input cross-section"
)
self._dict["output_section"] = _translate(
"Geometry", "output cross-section"
)
self._dict["begin_section"] = _translate(
"Geometry", "first cross-section"
)
self._dict["end_section"] = _translate(
"Geometry", "last cross-section"
)
self._dict["cross_section_pk"] = _translate(
"Geometry", "cross-section PK"
)
weather_parameters = WeatherParametersTranslate().get_dict(
"weather_parameters"
)
self._sub_dict["weather_parameter_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"],
"ALB": weather_parameters["albedo"],
"SC": weather_parameters["shading_coefficient"],
"CCF": weather_parameters["cloud_cover_fraction"],
}
self._dict["transverse_abscissa"] = _translate(
"Geometry", "Transverse abscissa (m)"
@@ -97,6 +158,15 @@ class GeometryTranslate(MainTranslate):
self._dict["format_not_exportable"] = _translate(
"Geometry", "The format of the file is not exportable."
)
self._dict["export_options"] = _translate(
"Geometry", "Export options"
)
self._dict["export_options_text"] = _translate(
"Geometry", "Choose which cross-sections to export."
)
self._dict["export_interpolated_profiles"] = _translate(
"Geometry", "Export interpolated cross-sections"
)
self._dict["update_rk_error"] = _translate(
"Geometry",
"RK update can't be executed."
+21
View File
@@ -68,6 +68,27 @@ class SetRKCommand(SetDataCommand):
self._reach.profile(self._index).rk = self._new
class SetEnabledCommand(QUndoCommand):
def __init__(self, reach, rows, enabled):
QUndoCommand.__init__(self)
self._profiles = [reach.profile(row) for row in rows]
self._old = [profile.is_enabled for profile in self._profiles]
self._new = bool(enabled)
@staticmethod
def _set_enabled(profile, enabled):
profile.enabled = enabled
def undo(self):
for profile, enabled in zip(self._profiles, self._old):
self._set_enabled(profile, enabled)
def redo(self):
for profile in self._profiles:
self._set_enabled(profile, self._new)
class AddCommand(QUndoCommand):
def __init__(self, reach, index):
QUndoCommand.__init__(self)
+148 -8
View File
@@ -99,6 +99,7 @@ class GeometryWindow(PamhyrWindow):
self._profile_window = []
self.setup_table()
self.setup_checkbox()
self.setup_plots()
self.setup_statusbar()
self.setup_connections()
@@ -124,6 +125,10 @@ class GeometryWindow(PamhyrWindow):
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_checkbox(self):
self._checkbox = self.find(QCheckBox, "checkBox_enabled")
self._set_checkbox_state()
def setup_plots(self):
self.setup_plots_xy()
self.setup_plots_rkc()
@@ -223,6 +228,9 @@ class GeometryWindow(PamhyrWindow):
.selectionChanged\
.connect(self.select_current_profile)
if not self._study.is_read_only():
self._checkbox.clicked.connect(self._set_profiles_state)
self._table.layoutChanged.connect(self.update_redraw)
def update(self):
@@ -287,13 +295,14 @@ class GeometryWindow(PamhyrWindow):
if self.sub_window_exists(
ProfileWindow,
data=[None, None, profile]
data=[self._study, self._config, profile]
):
continue
win = ProfileWindow(
profile=profile,
study=self._study,
config=self._config,
parent=self,
)
self._profile_window.append(win)
@@ -314,6 +323,10 @@ class GeometryWindow(PamhyrWindow):
trad=self._trad,
parent=self
)
dlg.adjustSize()
dialog_geometry = dlg.frameGeometry()
dialog_geometry.moveCenter(self.frameGeometry().center())
dlg.move(dialog_geometry.topLeft())
if dlg.exec():
data = {
"step": dlg.space_step,
@@ -350,14 +363,22 @@ class GeometryWindow(PamhyrWindow):
tableview = self.tableView
def fn():
self._table.meshing(mesher, data, tableview)
# self._table.meshing(mesher, data, tableview)
return mesher.meshing(self._reach, **data)
dlg2 = WaitingDialog(
payload_fn=fn,
title="waiting_mesh",
parent=self
)
dlg2.exec_()
if dlg2.worker_exception is not None:
raise dlg2.worker_exception
# Models, undo stacks and selections belong to the GUI thread.
self._table.apply_meshing(
dlg2.worker_result, data, tableview
)
self._table.update()
except Exception as e:
@@ -512,6 +533,92 @@ class GeometryWindow(PamhyrWindow):
self._plot_ac.draw()
self.tableView.model().blockSignals(False)
self._set_checkbox_state()
def index_selected_rows(self):
return [
index.row()
for index in self.tableView.selectionModel().selectedRows()
]
def _set_checkbox_state(self):
rows = self.index_selected_rows()
editable = not self._study.is_read_only()
disabled_count = (self._reach.number_profiles -
self._reach.number_enabled_profiles)
self._checkbox.setText(
self._trad["cross_section_enabled_count"].format(
count=disabled_count
)
)
self._checkbox.setEnabled(editable and len(rows) > 0)
if len(rows) == 0:
self._checkbox.setTristate(False)
self._checkbox.setChecked(False)
return
states = {
self._reach.profile(row).is_enabled
for row in rows
}
if len(states) == 1:
self._checkbox.setTristate(False)
self._checkbox.setChecked(states.pop())
else:
self._checkbox.setTristate(True)
self._checkbox.setCheckState(Qt.PartiallyChecked)
def _set_profiles_state(self, enabled):
rows = self.index_selected_rows()
if len(rows) == 0:
return
if not enabled:
blocked = []
for row in rows:
profile = self._reach.profile(row)
dependencies = self._study.river.profile_dependencies(profile)
if len(dependencies) != 0:
blocked.append((profile, dependencies))
if len(blocked) != 0:
details = []
for profile, dependencies in blocked:
details.append(f"{profile.name}:")
for dependency in dependencies:
item = dependency["item"]
if dependency["kind"] == "weather_parameter":
name = self._trad.get_dict(
"weather_parameter_types"
).get(item.type, item.type)
else:
name = getattr(item, "name", "")
if name == "":
name = self._trad[dependency["kind"]]
details.append(
" - " +
self._trad["profile_dependency"].format(
kind=self._trad[dependency["kind"]],
name=name,
association=self._trad[
dependency["association"]
],
)
)
self._set_checkbox_state()
self.message_box(
self._trad["profile_disable_blocked"],
self._trad["profile_disable_blocked_info"].format(
dependencies="\n".join(details)
),
window_title=self._trad["warning"],
)
return
self._checkbox.setTristate(False)
self._table.enabled(rows, enabled)
def add(self):
if len(self.tableView.selectedIndexes()) == 0:
@@ -727,7 +834,11 @@ class GeometryWindow(PamhyrWindow):
suffix = Path(filename).suffix
if suffix != "":
if suffix == ".st" or suffix == ".ST":
self._export_to_file_st(filename[:-3])
export_interpolated = self._ask_export_options()
if export_interpolated is not None:
self._export_to_file_st(
filename[:-3], export_interpolated
)
else:
# Warning popup when the format is not exportable
win = QtWidgets.QMessageBox()
@@ -738,16 +849,45 @@ class GeometryWindow(PamhyrWindow):
pass
else:
self._export_to_file_st(filename)
export_interpolated = self._ask_export_options()
if export_interpolated is not None:
self._export_to_file_st(filename, export_interpolated)
def _export_to_file_st(self, filename):
def _ask_export_options(self):
win = QtWidgets.QMessageBox(self)
win.setIcon(QtWidgets.QMessageBox.Question)
win.setWindowTitle(self._trad["export_options"])
win.setText(self._trad["export_options_text"])
win.setStandardButtons(
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel
)
export_interpolated = QCheckBox(
self._trad["export_interpolated_profiles"], win
)
export_interpolated.setChecked(True)
win.setCheckBox(export_interpolated)
if win.exec() != QtWidgets.QMessageBox.Ok:
return None
return export_interpolated.isChecked()
def _export_to_file_st(self, filename, export_interpolated=True):
with open(filename+".st", "w+") as f:
f.write("# Exported from Pamhyr2\n")
self._export_to_file_st_reach(f, self._reach)
self._export_to_file_st_reach(
f, self._reach, export_interpolated
)
def _export_to_file_st_reach(self, wfile, reach):
def _export_to_file_st_reach(self, wfile, reach,
export_interpolated=True):
pid = 0
for profile in reach.profiles:
is_interpolated = profile.name.lower().startswith("interpol")
if is_interpolated and not export_interpolated:
continue
self._export_to_file_st_profile(wfile, profile, pid)
pid += 1
+13 -9
View File
@@ -61,7 +61,7 @@ class ComboBoxDelegate(QItemDelegate):
val = []
if self._mode == "rk":
reach = self._data.hydraulic_structures\
.get(index.row())\
.get_for_enabled_reaches(index.row())\
.input_reach
if reach is not None:
val = list(
@@ -73,7 +73,7 @@ class ComboBoxDelegate(QItemDelegate):
else:
val = list(
map(
lambda n: n.name, self._data.edges()
lambda n: n.name, self._data.enable_edges()
)
)
@@ -94,7 +94,7 @@ class ComboBoxDelegate(QItemDelegate):
if self._mode == "rk":
reach = self._data.hydraulic_structures\
.get(index.row())\
.get_for_enabled_reaches(index.row())\
.input_reach
profiles = list(
filter(
@@ -128,20 +128,22 @@ class TableModel(PamhyrTableModel):
self._lst = self._data._hydraulic_structures
def get_true_data_row(self, row):
hs = self._lst.get(row)
hs = self._lst.get_for_enabled_reaches(row)
if hs is None:
return len(self._lst.lst)
return next(
map(
lambda e: e[0],
filter(
lambda e: e[1] == hs,
enumerate(self._lst._lst)
enumerate(self._lst.lst)
)
), 0
)
def rowCount(self, parent):
return len(self._lst)
return len(self._lst.enabled_reaches_list)
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
@@ -149,16 +151,17 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
structure = self._lst.get_for_enabled_reaches(row)
if self._headers[column] == "name":
return self._lst.get(row).name
return structure.name
elif self._headers[column] == "reach":
n = self._lst.get(row).input_reach
n = structure.input_reach
if n is None:
return self._trad['not_associated']
return n.name
elif self._headers[column] == "rk":
n = self._lst.get(row).input_section
n = structure.input_section
if n is None:
return self._trad['not_associated']
return n.display_name()
@@ -174,6 +177,7 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
row = self.get_true_data_row(row)
na = self._trad['not_associated']
try:
+9 -6
View File
@@ -220,7 +220,7 @@ class HydraulicStructuresWindow(PamhyrWindow):
def add(self):
rows = self.index_selected_rows()
if len(self._hs_lst) == 0 or len(rows) == 0:
if len(self._hs_lst.enabled_reaches_list) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
@@ -247,7 +247,7 @@ class HydraulicStructuresWindow(PamhyrWindow):
def edit(self):
rows = self.index_selected_rows()
for row in rows:
data = self._hs_lst.get(row)
data = self._hs_lst.get_for_enabled_reaches(row)
if self.sub_window_exists(
BasicHydraulicStructuresWindow,
@@ -269,7 +269,8 @@ class HydraulicStructuresWindow(PamhyrWindow):
self._checkbox.setChecked(True)
else:
self._checkbox.setEnabled(True)
self._checkbox.setChecked(self._hs_lst.get(row).enabled)
structure = self._hs_lst.get_for_enabled_reaches(row)
self._checkbox.setChecked(structure.enabled)
def _set_structure_state(self):
rows = self.index_selected_rows()
@@ -288,16 +289,18 @@ class HydraulicStructuresWindow(PamhyrWindow):
def _update_clear_plot(self):
rows = self.index_selected_rows()
if len(rows) == 0 or len(self._hs_lst) == 0:
if (len(rows) == 0 or
len(self._hs_lst.enabled_reaches_list) == 0):
self._update_clear_all()
return
reach = self._hs_lst.get(rows[0]).input_reach
structure = self._hs_lst.get_for_enabled_reaches(rows[0])
reach = structure.input_reach
if reach is not None:
self.plot_rkc.set_reach(reach)
self.plot_ac.set_reach(reach)
profile = self._hs_lst.get(rows[0]).input_section
profile = structure.input_section
if profile is not None:
profiles = reach.reach\
.get_profiles_from_rk(
+21 -14
View File
@@ -55,17 +55,27 @@ class ComboBoxDelegate(QItemDelegate):
self._trad = trad
self._ic_spec_lst = ic_spec_lst
def _specifications(self):
if hasattr(
self._ic_spec_lst,
"get_specs_for_enabled_reaches"
):
return self._ic_spec_lst.get_specs_for_enabled_reaches(
self._data.edges()
)
return self._ic_spec_lst
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
val = []
if self._mode == "rk":
reach_id = self._ic_spec_lst[index.row()].reach
reach_id = self._specifications()[index.row()].reach
reach = next(filter(lambda edge: edge.id == reach_id,
self._data.edges()), None)
if reach_id is not None:
if reach is not None:
val = list(
map(
lambda rk: str(rk), reach.reach.get_rk()
@@ -74,7 +84,7 @@ class ComboBoxDelegate(QItemDelegate):
else:
val = list(
map(
lambda n: n.name, self._data.edges()
lambda n: n.name, self._data.enable_edges()
)
)
@@ -117,11 +127,8 @@ class InitialConditionTableModel(PamhyrTableModel):
self._data = data
def _setup_lst(self):
self._lst = list(
filter(
lambda ica: ica._deleted is False,
self._data._data
)
self._lst = self._data.get_specs_for_enabled_reaches(
self._river.edges()
)
def rowCount(self, parent):
@@ -137,7 +144,7 @@ class InitialConditionTableModel(PamhyrTableModel):
if self._headers[column] == "name":
n = self._lst[row].name
if n is None or n == "":
return self._trad['not_associated']
return self._trad['not_defined']
return n
elif self._headers[column] == "reach":
n = self._lst[row].reach
@@ -159,27 +166,27 @@ class InitialConditionTableModel(PamhyrTableModel):
elif self._headers[column] == "concentration":
n = self._lst[row].concentration
if n is None:
return self._trad['not_associated']
return self._trad['not_defined']
return n
elif self._headers[column] == "eg":
n = self._lst[row].eg
if n is None:
return self._trad['not_associated']
return self._trad['not_defined']
return n
elif self._headers[column] == "em":
n = self._lst[row].em
if n is None:
return self._trad['not_associated']
return self._trad['not_defined']
return n
elif self._headers[column] == "ed":
n = self._lst[row].ed
if n is None:
return self._trad['not_associated']
return self._trad['not_defined']
return n
elif self._headers[column] == "rate":
n = self._lst[row].rate
if n is None:
return self._trad['not_associated']
return self._trad['not_defined']
return n
return QVariant()
+6 -3
View File
@@ -152,14 +152,14 @@ class InitialConditionsAdisTSWindow(PamhyrWindow):
self._delegate_reach = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
ic_spec_lst=self._data[0]._data,
ic_spec_lst=self._data[0],
parent=self,
mode="reaches"
)
self._delegate_rk = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
ic_spec_lst=self._data[0]._data,
ic_spec_lst=self._data[0],
parent=self,
mode="rk"
)
@@ -308,7 +308,10 @@ class InitialConditionsAdisTSWindow(PamhyrWindow):
def add(self):
rows = self.index_selected_rows()
if len(self._data[0]._data) == 0 or len(rows) == 0:
visible = self._data[0].get_specs_for_enabled_reaches(
self._study.river.edges()
)
if len(visible) == 0 or len(rows) == 0:
self._table_spec.add(0)
else:
self._table_spec.add(rows[0])
@@ -0,0 +1,307 @@
# Table.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.InitialConditionsTemperature.UndoCommand import (
SetCommand, AddCommand, SetCommandSpec,
DelCommand,
)
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, ic_spec_lst=None,
trad=None, parent=None, mode="reaches"):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._mode = mode
self._trad = trad
self._ic_spec_lst = ic_spec_lst
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
val = []
if self._mode == "rk":
reach_id = self._ic_spec_lst[index.row()].reach
reach = next(filter(lambda edge: edge.id == reach_id,
self._data.edges()), None)
if reach is not None:
val = list(
map(
lambda profile: profile.display_name(),
reach.reach.profiles
)
)
else:
val = list(
map(
lambda n: n.name, self._data.enable_edges()
)
)
self.editor.addItems(
[self._trad['not_associated']] +
val
)
self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
return self.editor
def setEditorData(self, editor, index):
value = index.data(Qt.DisplayRole)
self.editor.currentTextChanged.connect(self.currentItemChanged)
def setModelData(self, editor, model, index):
text = str(editor.currentText())
value = text
if self._mode == "rk":
reach_id = self._ic_spec_lst[index.row()].reach
reach = next(
(
edge for edge in self._data.edges()
if edge.id == reach_id
),
None
)
if reach is not None:
profile = next(
(
profile for profile in reach.reach.profiles
if profile.display_name() == text
),
None
)
if profile is not None:
value = profile.rk
model.setData(index, value)
editor.close()
editor.deleteLater()
def updateEditorGeometry(self, editor, option, index):
r = QRect(option.rect)
if self.editor.windowFlags() & Qt.Popup:
if editor.parent() is not None:
r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
editor.setGeometry(r)
@pyqtSlot()
def currentItemChanged(self):
self.commitData.emit(self.sender())
class InitialConditionTableModel(PamhyrTableModel):
def __init__(self, river=None, data=None, **kwargs):
self._river = river
super(InitialConditionTableModel, self).__init__(data=data, **kwargs)
self._data = data
def _setup_lst(self):
enabled_reach_ids = {
reach.id
for reach in self._river.enable_edges()
}
self._lst = list(
filter(
lambda ica: (
ica._deleted is False and
(
ica.reach in (None, -1) or
ica.reach in enabled_reach_ids
)
),
self._data._data
)
)
def refresh(self):
self.beginResetModel()
try:
self._setup_lst()
finally:
self.endResetModel()
def rowCount(self, parent):
return len(self._lst)
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
if self._headers[column] == "name":
n = self._lst[row].name
if n is None or n == "":
return self._trad['not_defined']
return n
elif self._headers[column] == "reach":
n = self._lst[row].reach
if n is None:
return self._trad['not_associated']
return next(filter(
lambda edge: edge.id == n, self._river.edges()
)).name
elif self._headers[column] == "rk":
n = self._lst[row].start_rk
if n is None:
return self._trad['not_associated']
return self._rk_display_name(row, n)
elif self._headers[column] == "temperature":
n = self._lst[row].temperature
if n is None:
return self._trad['not_defined']
return n
return QVariant()
def _rk_display_name(self, row, rk):
reach_id = self._lst[row].reach
reach = next(
(
edge for edge in self._river.edges()
if edge.id == reach_id
),
None
)
if reach is None:
return str(rk)
profile = next(
(
profile for profile in reach.reach.profiles
if profile.rk == rk
),
None
)
return profile.display_name() if profile is not None else str(rk)
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
try:
if self._headers[column] in ["name", "temperature"]:
self._undo.push(
SetCommand(
self._lst, row, self._headers[column], value
)
)
else:
new_value = value
if self._headers[column] == "reach":
reach = self._river.edge(value)
rks = reach.reach.get_rk()
if not rks:
return False
new_value = (reach.id, min(rks))
self._undo.push(
SetCommandSpec(
self._lst, row, self._headers[column],
new_value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
if self._headers[column] == "reach":
rk_index = self.index(
row, self._headers.index("rk")
)
self.dataChanged.emit(index, rk_index)
else:
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
self._undo.push(
AddCommand(
self._data, self._lst, row
)
)
self._setup_lst()
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
data_rows = {
id(ica): i for i, ica in enumerate(self._data._data)
}
self._undo.push(
DelCommand(
self._data,
self._data._data,
[data_rows[id(self._lst[row])] for row in rows]
)
)
self._setup_lst()
self.endRemoveRows()
self.layoutChanged.emit()
def undo(self):
self._undo.undo()
self._setup_lst()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self._setup_lst()
self.layoutChanged.emit()
@@ -0,0 +1,94 @@
# Table.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.InitialConditionsTemperature.UndoCommand import (
SetCommand,
)
logger = logging.getLogger()
_translate = QCoreApplication.translate
class InitialConditionTableDefaultModel(PamhyrTableModel):
def __init__(self, **kwargs):
super(InitialConditionTableDefaultModel, self).__init__(**kwargs)
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
if self._headers[column] == "name":
return self._data[row].name
elif self._headers[column] == "temperature":
n = self._data[row].temperature
if n is None:
return 0
return n
return QVariant()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
row = index.row()
column = index.column()
try:
if self._headers[column] is not None:
self._undo.push(
SetCommand(
self._data, row, self._headers[column], value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def undo(self):
self._undo.undo()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self.layoutChanged.emit()
@@ -0,0 +1,159 @@
# UndoCommand.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.InitialConditionsTemperature.InitialConditionsTemperature \
import InitialConditionsTemperature
from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
import InitialConditionsTemperatureList
class SetCommand(QUndoCommand):
def __init__(self, data, row, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._row = row
self._column = column
if self._column == "name":
self._old = self._data[self._row].name
elif self._column == "temperature":
self._old = self._data[self._row].temperature
_type = float
if column == "name":
_type = str
self._new = _type(new_value)
def undo(self):
if self._column == "name":
self._data[self._row].name = self._old
elif self._column == "temperature":
self._data[self._row].temperature = self._old
def redo(self):
if self._column == "name":
self._data[self._row].name = self._new
elif self._column == "temperature":
self._data[self._row].temperature = self._new
class SetCommandSpec(QUndoCommand):
def __init__(self, data, row, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._row = row
self._column = column
if self._column == "name":
self._old = self._data[self._row].name
elif self._column == "reach":
self._old = self._data[self._row].reach
self._old_start_rk = self._data[self._row].start_rk
self._old_end_rk = self._data[self._row].end_rk
elif self._column == "rk":
self._old = self._data[self._row].start_rk
elif self._column == "temperature":
self._old = self._data[self._row].temperature
_type = float
if column == "name":
_type = str
elif column == "reach":
_type = int
if column == "reach":
self._new = _type(new_value[0])
self._new_rk = float(new_value[1])
else:
self._new = _type(new_value)
def undo(self):
if self._column == "name":
self._data[self._row].name = self._old
elif self._column == "reach":
self._data[self._row].reach = self._old
self._data[self._row].start_rk = self._old_start_rk
self._data[self._row].end_rk = self._old_end_rk
elif self._column == "rk":
self._data[self._row].start_rk = self._old
self._data[self._row].end_rk = self._old
elif self._column == "temperature":
self._data[self._row].temperature = self._old
def redo(self):
if self._column == "name":
self._data[self._row].name = self._new
elif self._column == "reach":
self._data[self._row].reach = self._new
self._data[self._row].start_rk = self._new_rk
self._data[self._row].end_rk = self._new_rk
elif self._column == "rk":
self._data[self._row].start_rk = self._new
self._data[self._row].end_rk = self._new
elif self._column == "temperature":
self._data[self._row].temperature = self._new
class AddCommand(QUndoCommand):
def __init__(self, data, ics_spec, index):
QUndoCommand.__init__(self)
self._data = data
self._ics_spec = ics_spec
self._index = index
self._new = None
def undo(self):
self._data.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._data.new(self._index)
else:
self._data.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, data, ics_spec, rows):
QUndoCommand.__init__(self)
self._data = data
self._ics_spec = ics_spec
self._rows = rows
self._ic = []
for row in rows:
self._ic.append((row, self._ics_spec[row]))
self._ic.sort()
def undo(self):
for row, el in self._ic:
self._data.insert(row, el)
def redo(self):
self._data.delete_i(self._rows)
@@ -0,0 +1,318 @@
# Window.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import os
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5.QtGui import (
QKeySequence, QIcon,
)
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QItemSelectionModel,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QVBoxLayout, QToolBar, QAction, QToolButton,
)
from Modules import Modules
from View.InitialConditionsTemperature.UndoCommand import (
SetCommand,
)
from View.InitialConditionsTemperature.TableDefault import (
InitialConditionTableDefaultModel,
)
from View.InitialConditionsTemperature.Table import (
InitialConditionTableModel, ComboBoxDelegate,
)
from View.InitialConditionsTemperature.translate import IcTemperatureTranslate
from Solver.Mage import Mage8
_translate = QCoreApplication.translate
logger = logging.getLogger()
class InitialConditionsTemperatureWindow(PamhyrWindow):
_pamhyr_ui = "InitialConditionsTemperature"
_pamhyr_name = "Initial condition Temperature"
def __init__(self, data=None, study=None,
config=None, parent=None):
self._data = []
self._data.append(data)
trad = IcTemperatureTranslate()
name = (
trad[self._pamhyr_name] +
" - " + study.name
)
super(InitialConditionsTemperatureWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._hash_data.append(data)
# self._ics_Temperature_lst = study.river.ic_Temperature
self.setup_table()
def setup_table(self):
path_icons = os.path.join(self._get_ui_directory(), f"ressources")
table_default = self.find(QTableView, f"tableView")
if self._study.is_editable():
editable_headers = self._trad.get_dict("table_headers")
else:
editable_headers = []
self._table = InitialConditionTableDefaultModel(
table_view=table_default,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates={},
data=self._data,
undo=self._undo_stack,
trad=self._trad
)
table_default.setModel(self._table)
table_default.setSelectionBehavior(QAbstractItemView.SelectRows)
table_default.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch
)
table_default.setAlternatingRowColors(True)
layout = self.find(QVBoxLayout, f"verticalLayout_1")
toolBar = QToolBar()
layout.addWidget(toolBar)
action_add = QAction(self)
action_add.setIcon(QIcon(os.path.join(path_icons, f"add.png")))
if self._study.is_editable():
action_add.triggered.connect(self.add)
action_delete = QAction(self)
action_delete.setIcon(QIcon(os.path.join(path_icons, f"del.png")))
if self._study.is_editable():
action_delete.triggered.connect(self.delete)
toolBar.addAction(action_add)
toolBar.addAction(action_delete)
self.table_spec = QTableView()
layout.addWidget(self.table_spec)
self._delegate_reach = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
ic_spec_lst=self._data[0]._data,
parent=self,
mode="reaches"
)
self._delegate_rk = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
ic_spec_lst=self._data[0]._data,
parent=self,
mode="rk"
)
if self._study.is_editable():
editable_headers_spec = self._trad.get_dict("table_headers_spec")
else:
editable_headers_spec = []
self._table_spec = InitialConditionTableModel(
table_view=self.table_spec,
table_headers=self._trad.get_dict("table_headers_spec"),
editable_headers=editable_headers_spec,
delegates={
"reach": self._delegate_reach,
"rk": self._delegate_rk
},
data=self._data[0],
undo=self._undo_stack,
trad=self._trad,
river=self._study.river
)
self.table_spec.setModel(self._table_spec)
self.table_spec.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table_spec.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch
)
self.table_spec.setAlternatingRowColors(True)
selectionModel = self.table_spec.selectionModel()
index = self.table_spec.model().index(0, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
self.table_spec.scrollTo(index)
def index_selected_row(self):
# table = self.find(QTableView, f"tableView")
table = self.table_spec
rows = table.selectionModel()\
.selectedRows()
if len(rows) == 0:
return 0
return rows[0].row()
def index_selected_rows(self):
# table = self.find(QTableView, f"tableView")
table = self.table_spec
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def move_up(self):
row = self.index_selected_row()
self._table.move_up(row)
self._update()
def move_down(self):
row = self.index_selected_row()
self._table.move_down(row)
self._update()
def _copy(self):
rows = list(
map(
lambda row: row.row(),
self.tableView.selectionModel().selectedRows()
)
)
table = list(
map(
lambda eic: list(
map(
lambda k: eic[1][k],
["rk", "discharge", "elevation"]
)
),
filter(
lambda eic: eic[0] in rows,
enumerate(self._ics.lst())
)
)
)
self.copyTableIntoClipboard(table)
def _paste(self):
header, data = self.parseClipboardTable()
if len(data) + len(header) == 0:
return
logger.debug(
"IC: Paste: " +
f"header = {header}, " +
f"data = {data}"
)
try:
row = self.index_selected_row()
# self._table.paste(row, header, data)
self._table.paste(row, [], data)
except Exception as e:
logger_exception(e)
self._update()
def _undo(self):
undo_stack = self._undo_stack
if undo_stack is None or not undo_stack.canUndo():
return
if isinstance(undo_stack.command(undo_stack.index() - 1), SetCommand):
self._table.undo()
else:
self._table_spec.undo()
self._update()
def _redo(self):
undo_stack = self._undo_stack
if undo_stack is None or not undo_stack.canRedo():
return
if isinstance(undo_stack.command(undo_stack.index()), SetCommand):
self._table.redo()
else:
self._table_spec.redo()
self._update()
def add(self):
rows = self.index_selected_rows()
if len(self._data[0]._data) == 0 or len(rows) == 0:
self._table_spec.add(0)
else:
self._table_spec.add(rows[0])
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table_spec.delete(rows)
def _propagated_update(self, key=Modules(0)):
if Modules.NETWORK not in key:
return
self._table_spec.refresh()
self.table_spec.clearSelection()
@@ -0,0 +1,45 @@
# translate.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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class IcTemperatureTranslate(MainTranslate):
def __init__(self):
super(IcTemperatureTranslate, self).__init__()
self._dict["Initial condition Temperature"] = _translate(
"InitialConditionTemperature", "Initial condition temperature")
self._dict["rk"] = self._dict["unit_rk"]
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"temperature": self._dict["unit_temperature"],
}
self._sub_dict["table_headers_spec"] = {
"name": self._dict["name"],
"reach": self._dict["reach"],
"rk": _translate("Unit", "RK (m)"),
"temperature": self._dict["unit_temperature"],
}
@@ -147,6 +147,7 @@ class EditLateralContributionWindow(PamhyrWindow):
def update(self):
self.plot.update()
self.plot.fit_to_data()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
+18 -10
View File
@@ -103,7 +103,10 @@ class ComboBoxDelegate(QItemDelegate):
else:
self.editor.addItems(
[self._trad['not_associated']] +
self._data.edges_names()
[
reach.name
for reach in self._data.enable_edges()
]
)
self.editor.setCurrentText(index.data(Qt.DisplayRole))
@@ -154,7 +157,9 @@ class TableModel(PamhyrTableModel):
self._long_types = self._trad.get_dict("long_types")
def get_true_data_row(self, row):
lc = self._lst.get(self._tab, row)
lc = self._lst.get_for_enabled_reaches(self._tab, row)
if lc is None:
return len(self._lst.get_tab(self._tab))
return next(
map(
@@ -167,7 +172,7 @@ class TableModel(PamhyrTableModel):
)
def rowCount(self, parent):
return self._lst.len(self._tab)
return len(self._lst.get_tab_for_enabled_reaches(self._tab))
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
@@ -175,21 +180,22 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
contribution = self._lst.get_for_enabled_reaches(self._tab, row)
if self._headers[column] == "name":
return self._lst.get(self._tab, row).name
return contribution.name
elif self._headers[column] == "type":
t = self._lst.get(self._tab, row).lctype
t = contribution.lctype
return self._long_types[t]
elif self._headers[column] == "edge":
n = self._lst.get(self._tab, row).reach
n = contribution.reach
if n is None:
return self._trad['not_associated']
return n.name
elif self._headers[column] == "begin_rk":
return str(self._lst.get(self._tab, row).begin_rk)
return str(contribution.begin_rk)
elif self._headers[column] == "end_rk":
return str(self._lst.get(self._tab, row).end_rk)
return str(contribution.end_rk)
return QVariant()
@@ -202,6 +208,8 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
contribution = self._lst.get_for_enabled_reaches(self._tab, row)
row = self.get_true_data_row(row)
try:
if self._headers[column] == "name":
@@ -225,7 +233,7 @@ class TableModel(PamhyrTableModel):
)
)
elif self._headers[column] == "begin_rk":
_edge = self._lst.get(self._tab, row).reach
_edge = contribution.reach
_begin_rk = next(
p for p in _edge.reach.profiles
if p.pamhyr_id == value
@@ -236,7 +244,7 @@ class TableModel(PamhyrTableModel):
)
)
elif self._headers[column] == "end_rk":
_edge = self._lst.get(self._tab, row).reach
_edge = contribution.reach
_end_rk = next(
p for p in _edge.reach.profiles
if p.pamhyr_id == value
+5 -4
View File
@@ -213,11 +213,11 @@ class LateralContributionWindow(PamhyrWindow):
if len(rows) > 0:
edge = self._study.river\
.lateral_contribution\
.get(tab, rows[0])\
.get_for_enabled_reaches(tab, rows[0])\
.reach
if edge:
data = edge.reach
lc = self._lcs.get(tab, rows[0])
lc = self._lcs.get_for_enabled_reaches(tab, rows[0])
highlight = (lc.begin_rk, lc.end_rk)
for delegate in self._delegate_rk:
@@ -236,7 +236,8 @@ class LateralContributionWindow(PamhyrWindow):
def add(self):
tab = self.current_tab()
rows = self.index_selected_rows()
if self._lcs.len(tab) == 0 or len(rows) == 0:
visible = self._lcs.get_tab_for_enabled_reaches(tab)
if len(visible) == 0 or len(rows) == 0:
self._table[tab].add(0)
else:
self._table[tab].add(rows[0])
@@ -291,7 +292,7 @@ class LateralContributionWindow(PamhyrWindow):
tab = self.current_tab()
rows = self.index_selected_rows()
for row in rows:
data = self._lcs.get(tab, row)
data = self._lcs.get_for_enabled_reaches(tab, row)
if self.sub_window_exists(
EditLateralContributionWindow,
@@ -160,6 +160,7 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
def update(self):
self.plot.update()
self.plot.fit_to_data()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
+43 -4
View File
@@ -81,7 +81,10 @@ class ComboBoxDelegate(QItemDelegate):
else:
self.editor.addItems(
[self._trad['not_associated']] +
self._data.edges_names()
[
reach.name
for reach in self._data.enable_edges()
]
)
self.editor.setCurrentText(index.data(Qt.DisplayRole))
@@ -119,6 +122,11 @@ class TableModel(PamhyrTableModel):
self._setup_lst()
def _setup_lst(self):
enabled_reach_ids = {
reach.id
for reach in self._data.enable_edges()
}
if self._lcs_list is not None:
self._lcs_pol_list = [
lcs for lcs in self._lcs_list._lst
@@ -127,7 +135,13 @@ class TableModel(PamhyrTableModel):
self._lst = list(
filter(
lambda x: x._deleted is False,
lambda x: (
x._deleted is False and
(
x.reach in (None, -1) or
x.reach in enabled_reach_ids
)
),
self._lcs_pol_list
)
)
@@ -137,6 +151,18 @@ class TableModel(PamhyrTableModel):
def rowCount(self, parent):
return len(self._lst)
def get(self, row):
if 0 <= row < len(self._lst):
return self._lst[row]
return None
def refresh(self):
self.beginResetModel()
try:
self._setup_lst()
finally:
self.endResetModel()
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
@@ -170,11 +196,18 @@ class TableModel(PamhyrTableModel):
try:
if self._headers[column] == "reach":
reach = self._data.reach(value)
rks = reach.reach.get_rk()
if len(rks) == 0:
return False
self._undo.push(
SetReachCommand(
self._lcs_list, self._lst,
row,
self._data.reach(value).id
reach.id,
min(rks),
max(rks)
)
)
elif self._headers[column] == "begin_rk":
@@ -193,7 +226,13 @@ class TableModel(PamhyrTableModel):
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
if self._headers[column] == "reach":
end_index = self.index(
row, self._headers.index("end_rk")
)
self.dataChanged.emit(index, end_index)
else:
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
@@ -64,20 +64,30 @@ class SetEndCommand(QUndoCommand):
class SetReachCommand(QUndoCommand):
def __init__(self, lcs, lcs_lst, index, reach):
def __init__(self, lcs, lcs_lst, index, reach, begin_rk, end_rk):
QUndoCommand.__init__(self)
self._lcs = lcs
self._lcs_lst = lcs_lst
self._index = index
self._old = self._lcs_lst[self._index].reach
self._old_begin_rk = self._lcs_lst[self._index].begin_rk
self._old_end_rk = self._lcs_lst[self._index].end_rk
self._new = reach
self._new_begin_rk = begin_rk
self._new_end_rk = end_rk
def undo(self):
self._lcs_lst[self._index].reach = self._old
contribution = self._lcs_lst[self._index]
contribution.reach = self._old
contribution.begin_rk = self._old_begin_rk
contribution.end_rk = self._old_end_rk
def redo(self):
self._lcs_lst[self._index].reach = self._new
contribution = self._lcs_lst[self._index]
contribution.reach = self._new
contribution.begin_rk = self._new_begin_rk
contribution.end_rk = self._new_end_rk
class AddCommand(QUndoCommand):
+22 -8
View File
@@ -20,6 +20,7 @@ import logging
from tools import trace, timer
from Modules import Modules
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5.QtGui import (
@@ -187,12 +188,13 @@ class LateralContributionAdisTSWindow(PamhyrWindow):
data = None
highlight = None
tab = "liquid"
if len(rows) > 0:
reach_id = self._study.river\
.lateral_contributions_adists.lst[rows[0]]\
.reach
contribution = self._table.get(rows[0])
reach_id = (
contribution.reach
if contribution is not None
else None
)
if reach_id:
reach = next(
@@ -201,8 +203,10 @@ class LateralContributionAdisTSWindow(PamhyrWindow):
self._study.river.reachs()))
data = reach.reach
lc = self._lcs.lst[rows[0]]
highlight = (lc.begin_rk, lc.end_rk)
highlight = (
contribution.begin_rk,
contribution.end_rk
)
for delegate in self._delegate_rk:
delegate.data = reach
@@ -254,13 +258,23 @@ class LateralContributionAdisTSWindow(PamhyrWindow):
self._table.redo()
self._set_current_reach()
def _propagated_update(self, key=Modules(0)):
if Modules.NETWORK not in key:
return
self._table.refresh()
self.find(QTableView, "tableView").clearSelection()
self._set_current_reach()
def edit(self):
rows = self.index_selected_rows()
if not rows:
return
for row in rows:
data = self._lcs.lst[row]
data = self._table.get(row)
if data is None:
continue
if self.sub_window_exists(
EditLateralContributionAdisTSWindow,
+208 -35
View File
@@ -91,6 +91,11 @@ from View.Results.CompareDialog import (
CompareScenariosWindow
)
from View.RunSolver.WindowAdisTT import (
SelectSolverWindowAdisTT,
SolverLogWindowAdisTT
)
from View.RunSolver.WindowAdisTS import (
SelectSolverWindowAdisTS,
SolverLogWindowAdisTS
@@ -98,14 +103,26 @@ from View.RunSolver.WindowAdisTS import (
from View.CheckList.Window import CheckListWindow
from View.CheckList.WindowAdisTS import CheckListWindowAdisTS
from View.Results.WindowAdisTS import ResultsWindowAdisTS
from View.Results.WindowAdisTT import ResultsWindowAdisTT
from View.Results.ReadingResultsDialog import ReadingResultsDialog
from View.Debug.Window import ReplWindow, TimerWindow
from View.OutputRKAdisTS.Window import OutputRKAdisTSWindow
from View.Pollutants.Window import PollutantsWindow
from View.D90AdisTS.Window import D90AdisTSWindow
from View.DIFAdisTS.Window import DIFAdisTSWindow
from View.InitialConditionsTemperature.Window import (
InitialConditionsTemperatureWindow
)
from View.BoundaryConditionsTemperature.Window import (
BoundaryConditionsTemperatureWindow
)
from View.WeatherParameters.Window import (
WeatherParametersWindow
)
from Solver.Solvers import GenericSolver
from Model.Results.Results import Results
@@ -131,7 +148,7 @@ model_action = [
"action_menu_close", "action_menu_edit", "action_menu_save",
"action_menu_save_as", "action_toolBar_close", "action_toolBar_save",
"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 = [
@@ -141,6 +158,7 @@ other_model_action = [
define_model_action = [
# Toolbar
"action_toolBar_network", "action_toolBar_geometry",
"action_toolBar_meshing",
"action_toolBar_boundary_cond", "action_toolBar_lateral_contrib",
"action_toolBar_frictions", "action_toolBar_initial_cond",
# Menu
@@ -155,9 +173,13 @@ define_model_action = [
"action_menu_results_last", "action_menu_open_results_from_file",
"action_menu_compare_scenarios_results",
"action_menu_boundary_conditions_sediment",
"action_menu_rep_additional_lines", "action_menu_output_rk",
"action_menu_rep_additional_lines",
"action_menu_run_adists", "action_menu_pollutants",
"action_menu_d90", "action_menu_dif", "action_menu_edit_geotiff"
"action_menu_d90", "action_menu_dif", "action_menu_edit_geotiff",
"action_menu_meshing",
"action_menu_boundary_conditions_temperature",
"action_menu_initial_conditions_temperature",
"action_menu_weather_parameters", "action_menu_run_adistt",
]
action = (
@@ -267,11 +289,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"""
actions = {
# Menu action
"action_menu_initial_conditions_temperature":
self.open_initial_conditions_temperature,
"action_menu_boundary_conditions_temperature":
self.open_boundary_conditions_temperature,
"action_menu_weather_parameters":
self.open_weather_parameters,
"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_output_rk": self.open_output_rk_adists,
"action_menu_run_adists":
self.select_run_solver_adists,
"action_menu_run_adistt":
self.select_run_solver_adistt,
"action_menu_config": self.open_configure,
"action_menu_new": self.open_new_study,
"action_menu_edit": self.open_edit_study,
@@ -282,6 +312,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_edit_scenarios": self.open_scenarios,
"action_menu_edit_network": self.open_network,
"action_menu_edit_geometry": self.open_geometry,
"action_menu_meshing": self.open_meshing,
"action_menu_boundary_conditions": self.open_boundary_cond,
"action_menu_boundary_conditions_sediment":
self.open_boundary_cond_sed,
@@ -305,6 +336,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_compare_scenarios_results":
self.compare_results_scenarios,
"action_menu_open_results_adists": self.open_results_adists,
"action_menu_open_results_adistt":
lambda: self.open_results_adists(mode="adistt"),
# Help
"action_menu_pamhyr_users_wiki": self.open_doc_user,
"action_menu_pamhyr_developers_pdf":
@@ -321,6 +354,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# Current actions
"action_toolBar_network": self.open_network,
"action_toolBar_geometry": self.open_geometry,
"action_toolBar_meshing": self.open_meshing,
"action_toolBar_boundary_cond": self.open_boundary_cond,
"action_toolBar_lateral_contrib": self.open_lateral_contrib,
"action_toolBar_frictions": self.open_frictions,
@@ -1029,6 +1063,60 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# SUB WINDOWS #
###############
def open_weather_parameters(self):
river = self._study.river
weather_param = river.weather_parameters
if self.sub_window_exists(
WeatherParametersWindow,
data=[self._study, None, weather_param]
):
return
bound = WeatherParametersWindow(
study=self._study,
parent=self,
data=weather_param
)
bound.show()
def open_boundary_conditions_temperature(self):
river = self._study.river
bclist = river.boundary_conditions_temperature.lst
if self.sub_window_exists(
BoundaryConditionsTemperatureWindow,
data=[self._study, None]
):
return
bound = BoundaryConditionsTemperatureWindow(
study=self._study,
parent=self,
data=bclist,
)
bound.show()
def open_initial_conditions_temperature(self):
iclist = self._study.river.ic_temperature.lst
if len(iclist) != 0:
ic_default = iclist[0]
else:
ic_default = self._study.river.ic_temperature.new(0)
if self.sub_window_exists(
InitialConditionsTemperatureWindow,
data=[self._study, None]
):
return
InitialConditions = InitialConditionsTemperatureWindow(
study=self._study,
parent=self,
data=ic_default
)
InitialConditions.show()
def open_d90(self):
if len(self._study.river.d90_adists.lst) != 0:
d90_default = self._study.river.d90_adists.lst[0]
@@ -1080,19 +1168,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
Pollutants.show()
def open_output_rk_adists(self):
if self.sub_window_exists(
OutputRKAdisTSWindow,
data=[self._study, None]
):
return
Output_RK_AdisTS = OutputRKAdisTSWindow(
study=self._study,
parent=self
)
Output_RK_AdisTS.show()
def open_configure(self):
"""Open configure window
@@ -1268,7 +1343,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
GeometryWindow,
data=[self._study, self.conf, reach]
):
return
return self.get_sub_window(
GeometryWindow,
data=[self._study, self.conf, reach]
)
geometry = GeometryWindow(
study=self._study,
@@ -1277,8 +1355,21 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
parent=self
)
geometry.show()
return geometry
else:
self.msg_select_reach()
return None
def open_meshing(self):
"""Open the current reach geometry and its meshing dialog."""
geometry = self.open_geometry()
if geometry is None or self._study.is_read_only():
return
geometry.raise_()
geometry.activateWindow()
QApplication.processEvents()
geometry.edit_meshing()
def open_boundary_cond_sed(self):
self.open_boundary_cond(tab=1)
@@ -1356,12 +1447,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if self.sub_window_exists(
FrictionsWindow,
data=[self._study, None, reach]
data=[self._study, self.conf, reach]
):
return
frictions = FrictionsWindow(
study=self._study,
config=self.conf,
parent=self
)
frictions.show()
@@ -1454,6 +1546,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
sl.show()
def open_reach_sediment_layers(self):
if self._study is None:
logger.debug(
"No study open for reach sediment layers edition..."
)
return
if not self._study.river.has_current_reach():
self.msg_select_reach()
return
reach = self._study.river.current_reach().reach
if self.sub_window_exists(
@@ -1505,7 +1607,25 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if run.exec():
self.run_solver_adists(run.solver, run.mage_rep)
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:
return
@@ -1515,7 +1635,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self._study,
self.conf,
solver,
mage_rep
mage_rep,
mode,
]
):
return
@@ -1526,6 +1647,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
solver=solver,
parent=self,
mage_rep=mage_rep,
mode=mode,
)
check.show()
@@ -1561,6 +1683,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
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):
sol = SolverLogWindow(
study=self._study,
@@ -1680,6 +1812,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
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():
self._tmp_results = results
@@ -1721,7 +1863,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# Windows already opened
if self.sub_window_exists(
ResultsWindowAdisTS,
window_class,
data=[
self._study,
None, # No config
@@ -1732,7 +1874,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
):
return
res = ResultsWindowAdisTS(
res = window_class(
study=self._study,
solver=solver,
results=results,
@@ -1759,6 +1901,23 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
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):
if self._last_solver is None:
return
@@ -2023,7 +2182,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
return [result4, result5, result3]
def open_results_adists(self):
def open_results_adists(self, mode="adists"):
if self._study is None:
return
@@ -2060,10 +2219,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
text=self._trad["mb_results_bin_not_found"]
)
return
path = os.path.normpath(dir_path)
solver_name = path.split(os.sep)[-2]
solver_results = next(filter(lambda x: x.name == solver_name,
self.conf.solvers), None)
solver_results = self._solver_from_results_path(dir_path)
if solver_results is None:
self.msg_results_warning(
info=self._trad["mb_results_not_found"],
@@ -2071,16 +2227,34 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
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(
self._study,
repertory=dir_path, qlog=None) # self._output)
logger.info(f"Select results: {dir_path}")
if len(bin_list) >= 2 and ("total_sediment.bin" in bin_list):
self.open_solver_results_adists(
solver_results,
results=solver_results_adists,
)
if mode == "adistt":
self.open_solver_results_adistt(
solver_results,
results=solver_results_adists,
)
else:
self.open_solver_results_adists(
solver_results,
results=solver_results_adists,
)
else:
self.msg_results_warning(
info=self._trad["mb_results_not_found"],
@@ -2165,6 +2339,5 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
file = self._study.filename
_ = subprocess.Popen(
f"sqlitebrowser {file}",
shell=True
["sqlitebrowser", file]
)
+13
View File
@@ -928,6 +928,11 @@ class GraphWidget(QGraphicsView):
if self._only_display:
return
if event.button() == Qt.RightButton \
and self._block_right_click_in_add_state():
event.accept()
return
locked = self.graph._status.is_read_only()
pos = self.mapToScene(event.pos())
@@ -1062,7 +1067,15 @@ class GraphWidget(QGraphicsView):
# Contextual menu
def _block_right_click_in_add_state(self):
return self._state == "add" \
and self.selected_new_edge_src_node() is not None
def contextMenuEvent(self, event):
if self._block_right_click_in_add_state():
event.accept()
return
if self._only_display or self.graph._status.is_read_only():
return
+11 -8
View File
@@ -64,6 +64,7 @@ class SelectProfileDialog(PamhyrDialog):
self._reach = reach
self._profile = None
self._profiles = self._reach.reach.enabled_profiles
self.setup_combobox()
@@ -73,17 +74,19 @@ class SelectProfileDialog(PamhyrDialog):
list(
map(
lambda p: p.display_name(),
self._reach.reach.profiles)))
self._profiles)))
if len(self._profiles) == 0:
self.find(QDialogButtonBox, "buttonBox")\
.button(QDialogButtonBox.Ok)\
.setEnabled(False)
def accept(self):
profile = self.get_combobox_text("comboBox")
index = self.find(QComboBox, "comboBox").currentIndex()
if not 0 <= index < len(self._profiles):
return
self._profile = next(
filter(
lambda p: p.display_name() == profile,
self._reach.reach.profiles
)
)
self._profile = self._profiles[index]
super(SelectProfileDialog, self).accept()
+5 -1
View File
@@ -180,7 +180,11 @@ class NetworkWindow(PamhyrWindow):
key = event.key()
if key == Qt.Key_Escape:
self._graph_widget.reset_selection
self._graph_widget.reset_selection()
event.accept()
return
super(NetworkWindow, self).keyPressEvent(event)
def add_node_reservoir(self, node):
if self._reservoir.get_assoc_to_node(node) is None:
-155
View File
@@ -1,155 +0,0 @@
# UndoCommand.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
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
logger = logging.getLogger()
class SetTitleCommand(QUndoCommand):
def __init__(self, outputrk_lst, index, new_value):
QUndoCommand.__init__(self)
self._outputrk_lst = outputrk_lst
self._index = index
self._old = self._outputrk_lst.get(self._index).title
self._new = str(new_value)
def undo(self):
self._outputrk_lst.get(self._index).title = self._old
def redo(self):
self._outputrk_lst.get(self._index).title = self._new
class SetReachCommand(QUndoCommand):
def __init__(self, outputrk_lst, index, reach):
QUndoCommand.__init__(self)
self._outputrk_lst = outputrk_lst
self._index = index
self._old = self._outputrk_lst.get(self._index).reach
self._new = reach.id
def undo(self):
i = self._outputrk_lst.get(self._index)
i.reach = self._old
def redo(self):
i = self._outputrk_lst.get(self._index)
i.reach = self._new
class SetRKCommand(QUndoCommand):
def __init__(self, outputrk_lst, index, rk):
QUndoCommand.__init__(self)
self._outputrk_lst = outputrk_lst
self._index = index
self._old = self._outputrk_lst.get(self._index).rk
self._new = rk
def undo(self):
self._outputrk_lst.get(self._index).rk = self._old
def redo(self):
self._outputrk_lst.get(self._index).rk = self._new
class SetEnabledCommand(QUndoCommand):
def __init__(self, outputrk_lst, index, enabled):
QUndoCommand.__init__(self)
self._outputrk_lst = outputrk_lst
self._index = index
self._old = not enabled
self._new = enabled
def undo(self):
self._outputrk_lst.get(self._index).enabled = self._old
def redo(self):
self._outputrk_lst.get(self._index).enabled = self._new
class AddCommand(QUndoCommand):
def __init__(self, outputrk_lst, index):
QUndoCommand.__init__(self)
self._outputrk_lst = outputrk_lst
self._index = index
self._new = None
def undo(self):
self._outputrk_lst.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._outputrk_lst.new(self._outputrk_lst, self._index)
else:
self._outputrk_lst.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, outputrk_lst, rows):
QUndoCommand.__init__(self)
self._outputrk_lst = outputrk_lst
self._rows = rows
self._outputrk = []
for row in rows:
self._outputrk.append((row, self._outputrk_lst.get(row)))
self._outputrk.sort()
def undo(self):
for row, el in self._outputrk:
self._outputrk_lst.insert(row, el)
def redo(self):
self._outputrk_lst.delete_i(self._rows)
class PasteCommand(QUndoCommand):
def __init__(self, outputrk_lst, row, outputrk):
QUndoCommand.__init__(self)
self._outputrk_lst = outputrk_lst
self._row = row
self._outputrk = deepcopy(outputrk)
self._outputrk.reverse()
def undo(self):
self._outputrk_lst.delete_i(
self._tab,
range(self._row, self._row + len(self._outputrk))
)
def redo(self):
for r in self._outputrk:
self._outputrk_lst.insert(self._row, r)
-209
View File
@@ -1,209 +0,0 @@
# Window.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
from tools import timer, trace
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5 import QtCore
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel, QCoreApplication,
pyqtSlot, pyqtSignal, QItemSelectionModel,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.OutputRKAdisTS.Table import (
TableModel, ComboBoxDelegate,
)
from View.Network.GraphWidget import GraphWidget
from View.OutputRKAdisTS.Translate import OutputRKAdisTSTranslate
logger = logging.getLogger()
class OutputRKAdisTSWindow(PamhyrWindow):
_pamhyr_ui = "OutputRKAdisTS"
_pamhyr_name = "Output RK"
def __init__(self, study=None, config=None, parent=None):
trad = OutputRKAdisTSTranslate()
name = trad[self._pamhyr_name] + " - " + study.name
super(OutputRKAdisTSWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._outputrk_lst = self._study.river._Output_rk_adists
self.setup_table()
self.setup_checkbox()
self.setup_connections()
self.update()
def setup_table(self):
self._table = None
self._delegate_reach = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
parent=self,
mode="reaches"
)
self._delegate_rk = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
parent=self,
mode="rk"
)
table = self.find(QTableView, f"tableView")
self._table = TableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=["title", "reach", "rk"],
delegates={
"reach": self._delegate_reach,
"rk": self._delegate_rk,
},
trad=self._trad,
data=self._study.river,
undo=self._undo_stack,
)
selectionModel = table.selectionModel()
index = table.model().index(0, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
def setup_checkbox(self):
self._checkbox = self.find(QCheckBox, f"checkBox")
self._set_checkbox_state()
def setup_connections(self):
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_delete").triggered.connect(self.delete)
self._checkbox.clicked.connect(self._set_outputrk_state)
table = self.find(QTableView, "tableView")
table.selectionModel()\
.selectionChanged\
.connect(self.update)
self._table.dataChanged.connect(self.update)
self._table.layoutChanged.connect(self.update)
def index_selected(self):
table = self.find(QTableView, "tableView")
r = table.selectionModel().selectedRows()
if len(r) > 0:
return r[0]
else:
return None
def index_selected_row(self):
table = self.find(QTableView, "tableView")
r = table.selectionModel().selectedRows()
if len(r) > 0:
return r[0].row()
else:
return None
def index_selected_rows(self):
table = self.find(QTableView, "tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._outputrk_lst) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _copy(self):
logger.info("TODO: copy")
def _paste(self):
logger.info("TODO: paste")
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def _set_checkbox_state(self):
row = self.index_selected_row()
if row is None:
self._checkbox.setEnabled(False)
self._checkbox.setChecked(True)
else:
self._checkbox.setEnabled(True)
self._checkbox.setChecked(self._outputrk_lst.get(row).enabled)
def _set_outputrk_state(self):
rows = self.index_selected_rows()
if len(rows) != 0:
for row in rows:
if row is not None:
self._table.enabled(
row,
self._checkbox.isChecked()
)
def update(self):
self._set_checkbox_state()
+1
View File
@@ -137,6 +137,7 @@ class EditReservoirWindow(PamhyrWindow):
def update(self):
self.plot.update()
self.plot.fit_to_data()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
+1 -1
View File
@@ -95,7 +95,7 @@ class CustomPlot(PamhyrPlot):
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
rk = reach.geometry.get_rk()
# rk = reach.geometry.get_rk()
z = list(
map(
lambda p: table[ts, p.global_index],
+71 -6
View File
@@ -95,7 +95,7 @@ class PlotAC(PamhyrPlot):
def draw_profile(self, reach, profile):
x = profile.geometry.get_station()
z = profile.geometry.z()
z = self.get_bottom_elevation(profile)
self.line_rk, = self.canvas.axes.plot(
x, z,
@@ -110,7 +110,7 @@ class PlotAC(PamhyrPlot):
result = self.results[self._current_res_id]
x = profile.geometry.get_station()
z = profile.geometry.z()
z = self.get_bottom_elevation(profile)
rk = reach.geometry.get_rk()
water_z = result.get("table")["Z"][
result.get_timestamp_id(
@@ -139,7 +139,7 @@ class PlotAC(PamhyrPlot):
result = self.results[self._current_res_id]
x = profile.geometry.get_station()
z = profile.geometry.z()
z = self.get_bottom_elevation(profile)
rk = reach.geometry.get_rk()
water_z = np.max(
@@ -162,7 +162,7 @@ class PlotAC(PamhyrPlot):
return
station = profile.geometry.get_station()
elevation = profile.geometry.z()
elevation = self.get_bottom_elevation(profile)
gl = profile.geometry.names()
self.annotation = []
@@ -239,9 +239,11 @@ class PlotAC(PamhyrPlot):
self._current_reach_id)
profile = reach.profile(self._current_profile_id)
x = profile.geometry.get_station()
z = profile.geometry.z()
z = self.get_bottom_elevation(profile)
self.update_river_bottom(reach, profile, x, z)
self.update_water(reach, profile, x, z)
self.update_gl()
self.update_idle()
def update(self):
@@ -252,7 +254,7 @@ class PlotAC(PamhyrPlot):
.river.reach(self._current_reach_id)
profile = reach.profile(self._current_profile_id)
x = profile.geometry.get_station()
z = profile.geometry.z()
z = self.get_bottom_elevation(profile)
self.update_river_bottom(reach, profile, x, z)
self.update_water(reach, profile, x, z)
@@ -279,6 +281,69 @@ class PlotAC(PamhyrPlot):
def update_river_bottom(self, reach, profile, x, z):
self.line_rk.set_data(x, z)
def get_bottom_elevation(self, profile):
"""Return the cross-section elevation at the selected timestamp."""
initial_z = np.asarray(profile.geometry.z(), dtype=float)
result = self.results[self._current_res_id]
try:
sediment_timestamps = sorted(result.get("sediment_timestamps"))
except (KeyError, TypeError):
return initial_z
if not sediment_timestamps:
return initial_z
initial_timestamp = sediment_timestamps[0]
current_timestamp = min(
sediment_timestamps,
key=lambda timestamp: abs(timestamp - self._current_timestamp)
)
initial_layers = profile.get_ts_key(initial_timestamp, "sl")
current_layers = profile.get_ts_key(current_timestamp, "sl")
if not initial_layers or not current_layers:
return initial_z
initial_thickness = self._get_sediment_thickness(initial_layers)
current_thickness = self._get_sediment_thickness(current_layers)
point_count = len(initial_z)
wet_mask = self._get_wet_mask(profile, initial_z)
if wet_mask is None:
return initial_z
if len(initial_thickness) == len(current_thickness) == 1:
elevation_delta = current_thickness[0] - initial_thickness[0]
return initial_z + np.where(wet_mask, elevation_delta, 0.0)
if (len(initial_thickness) == len(current_thickness) == point_count):
elevation_delta = np.asarray(current_thickness) \
- np.asarray(initial_thickness)
return initial_z + np.where(wet_mask, elevation_delta, 0.0)
return initial_z
def _get_wet_mask(self, profile, elevation):
result = self.results[self._current_res_id]
try:
water_elevation = result.get("table")["Z"][
result.get_timestamp_id(self._current_timestamp),
profile.global_index
]
except (KeyError, IndexError, TypeError):
return None
return elevation <= water_elevation
@staticmethod
def _get_sediment_thickness(section_layers):
return [
sum(float(layer[0]) for layer in point_layers)
for point_layers in section_layers
]
def update_water(self, reach, profile, x, z):
result = self.results[self._current_res_id]
water_z = result.get("table")["Z"][
+27 -21
View File
@@ -62,6 +62,8 @@ class PlotRKC(PamhyrPlot):
self._auto_relim_update = True
self._autoscale_update = False
self.profile = None
@property
def results(self):
return self.data
@@ -71,9 +73,19 @@ class PlotRKC(PamhyrPlot):
self.data = results
self._current_timestamp = max(self._timestamps)
@staticmethod
def reach_geometry(reach):
"""Return geometry values for profiles present in the results."""
return (
reach.geometry.get_rk_enabled_profiles(),
reach.geometry.get_z_min_enabled_profiles(),
reach.geometry.get_z_max_enabled_profiles()
)
@timer
def draw(self, highlight=None):
self.init_axes()
self.profile = None
if self.results is None:
return
@@ -104,7 +116,7 @@ class PlotRKC(PamhyrPlot):
def draw_bottom_with_bedload(self, reach):
results = self.results[self._current_res_id]
rk = reach.geometry.get_rk()
rk, _, _ = self.reach_geometry(reach)
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
@@ -136,8 +148,7 @@ class PlotRKC(PamhyrPlot):
for hs in lhs:
x = hs.input_section.rk
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
_, z_min, z_max = self.reach_geometry(reach)
self.canvas.axes.plot(
[x, x],
@@ -157,9 +168,7 @@ class PlotRKC(PamhyrPlot):
)
def draw_bottom_geometry(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
rk, z_min, _ = self.reach_geometry(reach)
self.line_rk_zmin = self.canvas.axes.plot(
rk, z_min,
@@ -170,10 +179,9 @@ class PlotRKC(PamhyrPlot):
self._river_bottom = z_min
def draw_water_elevation(self, reach):
if len(reach.geometry.profiles) != 0:
if len(reach.profiles) != 0:
result = self.results[self._current_res_id]
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
rk, _, _ = self.reach_geometry(reach)
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
@@ -201,10 +209,9 @@ class PlotRKC(PamhyrPlot):
)
def draw_water_elevation_max(self, reach):
if len(reach.geometry.profiles) != 0:
if len(reach.profiles) != 0:
result = self.results[self._current_res_id]
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
rk, _, _ = self.reach_geometry(reach)
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
@@ -250,9 +257,7 @@ class PlotRKC(PamhyrPlot):
self.canvas.draw_idle()
def draw_current(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
rk, z_min, z_max = self.reach_geometry(reach)
self.profile, = self.canvas.axes.plot(
[
@@ -349,8 +354,7 @@ class PlotRKC(PamhyrPlot):
def update_water_elevation(self):
result = self.results[self._current_res_id]
reach = result.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
rk, _, _ = self.reach_geometry(reach)
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
@@ -380,11 +384,13 @@ class PlotRKC(PamhyrPlot):
self.update_idle()
def update_current(self):
if not self._init or self.profile is None:
self.draw()
return
results = self.results[self._current_res_id]
reach = results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
rk, z_min, z_max = self.reach_geometry(reach)
cid = self._current_profile_id
self.profile.set_data(
@@ -396,7 +402,7 @@ class PlotRKC(PamhyrPlot):
def update_bottom_with_bedload(self):
results = self.results[self._current_res_id]
reach = results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
rk, _, _ = self.reach_geometry(reach)
# z = self.sl_compute_current_z(reach)
table = results.get("table")["zfd"]
+8 -1
View File
@@ -63,6 +63,9 @@ class PlotAdis(PamhyrPlot):
self.label["C"] = _translate("Results", "Concentration")
self.label_max["C"] = _translate("Results", "Max 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
self.label["M"] = _translate("Results", "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.val_id = {}
self.val_id["C"] = 0
self.val_id["T"] = 0
self.val_id["G"] = 1
self.val_id["M"] = 2
self.val_id["D"] = 3
@@ -205,9 +209,12 @@ class PlotAdis_dx(PlotAdis):
self._current_timestamp, "pols"
)[pol_id][self.val_id[self._key]],
reach.profiles))
label = self.label[self._key]
if name != "TEM":
label += (" "+name)
self._lines.append(self.canvas.axes.plot(
x, y,
label=self.label[self._key]+" "+name,
label=label,
color=self.colors[pol_id % len(self.colors)],
**self.plot_default_kargs
)[0])
+128
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()
+7 -5
View File
@@ -527,14 +527,16 @@ class ResultsWindow(PamhyrWindow):
# self._additional_plot[plot].set_result(solver_id)
if timestamp is not None:
self.plot_xy.set_timestamp(timestamp)
self.plot_ac.set_timestamp(timestamp)
self.plot_rkc.set_timestamp(timestamp)
self.plot_xy.set_timestamp_preserve_view(timestamp)
self.plot_ac.set_timestamp_preserve_view(timestamp)
self.plot_rkc.set_timestamp_preserve_view(timestamp)
if tab_index == 2:
self.plot_h.set_timestamp(timestamp)
self.plot_h.set_timestamp_preserve_view(timestamp)
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:
# self._additional_plot[plot].set_timestamp(timestamp)
+6 -6
View File
@@ -532,12 +532,12 @@ class ResultsWindowAdisTS(PamhyrWindow):
self.plot_mdt.set_pollutant(self._current_pol_id)
if timestamp is not None:
self.plot_cdt.set_timestamp(timestamp)
self.plot_cdx.set_timestamp(timestamp)
self.plot_mdx.set_timestamp(timestamp)
self.plot_mdt.set_timestamp(timestamp)
self.plot_tdt.set_timestamp(timestamp)
self.plot_tdx.set_timestamp(timestamp)
self.plot_cdt.set_timestamp_preserve_view(timestamp)
self.plot_cdx.set_timestamp_preserve_view(timestamp)
self.plot_mdx.set_timestamp_preserve_view(timestamp)
self.plot_mdt.set_timestamp_preserve_view(timestamp)
self.plot_tdt.set_timestamp_preserve_view(timestamp)
self.plot_tdx.set_timestamp_preserve_view(timestamp)
self._table["raw_data"].set_timestamp(timestamp)
+756
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
+6 -2
View File
@@ -121,8 +121,11 @@ class ResultsTranslate(MainTranslate):
}
tmp = self._sub_dict["table_headers_raw_data"]
for pol in self.pollutants:
tmp[pol + " Concentration"] = pol + "\n Concentration"
tmp[pol + " Mass"] = pol + "\n Mass"
if pol == "TEM":
tmp[pol + " Temperature"] = "Temperature (°C)"
else:
tmp[pol + " Concentration"] = pol + "\n Concentration"
tmp[pol + " Mass"] = pol + "\n Mass"
else:
self._sub_dict["table_headers_raw_data"] = {
"name": _translate("Results", "Profile"),
@@ -146,6 +149,7 @@ class ResultsTranslate(MainTranslate):
self._sub_dict["values_y_pol"] = {
"unit_C": self._dict["unit_concentration"],
"unit_T": self._dict["unit_temperature"],
"unit_M": self._dict["unit_mass"],
"unit_thickness": self._dict["unit_thickness"],
}
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more