Compare commits

...
69 Commits
Author SHA1 Message Date
Dylan Jeannin 797de09683 InitialConditions: sort rows after each generation (single undo step), and fix undo of generation and sort (avoid STOP7 error on Hogneau example) 2026-10-26 16:55:01 +01:00
Dylan Jeannin ef10588880 BoundaryCondition: refit the edit plot to the data after each change 2026-10-26 16:06:55 +01:00
Dylan Jeannin c2a8e17bf3 HydraulicStructures: always show the reach plot, highlight the selected structure in blue, full zoom toolbars and fitted views 2026-10-26 15:52:46 +01:00
Dylan Jeannin a4eee962f7 Geometry: fix error on the first right click in the profile plot (water limits not yet cached) 2026-10-26 10:57:29 +01:00
Dylan Jeannin d8d1b4cd1f HydraulicStructures: fix crash when selecting a rk before the reach 2026-10-23 13:37:03 +02:00
Dylan Jeannin cdeca8a2e8 BoundaryCondition: restore back/forward and view buttons in the edit plot toolbar 2026-10-23 13:23:34 +02:00
Theophile Terraz bddaad0193 debug mesher 2026-09-23 09:28:42 +02:00
Dylan Jeannin 49eb28056e Mage: fix missing $ markers in VAR rules and separate exported check valve entries 2026-09-18 03:10:21 +02:00
Dylan Jeannin 5fb36e7fbe BoundaryConditions: fix index error in deletion for boundary cond. 2026-09-11 16:51:37 +02:00
Dylan Jeannin 71914fdf57 Merge branch 'master' of https://gitlab.com/pamhyr/pamhyr2 2026-09-06 23:15:24 +02:00
Dylan Jeannin 2d7409debb Export csv results: fix export flowrate as a function of time, on a study without sediments transport 2026-09-04 16:45:59 +02:00
parouby 92fb653676 Geometry: Point: Try to create index at each update. 2026-09-04 11:30:30 +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
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
parouby 5327c29641 Results: Custom plot: Use 'zfd' np table. 2026-07-09 18:05:10 +02:00
parouby be865102a8 Results: Put 'zfd' data into np table. 2026-07-09 16:53:48 +02:00
parouby 37f1b43b93 Debug: Fix pep8. 2026-07-09 10:16:23 +02:00
parouby 16f0775783 Pamhyr: Update all copyright. 2026-07-09 10:13:06 +02:00
parouby f2c4285b4c Debug: Minor change. 2026-07-09 09:03:11 +02:00
parouby ee95f0e89e Debug: Add csv export method. 2026-07-08 17:55:31 +02:00
parouby 1306977954 Debug: Add Timer window. 2026-07-08 16:38:41 +02:00
parouby 476fa81bc2 Results: Plot{AC,RKC}: Fix max elevation and minor change. 2026-07-07 21:56:19 +02:00
parouby e8f1f0ac43 Results: Add TableData pamhyr object for np table. 2026-07-03 17:03:33 +02:00
Dylan Jeannin cfbc2d96a1 Mage: Fix parsing for .LAT files 2026-07-03 13:07:10 +02:00
parouby 1adf24ce77 Geometry: ProfileXYZ: Minor optimisation of computation. 2026-07-02 15:04:33 +02:00
parouby a61d130baa Solver: Mage: Comment setter Z, Q and V into profile and bufferize and fix indent. 2026-07-02 11:43:00 +02:00
parouby 0a1760cee6 Results: CustomPlot: Continue to switch to np table. 2026-07-02 11:00:51 +02:00
parouby 3e094af325 Results: CustomPlot: Use np table. 2026-07-01 17:54:34 +02:00
parouby 762e4d70a0 Results: Exports: Use np.{min,max}. 2026-07-01 17:54:08 +02:00
parouby 6c4f863b96 Results: Export: Use np table. 2026-07-01 15:39:52 +02:00
parouby b87f82f74d Results: PlotRKC: Use table to get results table. 2026-06-30 16:57:42 +02:00
parouby ffad53a331 Results: Table: Use results tables. 2026-06-30 15:30:44 +02:00
parouby 662a218e41 Results: Reach: Keep profiles global min and max. 2026-06-26 16:51:58 +02:00
parouby 0501ae9bec Solver: Mage: read_bin: Use np table for velocity computation. 2026-06-26 16:20:32 +02:00
parouby 7fd38eec00 Results: Table: Minor change. 2026-06-26 14:54:55 +02:00
parouby 4bab4580ad Results: PlotXY: Use table Z to display max elevation. 2026-06-26 14:41:24 +02:00
parouby 21d2e6ad20 Results: PlotAC: Use Z table. 2026-06-26 14:23:00 +02:00
parouby d0a8f369ef Results: PlotH: Use np table. 2026-06-26 14:05:29 +02:00
parouby fef2e1a40c Results: PlotXY: Minor change. 2026-06-26 10:51:36 +02:00
parouby 20c2cb7ea4 Merge branch 'master' into opt-result 2026-06-26 09:29:39 +02:00
parouby 64a26e987e ci: Fix again. 2026-06-25 15:48:41 +02:00
parouby eb233847de ci: Fix pipeline for release. 2026-06-25 15:42:46 +02:00
parouby 7bb810d427 Pollutants: Fix 'enabled' column update version. 2026-06-25 15:00:41 +02:00
Dylan Jeannin e49724072b Merge branch 'dev_dylan' into scenarios 2026-06-25 14:41:24 +02:00
Dylan Jeannin 3664ba93e9 Results: Fix open mage results from AdisTS solver 2026-06-25 14:39:45 +02:00
Dylan Jeannin a9903e987b AdisTS: Resolve bug with node ids not matching in config files 2026-06-25 14:05:04 +02:00
Dylan Jeannin f9a88a211c Fix pep8 2026-06-22 17:10:57 +02:00
Dylan Jeannin 32535af3e6 UpdateRK: avoid crash when updating RK with missing points on a section, warning message instead 2026-06-22 17:04:39 +02:00
Dylan Jeannin fa8948c1b6 Meshing: Debug error caused by index out of range when adding an empty transect on a reach 2026-06-22 15:18:08 +02:00
Dylan Jeannin 2d5455c09d Meshing: Debug crashing case when divide by 0 2026-06-22 14:16:33 +02:00
Dylan Jeannin 83680bbe57 Pollutant: quickfix, back to previous version 2026-06-22 12:12:02 +02:00
Dylan Jeannin 1d1a00a0ff Pollutants: merge conflicts 2026-06-22 10:42:29 +02:00
Dylan Jeannin ffd95b9fdf Pollutants: merge conflicts 2026-06-22 10:42:07 +02:00
parouby ecb2544cab Results: Keep timestamps index table. 2026-06-19 15:25:40 +02:00
parouby 9c66021b53 Results: Give access profiles to np table and keep profile global id. 2026-06-19 14:54:26 +02:00
parouby 8255fc3c1b River: Add some timer. 2026-06-19 09:23:42 +02:00
parouby 32e7413aad Solver: Mage: read_bin: Put velocity into np table. 2026-06-18 15:45:11 +02:00
parouby d87db5fd48 Solver: Mage: read_bin: Optimize iprofiles. 2026-06-18 15:02:34 +02:00
parouby 388368adad Solver: Mage: Add data np table to results. 2026-06-18 14:29:50 +02:00
parouby 4d014d5a1d Results: Minor change. 2026-06-18 10:11:17 +02:00
parouby 181a6c226f Geometry: ProfileXYZ: Add 'get_all_water_limits_ac' cache. 2026-06-18 10:09:51 +02:00
parouby 1975ed7eaa Profile: Create cache for 'get_water_limits' results. 2026-06-17 17:41:06 +02:00
parouby 565a7530fb Results: Minor results reading optimisation with async computation. 2026-06-17 17:04:58 +02:00
Dylan Jeannin 48d6728e1e Enable pollutant: fix the list to browse to enable a pol 2026-05-28 15:11:06 +02:00
Dylan Jeannin c51c06e6ec DB version: Upgrade to add enabled in pollutants db 2026-05-28 15:02:40 +02:00
Dylan Jeannin a425c9a6ac BoundaryConditions: delete setter/getter duplicated for d50 and sigma 2026-05-28 15:01:36 +02:00
289 changed files with 1891 additions and 918 deletions

No files matched your search

