Compare commits

..
2 Commits
Author SHA1 Message Date
Theophile Terraz 22771c7f3a debug + pep8 2025-11-06 16:40:12 +01:00
Theophile Terraz 6d60a2017c add sound option in results for Q 2025-11-06 16:28:59 +01:00
408 changed files with 4245 additions and 27043 deletions

No files matched your search

+8 -46
View File
@@ -38,7 +38,6 @@ variables:
variables: variables:
MAGE_8_VERSION: "permalink/latest" MAGE_8_VERSION: "permalink/latest"
ADISTS_VERSION: "permalink/latest" ADISTS_VERSION: "permalink/latest"
RUBAR_VERSION: "permalink/latest"
dl-mage8-doc: dl-mage8-doc:
stage: downloads stage: downloads
@@ -112,36 +111,6 @@ dl-adists-windows:
paths: paths:
- adists-windows/adists.exe - adists-windows/adists.exe
dl-rubar-linux:
stage: downloads
tags:
- linux
rules:
- if: $CI_COMMIT_BRANCH == 'ci-test' || $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG
script:
- mkdir -p rubar-linux
- cd rubar-linux
- curl -L -o rubar.tgz https://forge.inrae.fr/river-hydraulics/rubarbe/-/releases/$RUBAR_VERSION/downloads/packages/rubar_linux.tgz
- tar xf rubar.tgz
artifacts:
paths:
- rubar-linux/rubarbe
dl-rubar-windows:
stage: downloads
tags:
- linux
rules:
- if: $CI_COMMIT_BRANCH == 'ci-test' || $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG
script:
- mkdir -p rubar-windows
- cd rubar-windows
- curl -L -o rubar.tgz https://forge.inrae.fr/river-hydraulics/rubarbe/-/releases/$RUBAR_VERSION/downloads/packages/rubar_windows.tgz
- tar xf rubar.tgz
artifacts:
paths:
- rubar-windows/rubarbe.exe
############# #############
# CONFIGURE # # CONFIGURE #
############# #############
@@ -180,13 +149,13 @@ unittest:
- job: set-version - job: set-version
artifacts: true artifacts: true
script: script:
- python3.12 -m venv venv - python3 -m venv venv
- . venv/bin/activate - . venv/bin/activate
- pip3 install -U pip - pip3 install -U pip
- pip3 install -r ./full-requirements.txt - pip3 install -r ./full-requirements.txt
- pip3 install -U -r ./full-requirements.txt - pip3 install -U -r ./full-requirements.txt
- cd src - cd src
- python3.12 -m unittest discover -t . - python3 -m unittest discover -t .
test-pep8: test-pep8:
stage: test stage: test
@@ -198,7 +167,7 @@ test-pep8:
- job: unittest - job: unittest
script: script:
# Setup virtual env # Setup virtual env
- python3.12 -m venv venv - python3 -m venv venv
- . venv/bin/activate - . venv/bin/activate
- pip3 install -U pip - pip3 install -U pip
- pip3 install -r ./requirements.txt - pip3 install -r ./requirements.txt
@@ -276,7 +245,7 @@ build-developers-doc:
- ./build.sh - ./build.sh
# Create html documentation # Create html documentation
- cd ../../src - cd ../../src
- python3.12 -m venv doc-venv - python3 -m venv doc-venv
- . doc-venv/bin/activate - . doc-venv/bin/activate
- pip3 install -U pip - pip3 install -U pip
- pip3 install -r ../full-requirements.txt - pip3 install -r ../full-requirements.txt
@@ -304,8 +273,6 @@ build-linux:
needs: needs:
- job: dl-adists-linux - job: dl-adists-linux
artifacts: true artifacts: true
- job: dl-rubar-linux
artifacts: true
- job: dl-mage8-linux - job: dl-mage8-linux
artifacts: true artifacts: true
- job: dl-mage8-doc - job: dl-mage8-doc
@@ -326,13 +293,13 @@ build-linux:
- mkdir -p linux - mkdir -p linux
- cd linux - cd linux
# Setup virtual env # Setup virtual env
- python3.12 -m venv linux-venv - python3 -m venv linux-venv
- . linux-venv/bin/activate - . linux-venv/bin/activate
- pip3 install -U pip - pip3 install -U pip
- pip3 install -r ../requirements.txt - pip3 install -r ../requirements.txt
- pip3 install -U -r ../requirements.txt - pip3 install -U -r ../requirements.txt
# Run Pyinstaller # Run Pyinstaller
- pyinstaller -y $HIDDEN_IMPORTS --paths linux-venv/lib/python3.12/site-packages ../src/pamhyr.py - pyinstaller -y $HIDDEN_IMPORTS --paths linux-venv/lib/python3.8/site-packages ../src/pamhyr.py
# Create directory # Create directory
- mkdir -p pamhyr - mkdir -p pamhyr
- mkdir -p pamhyr/_internal - mkdir -p pamhyr/_internal
@@ -347,9 +314,6 @@ build-linux:
# Copy adists # Copy adists
- mkdir -p pamhyr/adists - mkdir -p pamhyr/adists
- cp -v ../adists-linux/* pamhyr/adists/ - cp -v ../adists-linux/* pamhyr/adists/
# Copy rubar
- mkdir -p pamhyr/rubar
- cp -v ../rubar-linux/* pamhyr/rubar/
# Copy Pamhyr # Copy Pamhyr
- cp -r dist/pamhyr/* pamhyr/ - cp -r dist/pamhyr/* pamhyr/
# Pamhyr script to force x11 # Pamhyr script to force x11
@@ -415,8 +379,6 @@ build-windows:
artifacts: true artifacts: true
- job: dl-adists-windows - job: dl-adists-windows
artifacts: true artifacts: true
- job: dl-rubar-windows
artifacts: true
- job: dl-mage8-doc - job: dl-mage8-doc
artifacts: true artifacts: true
- job: set-version - job: set-version
@@ -638,7 +600,7 @@ shadow-release:
- job: pkg-hash - job: pkg-hash
artifacts: true artifacts: true
rules: rules:
- if: $CI_COMMIT_TAG && $CI_COMMIT_TAG !~ /^v/ - if: $CI_COMMIT_BRANCH != 'master' && $CI_COMMIT_TAG
artifacts: artifacts:
paths: paths:
- linux/pamhyr-gnulinux.tar.xz - linux/pamhyr-gnulinux.tar.xz
@@ -672,7 +634,7 @@ tag-release:
- job: pkg-hash - job: pkg-hash
artifacts: true artifacts: true
rules: rules:
- if: $CI_COMMIT_TAG =~ /^v/ - if: $CI_COMMIT_BRANCH == 'master' && $CI_COMMIT_TAG
artifacts: artifacts:
paths: paths:
- linux/pamhyr-gnulinux.tar.xz - linux/pamhyr-gnulinux.tar.xz
+1 -2
View File
@@ -1,4 +1,3 @@
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 - 2026 Pierre-Antoine ROUBY, INRAE, 2023 - 2025
Dylan JEANNIN, INRAE, 2026
+8 -8
View File
@@ -3,7 +3,7 @@
Pamhyr is a free and open source graphical user interface for 1D hydro-sedimentary Pamhyr is a free and open source graphical user interface for 1D hydro-sedimentary
modelling of rivers. modelling of rivers.
![logo](https://gitlab.com/pamhyr/pamhyr2/-/raw/master/src/View/ui/ressources/Pamhyr2_logo.png) ![logo](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/raw/master/src/View/ui/ressources/Pamhyr2_logo.png)
## Features ## Features
@@ -21,9 +21,9 @@ modelling of rivers.
+ Create and edit study alternative scenario tree + Create and edit study alternative scenario tree
Let see the Let see the
[documentation](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home) [documentation](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home)
([:fr:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home-fr), ([:fr:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home-fr),
[:gb:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home-en)) [:gb:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home-en))
for more details. for more details.
## Install ## Install
@@ -31,11 +31,11 @@ for more details.
### GNU/Linux ### GNU/Linux
See documentation: See documentation:
- [French :fr:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/fr/Install%20on%20GNULinux) - [French :fr:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/fr/Install%20on%20GNULinux)
- [English :gb:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/en/Install%20on%20GNULinux) - [English :gb:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/en/Install%20on%20GNULinux)
### Windows ### Windows
See documentation: See documentation:
- [French :fr:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/fr/Install%20on%20Windows) - [French :fr:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/fr/Install%20on%20Windows)
- [English :gb:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/en/Install%20on%20Windows) - [English :gb:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/en/Install%20on%20Windows)
-6
View File
@@ -1069,12 +1069,6 @@ gitlab-ci.
sudo python3 -m pip install pyinstaller sudo python3 -m pip install pyinstaller
#+end_src #+end_src
In addition, you must install Glab[fn:glab] to allow the runner to
proceed with the publication.
[fn:glab] The Gitlab cli releases page:
https://gitlab.com/gitlab-org/cli/-/releases
*** Windows (Wine) {{{windows}}} *** Windows (Wine) {{{windows}}}
The ci-runner environment for Wine need at least wine version 8, let The ci-runner environment for Wine need at least wine version 8, let
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

+6 -6
View File
@@ -157,12 +157,12 @@
%% %%
%% FIXME: Delete me for the first realease %% FIXME: Delete me for the first realease
%% \usepackage{draftwatermark} \usepackage{draftwatermark}
%% \SetWatermarkLightness{0.8} \SetWatermarkLightness{0.8}
%% \SetWatermarkAngle{25} \SetWatermarkAngle{25}
%% \SetWatermarkScale{3} \SetWatermarkScale{3}
%% \SetWatermarkFontSize{1cm} \SetWatermarkFontSize{1cm}
%% \SetWatermarkText{Work in progress} \SetWatermarkText{Work in progress}
%% Icons %% Icons
+3 -4
View File
@@ -1,10 +1,10 @@
PyQt5==5.15.11 PyQt5==5.15.4
PyQt5-Qt5==5.15.2 PyQt5-Qt5==5.15.2
PyQt5-sip==12.18.0 PyQt5-sip==12.12.2
PyQtWebEngine==5.15.6 PyQtWebEngine==5.15.6
QScintilla>=2.14.1 QScintilla>=2.14.1
pyqtgraph>=0.12.1 pyqtgraph>=0.12.1
matplotlib==3.10.8 matplotlib>=3.7.1
numpy>=1.24.2 numpy>=1.24.2
colorama>=0.4.3 colorama>=0.4.3
pyinstaller>=5.11.0 pyinstaller>=5.11.0
@@ -14,4 +14,3 @@ platformdirs>=4.2.0
pyshp>=2.3.1 pyshp>=2.3.1
rasterio==1.3.11 rasterio==1.3.11
#fortranformat==2.0.3 #fortranformat==2.0.3
psutil>=7.2.2
+1 -1
View File
@@ -20,4 +20,4 @@
"python-pyqtgraph" "python-qscintilla" "python-pyqtgraph" "python-qscintilla"
"python-numpy@1" "python-shapely" "python-pyshp" "python-numpy@1" "python-shapely" "python-pyshp"
"python-matplotlib" "python-colorama" "python-matplotlib" "python-colorama"
"python-rasterio" "python-psutil")) "python-rasterio"))
-4
View File
@@ -46,10 +46,6 @@ copy /y ..\mage8-windows\mage_extraire.exe pamhyr\mage8\
copy /y ..\mage8-windows\mailleurTT.exe pamhyr\mage8\ copy /y ..\mage8-windows\mailleurTT.exe pamhyr\mage8\
copy /y ..\mage8-windows\libbief.dll pamhyr\mage8\ copy /y ..\mage8-windows\libbief.dll pamhyr\mage8\
rem rubar
mkdir pamhyr\rubar
copy /y ..\rubar-windows\rubarbe.exe pamhyr\rubar\
rem adists rem adists
mkdir pamhyr\adists mkdir pamhyr\adists
copy /y ..\adists-windows\adists.exe pamhyr\adists\ copy /y ..\adists-windows\adists.exe pamhyr\adists\
+3 -4
View File
@@ -1,10 +1,10 @@
PyQt5==5.15.11 PyQt5==5.15.4
PyQt5-Qt5==5.15.2 PyQt5-Qt5==5.15.2
PyQt5-sip==12.18.0 PyQt5-sip==12.12.2
#PyQtWebEngine==5.15.6 #PyQtWebEngine==5.15.6
QScintilla>=2.14.1 QScintilla>=2.14.1
pyqtgraph>=0.12.1 pyqtgraph>=0.12.1
matplotlib==3.10.8 matplotlib>=3.7.1
numpy>=1.24.2 numpy>=1.24.2
colorama>=0.4.3 colorama>=0.4.3
pyinstaller>=5.11.0 pyinstaller>=5.11.0
@@ -14,4 +14,3 @@ platformdirs>=4.2.0
pyshp>=2.3.1 pyshp>=2.3.1
rasterio==1.3.11 rasterio==1.3.11
#fortranformat==2.0.3 #fortranformat==2.0.3
psutil>=7.2.2
@@ -1,4 +1,4 @@
# translate.py -- Pamhyr # Adists.py -- Pamhyr study checkers
# Copyright (C) 2023-2025 INRAE # Copyright (C) 2023-2025 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
@@ -16,25 +16,37 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time
import logging
from functools import reduce
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate from Modules import Modules
from Checker.Checker import AbstractModelChecker, STATUS
_translate = QCoreApplication.translate _translate = QCoreApplication.translate
logger = logging.getLogger()
class BCTemperatureTranslate(MainTranslate):
class AdistsOutputRKChecker(AbstractModelChecker):
def __init__(self): def __init__(self):
super(BCTemperatureTranslate, self).__init__() super(AdistsOutputRKChecker, self).__init__()
self._dict["Boundary conditions temperature"] = _translate( self._name = _translate("Checker", "AdisTS output RK checker")
"BoundaryConditionsTemperature", "Boundary conditions temperature" self._description = _translate(
) "Checker", "Check output RK"
self._dict["default"] = _translate(
"BoundaryConditionsTemperature", "default"
) )
self._modules = Modules.OUTPUT_RK
self._sub_dict["table_headers"] = { def run(self, study):
"name": self._dict["name"], ok = True
"node": _translate("BoundaryCondition", "Node"), nerror = 0
} self._summary = "ok"
self._status = STATUS.OK
if not self.basic_check(study):
return False
return ok
+1 -1
View File
@@ -1,5 +1,5 @@
# Checker.py -- Pamhyr abstract checker class # Checker.py -- Pamhyr abstract checker class
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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 -6
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr MAGE checkers # Mage.py -- Pamhyr MAGE checkers
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -229,11 +229,7 @@ for each reach"
gls = [] gls = []
for edge in edges: for edge in edges:
profiles = edge.reach.enabled_profiles comp, incomp = edge.reach.compute_guidelines()
comp, incomp = edge.reach.compute_guidelines(
profiles=profiles,
update_cache=False
)
if len(incomp) != 0: if len(incomp) != 0:
self._status = STATUS.WARNING self._status = STATUS.WARNING
self._summary = "incomplete_guideline" self._summary = "incomplete_guideline"
@@ -241,6 +237,7 @@ for each reach"
gls.append(comp) gls.append(comp)
profiles = edge.reach.profiles
for profile in profiles: for profile in profiles:
if not profile.has_standard_named_points(): if not profile.has_standard_named_points():
self._status = STATUS.WARNING self._status = STATUS.WARNING
+4 -8
View File
@@ -1,5 +1,5 @@
# Study.py -- Pamhyr study checkers # Study.py -- Pamhyr study checkers
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -87,7 +87,7 @@ class StudyGeometryChecker(AbstractModelChecker):
return False return False
for edge in edges: for edge in edges:
if len(edge.reach.enabled_profiles) < 2: if len(edge.reach.profiles) < 2:
summary = "no_geometry_defined" summary = "no_geometry_defined"
status = STATUS.ERROR status = STATUS.ERROR
ok = False ok = False
@@ -132,12 +132,8 @@ class StudyInitialConditionsChecker(AbstractModelChecker):
return ok return ok
ic = river.initial_conditions[reach] ic = river.initial_conditions[reach]
enabled_profiles = set(reach.enabled_profiles) len_ic = len(ic)
len_ic = sum( len_reach = len(reach)
data["section"] in enabled_profiles
for data in ic.data
)
len_reach = len(enabled_profiles)
if len_ic < len_reach: if len_ic < len_reach:
self._summary = "initial_condition_missing_profile" self._summary = "initial_condition_missing_profile"
+1 -1
View File
@@ -1,5 +1,5 @@
# AMeshingTool.py -- Pamhyr # AMeshingTool.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
+126 -194
View File
@@ -1,5 +1,5 @@
# Internal.py -- Pamhyr # Internal.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -40,31 +40,20 @@ class InternalMeshing(AMeshingTool):
step: float = 50, step: float = 50,
limites=[-1, -1], limites=[-1, -1],
linear: bool = True): linear: bool = True):
if reach is None or len(reach.profiles) == 0:
return reach
# pre process if limites[0] > limites[1]:
lim = limites[1]
limites[1] = limites[0]
limites[0] = lim
elif limites[0] == limites[1]:
return reach
profiles = self.get_profiles(reach, limites) # we never modify original profiles, we work on copies
if profiles is None: m_profiles = self.st_to_m(reach, limites)
return [] new_profiles = self.interpolate_transversal_step(m_profiles,
step)
for p in profiles:
if len(p) < 1:
return []
guide_list = self.get_guide_list(reach, profiles)
if guide_list is None:
return []
# we never modify original profiles, we work on the copies
m_profiles = self.st_to_m(profiles, guide_list)
# Adapt the spacing to the kilometre-point direction. The value from
# the dialog is a distance, so only its magnitude is significant.
step = self.signed_step(
step, m_profiles[0].rk, m_profiles[-1].rk
)
new_profiles = self.interpolate_transversal_step(m_profiles, step)
for new_profiles2 in new_profiles: for new_profiles2 in new_profiles:
for p in new_profiles2: for p in new_profiles2:
@@ -72,38 +61,52 @@ class InternalMeshing(AMeshingTool):
return new_profiles return new_profiles
@staticmethod def st_to_m(self, reach, limites):
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): gl = reach.compute_guidelines()
guide_list = ["un"] + guide_list + ["np"] profiles = [reach.profile(i).copy()
max_values = [0] * (len(guide_list) - 1) for i in range(limites[0], limites[1]+1)
max_values_index = [0] * (len(guide_list) - 1) ]
# we make sure that the lines are in the left-to-right order
guide_list = [
x.name
for x in profiles[0].named_points()
if x.name in gl[0]
]
gl1 = [""] + guide_list
gl2 = guide_list + [""]
max_values = [0] * (len(guide_list) + 1)
max_values_index = [0] * (len(guide_list) + 1)
# we work between each couple of guidelines # we work between each couple of guidelines
for j, p in enumerate(profiles): for j, p in enumerate(profiles):
index1 = 0
index2 = p.named_point_index(guide_list[0])
if index2 - index1 > max_values[0]:
max_values[0] = index2 - index1
max_values_index[0] = j
for i in range(len(guide_list) - 1): for i in range(len(guide_list) - 1):
index1 = p.named_point_index(guide_list[i]) index1 = p.named_point_index(guide_list[i])
index2 = p.named_point_index(guide_list[i+1]) index2 = p.named_point_index(guide_list[i+1])
if index2 - index1 > max_values[i]: if index2 - index1 > max_values[i+1]:
max_values[i] = index2 - index1 max_values[i+1] = index2 - index1
max_values_index[i] = j max_values_index[i+1] = j
index1 = p.named_point_index(guide_list[-1])
index2 = len(p) - 1
if index2 - index1 > max_values[-1]:
max_values[-1] = index2 - index1
max_values_index[-1] = j
for i in range(len(max_values)): for i in range(len(max_values)):
for isect in range(max_values_index[i], len(profiles)-1): for isect in range(max_values_index[i], len(profiles)-1):
self.compl_sect(profiles[isect], self.compl_sect(profiles[isect],
profiles[isect+1], profiles[isect+1],
guide_list[i], gl1[i], gl2[i])
guide_list[i+1])
for isect in reversed(range(0, max_values_index[i])): for isect in reversed(range(0, max_values_index[i])):
self.compl_sect(profiles[isect+1], self.compl_sect(profiles[isect+1],
profiles[isect], profiles[isect],
guide_list[i], gl1[i], gl2[i])
guide_list[i+1])
return profiles return profiles
def interpolate_transversal_step(self, def interpolate_transversal_step(self,
@@ -137,14 +140,12 @@ class InternalMeshing(AMeshingTool):
p.points[k].z = pt1[k].z + \ p.points[k].z = pt1[k].z + \
dj*(pt2[k].z - dj*(pt2[k].z -
pt1[k].z) pt1[k].z)
p.rk = round(profiles[i].rk + p.rk = profiles[i].rk + \
dj*(profiles[i+1].rk - dj*(profiles[i+1].rk -
profiles[i].rk), 2) profiles[i].rk)
p.name = f'interpol{p.rk}' p.name = f'interpol{p.rk}'
p.interpolated = True
new_profiles2.append(p) new_profiles2.append(p)
new_profiles.append(new_profiles2) new_profiles.append(new_profiles2)
return new_profiles return new_profiles
def compl_sect(self, sect1, sect2, tag1, tag2): def compl_sect(self, sect1, sect2, tag1, tag2):
@@ -155,9 +156,17 @@ class InternalMeshing(AMeshingTool):
# len1 and 2: number of intervals # len1 and 2: number of intervals
# len1: target len # len1: target len
if tag1 == '': # left end point
start1 = 0
start2 = 0
else:
start1 = sect1.named_point_index(tag1) start1 = sect1.named_point_index(tag1)
start2 = sect2.named_point_index(tag1) start2 = sect2.named_point_index(tag1)
if tag2 == '': # right end point
end1 = sect1.nb_points-1
end2 = sect2.nb_points-1
else:
end1 = sect1.named_point_index(tag2) end1 = sect1.named_point_index(tag2)
end2 = sect2.named_point_index(tag2) end2 = sect2.named_point_index(tag2)
@@ -165,140 +174,104 @@ class InternalMeshing(AMeshingTool):
len2 = end2 - start2 len2 = end2 - start2
if len1 < len2: if len1 < len2:
self.compl_sect(sect2, sect1, tag1, tag2) self.compl_sect(sect2, sect1, start2, start1,
len2, len1, tag1, tag2)
return return
elif len1 == len2: elif len1 == len2:
return return
ltot1 = 0.0 ltot1 = 0.0
ltot2 = 0.0 ltot2 = 0.0
alpha = [0.0] alpha = []
beta = [0.0] beta = []
# 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)
beta = list(map(lambda x: x/ltot2, beta)) # current ratios
for i in range(len1 - len2):
beta.append(1.0)
beta.append(0.0) # beta[-1]
if len2 < 1 or ltot2 < 0.0001: if len2 < 1 or ltot2 < 0.0001:
# unique point (copy len1 times) # unique point (copy len1 times)
# we are at an edge of the profile # we are at an edge of the profile
for i in range(len1-len2): for i in range(len1-len2):
p = sect2.point(start2).copy() p = sect2.point(start2).copy()
sect2.insert_point(start2, p) sect2.insert_point(start2, p)
len2 += 1 if tag1 == '': # left end point
sect2.point(start2).name = ''
if tag2 == '': # left end point
sect2.point(start2+1).name = ''
elif ltot1 < 0.0001: elif ltot1 < 0.0001:
sect2.add_npoints(len1-len2) sect2.add_npoints(len1-len2)
len2 = len1
else: # regular case else: # regular case
alpha = list(map(lambda x: x/ltot1, alpha)) # target ratios # loop on the points to insert
beta = list(map(lambda x: x/ltot2, beta)) # current ratios for k in range(len1-len2):
match = self._match_indices(alpha, beta) trouve = False
for j0 in range(len2):
insertions_per_segment = [] # liste de (index, [points]) if trouve:
for j in range(len2): break
i_start = match[j] undeplus = True
i_end = match[j + 1] for j1 in range(j0, len2):
q_start = sect2.point(start2 + j) if undeplus:
q_end = sect2.point(start2 + j + 1) if beta[j1] <= alpha[j1]:
undeplus = False
new_points = [] break
if i_end - i_start > 1: if undeplus:
u_start, u_end = alpha[i_start], alpha[i_end] if j0 == len2:
span = u_end - u_start trouve = True
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
if tag1 != "un":
sect2.point(start2).name = tag1
if tag2 != "np":
sect2.point(start2+len2).name = tag2
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: else:
best = (INF, None) j1 = j0+1
for i_prev in candidates[:idx_pos]: if beta[j0]-alpha[j0] > alpha[j1]-beta[j0]:
if i_prev in dp[j - 1]: trouve = True
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 if trouve:
# meilleur indice tel que la suite jusqu'à fixed_last len2 += 1
# reste croissante et valide. for i in reversed(range(j0, len2)):
last_level = dp[-1] beta[i+1] = beta[i]
valid = {i: c for i, c in last_level.items() if i < fixed_last} if (beta[j0] == beta[j0-1]):
if not valid: # rien
raise RuntimeError("Alignement impossible avec ces données.") pass
best_i = min(valid, key=lambda i: valid[i][0]) else:
ratio = (alpha[j0] - beta[j0-1]) \
/ (beta[j0] - beta[j0-1])
if ratio < 0.0:
print(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]
chain = [best_i] if len1 > len2:
for j in range(len(inner_targets) - 1, 0, -1): for i in range(len1 - len2):
prev_i = dp[j][chain[-1]][1] p = sect2.point(start2+len2).copy()
chain.append(prev_i) sect2.insert_point(start2+len2, p)
chain.reverse() sect2.point(start2+len2).name = ''
sect2.point(start2).name = tag1
return [fixed_first] + chain + [fixed_last] sect2.point(start2+len2).name = tag2
sect2.modified()
def update_rk(self, reach, begin_rk, end_rk, def update_rk(self, reach, begin_rk, end_rk,
step: float = 50, step: float = 50,
@@ -331,8 +304,6 @@ class InternalMeshing(AMeshingTool):
sgn = -1.0 sgn = -1.0
else: else:
sgn = old_orientation sgn = old_orientation
if abs(sgn) < 0.5:
sgn = 1.0
new_rk = [0.0]*nprof new_rk = [0.0]*nprof
new_rk[origin] = origin_value new_rk[origin] = origin_value
@@ -373,42 +344,3 @@ class InternalMeshing(AMeshingTool):
return new_rk, new_end_rk, new_begin_rk return new_rk, new_end_rk, new_begin_rk
else: else:
return new_rk, new_begin_rk, new_end_rk return new_rk, new_begin_rk, new_end_rk
def get_guide_list(self, reach, profiles):
gl = reach.compute_guidelines()
# we make sure that the lines are in the left-to-right order
guide_list = [
[
x.name
for x in p.named_points()
if x.name in gl[0]
]
for p in profiles]
# we make sure the lines never cross
for gl in guide_list:
for i in range(len(guide_list[0])):
if gl[i] != guide_list[0][i]:
return None
return guide_list[0]
def get_profiles(self, reach, limites):
if reach is None or len(reach.profiles) == 0:
return None
if limites[0] > limites[1]:
lim = limites[1]
limites[1] = limites[0]
limites[0] = lim
elif limites[0] == limites[1]:
return None
profiles = [reach.profile(i).copy()
for i in range(limites[0], limites[1]+1)
]
return profiles
+1 -1
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr # Mage.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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
@@ -232,8 +232,6 @@ class BoundaryCondition(SQLSubModel):
type TEXT NOT NULL, type TEXT NOT NULL,
tab TEXT NOT NULL, tab TEXT NOT NULL,
node INTEGER, node INTEGER,
d50 REAL DEFAULT 0.002,
sigma REAL DEFAULT 1,
{Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}, {Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id), FOREIGN KEY(node) REFERENCES river_node(pamhyr_id),
@@ -260,17 +258,6 @@ class BoundaryCondition(SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
) )
if major == "0" and int(minor) <= 2:
if int(release) < 4:
execute(
"ALTER TABLE boundary_condition " +
"ADD COLUMN d50 REAL DEFAULT 0.002"
)
execute(
"ALTER TABLE boundary_condition " +
"ADD COLUMN sigma REAL DEFAULT 1"
)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@classmethod @classmethod
@@ -329,8 +316,7 @@ class BoundaryCondition(SQLSubModel):
return new return new
table = execute( table = execute(
"SELECT pamhyr_id, deleted, name, type, node, d50, sigma, " + "SELECT pamhyr_id, deleted, name, type, node, scenario " +
"scenario " +
"FROM boundary_condition " + "FROM boundary_condition " +
f"WHERE tab = '{tab}' " + f"WHERE tab = '{tab}' " +
f"AND scenario = {scenario.id} " + f"AND scenario = {scenario.id} " +
@@ -345,8 +331,6 @@ class BoundaryCondition(SQLSubModel):
name = next(it) name = next(it)
t = next(it) t = next(it)
node = next(it) node = next(it)
d50 = next(it)
sigma = next(it)
owner_scenario = next(it) owner_scenario = next(it)
ctor = cls._get_ctor_from_type(t) ctor = cls._get_ctor_from_type(t)
@@ -356,14 +340,9 @@ class BoundaryCondition(SQLSubModel):
status=data['status'], status=data['status'],
owner_scenario=owner_scenario owner_scenario=owner_scenario
) )
if deleted: if deleted:
bc.set_as_deleted() bc.set_as_deleted()
if t == "SL":
bc.d50 = d50
bc.sigma = sigma
bc.node = None bc.node = None
if node != -1: if node != -1:
bc.node = next(filter(lambda n: n.id == node, nodes), None) bc.node = next(filter(lambda n: n.id == node, nodes), None)
@@ -396,22 +375,15 @@ class BoundaryCondition(SQLSubModel):
if self._node is not None: if self._node is not None:
node = self._node.id node = self._node.id
d50 = 0.002
sigma = 1
if self._type == "SL":
d50 = self._d50
sigma = self._sigma
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"boundary_condition(" + "boundary_condition(" +
"pamhyr_id, deleted, name, type, tab, node, d50, sigma, scenario" + "pamhyr_id, deleted, name, type, tab, node, scenario" +
") " + ") " +
"VALUES (" + "VALUES (" +
f"{self._pamhyr_id}, {self._db_format(self.is_deleted())}, " + f"{self._pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " + f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', '{tab}', {node}, " + f"'{self._db_format(self._type)}', '{tab}', {node}, " +
f"{d50}, {sigma}, " +
f"{self._status.scenario_id}" + f"{self._status.scenario_id}" +
")" ")"
) )
@@ -496,24 +468,6 @@ class BoundaryCondition(SQLSubModel):
def has_node(self): def has_node(self):
return self._node is not None return self._node is not None
@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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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
@@ -70,8 +70,9 @@ class Data(SQLSubModel):
def _db_update(cls, execute, version, data=None): def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False created = False
if major == "0" and minor == "0":
if int(release) < 11: if major == "0" and int(minor) <= 1:
if int(release) < 7:
cls._db_create(execute) cls._db_create(execute)
created = True created = True
@@ -142,9 +143,8 @@ class Data(SQLSubModel):
table = execute( table = execute(
"SELECT pamhyr_id, deleted, data0, data1, scenario FROM " + "SELECT pamhyr_id, deleted, data0, data1, scenario FROM " +
"boundary_condition_data_adists " + "boundary_condition_data_adists " +
f"WHERE scenario = {scenario.id} " + f"WHERE bca = {bca.id} " +
# f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " + f"AND scenario = {scenario.id}"
f"AND bca = {bca.id}"
) )
if table is not None: if table is not None:
@@ -187,9 +187,8 @@ class Data(SQLSubModel):
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"boundary_condition_data_adists(" + "boundary_condition_data_adists(" +
"pamhyr_id, deleted, data0, data1, bca, scenario) " + "pamhyr_id, data0, data1, bca, scenario) " +
f"VALUES ({self.id}, {self._db_format(self.is_deleted())}, " + f"VALUES ({self.id}, '{data0}', {data1}, {bca.id}, " +
f"'{data0}', {data1}, {bca.id}, " +
f"{self._status.scenario_id})" f"{self._status.scenario_id})"
) )
@@ -248,13 +247,15 @@ class BoundaryConditionAdisTS(SQLSubModel):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False created = False
if major == "0" and int(minor) < 2: if major == "0" and int(minor) <= 1:
if cls.is_table_exists(execute, "boundary_condition_adists"): if int(release) < 7:
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute) cls._db_create(execute)
created = True created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created: if not created:
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@@ -299,14 +300,6 @@ class BoundaryConditionAdisTS(SQLSubModel):
if pol_id == -1: if pol_id == -1:
continue continue
if pol_id not in pid_pol:
# ⚠️ cas important : probablement déjà migré
logger.warning(
f"pol_id {pol_id} not in pid_pol " +
"→ probably already migrated"
)
continue
execute( execute(
f"UPDATE boundary_condition_adists " + f"UPDATE boundary_condition_adists " +
f"SET pollutant = {pid_pol[pol_id]} " + f"SET pollutant = {pid_pol[pol_id]} " +
@@ -319,21 +312,17 @@ class BoundaryConditionAdisTS(SQLSubModel):
status = data['status'] status = data['status']
nodes = data['nodes'] nodes = data['nodes']
scenario = data["scenario"] scenario = data["scenario"]
pollutant = data.get("pollutant")
loaded = data['loaded_pid'] loaded = data['loaded_pid']
if scenario is None: if scenario is None:
return new return new
sql = ( table = execute(
"SELECT pamhyr_id, deleted, pollutant, type, node, scenario " "SELECT pamhyr_id, deleted, pollutant, type, node, scenario " +
"FROM boundary_condition_adists " + "FROM boundary_condition_adists " +
f"WHERE scenario = {scenario.id} " f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
) )
if pollutant is not None:
sql += f"AND pollutant = {pollutant.id} "
table = execute(sql)
if table is not None: if table is not None:
for row in table: for row in table:
@@ -358,7 +347,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
bc.type = bc_type bc.type = bc_type
bc.node = None bc.node = None
if node != -1: if row[3] != -1:
tmp = next( tmp = next(
filter( filter(
lambda n: n.id == node, nodes lambda n: n.id == node, nodes
@@ -437,14 +426,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
d._data_traversal(predicate, modifier, data) d._data_traversal(predicate, modifier, data)
def __len__(self): def __len__(self):
return len( return len(self._data)
list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
)
@classmethod @classmethod
def time_convert(cls, data): def time_convert(cls, data):
@@ -469,7 +451,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@node.setter @node.setter
def node(self, node): def node(self, node):
self._node = node self._node = node
self.modified() self._status.modified()
@property @property
def header(self): def header(self):
@@ -478,7 +460,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@header.setter @header.setter
def header(self, header): def header(self, header):
self._header = header self._header = header
self.modified() self._status.modified()
@property @property
def pollutant(self): def pollutant(self):
@@ -487,7 +469,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@pollutant.setter @pollutant.setter
def pollutant(self, pollutant): def pollutant(self, pollutant):
self._pollutant = pollutant self._pollutant = pollutant
self.modified() self._status.modified()
@property @property
def type(self): def type(self):
@@ -496,16 +478,11 @@ class BoundaryConditionAdisTS(SQLSubModel):
@type.setter @type.setter
def type(self, type): def type(self, type):
self._type = type self._type = type
self.modified() self._status.modified()
@property @property
def data(self): def data(self):
return list( return self._data.copy()
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property @property
def _default_0(self): def _default_0(self):
@@ -531,42 +508,36 @@ class BoundaryConditionAdisTS(SQLSubModel):
new_0 = self._types[0](data[0].replace(",", ".")) new_0 = self._types[0](data[0].replace(",", "."))
new_1 = self._types[1](data[1].replace(",", ".")) new_1 = self._types[1](data[1].replace(",", "."))
return Data(new_0, new_1, types=self._types, status=self._status) return (new_0, new_1)
def add(self, index: int): def add(self, index: int):
value = Data( value = (self._default_0, self._default_1)
self._default_0, self._default_1,
status=self._status
)
self._data.insert(index, value) self._data.insert(index, value)
self.modified() self._status.modified()
return value return value
def insert(self, index: int, value): def insert(self, index: int, value):
if value in self._data:
value.set_as_not_deleted()
else:
self._data.insert(index, value) self._data.insert(index, value)
self.modified() self._status.modified()
def delete_i(self, indexes): def delete_i(self, indexes):
list( self._data = list(
map( map(
lambda e: e[1].set_as_deleted(), lambda e: e[1],
filter( filter(
lambda e: e[0] in indexes, lambda e: e[0] not in indexes,
enumerate(self.data) enumerate(self.data)
) )
) )
) )
self.modified() self._status.modified()
def sort(self, _reverse=False, key=None): def sort(self, _reverse=False, key=None):
if key is None: if key is None:
self._data.sort(reverse=_reverse) self._data.sort(reverse=_reverse)
else: else:
self._data.sort(reverse=_reverse, key=key) self._data.sort(reverse=_reverse, key=key)
self.modified() self._status.modified()
def index(self, bc): def index(self, bc):
self._data.index(bc) self._data.index(bc)
@@ -581,10 +552,10 @@ class BoundaryConditionAdisTS(SQLSubModel):
return lst return lst
def _set_i_c_v(self, index, column, value): def _set_i_c_v(self, index, column, value):
v = self._data[index] v = list(self._data[index])
v[column] = self._types[column](value) v[column] = self._types[column](value)
self._data[index] = v self._data[index] = tuple(v)
self.modified() self._status.modified()
def set_i_0(self, index: int, value): def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value) self._set_i_c_v(index, 0, value)
@@ -1,5 +1,5 @@
# BoundaryConditionsAdisTSList.py -- Pamhyr # BoundaryConditionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -38,13 +38,9 @@ class BoundaryConditionsAdisTSList(PamhyrModelList):
if data is None: if data is None:
data = {} data = {}
previous_pollutant = data.get("pollutant") new._lst = BoundaryConditionAdisTS._db_load(
data.pop("pollutant", None) execute, data
)
new._lst = BoundaryConditionAdisTS._db_load(execute, data)
if previous_pollutant is not None:
data["pollutant"] = previous_pollutant
return new return new
@@ -1,481 +0,0 @@
# BoundaryConditionsTemperature.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class Data(SQLSubModel):
_sub_classes = []
def __init__(self,
data0, data1,
id: int = -1,
types=[float, float],
status=None,
owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._types = types
self._data = [data0, data1]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_data_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
bca INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(bca)
REFERENCES boundary_condition_temperature(pamhyr_id)
)
""")
return True
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and minor == "0":
if int(release) < 11:
cls._db_create(execute)
created = True
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute,
"boundary_condition_data_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
bca = data["bca"]
table = execute(
"SELECT pamhyr_id, deleted, data0, data1, scenario FROM " +
"boundary_condition_data_temperature " +
f"WHERE scenario = {scenario.id} " +
# f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
f"AND bca = {bca.id}"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
data0 = next(it)
data1 = next(it)
owner_scenario = next(it)
bc = cls(
data0, data1,
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
bca = data["bca"]
data0 = self._db_format(self._data[0])
data1 = self._db_format(self._data[1])
execute(
"INSERT INTO " +
"boundary_condition_data_temperature(" +
"pamhyr_id, deleted, data0, data1, bca, scenario) " +
f"VALUES ({self.id}, {self._db_format(self.is_deleted())}, " +
f"'{data0}', {data1}, {bca.id}, " +
f"{self._status.scenario_id})"
)
return True
def __getitem__(self, key):
return self._types[key](self._data[key])
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
class BoundaryConditionTemperature(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1, status=None, owner_scenario=-1):
super(BoundaryConditionTemperature, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = ""
self._node = None
self._data = []
self._header = []
self._types = [self.time_convert, float]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
node INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and (int(minor) < 2 or (
int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute,
"boundary_condition_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
nodes = data['nodes']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
sql = (
"SELECT pamhyr_id, deleted, name, node, scenario "
"FROM boundary_condition_temperature " +
f"WHERE scenario = {scenario.id} "
)
table = execute(sql)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
bc_name = next(it)
node = next(it)
owner_scenario = next(it)
bc = cls(
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
bc.name = bc_name
bc.node = None
if node != -1:
tmp = next(
filter(
lambda n: n.id == node, nodes
),
None
)
if tmp is not None:
bc.node = tmp.id
else:
bc.node = -1
data["bca"] = bc
bc._data = Data._db_load(execute, data=data)
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM boundary_condition_temperature " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE bca = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
node = -1
if self._node is not None:
node = self._node
execute(
"INSERT INTO " +
"boundary_condition_temperature(" +
"pamhyr_id, deleted, name, " +
"node, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', {node}, " +
f"{self._status.scenario_id}" +
")"
)
data["bca"] = self
ind = 0
for d in self._data:
data["ind"] = ind
d._db_save(execute, data)
ind += 1
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for d in self._data:
d._data_traversal(predicate, modifier, data)
def __len__(self):
return len(
list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
)
@classmethod
def time_convert(cls, data):
if type(data) is str:
if data.count("-") == 2:
return date_iso_to_timestamp(data)
if data.count("/") == 2:
return date_dmy_to_timestamp(data)
if data.count(":") == 3:
return old_pamhyr_date_to_timestamp(data)
if data.count(":") == 2:
return old_pamhyr_date_to_timestamp("00:" + data)
if data.count(".") == 1:
return round(float(data))
return int(data)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def node(self):
return self._node
@node.setter
def node(self, node):
self._node = node
self.modified()
@property
def header(self):
return self._header.copy()
@header.setter
def header(self, header):
self._header = header
self.modified()
@property
def data(self):
return list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property
def _default_0(self):
return self._types[0](0)
@property
def _default_1(self):
return self._types[1](0.0)
def new_from_data(self, header, data):
new_0 = self._default_0
new_1 = self._default_1
if len(header) != 0:
for i in [0, 1]:
for j in range(len(header)):
if self._header[i] == header[j]:
if i == 0:
new_0 = self._types[i](data[j].replace(",", "."))
else:
new_1 = self._types[i](data[j].replace(",", "."))
else:
new_0 = self._types[0](data[0].replace(",", "."))
new_1 = self._types[1](data[1].replace(",", "."))
return Data(new_0, new_1, types=self._types, status=self._status)
def add(self, index: int):
value = Data(
self._default_0, self._default_1,
status=self._status
)
self._data.insert(index, value)
self.modified()
return value
def insert(self, index: int, value):
if value in self._data:
value.set_as_not_deleted()
else:
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self.data)
)
)
)
self.modified()
def sort(self, _reverse=False, key=None):
if key is None:
self._data.sort(reverse=_reverse)
else:
self._data.sort(reverse=_reverse, key=key)
self.modified()
def index(self, bc):
self._data.index(bc)
def get_i(self, index):
return self.data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index, column, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)
@@ -1,71 +0,0 @@
# BoundaryConditionsTemperatureList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Except import NotImplementedMethodeError
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
class BoundaryConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
BoundaryConditionTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = BoundaryConditionTemperature._db_load(execute, data)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM boundary_condition_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for bca in self._lst:
bca._db_save(execute, data=data)
return True
def new(self, index):
n = BoundaryConditionTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
@property
def BCs_Temperature_List(self):
return self.lst
+7 -46
View File
@@ -1,5 +1,5 @@
# D90AdisTS.py -- Pamhyr # D90AdisTS.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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,13 +73,15 @@ class D90AdisTS(SQLSubModel):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False created = False
if major == "0" and int(minor) < 2: if major == "0" and int(minor) <= 1:
if cls.is_table_exists(execute, "d90_adists"): if int(release) < 6:
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute) cls._db_create(execute)
created = True created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created: if not created:
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@@ -275,44 +277,3 @@ class D90AdisTS(SQLSubModel):
x.set_as_not_deleted() x.set_as_not_deleted()
self.modified() self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
parts = [
(reach1.id, min(reach_rks), split_rk),
(reach2.id, split_rk, max(reach_rks)),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
+1 -5
View File
@@ -1,5 +1,5 @@
# D90AdisTSList.py -- Pamhyr # D90AdisTSList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -61,10 +61,6 @@ class D90AdisTSList(PamhyrModelList):
self._status.modified() self._status.modified()
return n return n
def split_reach(self, reach, profile, reach1, reach2):
for d90 in self.lst:
d90.split_reach(reach, profile, reach1, reach2)
@property @property
def D90_AdisTS_List(self): def D90_AdisTS_List(self):
return self.lst return self.lst
+15 -25
View File
@@ -1,5 +1,5 @@
# D90AdisTSSpec.py -- Pamhyr # D90AdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -70,12 +70,16 @@ class D90AdisTSSpec(SQLSubModel):
@classmethod @classmethod
def _db_update(cls, execute, version, data=None): def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False
if major == 0 and minor < 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2: if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "d90_spec"): if not created:
cls._db_update_to_0_2_0(execute, data) cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True return True
@@ -83,7 +87,6 @@ class D90AdisTSSpec(SQLSubModel):
def _db_update_to_0_2_0(cls, execute, data): def _db_update_to_0_2_0(cls, execute, data):
table = "d90_spec" table = "d90_spec"
table_new = "d90_adists_spec" table_new = "d90_adists_spec"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data) cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table) Scenario.update_db_add_scenario(execute, table)
@@ -92,10 +95,8 @@ class D90AdisTSSpec(SQLSubModel):
execute( execute(
f"INSERT INTO {table_new}_tmp " + f"INSERT INTO {table_new}_tmp " +
"(pamhyr_id, d90_default, name, reach, " + "(pamhyr_id, name, d90, enabled, scenario) " +
" start_rk, end_rk, d90, enabled, scenario) " + "SELECT pamhyr_id, name, d90, enabled, scenario " +
"SELECT pamhyr_id, d90_default, name, reach, " +
" start_rk, end_rk, d90, enabled, scenario " +
f"FROM {table}" f"FROM {table}"
) )
@@ -103,13 +104,12 @@ class D90AdisTSSpec(SQLSubModel):
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}") execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
cls._db_update_to_0_2_0_set_d90_pid(execute, data) cls._db_update_to_0_2_0_set_d90_pid(execute, data)
cls._db_update_to_0_2_0_set_reach_pid(execute, table_new, reachs)
@classmethod @classmethod
def _db_update_to_0_2_0_set_d90_pid(cls, execute, data): def _db_update_to_0_2_0_set_d90_pid(cls, execute, data):
pid_d90 = data["id2pid"]["d90_adists"] pid_d90 = data["id2pid"]["d90_adists"]
els = execute( els = execute(
f"SELECT pamhyr_id, d90_default FROM d90_adists_spec" f"SELECT pamhyr_id, d90 FROM d90_adists_spec"
) )
for row in els: for row in els:
@@ -122,7 +122,7 @@ class D90AdisTSSpec(SQLSubModel):
execute( execute(
f"UPDATE d90_adists_spec" + f"UPDATE d90_adists_spec" +
f"SET d90_default = {pid_d90[d90_id]} " + f"SET d90 = {pid_d90[d90_id]} " +
f"WHERE pamhyr_id = {pid}" f"WHERE pamhyr_id = {pid}"
) )
@@ -167,7 +167,7 @@ class D90AdisTSSpec(SQLSubModel):
owner_scenario=owner_scenario owner_scenario=owner_scenario
) )
if deleted: if deleted:
new_spec.set_as_deleted() new_spec.is_deleted()
new_spec.reach = reach new_spec.reach = reach
new_spec.start_rk = start_rk new_spec.start_rk = start_rk
@@ -196,12 +196,12 @@ class D90AdisTSSpec(SQLSubModel):
"d90_default, name, reach, " + "d90_default, name, reach, " +
"start_rk, end_rk, d90, enabled, scenario) " + "start_rk, end_rk, d90, enabled, scenario) " +
"VALUES (" + "VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " + f"{self.id}, {self._db_format(self.is_deleted())}" +
f"{d90_default}, " + f"{d90_default}, " +
f"'{self._db_format(self._name_section)}', " + f"'{self._db_format(self._name_section)}', " +
f"{self._reach}, " + f"{self._reach}, " +
f"{self._start_rk}, {self._end_rk}, " + f"{self._start_rk}, {self._end_rk}, " +
f"{self._d90}, {self._enabled}, " + f"{self._d90}, {self._enabled}" +
f"{self._status.scenario_id}" + f"{self._status.scenario_id}" +
")" ")"
) )
@@ -261,13 +261,3 @@ class D90AdisTSSpec(SQLSubModel):
def enabled(self, enabled): def enabled(self, enabled):
self._enabled = enabled self._enabled = enabled
self.modified() self.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = D90AdisTSSpec(name=self._name_section, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._d90 = self._d90
new._enabled = self._enabled
new.modified()
return new
+17 -56
View File
@@ -1,5 +1,5 @@
# DIFAdisTS.py -- Pamhyr # DIFAdisTS.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -79,13 +79,15 @@ class DIFAdisTS(SQLSubModel):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False created = False
if major == "0" and int(minor) < 2: if major == "0" and int(minor) <= 1:
if cls.is_table_exists(execute, "dif_adists"): if int(release) < 6:
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute) cls._db_create(execute)
created = True created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created: if not created:
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@@ -140,24 +142,24 @@ class DIFAdisTS(SQLSubModel):
enabled = (next(it) == 1) enabled = (next(it) == 1)
owner_scenario = next(it) owner_scenario = next(it)
DIF = cls( dif = cls(
id=pid, id=pid,
name=name, name=name,
status=status, status=status,
owner_scenario=owner_scenario, owner_scenario=owner_scenario,
) )
DIF.method = method dif.method = method
DIF.dif = dif dif.dif = dif
DIF.b = b dif.b = b
DIF.c = c dif.c = c
DIF.enabled = enabled dif.enabled = enabled
data['dif_default_id'] = pid data['dif_default_id'] = pid
DIF._data = DIFAdisTSSpec._db_load(execute, data) dif._data = DIFAdisTSSpec._db_load(execute, data)
loaded.add(pid) loaded.add(pid)
new.append(DIF) new.append(dif)
data["scenario"] = scenario.parent data["scenario"] = scenario.parent
new += cls._db_load(execute, data) new += cls._db_load(execute, data)
@@ -188,7 +190,7 @@ class DIFAdisTS(SQLSubModel):
b = self.b b = self.b
c = -1. c = -1.
if self.c is not None: if self.dif is not None:
c = self.c c = self.c
sql = ( sql = (
@@ -199,7 +201,7 @@ class DIFAdisTS(SQLSubModel):
"VALUES (" + "VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " + f"{self.id}, '{self._db_format(self._name)}', " +
f"'{self._db_format(self._method)}', " + f"'{self._db_format(self._method)}', " +
f"{dif}, {b}, {c}, {self._enabled}, " + f"{dif.id}, {b}, {c}, {self._enabled}, " +
f"{self._status.scenario_id}" + f"{self._status.scenario_id}" +
")" ")"
) )
@@ -329,44 +331,3 @@ class DIFAdisTS(SQLSubModel):
x.set_as_not_deleted() x.set_as_not_deleted()
self.modified() self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
parts = [
(reach1.id, min(reach_rks), split_rk),
(reach2.id, split_rk, max(reach_rks)),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
+1 -5
View File
@@ -1,5 +1,5 @@
# DIFAdisTSList.py -- Pamhyr # DIFAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -61,10 +61,6 @@ class DIFAdisTSList(PamhyrModelList):
self._status.modified() self._status.modified()
return n return n
def split_reach(self, reach, profile, reach1, reach2):
for dif in self.lst:
dif.split_reach(reach, profile, reach1, reach2)
@property @property
def DIF_AdisTS_List(self): def DIF_AdisTS_List(self):
return self.lst return self.lst
+21 -26
View File
@@ -1,5 +1,5 @@
# DIFAdisTSSpec.py -- Pamhyr # DIFAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -33,14 +33,16 @@ class DIFAdisTSSpec(SQLSubModel):
_id_cnt = 0 _id_cnt = 0
def __init__(self, id: int = -1, method: str = "", def __init__(self, id: int = -1, method: str = "",
status=None, owner_scenario=-1): status=None):
super(DIFAdisTSSpec, self).__init__( super(DIFAdisTSSpec, self).__init__()
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status self._status = status
if id == -1:
self.id = DIFAdisTSSpec._id_cnt
else:
self.id = id
self._method = method self._method = method
self._reach = None self._reach = None
self._start_rk = None self._start_rk = None
@@ -50,6 +52,8 @@ class DIFAdisTSSpec(SQLSubModel):
self._c = None self._c = None
self._enabled = True self._enabled = True
DIFAdisTSSpec._id_cnt = max(DIFAdisTSSpec._id_cnt + 1, self.id)
@classmethod @classmethod
def _db_create(cls, execute, ext=""): def _db_create(cls, execute, ext=""):
execute(f""" execute(f"""
@@ -77,12 +81,16 @@ class DIFAdisTSSpec(SQLSubModel):
@classmethod @classmethod
def _db_update(cls, execute, version, data=None): def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False
if major == 0 and minor < 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2: if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "dif_spec"): if not created:
cls._db_update_to_0_2_0(execute, data) cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True return True
@@ -187,7 +195,7 @@ class DIFAdisTSSpec(SQLSubModel):
new_spec.c = c new_spec.c = c
new_spec.enabled = enabled new_spec.enabled = enabled
loaded.add(id) loaded.add(pid)
new.append(new_spec) new.append(new_spec)
data["scenario"] = scenario.parent data["scenario"] = scenario.parent
@@ -202,23 +210,22 @@ class DIFAdisTSSpec(SQLSubModel):
dif_default = data['dif_default_id'] dif_default = data['dif_default_id']
sql = ( execute(
"INSERT INTO " + "INSERT INTO " +
"dif_adists_spec(pamhyr_id, deleted, " + "dif_adists_spec(pamhyr_id, deleted, " +
"dif_default, method, reach, " + "dif_default, method, reach, " +
"start_rk, end_rk, dif, b, c, enabled, scenario) " + "start_rk, end_rk, dif, b, c, enabled, scenario) " +
"VALUES (" + "VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " + f"{self.id}, {self._db_format(self.is_deleted())}" +
f"{dif_default}, " + f"{dif_default}, " +
f"'{self._db_format(self._method)}', " + f"'{self._db_format(self._method)}', " +
f"{self._reach}, " + f"{self._reach}, " +
f"{self._start_rk}, {self._end_rk}, " + f"{self._start_rk}, {self._end_rk}, " +
f"{self._dif}, {self._b}, {self._c}, " + f"{self._dif}, {self._b}, {self._c}, " +
f"{self._enabled}, " + f"{self._enabled}" +
f"{self._status.scenario_id}" + f"{self._status.scenario_id}" +
")" ")"
) )
execute(sql)
return True return True
@@ -293,15 +300,3 @@ class DIFAdisTSSpec(SQLSubModel):
def enabled(self, enabled): def enabled(self, enabled):
self._enabled = enabled self._enabled = enabled
self._status.modified() self._status.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = DIFAdisTSSpec(method=self._method, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._dif = self._dif
new._b = self._b
new._c = self._c
new._enabled = self._enabled
new.modified()
return new
-307
View File
@@ -1,307 +0,0 @@
# Distribution.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 -*-
import struct
import logging
import itertools
import numpy as np
from tools import logger_exception
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
logger = logging.getLogger()
class Distribution(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Distribution, self).__init__(
id=id, status=status,
owner_scenario=0
)
self._name = name
self._type = "generic"
self._script = script
self._labels = []
self._default_values = []
self._params_checkers = []
self._params_tips = []
@property
def name(self):
return self._name
@property
def labels(self):
return self._labels
@property
def default_values(self):
return self._default_values
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble_distribution{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
script TEXT NOT NULL,
PRIMARY KEY(pamhyr_id)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, script " +
"FROM ensemble_distribution " +
f"WHERE pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
script = next(it)
new_distribution = cls(
id, name=name, script=script,
status=data["status"]
)
if deleted:
new_distribution.set_as_deleted()
loaded.add(id)
new.append(new_distribution)
return new
def _db_save(self, execute, data=None):
# Only save custom distribution
if self._type != "custom":
return True
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"ensemble_distribution(pamhyr_id, deleted, " +
" name, script) " +
"VALUES (?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._script
)
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
def is_valid_parameters(self, parameters):
valid = True
tips = []
for i, checker in enumerate(self._params_checkers):
ok = checker(parameters)
if not ok:
tips.append(self._params_tips[i])
valid = False
return (valid, tips)
def check_params(self, params):
return True
def random(self, r):
raise NotImplementedMethodeError(cls, cls._load)
###############################
# DISTRIBUTION IMPLEMENTATION #
###############################
class Uniform(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "uniform", script: str = "",
status=None):
super(Uniform, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["low", "high"]
self._default_values = [20.0, 40.0]
self._params_checkers = [self.check_params, self.check_params]
self._params_tips = ["uniform_low_tips", "uniform_high_tips"]
def check_params(self, params):
return (params[0] < params[1])
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.uniform(*lst)
class Normal(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "normal", script: str = "",
status=None):
super(Normal, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["loc", "scale"]
self._default_values = [30.0, 5.0]
self._params_checkers = [self.check_params, self.check_scale]
self._params_tips = ["normal_loc_tips", "normal_scale_tips"]
def check_scale(self, params):
return 0 <= params[1]
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.normal(*lst)
class Binomial(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "binomial", script: str = "",
status=None):
super(Binomial, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["binomial_n", "binomial_p"]
self._default_values = [100.0, 0.3]
self._params_checkers = [self.check_n, self.check_p]
self._params_tips = ["binomial_n_tips", "binomial_p_tips"]
def check_n(self, params):
n = params[0]
return (0 <= n) and (n == round(n))
def check_p(self, params):
p = params[1]
return 0 <= p <= 1
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.binomial(*lst)
class Poisson(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "poisson", script: str = "",
status=None):
super(Poisson, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["poisson_lam"]
self._default_values = [5.0]
self._params_checkers = [self.check_lam]
self._params_tips = ["poisson_lam_tips"]
def check_lam(self, params):
lam = params[0]
return 0 <= lam
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.poisson(*lst)
class Custom(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Custom, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["custom"]
self._type = "custom"
self._default_values = [""]
self._params_checkers = [self.check_params]
self._params_tips = ["custom_params_tips"]
def random(self, params, nb):
try:
# Run script to (re)define the sample distribution
f = eval(self._script)
return f(params, nb)
except Exception as e:
logger_exception(e)
return None # TODO: Raise helpful exception
-62
View File
@@ -1,62 +0,0 @@
# DistributionList.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 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Distribution import *
class DistributionList(PamhyrModelList):
_sub_classes = [Distribution]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
# Default distributions
d = {"status": data['status']}
new._lst = [
Uniform(**d),
Normal(**d),
Binomial(**d),
Poisson(**d),
]
# DB custom distributions
new._lst += Distribution._db_load(
execute, data
)
return new
def _db_save(self, execute, data={}):
execute(
"DELETE FROM ensemble_distribution"
)
for fun in self._lst:
fun._db_save(execute, data)
return True
def new(self, index):
r = Custom(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
-279
View File
@@ -1,279 +0,0 @@
# Ensemble.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 -*-
import struct
import logging
import itertools
import numpy as np
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Ensembles.Distribution import Uniform
logger = logging.getLogger()
class Ensemble(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
distribution=None, params=[0., 100.],
data_type=None, target_data=None,
status=None, owner_scenario=-1):
super(Ensemble, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._data_type = data_type
self._distribution = distribution
self._params = params
self._target_data = target_data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type VARCHAR(8) NOT NULL,
distribution INTEGER NOT NULL,
distribution_name VARCHAR(32) NOT NULL,
params BLOB NOT NULL,
params_len INTEGER NOT NULL,
data_pid INTEGER NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
distributions = data["ens_distributions"]
stricklers = data["stricklers"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"distribution, distribution_name, params, params_len, data_pid, " +
"scenario " +
"FROM ensemble " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
data_type = next(it)
fid = next(it)
fname = next(it)
params = next(it)
params_len = next(it)
data_pid = next(it)
owner_scenario = next(it)
def fn(f): return f._name == fname
if fid != -1:
def fn(f): return f._pamhyr_id == fid
distribution = next(
filter(fn, distributions.lst),
None
)
target_data = next(
filter(
lambda s: s._pamhyr_id == data_pid,
stricklers.lst
),
None
)
rdata = list(cls._decode_params(params, params_len))
new_ensemble = cls(
id, name=name,
distribution=distribution,
params=rdata,
data_type=data_type,
target_data=target_data,
status=data["status"],
owner_scenario=owner_scenario
)
if deleted:
new_ensemble.set_as_deleted()
loaded.add(id)
new.append(new_ensemble)
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM ensemble " +
f"WHERE pamhyr_id = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
bparams, length = self._encode_params()
if self._distribution is None:
fid = -1
fname = "NULL"
else:
fid = self._distribution._pamhyr_id
fname = self._distribution._name
if self._distribution._type == "generic":
fid = -1
data_pid = -1
if self._target_data is not None:
data_pid = self._target_data._pamhyr_id
execute(
"INSERT INTO " +
"ensemble(pamhyr_id, deleted, name, type, " +
" distribution, distribution_name, params, params_len, " +
" data_pid, scenario) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._data_type,
fid, fname, bparams, length,
data_pid, self._status.scenario_id
)
return True
def _encode_params(self):
length = len(self._params)
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.pack(data_format, *self._params), length
@classmethod
def _decode_params(cls, bparams, length):
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.unpack(data_format, bparams)
def random(self, nb):
return np.nditer(
self._distribution.random(
self._params, nb
)
)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
@property
def distribution(self):
return self._distribution
@distribution.setter
def distribution(self, distribution):
self._distribution = distribution
self._status.modified()
@property
def params(self):
return self._params
@params.setter
def params(self, params):
self._params = params
self._status.modified()
@property
def data_type(self):
return self._data_type
@data_type.setter
def data_type(self, data_type):
self._data_type = data_type
self._status.modified()
@property
def target_data(self):
return self._target_data
@target_data.setter
def target_data(self, target_data):
self._target_data = target_data
self._status.modified()
def get_data_dict(self, use_ids=False):
target_data = self.target_data
distribution = self.distribution
if use_ids:
if self.target_data:
target_data = self.target_data.pamhyr_id
if self.distribution:
distribution = self.distribution.name
return {
"name": self.name,
"data_type": self.data_type,
"target_data": target_data,
"distribution": distribution,
"params": self.params,
}
def set_data_dict(self, data):
self.name = data["name"]
self.data_type = data["data_type"]
self.target_data = data["target_data"]
self.distribution = data["distribution"]
self.params = data["params"]
-62
View File
@@ -1,62 +0,0 @@
# EnsembleList.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 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Ensemble import Ensemble
class EnsembleList(PamhyrModelList):
_sub_classes = [Ensemble]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
new._lst = Ensemble._db_load(
execute, data
)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM ensemble " +
f"WHERE scenario = {self._status.scenario_id}"
)
for ens in self._lst:
ens._db_save(execute, data=data)
return True
def new(self, index):
r = Ensemble(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
def new_with_data(self, index, data):
r = Ensemble(status=self._status)
r.set_data_dict(data)
self._lst.insert(index, r)
self._status.modified()
return r
+1 -1
View File
@@ -1,5 +1,5 @@
# Except.py -- Pamhyr model exceptions # Except.py -- Pamhyr model exceptions
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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 -10
View File
@@ -1,5 +1,5 @@
# Friction.py -- Pamhyr # Friction.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -228,10 +228,9 @@ class Friction(SQLSubModel):
ssi = self._status.scenario_id ssi = self._status.scenario_id
return ( return (
self._begin_strickler is not None self._begin_strickler._owner_scenario == ssi
and (self._begin_strickler._owner_scenario == ssi
# or self._end_strickler._owner_scenario == ssi # or self._end_strickler._owner_scenario == ssi
or super(Friction, self).must_be_saved()) or super(Friction, self).must_be_saved()
) )
def _db_save(self, execute, data=None): def _db_save(self, execute, data=None):
@@ -365,9 +364,3 @@ class Friction(SQLSubModel):
# self.end_strickler.medium]) # self.end_strickler.medium])
return minor, medium return minor, medium
def update_rks(self, begin_rk, end_rk):
self._begin_rk = begin_rk
self._end_rk = end_rk
self.modified()
+8 -14
View File
@@ -1,5 +1,5 @@
# FrictionList.py -- Pamhyr # FrictionList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -109,22 +109,16 @@ class FrictionList(PamhyrModelList):
((999 ** 100), -(999 ** 100)) ((999 ** 100), -(999 ** 100))
) )
for friction in self.lst: frictions = list(
lower, upper = sorted((friction.begin_rk, friction.end_rk)) filter(
clipped_lower = max(lower, rk_min) lambda f: ((rk_min <= f.begin_rk <= rk_max) or
clipped_upper = min(upper, rk_max) (rk_min <= f.end_rk <= rk_max)),
if clipped_lower >= clipped_upper: self._lst))
continue
for ind, friction in enumerate(frictions):
n = friction.cloned_for(reach) n = friction.cloned_for(reach)
if friction.begin_rk > friction.end_rk:
n.begin_rk = clipped_upper
n.end_rk = clipped_lower
else:
n.begin_rk = clipped_lower
n.end_rk = clipped_upper
new._lst.append(n) new._lst.insert(ind, n)
new._status.modified() new._status.modified()
return new return new
-368
View File
@@ -1,368 +0,0 @@
# GeoTIFF.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import os
import struct
import logging
from functools import reduce
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
logger = logging.getLogger()
class GeoTIFF(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, enabled=True,
name="", description="",
path="", coordinates=None,
status=None, owner_scenario=-1):
super(GeoTIFF, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._enabled = enabled
self._name = f"GeoTIFF #{self._pamhyr_id}" if name == "" else name
self._description = description
self._file_bytes = b''
if coordinates is None:
self._coordinates = {
"bottom": 0.0,
"top": 0.0,
"left": 0.0,
"right": 0.0,
}
else:
self._coordinates = coordinates
self._file_name = ""
if path != "":
self.read_file(path)
self._memfile = None
def __getitem__(self, key):
value = None
if key == "enabled":
value = self._enabled
elif key == "name":
value = self.name
elif key == "description":
value = self.description
elif key == "file_name":
value = self.file_name
elif key == "coordinates":
value = self.coordinates
elif key == "coordinates_bottom":
value = self.coordinates["bottom"]
elif key == "coordinates_top":
value = self.coordinates["top"]
elif key == "coordinates_left":
value = self.coordinates["left"]
elif key == "coordinates_right":
value = self.coordinates["right"]
elif key == "memfile":
value = self.memfile
return value
def __setitem__(self, key, value):
if key == "enabled":
self.enabled = value
elif key == "name":
self.name = value
elif key == "description":
self.description = value
elif key == "file_name":
if self._file_name != value:
self.read_file(value)
elif key == "coordinates":
self.coordinates = value
elif key == "coordinates_bottom" or key == "bottom":
self.coordinates["bottom"] = value
elif key == "coordinates_top" or key == "top":
self.coordinates["top"] = value
elif key == "coordinates_left" or key == "left":
self.coordinates["left"] = value
elif key == "coordinates_right" or key == "right":
self.coordinates["right"] = value
self.modified()
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self.modified()
def is_enabled(self):
return self._enabled
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def description(self):
return self._description
@description.setter
def description(self, description):
self._description = description
self.modified()
@property
def file_name(self):
return self._file_name
@file_name.setter
def file_name(self, file_name):
self._file_name = file_name
self.modified()
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
self.modified()
@property
def coord_bottom(self):
if self._coordinates is None:
return 0.0
return self._coordinates["bottom"]
@property
def coord_top(self):
if self._coordinates is None:
return 0.0
return self._coordinates["top"]
@property
def coord_left(self):
if self._coordinates is None:
return 0.0
return self._coordinates["left"]
@property
def coord_right(self):
if self._coordinates is None:
return 0.0
return self._coordinates["right"]
@property
def memfile(self):
if not _rasterio_loaded:
return None
if self._file_bytes == b'':
return None
if self._memfile is None:
self._memfile = MemoryFile()
self._memfile.write(self._file_bytes)
return self._memfile
def read_file(self, path):
logger.debug(f"Read GeoTIFF file at : '{path}'")
self._file_name = path
self._file_bytes = b''
self._memfile = None
nbytes = 0
with open(path, "rb") as f:
while True:
data = f.read(4096)
if not data:
break
nbytes += len(data)
self._file_bytes += data
logger.debug(f"Read GeoTIFF: {nbytes} bytes readed")
def write_file(self, path):
with open(path, "w+b") as f:
f.write(self._file_bytes)
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE geotiff{ext} (
{cls.create_db_add_pamhyr_id()},
enabled BOOLEAN NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
description TEXT NOT NULL,
file_name TEXT NOT NULL,
file_bytes BLOB NOT NULL,
coordinates_bottom REAL NOT NULL,
coordinates_top REAL NOT NULL,
coordinates_left REAL NOT NULL,
coordinates_right REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
if major == "0" and int(minor) == 2:
if int(release) < 3:
cls._db_create(execute)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, enabled, deleted, " +
"name, description, file_name, file_bytes, " +
"coordinates_bottom, coordinates_top, " +
"coordinates_left, coordinates_right, " +
"scenario " +
"FROM geotiff " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
if table is not None:
for row in table:
it = iter(row)
id = next(it)
enabled = (next(it) == 1)
deleted = (next(it) == 1)
name = next(it)
description = next(it)
file_name = next(it)
file_bytes = next(it)
coordinates_bottom = next(it)
coordinates_top = next(it)
coordinates_left = next(it)
coordinates_right = next(it)
owner_scenario = next(it)
f = cls(
id=id, enabled=enabled, name=name,
description=description, coordinates={
"bottom": coordinates_bottom,
"top": coordinates_top,
"left": coordinates_left,
"right": coordinates_right,
},
status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
f.set_as_deleted()
f._file_bytes = file_bytes
loaded.add(id)
new.append(f)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"INSERT INTO geotiff (" +
"pamhyr_id, enabled, deleted, " +
"name, description, file_name, file_bytes, " +
"coordinates_bottom, coordinates_top, " +
"coordinates_left, coordinates_right, " +
"scenario) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
self._pamhyr_id,
self._enabled,
self.is_deleted(),
self.name,
self.description,
self.file_name,
self._file_bytes,
self.coordinates['bottom'],
self.coordinates['top'],
self.coordinates['left'],
self.coordinates['right'],
self._status.scenario_id,
)
return True
+1 -4
View File
@@ -1,5 +1,5 @@
# Point.py -- Pamhyr # Point.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -55,11 +55,8 @@ class Point(SQLSubModel):
@name.setter @name.setter
def name(self, name): def name(self, name):
changed = self._name != name
self._name = name self._name = name
self.modified() self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
def is_named(self): def is_named(self):
""" """
+1 -1
View File
@@ -1,5 +1,5 @@
# PointXY.py -- Pamhyr # PointXY.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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 -28
View File
@@ -1,5 +1,5 @@
# PointXYZ.py -- Pamhyr # PointXYZ.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -66,11 +66,6 @@ class PointXYZ(Point):
) )
""") """)
execute(
"CREATE INDEX idx_point " +
"ON geometry_pointXYZ(profile, scenario);"
)
return cls._create_submodel(execute) return cls._create_submodel(execute)
@classmethod @classmethod
@@ -95,12 +90,6 @@ class PointXYZ(Point):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
) )
# Try to create index at each update
execute(
"CREATE INDEX IF NOT EXISTS idx_point " +
"ON geometry_pointXYZ(profile, scenario);"
)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@classmethod @classmethod
@@ -328,11 +317,8 @@ class PointXYZ(Point):
@x.setter @x.setter
def x(self, value): def x(self, value):
changed = self._x != float(value)
self._x = float(value) self._x = float(value)
self.modified() self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
@property @property
def y(self): def y(self):
@@ -340,11 +326,8 @@ class PointXYZ(Point):
@y.setter @y.setter
def y(self, value): def y(self, value):
changed = self._y != float(value)
self._y = float(value) self._y = float(value)
self.modified() self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
@property @property
def z(self): def z(self):
@@ -352,18 +335,8 @@ class PointXYZ(Point):
@z.setter @z.setter
def z(self, value): def z(self, value):
changed = self._z != float(value)
self._z = float(value) self._z = float(value)
self.modified() self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
def shift(self, x, y, z):
self.x += x
self.y += y
self.z += z
self.modified()
def get_coordinate(self): def get_coordinate(self):
return (self._x, self._y, self._z) return (self._x, self._y, self._z)
+12 -76
View File
@@ -1,5 +1,5 @@
# Profile.py -- Pamhyr # Profile.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -47,8 +47,6 @@ class Profile(object):
self._sl = None self._sl = None
self._points: List[Point] = [] self._points: List[Point] = []
self._modification_count = 0
self._old_interpolated = ""
self._profile_type = _type self._profile_type = _type
@@ -78,31 +76,11 @@ class Profile(object):
return self.points[index] return self.points[index]
def named_point(self, name): def named_point(self, name):
if name == "un":
return self.points[0]
elif name == "np":
return self.points[-1]
elif name == "talweg":
zm = self.z_min()
return next((p for p in self.points if p.z == zm), None)
else:
return next((p for p in self.points if p.name == name), None) return next((p for p in self.points if p.name == name), None)
def named_point_index(self, name): def named_point_index(self, name):
if name == "un":
return 0
elif name == "np":
return len(self.points) - 1
elif name == "talweg":
zm = self.z_min()
return next( return next(
(p for p in enumerate(self.points) if p[1].z == zm), (p for p in enumerate(self.points) if p[1].name == name), None
None
)[0]
else:
return next(
(p for p in enumerate(self.points) if p[1].name == name),
None
)[0] )[0]
@property @property
@@ -162,12 +140,7 @@ class Profile(object):
@rk.setter @rk.setter
def rk(self, value: float): def rk(self, value: float):
value = float(value) self._rk = float(value)
changed = value != self._rk
self._rk = value
if changed:
self.content_modified()
else:
self.modified() self.modified()
@property @property
@@ -180,12 +153,7 @@ class Profile(object):
@name.setter @name.setter
def name(self, value: str): def name(self, value: str):
value = value.strip() self._name = value.strip()
changed = value != self._name
self._name = value
if changed:
self.content_modified()
else:
self.modified() self.modified()
@property @property
@@ -229,38 +197,6 @@ class Profile(object):
return [point for point in self.points return [point for point in self.points
if point.is_named()] if point.is_named()]
@property
def modification_count(self):
"""Content revision for this session; not stored in the database."""
return self._modification_count
def increase_modification_count(self):
"""Increase content revision for this session;
not stored in the database."""
self._modification_count += 1
def decrease_modification_count(self):
"""Decrease content revision for this session;
not stored in the database."""
self._modification_count -= 1
@property
def old_interpolated(self):
"""Old interpolated value for this session;
not stored in the database."""
return self._old_interpolated
@old_interpolated.setter
def old_interpolated(self, value):
self._old_interpolated = value
def content_modified(self):
"""An edited profile is no longer an interpolation result."""
# self._modification_count += 1
if hasattr(self, "_interpolated"):
self._interpolated = False
self.modified()
def insert_point(self, index: int, point: Point): def insert_point(self, index: int, point: Point):
"""Insert point at index. """Insert point at index.
@@ -276,7 +212,7 @@ class Profile(object):
else: else:
self._points.insert(index, point) self._points.insert(index, point)
self.content_modified() self.modified()
def delete_i(self, indexes: list): def delete_i(self, indexes: list):
list( list(
@@ -289,7 +225,7 @@ class Profile(object):
) )
) )
self.content_modified() self.modified()
def delete_points(self, points): def delete_points(self, points):
"""Delete some elements in profile list """Delete some elements in profile list
@@ -306,7 +242,7 @@ class Profile(object):
points points
) )
) )
self.content_modified() self.modified()
# Move # Move
@@ -316,7 +252,7 @@ class Profile(object):
p = self._points p = self._points
p[index], p[next] = p[next], p[index] p[index], p[next] = p[next], p[index]
self.content_modified() self.modified()
def move_down_point(self, index: int): def move_down_point(self, index: int):
if index >= 0: if index >= 0:
@@ -324,7 +260,7 @@ class Profile(object):
p = self._points p = self._points
p[index], p[prev] = p[prev], p[index] p[index], p[prev] = p[prev], p[index]
self.content_modified() self.modified()
# Sort # Sort
@@ -341,7 +277,7 @@ class Profile(object):
key=predicate, key=predicate,
reverse=is_reversed reverse=is_reversed
) )
self.content_modified() self.modified()
@timer @timer
def sort_with_indexes(self, indexes: list): def sort_with_indexes(self, indexes: list):
@@ -357,12 +293,12 @@ class Profile(object):
) )
) )
) )
self.content_modified() self.modified()
@timer @timer
def reverse(self): def reverse(self):
self._points.reverse() self._points.reverse()
self.content_modified() self.modified()
# Sediment Layers # Sediment Layers
+57 -151
View File
@@ -1,5 +1,5 @@
# ProfileXYZ.py -- Pamhyr # ProfileXYZ.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -24,7 +24,7 @@ from typing import List
from functools import reduce from functools import reduce
from dataclasses import dataclass from dataclasses import dataclass
from tools import trace, timer, flatten from tools import timer, flatten
from shapely import geometry from shapely import geometry
from Model.Tools.PamhyrDB import SQLSubModel from Model.Tools.PamhyrDB import SQLSubModel
@@ -58,8 +58,7 @@ class ProfileXYZ(Profile, SQLSubModel):
num=0, num=0,
nb_point: int = 0, nb_point: int = 0,
code1: int = 0, code2: int = 0, code1: int = 0, code2: int = 0,
status=None, owner_scenario=-1, status=None, owner_scenario=-1):
enabled=True, interpolated=False):
"""ProfileXYZ constructor """ProfileXYZ constructor
Args: Args:
@@ -91,11 +90,6 @@ class ProfileXYZ(Profile, SQLSubModel):
self.time_l = 0.0 self.time_l = 0.0
self._station = [] self._station = []
self.station_up_to_date = False self.station_up_to_date = False
self._enabled = bool(enabled)
self._interpolated = bool(interpolated)
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=""):
@@ -111,8 +105,6 @@ class ProfileXYZ(Profile, SQLSubModel):
code1 INTEGER NOT NULL, code1 INTEGER NOT NULL,
code2 INTEGER NOT NULL, code2 INTEGER NOT NULL,
sl INTEGER, sl INTEGER,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interpolated BOOLEAN NOT NULL DEFAULT FALSE,
{Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}, {Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id), FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
@@ -157,27 +149,6 @@ class ProfileXYZ(Profile, SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
) )
if major == "0" and int(minor) <= 2:
if int(release) < 10:
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE"
)
if major == "0" and (int(minor) < 2 or
(minor == "2" and int(release) < 10)):
if minor != "0":
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN interpolated BOOLEAN NOT NULL DEFAULT FALSE"
)
execute(
"UPDATE geometry_profileXYZ " +
"SET interpolated = CASE " +
"WHEN LOWER(name) LIKE 'interpol%' THEN TRUE " +
"ELSE FALSE END"
)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@classmethod @classmethod
@@ -263,7 +234,7 @@ class ProfileXYZ(Profile, SQLSubModel):
table = execute( table = execute(
"SELECT pamhyr_id, ind, deleted, name, rk, num, " + "SELECT pamhyr_id, ind, deleted, name, rk, num, " +
"code1, code2, sl, scenario, enabled, interpolated " + "code1, code2, sl, scenario " +
"FROM geometry_profileXYZ " + "FROM geometry_profileXYZ " +
f"WHERE reach = {reach.id} " + f"WHERE reach = {reach.id} " +
f"AND scenario = {scenario.id} " + f"AND scenario = {scenario.id} " +
@@ -284,8 +255,6 @@ class ProfileXYZ(Profile, SQLSubModel):
code2 = next(it) code2 = next(it)
sl = next(it) sl = next(it)
owner_scenario = next(it) owner_scenario = next(it)
enabled = (next(it) == 1)
interpolated = (next(it) == 1)
profile = cls( profile = cls(
id=pid, ind=ind, num=num, id=pid, ind=ind, num=num,
@@ -293,9 +262,7 @@ class ProfileXYZ(Profile, SQLSubModel):
code1=code1, code2=code2, code1=code1, code2=code2,
reach=reach, reach=reach,
status=status, status=status,
owner_scenario=owner_scenario, owner_scenario=owner_scenario
enabled=enabled,
interpolated=interpolated
) )
if deleted: if deleted:
profile.set_as_deleted() profile.set_as_deleted()
@@ -348,14 +315,12 @@ class ProfileXYZ(Profile, SQLSubModel):
execute( execute(
"INSERT OR REPLACE INTO " + "INSERT OR REPLACE INTO " +
"geometry_profileXYZ(pamhyr_id, deleted, ind, name, reach, " + "geometry_profileXYZ(pamhyr_id, deleted, ind, name, reach, " +
"rk, num, code1, code2, sl, scenario, enabled, interpolated) " + "rk, num, code1, code2, sl, scenario) " +
"VALUES (" + "VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " + f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(self._name)}', " + f"{ind}, '{self._db_format(self._name)}', " +
f"{self.reach.pamhyr_id}, {self.rk}, {self.num}, " + f"{self.reach.pamhyr_id}, {self.rk}, {self.num}, " +
f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, " + f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}" +
f"{self._db_format(self._enabled)}, " +
f"{self._db_format(self._interpolated)}" +
")" ")"
) )
@@ -422,9 +387,7 @@ class ProfileXYZ(Profile, SQLSubModel):
name=self.name, name=self.name,
rk=self.rk, rk=self.rk,
reach=self.reach, reach=self.reach,
status=self._status, status=self._status
enabled=self.is_enabled,
interpolated=self.interpolated
) )
if self.is_deleted(): if self.is_deleted():
new_p.set_as_deleted() new_p.set_as_deleted()
@@ -433,6 +396,7 @@ class ProfileXYZ(Profile, SQLSubModel):
for point in self._points: for point in self._points:
p = point.copy() p = point.copy()
print(p)
new_p._points.append(p) new_p._points.append(p)
new_p.modified() new_p.modified()
@@ -444,9 +408,7 @@ class ProfileXYZ(Profile, SQLSubModel):
name=self.name, name=self.name,
rk=self.rk, rk=self.rk,
reach=new_reach, reach=new_reach,
status=self._status, status=self._status
enabled=self.is_enabled,
interpolated=self.interpolated
) )
if self.is_deleted(): if self.is_deleted():
new_p.set_as_deleted() new_p.set_as_deleted()
@@ -559,7 +521,7 @@ class ProfileXYZ(Profile, SQLSubModel):
) )
self._points.append(pt) self._points.append(pt)
self.content_modified() self.modified()
def get_point_i(self, index: int) -> PointXYZ: def get_point_i(self, index: int) -> PointXYZ:
"""Get point at index. """Get point at index.
@@ -624,7 +586,7 @@ class ProfileXYZ(Profile, SQLSubModel):
) )
self.points.append(point_xyz) self.points.append(point_xyz)
self.content_modified() self.modified()
def insert(self, index: int): def insert(self, index: int):
"""Insert a new point at index. """Insert a new point at index.
@@ -643,7 +605,7 @@ class ProfileXYZ(Profile, SQLSubModel):
point = PointXYZ(x=x, y=y, z=z, profile=self, status=self._status) point = PointXYZ(x=x, y=y, z=z, profile=self, status=self._status)
self._points.insert(index, point) self._points.insert(index, point)
self.content_modified() self.modified()
return point return point
@@ -658,7 +620,6 @@ 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)
@@ -710,7 +671,6 @@ 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:
@@ -751,7 +711,6 @@ 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):
@@ -774,7 +733,6 @@ 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:
@@ -851,6 +809,7 @@ 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:
@@ -862,20 +821,10 @@ class ProfileXYZ(Profile, SQLSubModel):
return n_zones return n_zones
def froude(self, z, v):
return v / np.sqrt(
9.81 * (
self.wet_area(z) / self.wet_width(z)
)
)
@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:
@@ -908,94 +857,69 @@ 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 [], []
res = start, list(reversed(end)) return 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 = next( i_left = -1
filter( i_right = -1
lambda i: self.point(i).z <= z,
range(self.number_points)
),
-1
)
i_right = next( for i in range(self.number_points):
filter( if self.point(i).z <= z:
lambda i: self.point(i).z <= z, i_left = i
reversed(range(self.number_points)) break
),
-1 for i in reversed(range(self.number_points)):
) 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):
pi = self.point(i_left) if abs(self.point(i_left - 1).z - self.point(i_left).z) < 1e-20:
pim1 = self.point(i_left - 1) pt_left = self.point(i_left)
if abs(pim1.z - pi.z) < 1e-20:
pt_left = pi
else: else:
fact = (z - pi.z) / (pim1.z - pi.z) fact = (z - self.point(i_left).z) / \
x = pi.x + fact * (pim1.x - pi.x) (self.point(i_left - 1).z - self.point(i_left).z)
y = pi.y + fact * (pim1.y - pi.y) x = self.point(i_left).x + fact * \
(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):
pi = self.point(i_right) if abs(self.point(i_right + 1).z - self.point(i_right).z) < 1e-20:
pip1 = self.point(i_right + 1) pt_right = self.point(i_right)
if abs(pip1.z - pi.z) < 1e-20:
pt_right = pi
else: else:
fact = (z - pi.z) / (pip1.z - pi.z) fact = (z - self.point(i_right).z) / \
x = pi.x + fact * (pip1.x - pi.x) (self.point(i_right + 1).z - self.point(i_right).z)
y = pi.y + fact * (pip1.y - pi.y) x = self.point(i_right).x + fact * \
(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)
v = (pt_left, pt_right) return 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
@@ -1010,7 +934,7 @@ class ProfileXYZ(Profile, SQLSubModel):
Projection of the points of the profile on a plane. Projection of the points of the profile on a plane.
""" """
if self.nb_points < 2: if self.nb_points < 2:
return [0.0] * self.nb_points return [0.0]
else: else:
if self.station_up_to_date: if self.station_up_to_date:
return self._station return self._station
@@ -1150,10 +1074,16 @@ class ProfileXYZ(Profile, SQLSubModel):
def shift(self, x, y, z): def shift(self, x, y, z):
for p in self._points: for p in self._points:
p.shift(x, y, z) p.x = p.x + x
p.y = p.y + y
p.z = p.z + z
self.modified() self.modified()
def modified(self):
self.tab_up_to_date = False
self.station_up_to_date = False
def add_npoints(self, npoints): def add_npoints(self, npoints):
# add npoints in a profile # add npoints in a profile
for k in range(npoints): for k in range(npoints):
@@ -1171,17 +1101,15 @@ class ProfileXYZ(Profile, SQLSubModel):
self.point(i+1).z = 0.5 * self.point(i).z + 0.5 * self.point(i+2).z self.point(i+1).z = 0.5 * self.point(i).z + 0.5 * self.point(i+2).z
def copy(self): def copy(self):
p = ProfileXYZ(name=self.name, p = ProfileXYZ(id=self.id,
name=self.name,
rk=self.rk, rk=self.rk,
reach=self.reach, reach=self.reach,
status=self._status, num=self.num,
enabled=self.is_enabled, status=self._status)
interpolated=self.interpolated)
for i, k in enumerate(self.points): for i, k in enumerate(self.points):
p.insert_point(i, k.cloned_for(p)) p.insert_point(i, k.copy())
p._interpolated = self.interpolated
return p return p
def purge_aligned_points(self): def purge_aligned_points(self):
@@ -1211,25 +1139,3 @@ class ProfileXYZ(Profile, SQLSubModel):
break break
self._points = points self._points = points
self.modified() self.modified()
@property
def is_enabled(self):
return self._enabled
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, value):
self._enabled = bool(value)
self.modified()
@property
def interpolated(self):
return self._interpolated
@interpolated.setter
def interpolated(self, value):
self._interpolated = bool(value)
self.modified()
+14 -114
View File
@@ -1,5 +1,5 @@
# Reach.py -- Pamhyr # Reach.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -52,11 +52,6 @@ class Reach(SQLSubModel):
self._guidelines_is_valid = False self._guidelines_is_valid = False
self._guidelines = {} self._guidelines = {}
super(Reach, self).__init__(
id=parent.pamhyr_id,
status=status,
)
@property @property
def pamhyr_id(self): def pamhyr_id(self):
return self._parent.pamhyr_id return self._parent.pamhyr_id
@@ -65,10 +60,6 @@ class Reach(SQLSubModel):
def _pamhyr_id(self): def _pamhyr_id(self):
return self._parent.pamhyr_id return self._parent.pamhyr_id
@_pamhyr_id.setter
def _pamhyr_id(self, id):
return
@classmethod @classmethod
def _db_create(cls, execute): def _db_create(cls, execute):
return cls._create_submodel(execute) return cls._create_submodel(execute)
@@ -80,13 +71,11 @@ class Reach(SQLSubModel):
@classmethod @classmethod
def _db_load(cls, execute, data=None): def _db_load(cls, execute, data=None):
new = cls(status=data["status"], parent=data["reach"]) new = cls(status=data["status"], parent=data["reach"])
profile_data = data.copy()
profile_data["reach"] = new
new._profiles = sorted( new._profiles = sorted(
ProfileXYZ._db_load( ProfileXYZ._db_load(
execute, execute,
data=profile_data data=data.copy()
), ),
key=lambda p: p._db_ind key=lambda p: p._db_ind
) )
@@ -114,9 +103,6 @@ class Reach(SQLSubModel):
predicate=lambda obj, data: True, predicate=lambda obj, data: True,
modifier=lambda obj, data: None, modifier=lambda obj, data: None,
data={}): data={}):
if predicate(self, data):
modifier(self, data)
for p in self._profiles: for p in self._profiles:
p._data_traversal(predicate, modifier, data) p._data_traversal(predicate, modifier, data)
@@ -159,14 +145,6 @@ class Reach(SQLSubModel):
def profiles(self, profiles): def profiles(self, profiles):
self._profiles = profiles self._profiles = profiles
@property
def enabled_profiles(self):
return list(filter(lambda p: p.is_enabled, self.profiles))
@property
def number_enabled_profiles(self):
return len(self.enabled_profiles)
def get_profiles_from_rk(self, rk): def get_profiles_from_rk(self, rk):
return list( return list(
filter( filter(
@@ -217,16 +195,13 @@ class Reach(SQLSubModel):
""" """
if profile in self._profiles: if profile in self._profiles:
self.undelete([profile]) self.undelete([profile])
profile.num = index
else: else:
gi = self.get_global_profil_index(index) self._profiles.insert(index, profile)
profile.num = gi
self._profiles.insert(gi, profile)
self.modified() self.modified()
def delete(self, indexes): def delete(self, indexes):
"""Set some elements as deleted in profile list """Delete some elements in profile list
Args: Args:
indexes: The list of index to delete indexes: The list of index to delete
@@ -234,12 +209,11 @@ class Reach(SQLSubModel):
Returns: Returns:
Nothing. Nothing.
""" """
gi = [self.get_global_profil_index(i) for i in indexes]
list( list(
map( map(
lambda p: p[1].set_as_deleted(), lambda p: p[1].set_as_deleted(),
filter( filter(
lambda e: e[0] in gi, lambda e: e[0] in indexes,
enumerate(self._profiles) enumerate(self._profiles)
) )
) )
@@ -255,12 +229,11 @@ class Reach(SQLSubModel):
Returns: Returns:
Nothing. Nothing.
""" """
gi = [self.get_global_profil_index(i) for i in indexes]
self._profiles = list( self._profiles = list(
map( map(
lambda p: p[1], lambda p: p[1],
filter( filter(
lambda e: e[0] not in gi, lambda e: e[0] not in indexes,
enumerate(self._profiles) enumerate(self._profiles)
) )
) )
@@ -268,7 +241,7 @@ class Reach(SQLSubModel):
self.modified() self.modified()
def delete_profiles(self, profiles): def delete_profiles(self, profiles):
"""Set profiles list as deleted """Delete some elements in profile list
Args: Args:
profiles: The list of profile to delete profiles: The list of profile to delete
@@ -377,28 +350,6 @@ class Reach(SQLSubModel):
) )
) )
def get_z_min_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_min(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_z_max_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_max(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk(self): def get_rk(self):
"""List of profiles rk """List of profiles rk
@@ -407,17 +358,6 @@ class Reach(SQLSubModel):
""" """
return [profile.rk for profile in self.profiles] return [profile.rk for profile in self.profiles]
def get_rk_enabled_profiles(self):
return list(
map(
lambda profile: profile.rk,
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk_complete_profiles(self): def get_rk_complete_profiles(self):
return list( return list(
map( map(
@@ -529,22 +469,14 @@ class Reach(SQLSubModel):
) )
@timer @timer
def compute_guidelines(self, profiles=None, update_cache=True): def compute_guidelines(self):
"""Compute reach guidelines """Compute reach guidelines
Args:
profiles: Profiles used to compute the guidelines. If omitted,
all non-deleted profiles of the reach are used.
update_cache: Whether to update the reach guidelines cache.
Returns: Returns:
Tuple of complete and incomplete guidelines name. Tuple of complete and incomplete guidelines name.
""" """
if profiles is None:
profiles = self.profiles
# Get all point contained into a guideline # Get all point contained into a guideline
named_points = [profile.named_points() for profile in profiles] named_points = [profile.named_points() for profile in self.profiles]
points_name = list( points_name = list(
map( map(
lambda lst: list(map(lambda p: p.name, lst)), lambda lst: list(map(lambda p: p.name, lst)),
@@ -575,13 +507,11 @@ class Reach(SQLSubModel):
complete = guide_set - incomplete complete = guide_set - incomplete
if update_cache:
# Compute guideline and put data in cache # Compute guideline and put data in cache
self._compute_guidelines_cache( self._compute_guidelines_cache(guide_set, named_points,
guide_set, named_points, complete, incomplete complete, incomplete)
)
self.modified()
self.modified()
return (complete, incomplete) return (complete, incomplete)
def _map_guidelines_points(self, func, full=False): def _map_guidelines_points(self, func, full=False):
@@ -805,11 +735,8 @@ class Reach(SQLSubModel):
list_profile.append(list_point_profile) list_profile.append(list_point_profile)
list_point_profile = [] list_point_profile = []
else: else:
# MAGE-8 ST files can append hydraulic data line.append("")
# after the regular x, y, z and point-name list_point_profile.append(line[:3])
# fields. ProfileXYZ still expects exactly
# those first four fields.
list_point_profile.append(line[:4])
else: else:
pass pass
@@ -1002,30 +929,3 @@ class Reach(SQLSubModel):
nr2._profiles = list(map(lambda p: p.cloned_for(nr2), nr2_profiles)) nr2._profiles = list(map(lambda p: p.cloned_for(nr2), nr2_profiles))
return nr1, nr2 return nr1, nr2
def get_local_profil_index(self, index):
local_index = -1
for i, p in enumerate(self._profiles):
if not p.is_deleted():
local_index += 1
if i == index:
break
return local_index
def get_global_profil_index(self, index):
local_index = -1
for i, p in enumerate(self._profiles):
if not p.is_deleted():
local_index += 1
if local_index == index:
global_index = i
break
return global_index
def update_rks(self, rks):
for rk, profile in zip(rks, self.profiles):
profile.rk = rk
self.modified()
+1 -1
View File
@@ -1,5 +1,5 @@
# Vector_1d.py -- Pamhyr # Vector_1d.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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
@@ -183,6 +183,7 @@ class BasicHS(SQLSubModel):
bhs._data = BHSValue._db_load( bhs._data = BHSValue._db_load(
execute, data execute, data
) )
print(f"{bhs_pid} : {deleted}")
if deleted: if deleted:
bhs.set_as_deleted() bhs.set_as_deleted()
@@ -277,10 +278,3 @@ class BasicHS(SQLSubModel):
def convert(self, new_type): def convert(self, new_type):
return new_type(id=self.id, name=self.name, status=self._status) return new_type(id=self.id, name=self.name, status=self._status)
def cloned(self):
new = type(self)(name=self._name, status=self._status)
new._enabled = self._enabled
new._data = [value.cloned() for value in self._data]
new.modified()
return new
+1 -1
View File
@@ -1,5 +1,5 @@
# Types.py -- Pamhyr # Types.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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 -9
View File
@@ -1,5 +1,5 @@
# Value.py -- Pamhyr # Value.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -208,11 +208,3 @@ class BHSValue(SQLSubModel):
def value(self, value): def value(self, value):
self._value = self._type(value) self._value = self._type(value)
self.modified() self.modified()
def cloned(self):
return BHSValue(
name=self._name,
type=self._type,
value=self._value,
status=self._status
)
@@ -1,5 +1,5 @@
# HydraulicStructures.py -- Pamhyr # HydraulicStructures.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -47,7 +47,9 @@ class HydraulicStructure(SQLSubModel):
self._name = name self._name = name
self._input_section = None self._input_section = None
self._output_section = None
self._input_reach = None self._input_reach = None
self._output_reach = None
self._enabled = True self._enabled = True
self._data = [] self._data = []
@@ -60,12 +62,17 @@ class HydraulicStructure(SQLSubModel):
name TEXT NOT NULL, name TEXT NOT NULL,
enabled BOOLEAN NOT NULL, enabled BOOLEAN NOT NULL,
input_reach INTEGER, input_reach INTEGER,
output_reach INTEGER,
input_section INTEGER, input_section INTEGER,
output_section INTEGER,
{Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}, {Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(input_reach) REFERENCES river_reach(pamhyr_id), FOREIGN KEY(input_reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(output_reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(input_section) FOREIGN KEY(input_section)
REFERENCES geometry_profileXYZ(pamhyr_id), REFERENCES geometry_profileXYZ(pamhyr_id),
FOREIGN KEY(output_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario) PRIMARY KEY(pamhyr_id, scenario)
) )
""") """)
@@ -85,10 +92,11 @@ class HydraulicStructure(SQLSubModel):
return True return True
if rl < 11: if rl < 11:
for v in ["input", "output"]:
execute( execute(
""" f"""
ALTER TABLE hydraulic_structures ALTER TABLE hydraulic_structures
RENAME COLUMN input_kp TO input_rk RENAME COLUMN {v}_kp TO {v}_rk
""" """
) )
@@ -104,28 +112,8 @@ class HydraulicStructure(SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
) )
if major == "0" and (int(minor) < 2 or
(minor == "2" and rl < 10)):
cls._db_update_to_0_2_10(execute)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_10(cls, execute):
table = "hydraulic_structures"
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, deleted, name, enabled, input_section, " +
"input_reach, scenario) " +
"SELECT pamhyr_id, deleted, name, enabled, input_section, " +
f"input_reach, scenario FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod @classmethod
def _db_update_to_0_2_0(cls, execute, data): def _db_update_to_0_2_0(cls, execute, data):
table = "hydraulic_structures" table = "hydraulic_structures"
@@ -143,10 +131,11 @@ class HydraulicStructure(SQLSubModel):
execute( execute(
f"INSERT INTO {table}_tmp " + f"INSERT INTO {table}_tmp " +
"(pamhyr_id, name, enabled, input_section, " + "(pamhyr_id, name, enabled, input_section, output_section, " +
"input_reach, scenario) " + "input_reach, output_reach, scenario) " +
"SELECT pamhyr_id, name, enabled, " + "SELECT pamhyr_id, name, enabled, " +
"input_section, input_reach, scenario " + "input_section, output_section, " +
"input_reach, output_reach, scenario " +
f"FROM {table}" f"FROM {table}"
) )
@@ -156,36 +145,42 @@ class HydraulicStructure(SQLSubModel):
@classmethod @classmethod
def _db_update_to_0_2_0_set_reach_pid(cls, execute, table, reachs): def _db_update_to_0_2_0_set_reach_pid(cls, execute, table, reachs):
els = execute( els = execute(
f"SELECT pamhyr_id, input_reach FROM {table}" f"SELECT pamhyr_id, input_reach, output_reach FROM {table}"
) )
for row in els: for row in els:
it = iter(row) it = iter(row)
pid = next(it) pid = next(it)
in_reach_id = next(it) in_reach_id = next(it)
out_reach_id = next(it)
if in_reach_id == -1: if in_reach_id == -1:
return return
if out_reach_id == -1:
out_reach_id = in_reach_id
execute( execute(
f"UPDATE {table} " + f"UPDATE {table} " +
f"SET input_reach = {reachs[in_reach_id]} " + f"SET input_reach = {reachs[in_reach_id]}, " +
f"output_reach = {reachs[out_reach_id]} " +
f"WHERE pamhyr_id = {pid}" f"WHERE pamhyr_id = {pid}"
) )
@classmethod @classmethod
def _db_update_to_0_1_1(cls, execute, data, def _db_update_to_0_1_1(cls, execute, data,
origin_version="0.1.0"): origin_version="0.1.0"):
for v in ["input", "output"]:
execute( execute(
"ALTER TABLE hydraulic_structures " + "ALTER TABLE hydraulic_structures " +
"ADD COLUMN input_section INTEGER" f"ADD COLUMN {v}_section INTEGER"
) )
cls._db_update_to_0_1_1_assoc_section_from_rk( cls._db_update_to_0_1_1_assoc_section_from_rk(
execute, "hydraulic_structures", execute, "hydraulic_structures",
reach_column="input_reach", reach_column=f"{v}_reach",
rk_column="input_rk", rk_column=f"{v}_rk",
section_column="input_section", section_column=f"{v}_section",
origin_version=origin_version origin_version=origin_version
) )
@@ -201,7 +196,8 @@ class HydraulicStructure(SQLSubModel):
table = execute( table = execute(
"SELECT pamhyr_id, deleted, name, enabled, " + "SELECT pamhyr_id, deleted, name, enabled, " +
"input_section, input_reach, scenario " + "input_section, output_section, " +
"input_reach, output_reach, scenario " +
"FROM hydraulic_structures " + "FROM hydraulic_structures " +
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))})"
@@ -231,7 +227,12 @@ class HydraulicStructure(SQLSubModel):
input_section_id = ( input_section_id = (
-1 if input_section_id is None else input_section_id -1 if input_section_id is None else input_section_id
) )
output_section_id = next(it)
output_section_id = (
-1 if output_section_id is None else output_section_id
)
input_reach_id = next(it) input_reach_id = next(it)
output_reach_id = next(it)
owner_scenario = next(it) owner_scenario = next(it)
hs = cls( hs = cls(
@@ -242,19 +243,27 @@ class HydraulicStructure(SQLSubModel):
hs.set_as_deleted() hs.set_as_deleted()
hs.enabled = enabled hs.enabled = enabled
hs.input_reach = reduce( hs.input_reach, hs.output_reach = reduce(
lambda acc, n: n if n.pamhyr_id == input_reach_id else acc, lambda acc, n: (
n if n.pamhyr_id == input_reach_id else acc[0],
n if n.pamhyr_id == output_reach_id else acc[1]
),
data["edges"], data["edges"],
None [None, None]
) )
sections = [] sections = []
if hs.input_reach is not None: if hs.input_reach is not None:
sections += hs.input_reach.reach.profiles sections += hs.input_reach.reach.profiles
if hs.output_reach is not None:
sections += hs.output_reach.reach.profiles
hs.input_section = reduce( hs.input_section, hs.output_section = reduce(
lambda acc, s: s if s.pamhyr_id == input_section_id else acc, lambda acc, s: (
s if s.pamhyr_id == input_section_id else acc[0],
s if s.pamhyr_id == output_section_id else acc[1]
),
sections, sections,
None [None, None]
) )
loaded.add(hs_id) loaded.add(hs_id)
@@ -265,9 +274,6 @@ class HydraulicStructure(SQLSubModel):
return hs return hs
def _db_save(self, execute, data=None): def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute( execute(
"DELETE FROM hydraulic_structures " + "DELETE FROM hydraulic_structures " +
f"WHERE pamhyr_id = {self.pamhyr_id} " + f"WHERE pamhyr_id = {self.pamhyr_id} " +
@@ -278,15 +284,24 @@ class HydraulicStructure(SQLSubModel):
if self._input_reach is not None: if self._input_reach is not None:
input_reach_id = self._input_reach.pamhyr_id input_reach_id = self._input_reach.pamhyr_id
output_reach_id = -1
if self._output_reach is not None:
output_reach_id = self._output_reach.pamhyr_id
input_section = 'NULL' input_section = 'NULL'
if self.input_section is not None: if self.input_section is not None:
input_section = self.input_section.pamhyr_id input_section = self.input_section.pamhyr_id
output_section = 'NULL'
if self.output_section is not None:
output_section = self.output_section.pamhyr_id
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"hydraulic_structures(" + "hydraulic_structures(" +
" pamhyr_id, deleted, name, enabled, " + " pamhyr_id, deleted, name, enabled, " +
" input_section, input_reach, " + " input_section, output_section, " +
" input_reach, output_reach, " +
" scenario" + " scenario" +
") " + ") " +
"VALUES (" + "VALUES (" +
@@ -294,7 +309,8 @@ class HydraulicStructure(SQLSubModel):
f"{self._db_format(self.is_deleted())}, " + f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " + f"'{self._db_format(self._name)}', " +
f"{self._db_format(self.enabled)}, " + f"{self._db_format(self.enabled)}, " +
f"{input_section}, {input_reach_id}, " + f"{input_section}, {output_section}, " +
f"{input_reach_id}, {output_reach_id}, " +
f"{self._status.scenario_id}" f"{self._status.scenario_id}"
")" ")"
) )
@@ -352,6 +368,13 @@ class HydraulicStructure(SQLSubModel):
return self._input_section return self._input_section
@property
def output_rk(self):
if self._output_section is None:
return None
return self._output_section
@property @property
def input_section(self): def input_section(self):
return self._input_section return self._input_section
@@ -361,6 +384,15 @@ class HydraulicStructure(SQLSubModel):
self._input_section = input_section self._input_section = input_section
self.modified() self.modified()
@property
def output_section(self):
return self._output_section
@output_section.setter
def output_section(self, output_section):
self._output_section = output_section
self.modified()
@property @property
def enabled(self): def enabled(self):
return self._enabled return self._enabled
@@ -379,19 +411,19 @@ class HydraulicStructure(SQLSubModel):
self._input_reach = input_reach self._input_reach = input_reach
self.modified() self.modified()
@property
def output_reach(self):
return self._output_reach
@output_reach.setter
def output_reach(self, output_reach):
self._output_reach = output_reach
self.modified()
@property @property
def basic_structures(self): def basic_structures(self):
return self.lst.copy() return self.lst.copy()
def cloned_for(self, input_reach, input_section):
new = HydraulicStructure(name=self._name, status=self._status)
new._enabled = self._enabled
new._input_reach = input_reach
new._input_section = input_section
new._data = [structure.cloned() for structure in self.lst]
new.modified()
return new
def basic_structure(self, index: int): def basic_structure(self, index: int):
if len(self._data) == 0: if len(self._data) == 0:
return None return None
@@ -1,5 +1,5 @@
# HydraulicStructuresList.py -- Pamhyr # HydraulicStructuresList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -61,64 +61,6 @@ class HydraulicStructureList(PamhyrModelList):
self.modified() self.modified()
return n return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
sections1 = dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
sections2 = dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
def split_endpoint(endpoint_reach, section):
if endpoint_reach is not reach:
return endpoint_reach, section
if section not in profiles:
return endpoint_reach, section
index = profiles.index(section)
if index < split_index:
return reach1, sections1[section]
return reach2, sections2[section]
structures = [
structure
for structure in self.lst
if structure.input_reach is reach
]
for structure in structures:
input_reach, input_section = split_endpoint(
structure.input_reach,
structure.input_section
)
self._lst.append(structure.cloned_for(
input_reach,
input_section
))
if len(structures) != 0:
self.modified()
@property
def enabled_reaches_list(self):
return [
structure
for structure in self.lst
if (structure.input_reach is None or
(not structure.input_reach.is_deleted() and
structure.input_reach.is_enable()))
]
def get_for_enabled_reaches(self, index):
structures = self.enabled_reaches_list
if not 0 <= index < len(structures):
return None
return structures[index]
def __copy__(self): def __copy__(self):
new = HydraulicStructureList() new = HydraulicStructureList()
@@ -1,5 +1,5 @@
# InitialConditions.py -- Pamhyr # InitialConditions.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -19,7 +19,6 @@
import logging import logging
from copy import copy, deepcopy from copy import copy, deepcopy
from math import isclose
from tools import trace, timer from tools import trace, timer
from functools import reduce from functools import reduce
from numpy import interp from numpy import interp
@@ -226,8 +225,6 @@ class Data(SQLSubModel):
return new return new
def _db_save(self, execute, data=None): def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ind = data["ind"] ind = data["ind"]
execute( execute(
@@ -257,17 +254,6 @@ class Data(SQLSubModel):
) )
return new return new
def cloned_for(self, reach, section):
return Data(
name=self._name,
comment=self._comment,
section=section,
discharge=self._discharge,
height=self._height,
reach=reach,
status=self._status,
)
@property @property
def name(self): def name(self):
return self._name return self._name
@@ -410,7 +396,7 @@ class InitialConditions(SQLSubModel):
@reach.setter @reach.setter
def reach(self, new): def reach(self, new):
self._reach = new self._reach = reach
self.modified() self.modified()
@property @property
@@ -440,28 +426,15 @@ class InitialConditions(SQLSubModel):
def new_from_data(self, rk, discharge, elevation): def new_from_data(self, rk, discharge, elevation):
n = Data(reach=self._reach, status=self._status) n = Data(reach=self._reach, status=self._status)
# Values read from Mage INI files are strings, whereas profile RK
# values are stored as floats. Normalize the imported value before
# looking up its associated profile.
rk = float(rk)
section = reduce( section = reduce(
lambda acc, s: ( lambda acc, s: (
# Mage INI files store RK values with two decimal places. s if s.rk == rk else acc
# Allow for the rounding introduced by that format.
s if isclose(s.rk, rk, rel_tol=0.0, abs_tol=0.0051)
else acc
), ),
self._reach.reach.profiles, self._reach.reach.profiles,
None None
) )
if section is None:
raise ValueError(
f"No profile with RK {rk} exists in reach "
f"{self._reach.name}"
)
n['section'] = section n['section'] = section
n['discharge'] = discharge n['discharge'] = discharge
n['elevation'] = elevation n['elevation'] = elevation
@@ -530,20 +503,6 @@ class InitialConditions(SQLSubModel):
for data in self._data: for data in self._data:
data.set_as_deleted() data.set_as_deleted()
def _splited_for(self, reach, sections):
new = InitialConditions(reach=reach, status=self._status)
new._data = [
data.cloned_for(reach, sections[data["section"]])
for data in self.data
if data["section"] in sections
]
if len(new._data) != 0:
new.modified()
return new
def generate_growing_constant_depth(self, height: float, def generate_growing_constant_depth(self, height: float,
compute_discharge: bool): compute_discharge: bool):
profiles = self._reach.reach.profiles.copy() profiles = self._reach.reach.profiles.copy()
@@ -578,10 +537,7 @@ class InitialConditions(SQLSubModel):
strickler = 25.0 strickler = 25.0
if not compute_discharge: if not compute_discharge:
if profile.rk in data_discharge:
discharge = data_discharge[profile.rk] discharge = data_discharge[profile.rk]
else:
discharge = 0.0
else: else:
discharge = ( discharge = (
# ((width * 0.8) # ((width * 0.8)
@@ -627,6 +583,8 @@ class InitialConditions(SQLSubModel):
for data in self._data: for data in self._data:
data_height[data["rk"].rk] = data["height"] data_height[data["rk"].rk] = data["height"]
print(data_height)
incline = self._reach.reach.get_incline_median_mean() incline = self._reach.reach.get_incline_median_mean()
logger.debug(f"incline = {incline}") logger.debug(f"incline = {incline}")
@@ -647,10 +605,7 @@ class InitialConditions(SQLSubModel):
strickler = 25.0 strickler = 25.0
if not compute_height: if not compute_height:
if profile.rk in data_height:
height = data_height[profile.rk] height = data_height[profile.rk]
else:
height = 0.0
else: else:
if abs(incline) <= 0: if abs(incline) <= 0:
height = 0.0 height = 0.0
@@ -701,10 +656,7 @@ class InitialConditions(SQLSubModel):
for profile in profiles: for profile in profiles:
if not compute_discharge: if not compute_discharge:
if profile.rk in data_discharge:
d = data_discharge[profile.rk] d = data_discharge[profile.rk]
else:
d = 0.0
else: else:
d = discharge d = discharge
elevation = interp(profile.rk, elevation = interp(profile.rk,
@@ -1,5 +1,5 @@
# InitialConditionsDict.py -- Pamhyr # InitialConditionsDict.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -88,28 +88,3 @@ class InitialConditionsDict(PamhyrModelDict):
new = InitialConditions(reach=reach, status=self._status) new = InitialConditions(reach=reach, status=self._status)
self.set(reach, new) self.set(reach, new)
return new return new
def split_reach(self, reach, profile, reach1, reach2):
if reach not in self._dict:
return
profiles = reach.reach.profiles
split_index = profiles.index(profile)
sections1 = dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
sections2 = dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
self.set(
reach1,
self._dict[reach]._splited_for(reach1, sections1)
)
self.set(
reach2,
self._dict[reach]._splited_for(reach2, sections2)
)
@@ -1,5 +1,5 @@
# InitialConditionsAdisTS.py -- Pamhyr # InitialConditionsAdisTS.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -79,12 +79,16 @@ class InitialConditionsAdisTS(SQLSubModel):
@classmethod @classmethod
def _db_update(cls, execute, version, data=None): def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2: if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "initial_conditions_adists"): if not created:
cls._db_update_to_0_2_0(execute, data) cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True return True
@@ -263,7 +267,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@name.setter @name.setter
def name(self, name): def name(self, name):
self._name = name self._name = name
self.modified() self._status.modified()
@property @property
def pollutant(self): def pollutant(self):
@@ -272,7 +276,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@pollutant.setter @pollutant.setter
def pollutant(self, pollutant): def pollutant(self, pollutant):
self._pollutant = pollutant self._pollutant = pollutant
self.modified() self._status.modified()
@property @property
def concentration(self): def concentration(self):
@@ -281,7 +285,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@concentration.setter @concentration.setter
def concentration(self, concentration): def concentration(self, concentration):
self._concentration = concentration self._concentration = concentration
self.modified() self._status.modified()
@property @property
def eg(self): def eg(self):
@@ -290,7 +294,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@eg.setter @eg.setter
def eg(self, eg): def eg(self, eg):
self._eg = eg self._eg = eg
self.modified() self._status.modified()
@property @property
def em(self): def em(self):
@@ -299,7 +303,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@em.setter @em.setter
def em(self, em): def em(self, em):
self._em = em self._em = em
self.modified() self._status.modified()
@property @property
def ed(self): def ed(self):
@@ -308,7 +312,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@ed.setter @ed.setter
def ed(self, ed): def ed(self, ed):
self._ed = ed self._ed = ed
self.modified() self._status.modified()
@property @property
def enabled(self): def enabled(self):
@@ -317,102 +321,28 @@ class InitialConditionsAdisTS(SQLSubModel):
@enabled.setter @enabled.setter
def enabled(self, enabled): def enabled(self, enabled):
self._enabled = enabled self._enabled = enabled
self.modified() self._status.modified()
def new(self, index): def new(self, index):
n = ICAdisTSSpec(status=self._status) n = ICAdisTSSpec(status=self._status)
self._data.insert(index, n) self._data.insert(index, n)
self.modified() self._status.modified()
return n return n
def delete(self, data): def delete(self, data):
list( self._data = list(
map( filter(
lambda x: x.set_as_deleted(), lambda x: x not in data,
data self._data
) )
) )
self.modified() self._status.modified()
def delete_i(self, indexes): def delete_i(self, indexes):
list( for ind in indexes:
map( del self._data[ind]
lambda e: e[1].set_as_deleted(), self._status.modified()
filter(
lambda e: e[0] in indexes,
enumerate(self._data)
)
)
)
self.modified()
def insert(self, index, data): def insert(self, index, data):
if data in self._data:
self.undelete([data])
else:
self._data.insert(index, data) self._data.insert(index, data)
self._status.modified()
self.modified()
def undelete(self, lst):
for x in lst:
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
lower_reach_rk = min(reach_rks)
upper_reach_rk = max(reach_rks)
parts = [
(reach1.id, lower_reach_rk, split_rk),
(reach2.id, split_rk, upper_reach_rk),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
def get_specs_for_enabled_reaches(self, reaches):
enabled_reaches = {
reach.id
for reach in reaches
if not reach.is_deleted() and reach.is_enable()
}
return [
specification
for specification in self._data
if (not specification.is_deleted() and
(specification.reach in (None, -1) or
specification.reach in enabled_reaches))
]
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSList.py -- Pamhyr # InitialConditionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -62,12 +62,6 @@ class InitialConditionsAdisTSList(PamhyrModelList):
self._status.modified() self._status.modified()
return n return n
def split_reach(self, reach, profile, reach1, reach2):
for initial_condition in self.lst:
initial_condition.split_reach(
reach, profile, reach1, reach2
)
@property @property
def Initial_Conditions_List(self): def Initial_Conditions_List(self):
return self.lst return self.lst
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSSpec.py -- Pamhyr # InitialConditionsAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -31,12 +31,10 @@ class ICAdisTSSpec(SQLSubModel):
_sub_classes = [] _sub_classes = []
def __init__(self, id: int = -1, name: str = "", def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1): status=None):
super(ICAdisTSSpec, self).__init__( super(ICAdisTSSpec, self).__init__()
id=id,
status=status, self._status = status
owner_scenario=owner_scenario
)
self._name_section = name self._name_section = name
self._reach = None self._reach = None
@@ -79,12 +77,16 @@ class ICAdisTSSpec(SQLSubModel):
@classmethod @classmethod
def _db_update(cls, execute, version, data=None): def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False
if major == 0 and minor < 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2: if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "initial_conditions_adists_spec"): if not created:
cls._db_update_to_0_2_0(execute, data) cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True return True
@@ -192,7 +194,7 @@ class ICAdisTSSpec(SQLSubModel):
new_spec.rate = rate new_spec.rate = rate
new_spec.enabled = enabled new_spec.enabled = enabled
loaded.add(id) loaded.add(pid)
new.append(new_spec) new.append(new_spec)
data["scenario"] = scenario.parent data["scenario"] = scenario.parent
@@ -209,7 +211,7 @@ class ICAdisTSSpec(SQLSubModel):
sql = ( sql = (
"INSERT INTO " + "INSERT INTO " +
"initial_conditions_adists_spec(pamhyr_id, deleted, " + "initial_conditions_adists_spec(id, deleted, " +
"ic_default, name, reach, " + "ic_default, name, reach, " +
"start_rk, end_rk, concentration, eg, em, ed, rate, " + "start_rk, end_rk, concentration, eg, em, ed, rate, " +
"enabled, scenario) " + "enabled, scenario) " +
@@ -236,8 +238,7 @@ class ICAdisTSSpec(SQLSubModel):
@name.setter @name.setter
def name(self, name): def name(self, name):
self._name_section = name self._name_section = name
self.modified() self._status.modified()
self.modified()
@property @property
def reach(self): def reach(self):
@@ -246,8 +247,7 @@ class ICAdisTSSpec(SQLSubModel):
@reach.setter @reach.setter
def reach(self, reach): def reach(self, reach):
self._reach = reach self._reach = reach
self.modified() self._status.modified()
self.modified()
@property @property
def start_rk(self): def start_rk(self):
@@ -256,8 +256,7 @@ class ICAdisTSSpec(SQLSubModel):
@start_rk.setter @start_rk.setter
def start_rk(self, start_rk): def start_rk(self, start_rk):
self._start_rk = start_rk self._start_rk = start_rk
self.modified() self._status.modified()
self.modified()
@property @property
def end_rk(self): def end_rk(self):
@@ -266,8 +265,7 @@ class ICAdisTSSpec(SQLSubModel):
@end_rk.setter @end_rk.setter
def end_rk(self, end_rk): def end_rk(self, end_rk):
self._end_rk = end_rk self._end_rk = end_rk
self.modified() self._status.modified()
self.modified()
@property @property
def concentration(self): def concentration(self):
@@ -276,8 +274,7 @@ class ICAdisTSSpec(SQLSubModel):
@concentration.setter @concentration.setter
def concentration(self, concentration): def concentration(self, concentration):
self._concentration = concentration self._concentration = concentration
self.modified() self._status.modified()
self.modified()
@property @property
def eg(self): def eg(self):
@@ -286,8 +283,7 @@ class ICAdisTSSpec(SQLSubModel):
@eg.setter @eg.setter
def eg(self, eg): def eg(self, eg):
self._eg = eg self._eg = eg
self.modified() self._status.modified()
self.modified()
@property @property
def em(self): def em(self):
@@ -296,8 +292,7 @@ class ICAdisTSSpec(SQLSubModel):
@em.setter @em.setter
def em(self, em): def em(self, em):
self._em = em self._em = em
self.modified() self._status.modified()
self.modified()
@property @property
def ed(self): def ed(self):
@@ -306,8 +301,7 @@ class ICAdisTSSpec(SQLSubModel):
@ed.setter @ed.setter
def ed(self, ed): def ed(self, ed):
self._ed = ed self._ed = ed
self.modified() self._status.modified()
self.modified()
@property @property
def rate(self): def rate(self):
@@ -316,8 +310,7 @@ class ICAdisTSSpec(SQLSubModel):
@rate.setter @rate.setter
def rate(self, rate): def rate(self, rate):
self._rate = rate self._rate = rate
self.modified() self._status.modified()
self.modified()
@property @property
def enabled(self): def enabled(self):
@@ -326,18 +319,4 @@ class ICAdisTSSpec(SQLSubModel):
@enabled.setter @enabled.setter
def enabled(self, enabled): def enabled(self, enabled):
self._enabled = enabled self._enabled = enabled
self.modified() self._status.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = ICAdisTSSpec(name=self._name_section, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._concentration = self._concentration
new._eg = self._eg
new._em = self._em
new._ed = self._ed
new._rate = self._rate
new._enabled = self._enabled
new.modified()
return new
@@ -1,272 +0,0 @@
# InitialConditionsTemperature.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from functools import reduce
from tools import trace, timer, old_pamhyr_date_to_timestamp
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
from Model.InitialConditionsTemperature.InitialConditionsTemperatureSpec \
import ICTemperatureSpec
logger = logging.getLogger()
class InitialConditionsTemperature(SQLSubModel):
_sub_classes = [
ICTemperatureSpec,
]
def __init__(self, id: int = -1, name: str = "default",
status=None, owner_scenario=-1):
super(InitialConditionsTemperature, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = name
self._temperature = None
self._data = []
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
temperature REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7):
table_name = "initial_conditions_temperature"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, " +
"name, temperature, scenario " +
"FROM initial_conditions_temperature " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
temperature = next(it)
owner_scenario = next(it)
ic = cls(
id=pid,
name=name,
status=status,
owner_scenario=owner_scenario
)
if deleted:
ic.set_as_deleted()
ic.temperature = temperature
data['ic_default_id'] = pid
ic._data = ICTemperatureSpec._db_load(execute, data)
loaded.add(pid)
new.append(ic)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM initial_conditions_temperature " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
temperature = self._temperature if \
self._temperature is not None else 0.0
sql = (
"INSERT INTO " +
"initial_conditions_temperature(" +
"pamhyr_id, deleted, name, temperature, " +
"scenario" +
") " +
"VALUES (" +
f"{self.id}, {self.is_deleted()}, " +
f"'{self._db_format(self._name)}', " +
f"{temperature}, {self._status.scenario_id}" +
")"
)
execute(sql)
data['ic_default_id'] = self.id
execute(
"DELETE FROM initial_conditions_temperature_spec " +
f"WHERE ic_default = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
for ic_spec in self._data:
ic_spec._db_save(execute, data)
return True
def __len__(self):
return len(self.data)
@property
def data(self):
return list(filter(lambda item: not item.is_deleted(), self._data))
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temperature):
self._temperature = temperature
self.modified()
def new(self, index):
n = ICTemperatureSpec(status=self._status)
self._data.insert(index, n)
self.modified()
return n
def delete(self, data):
list(
map(
lambda x: x.set_as_deleted(),
data
)
)
self.modified()
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self._data)
)
)
)
self.modified()
def insert(self, index, data):
if data in self._data:
self.undelete([data])
else:
self._data.insert(index, data)
self.modified()
def undelete(self, lst):
for x in lst:
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
parts = []
for new_reach in (reach1, reach2):
rks = new_reach.reach.get_rk()
if rks:
parts.append((new_reach.id, min(rks), max(rks)))
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
clones = []
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
clones.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if clones:
self._data.extend(clones)
self.modified()
@@ -1,73 +0,0 @@
# InitialConditionsTemperatureList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.InitialConditionsTemperature.InitialConditionsTemperature \
import InitialConditionsTemperature
class InitialConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
InitialConditionsTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = InitialConditionsTemperature._db_load(
execute, data
)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM initial_conditions_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for ic in self._lst:
ic._db_save(execute, data=data)
return True
def new(self, index):
n = InitialConditionsTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for initial_condition in self.lst:
initial_condition.split_reach(
reach, profile, reach1, reach2
)
@property
def Initial_Conditions_List(self):
return self.lst
@@ -1,215 +0,0 @@
# InitialConditionsAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class ICTemperatureSpec(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(ICTemperatureSpec, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name_section = name
self._reach = None
self._start_rk = None
self._end_rk = None
self._temperature = None
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_temperature_spec{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ic_default INTEGER NOT NULL,
name TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
temperature REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(ic_default)
REFERENCES initial_conditions_temperature(pamhyr_id),
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7):
table_name = "initial_conditions_temperature_spec"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
reachs = data["edges"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, name, reach, start_rk, end_rk, " +
"temperature, scenario " +
"FROM initial_conditions_temperature_spec " +
f"WHERE ic_default = {data['ic_default_id']} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
if table is not None:
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
reach = next(it)
start_rk = next(it)
end_rk = next(it)
temperature = next(it)
owner_scenario = next(it)
new_spec = cls(
id=id, name=name,
status=status,
owner_scenario=owner_scenario
)
if deleted:
new_spec.set_as_deleted()
new_spec.reach = reach
new_spec.start_rk = start_rk
new_spec.end_rk = end_rk
new_spec.temperature = temperature
loaded.add(id)
new.append(new_spec)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ic_default = data['ic_default_id']
sql = (
"INSERT INTO " +
"initial_conditions_temperature_spec(pamhyr_id, deleted, " +
"ic_default, name, reach, " +
"start_rk, end_rk, temperature, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{ic_default}, " +
f"'{self._db_format(self._name_section)}', " +
f"{self._reach}, " +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._temperature}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
@property
def name(self):
return self._name_section
@name.setter
def name(self, name):
self._name_section = name
self.modified()
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach):
self._reach = reach
self.modified()
@property
def start_rk(self):
return self._start_rk
@start_rk.setter
def start_rk(self, start_rk):
self._start_rk = start_rk
self.modified()
@property
def end_rk(self):
return self._end_rk
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self.modified()
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temperature):
self._temperature = temperature
self._status.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = ICTemperatureSpec(
name=self._name_section,
status=self._status
)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._temperature = self._temperature
new.modified()
return new
@@ -1,5 +1,5 @@
# LateralContribution.py -- Pamhyr # LateralContribution.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -147,9 +147,9 @@ class Data(SQLSubModel):
it = iter(v) it = iter(v)
pid = next(it) pid = next(it)
deleted = next(it) delete = next(it)
data0 = lc._types[0](next(it)) data0 = bc._types[0](next(it))
data1 = lc._types[1](next(it)) data1 = bc._types[1](next(it))
owner_scenario = next(it) owner_scenario = next(it)
nd = cls( nd = cls(
@@ -196,13 +196,6 @@ class Data(SQLSubModel):
def __setitem__(self, key, value): def __setitem__(self, key, value):
self._data[key] = self._types[key](value) self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class LateralContribution(SQLSubModel): class LateralContribution(SQLSubModel):
_sub_classes = [Data] _sub_classes = [Data]
@@ -518,15 +511,6 @@ class LateralContribution(SQLSubModel):
def has_reach(self): def has_reach(self):
return self._reach is not None return self._reach is not None
def cloned_for(self, reach, begin_section, end_section):
new = type(self)(name=self._name, status=self._status)
new._reach = reach
new._begin_section = begin_section
new._end_section = end_section
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property @property
def begin_rk(self): def begin_rk(self):
if self._begin_section is None: if self._begin_section is None:
@@ -1,5 +1,5 @@
# LateralContributionList.py -- Pamhyr # LateralContributionList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -71,89 +71,6 @@ class LateralContributionList(PamhyrModelListWithTab):
self._status.modified() self._status.modified()
return n return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
parts = [
(
reach1,
0,
split_index,
dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
),
(
reach2,
split_index,
len(profiles) - 1,
dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
),
]
for tab in self._tabs:
contributions = [
contribution
for contribution in self.get_tab(tab)
if contribution.reach is reach
]
for contribution in contributions:
if (contribution.begin_section not in profiles or
contribution.end_section not in profiles):
continue
begin = profiles.index(contribution.begin_section)
end = profiles.index(contribution.end_section)
lower, upper = sorted((begin, end))
reverse = begin > end
clones = []
for new_reach, part_lower, part_upper, sections in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
begin_index = (
clipped_upper if reverse else clipped_lower
)
end_index = (
clipped_lower if reverse else clipped_upper
)
clones.append(contribution.cloned_for(
new_reach,
sections[profiles[begin_index]],
sections[profiles[end_index]]
))
if len(clones) == 0:
continue
self._tabs[tab].extend(clones)
self._status.modified()
def get_tab_for_enabled_reaches(self, tab):
return [
contribution
for contribution in self.get_tab(tab)
if (contribution.reach is None or
(not contribution.reach.is_deleted() and
contribution.reach.is_enable()))
]
def get_for_enabled_reaches(self, tab, index):
contributions = self.get_tab_for_enabled_reaches(tab)
if not 0 <= index < len(contributions):
return None
return contributions[index]
def __copy__(self): def __copy__(self):
new = LateralContributionList() new = LateralContributionList()
@@ -1,5 +1,5 @@
# LateralContributionTypes.py -- Pamhyr # LateralContributionTypes.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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
@@ -69,12 +69,14 @@ class Data(SQLSubModel):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False created = False
if major == "0" and int(minor) < 2: if major == "0" and int(minor) <= 1:
if cls.is_table_exists( if int(release) < 7:
execute, "lateral_contribution_data_adists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute) cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
return True return True
@@ -130,13 +132,11 @@ class Data(SQLSubModel):
if scenario is None: if scenario is None:
return new return new
lca = data["lca"]
table = execute( table = execute(
"SELECT pamhyr_id, deleted, data0, data1, scenario " + "SELECT pamhyr_id, deleted, data0, data1 " +
"FROM lateral_contribution_data_adists " + "FROM lateral_contribution_data_adists " +
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))}) "
f"AND lca = '{lca.id}'" f"AND lca = '{lca.id}'"
) )
@@ -150,16 +150,16 @@ class Data(SQLSubModel):
data1 = next(it) data1 = next(it)
owner_scenario = next(it) owner_scenario = next(it)
lc = cls( data = cls(
data0, data1, data0, data1,
id=pid, status=status, id=pid, status=status,
owner_scenario=owner_scenario owner_scenario=owner_scenario
) )
if deleted: if deleted:
lc.set_as_deleted() data.set_as_deleted()
loaded.add(pid) loaded.add(pid)
new.append(lc) new.append(data)
data["scenario"] = scenario.parent data["scenario"] = scenario.parent
new += cls._db_load(execute, data) new += cls._db_load(execute, data)
@@ -175,7 +175,7 @@ class Data(SQLSubModel):
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"lateral_contribution_data_adists(pamhyr_id, deleted," + "lateral_contribution_adists(pamhyr_id, deleted," +
"data0, data1, lca, scenario) " + "data0, data1, lca, scenario) " +
"VALUES (" + "VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " + f"{self.id}, {self._db_format(self.is_deleted())}, " +
@@ -192,13 +192,6 @@ class Data(SQLSubModel):
def __setitem__(self, key, value): def __setitem__(self, key, value):
self._data[key] = self._types[key](value) self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class LateralContributionAdisTS(SQLSubModel): class LateralContributionAdisTS(SQLSubModel):
_sub_classes = [Data] _sub_classes = [Data]
@@ -248,13 +241,15 @@ class LateralContributionAdisTS(SQLSubModel):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False created = False
if major == "0" and int(minor) < 2: if major == "0" and int(minor) <= 1:
if cls.is_table_exists(execute, "lateral_contribution_adists"): if int(release) < 7:
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute) cls._db_create(execute)
created = True created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created: if not created:
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@@ -377,18 +372,16 @@ class LateralContributionAdisTS(SQLSubModel):
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"lateral_contribution_adists(pamhyr_id, deleted, " + "lateral_contribution_adists(id, deleted," +
"pollutant, reach, begin_rk, end_rk, scenario) " + "pollutant, reach, begin_rk, end_rk, scenario) " +
"VALUES (" + "VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " + f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._pollutant}, {self.reach}, " + f"{self._pollutant}, {self.reach}, " +
f"{self._begin_rk}, {self._end_rk}, " + f"{self._begin_rk}, {self._end_rk}" +
f"{self._status.scenario_id}" + f"{self._status.scenario_id}" +
")" ")"
) )
data["lca"] = self
ind = 0 ind = 0
for d in self._data: for d in self._data:
data["ind"] = ind data["ind"] = ind
@@ -413,7 +406,7 @@ class LateralContributionAdisTS(SQLSubModel):
return len( return len(
list( list(
filter( filter(
lambda el: el is not None and not el.is_deleted(), lambda el: not el.is_deleted(),
self._data self._data
) )
) )
@@ -459,12 +452,7 @@ class LateralContributionAdisTS(SQLSubModel):
@property @property
def data(self): def data(self):
return list( return self._data.copy()
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property @property
def begin_rk(self): def begin_rk(self):
@@ -483,69 +471,3 @@ class LateralContributionAdisTS(SQLSubModel):
def end_rk(self, end_rk): def end_rk(self, end_rk):
self._end_rk = end_rk self._end_rk = end_rk
self.modified() self.modified()
def cloned_for(self, reach, begin_rk, end_rk):
new = LateralContributionAdisTS(
pollutant=self._pollutant,
status=self._status
)
new._reach = reach
new._begin_rk = begin_rk
new._end_rk = end_rk
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def _default_0(self):
return self._types[0](0)
@property
def _default_1(self):
return self._types[1](0.0)
def add(self, index: int):
value = Data(self._default_0, self._default_1, status=self._status)
self._data.insert(index, value)
self.modified()
return value
def insert(self, index: int, data: Data):
self._data.insert(index, data)
self.modified()
def delete_i(self, indexes):
self._data = list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] not in indexes,
enumerate(self.data)
)
)
)
self.modified()
def index(self, bc):
self._data.index(bc)
def get_i(self, index: int):
return self._data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index: int, column: int, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)
@@ -1,5 +1,5 @@
# LateralContributionsAdisTSList.py -- Pamhyr # LateralContributionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -68,46 +68,6 @@ class LateralContributionsAdisTSList(PamhyrModelList):
self._status.modified() self._status.modified()
return n return n
def split_reach(self, reach, profile, reach1, reach2):
parts = []
for new_reach in (reach1, reach2):
rks = new_reach.reach.get_rk()
if len(rks) == 0:
continue
parts.append((new_reach.id, min(rks), max(rks)))
contributions = [
contribution
for contribution in self.lst
if contribution.reach == reach.id
]
clones = []
for contribution in contributions:
begin = contribution.begin_rk
end = contribution.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
begin_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
clones.append(contribution.cloned_for(
new_reach, begin_rk, end_rk
))
if len(clones) != 0:
self._lst.extend(clones)
self._status.modified()
@property @property
def Lat_Cont_List(self): def Lat_Cont_List(self):
return self.lst return self.lst
+1 -1
View File
@@ -1,5 +1,5 @@
# Edge.py -- Pamhyr # Edge.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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
+212
View File
@@ -0,0 +1,212 @@
# OutputRKAdists.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class OutputRKAdists(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, reach=None,
rk=None, title: str = "", status=None,
owner_scenario=-1):
super(OutputRKAdists, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._reach = reach
self._rk = rk
self._title = str(title)
self._enabled = True
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach_id):
self._reach = reach_id
self._status.modified()
@property
def rk(self):
return self._rk
@rk.setter
def rk(self, profile_id):
self._rk = profile_id
self._status.modified()
@property
def title(self):
return self._title
@title.setter
def title(self, title):
self._title = title
self._status.modified()
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE output_rk_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
reach INTEGER NOT NULL,
rk REAL NOT NULL,
title TEXT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "OutputRKAdists"
table_new = "output_rk_adists"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table_new}_tmp " +
"(pamhyr_id, reach, rk, title, scenario) " +
"SELECT pamhyr_id, reach, rk, title, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table_new, reachs)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
# reach = data["reach"]
# profile = data["profile"]
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, reach, rk, title, scenario " +
f"FROM output_rk_adists"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
id_reach = next(it)
id_rk = next(it)
title = next(it)
owner_scenario = next(it)
new_output = cls(
id=pid, reach=id_reach,
rk=id_rk, title=title,
status=status,
owner_scenario=owner_scenario,
)
if deleted:
new_output.set_as_deleted()
loaded.add(pid)
new.append(new_output)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM output_rk_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"INSERT INTO " +
"output_rk_adists(pamhyr_id, deleted, " +
"reach, rk, title, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}" +
f"{self._reach}, {self._rk}, " +
f"'{self._db_format(self._title)}', " +
f"{self._status.scenario_id}" +
")"
)
return True
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
@@ -1,4 +1,4 @@
# GeoTIFFList.py -- Pamhyr # OutputRKListAdists.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE # Copyright (C) 2024-2025 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
@@ -20,17 +20,17 @@ from tools import trace, timer
from Model.Except import NotImplementedMethodeError from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrListExt import PamhyrModelList from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.GeoTIFF.GeoTIFF import GeoTIFF from Model.OutputRKAdists.OutputRKAdists import OutputRKAdists
class GeoTIFFList(PamhyrModelList): class OutputRKAdistsList(PamhyrModelList):
_sub_classes = [GeoTIFF] _sub_classes = [OutputRKAdists]
@classmethod @classmethod
def _db_load(cls, execute, data=None): def _db_load(cls, execute, data=None):
new = cls(status=data["status"]) new = cls(status=data["status"])
new._lst = GeoTIFF._db_load(execute, data) new._lst = OutputRKAdists._db_load(execute, data)
return new return new
@@ -39,21 +39,21 @@ class GeoTIFFList(PamhyrModelList):
# Delete previous data # Delete previous data
execute( execute(
"DELETE FROM geotiff " + "DELETE FROM output_rk_adists " +
f"WHERE scenario = {self._status.scenario_id}" f"WHERE scenario = {self._status.scenario_id}"
) )
for gt in self._lst: for sl in self._lst:
ok &= gt._db_save(execute, data) ok &= sl._db_save(execute, data)
return ok return ok
@property @property
def files(self): def OutputRK_List(self):
return self.lst return self.lst
def new(self, index): def new(self, lst, index):
n = GeoTIFF(status=self._status) n = OutputRKAdists(status=self._status)
self.insert(index, n) self._lst.insert(index, n)
self._status.modified() self._status.modified()
return n return n
+20 -81
View File
@@ -1,5 +1,5 @@
# Pollutants.py -- Pamhyr # Pollutants.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -28,9 +28,6 @@ from tools import (
from Model.Tools.PamhyrDB import SQLSubModel from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario from Model.Scenario import Scenario
from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS import (
BoundaryConditionAdisTS,
)
logger = logging.getLogger() logger = logging.getLogger()
@@ -104,12 +101,6 @@ class PollutantCharacteristics(SQLSubModel):
self._ac = value self._ac = value
elif key == 8: elif key == 8:
self._bc = value self._bc = value
self.propagate_modified()
def propagate_modified(self):
self.modified()
if hasattr(self, "_pollutant") and self._pollutant:
self._pollutant.modified()
@classmethod @classmethod
def _db_create(cls, execute, ext=""): def _db_create(cls, execute, ext=""):
@@ -138,12 +129,16 @@ class PollutantCharacteristics(SQLSubModel):
@classmethod @classmethod
def _db_update(cls, execute, version, data=None): def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2: if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "Pollutants_characteristics"): if not created:
cls._db_update_to_0_2_0(execute, data) cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@@ -258,12 +253,6 @@ class PollutantCharacteristics(SQLSubModel):
if not self.must_be_saved(): if not self.must_be_saved():
return True return True
execute(
"DELETE FROM pollutants_characteristics " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"pollutants_characteristics(pamhyr_id, deleted, " + "pollutants_characteristics(pamhyr_id, deleted, " +
@@ -286,10 +275,7 @@ class PollutantCharacteristics(SQLSubModel):
class Pollutants(SQLSubModel): class Pollutants(SQLSubModel):
_sub_classes = [ _sub_classes = [PollutantCharacteristics]
PollutantCharacteristics,
# BoundaryConditionAdisTS
]
def __init__(self, id: int = -1, name: str = "", def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1): status=None, owner_scenario=-1):
@@ -307,11 +293,6 @@ class Pollutants(SQLSubModel):
self._enabled = True self._enabled = True
self._data = [] self._data = []
self._boundary_conditions_adists = []
@property
def boundary_conditions_adists(self):
return self._boundary_conditions_adists
@property @property
def name(self): def name(self):
@@ -320,37 +301,18 @@ class Pollutants(SQLSubModel):
@name.setter @name.setter
def name(self, name): def name(self, name):
self._name = name self._name = name
self.modified() self._status.modified()
@property @property
def data(self): def data(self):
return self._data return self._data
@data.setter
def data(self, data):
self._data = [
PollutantCharacteristics(
type=int(data[0]),
diametre=float(data[1]),
rho=float(data[2]),
porosity=float(data[3]),
cdc_riv=float(data[4]),
cdc_cas=float(data[5]),
apd=float(data[6]),
ac=float(data[7]),
bc=float(data[8]),
pollutant=self,
status=self._status
)
]
@classmethod @classmethod
def _db_create(cls, execute, ext=""): def _db_create(cls, execute, ext=""):
execute(f""" execute(f"""
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()}
@@ -367,20 +329,14 @@ class Pollutants(SQLSubModel):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
created = False created = False
if major == "0" and int(minor) < 2: if major == "0" and int(minor) <= 1:
if cls.is_table_exists(execute, "Pollutants"): if int(release) < 7:
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute) cls._db_create(execute)
created = True created = True
if major == "0" and minor == "2": if major == "0" and int(minor) < 2:
if int(release) < 6: if not created:
execute(f"ALTER TABLE pollutants " + cls._db_update_to_0_2_0(execute, data)
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)
@@ -419,8 +375,7 @@ class Pollutants(SQLSubModel):
return new return new
table = execute( table = execute(
"SELECT pamhyr_id, deleted, enabled, name, scenario " + "SELECT pamhyr_id, deleted, name FROM pollutants " +
"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))})"
) )
@@ -431,30 +386,18 @@ 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)
new_pollutant = cls( new_pollutant = cls(
id=pid, name=name, id=pid, name=name,
status=status, status=status
owner_scenario=owner_scenario
) )
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(
execute, data=data execute, data.copy()
)
new_pollutant._boundary_conditions_adists = (
BoundaryConditionAdisTS._db_load(
execute,
data=data
)
) )
loaded.add(pid) loaded.add(pid)
@@ -478,17 +421,16 @@ class Pollutants(SQLSubModel):
f"AND scenario = {self._status.scenario_id}" f"AND scenario = {self._status.scenario_id}"
) )
execute( execute(
"DELETE FROM pollutants_characteristics " + f"DELETE FROM pollutants_characteristics " +
f"WHERE pollutant = {self.id} " + f"WHERE pollutant = {self.id} " +
f"AND scenario = {self._status.scenario_id}" f"AND scenario = {self._status.scenario_id}"
) )
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"pollutants(pamhyr_id, deleted, enabled, name, scenario) " + "pollutants(pamhyr_id, deleted, 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}" +
")" ")"
@@ -497,9 +439,6 @@ class Pollutants(SQLSubModel):
for d in self._data: for d in self._data:
ok &= d._db_save(execute, data) ok &= d._db_save(execute, data)
for bc in self._boundary_conditions_adists:
ok &= bc._db_save(execute, data)
return ok return ok
def _data_traversal(self, def _data_traversal(self,
+1 -1
View File
@@ -19,7 +19,7 @@
from tools import trace, timer from tools import trace, timer
from Model.Except import NotImplementedMethodeError from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrListExt import PamhyrModelList from Model.Tools.PamhyrList import PamhyrModelList
from Model.Pollutants.Pollutants import Pollutants from Model.Pollutants.Pollutants import Pollutants
+5 -3
View File
@@ -1,5 +1,5 @@
# Reservoir.py -- Pamhyr # Reservoir.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -263,6 +263,8 @@ class Reservoir(SQLSubModel):
cls._db_update_to_0_2_0_set_node_pid(execute, table, nodes) cls._db_update_to_0_2_0_set_node_pid(execute, table, nodes)
Scenario.update_db_add_scenario(execute, table) Scenario.update_db_add_scenario(execute, table)
print(execute(f"SELECT * FROM {table}"))
cls._db_create(execute, ext="_tmp") cls._db_create(execute, ext="_tmp")
execute( execute(
f"INSERT INTO {table}_tmp " + f"INSERT INTO {table}_tmp " +
@@ -308,11 +310,11 @@ class Reservoir(SQLSubModel):
new_reservoir.set_as_deleted() new_reservoir.set_as_deleted()
new_reservoir._node = None new_reservoir._node = None
if node_id != -1 and node_id is not None: if node_id != -1:
new_reservoir._node = next( new_reservoir._node = next(
filter( filter(
lambda n: n.id == node_id, data["nodes"] lambda n: n.id == node_id, data["nodes"]
), None )
) )
data["reservoir"] = new_reservoir data["reservoir"] = new_reservoir
+1 -1
View File
@@ -1,5 +1,5 @@
# ReservoirList.py -- Pamhyr # ReservoirList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
+10 -429
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr # Results.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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,8 +22,6 @@ 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
@@ -31,309 +29,11 @@ from Model.Results.River.River import River
logger = logging.getLogger() logger = logging.getLogger()
class AdditionalData(SQLSubModel):
_sub_classes = []
def __init__(self, id=-1, study=None, data=None):
super(AdditionalData, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
)
self._study = study
self._data = data
@property
def data(self):
return self._data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results_add_data{ext} (
{cls.create_db_add_pamhyr_id()},
result INTEGER NOT NULL,
type_x TEXT NOT NULL,
type_y TEXT NOT NULL,
legend TEXT NOT NULL,
unit TEXT NOT NULL,
data_len INTEGER NOT NULL,
x BLOB NOT NULL,
y 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, 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(".")
if major == "0" and int(minor) == 2 and int(release) <= 1:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
study = data['study']
status = data['status']
scenario = data["scenario"]
table = execute(
"SELECT pamhyr_id, type_x, type_y, " +
"legend, unit, data_len, x, y, " +
"scenario " +
"FROM results_add_data " +
f"WHERE scenario = {scenario.id}"
)
if table is None:
return new
for v in table:
it = iter(v)
pid = next(it)
type_x = next(it)
type_y = next(it)
legend = next(it)
unit = next(it)
data_len = next(it)
bx = next(it)
by = next(it)
owner_scenario = next(it)
data_format = ">" + ''.join(itertools.repeat("d", data_len))
x = struct.unpack(data_format, bx)
y = struct.unpack(data_format, by)
data = {
'type_x': type_x,
'type_y': type_y,
'legend': legend,
'unit': unit,
'x': x, 'y': y
}
new_data = cls(study=study)
new_data._data = data
new.append(new_data)
return new
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
return
pid = self._pamhyr_id
data_len = len(self._data["x"])
data_format = ">" + ''.join(itertools.repeat("d", data_len))
bx = struct.pack(data_format, *self._data["x"])
by = struct.pack(data_format, *self._data["y"])
execute(
"INSERT INTO " +
"results_add_data (pamhyr_id, result, " +
"type_x, type_y, " +
"legend, unit, data_len, x, y, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, data["result"],
self._data["type_x"], self._data["type_y"],
self._data["legend"], self._data["unit"],
data_len, bx, by, self._owner_scenario
)
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):
if not cls.is_table_exists(execute, "results_table_data"):
cls._db_create(execute)
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._data[key]
class Results(SQLSubModel): class Results(SQLSubModel):
_sub_classes = [River, TableData, AdditionalData] _sub_classes = [River]
def __init__(self, id=-1, study=None, solver=None, def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0", is_ensemble=False): repertory="", name="0"):
super(Results, self).__init__( super(Results, self).__init__(
id=id, status=study.status, id=id, status=study.status,
owner_scenario=study.status.scenario.id owner_scenario=study.status.scenario.id
@@ -341,30 +41,20 @@ class Results(SQLSubModel):
self._name = name self._name = name
self._study = study self._study = study
# Solver readers add their own reaches; ensembles need them upfront. self._river = River(self._study)
self._river = River(self._study, self, init=is_ensemble)
self._solver = solver self._solver = solver
self._repertory = repertory self._repertory = repertory
self._is_ensemble = is_ensemble
self._meta_data = { self._meta_data = {
# Keep results creation date # Keep results creation date
"creation_date": datetime.now(), "creation_date": datetime.now(),
"study_revision": study.status.version, "study_revision": study.status.version,
"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)
def is_ensemble(self):
return self._is_ensemble
@property @property
def date(self): def date(self):
date = self._meta_data["creation_date"] date = self._meta_data["creation_date"]
@@ -378,7 +68,6 @@ 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
@@ -396,16 +85,6 @@ 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,
@@ -413,11 +92,6 @@ class Results(SQLSubModel):
qlog=None, qlog=None,
) )
def bufferize(self, key):
if self.is_valid:
for reach in self._river._reachs:
reach.bufferize(self._meta_data["timestamps"], key)
def timestamps_to_struct(self): def timestamps_to_struct(self):
ts = self._meta_data["timestamps"] ts = self._meta_data["timestamps"]
sf = ">" + ''.join(itertools.repeat("d", len(ts))) sf = ">" + ''.join(itertools.repeat("d", len(ts)))
@@ -432,11 +106,9 @@ class Results(SQLSubModel):
solver_name TEXT NOT NULL, solver_name TEXT NOT NULL,
solver_type TEXT NOT NULL, solver_type TEXT NOT NULL,
study_revision INTEGER NOT NULL, study_revision INTEGER NOT NULL,
creation_date DATE NOT NULL, creation_data DATE NOT NULL,
nb_timestamps INTEGER NOT NULL, nb_timestamps INTEGER NOT NULL,
timestamps BLOB NOT NULL, timestamps BLOB NOT NULL,
seed INTEGER,
number_of_run INTEGER,
{Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}, {Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario) PRIMARY KEY(pamhyr_id, scenario)
@@ -462,21 +134,6 @@ class Results(SQLSubModel):
cls._db_create(execute) cls._db_create(execute)
create = True create = True
if major == "0" and int(minor) <= 2:
if int(release) < 9 and not create:
execute(
"ALTER TABLE results ADD COLUMN " +
"seed INTEGER"
)
execute(
"ALTER TABLE results ADD COLUMN " +
"number_of_run INTEGER"
)
execute(
"ALTER TABLE results RENAME COLUMN " +
"creation_data TO creation_date"
)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@classmethod @classmethod
@@ -489,9 +146,7 @@ class Results(SQLSubModel):
table = execute( table = execute(
"SELECT pamhyr_id, solver_name, solver_type, " + "SELECT pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_date, " + "study_revision, creation_data, nb_timestamps, timestamps, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " +
"scenario " + "scenario " +
"FROM results " + "FROM results " +
f"WHERE scenario = {scenario.id}" f"WHERE scenario = {scenario.id}"
@@ -511,8 +166,6 @@ class Results(SQLSubModel):
solver_type = next(it) solver_type = next(it)
revision = next(it) revision = next(it)
creation_date = next(it) creation_date = next(it)
seed = next(it)
number_of_run = next(it)
nb_timestamps = next(it) nb_timestamps = next(it)
timestamps_bytes = next(it) timestamps_bytes = next(it)
owner_scenario = next(it) owner_scenario = next(it)
@@ -523,45 +176,14 @@ class Results(SQLSubModel):
new_results.set("solver_type", solver_type) new_results.set("solver_type", solver_type)
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)
if seed is not None:
new_results.set("seed", seed)
new_results.set("number_of_run", number_of_run)
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(
"additional_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))
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):
@@ -584,26 +206,10 @@ 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(
"DELETE FROM results_add_data " +
f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}"
)
def _is_valid(self):
return "timestamps" in self._meta_data
def _db_save(self, execute, data=None): def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario: if self._status.scenario.id != self._owner_scenario:
return True return
if not self._is_valid():
return False
pid = self._pamhyr_id pid = self._pamhyr_id
if self._solver is None: if self._solver is None:
@@ -618,43 +224,18 @@ class Results(SQLSubModel):
ts = sorted(self.get("timestamps")) ts = sorted(self.get("timestamps"))
sf = ">" + ''.join(itertools.repeat("d", len(ts))) sf = ">" + ''.join(itertools.repeat("d", len(ts)))
if "seed" in self._meta_data:
seed = int(self.get("seed"))
number_of_run = int(self.get("number_of_run"))
else:
seed = None
number_of_run = None
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"results (pamhyr_id, solver_name, solver_type, " + "results (pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_date, " + "study_revision, creation_data, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " + "nb_timestamps, timestamps, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, solver_name, solver_type, self._pamhyr_id, solver_name, solver_type,
self._status.version, self.get("creation_date"), self._status.version, self.get("creation_date"),
seed, number_of_run, len(ts), struct.pack(sf, *ts), self._owner_scenario
len(ts), struct.pack(sf, *ts),
self._owner_scenario
) )
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"):
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
)
+2 -9
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr # Results.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -21,13 +21,12 @@ import os
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Results.River.RiverAdisTS import River from Model.Results.River.RiverAdisTS import River
logger = logging.getLogger() logger = logging.getLogger()
class Results(SQLSubModel): class Results(object):
def __init__(self, study=None, solver=None, def __init__(self, study=None, solver=None,
repertory="", name="0", type_pol=None): repertory="", name="0", type_pol=None):
self._study = study self._study = study
@@ -51,9 +50,6 @@ class Results(SQLSubModel):
self._phys_var_list = ["C", "G", "M", "D", "L", "N", "R"] self._phys_var_list = ["C", "G", "M", "D", "L", "N", "R"]
if solver is not None:
self.set("solver_type", solver._type)
@property @property
def date(self): def date(self):
date = self._meta_data["creation_date"] date = self._meta_data["creation_date"]
@@ -91,6 +87,3 @@ class Results(SQLSubModel):
self._repertory, self._repertory,
qlog=None, qlog=None,
) )
def _db_save(self, execute, data=None):
return False
+38 -113
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr # River.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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,13 +14,11 @@
# 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
from tools import flatten, timer from tools import flatten
from functools import reduce from functools import reduce
from datetime import datetime from datetime import datetime
@@ -31,17 +29,15 @@ logger = logging.getLogger()
class Profile(SQLSubModel): class Profile(SQLSubModel):
def __init__(self, profile, study, parent): def __init__(self, profile, study):
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)
@@ -58,26 +54,15 @@ 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],
@@ -85,25 +70,13 @@ 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]:
v = self._data[timestamp][key] return 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(self):
return any(map(lambda ts: "sl" in self._data[ts], self._data))
def has_bedload(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))
@classmethod @classmethod
@@ -132,14 +105,24 @@ class Profile(SQLSubModel):
@classmethod @classmethod
def _db_update(cls, execute, version, data=None): def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".") major, minor, release = version.strip().split(".")
create = False
if major == "0" and int(minor) < 2:
cls._db_create(execute)
create = True
if major == "0" and int(minor) == 2:
if int(release) < 1 and not create:
cls._db_create(execute)
create = True
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@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']
@@ -147,14 +130,11 @@ 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 " +
"FROM results_data " + "FROM results_data " +
f"WHERE scenario = {scenario.id} " + f"WHERE scenario = {scenario.id} " +
f"AND result = {data['result_pid']} " +
f"AND reach = {reach.pamhyr_id} " + f"AND reach = {reach.pamhyr_id} " +
f"AND section = {profile.pamhyr_id}" f"AND section = {profile.pamhyr_id}"
) )
@@ -169,6 +149,12 @@ 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)
@@ -181,11 +167,11 @@ class Profile(SQLSubModel):
) )
for timestamp, value in zip(timestamps, values): for timestamp, value in zip(timestamps, values):
new.set(timestamp, key, value) new_data.set(timestamp, key, value)
if key == "Z": if key == "Z":
new.update_water_limits(timestamp, value) new_data.update_water_limits(timestamp, value)
return new return list(new.values())
@classmethod @classmethod
def _db_load_data_sl_format(cls, values, len_data, timestamps): def _db_load_data_sl_format(cls, values, len_data, timestamps):
@@ -277,23 +263,20 @@ class Profile(SQLSubModel):
class Reach(SQLSubModel): class Reach(SQLSubModel):
_sub_classes = [Profile] _sub_classes = [Profile]
def __init__(self, reach, study, parent, with_init=True, profiles=None): def __init__(self, reach, study, 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:
if profiles is None:
profiles = reach.profiles
self._profiles = list( self._profiles = list(
map( map(
lambda p: Profile(p, self._study, self._parent), lambda p: Profile(p, self._study),
profiles reach.profiles
) )
) )
@@ -303,10 +286,6 @@ class Reach(SQLSubModel):
) )
) )
self._global_index = (-1, -1)
self._buffers = {}
def __len__(self): def __len__(self):
return len(self._profiles) return len(self._profiles)
@@ -326,42 +305,14 @@ class Reach(SQLSubModel):
def profile_mask(self): def profile_mask(self):
return self._profile_mask return self._profile_mask
@property
def buffers(self):
return self._buffers
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)
def has_sediment(self): def has_sediment(self):
return any(map(lambda profile: profile.has_sediment_layers(), return any(map(lambda profile: profile.has_sediment(), self._profiles))
self._profiles))
def has_bedload(self):
return any(map(lambda profile: profile.has_bedload(), self._profiles))
def bufferize(self, timestamps, key):
self._buffers[key] = np.zeros((len(timestamps), len(self)))
for i, p in enumerate(self._profiles):
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=""):
@@ -376,20 +327,12 @@ class Reach(SQLSubModel):
reach = data["reach"] reach = data["reach"]
new_reach = cls( new_reach = cls(
data["reach"], data["study"], data["parent"], data["reach"], data["study"], with_init=False
with_init=False
) )
for i, profile in enumerate(reach.profiles): for i, profile in enumerate(reach.profiles):
data["profile"] = profile data["profile"] = profile
result_profile = Profile._db_load(execute, data) new_reach._profiles += Profile._db_load(execute, data)
if len(result_profile) > 0:
new_reach._profiles.append(result_profile)
new_reach._profile_mask = [
profile.name[0:8] != 'interpol'
for profile in new_reach._profiles
]
return new_reach return new_reach
@@ -403,20 +346,16 @@ class Reach(SQLSubModel):
class River(SQLSubModel): class River(SQLSubModel):
_sub_classes = [Reach] _sub_classes = [Reach]
def __init__(self, study, parent, init=True): def __init__(self, study):
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 = []
if init:
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
def __len__(self): def __len__(self):
return len(self._reachs) return len(self._reachs)
@@ -431,13 +370,10 @@ class River(SQLSubModel):
def has_reach(self, id): def has_reach(self, id):
return 0 <= id < len(self._reachs) return 0 <= id < len(self._reachs)
def add(self, reach_id, profiles=None): def add(self, reach_id):
reachs = self._study.river.enable_edges() reachs = self._study.river.enable_edges()
new = Reach( new = Reach(reachs[reach_id].reach, self._study)
reachs[reach_id].reach, self._study, self._parent,
profiles=profiles
)
self._reachs.append(new) self._reachs.append(new)
return new return new
@@ -450,15 +386,6 @@ 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] + 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)
@@ -470,15 +397,13 @@ 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"]
parent = data["parent"] new_river = cls(study)
new_river = cls(study, parent, init=False)
for reach in study.river.reachs(): for reach in study.river.reachs():
data["reach"] = reach.reach data["reach"] = reach.reach
result_reach = Reach._db_load(execute, data) new_river._reachs.append(
if len(result_reach) > 0: Reach._db_load(execute, data)
new_river._reachs.append(result_reach) )
return new_river return new_river
+5 -11
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr # River.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -64,15 +64,13 @@ class Profile(object):
class Reach(object): class Reach(object):
def __init__(self, reach, study, profiles=None): def __init__(self, reach, study):
self._study = study self._study = study
self._reach = reach # Source reach in the study self._reach = reach # Source reach in the study
if profiles is None:
profiles = reach.profiles
self._profiles = list( self._profiles = list(
map( map(
lambda p: Profile(p, self._study), lambda p: Profile(p, self._study),
profiles reach.profiles
) )
) )
@@ -91,10 +89,6 @@ class Reach(object):
def profiles(self): def profiles(self):
return self._profiles.copy() return self._profiles.copy()
def get_rk(self):
"""Return the PK values for profiles present in the results."""
return [profile.rk for profile in self._profiles]
def profile(self, id): def profile(self, id):
return self._profiles[id] return self._profiles[id]
@@ -125,10 +119,10 @@ class River(object):
def has_reach(self, id): def has_reach(self, id):
return 0 <= id < len(self._reachs) return 0 <= id < len(self._reachs)
def add(self, reach_id, profiles=None): 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, profiles=profiles) new = Reach(reachs[reach_id].reach, self._study)
self._reachs.append(new) self._reachs.append(new)
return new return new
+17 -162
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr river model # River.py -- Pamhyr river model
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -49,6 +49,7 @@ from Model.REPLine.REPLineList import REPLineList
from Solver.Solvers import solver_type_list from Solver.Solvers import solver_type_list
from Model.OutputRKAdists.OutputRKListAdists import OutputRKAdistsList
from Model.Pollutants.PollutantsList import PollutantsList from Model.Pollutants.PollutantsList import PollutantsList
from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList \ from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList \
import InitialConditionsAdisTSList import InitialConditionsAdisTSList
@@ -58,18 +59,6 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
import LateralContributionsAdisTSList import LateralContributionsAdisTSList
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
import InitialConditionsTemperatureList
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
from Model.WeatherParameters.WeatherParametersList \
import WeatherParametersList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results from Model.Results.Results import Results
logger = logging.getLogger() logger = logging.getLogger()
@@ -190,9 +179,6 @@ class RiverNode(Node):
return nodes return nodes
def _db_save(self, execute, data=None): def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute( execute(
"INSERT OR REPLACE INTO river_node(" + "INSERT OR REPLACE INTO river_node(" +
"pamhyr_id, deleted, name, x, y, scenario" + "pamhyr_id, deleted, name, x, y, scenario" +
@@ -374,7 +360,7 @@ class RiverReach(Edge):
) )
new.enable(enable=enabled) new.enable(enable=enabled)
if deleted: if deleted:
new.set_as_deleted() nd.set_as_deleted()
data["reach"] = new data["reach"] = new
new._reach = Reach._db_load(execute, data) new._reach = Reach._db_load(execute, data)
@@ -390,9 +376,6 @@ class RiverReach(Edge):
return reachs return reachs
def _db_save(self, execute, data=None): def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute( execute(
"INSERT OR REPLACE INTO " + "INSERT OR REPLACE INTO " +
"river_reach(" + "river_reach(" +
@@ -402,7 +385,7 @@ class RiverReach(Edge):
"VALUES (" + "VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " + f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " + f"'{self._db_format(self._name)}', " +
f"{self._db_format(self.is_enable())}," + f"{self._db_format(self.is_enable())},"
f"{self.node1.pamhyr_id}, {self.node2.pamhyr_id}, " + f"{self.node1.pamhyr_id}, {self.node2.pamhyr_id}, " +
f"{self._status.scenario_id}" + f"{self._status.scenario_id}" +
")" ")"
@@ -478,17 +461,13 @@ class River(Graph):
HydraulicStructureList, HydraulicStructureList,
AddFileList, AddFileList,
REPLineList, REPLineList,
OutputRKAdistsList,
PollutantsList, PollutantsList,
InitialConditionsAdisTSList, InitialConditionsAdisTSList,
BoundaryConditionsAdisTSList, BoundaryConditionsAdisTSList,
LateralContributionsAdisTSList, LateralContributionsAdisTSList,
D90AdisTSList, D90AdisTSList,
DIFAdisTSList, DIFAdisTSList,
InitialConditionsTemperatureList,
BoundaryConditionsTemperatureList,
WeatherParametersList,
GeoTIFFList,
EnsembleList,
Results Results
] ]
@@ -515,6 +494,7 @@ class River(Graph):
) )
self._additional_files = AddFileList(status=self._status) self._additional_files = AddFileList(status=self._status)
self._rep_lines = REPLineList(status=self._status) self._rep_lines = REPLineList(status=self._status)
self._Output_rk_adists = OutputRKAdistsList(status=self._status)
self._Pollutants = PollutantsList(status=self._status) self._Pollutants = PollutantsList(status=self._status)
self._InitialConditionsAdisTS = InitialConditionsAdisTSList( self._InitialConditionsAdisTS = InitialConditionsAdisTSList(
status=self._status) status=self._status)
@@ -524,18 +504,6 @@ class River(Graph):
status=self._status) status=self._status)
self._D90AdisTS = D90AdisTSList(status=self._status) self._D90AdisTS = D90AdisTSList(status=self._status)
self._DIFAdisTS = DIFAdisTSList(status=self._status) self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._InitialConditionsTemperature = (
InitialConditionsTemperatureList(status=self._status)
)
self._BoundaryConditionsTemperature = (
BoundaryConditionsTemperatureList(status=self._status)
)
self._WeatherParameters = (WeatherParametersList(status=self._status))
self._geotiff = GeoTIFFList(status=self._status)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {} self._results = {}
@@ -571,7 +539,7 @@ class River(Graph):
new._nodes = RiverNode._db_load( new._nodes = RiverNode._db_load(
execute, data execute, data
) )
data["nodes"] = new._nodes data["nodes"] = new.nodes()
data['loaded_pid'] = set() data['loaded_pid'] = set()
new._edges = RiverReach._db_load( new._edges = RiverReach._db_load(
@@ -632,6 +600,10 @@ class River(Graph):
new._Pollutants = PollutantsList._db_load(execute, data) new._Pollutants = PollutantsList._db_load(execute, data)
new._Output_rk_adists = OutputRKAdistsList._db_load(
execute, data
)
new._InitialConditionsAdisTS = InitialConditionsAdisTSList._db_load( new._InitialConditionsAdisTS = InitialConditionsAdisTSList._db_load(
execute, data) execute, data)
@@ -645,18 +617,6 @@ class River(Graph):
new._DIFAdisTS = DIFAdisTSList._db_load(execute, data) new._DIFAdisTS = DIFAdisTSList._db_load(execute, data)
new._InitialConditionsTemperature = \
InitialConditionsTemperatureList._db_load(execute, data)
new._BoundaryConditionsTemperature = \
BoundaryConditionsTemperatureList._db_load(execute, data)
new._WeatherParameters = WeatherParametersList._db_load(execute, data)
new._geotiff = GeoTIFFList._db_load(execute, data)
new._ensembles = EnsembleList._db_load(execute, data)
return new return new
def _db_load_results(self, execute, data=None): def _db_load_results(self, execute, data=None):
@@ -682,6 +642,7 @@ class River(Graph):
for solver in self._parameters: for solver in self._parameters:
objs.append(self._parameters[solver]) objs.append(self._parameters[solver])
objs.append(self._Output_rk_adists)
objs.append(self._Pollutants) objs.append(self._Pollutants)
objs.append(self._InitialConditionsAdisTS) objs.append(self._InitialConditionsAdisTS)
objs.append(self._BoundaryConditionsAdisTS) objs.append(self._BoundaryConditionsAdisTS)
@@ -689,14 +650,6 @@ class River(Graph):
objs.append(self._D90AdisTS) objs.append(self._D90AdisTS)
objs.append(self._DIFAdisTS) objs.append(self._DIFAdisTS)
objs.append(self._InitialConditionsTemperature)
objs.append(self._BoundaryConditionsTemperature)
objs.append(self._WeatherParameters)
objs.append(self._geotiff)
objs.append(self._ensembles)
for solv_type in self.results: for solv_type in self.results:
objs.append(self.results[solv_type]) objs.append(self.results[solv_type])
@@ -767,15 +720,12 @@ class River(Graph):
self._hydraulic_structures, self._hydraulic_structures,
self._additional_files, self._additional_files,
self._rep_lines, self._rep_lines,
self._Output_rk_adists,
self._Pollutants, self._Pollutants,
self._InitialConditionsAdisTS, self._InitialConditionsAdisTS,
self._BoundaryConditionsAdisTS, self._BoundaryConditionsAdisTS,
self._LateralContributionsAdisTS, self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS, self._D90AdisTS, self._DIFAdisTS,
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
self._WeatherParameters,
self._geotiff, self._ensembles,
] ]
for solver in self._parameters: for solver in self._parameters:
@@ -868,10 +818,6 @@ Last export at: @date."""
def additional_files(self): def additional_files(self):
return self._additional_files return self._additional_files
@property
def geotiff(self):
return self._geotiff
@property @property
def rep_lines(self): def rep_lines(self):
return self._rep_lines return self._rep_lines
@@ -880,6 +826,10 @@ Last export at: @date."""
def parameters(self): def parameters(self):
return self._parameters return self._parameters
@property
def Output_rk_adists(self):
return self._Output_rk_adists
@property @property
def Pollutants(self): def Pollutants(self):
return self._Pollutants return self._Pollutants
@@ -904,67 +854,6 @@ Last export at: @date."""
def dif_adists(self): def dif_adists(self):
return self._DIFAdisTS return self._DIFAdisTS
@property
def ic_temperature(self):
return self._InitialConditionsTemperature
@property
def boundary_conditions_temperature(self):
return self._BoundaryConditionsTemperature
@property
def weather_parameters(self):
return self._WeatherParameters
def profile_dependencies(self, profile):
"""Return model data which directly reference *profile*.
PK-based intervals are deliberately not included: only explicit
references to the profile object prevent it from being disabled.
"""
dependencies = []
def add(kind, item, association):
dependencies.append({
"kind": kind,
"item": item,
"association": association,
})
for structure in self._hydraulic_structures.lst:
if structure.input_section is profile:
add("hydraulic_structure", structure, "")
for tab in self._lateral_contribution._tabs_list:
for contribution in self._lateral_contribution.get_tab(tab):
if contribution.begin_section is profile:
add("lateral_contribution", contribution, "begin_section")
if contribution.end_section is profile:
add("lateral_contribution", contribution, "end_section")
for parameter in self._WeatherParameters.lst:
if parameter.begin_section is profile:
add("weather_parameter", parameter, "begin_section")
if parameter.end_section is profile:
add("weather_parameter", parameter, "end_section")
reach_id = profile.reach.pamhyr_id
for initial_condition in self._InitialConditionsTemperature.lst:
for specification in initial_condition.data:
same_reach = specification.reach == reach_id
same_rk = (
specification.start_rk == profile.rk or
specification.end_rk == profile.rk
)
if same_reach and same_rk:
add(
"initial_condition_temperature",
specification,
"cross_section_pk",
)
return dependencies
def get_params(self, solver): def get_params(self, solver):
if solver in self._parameters: if solver in self._parameters:
return self._parameters[solver] return self._parameters[solver]
@@ -1021,12 +910,6 @@ Last export at: @date."""
self._results[solv_type] = results self._results[solv_type] = results
def _split_reach(self, reach, profile): def _split_reach(self, reach, profile):
profiles = reach.reach.profiles
if not profiles or profile not in profiles[1:-1]:
raise ValueError(
"A reach cannot be split at its first or last profile"
)
node1 = reach.node1 node1 = reach.node1
node2 = reach.node2 node2 = reach.node2
@@ -1043,32 +926,4 @@ Last export at: @date."""
self._add_edge(r1) self._add_edge(r1)
self._add_edge(r2) self._add_edge(r2)
self._initial_conditions.split_reach(
reach, profile, r1, r2
)
self._lateral_contribution.split_reach(
reach, profile, r1, r2
)
self._hydraulic_structures.split_reach(
reach, profile, r1, r2
)
self._InitialConditionsAdisTS.split_reach(
reach, profile, r1, r2
)
self._LateralContributionsAdisTS.split_reach(
reach, profile, r1, r2
)
self._InitialConditionsTemperature.split_reach(
reach, profile, r1, r2
)
self._WeatherParameters.split_reach(
reach, profile, r1, r2
)
self._D90AdisTS.split_reach(
reach, profile, r1, r2
)
self._DIFAdisTS.split_reach(
reach, profile, r1, r2
)
return r1, r2 return r1, r2
+44 -137
View File
@@ -17,63 +17,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import logging
from tools import logger_exception, timer, flatten from tools import logger_exception
from Model.Tools.PamhyrDB import SQLSubModel from Model.Tools.PamhyrDB import SQLSubModel
logger = logging.getLogger()
class Scenario(SQLSubModel): class Scenario(SQLSubModel):
_id_cnt = 0 _id_cnt = 0
_sub_classes = [] _sub_classes = []
tables_with_deleted_column = [
# Adists
"boundary_condition_adists", "boundary_condition_data_adists",
"lateral_contribution_adists", "lateral_contribution_data_adists",
"initial_conditions_adists", "initial_conditions_adists_spec",
"d90_adists", "d90_adists_spec",
"dif_adists_spec",
"pollutants", "pollutants_characteristics",
# Hydraulic
"additional_files",
"boundary_condition", "boundary_condition_data",
"lateral_contribution", "lateral_contribution_data",
"friction", "stricklers",
"hydraulic_structures",
"hydraulic_structures_basic",
"initial_conditions",
"sedimentary_layer", "sedimentary_layer_layer",
"reservoir", "reservoir_data",
"rep_lines",
"geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node",
"geotiff", "reservoir", "reservoir_data",
# Temperature
"boundary_condition_temperature",
"boundary_condition_data_temperature",
"initial_conditions_temperature",
"initial_conditions_temperature_spec",
"weather_parameters", "weather_parameters_data",
"ensemble",
]
related_tables = tables_with_deleted_column + [
"dif_adists",
"solver_parameter",
"hydraulic_structures_basic_value",
"results", "results_data", "results_add_data",
]
def __init__(self, def __init__(self,
id: int = -1, id: int = -1,
name: str = "", name: str = "",
description: str = "", description: str = "",
x: int = 1000.0, y: int = 1000.0, x: int = 0.0, y: int = 0.0,
is_ensemble: bool = False,
revision: int = 0, revision: int = 0,
parent=None): parent=None):
super(Scenario, self).__init__() super(Scenario, self).__init__()
@@ -83,7 +41,6 @@ class Scenario(SQLSubModel):
self._x, self._y = x, y self._x, self._y = x, y
self._name = name self._name = name
self._description = description self._description = description
self._is_ensemble = is_ensemble
self._revision = revision self._revision = revision
self._parent = parent self._parent = parent
@@ -106,7 +63,6 @@ class Scenario(SQLSubModel):
y REAL NOT NULL DEFAULT 1000, y REAL NOT NULL DEFAULT 1000,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
is_ensemble BOOL DEFAULT FALSE,
revision INTEGER NOT NULL, revision INTEGER NOT NULL,
parent_id INTEGER REFERENCES scenario(id) parent_id INTEGER REFERENCES scenario(id)
) )
@@ -142,13 +98,6 @@ class Scenario(SQLSubModel):
cls._db_create(execute) cls._db_create(execute)
cls._db_add_default(execute) cls._db_add_default(execute)
if major == "0" and int(minor) == 2:
if int(release) < 8:
execute(
f"ALTER TABLE scenario " +
"ADD COLUMN is_ensemble BOOL DEFAULT FALSE"
)
return True return True
@classmethod @classmethod
@@ -169,8 +118,7 @@ class Scenario(SQLSubModel):
scenarios = {} scenarios = {}
table = execute( table = execute(
"SELECT id, x, y, name, description, " + "SELECT id, x, y, name, description, revision, parent_id " +
" is_ensemble, revision, parent_id " +
"FROM scenario " + "FROM scenario " +
"ORDER BY id ASC" "ORDER BY id ASC"
) )
@@ -182,7 +130,6 @@ class Scenario(SQLSubModel):
x, y = next(it), next(it) x, y = next(it), next(it)
name = next(it) name = next(it)
desc = next(it) desc = next(it)
is_ens = next(it)
revi = next(it) revi = next(it)
parent = next(it) parent = next(it)
@@ -192,9 +139,7 @@ class Scenario(SQLSubModel):
new = cls( new = cls(
id=id, x=x, y=y, id=id, x=x, y=y,
name=name, description=desc, name=name, description=desc,
is_ensemble=is_ens, revision=revi, parent=parent
revision=revi,
parent=parent
) )
scenarios[id] = new scenarios[id] = new
@@ -204,18 +149,21 @@ class Scenario(SQLSubModel):
if self.is_deleted(): if self.is_deleted():
return self.drop_all(execute) return self.drop_all(execute)
parent = None parent = 'NULL'
if self.parent is not None: if self.parent is not None:
parent = self.parent._id parent = self.parent._id
execute( execute(
"INSERT OR REPLACE INTO " + "INSERT OR REPLACE INTO " +
"scenario(id, x, y, name, description, " + "scenario(id, x, y, name, description, revision, parent_id) " +
" is_ensemble, revision, parent_id) " + "VALUES (" +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)", f"{self._id}, " +
self._id, self.x, self.y, f"{self.x}, {self.y}, " +
self.name, self.description, self._is_ensemble, f"'{self._db_format(self.name)}', " +
self._revision, parent f"'{self._db_format(self.description)}', " +
f"{self._revision}, " +
f"{parent}" +
")"
) )
return True return True
@@ -223,7 +171,29 @@ class Scenario(SQLSubModel):
def drop_all(self, execute): def drop_all(self, execute):
execute(f"DELETE FROM scenario WHERE id = {self.id}") execute(f"DELETE FROM scenario WHERE id = {self.id}")
tables = self.related_tables tables = [
# Adists
"output_rk_adists",
"boundary_condition_adists", "boundary_condition_data_adists",
"lateral_contribution_adists", "lateral_contribution_data_adists",
"initial_conditions_adists",
"d90_adists", "dif_adists",
"pollutants", "pollutants_characteristics",
# Hydraulic
"additional_files",
"boundary_condition", "boundary_condition_data",
"lateral_contribution", "lateral_contribution_data",
"friction", "stricklers",
"hydraulic_structures",
"hydraulic_structures_basic", "hydraulic_structures_basic_value",
"initial_conditions",
"sedimentary_layer", "sedimentary_layer_layer",
"reservoir", "reservoir_data",
"rep_lines",
"solver_parameter",
"geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node",
]
for table in tables: for table in tables:
execute( execute(
@@ -233,67 +203,6 @@ class Scenario(SQLSubModel):
return True return True
def get_parent_branch(self):
def aux(scenario, acc):
if scenario is None:
return acc
return aux(scenario.parent, [scenario.id] + acc)
return aux(self, [])
@timer
def clean_deleted_data(self, execute):
tables = self.tables_with_deleted_column
branch = self.get_parent_branch()
all_ids = []
for table in tables:
if self.parent is None:
# This scenario is the default scenario, so we can
# delete all data marked as deleted
execute(
f"DELETE FROM {table} " +
f"WHERE scenario = {self.id} " +
"AND deleted = TRUE"
)
continue
# Select pamhyr_id for each deleted data in this scenario
# who do not exists into parents scenarios
ids = execute(
f"""
SELECT pamhyr_id FROM {table}
WHERE deleted = TRUE
AND scenario = {self.id}
AND pamhyr_id NOT IN (
SELECT pamhyr_id FROM {table}
WHERE scenario IN
({', '.join(map(str, branch[:-1]))})
)
""", fetch_one=False
)
if ids is None or len(ids) == 0:
continue
ids = flatten(ids)
logger.debug(
f"({self.name}) Drop deleted data into '{table}' : {ids}"
)
execute(
f"DELETE FROM {table} " +
f"WHERE scenario = {self.id} " +
"AND pamhyr_id IN " +
f"({', '.join(map(str, ids))})"
)
all_ids += ids
return all_ids
@property @property
def id(self): def id(self):
return self._id return self._id
@@ -340,9 +249,6 @@ class Scenario(SQLSubModel):
def parent(self): def parent(self):
return self._parent return self._parent
def is_ensemble(self):
return self._is_ensemble
def set_pos(self, x, y): def set_pos(self, x, y):
self._x = x self._x = x
self._y = y self._y = y
@@ -360,16 +266,17 @@ class Scenario(SQLSubModel):
return self.name return self.name
if key == "description": if key == "description":
return self.description return self.description
if key == "is_ensemble":
return self.is_ensemble()
if key == "parent": if key == "parent":
return self.parent return self.parent
return None return None
def workdir(self): def workdir(self):
current = "scenario_" + str(self.id) srep = ""
if self.parent is None or self.parent.id == 0: if self.id != 0:
return current srep = os.path.join(
self.parent.workdir(),
"scenario_" + str(self.id)
)
return os.path.join(self.parent.workdir(), current) return srep
+2 -16
View File
@@ -16,8 +16,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from functools import reduce
from tools import logger_exception from tools import logger_exception
from Model.Tools.PamhyrDict import PamhyrModelDict from Model.Tools.PamhyrDict import PamhyrModelDict
@@ -57,11 +55,8 @@ class Scenarios(PamhyrModelDict):
return None return None
def new(self, parent, is_ensemble=False): def new(self, parent):
new = Scenario( new = Scenario(parent=parent)
parent=parent,
is_ensemble=is_ensemble
)
self.set(new._id, new) self.set(new._id, new)
return new return new
@@ -83,12 +78,3 @@ class Scenarios(PamhyrModelDict):
self._dict[key].set_as_deleted() self._dict[key].set_as_deleted()
self._status.modified() self._status.modified()
def is_leaf(self, scenario):
return not reduce(
lambda acc, s: (
acc or (not s.is_deleted() and s.parent is scenario)
),
self._dict.values(),
False
)
+1 -1
View File
@@ -1,5 +1,5 @@
# Serializable.py -- Pamhyr pickle abstract class # Serializable.py -- Pamhyr pickle abstract class
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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-2026 INRAE # Copyright (C) 2023-2025 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
@@ -216,9 +216,6 @@ class Parameter(SQLSubModel):
return new return new
def _db_save(self, execute, data=None): def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ind = data["ind"] ind = data["ind"]
solver = data["solver"] solver = data["solver"]
+1 -14
View File
@@ -1,5 +1,5 @@
# Status.py -- Pamhyr model status class # Status.py -- Pamhyr model status class
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -34,7 +34,6 @@ class StudyStatus(object):
self._status = Status.EDIT self._status = Status.EDIT
self._scenario = scenario self._scenario = scenario
self._saved = True self._saved = True
self._loading = False
@property @property
def scenario_id(self): def scenario_id(self):
@@ -87,15 +86,6 @@ class StudyStatus(object):
def is_saved(self): def is_saved(self):
return self._saved return self._saved
def is_loading(self):
return self._loading
def start_loading(self):
self._loading = True
def stop_loading(self):
self._loading = False
def save(self): def save(self):
logger.debug("model status set as saved") logger.debug("model status set as saved")
self._saved = True self._saved = True
@@ -107,9 +97,6 @@ class StudyStatus(object):
self._saved = True self._saved = True
def modified(self): def modified(self):
if self._loading:
return
if self._saved: if self._saved:
self.version += 1 self.version += 1
+1 -1
View File
@@ -1,5 +1,5 @@
# Stricklers.py -- Pamhyr # Stricklers.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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 -5
View File
@@ -1,5 +1,5 @@
# StricklersList.py -- Pamhyr # StricklersList.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -60,12 +60,9 @@ class StricklersList(PamhyrModelList):
return self.lst return self.lst
def new(self, index): def new(self, index):
owner_scenario = (
self._status.scenario_id if self._status is not None else -1
)
s = Stricklers( s = Stricklers(
status=self._status, status=self._status,
owner_scenario=owner_scenario owner_scenario=self._status.scenario_id
) )
self.insert(index, s) self.insert(index, s)
self.modified() self.modified()
+14 -168
View File
@@ -1,5 +1,5 @@
# Study.py -- Pamhyr Study class # Study.py -- Pamhyr Study class
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -21,26 +21,15 @@ import shutil
import logging import logging
from datetime import datetime from datetime import datetime
from tools import timer, timestamp, parse_datetime from tools import timer, timestamp
from functools import reduce from functools import reduce
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Tools.PamhyrDict import PamhyrModelDict
from Model.Tools.PamhyrDB import SQLModel from Model.Tools.PamhyrDB import SQLModel
from Model.Scenarios import Scenarios from Model.Scenarios import Scenarios
from Model.Scenario import Scenario from Model.Scenario import Scenario
from Model.Status import StudyStatus from Model.Status import StudyStatus
from Model.Except import NotImplementedMethodeError from Model.Except import NotImplementedMethodeError
from Model.River import River from Model.River import River
from Model.TimeShift import shift_river_times, shift_database_times
from Model.Geometry.Reach import Reach
from Model.Ensembles.DistributionList import DistributionList
from Model.HydraulicStructures.HydraulicStructures import (
HydraulicStructure
)
from Model.HydraulicStructures.Basic.HydraulicStructures import (
BasicHS
)
from Checker.Study import * from Checker.Study import *
@@ -48,11 +37,10 @@ logger = logging.getLogger()
class Study(SQLModel): class Study(SQLModel):
_version = "0.2.10" _version = "0.2.1"
_sub_classes = [ _sub_classes = [
Scenario, Scenario,
DistributionList,
River, River,
] ]
@@ -75,7 +63,6 @@ class Study(SQLModel):
self.description = "" self.description = ""
# Time system # Time system
self._time_system = "time" self._time_system = "time"
self._pending_time_offset = 0.0
self._date = datetime.fromtimestamp(0) self._date = datetime.fromtimestamp(0)
if init_new: if init_new:
@@ -88,9 +75,6 @@ class Study(SQLModel):
self.scenarios = Scenarios(status=self.status) self.scenarios = Scenarios(status=self.status)
self.scenarios[0] = s0 self.scenarios[0] = s0
self.status.scenario = s0 self.status.scenario = s0
self._ens_distributions = DistributionList(status=self.status)
self._river = River(status=self.status) self._river = River(status=self.status)
else: else:
self._init_db_file(filename, is_new=False) self._init_db_file(filename, is_new=False)
@@ -205,16 +189,6 @@ class Study(SQLModel):
self._date = date self._date = date
self.status.modified() self.status.modified()
def change_start_date(self, date, preserve_dates=False):
"""Change the origin, optionally preserving series calendar dates."""
new_date = parse_datetime(date)
offset = (parse_datetime(self.date) - new_date).total_seconds()
if preserve_dates and offset:
shift_river_times(self.river, offset)
self._pending_time_offset += offset
self._river_scenario_cache.clear()
self.use_date(new_date)
@property @property
def date(self): def date(self):
return self._date return self._date
@@ -234,7 +208,8 @@ class Study(SQLModel):
me.name = name me.name = name
me.description = description me.description = description
if date is not None: if date is not None:
me.use_date(date) me.use_date()
me.date = date
return me return me
@classmethod @classmethod
@@ -306,14 +281,7 @@ class Study(SQLModel):
if major == "0" and int(minor) < 2: if major == "0" and int(minor) < 2:
self._add_into_info_if_not_exists('current_scenario', '0') self._add_into_info_if_not_exists('current_scenario', '0')
needs_foreign_keys_disabled = ( if major == "0" and int(minor) < 2:
major == "0" and (
int(minor) < 2 or
(minor == "2" and int(release) < 10)
)
)
if needs_foreign_keys_disabled:
# Need to temporary disable the sqlite foreign keys # Need to temporary disable the sqlite foreign keys
# checking to update db dans change the table id fk to # checking to update db dans change the table id fk to
# table pamhyr_id fk # table pamhyr_id fk
@@ -323,7 +291,7 @@ class Study(SQLModel):
ok = self._update_submodel(version[0], data={}) ok = self._update_submodel(version[0], data={})
if needs_foreign_keys_disabled: if major == "0" and int(minor) < 2:
# Reactivate foreign keys checking # Reactivate foreign keys checking
self.execute( self.execute(
"PRAGMA foreign_keys = ON;" "PRAGMA foreign_keys = ON;"
@@ -411,19 +379,12 @@ class Study(SQLModel):
lambda a, s: a or (s.parent is scenario), lambda a, s: a or (s.parent is scenario),
new.scenarios.lst, new.scenarios.lst,
False False
) or scenario.is_ensemble(): ):
new.status.set_as_read_only() new.status.set_as_read_only()
data["scenario"] = scenario data["scenario"] = scenario
data["loaded_pid"] = set() data["loaded_pid"] = set()
# Get ensemble distribution
new._ens_distributions = DistributionList._db_load(
sql_exec, data=data
)
data["ens_distributions"] = new._ens_distributions
# Load river data # Load river data
new._river = River._db_load( new._river = River._db_load(
sql_exec, data=data sql_exec, data=data
@@ -484,94 +445,12 @@ class Study(SQLModel):
) )
progress() progress()
if self._pending_time_offset:
self._db.execute('SAVEPOINT shift_time_origin')
try:
shift_database_times(self._db, self._pending_time_offset)
self._db.execute('RELEASE SAVEPOINT shift_time_origin')
except Exception:
self._db.execute('ROLLBACK TO SAVEPOINT shift_time_origin')
self._db.execute('RELEASE SAVEPOINT shift_time_origin')
raise
self._pending_time_offset = 0.0
if self.status.scenario.is_ensemble():
lst = [
self.scenarios, self._ens_distributions,
self._river._ensembles,
]
for solv in self._river._results:
lst.append(self._river._results[solv])
self._save_submodel( self._save_submodel(
lst, data=progress [self.scenarios, self._river],
)
progress()
self.commit()
return
self._save_submodel(
[
self.scenarios, self._ens_distributions,
self._river
],
data=progress data=progress
) )
# Clear DB for current scenario
ids = self.status.scenario.clean_deleted_data(self.execute)
self.memory_clean(ids)
progress()
self.commit() self.commit()
@timer
def memory_clean(self, ids):
if len(ids) == 0:
return
reach_class = Reach
hs_classes = [HydraulicStructure, BasicHS]
list_classes = set(PamhyrModelList.__subclasses__())
dict_classes = set(PamhyrModelDict.__subclasses__())
def modifier(obj, data):
t = type(obj)
if t is reach_class:
obj._profiles = list(
filter(
lambda el: el.id not in ids,
obj._profiles
)
)
elif t in hs_classes:
obj._data = list(
filter(
lambda el: el.id not in ids,
obj._data
)
)
elif t in dict_classes:
new = {}
for key in obj._dict:
if obj._dict[key].id not in ids:
new[key] = obj._dict[key]
obj._dict = new
elif t in list_classes:
obj._lst = list(
filter(
lambda el: el.id not in ids,
obj._lst
)
)
self.river._data_traversal(
lambda o, d: True, modifier, {}
)
def sql_save_request_count(self, *args, **kargs): def sql_save_request_count(self, *args, **kargs):
return self._count() return self._count()
@@ -580,10 +459,7 @@ class Study(SQLModel):
[self.scenarios, self._river] [self.scenarios, self._river]
) )
logger.debug(cnt) logger.debug(cnt)
# HACK: 8 is the number of SQL request do and dont counted in return cnt + 7
# submodel save_count (let's count sql execute into the
# '_save' method)
return cnt + 8
def close(self): def close(self):
"""Close db connection """Close db connection
@@ -609,8 +485,7 @@ class Study(SQLModel):
self.status.scenario = scenario self.status.scenario = scenario
if reduce( if reduce(
lambda a, s: a or (s.parent is scenario lambda a, s: a or (s.parent is scenario),
and not s.is_deleted()),
self.scenarios.lst, self.scenarios.lst,
False False
): ):
@@ -633,28 +508,19 @@ class Study(SQLModel):
data = { data = {
"status": self.status, "status": self.status,
"loaded_pid": set(), "loaded_pid": set(),
"scenario": scenario, "scenario": scenario
"ens_distributions": self._ens_distributions,
} }
self.status.start_loading()
try:
# Load river data # Load river data
river = River._db_load( river = River._db_load(
sql_exec, data=data sql_exec, data=data
) )
data["study"] = self data["study"] = self
river._db_load_results(sql_exec, data=data) river._db_load_results(sql_exec, data=data)
finally:
self.status.stop_loading() self._river_scenario_cache[scenario] = river
self.status.scenario = old_scenario self.status.scenario = old_scenario
if self._pending_time_offset:
shift_river_times(river, self._pending_time_offset)
# FIXME: Disable scenario cache to save memory usage
# self._river_scenario_cache[scenario] = river
return river return river
def copy_from_scenario(self, scenario): def copy_from_scenario(self, scenario):
@@ -711,7 +577,6 @@ class Study(SQLModel):
self.status.scenario.y + 100) self.status.scenario.y + 100)
new.name = self.status.scenario.name + " (copy)" new.name = self.status.scenario.name + " (copy)"
new.is_ensemble = self.status.scenario.is_ensemble
self.status.scenario = new self.status.scenario = new
self.river._data_traversal( self.river._data_traversal(
@@ -723,25 +588,6 @@ class Study(SQLModel):
self.status.set_as_editable() self.status.set_as_editable()
return new return new
def create_ensemble_scenario(self, pos=None, switch=True):
new = self.scenarios.new(
self.status.scenario,
is_ensemble=True
)
new.name = self.status.scenario.name + " (ensemble)"
if pos is not None:
new.set_pos(pos.x(), pos.y())
else:
new.set_pos(self.status.scenario.x + 100,
self.status.scenario.y)
if switch:
self.status.scenario = new
self.status.set_as_read_only()
return new
@property @property
def results(self): def results(self):
return self._river.results return self._river.results
-82
View File
@@ -1,82 +0,0 @@
"""Shift temporal series when preserving calendar dates across a new origin."""
from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition
from Model.LateralContribution.LateralContribution import LateralContribution
from Model.WeatherParameters.WeatherParameters import WeatherParameters
from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS import (
BoundaryConditionAdisTS
)
from Model.LateralContributionsAdisTS.LateralContributionAdisTS import (
LateralContributionAdisTS
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature import (
BoundaryConditionTemperature
)
def shift_river_times(river, offset):
changes = []
seen = set()
def collect(obj, data):
temporal = isinstance(obj, (
BoundaryConditionAdisTS,
LateralContributionAdisTS,
BoundaryConditionTemperature
))
if isinstance(obj, (
BoundaryCondition, LateralContribution, WeatherParameters
)):
temporal = bool(obj.header) and obj.header[0] == 'time'
if temporal:
for point in obj._data:
if id(point) not in seen:
changes.append((point, float(point[0]) + offset))
seen.add(id(point))
river._data_traversal(modifier=collect)
for point, value in changes:
# This is a global change of origin, not a scenario-specific edit.
point[0] = value
def shift_database_times(connection, offset):
tables = [
(
'boundary_condition_data', 'boundary_condition',
'bc', BoundaryCondition
),
(
'lateral_contribution_data', 'lateral_contribution',
'lc', LateralContribution
),
(
'weather_parameters_data', 'weather_parameters',
'wp', WeatherParameters
),
('boundary_condition_data_adists', None, None, None),
('lateral_contribution_data_adists', None, None, None),
('boundary_condition_data_temperature', None, None, None),
]
for table, parent, fk, model in tables:
if model is None:
rows = connection.execute(
f'SELECT rowid, data0 FROM {table}'
).fetchall()
else:
rows = []
for kind, in connection.execute(
f'SELECT DISTINCT type FROM {parent}'
):
headers = model._get_ctor_from_type(kind)().header
if not headers or headers[0] != 'time':
continue
rows.extend(connection.execute(
f'SELECT d.rowid, d.data0 FROM {table} d JOIN {parent} p '
f'ON p.pamhyr_id=d.{fk} AND p.scenario=d.scenario '
f'WHERE p.type=?',
(kind,)
).fetchall())
connection.executemany(
f'UPDATE {table} SET data0=? WHERE rowid=?',
[(float(value) + offset, rowid) for rowid, value in rows]
)
+3 -31
View File
@@ -1,5 +1,5 @@
# PamhyrDB.py -- Pamhyr abstract model database classes # PamhyrDB.py -- Pamhyr abstract model database classes
# Copyright (C) 2023-2026 INRAE # Copyright (C) 2023-2025 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
@@ -26,8 +26,6 @@ from functools import reduce
from SQL import SQL from SQL import SQL
from Model.Except import NotImplementedMethodeError from Model.Except import NotImplementedMethodeError
from tools import trace
from Model.Tools.PamhyrID import PamhyrID from Model.Tools.PamhyrID import PamhyrID
logger = logging.getLogger() logger = logging.getLogger()
@@ -50,9 +48,11 @@ class SQLModel(SQL):
self._cur = self._db.cursor() self._cur = self._db.cursor()
if is_new: if is_new:
logger.info("Create database")
self._create() # Create db self._create() # Create db
# self._save() # Save # self._save() # Save
else: else:
logger.info("Update database")
self._update() # Update db scheme if necessary self._update() # Update db scheme if necessary
# self._load() # Load data # self._load() # Load data
@@ -185,13 +185,6 @@ class SQLSubModel(PamhyrID):
else: else:
self._owner_scenario = owner_scenario self._owner_scenario = owner_scenario
def __str__(self):
t = type(self)
return (
f"'{t.__module__}.{t.__name__}" +
f"@{self._pamhyr_id}.{self._owner_scenario}'"
)
def must_be_saved(self): def must_be_saved(self):
"""Return True if this object MUST be save in the save file. """Return True if this object MUST be save in the save file.
@@ -209,8 +202,6 @@ class SQLSubModel(PamhyrID):
""" """
if self._status is None: if self._status is None:
return return
if self._status.is_loading():
return
self._owner_scenario = self._status.scenario_id self._owner_scenario = self._status.scenario_id
self._status.modified() self._status.modified()
@@ -227,8 +218,6 @@ class SQLSubModel(PamhyrID):
""" """
if self._status is None: if self._status is None:
return return
if self._status.is_loading():
return
self._owner_scenario = self._status.scenario_id self._owner_scenario = self._status.scenario_id
self._status.modified() self._status.modified()
@@ -308,15 +297,6 @@ class SQLSubModel(PamhyrID):
""" """
raise NotImplementedMethodeError(cls, cls._db_update) raise NotImplementedMethodeError(cls, cls._db_update)
@classmethod
def is_table_exists(cls, execute, table):
return any(
execute(
"SELECT name FROM sqlite_master " +
f"WHERE type='table' AND name='{table}'"
)
)
@classmethod @classmethod
def _db_update_to_0_2_0_set_node_pid(cls, execute, table, nodes): def _db_update_to_0_2_0_set_node_pid(cls, execute, table, nodes):
els = execute( els = execute(
@@ -331,14 +311,6 @@ class SQLSubModel(PamhyrID):
if node_id == -1: if node_id == -1:
continue continue
if node_id not in nodes:
# ⚠️ cas important : probablement déjà migré
logger.warning(
f"node_id {node_id} not in nodes " +
"→ probably already migrated"
)
continue
execute( execute(
f"UPDATE {table} " + f"UPDATE {table} " +
f"SET node = {nodes[node_id]} " + f"SET node = {nodes[node_id]} " +
+1 -4
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-2026 INRAE # Copyright (C) 2023-2025 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,9 +97,6 @@ class PamhyrModelDict(SQLSubModel):
Nothing Nothing
""" """
if predicate(self, data):
modifier(self, data)
for key in self._dict: for key in self._dict:
self._dict[key]\ self._dict[key]\
._data_traversal(predicate, modifier, data) ._data_traversal(predicate, modifier, data)
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff. Show more