+2 -2
View File
@@ -638,7 +638,7 @@ shadow-release:
- job: pkg-hash - job: pkg-hash
artifacts: true artifacts: true
rules: rules:
- if: $CI_COMMIT_BRANCH != 'master' && $CI_COMMIT_TAG - if: $CI_COMMIT_TAG && $CI_COMMIT_TAG !~ /^v/
artifacts: artifacts:
paths: paths:
- linux/pamhyr-gnulinux.tar.xz - linux/pamhyr-gnulinux.tar.xz
@@ -672,7 +672,7 @@ tag-release:
- job: pkg-hash - job: pkg-hash
artifacts: true artifacts: true
rules: rules:
- if: $CI_COMMIT_BRANCH == 'master' && $CI_COMMIT_TAG - if: $CI_COMMIT_TAG =~ /^v/
artifacts: artifacts:
paths: paths:
- linux/pamhyr-gnulinux.tar.xz - linux/pamhyr-gnulinux.tar.xz
+1 -1
View File
@@ -1,4 +1,4 @@
Sylvain COULIBALY, INRAE, 2020 - 2022 Sylvain COULIBALY, INRAE, 2020 - 2022
Théophile TERRAZ, INRAE, 2022 - 2024 Théophile TERRAZ, INRAE, 2022 - 2024
Pierre-Antoine ROUBY, INRAE, 2023 - 2025 Pierre-Antoine ROUBY, INRAE, 2023 - 2026
Dylan JEANNIN, INRAE, 2026 Dylan JEANNIN, INRAE, 2026
+1 -1
View File
@@ -1,5 +1,5 @@
# Adists.py -- Pamhyr study checkers # Adists.py -- Pamhyr study checkers
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Checker.py -- Pamhyr abstract checker class # Checker.py -- Pamhyr abstract checker class
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr MAGE checkers # Mage.py -- Pamhyr MAGE checkers
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Study.py -- Pamhyr study checkers # Study.py -- Pamhyr study checkers
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# AMeshingTool.py -- Pamhyr # AMeshingTool.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+111 -62
View File
@@ -1,5 +1,5 @@
# Internal.py -- Pamhyr # Internal.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -57,6 +57,13 @@ class InternalMeshing(AMeshingTool):
# we never modify original profiles, we work on the copies # we never modify original profiles, we work on the copies
m_profiles = self.st_to_m(profiles, guide_list) 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) new_profiles = self.interpolate_transversal_step(m_profiles, step)
for new_profiles2 in new_profiles: for new_profiles2 in new_profiles:
@@ -65,6 +72,12 @@ class InternalMeshing(AMeshingTool):
return new_profiles 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): def st_to_m(self, profiles, guide_list):
guide_list = ["un"] + guide_list + ["np"] guide_list = ["un"] + guide_list + ["np"]
max_values = [0] * (len(guide_list) - 1) max_values = [0] * (len(guide_list) - 1)
@@ -158,15 +171,14 @@ class InternalMeshing(AMeshingTool):
ltot1 = 0.0 ltot1 = 0.0
ltot2 = 0.0 ltot2 = 0.0
alpha = [] alpha = [0.0]
beta = [] beta = [0.0]
# remove names # remove names
sect2.points[start2].name = '' sect2.points[start2].name = ''
sect2.points[end2].name = '' sect2.points[end2].name = ''
for i in range(len1): for i in range(len1):
ltot1 += sect1.point(start1+i).dist(sect1.point(start1+i+1)) ltot1 += sect1.point(start1+i).dist(sect1.point(start1+i+1))
alpha.append(ltot1) alpha.append(ltot1)
alpha = list(map(lambda x: x/ltot1, alpha)) # target ratios
for i in range(len2): for i in range(len2):
ltot2 += sect2.point(start2+i).dist(sect2.point(start2+i+1)) ltot2 += sect2.point(start2+i).dist(sect2.point(start2+i+1))
beta.append(ltot2) beta.append(ltot2)
@@ -181,68 +193,40 @@ class InternalMeshing(AMeshingTool):
sect2.add_npoints(len1-len2) sect2.add_npoints(len1-len2)
len2 = len1 len2 = len1
else: # regular case else: # regular case
alpha = list(map(lambda x: x/ltot1, alpha)) # target ratios
beta = list(map(lambda x: x/ltot2, beta)) # current ratios beta = list(map(lambda x: x/ltot2, beta)) # current ratios
for i in range(len1 - len2): match = self._match_indices(alpha, beta)
beta.append(1.0)
beta.append(0.0) # beta[-1]
# loop on the points to insert
for k in range(len1-len2):
trouve = False
for j0 in range(len2):
if trouve:
break
undeplus = True
for j1 in range(j0, len2):
if undeplus:
if beta[j1] <= alpha[j1]:
undeplus = False
break
if undeplus:
if j0 == len2:
trouve = True
else:
j1 = j0+1
if beta[j0]-alpha[j0] > alpha[j1]-beta[j0]:
trouve = True
if trouve: insertions_per_segment = [] # liste de (index, [points])
for j in range(len2):
i_start = match[j]
i_end = match[j + 1]
q_start = sect2.point(start2 + j)
q_end = sect2.point(start2 + j + 1)
new_points = []
if i_end - i_start > 1:
u_start, u_end = alpha[i_start], alpha[i_end]
span = u_end - u_start
for k in range(i_start + 1, i_end):
ratio = (alpha[k] - u_start) / span # dans [0,1]
p = q_start.copy()
p.name = ''
p.x = (1.0-ratio) * q_start.x + ratio * q_end.x
p.y = (1.0-ratio) * q_start.y + ratio * q_end.y
p.z = (1.0-ratio) * q_start.z + ratio * q_end.z
new_points.append(p)
# à insérer après l'indice j
insertions_per_segment.append((j, new_points))
# Insertion en ordre décroissant de j
# pour ne pas perturber les indices
for j, new_points in reversed(insertions_per_segment):
for offset, p in enumerate(new_points):
sect2.insert_point(start2 + j + 1 + offset, p)
len2 += 1 len2 += 1
for i in reversed(range(j0, len2)):
beta[i+1] = beta[i]
if (beta[j0] == beta[j0-1]):
# rien
pass
else:
ratio = (alpha[j0] - beta[j0-1]) \
/ (beta[j0] - beta[j0-1])
if ratio < 0.0:
logger.warning(f"ratio négatif {ratio}")
# on double le point a gauche
p = sect2.point(start2+j0-1).copy()
sect2.insert_point(start2+j0-1, p)
sect2.point(start2+j0-1).name = ''
beta[j0] = beta[j0-1]
else:
p = sect2.point(start2+j0).copy()
sect2.insert_point(start2+j0, p)
sect2.point(start2+j0+1).name = ''
sect2.point(start2+j0+1).x = \
(1.0-ratio) * sect2.point(start2+j0).x + \
ratio * sect2.point(start2+j0+2).x
sect2.point(start2+j0+1).y = \
(1.0-ratio) * sect2.point(start2+j0).y + \
ratio * sect2.point(start2+j0+2).y
sect2.point(start2+j0+1).z = \
(1.0-ratio) * sect2.point(start2+j0).z + \
ratio * sect2.point(start2+j0+2).z
beta[j0] = (1.-ratio)*beta[j0-1] \
+ ratio*beta[j0+1]
if len1 > len2:
for i in range(len1 - len2):
p = sect2.point(start2+len2).copy()
sect2.insert_point(start2+len2, p)
sect2.point(start2+len2).name = ''
if tag1 != "un": if tag1 != "un":
sect2.point(start2).name = tag1 sect2.point(start2).name = tag1
if tag2 != "np": if tag2 != "np":
@@ -250,6 +234,71 @@ class InternalMeshing(AMeshingTool):
sect2.modified() sect2.modified()
def _match_indices(self, u_ref, v_target):
# appariement optimal (ordre croissant) par programmation dynamique
"""
Trouve, pour chaque v_target[j], un indice i_j dans u_ref tel que
i_0 < i_1 < ... < i_{m-1} et
Sum |u_ref[i_j] - v_target[j]| soit minimal.
Retourne la liste des indices i_j (longueur m),
avec i_0 = 0 et i_{m-1} = n-1 imposés
(les extrémités se correspondent forcément).
"""
n = len(u_ref)
m = len(v_target)
# Les extrémités sont fixées.
fixed_first, fixed_last = 0, n - 1
# DP sur les indices disponibles entre fixed_first+1 et fixed_last-1
# dp[j][i] = coût minimal pour apparier v_target[1..j] aux indices
# se terminant à l'indice i (i croissant strict), j de 1 à m-2.
if m == 2:
return [fixed_first, fixed_last]
# indices internes possibles
candidates = list(range(fixed_first + 1, fixed_last))
inner_targets = v_target[1:-1] # cibles internes (j = 1..m-2)
INF = float("inf")
# dp[j] : dict indice_candidat -> (coût_min, provenance)
dp = [{} for _ in range(len(inner_targets))]
for j, vt in enumerate(inner_targets):
for idx_pos, i in enumerate(candidates):
cost_here = abs(u_ref[i] - vt)
if j == 0:
# doit être > fixed_first
dp[j][i] = (cost_here, fixed_first)
else:
best = (INF, None)
for i_prev in candidates[:idx_pos]:
if i_prev in dp[j - 1]:
c = dp[j - 1][i_prev][0] + cost_here
if c < best[0]:
best = (c, i_prev)
if best[1] is not None:
dp[j][i] = best
# Reconstruction : on part du dernier niveau, on choisit le
# meilleur indice tel que la suite jusqu'à fixed_last
# reste croissante et valide.
last_level = dp[-1]
valid = {i: c for i, c in last_level.items() if i < fixed_last}
if not valid:
raise RuntimeError("Alignement impossible avec ces données.")
best_i = min(valid, key=lambda i: valid[i][0])
chain = [best_i]
for j in range(len(inner_targets) - 1, 0, -1):
prev_i = dp[j][chain[-1]][1]
chain.append(prev_i)
chain.reverse()
return [fixed_first] + chain + [fixed_last]
def update_rk(self, reach, begin_rk, end_rk, def update_rk(self, reach, begin_rk, end_rk,
step: float = 50, step: float = 50,
limites=[-1, -1], limites=[-1, -1],
+1 -1
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr # Mage.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# __init__.py -- Pamhyr # __init__.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# test_Model.py -- Pamhyr # test_Model.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# BoundaryCondition.py -- Pamhyr # BoundaryCondition.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -514,42 +514,6 @@ class BoundaryCondition(SQLSubModel):
self._sigma = float(value) self._sigma = float(value)
self.modified() self.modified()
@property
def d50(self):
return self._d50
@d50.setter
def d50(self, value):
self._d50 = float(value)
self.modified()
@property
def sigma(self):
return self._sigma
@sigma.setter
def sigma(self, value):
self._sigma = float(value)
self.modified()
@property
def d50(self):
return self._d50
@d50.setter
def d50(self, value):
self._d50 = float(value)
self.modified()
@property
def sigma(self):
return self._sigma
@sigma.setter
def sigma(self, value):
self._sigma = float(value)
self.modified()
@property @property
def header(self): def header(self):
return self._header.copy() return self._header.copy()
@@ -1,5 +1,5 @@
# BoundaryConditionList.py -- Pamhyr # BoundaryConditionList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# BoundaryConditionTypes.py -- Pamhyr # BoundaryConditionTypes.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# BoundaryConditionsAdisTS.py -- Pamhyr # BoundaryConditionsAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# BoundaryConditionsAdisTSList.py -- Pamhyr # BoundaryConditionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# D90AdisTS.py -- Pamhyr # D90AdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# D90AdisTSList.py -- Pamhyr # D90AdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# D90AdisTSSpec.py -- Pamhyr # D90AdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# DIFAdisTS.py -- Pamhyr # DIFAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# DIFAdisTSList.py -- Pamhyr # DIFAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# DIFAdisTSSpec.py -- Pamhyr # DIFAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Except.py -- Pamhyr model exceptions # Except.py -- Pamhyr model exceptions
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Friction.py -- Pamhyr # Friction.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# FrictionList.py -- Pamhyr # FrictionList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Point.py -- Pamhyr # Point.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# PointXY.py -- Pamhyr # PointXY.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+3 -4
View File
@@ -1,5 +1,5 @@
# PointXYZ.py -- Pamhyr # PointXYZ.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -95,10 +95,9 @@ class PointXYZ(Point):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
) )
if major == "0" and minor == "2": # Try to create index at each update
if int(release) < 5:
execute( execute(
"CREATE INDEX idx_point " + "CREATE INDEX IF NOT EXISTS idx_point " +
"ON geometry_pointXYZ(profile, scenario);" "ON geometry_pointXYZ(profile, scenario);"
) )
+1 -1
View File
@@ -1,5 +1,5 @@
# Profile.py -- Pamhyr # Profile.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+72 -33
View File
@@ -1,5 +1,5 @@
# ProfileXYZ.py -- Pamhyr # ProfileXYZ.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -17,6 +17,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import queue import queue
import types
import random import random
import logging import logging
import numpy as np import numpy as np
@@ -91,6 +92,9 @@ class ProfileXYZ(Profile, SQLSubModel):
self._station = [] self._station = []
self.station_up_to_date = False self.station_up_to_date = False
self._get_water_limits_cache = {}
self._get_water_limits_ac_cache = {}
@classmethod @classmethod
def _db_create(cls, execute, ext=""): def _db_create(cls, execute, ext=""):
execute(f""" execute(f"""
@@ -619,6 +623,7 @@ class ProfileXYZ(Profile, SQLSubModel):
""" """
return [x for x in lst if not np.isnan(x)] return [x for x in lst if not np.isnan(x)]
@timer
def speed(self, q, z): def speed(self, q, z):
area = self.wet_area(z) area = self.wet_area(z)
@@ -670,6 +675,7 @@ class ProfileXYZ(Profile, SQLSubModel):
length += line.length length += line.length
return length return length
@timer
def compute_wet_area(self, z): def compute_wet_area(self, z):
area = 0.0 area = 0.0
if len(self.tab.L) > 0: if len(self.tab.L) > 0:
@@ -710,6 +716,7 @@ class ProfileXYZ(Profile, SQLSubModel):
if len(line.coords) > 2: if len(line.coords) > 2:
poly = geometry.Polygon(line) poly = geometry.Polygon(line)
area += poly.area area += poly.area
return area return area
def wet_radius(self, z): def wet_radius(self, z):
@@ -732,6 +739,7 @@ class ProfileXYZ(Profile, SQLSubModel):
line = geometry.LineString(list(zip(station, zz))) line = geometry.LineString(list(zip(station, zz)))
return line return line
@timer
def wet_lines(self, z): def wet_lines(self, z):
points = self._points points = self._points
if len(points) < 3: if len(points) < 3:
@@ -799,7 +807,11 @@ class ProfileXYZ(Profile, SQLSubModel):
return poly return poly
def wet_points(self, z): def wet_points(self, z):
left, right = self.get_water_limits(z) limits = self.get_water_limits(z)
if isinstance(limits, types.GeneratorType):
limits = next(limits)
left, right = limits
points = list(filter(lambda p: p.z < z, self._points)) points = list(filter(lambda p: p.z < z, self._points))
points = [left] + points + [right] points = [left] + points + [right]
@@ -808,7 +820,6 @@ class ProfileXYZ(Profile, SQLSubModel):
return points return points
def get_nb_wet_areas(self, z): def get_nb_wet_areas(self, z):
n_zones = 0 n_zones = 0
points = self._points points = self._points
if points[0].z <= z: if points[0].z <= z:
@@ -820,10 +831,13 @@ class ProfileXYZ(Profile, SQLSubModel):
return n_zones return n_zones
@timer
def get_all_water_limits_ac(self, z): def get_all_water_limits_ac(self, z):
""" """
Determine all water limits for z elevation. Determine all water limits for z elevation.
""" """
if z in self._get_water_limits_ac_cache:
return self._get_water_limits_ac_cache[z]
points = self._points points = self._points
if len(points) < 3: if len(points) < 3:
@@ -856,69 +870,94 @@ class ProfileXYZ(Profile, SQLSubModel):
logger.error(f"ERROR in get_all_water_limits_ac") logger.error(f"ERROR in get_all_water_limits_ac")
return [], [] return [], []
return start, list(reversed(end)) res = start, list(reversed(end))
self._get_water_limits_ac_cache[z] = res
return res
@timer
def get_water_limits(self, z): def get_water_limits(self, z):
if z in self._get_water_limits_cache:
return self._get_water_limits_cache[z]
v = self.get_water_limits_compute(z)
return v
def get_water_limits_compute(self, z):
""" """
Determine left and right limits of water elevation. Determine left and right limits of water elevation.
""" """
# Get the index of first point with elevation lesser than water # Get the index of first point with elevation lesser than water
# elevation (for the right and left river side) # elevation (for the right and left river side)
i_left = -1 i_left = next(
i_right = -1 filter(
lambda i: self.point(i).z <= z,
range(self.number_points)
),
-1
)
for i in range(self.number_points): i_right = next(
if self.point(i).z <= z: filter(
i_left = i lambda i: self.point(i).z <= z,
break reversed(range(self.number_points))
),
for i in reversed(range(self.number_points)): -1
if self.point(i).z <= z: )
i_right = i
break
# Interpolate points at river left side # Interpolate points at river left side
if (i_left > 0): if (i_left > 0):
if abs(self.point(i_left - 1).z - self.point(i_left).z) < 1e-20: pi = self.point(i_left)
pt_left = self.point(i_left) pim1 = self.point(i_left - 1)
if abs(pim1.z - pi.z) < 1e-20:
pt_left = pi
else: else:
fact = (z - self.point(i_left).z) / \ fact = (z - pi.z) / (pim1.z - pi.z)
(self.point(i_left - 1).z - self.point(i_left).z) x = pi.x + fact * (pim1.x - pi.x)
x = self.point(i_left).x + fact * \ y = pi.y + fact * (pim1.y - pi.y)
(self.point(i_left - 1).x - self.point(i_left).x)
y = self.point(i_left).y + fact * \
(self.point(i_left - 1).y - self.point(i_left).y)
pt_left = PointXYZ(x=x, y=y, z=z, name="wl_left") pt_left = PointXYZ(x=x, y=y, z=z, name="wl_left")
else: else:
pt_left = self.point(0) pt_left = self.point(0)
# Interpolate points at river right side # Interpolate points at river right side
if (i_right < self.number_points - 1): if (i_right < self.number_points - 1):
if abs(self.point(i_right + 1).z - self.point(i_right).z) < 1e-20: pi = self.point(i_right)
pt_right = self.point(i_right) pip1 = self.point(i_right + 1)
if abs(pip1.z - pi.z) < 1e-20:
pt_right = pi
else: else:
fact = (z - self.point(i_right).z) / \ fact = (z - pi.z) / (pip1.z - pi.z)
(self.point(i_right + 1).z - self.point(i_right).z) x = pi.x + fact * (pip1.x - pi.x)
x = self.point(i_right).x + fact * \ y = pi.y + fact * (pip1.y - pi.y)
(self.point(i_right + 1).x - self.point(i_right).x)
y = self.point(i_right).y + fact * \
(self.point(i_right + 1).y - self.point(i_right).y)
pt_right = PointXYZ(x=x, y=y, z=z, name="wl_right") pt_right = PointXYZ(x=x, y=y, z=z, name="wl_right")
else: else:
pt_right = self.point(self.number_points - 1) pt_right = self.point(self.number_points - 1)
return pt_left, pt_right v = (pt_left, pt_right)
# Put results in cache
self._get_water_limits_cache[z] = v
# Create a generator to improve results data reading speed
yield v
@timer
def compute_tabulation(self): def compute_tabulation(self):
sorted_points = sorted(self._points, key=lambda p: p.z) sorted_points = sorted(self._points, key=lambda p: p.z)
self.tab.z = np.array([p.z for p in sorted_points], np.float64) self.tab.z = np.array([p.z for p in sorted_points], np.float64)
self.tab.L = np.zeros(len(self.tab.z), np.float64) self.tab.L = np.zeros(len(self.tab.z), np.float64)
self.tab.A = np.zeros(len(self.tab.z), np.float64) self.tab.A = np.zeros(len(self.tab.z), np.float64)
for i in range(1, len(self.tab.z)): for i in range(1, len(self.tab.z)):
self.tab.L[i] = self.compute_wet_width(self.tab.z[i]) self.tab.L[i] = self.compute_wet_width(self.tab.z[i])
dx = (self.tab.L[i] + self.tab.L[i-1])/2 dx = (self.tab.L[i] + self.tab.L[i-1])/2
dz = self.tab.z[i] - self.tab.z[i-1] dz = self.tab.z[i] - self.tab.z[i-1]
self.tab.A[i] = self.tab.A[i-1] + dz * dx self.tab.A[i] = self.tab.A[i-1] + dz * dx
self.tab_up_to_date = True self.tab_up_to_date = True
+1 -1
View File
@@ -1,5 +1,5 @@
# Reach.py -- Pamhyr # Reach.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Vector_1d.py -- Pamhyr # Vector_1d.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# HydraulicStructures.py -- Pamhyr # HydraulicStructures.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Types.py -- Pamhyr # Types.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Value.py -- Pamhyr # Value.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# HydraulicStructures.py -- Pamhyr # HydraulicStructures.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# HydraulicStructuresList.py -- Pamhyr # HydraulicStructuresList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# InitialConditions.py -- Pamhyr # InitialConditions.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# InitialConditionsDict.py -- Pamhyr # InitialConditionsDict.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# InitialConditionsAdisTS.py -- Pamhyr # InitialConditionsAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSList.py -- Pamhyr # InitialConditionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSSpec.py -- Pamhyr # InitialConditionsAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# LateralContribution.py -- Pamhyr # LateralContribution.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# LateralContributionList.py -- Pamhyr # LateralContributionList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# LateralContributionTypes.py -- Pamhyr # LateralContributionTypes.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# LateralContributionAdisTS.py -- Pamhyr # LateralContributionAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# LateralContributionsAdisTSList.py -- Pamhyr # LateralContributionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Edge.py -- Pamhyr # Edge.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Graph.py -- Pamhyr # Graph.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Node.py -- Pamhyr # Node.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Point.py -- Pamhyr # Point.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# __init__.py -- Pamhyr # __init__.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# test_Network.py -- Pamhyr # test_Network.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# OutputRKAdists.py -- Pamhyr # OutputRKAdists.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+17 -3
View File
@@ -1,5 +1,5 @@
# Pollutants.py -- Pamhyr # Pollutants.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -350,6 +350,7 @@ class Pollutants(SQLSubModel):
CREATE TABLE pollutants{ext}( CREATE TABLE pollutants{ext}(
{cls.create_db_add_pamhyr_id()}, {cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
name TEXT NOT NULL, name TEXT NOT NULL,
{Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()} {Scenario.create_db_add_scenario_fk()}
@@ -373,6 +374,14 @@ class Pollutants(SQLSubModel):
cls._db_create(execute) cls._db_create(execute)
created = True created = True
if major == "0" and minor == "2":
if int(release) < 6:
execute(f"ALTER TABLE pollutants " +
f"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE")
# cls._db_update_to_0_2_5(execute, data)
# # created = True
if not created: if not created:
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@@ -410,7 +419,8 @@ class Pollutants(SQLSubModel):
return new return new
table = execute( table = execute(
"SELECT pamhyr_id, deleted, name, scenario FROM pollutants " + "SELECT pamhyr_id, deleted, enabled, name, scenario " +
"FROM pollutants " +
f"WHERE scenario = {scenario.id} " + f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})" f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
) )
@@ -421,6 +431,7 @@ class Pollutants(SQLSubModel):
pid = next(it) pid = next(it)
deleted = (next(it) == 1) deleted = (next(it) == 1)
enabled = (next(it) == 1)
name = next(it) name = next(it)
owner_scenario = next(it) owner_scenario = next(it)
@@ -431,6 +442,8 @@ class Pollutants(SQLSubModel):
) )
if deleted: if deleted:
new_pollutant.set_as_deleted() new_pollutant.set_as_deleted()
if not enabled:
new_pollutant.enabled = False
data["pollutant"] = new_pollutant data["pollutant"] = new_pollutant
new_pollutant._data = PollutantCharacteristics._db_load( new_pollutant._data = PollutantCharacteristics._db_load(
@@ -472,9 +485,10 @@ class Pollutants(SQLSubModel):
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"pollutants(pamhyr_id, deleted, name, scenario) " + "pollutants(pamhyr_id, deleted, enabled, name, scenario) " +
"VALUES (" + "VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " + f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._db_format(self._enabled)}, " +
f"'{self._db_format(self._name)}', " + f"'{self._db_format(self._name)}', " +
f"{self._status.scenario_id}" + f"{self._status.scenario_id}" +
")" ")"
+1 -1
View File
@@ -1,5 +1,5 @@
# Reservoir.py -- Pamhyr # Reservoir.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# ReservoirList.py -- Pamhyr # ReservoirList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+236 -3
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr # Results.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@ import numpy as np
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime
from tools import timer
from Model.Scenario import Scenario from Model.Scenario import Scenario
from Model.Tools.PamhyrDB import SQLSubModel from Model.Tools.PamhyrDB import SQLSubModel
from Model.Results.River.River import River from Model.Results.River.River import River
@@ -155,8 +157,185 @@ class AdditionalData(SQLSubModel):
return True return True
class TableData(SQLSubModel):
_sub_classes = []
def __init__(self, id=-1, study=None,
legend="", data=None,
owner_scenario=-1):
super(TableData, self).__init__(
id=id, status=study.status,
owner_scenario=owner_scenario
)
self._study = study
self._legend = legend
self._data = data
@property
def data(self):
return self._data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results_table_data{ext} (
{cls.create_db_add_pamhyr_id()},
result INTEGER NOT NULL,
legend TEXT NOT NULL,
unit TEXT NOT NULL,
data_shape_x INTEGER NOT NULL,
data_shape_y INTEGER NOT NULL,
data_shape_z INTEGER DEFAULT -1,
data BLOB NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(result) REFERENCES results(pamhyr_id),
PRIMARY KEY(pamhyr_id, result, legend, scenario)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
create = False
if major == "0" and int(minor) == 2:
if int(release) < 7 and not create:
cls._db_create(execute)
create = True
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
res = {}
study = data['study']
status = data['status']
scenario = data["scenario"]
result = data["result"]
rid = data["result_pid"]
table = execute(
"SELECT pamhyr_id, " +
"legend, unit, " +
"data_shape_x, data_shape_y, data_shape_z, " +
"data, " +
"scenario " +
"FROM results_table_data " +
f"WHERE scenario = {scenario.id} " +
f"AND result = {rid}"
)
for v in table:
it = iter(v)
pid = next(it)
legend = next(it)
unit = next(it)
data_shape_x = next(it)
data_shape_y = next(it)
data_shape_z = next(it)
data = next(it)
owner_scenario = next(it)
dtype = cls._unit_to_dtype(unit)
np_data = np.frombuffer(data, dtype=dtype)
if data_shape_y == -1:
np_data = np.reshape(np_data, data_shape_x)
elif data_shape_z == -1:
np_data = np.reshape(np_data, (data_shape_x,
data_shape_y))
else:
np_data = np.reshape(np_data, (data_shape_x,
data_shape_y,
data_shape_z))
new_table = cls(study=study,
legend=legend, data=np_data,
owner_scenario=owner_scenario)
res[legend] = new_table
return res
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
return
shape_x = -1
shape_y = -1
shape_z = -1
shape = self._data.shape
if len(shape) == 2:
shape_x, shape_y = shape
elif len(shape == 1):
shape_x, = shape
elif len(shape == 3):
shape_x, shape_y, shape_z = shape
else:
logger.error(
f"Failed to parse numpy shape: {shape}"
)
execute(
"INSERT INTO " +
"results_table_data (pamhyr_id, result, " +
"legend, unit, " +
"data_shape_x, data_shape_y, data_shape_z, " +
"data, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, data["result"],
self._legend, self._dtype_to_unit(self._data.dtype),
shape_x, shape_y, shape_z,
self._data,
self._owner_scenario
)
return True
@classmethod
def _unit_to_dtype(cls, unit):
t = {
"i32": np.int32, "i64": np.int64,
"u32": np.uint32, "u64": np.uint64,
"f32": np.float32, "f64": np.float64,
}
return t[unit]
def _dtype_to_unit(self, dtype):
t = {
np.dtype(np.int32): "i32", np.dtype(np.int64): "i64",
np.dtype(np.uint32): "u32", np.dtype(np.uint64): "u64",
np.dtype(np.float32): "f32", np.dtype(np.float64): "f64",
}
return t[dtype]
def table(self):
return self._data
def __len__(self):
return len(self._data)
def __getitem__(self, key):
if isinstance(key, tuple):
return self._data[key[0], key[1]]
self._date[key]
class Results(SQLSubModel): class Results(SQLSubModel):
_sub_classes = [River, AdditionalData] _sub_classes = [River, TableData, AdditionalData]
def __init__(self, id=-1, study=None, solver=None, def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0"): repertory="", name="0"):
@@ -167,7 +346,7 @@ class Results(SQLSubModel):
self._name = name self._name = name
self._study = study self._study = study
self._river = River(self._study) self._river = River(self._study, self)
self._solver = solver self._solver = solver
self._repertory = repertory self._repertory = repertory
@@ -177,8 +356,11 @@ class Results(SQLSubModel):
"creation_date": datetime.now(), "creation_date": datetime.now(),
"study_revision": study.status.version, "study_revision": study.status.version,
"additional_data": [], "additional_data": [],
"table": {},
} }
self._ts_index = {}
if solver is not None: if solver is not None:
self.set("solver_type", solver._type) self.set("solver_type", solver._type)
@@ -195,6 +377,7 @@ class Results(SQLSubModel):
def study(self): def study(self):
return self._study return self._study
@timer
def set(self, key, value): def set(self, key, value):
self._meta_data[key] = value self._meta_data[key] = value
@@ -212,6 +395,16 @@ class Results(SQLSubModel):
def get(self, key): def get(self, key):
return self._meta_data[key] return self._meta_data[key]
def set_timestamp_index(self, index):
self._ts_index = index
def get_timestamp_id(self, ts):
return self._ts_index[ts]
def compute_timestamp_index(self):
for i, ts in enumerate(self._meta_data["timestamps"]):
self._ts_index[ts] = i
def reload(self): def reload(self):
return self._solver.results( return self._solver.results(
self._study, self._study,
@@ -309,11 +502,20 @@ class Results(SQLSubModel):
new_results.set("study_revision", revision) new_results.set("study_revision", revision)
new_results.set("creation_date", creation_date) new_results.set("creation_date", creation_date)
data["result_pid"] = pid
data["result"] = new_results
table_data = TableData._db_load(execute, data)
new_results.set("table", table_data)
sf = ">" + ''.join(itertools.repeat("d", nb_timestamps)) sf = ">" + ''.join(itertools.repeat("d", nb_timestamps))
ts = struct.unpack(sf, timestamps_bytes) ts = struct.unpack(sf, timestamps_bytes)
new_results.set("timestamps", ts) new_results.set("timestamps", ts)
data["timestamps"] = sorted(ts) data["timestamps"] = sorted(ts)
new_results.compute_timestamp_index()
data["parent"] = new_results
new_results._river = River._db_load(execute, data) new_results._river = River._db_load(execute, data)
new_results.set( new_results.set(
@@ -321,6 +523,20 @@ class Results(SQLSubModel):
AdditionalData._db_load(execute, data) AdditionalData._db_load(execute, data)
) )
# Update global index
i = 0
for reach in new_results._river.reachs:
nb = len(reach.profiles)
reach.set_global_index(range(i, i + nb + 1))
i += nb
if "Z" in table_data:
for ts in data["timestamps"]:
new_results._river.update_water_limits(
ts,
table_data["Z"][new_results.get_timestamp_id(ts), :]
)
yield (solver_type, new_results) yield (solver_type, new_results)
def _db_save_clear(self, execute, solver_type, data=None): def _db_save_clear(self, execute, solver_type, data=None):
@@ -343,6 +559,11 @@ class Results(SQLSubModel):
f"WHERE scenario = {self._owner_scenario} " + f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}" f"AND result = {pid}"
) )
execute(
"DELETE FROM results_table_data " +
f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}"
)
execute( execute(
"DELETE FROM results_add_data " + "DELETE FROM results_add_data " +
f"WHERE scenario = {self._owner_scenario} " + f"WHERE scenario = {self._owner_scenario} " +
@@ -386,7 +607,19 @@ class Results(SQLSubModel):
data["result"] = self._pamhyr_id data["result"] = self._pamhyr_id
self._river._db_save(execute, data) self._river._db_save(execute, data)
tables = self.get("table")
for legend in tables:
tables[legend]._db_save(execute, data)
for add_data in self.get("additional_data"): for add_data in self.get("additional_data"):
add_data._db_save(execute, data) add_data._db_save(execute, data)
return True return True
def new_table_data(self, legend, data):
return TableData(
study=self._study,
legend=legend,
data=data,
owner_scenario=self._owner_scenario
)
+1 -1
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr # Results.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+73 -22
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr # River.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -14,12 +14,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import types
import struct import struct
import logging import logging
import itertools import itertools
import numpy as np import numpy as np
from tools import flatten from tools import flatten, timer
from functools import reduce from functools import reduce
from datetime import datetime from datetime import datetime
@@ -30,15 +31,17 @@ logger = logging.getLogger()
class Profile(SQLSubModel): class Profile(SQLSubModel):
def __init__(self, profile, study): def __init__(self, profile, study, parent):
super(Profile, self).__init__( super(Profile, self).__init__(
id=-1, status=study.status, id=-1, status=study.status,
owner_scenario=study.status.scenario.id owner_scenario=study.status.scenario.id
) )
self._parent = parent
self._study = study self._study = study
self._profile = profile # Source profile in the study self._profile = profile # Source profile in the study
self._data = {} # Dict of dict {<ts>: {<key>: <value>, ...}, ...} self._data = {} # Dict of dict {<ts>: {<key>: <value>, ...}, ...}
self._global_index = -1
def __len__(self): def __len__(self):
return len(self._data) return len(self._data)
@@ -55,15 +58,26 @@ class Profile(SQLSubModel):
def geometry(self): def geometry(self):
return self._profile return self._profile
@property
def global_index(self):
return self._global_index
@global_index.setter
def global_index(self, id):
self._global_index = id
@timer
def set(self, timestamp, key, data): def set(self, timestamp, key, data):
if timestamp not in self._data: if timestamp not in self._data:
self._data[timestamp] = {} self._data[timestamp] = {}
self._data[timestamp][key] = data self._data[timestamp][key] = data
@timer
def get_ts(self, timestamp): def get_ts(self, timestamp):
return self._data[timestamp] return self._data[timestamp]
@timer
def get_key(self, key): def get_key(self, key):
res = list( res = list(
map(lambda ts: self._data[ts][key], map(lambda ts: self._data[ts][key],
@@ -71,17 +85,26 @@ class Profile(SQLSubModel):
) )
return res return res
@timer
def get_ts_key(self, timestamp, key): def get_ts_key(self, timestamp, key):
if timestamp in self._data: if timestamp in self._data:
if key in self._data[timestamp]: if key in self._data[timestamp]:
return self._data[timestamp][key] v = self._data[timestamp][key]
# If is a generator, compute value(s)
if isinstance(v, types.GeneratorType):
v = next(v)
self._data[timestamp][key] = v
return v
return None return None
def has_sediment_layers(self): def has_sediment_layers(self):
return any(map(lambda ts: "sl" in self._data[ts], self._data)) return any(map(lambda ts: "sl" in self._data[ts], self._data))
def has_bedload(self): def has_bedload(self):
return any(map(lambda ts: "zfd" in self._data[ts], self._data)) return any(map(lambda ts: "sl" in self._data[ts], self._data))
@classmethod @classmethod
def _db_create(cls, execute, ext=""): def _db_create(cls, execute, ext=""):
@@ -114,9 +137,9 @@ class Profile(SQLSubModel):
@classmethod @classmethod
def _db_load(cls, execute, data=None): def _db_load(cls, execute, data=None):
new = {}
status = data['status'] status = data['status']
parent = data['parent']
study = data['study'] study = data['study']
reach = data['reach'] reach = data['reach']
profile = data['profile'] profile = data['profile']
@@ -124,6 +147,8 @@ class Profile(SQLSubModel):
loaded = data['loaded_pid'] loaded = data['loaded_pid']
timestamps = data['timestamps'] timestamps = data['timestamps']
new = cls(profile, study, parent)
values = execute( values = execute(
"SELECT pamhyr_id, result, key, " + "SELECT pamhyr_id, result, key, " +
"len_data, data, scenario " + "len_data, data, scenario " +
@@ -143,12 +168,6 @@ class Profile(SQLSubModel):
data = next(it) data = next(it)
owner_scenario = next(it) owner_scenario = next(it)
if profile not in new:
new_data = cls(profile, study)
new[profile] = new_data
else:
new_data = new[profile]
if key in ["Z", "Q", "V"]: if key in ["Z", "Q", "V"]:
sf = ">" + ''.join(itertools.repeat("f", len_data)) sf = ">" + ''.join(itertools.repeat("f", len_data))
len_values = len(values) len_values = len(values)
@@ -161,11 +180,11 @@ class Profile(SQLSubModel):
) )
for timestamp, value in zip(timestamps, values): for timestamp, value in zip(timestamps, values):
new_data.set(timestamp, key, value) new.set(timestamp, key, value)
if key == "Z": if key == "Z":
new_data.update_water_limits(timestamp, value) new.update_water_limits(timestamp, value)
return list(new.values()) return new
@classmethod @classmethod
def _db_load_data_sl_format(cls, values, len_data, timestamps): def _db_load_data_sl_format(cls, values, len_data, timestamps):
@@ -257,19 +276,20 @@ class Profile(SQLSubModel):
class Reach(SQLSubModel): class Reach(SQLSubModel):
_sub_classes = [Profile] _sub_classes = [Profile]
def __init__(self, reach, study, with_init=True): def __init__(self, reach, study, parent, with_init=True):
super(Reach, self).__init__( super(Reach, self).__init__(
id=-1, status=study.status, id=-1, status=study.status,
owner_scenario=study.status.scenario.id owner_scenario=study.status.scenario.id
) )
self._parent = parent
self._study = study self._study = study
self._reach = reach # Source reach in the study self._reach = reach # Source reach in the study
self._profiles = [] self._profiles = []
if with_init: if with_init:
self._profiles = list( self._profiles = list(
map( map(
lambda p: Profile(p, self._study), lambda p: Profile(p, self._study, self._parent),
reach.profiles reach.profiles
) )
) )
@@ -279,6 +299,9 @@ class Reach(SQLSubModel):
lambda p: p.name[0:8] != 'interpol', self._profiles lambda p: p.name[0:8] != 'interpol', self._profiles
) )
) )
self._global_index = (-1, -1)
self._buffers = {} self._buffers = {}
def __len__(self): def __len__(self):
@@ -307,6 +330,17 @@ class Reach(SQLSubModel):
def profile(self, id): def profile(self, id):
return self._profiles[id] return self._profiles[id]
def set_global_index(self, indexs):
self._global_index = (indexs[0], indexs[-1])
for profile, ind in zip(self._profiles, indexs):
profile.global_index = ind
@property
def global_index(self):
return self._global_index
@timer
def set(self, profile_id, timestamp, key, data): def set(self, profile_id, timestamp, key, data):
self._profiles[profile_id].set(timestamp, key, data) self._profiles[profile_id].set(timestamp, key, data)
@@ -322,6 +356,10 @@ class Reach(SQLSubModel):
for i, p in enumerate(self._profiles): for i, p in enumerate(self._profiles):
self._buffers[key][:, i] = p.get_key(key) self._buffers[key][:, i] = p.get_key(key)
def update_water_limits(self, timestamp, z_data):
for profile, z in zip(self._profiles, z_data):
profile.update_water_limits(timestamp, z)
@classmethod @classmethod
def _db_create(cls, execute, ext=""): def _db_create(cls, execute, ext=""):
return cls._create_submodel(execute) return cls._create_submodel(execute)
@@ -335,12 +373,13 @@ class Reach(SQLSubModel):
reach = data["reach"] reach = data["reach"]
new_reach = cls( new_reach = cls(
data["reach"], data["study"], with_init=False data["reach"], data["study"], data["parent"],
with_init=False
) )
for i, profile in enumerate(reach.profiles): for i, profile in enumerate(reach.profiles):
data["profile"] = profile data["profile"] = profile
new_reach._profiles += Profile._db_load(execute, data) new_reach._profiles += [Profile._db_load(execute, data)]
return new_reach return new_reach
@@ -354,13 +393,14 @@ class Reach(SQLSubModel):
class River(SQLSubModel): class River(SQLSubModel):
_sub_classes = [Reach] _sub_classes = [Reach]
def __init__(self, study): def __init__(self, study, parent):
super(River, self).__init__( super(River, self).__init__(
id=-1, status=study.status, id=-1, status=study.status,
owner_scenario=study.status.scenario.id owner_scenario=study.status.scenario.id
) )
self._study = study self._study = study
self._parent = parent
# Dict with timestamps as key # Dict with timestamps as key
self._reachs = [] self._reachs = []
@@ -381,7 +421,7 @@ class River(SQLSubModel):
def add(self, reach_id): def add(self, reach_id):
reachs = self._study.river.enable_edges() reachs = self._study.river.enable_edges()
new = Reach(reachs[reach_id].reach, self._study) new = Reach(reachs[reach_id].reach, self._study, self._parent)
self._reachs.append(new) self._reachs.append(new)
return new return new
@@ -394,6 +434,15 @@ class River(SQLSubModel):
) )
) )
def update_water_limits(self, timestamp, data):
for reach in self._reachs:
ind = reach.global_index
reach.update_water_limits(
timestamp,
data[ind[0]:ind[1]]
)
@classmethod @classmethod
def _db_create(cls, execute, ext=""): def _db_create(cls, execute, ext=""):
return cls._create_submodel(execute) return cls._create_submodel(execute)
@@ -405,7 +454,9 @@ class River(SQLSubModel):
@classmethod @classmethod
def _db_load(cls, execute, data=None): def _db_load(cls, execute, data=None):
study = data["study"] study = data["study"]
new_river = cls(study) parent = data["parent"]
new_river = cls(study, parent)
for reach in study.river.reachs(): for reach in study.river.reachs():
data["reach"] = reach.reach data["reach"] = reach.reach
+1 -1
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr # River.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr river model # River.py -- Pamhyr river model
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Serializable.py -- Pamhyr pickle abstract class # Serializable.py -- Pamhyr pickle abstract class
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# SolverParametersList.py -- Pamhyr # SolverParametersList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Status.py -- Pamhyr model status class # Status.py -- Pamhyr model status class
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Stricklers.py -- Pamhyr # Stricklers.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# StricklersList.py -- Pamhyr # StricklersList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+2 -2
View File
@@ -1,5 +1,5 @@
# Study.py -- Pamhyr Study class # Study.py -- Pamhyr Study class
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -46,7 +46,7 @@ logger = logging.getLogger()
class Study(SQLModel): class Study(SQLModel):
_version = "0.2.5" _version = "0.2.7"
_sub_classes = [ _sub_classes = [
Scenario, Scenario,
+1 -1
View File
@@ -1,5 +1,5 @@
# PamhyrDB.py -- Pamhyr abstract model database classes # PamhyrDB.py -- Pamhyr abstract model database classes
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# PamhyrDict.py -- Pamhyr abstract dict for model classes # PamhyrDict.py -- Pamhyr abstract dict for model classes
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# PamhyrList.py -- Pamhyr Abstract List object for the Model # PamhyrList.py -- Pamhyr Abstract List object for the Model
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# PamhyrList.py -- Pamhyr Abstract List object for the Model # PamhyrList.py -- Pamhyr Abstract List object for the Model
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# __init__.py -- Pamhyr # __init__.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# test_Model.py -- Pamhyr # test_Model.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# SQL.py -- Pamhyr # SQL.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# AScript.py -- Pamhyr abstract script class # AScript.py -- Pamhyr abstract script class
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Hello.py -- Pamhyr # Hello.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# ListSolver.py -- Pamhyr # ListSolver.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# MageMesh.py -- Pamhyr # MageMesh.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# P3DST.py -- Pamhyr # P3DST.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Run.py -- Pamhyr # Run.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+17 -3
View File
@@ -1,3 +1,20 @@
# mage_to_mascaret.py -- Pamhyr
# Copyright (C) 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 -*-
#!/bin/env python3 #!/bin/env python3
import sys import sys
@@ -336,6 +353,3 @@ if __name__ == "__main__":
print( 'Number of arguments:', len(sys.argv), 'arguments.') print( 'Number of arguments:', len(sys.argv), 'arguments.')
print( 'Argument List:', str(sys.argv)) print( 'Argument List:', str(sys.argv))
mage_to_mascaret(sys.argv[1]) mage_to_mascaret(sys.argv[1])
+17
View File
@@ -1,3 +1,20 @@
# mascaret_to_mage.py -- Pamhyr
# Copyright (C) 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 -*-
#!/bin/env python3 #!/bin/env python3
import sys import sys
+1 -1
View File
@@ -1,5 +1,5 @@
# ASolver.py -- Pamhyr # ASolver.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+3 -44
View File
@@ -1,5 +1,5 @@
# AdisTS.py -- Pamhyr # AdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -280,8 +280,8 @@ class AdisTS(CommandLineSolver):
name = f"Reach_{e.id + 1:>3}".replace(" ", "0") name = f"Reach_{e.id + 1:>3}".replace(" ", "0")
id = name id = name
n1 = f"{e.node1.id:3}".replace(" ", "x") n1 = f"{self.get_node_name(e.node1):3}".replace(" ", "x")
n2 = f"{e.node2.id:3}".replace(" ", "x") n2 = f"{self.get_node_name(e.node2):3}".replace(" ", "x")
file = os.path.join("net", name + ".ST") file = os.path.join("net", name + ".ST")
f.write(f"{id} {n1} {n2} {file}\n") f.write(f"{id} {n1} {n2} {file}\n")
@@ -455,47 +455,6 @@ class AdisTS(CommandLineSolver):
def get_reach_name(self, reach): def get_reach_name(self, reach):
return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0") return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0")
def get_node_name(self, node):
"""Generate a 3 char name for node
Args:
node: The node
Returns:
A 3 char name string
"""
n = node.pamhyr_id
# print("node name id: ", n)
# if n in self._nodes_names:
# return self._nodes_names[n]
name = ""
checked_new = False
while not checked_new:
self._nodes_cnt += 1
nc = self._nodes_cnt
name = "".join(
map(
lambda i: self._alph[i % self._l_alph],
[
int(nc / (self._l_alph * self._l_alph)),
int(nc / self._l_alph),
nc
]
)
)
checked_new = name not in self._nodes_views
self._nodes_views.add(name)
self._nodes_names[n] = name
return name
################################# #################################
# Adis-TS in weak coupling mode # # Adis-TS in weak coupling mode #
################################# #################################
+5 -1
View File
@@ -1,5 +1,5 @@
# CommandLine.py -- Pamhyr # CommandLine.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -158,11 +158,15 @@ class CommandLineSolver(AbstractSolver):
txt = add_file.text txt = add_file.text
txt = txt.replace("@version", get_version()) txt = txt.replace("@version", get_version())
txt = txt.replace("@date", datetime.now().isoformat(sep=" ")) txt = txt.replace("@date", datetime.now().isoformat(sep=" "))
txt = self._prepare_additional_file_text(add_file.path, txt)
f.write(txt) f.write(txt)
return files return files
def _prepare_additional_file_text(self, path, text):
return text
def export_study_description(self, study, repertory, qlog, name="0"): def export_study_description(self, study, repertory, qlog, name="0"):
files = [] files = []
+1 -1
View File
@@ -1,5 +1,5 @@
# GenericSolver.py -- Pamhyr # GenericSolver.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+86 -38
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr # Mage.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -679,6 +679,23 @@ class Mage(CommandLineSolver):
return res return res
def _prepare_additional_file_text(self, path, text):
if os.path.splitext(path)[1].lower() != ".var":
return text
lines = []
for line in text.splitlines(keepends=True):
stripped = line.lstrip()
fields = stripped.split()
if stripped.startswith("$"):
# MAGE only recognizes rule markers in the first column.
line = stripped
elif (not stripped.startswith("*") and len(fields) >= 2
and fields[1] == "W2Z"):
line = "$ " + stripped
lines.append(line)
return "".join(lines)
def _export_VAR(self, study, repertory, qlog, name="0"): def _export_VAR(self, study, repertory, qlog, name="0"):
files = [] files = []
@@ -729,9 +746,11 @@ class Mage(CommandLineSolver):
name = bhs.name name = bhs.name
if name == "": if name == "":
name = f"HS_{bhs.id:>3}".replace(" ", "0") name = f"HS_{bhs.id:>3}".replace(" ", "0")
else:
name = name.replace(" ", "_")
f.write( f.write(
f"${name} clapet" f"${name} clapet\n"
) )
return files return files
@@ -1083,7 +1102,7 @@ class Mage8(Mage):
newline() newline()
reachs = [] reachs = []
iprofiles = {} iprofiles = []
profile_len = [] profile_len = []
reach_offset = {} reach_offset = {}
@@ -1098,15 +1117,18 @@ class Mage8(Mage):
i1 = data[2*i] - 1 i1 = data[2*i] - 1
i2 = data[2*i+1] - 1 i2 = data[2*i+1] - 1
# Keep profiles index of all river
r.set_global_index(range(i1, i2 + 1))
# Add profile id correspondance to reach # Add profile id correspondance to reach
key = (i1, i2) for key in range(i1, i2 + 1):
iprofiles[key] = r iprofiles.append(i)
# Profile ID offset # Profile ID offset
reach_offset[r] = i1 reach_offset[r] = i1
profile_len.append(i2-i1+1) profile_len.append(i2 - i1 + 1)
logger.debug(f"read_bin: iprofiles = {iprofiles}") logger.debug(f"read_bin: iprofiles = {len(iprofiles)}")
endline() endline()
@@ -1124,16 +1146,6 @@ class Mage8(Mage):
# Data # Data
newline() newline()
def ip_to_r(i): return iprofiles[
next(
filter(
lambda k: k[0] <= i <= k[1],
iprofiles
)
)
]
def ip_to_ri(r, i): return i - reach_offset[r]
# check results and geometry consistency # check results and geometry consistency
for i, r in enumerate(reachs): for i, r in enumerate(reachs):
lp = len(r.profiles) lp = len(r.profiles)
@@ -1144,6 +1156,8 @@ class Mage8(Mage):
return return
ts = set() ts = set()
tmp_table = {}
end = False end = False
while not end: while not end:
n = read_int(1)[0] n = read_int(1)[0]
@@ -1152,18 +1166,21 @@ class Mage8(Mage):
f, dtype=np.byte, count=1)).decode() f, dtype=np.byte, count=1)).decode()
data = read_float(n) data = read_float(n)
# logger.debug(f"read_bin: timestamp = {timestamp} sec") if key not in tmp_table:
tmp_table[key] = []
tmp_table[key].append(data)
ts.add(timestamp) ts.add(timestamp)
if key in ["Z", "Q"]: if key in ["Z", "Q"]:
for i, d in enumerate(data): for i, d in enumerate(data):
# Get reach corresponding to profile ID # Get reach corresponding to profile ID
reach = ip_to_r(i) reach = reachs[iprofiles[i]]
# Get profile id in reach # Get profile id in reach
ri = ip_to_ri(reach, i) ri = i - reach_offset[reach]
# Set data for profile RI # Set data for profile RI
reach.set(ri, timestamp, key, d) # reach.set(ri, timestamp, key, d)
if key == "Z": if key == "Z":
profile = reach.profile(ri) profile = reach.profile(ri)
limits = profile.geometry.get_water_limits(d) limits = profile.geometry.get_water_limits(d)
@@ -1176,6 +1193,15 @@ class Mage8(Mage):
endline() endline()
end = newline().size <= 0 end = newline().size <= 0
ts_index = {}
for i, t in enumerate(sorted(ts)):
ts_index[t] = i
results.set_timestamp_index(ts_index)
table = {}
for k in tmp_table:
table[k] = np.array(tmp_table[k])
results.set("timestamps", ts) results.set("timestamps", ts)
ts_list = sorted(ts) ts_list = sorted(ts)
logger.info(f"compute tab...") logger.info(f"compute tab...")
@@ -1185,19 +1211,33 @@ class Mage8(Mage):
logger.info(f"compute velocity...") logger.info(f"compute velocity...")
velocity_arr = np.zeros((len(ts), len(iprofiles)))
for ti, t in enumerate(ts_list):
j = 0
for r in reachs: for r in reachs:
for t in ts_list: for pi, p in enumerate(r.profiles):
for i, p in enumerate(r.profiles):
v = p.geometry.speed( v = p.geometry.speed(
p.get_ts_key(t, "Q"), table["Q"][ti, p.global_index],
p.get_ts_key(t, "Z") table["Z"][ti, p.global_index],
) )
r.set(i, t, "V", v) # r.set(pi, t, "V", v)
velocity_arr[ti, j] = v
j += 1
table["V"] = velocity_arr
for legend in table:
table[legend] = results.new_table_data(
legend, table[legend]
)
results.set("table", table)
logger.info(f"read_bin: ... end with {len(ts)} timestamp read") logger.info(f"read_bin: ... end with {len(ts)} timestamp read")
results.bufferize("Z") # results.bufferize("Z")
results.bufferize("Q") # results.bufferize("Q")
results.bufferize("V") # results.bufferize("V")
logger.info(f"reading time: '{time.time() - start}'") logger.info(f"reading time: '{time.time() - start}'")
@timer @timer
@@ -1267,8 +1307,8 @@ class Mage8(Mage):
reachs.append(r) reachs.append(r)
# ID of first and last reach profiles # ID of first and last reach profiles
i1 = data[2*i] - 1 i1 = data[2 * i] - 1
i2 = data[2*i+1] - 1 i2 = data[2 * i + 1] - 1
# Add profile id correspondance to reach # Add profile id correspondance to reach
key = (i1, i2) key = (i1, i2)
@@ -1322,6 +1362,7 @@ class Mage8(Mage):
newline() newline()
npts = read_int(n) npts = read_int(n)
endline() endline()
sum_npts = sum(npts) sum_npts = sum(npts)
logger.debug(f"read_gra: Number of points: {sum_npts}") logger.debug(f"read_gra: Number of points: {sum_npts}")
@@ -1371,12 +1412,14 @@ class Mage8(Mage):
logger.info(f"compute river bed elevation...") logger.info(f"compute river bed elevation...")
zfd_lst = []
for r in reachs: for r in reachs:
z_min = r.geometry.get_z_min() z_min = r.geometry.get_z_min()
sls = list(map( sls = map(
lambda p: p.get_ts_key(ts_list[0], "sl")[0], lambda p: p.get_ts_key(ts_list[0], "sl")[0],
r.profiles r.profiles
)) )
z_br = list(map( z_br = list(map(
lambda z, sl: reduce( lambda z, sl: reduce(
lambda z, h: z - h[0], lambda z, h: z - h[0],
@@ -1385,11 +1428,11 @@ class Mage8(Mage):
z_min, # Original geometry z_min, # Original geometry
sls # Original sediment layers sls # Original sediment layers
)) ))
for t in ts_list: for its, t in enumerate(ts_list):
sls = list(map( sls = map(
lambda p: p.get_ts_key(t, "sl")[0], lambda p: p.get_ts_key(t, "sl")[0],
r.profiles r.profiles
)) )
zfd = list(map( zfd = list(map(
lambda z, sl: reduce( lambda z, sl: reduce(
lambda z, h: z + h[0], lambda z, h: z + h[0],
@@ -1398,8 +1441,13 @@ class Mage8(Mage):
z_br, # bedrock z_br, # bedrock
sls # Original sediment layers sls # Original sediment layers
)) ))
for i, p in enumerate(r.profiles):
r.set(i, t, "zfd", zfd[i]) zfd_lst.append(np.array(zfd))
table = results.get("table")
table["zfd"] = results.new_table_data(
"zfd", np.array(zfd_lst)
)
results.set("sediment_timestamps", ts) results.set("sediment_timestamps", ts)
logger.info(f"read_gra: ... end with {len(ts)} timestamp read") logger.info(f"read_gra: ... end with {len(ts)} timestamp read")
+1 -1
View File
@@ -1,5 +1,5 @@
# Solvers.py -- Pamhyr # Solvers.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Window.py -- Pamhyr # Window.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# GenerateDialog.py -- Pamhyr # GenerateDialog.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+2 -2
View File
@@ -1,5 +1,5 @@
# Plot.py -- Pamhyr # Plot.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -54,7 +54,7 @@ class Plot(PamhyrPlot):
self._isometric_axis = False self._isometric_axis = False
self._auto_relim_update = True self._auto_relim_update = True
self._autoscale_update = False self._autoscale_update = True
def custom_ticks(self): def custom_ticks(self):
if self.data.header[0] != "time": if self.data.header[0] != "time":
+29 -7
View File
@@ -1,5 +1,5 @@
# Table.py -- Pamhyr # Table.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -143,21 +143,43 @@ class TableModel(PamhyrTableModel):
self.update() self.update()
def delete(self, rows, parent=QModelIndex()): def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1]) if not rows:
return
rows = list(map( rows = sorted(set(rows))
lambda r: self.get_true_data_row(r), rows = [
rows)) self.get_true_data_row(row)
for row in 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( self._undo.push(
DelCommand( DelCommand(
self._data, rows self._data, rows
) )
) )
finally:
self.endRemoveRows() self.endResetModel()
self.update() 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()): def sort(self, _reverse, parent=QModelIndex()):
self.layoutAboutToBeChanged.emit() self.layoutAboutToBeChanged.emit()
+14 -19
View File
@@ -1,5 +1,5 @@
# UndoCommand.py -- Pamhyr # UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -97,12 +97,11 @@ class DelCommand(QUndoCommand):
QUndoCommand.__init__(self) QUndoCommand.__init__(self)
self._data = data self._data = data
self._rows = rows self._rows = sorted(set(rows))
self._bc = [
self._bc = [] self._data.get_i(row)
for row in rows: for row in self._rows
self._bc.append(self._data.get_i(row)) ]
self._bc.sort()
def undo(self): def undo(self):
for el in self._bc: for el in self._bc:
@@ -120,13 +119,17 @@ class SortCommand(QUndoCommand):
self._data = data self._data = data
self._reverse = _reverse self._reverse = _reverse
self._old = self._data.data # Keep the order of the complete underlying list, including soft
self._indexes = None # 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): def undo(self):
ll = self._data.data
self._data.sort( self._data.sort(
key=lambda x: self._indexes[ll.index(x)] key=lambda item: self._old_positions[id(item)]
) )
def redo(self): def redo(self):
@@ -134,14 +137,6 @@ class SortCommand(QUndoCommand):
_reverse=self._reverse, _reverse=self._reverse,
key=lambda x: x[0] 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): class MoveCommand(QUndoCommand):
+5 -2
View File
@@ -1,5 +1,5 @@
# Window.py -- Pamhyr # Window.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -185,7 +185,10 @@ class EditBoundaryConditionWindow(PamhyrWindow):
self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas") self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar( self.toolbar = PamhyrPlotToolbar(
self.canvas, self self.canvas, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
) )
self.verticalLayout.addWidget(self.toolbar) self.verticalLayout.addWidget(self.toolbar)
self.verticalLayout.addWidget(self.canvas) self.verticalLayout.addWidget(self.canvas)
+1 -1
View File
@@ -1,5 +1,5 @@
# translate.py -- Pamhyr # translate.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Table.py -- Pamhyr # Table.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+2 -2
View File
@@ -1,5 +1,5 @@
# UndoCommand.py -- Pamhyr # UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -124,7 +124,7 @@ class DelCommand(QUndoCommand):
self._bcs.insert(self._tab, row, el) self._bcs.insert(self._tab, row, el)
def redo(self): def redo(self):
self._bcs.delete_i(self._tab, self._rows) self._bcs.delete(self._tab, [el for _, el in self._bc])
class SortCommand(QUndoCommand): class SortCommand(QUndoCommand):
+1 -1
View File
@@ -1,5 +1,5 @@
# Window.py -- Pamhyr # Window.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# translate.py -- Pamhyr # translate.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2026 INRAE
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
Loaded 100 of 289 files, more files were not shown because too many files have changed in this diff. Show more