Compare commits

..
138 Commits
Author SHA1 Message Date
Dylan Jeannin d2f03415c8 Model: micropatch to ensure all data is included in the table model after the merge 2026-10-08 17:14:14 +02:00
Dylan Jeannin 69371b5949 Result window: prevent duplicate result reaches and inclde last profile in water limits 2026-10-08 16:27:04 +02:00
Dylan Jeannin bbd8f7f832 Merge branch 'scenario-dev-pa' into dev_dylan 2026-10-08 16:12:12 +02:00
Dylan Jeannin 17792ec995 Plot widget: Fix plot selection crash on mouse wheel events 2026-10-06 11:16:22 +02:00
Dylan Jeannin 40b48a2080 Merge branch 'master' into dev_dylan 2026-10-05 09:55:57 +02:00
parouby 7eb39e5896 git: Update 'full-requirement.txt'. 2026-10-01 09:30:34 +02:00
parouby 28cc88b4b8 Doc: Wiki: En: Add ensemble images. 2026-09-30 16:56:00 +02:00
parouby c9925cd03c tests_cases: Add Saar version with ensemble. 2026-09-30 16:54:04 +02:00
parouby 40f9465929 Pamhyr2: PEP8 minor fix. 2026-09-29 17:17:02 +02:00
parouby 359805b713 Ensemble: Edit: Add message box for invalid parameter. 2026-09-29 16:24:25 +02:00
parouby 31565b9170 Ensemble: Distribution: Add parameters checkers. 2026-09-29 16:16:21 +02:00
parouby 9c2a0541df Pamhyr2: Fix PEP 8. 2026-09-25 16:51:04 +02:00
parouby 38d7d587fa Ensemble: Fix widget double spin box min and max. 2026-09-25 16:17:42 +02:00
parouby f19ab9455a Ensemble: Edit: Add Binomial distribution and minor change. 2026-09-25 15:16:32 +02:00
parouby cadffe40d6 Ensemble: Edit: Fix crash at change 1 params distribution to 2 params. 2026-09-25 14:58:50 +02:00
parouby 5a195e2822 Ensemble: Minor indent fix. 2026-09-25 11:51:09 +02:00
parouby 63592b91a9 Ensemble: Add Export/Import method with json files. 2026-09-25 11:46:37 +02:00
parouby 88345c58a7 Ensemble: Fix editable status at ensemble creation. 2026-09-24 16:04:42 +02:00
parouby 7124b2b61c Ensemble: Results: Setup export methods. 2026-09-24 15:14:35 +02:00
parouby f93e6a7d7c Results: Refactoring '_export_time' method. 2026-09-24 11:24:52 +02:00
parouby 5d46401b85 Results: Refactoring '_export_rk' methode. 2026-09-24 10:30:16 +02:00
parouby 97867080d9 Ensemble: Add undo command to ensemble edition. 2026-09-23 16:28:30 +02:00
parouby 93a4b44143 Ensemble: Results: Add standard derivation to hydrogram plot. 2026-09-23 16:07:31 +02:00
parouby 5e3e5628d2 Results: Fix river loading reachs creation. 2026-09-23 14:27:59 +02:00
Theophile Terraz bddaad0193 debug mesher 2026-09-23 09:28:42 +02:00
parouby 4a6349cd9c Merge branch 'scenario-dev-pa' of gitlab.com:pamhyr/pamhyr2 into scenario-dev-pa 2026-09-22 18:49:57 +02:00
parouby e9c998fec8 Ensemble: Results: Add seed and number_of_run to DB (0.2.9). 2026-09-22 18:48:37 +02:00
parouby 290b8918fc Ensemble: Fix context menu if right click on non current scenario. 2026-09-22 18:20:02 +02:00
parouby 0e6de5d0f0 Results: PlotRKC: Fix bedload evolution display. 2026-09-22 18:01:12 +02:00
parouby 98d30a326e Ensemble: Edit: Display current selected target data. 2026-09-22 16:39:55 +02:00
parouby 5f64499984 Ensemble: Results: Remove custom plot. 2026-09-22 15:43:00 +02:00
parouby 6e32c740ae Doc: Dev: Add text for glab installation. 2026-09-22 15:41:54 +02:00
Theophile Terraz e0721715d0 update matplotlib version in requirements.txt 2026-09-22 14:59:25 +02:00
parouby 10edc00973 Pamhyr2: Fix PEP8. 2026-09-18 16:52:40 +02:00
parouby f8829ce82c View: Tools: Tree: Minor change. 2026-09-18 16:44:26 +02:00
parouby ac91b712a3 Ensemble: Switch to edit dialog. 2026-09-18 16:34:29 +02:00
parouby 4bccbbac33 Ensemble: Select current tree item at dialog open. 2026-09-18 16:27:11 +02:00
parouby 5c8b7290ba Ensemble: Add stricklers select from tree view. 2026-09-18 16:06:51 +02:00
parouby 288713ce5b View: Tools: Add tree model. 2026-09-18 15:51:09 +02:00
parouby ff3b5a320c Ensemble: Edit: Add parameters widgets. 2026-09-18 10:32:45 +02:00
Dylan Jeannin 49eb28056e Mage: fix missing $ markers in VAR rules and separate exported check valve entries 2026-09-18 03:10:21 +02:00
Dylan Jeannin ddb682dff6 Mage: fix missing $ markers in VAR rules and separate exported check valve entries 2026-09-18 03:09:30 +02:00
parouby a2e0f6a517 Ensemble: Add base of edit dialog. 2026-09-17 16:07:46 +02:00
parouby 7356e5dd30 Ensemble: Create ensemble scenario from current scenario. 2026-09-17 10:02:01 +02:00
parouby 933c6c1952 Ensemble: PlotRKC: Add standard derivation. 2026-09-16 16:10:46 +02:00
parouby d8fd4488e2 Ensemble: Add standard derivation computation. 2026-09-16 15:24:00 +02:00
Dylan Jeannin 89c6e8bb29 Fix pep8 2026-09-16 13:56:51 +02:00
Dylan Jeannin d4c596a6fe Time/date: preserve calendar dates on start date changes + fix solver time exports with older format 2026-09-16 13:34:58 +02:00
parouby fef2a2e979 Pamhyr: Fix pep8. 2026-09-15 17:33:51 +02:00
parouby f001dffb68 Ensemble: PlotRKC: Minor change. 2026-09-15 17:29:37 +02:00
parouby 2818644b7f Ensemble: Distribution: Add default values for each distribution. 2026-09-15 16:57:00 +02:00
parouby 86d93fd702 Ensemble: Ui: Minor change. 2026-09-15 16:47:26 +02:00
parouby 593cc0c91b Ensemble: Mage: Fix iterative mean computation. 2026-09-15 16:32:27 +02:00
Dylan Jeannin d15fa684b7 Time/date format: warning message for users when they change format to another start date 2026-09-15 11:34:20 +02:00
Dylan Jeannin 9879094661 Time/date format: back to relative timestamps storage 2026-09-15 11:33:04 +02:00
Dylan Jeannin 5ce5478bf4 Interpolated profiles: fix undo/redo misconnection of undo_stack between geometry and profile window, adding a new data in the profile model, common to both window 2026-09-11 18:48:45 +02:00
Dylan Jeannin 5fb36e7fbe BoundaryConditions: fix index error in deletion for boundary cond. 2026-09-11 16:51:37 +02:00
Dylan Jeannin 0dbea26a2e BoundaryConditions: fix index error in deletion for boundary cond. 2026-09-11 16:50:47 +02:00
Dylan Jeannin b0e0dc75f1 Fix pep8 2026-09-11 14:17:04 +02:00
Dylan Jeannin 318f6a4668 Geometry: undo/redo restores interpolated data in the model 2026-09-11 14:10:55 +02:00
Dylan Jeannin fdb5cd23e7 Translation: correct albedo display to water albedo, + update translation 2026-09-11 10:12:59 +02:00
Dylan Jeannin e064c1aee5 Interpolated profiles: modify an interpolated profile, or something relative to it in the model, will no longer considerate this profile as an interpolated one 2026-09-10 19:17:23 +02:00
parouby 32a316589b Ensemble: Rename 'run_number' to 'number_of_run' and fix seed spin box max value. 2026-09-10 16:49:11 +02:00
parouby d4bc9bdf75 Ensemble: Set custom seed. 2026-09-10 16:39:38 +02:00
parouby 3325ba935b Ensemble: Run: Add function combobox and seed spin box. 2026-09-10 16:17:57 +02:00
parouby 345f24996f Ensemble: Rename function to disctribution. 2026-09-10 15:56:29 +02:00
Dylan Jeannin e0bc481ae2 Geometry: Orange color instead of red for interpolated profiles in the graph in geometry window 2026-09-10 15:18:59 +02:00
parouby 4040b2456c Ensemble: Function: Add Poisson law and 1 params dialog. 2026-09-10 15:09:24 +02:00
parouby 8e77d31a5a Ensemble: Add normal low and generalise minmaxdialog. 2026-09-10 14:57:48 +02:00
Dylan Jeannin 081e6e7322 Geometry:toggle interpolated profiles visibility in geometry table and plots 2026-09-10 13:56:46 +02:00
parouby 6e029beafc Ensemble: Function: Rename get_sample to random and use np.random. 2026-09-10 11:40:57 +02:00
parouby f5a7cd5984 Ensemble: PlotH: Add ensemble ploth. 2026-09-10 11:14:03 +02:00
parouby d4d5e26a2b Ensemble: Results: Rename Z to z_mean and dont keep individual result. 2026-09-10 10:44:03 +02:00
parouby 92990948ad Ensemble: PlotRKC: Fix table selection plot update. 2026-09-10 10:31:30 +02:00
Dylan Jeannin a0d8b9ea43 Split reach : disabled endpoint and struture-linked profiles in reach split selector, instead of deleting them from the list 2026-09-09 13:54:01 +02:00
Dylan Jeannin d97620ad56 Replace missing reach warning with a reach selection dropdown list on opening geometry, frictions, sediment layers, and initial conditions 2026-09-09 10:31:22 +02:00
parouby bf6d40f398 Ensemble: Results: PlotRKC: Add legend. 2026-09-08 17:50:51 +02:00
Dylan Jeannin 6d4dc3a22b Merge branch 'new_design_main_page' into dev_dylan 2026-09-08 17:31:54 +02:00
Dylan Jeannin 74d23386a7 Network: cleanest version of drag clic to move on the graph + network graph view only on the main tab (no more interaction on mouse hover) + graph on main tab 2026-09-08 17:31:01 +02:00
parouby 03299db99a Ensemble: Results: PlotRKC: Fix table access. 2026-09-08 16:33:29 +02:00
parouby a0886d900e Results: PlotRKC: Minor change to optimize drawing. 2026-09-08 16:04:19 +02:00
Dylan Jeannin 9f46bcd853 View main window: fix refresh network preview when selecting a reach 2026-09-08 15:42:04 +02:00
parouby 97fe038d9b Ensemble: Fix open results. 2026-09-08 15:08:11 +02:00
Dylan Jeannin bcc9dd1a0d View main window: keep zoom on the geographical graph after a change in the geometry 2026-09-08 14:02:53 +02:00
Dylan Jeannin fc40552d68 View main window: add a splitter to resize both network graph and geometry graph 2026-09-08 13:28:51 +02:00
Dylan Jeannin f41050fe5c Design main window: UI improvement for the graph tab, QBox, titles... 2026-09-07 17:37:52 +02:00
Dylan Jeannin 97f487c0af Design main window: Network graph is now connected with the edition window to refresh after every change 2026-09-07 17:06:24 +02:00
Dylan Jeannin 499e16975f Design main window: New tab with network of the study 2026-09-07 16:31:35 +02:00
Dylan Jeannin 75f3c07cd9 Solver: export the relative time for solver files, in the case of a study in date mode, which is the date mentionned in the data minus the study start date 2026-09-07 15:35:27 +02:00
Dylan Jeannin 0a0c00d877 Results with disabled profiles: fix a bug happening when we are looking at results, and we modify enable/disable some profiles at the same time. Now the plot uses the profiles stored with the results, and no more in the model 2026-09-07 13:48:17 +02:00
Dylan Jeannin 8d2c18b6af Timeseries: fix problem with copy/paste data in timeseries for the date mode 2026-09-07 12:10:45 +02:00
Dylan Jeannin 2d2f83436c Date/time: now save global timestamp in the model, instead of relative 2026-09-07 11:17:04 +02:00
Dylan Jeannin 71914fdf57 Merge branch 'master' of https://gitlab.com/pamhyr/pamhyr2 2026-09-06 23:15:24 +02:00
parouby 58e3da8942 Ensemble: Add base for RKC ensemble plot. 2026-09-04 17:51:52 +02:00
Dylan Jeannin 2d7409debb Export csv results: fix export flowrate as a function of time, on a study without sediments transport 2026-09-04 16:45:59 +02:00
Dylan Jeannin beece68498 SolverParameters: new combobox instead of index in the value field for critical shields, shields correction and solid capacity 2026-09-04 11:40:04 +02:00
parouby b7c3793a66 Geometry: Point: Fix index creation. 2026-09-04 11:31:23 +02:00
parouby 92fb653676 Geometry: Point: Try to create index at each update. 2026-09-04 11:30:30 +02:00
parouby 2498466a51 Ensemble: Results: Fix runs table. 2026-09-04 10:31:04 +02:00
parouby ef36eba039 Ensemble: Results: Add method to open ensemble results window. 2026-09-04 10:11:46 +02:00
Dylan Jeannin d8e9d786d9 LateralContributionsAdisTS: correctly highlight the profiles in the interval + refacto function name 2026-09-03 16:29:32 +02:00
parouby c40e28ed0f Ensemble: Results: Add ui file for results window. 2026-09-03 16:21:53 +02:00
parouby b21846a513 Ensemble: Results: Add base of 'ResultsEnsembleWindow' obj. 2026-09-03 16:17:49 +02:00
Dylan Jeannin 7ef305e724 Pollutants: ref to pol_name in the title of the lateral_contribution_pollutant 2026-09-03 13:00:53 +02:00
parouby 3532f75c2b Solver: Mage: Minor change. 2026-09-02 16:21:31 +02:00
parouby 6c59919972 Pep8: Fix. 2026-09-01 11:36:35 +02:00
parouby 77d56b8bec Ensemble: Keep results nim, mean and max for each run. 2026-09-01 11:34:37 +02:00
parouby 7ef7bc13dc Ensemble: Continue to adapt results. 2026-08-28 16:15:59 +02:00
parouby a1d85274b9 Ensemble: Put ensemble results data into results obj. 2026-08-28 11:10:29 +02:00
parouby e7b9bc92a2 Ensemble: Run: Log variation. 2026-08-27 10:10:08 +02:00
parouby 7d8e414fe2 Ensemble: Fix missing requirements 'psutil'. 2026-08-27 09:48:12 +02:00
parouby 476bcc9c55 Ensemble: Made it compatible with other solver than mage8. 2026-08-27 09:44:03 +02:00
parouby ae006f52f4 Ensemble: Table: Fix double assignation for data type and target. 2026-08-26 16:04:58 +02:00
parouby 3583a93ae0 Ensemble: Set ensemble as read only study (except for ensemble). 2026-08-25 17:07:34 +02:00
parouby 212a00a43c Ensemble: Fix missing file. 2026-08-25 14:42:41 +02:00
parouby e5dbba362a Debug: Add memory usage to debug timer window. 2026-08-21 16:55:50 +02:00
parouby bdc09addfa Ensemble: Fix results reading. 2026-08-21 16:55:17 +02:00
parouby 51b3987a4a Mage: Ensemble: Add bin reading method. 2026-08-21 15:28:17 +02:00
parouby e7290fa81b Ensemble: Fix ensemble study meta data save. 2026-08-21 15:27:17 +02:00
parouby b5432498c6 Ensemble: Add sequential runs with RUG variation into mage export. 2026-08-20 16:00:40 +02:00
parouby 807592eab2 Ensemble: Rename 'range' to 'params'. 2026-08-14 16:33:41 +02:00
parouby 394c0779fc Ensemble: Add min max dialog for range. 2026-08-14 16:30:27 +02:00
parouby 5b3f777100 Ensemble: Minor change and add target data to table. 2026-08-14 09:27:39 +02:00
parouby 8b36905842 Ensemble: Fix some view/model interations. 2026-08-14 09:27:36 +02:00
parouby b203e8e062 Ensemble: Fix editing ensemble list. 2026-08-12 16:46:13 +02:00
parouby 4d4df1bde2 Ensemble: Function: Randomize Uniform sample. 2026-08-12 15:19:12 +02:00
parouby 876780a349 Ensembles: Open Ensemble windows from scenarios. 2026-08-12 14:50:58 +02:00
parouby 3b83377f3c Ensemble: Draw ensemble scenario as rect and minor fix. 2026-08-07 16:28:13 +02:00
parouby e64c45b621 Ensemble: Create new ensemble scenario. 2026-08-07 15:18:22 +02:00
parouby 70dae9ab1b Ensemble: Prepare view with add base classes. 2026-08-06 15:33:53 +02:00
parouby 0bb3c5bc65 Ensemble: Minor change to prepare view integration. 2026-07-31 16:19:13 +02:00
parouby 54f5c7c548 Ensemble: Switch data pid to data ref, and minor change. 2026-07-30 11:14:33 +02:00
parouby 56217186ff Ensemble: Fix save/load methode. 2026-07-29 16:31:13 +02:00
parouby 82fb2bcc48 Ensemble: Change function to id and name, minor change and fix. 2026-07-29 15:19:36 +02:00
parouby 6dc42b637a Ensemble: Function: Some minor fix and add it to study. 2026-07-24 11:45:39 +02:00
parouby e6d12713e3 Ensemble: Minor change. 2026-07-24 10:04:15 +02:00
parouby 9456cf32fc Ensemble: Function: Add base classes. 2026-07-24 10:02:25 +02:00
parouby 9b1fb4e7d2 Ensemble: Add ensemble base classes. 2026-07-23 09:51:17 +02:00
108 changed files with 6892 additions and 910 deletions

No files matched your search

+6
View File
@@ -1069,6 +1069,12 @@ gitlab-ci.
sudo python3 -m pip install pyinstaller
#+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}}}
The ci-runner environment for Wine need at least wine version 8, let
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+2 -1
View File
@@ -4,7 +4,7 @@ PyQt5-sip==12.18.0
PyQtWebEngine==5.15.6
QScintilla>=2.14.1
pyqtgraph>=0.12.1
matplotlib==3.8.3
matplotlib==3.10.8
numpy>=1.24.2
colorama>=0.4.3
pyinstaller>=5.11.0
@@ -14,3 +14,4 @@ platformdirs>=4.2.0
pyshp>=2.3.1
rasterio==1.3.11
#fortranformat==2.0.3
psutil>=7.2.2
+1 -1
View File
@@ -20,4 +20,4 @@
"python-pyqtgraph" "python-qscintilla"
"python-numpy@1" "python-shapely" "python-pyshp"
"python-matplotlib" "python-colorama"
"python-rasterio"))
"python-rasterio" "python-psutil"))
+2 -2
View File
@@ -4,7 +4,7 @@ PyQt5-sip==12.18.0
#PyQtWebEngine==5.15.6
QScintilla>=2.14.1
pyqtgraph>=0.12.1
matplotlib==3.8.3
matplotlib==3.10.8
numpy>=1.24.2
colorama>=0.4.3
pyinstaller>=5.11.0
@@ -14,4 +14,4 @@ platformdirs>=4.2.0
pyshp>=2.3.1
rasterio==1.3.11
#fortranformat==2.0.3
psutil>=7.2.2
+97 -61
View File
@@ -172,8 +172,8 @@ class InternalMeshing(AMeshingTool):
ltot1 = 0.0
ltot2 = 0.0
alpha = []
beta = []
alpha = [0.0]
beta = [0.0]
# remove names
sect2.points[start2].name = ''
sect2.points[end2].name = ''
@@ -196,67 +196,38 @@ class InternalMeshing(AMeshingTool):
else: # regular case
alpha = list(map(lambda x: x/ltot1, alpha)) # target ratios
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]
# loop on the points to insert
for k in range(len1-len2):
trouve = False
for j0 in range(len2):
if trouve:
break
undeplus = True
for j1 in range(j0, len2):
if undeplus:
if beta[j1] <= alpha[j1]:
undeplus = False
break
if undeplus:
if j0 == len2:
trouve = True
else:
j1 = j0+1
if beta[j0]-alpha[j0] > alpha[j1]-beta[j0]:
trouve = True
match = self._match_indices(alpha, beta)
if trouve:
len2 += 1
for i in reversed(range(j0, len2)):
beta[i+1] = beta[i]
if (beta[j0] == beta[j0-1]):
# rien
pass
else:
ratio = (alpha[j0] - beta[j0-1]) \
/ (beta[j0] - beta[j0-1])
if ratio < 0.0:
logger.warning(f"ratio négatif {ratio}")
# on double le point a gauche
p = sect2.point(start2+j0-1).copy()
sect2.insert_point(start2+j0-1, p)
sect2.point(start2+j0-1).name = ''
beta[j0] = beta[j0-1]
else:
p = sect2.point(start2+j0).copy()
sect2.insert_point(start2+j0, p)
sect2.point(start2+j0+1).name = ''
sect2.point(start2+j0+1).x = \
(1.0-ratio) * sect2.point(start2+j0).x + \
ratio * sect2.point(start2+j0+2).x
sect2.point(start2+j0+1).y = \
(1.0-ratio) * sect2.point(start2+j0).y + \
ratio * sect2.point(start2+j0+2).y
sect2.point(start2+j0+1).z = \
(1.0-ratio) * sect2.point(start2+j0).z + \
ratio * sect2.point(start2+j0+2).z
beta[j0] = (1.-ratio)*beta[j0-1] \
+ ratio*beta[j0+1]
insertions_per_segment = [] # liste de (index, [points])
for j in range(len2):
i_start = match[j]
i_end = match[j + 1]
q_start = sect2.point(start2 + j)
q_end = sect2.point(start2 + j + 1)
new_points = []
if i_end - i_start > 1:
u_start, u_end = alpha[i_start], alpha[i_end]
span = u_end - u_start
for k in range(i_start + 1, i_end):
ratio = (alpha[k] - u_start) / span # dans [0,1]
p = q_start.copy()
p.name = ''
p.x = (1.0-ratio) * q_start.x + ratio * q_end.x
p.y = (1.0-ratio) * q_start.y + ratio * q_end.y
p.z = (1.0-ratio) * q_start.z + ratio * q_end.z
new_points.append(p)
# à insérer après l'indice j
insertions_per_segment.append((j, new_points))
# Insertion en ordre décroissant de j
# pour ne pas perturber les indices
for j, new_points in reversed(insertions_per_segment):
for offset, p in enumerate(new_points):
sect2.insert_point(start2 + j + 1 + offset, p)
len2 += 1
if len1 > len2:
for i in range(len1 - len2):
p = sect2.point(start2+len2).copy()
sect2.insert_point(start2+len2, p)
sect2.point(start2+len2).name = ''
if tag1 != "un":
sect2.point(start2).name = tag1
if tag2 != "np":
@@ -264,6 +235,71 @@ class InternalMeshing(AMeshingTool):
sect2.modified()
def _match_indices(self, u_ref, v_target):
# appariement optimal (ordre croissant) par programmation dynamique
"""
Trouve, pour chaque v_target[j], un indice i_j dans u_ref tel que
i_0 < i_1 < ... < i_{m-1} et
Sum |u_ref[i_j] - v_target[j]| soit minimal.
Retourne la liste des indices i_j (longueur m),
avec i_0 = 0 et i_{m-1} = n-1 imposés
(les extrémités se correspondent forcément).
"""
n = len(u_ref)
m = len(v_target)
# Les extrémités sont fixées.
fixed_first, fixed_last = 0, n - 1
# DP sur les indices disponibles entre fixed_first+1 et fixed_last-1
# dp[j][i] = coût minimal pour apparier v_target[1..j] aux indices
# se terminant à l'indice i (i croissant strict), j de 1 à m-2.
if m == 2:
return [fixed_first, fixed_last]
# indices internes possibles
candidates = list(range(fixed_first + 1, fixed_last))
inner_targets = v_target[1:-1] # cibles internes (j = 1..m-2)
INF = float("inf")
# dp[j] : dict indice_candidat -> (coût_min, provenance)
dp = [{} for _ in range(len(inner_targets))]
for j, vt in enumerate(inner_targets):
for idx_pos, i in enumerate(candidates):
cost_here = abs(u_ref[i] - vt)
if j == 0:
# doit être > fixed_first
dp[j][i] = (cost_here, fixed_first)
else:
best = (INF, None)
for i_prev in candidates[:idx_pos]:
if i_prev in dp[j - 1]:
c = dp[j - 1][i_prev][0] + cost_here
if c < best[0]:
best = (c, i_prev)
if best[1] is not None:
dp[j][i] = best
# Reconstruction : on part du dernier niveau, on choisit le
# meilleur indice tel que la suite jusqu'à fixed_last
# reste croissante et valide.
last_level = dp[-1]
valid = {i: c for i, c in last_level.items() if i < fixed_last}
if not valid:
raise RuntimeError("Alignement impossible avec ces données.")
best_i = min(valid, key=lambda i: valid[i][0])
chain = [best_i]
for j in range(len(inner_targets) - 1, 0, -1):
prev_i = dp[j][chain[-1]][1]
chain.append(prev_i)
chain.reverse()
return [fixed_first] + chain + [fixed_last]
def update_rk(self, reach, begin_rk, end_rk,
step: float = 50,
limites=[-1, -1],
@@ -531,7 +531,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
new_0 = self._types[0](data[0].replace(",", "."))
new_1 = self._types[1](data[1].replace(",", "."))
return (new_0, new_1)
return Data(new_0, new_1, types=self._types, status=self._status)
def add(self, index: int):
value = Data(
@@ -543,7 +543,10 @@ class BoundaryConditionAdisTS(SQLSubModel):
return value
def insert(self, index: int, value):
self._data.insert(index, value)
if value in self._data:
value.set_as_not_deleted()
else:
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
@@ -419,7 +419,7 @@ class BoundaryConditionTemperature(SQLSubModel):
new_0 = self._types[0](data[0].replace(",", "."))
new_1 = self._types[1](data[1].replace(",", "."))
return (new_0, new_1)
return Data(new_0, new_1, types=self._types, status=self._status)
def add(self, index: int):
value = Data(
@@ -431,7 +431,10 @@ class BoundaryConditionTemperature(SQLSubModel):
return value
def insert(self, index: int, value):
self._data.insert(index, value)
if value in self._data:
value.set_as_not_deleted()
else:
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
+307
View File
@@ -0,0 +1,307 @@
# 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
@@ -0,0 +1,62 @@
# 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
@@ -0,0 +1,279 @@
# 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
@@ -0,0 +1,62 @@
# 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
+3
View File
@@ -55,8 +55,11 @@ class Point(SQLSubModel):
@name.setter
def name(self, name):
changed = self._name != name
self._name = name
self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
def is_named(self):
"""
+14 -6
View File
@@ -95,12 +95,11 @@ class PointXYZ(Point):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and minor == "2":
if int(release) < 5:
execute(
"CREATE INDEX idx_point " +
"ON geometry_pointXYZ(profile, scenario);"
)
# 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)
@@ -329,8 +328,11 @@ class PointXYZ(Point):
@x.setter
def x(self, value):
changed = self._x != float(value)
self._x = float(value)
self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
@property
def y(self):
@@ -338,8 +340,11 @@ class PointXYZ(Point):
@y.setter
def y(self, value):
changed = self._y != float(value)
self._y = float(value)
self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
@property
def z(self):
@@ -347,8 +352,11 @@ class PointXYZ(Point):
@z.setter
def z(self, value):
changed = self._z != float(value)
self._z = float(value)
self.modified()
if changed and self._profile is not None:
self._profile.content_modified()
def shift(self, x, y, z):
self.x += x
+56 -12
View File
@@ -47,6 +47,8 @@ class Profile(object):
self._sl = None
self._points: List[Point] = []
self._modification_count = 0
self._old_interpolated = ""
self._profile_type = _type
@@ -160,8 +162,13 @@ class Profile(object):
@rk.setter
def rk(self, value: float):
self._rk = float(value)
self.modified()
value = float(value)
changed = value != self._rk
self._rk = value
if changed:
self.content_modified()
else:
self.modified()
@property
def name(self):
@@ -173,8 +180,13 @@ class Profile(object):
@name.setter
def name(self, value: str):
self._name = value.strip()
self.modified()
value = value.strip()
changed = value != self._name
self._name = value
if changed:
self.content_modified()
else:
self.modified()
@property
def sl(self):
@@ -217,6 +229,38 @@ class Profile(object):
return [point for point in self.points
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):
"""Insert point at index.
@@ -232,7 +276,7 @@ class Profile(object):
else:
self._points.insert(index, point)
self.modified()
self.content_modified()
def delete_i(self, indexes: list):
list(
@@ -245,7 +289,7 @@ class Profile(object):
)
)
self.modified()
self.content_modified()
def delete_points(self, points):
"""Delete some elements in profile list
@@ -262,7 +306,7 @@ class Profile(object):
points
)
)
self.modified()
self.content_modified()
# Move
@@ -272,7 +316,7 @@ class Profile(object):
p = self._points
p[index], p[next] = p[next], p[index]
self.modified()
self.content_modified()
def move_down_point(self, index: int):
if index >= 0:
@@ -280,7 +324,7 @@ class Profile(object):
p = self._points
p[index], p[prev] = p[prev], p[index]
self.modified()
self.content_modified()
# Sort
@@ -297,7 +341,7 @@ class Profile(object):
key=predicate,
reverse=is_reversed
)
self.modified()
self.content_modified()
@timer
def sort_with_indexes(self, indexes: list):
@@ -313,12 +357,12 @@ class Profile(object):
)
)
)
self.modified()
self.content_modified()
@timer
def reverse(self):
self._points.reverse()
self.modified()
self.content_modified()
# Sediment Layers
+13 -5
View File
@@ -158,7 +158,7 @@ class ProfileXYZ(Profile, SQLSubModel):
)
if major == "0" and int(minor) <= 2:
if int(release) < 9:
if int(release) < 10:
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE"
@@ -559,7 +559,7 @@ class ProfileXYZ(Profile, SQLSubModel):
)
self._points.append(pt)
self.modified()
self.content_modified()
def get_point_i(self, index: int) -> PointXYZ:
"""Get point at index.
@@ -624,7 +624,7 @@ class ProfileXYZ(Profile, SQLSubModel):
)
self.points.append(point_xyz)
self.modified()
self.content_modified()
def insert(self, index: int):
"""Insert a new point at index.
@@ -643,7 +643,7 @@ class ProfileXYZ(Profile, SQLSubModel):
point = PointXYZ(x=x, y=y, z=z, profile=self, status=self._status)
self._points.insert(index, point)
self.modified()
self.content_modified()
return point
@@ -862,6 +862,13 @@ class ProfileXYZ(Profile, SQLSubModel):
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):
"""
@@ -1172,8 +1179,9 @@ class ProfileXYZ(Profile, SQLSubModel):
interpolated=self.interpolated)
for i, k in enumerate(self.points):
p.insert_point(i, k.copy())
p.insert_point(i, k.cloned_for(p))
p._interpolated = self.interpolated
return p
def purge_aligned_points(self):
+50 -10
View File
@@ -272,9 +272,9 @@ class TableData(SQLSubModel):
if len(shape) == 2:
shape_x, shape_y = shape
elif len(shape == 1):
elif len(shape) == 1:
shape_x, = shape
elif len(shape == 3):
elif len(shape) == 3:
shape_x, shape_y, shape_z = shape
else:
logger.error(
@@ -326,14 +326,14 @@ class TableData(SQLSubModel):
if isinstance(key, tuple):
return self._data[key[0], key[1]]
self._date[key]
self._data[key]
class Results(SQLSubModel):
_sub_classes = [River, TableData, AdditionalData]
def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0"):
repertory="", name="0", is_ensemble=False):
super(Results, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
@@ -341,11 +341,14 @@ class Results(SQLSubModel):
self._name = name
self._study = study
self._river = River(self._study, self)
# Solver readers add their own reaches; ensembles need them upfront.
self._river = River(self._study, self, init=is_ensemble)
self._solver = solver
self._repertory = repertory
self._is_ensemble = is_ensemble
self._meta_data = {
# Keep results creation date
"creation_date": datetime.now(),
@@ -359,6 +362,9 @@ class Results(SQLSubModel):
if solver is not None:
self.set("solver_type", solver._type)
def is_ensemble(self):
return self._is_ensemble
@property
def date(self):
date = self._meta_data["creation_date"]
@@ -426,9 +432,11 @@ class Results(SQLSubModel):
solver_name TEXT NOT NULL,
solver_type TEXT NOT NULL,
study_revision INTEGER NOT NULL,
creation_data DATE NOT NULL,
creation_date DATE NOT NULL,
nb_timestamps INTEGER NOT NULL,
timestamps BLOB NOT NULL,
seed INTEGER,
number_of_run INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
@@ -454,6 +462,21 @@ class Results(SQLSubModel):
cls._db_create(execute)
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)
@classmethod
@@ -466,7 +489,9 @@ class Results(SQLSubModel):
table = execute(
"SELECT pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_data, nb_timestamps, timestamps, " +
"study_revision, creation_date, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " +
"scenario " +
"FROM results " +
f"WHERE scenario = {scenario.id}"
@@ -486,6 +511,8 @@ class Results(SQLSubModel):
solver_type = next(it)
revision = next(it)
creation_date = next(it)
seed = next(it)
number_of_run = next(it)
nb_timestamps = next(it)
timestamps_bytes = next(it)
owner_scenario = next(it)
@@ -496,6 +523,9 @@ class Results(SQLSubModel):
new_results.set("solver_type", solver_type)
new_results.set("study_revision", revision)
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
@@ -588,15 +618,25 @@ class Results(SQLSubModel):
ts = sorted(self.get("timestamps"))
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(
"INSERT INTO " +
"results (pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_data, " +
"study_revision, creation_date, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, solver_name, solver_type,
self._status.version, self.get("creation_date"),
len(ts), struct.pack(sf, *ts), self._owner_scenario
seed, number_of_run,
len(ts), struct.pack(sf, *ts),
self._owner_scenario
)
data["result"] = self._pamhyr_id
+6 -3
View File
@@ -403,7 +403,7 @@ class Reach(SQLSubModel):
class River(SQLSubModel):
_sub_classes = [Reach]
def __init__(self, study, parent):
def __init__(self, study, parent, init=True):
super(River, self).__init__(
id=-1, status=study.status,
owner_scenario=study.status.scenario.id
@@ -414,6 +414,9 @@ class River(SQLSubModel):
# Dict with timestamps as key
self._reachs = []
if init:
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
def __len__(self):
return len(self._reachs)
@@ -453,7 +456,7 @@ class River(SQLSubModel):
reach.update_water_limits(
timestamp,
data[ind[0]:ind[1]]
data[ind[0]:ind[1] + 1]
)
@classmethod
@@ -469,7 +472,7 @@ class River(SQLSubModel):
study = data["study"]
parent = data["parent"]
new_river = cls(study, parent)
new_river = cls(study, parent, init=False)
for reach in study.river.reachs():
data["reach"] = reach.reach
+10 -1
View File
@@ -69,6 +69,7 @@ from Model.WeatherParameters.WeatherParametersList \
import WeatherParametersList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results
logger = logging.getLogger()
@@ -487,6 +488,7 @@ class River(Graph):
BoundaryConditionsTemperatureList,
WeatherParametersList,
GeoTIFFList,
EnsembleList,
Results
]
@@ -532,6 +534,9 @@ class River(Graph):
self._geotiff = GeoTIFFList(status=self._status)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {}
@classmethod
@@ -650,6 +655,8 @@ class River(Graph):
new._geotiff = GeoTIFFList._db_load(execute, data)
new._ensembles = EnsembleList._db_load(execute, data)
return new
def _db_load_results(self, execute, data=None):
@@ -688,6 +695,8 @@ class River(Graph):
objs.append(self._geotiff)
objs.append(self._ensembles)
for solv_type in self.results:
objs.append(self.results[solv_type])
@@ -766,7 +775,7 @@ class River(Graph):
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
self._WeatherParameters,
self._geotiff,
self._geotiff, self._ensembles,
]
for solver in self._parameters:
+29 -12
View File
@@ -58,6 +58,7 @@ class Scenario(SQLSubModel):
"initial_conditions_temperature",
"initial_conditions_temperature_spec",
"weather_parameters", "weather_parameters_data",
"ensemble",
]
related_tables = tables_with_deleted_column + [
@@ -72,6 +73,7 @@ class Scenario(SQLSubModel):
name: str = "",
description: str = "",
x: int = 1000.0, y: int = 1000.0,
is_ensemble: bool = False,
revision: int = 0,
parent=None):
super(Scenario, self).__init__()
@@ -81,6 +83,7 @@ class Scenario(SQLSubModel):
self._x, self._y = x, y
self._name = name
self._description = description
self._is_ensemble = is_ensemble
self._revision = revision
self._parent = parent
@@ -103,6 +106,7 @@ class Scenario(SQLSubModel):
y REAL NOT NULL DEFAULT 1000,
name TEXT NOT NULL,
description TEXT NOT NULL,
is_ensemble BOOL DEFAULT FALSE,
revision INTEGER NOT NULL,
parent_id INTEGER REFERENCES scenario(id)
)
@@ -138,6 +142,13 @@ class Scenario(SQLSubModel):
cls._db_create(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
@classmethod
@@ -158,7 +169,8 @@ class Scenario(SQLSubModel):
scenarios = {}
table = execute(
"SELECT id, x, y, name, description, revision, parent_id " +
"SELECT id, x, y, name, description, " +
" is_ensemble, revision, parent_id " +
"FROM scenario " +
"ORDER BY id ASC"
)
@@ -170,6 +182,7 @@ class Scenario(SQLSubModel):
x, y = next(it), next(it)
name = next(it)
desc = next(it)
is_ens = next(it)
revi = next(it)
parent = next(it)
@@ -179,7 +192,9 @@ class Scenario(SQLSubModel):
new = cls(
id=id, x=x, y=y,
name=name, description=desc,
revision=revi, parent=parent
is_ensemble=is_ens,
revision=revi,
parent=parent
)
scenarios[id] = new
@@ -189,21 +204,18 @@ class Scenario(SQLSubModel):
if self.is_deleted():
return self.drop_all(execute)
parent = 'NULL'
parent = None
if self.parent is not None:
parent = self.parent._id
execute(
"INSERT OR REPLACE INTO " +
"scenario(id, x, y, name, description, revision, parent_id) " +
"VALUES (" +
f"{self._id}, " +
f"{self.x}, {self.y}, " +
f"'{self._db_format(self.name)}', " +
f"'{self._db_format(self.description)}', " +
f"{self._revision}, " +
f"{parent}" +
")"
"scenario(id, x, y, name, description, " +
" is_ensemble, revision, parent_id) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
self._id, self.x, self.y,
self.name, self.description, self._is_ensemble,
self._revision, parent
)
return True
@@ -328,6 +340,9 @@ class Scenario(SQLSubModel):
def parent(self):
return self._parent
def is_ensemble(self):
return self._is_ensemble
def set_pos(self, x, y):
self._x = x
self._y = y
@@ -345,6 +360,8 @@ class Scenario(SQLSubModel):
return self.name
if key == "description":
return self.description
if key == "is_ensemble":
return self.is_ensemble()
if key == "parent":
return self.parent
+5 -2
View File
@@ -57,8 +57,11 @@ class Scenarios(PamhyrModelDict):
return None
def new(self, parent):
new = Scenario(parent=parent)
def new(self, parent, is_ensemble=False):
new = Scenario(
parent=parent,
is_ensemble=is_ensemble
)
self.set(new._id, new)
return new
+83 -6
View File
@@ -21,7 +21,7 @@ import shutil
import logging
from datetime import datetime
from tools import timer, timestamp
from tools import timer, timestamp, parse_datetime
from functools import reduce
from Model.Tools.PamhyrListExt import PamhyrModelList
@@ -32,7 +32,9 @@ from Model.Scenario import Scenario
from Model.Status import StudyStatus
from Model.Except import NotImplementedMethodeError
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
)
@@ -50,6 +52,7 @@ class Study(SQLModel):
_sub_classes = [
Scenario,
DistributionList,
River,
]
@@ -72,6 +75,7 @@ class Study(SQLModel):
self.description = ""
# Time system
self._time_system = "time"
self._pending_time_offset = 0.0
self._date = datetime.fromtimestamp(0)
if init_new:
@@ -84,6 +88,9 @@ class Study(SQLModel):
self.scenarios = Scenarios(status=self.status)
self.scenarios[0] = s0
self.status.scenario = s0
self._ens_distributions = DistributionList(status=self.status)
self._river = River(status=self.status)
else:
self._init_db_file(filename, is_new=False)
@@ -198,6 +205,16 @@ class Study(SQLModel):
self._date = date
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
def date(self):
return self._date
@@ -217,8 +234,7 @@ class Study(SQLModel):
me.name = name
me.description = description
if date is not None:
me.use_date()
me.date = date
me.use_date(date)
return me
@classmethod
@@ -395,12 +411,19 @@ class Study(SQLModel):
lambda a, s: a or (s.parent is scenario),
new.scenarios.lst,
False
):
) or scenario.is_ensemble():
new.status.set_as_read_only()
data["scenario"] = scenario
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
new._river = River._db_load(
sql_exec, data=data
@@ -461,8 +484,38 @@ class Study(SQLModel):
)
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(
lst, data=progress
)
progress()
self.commit()
return
self._save_submodel(
[self.scenarios, self._river],
[
self.scenarios, self._ens_distributions,
self._river
],
data=progress
)
@@ -580,7 +633,8 @@ class Study(SQLModel):
data = {
"status": self.status,
"loaded_pid": set(),
"scenario": scenario
"scenario": scenario,
"ens_distributions": self._ens_distributions,
}
self.status.start_loading()
@@ -595,6 +649,9 @@ class Study(SQLModel):
self.status.stop_loading()
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
@@ -654,6 +711,7 @@ class Study(SQLModel):
self.status.scenario.y + 100)
new.name = self.status.scenario.name + " (copy)"
new.is_ensemble = self.status.scenario.is_ensemble
self.status.scenario = new
self.river._data_traversal(
@@ -665,6 +723,25 @@ class Study(SQLModel):
self.status.set_as_editable()
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
def results(self):
return self._river.results
+82
View File
@@ -0,0 +1,82 @@
"""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]
)
+7
View File
@@ -185,6 +185,13 @@ class SQLSubModel(PamhyrID):
else:
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):
"""Return True if this object MUST be save in the save file.
@@ -341,7 +341,7 @@ class WeatherParameters(SQLSubModel):
created = False
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) <= 7)):
(int(minor) == 2 and int(release) < 10)):
if not cls.is_table_exists(execute, "air_temperature"):
cls._db_create(execute)
created = True
+6 -1
View File
@@ -19,8 +19,9 @@
import os
import logging
from datetime import datetime
from datetime import datetime, timezone
from tools import timer, parse_command_line, get_version, logger_exception
from tools import parse_datetime
try:
# Installation allow Unix-like signal
@@ -158,11 +159,15 @@ class CommandLineSolver(AbstractSolver):
txt = add_file.text
txt = txt.replace("@version", get_version())
txt = txt.replace("@date", datetime.now().isoformat(sep=" "))
txt = self._prepare_additional_file_text(add_file.path, txt)
f.write(txt)
return files
def _prepare_additional_file_text(self, path, text):
return text
def export_study_description(self, study, repertory, qlog, name="0"):
files = []
+273 -11
View File
@@ -303,7 +303,7 @@ class Mage(CommandLineSolver):
# Point line
wfile.write(f"{x} {y} {z} {n} {sediment}\n")
def _export_BC(self, t, bounds, repertory, qlog, name="0"):
def _export_BC(self, study, t, bounds, repertory, qlog, name="0"):
files = []
if len(bounds) == 0:
@@ -333,7 +333,7 @@ class Mage(CommandLineSolver):
v1 = d[1]
if t in ["HYD", "LIM"]:
v0 /= 60 # Convert first column to minute
v0 /= 60 # Convert first column to minutes
f.write(f"{v0:9} {v1:9} \n")
@@ -365,10 +365,17 @@ class Mage(CommandLineSolver):
elif bound.bctype == "SL":
QSO.append(bound)
files = files + self._export_BC("AVA", AVA, repertory, qlog, name=name)
files = files + self._export_BC("HYD", HYD, repertory, qlog, name=name)
files = files + self._export_BC("LIM", LIM, repertory, qlog, name=name)
files = files + self._export_QSO(QSO, repertory, qlog, name=name)
files = files + self._export_BC(
study, "AVA", AVA, repertory, qlog, name=name
)
files = files + self._export_BC(
study, "HYD", HYD, repertory, qlog, name=name
)
files = files + self._export_BC(
study, "LIM", LIM, repertory, qlog, name=name
)
files = files + self._export_QSO(study, QSO, repertory,
qlog, name=name)
return files
@@ -422,10 +429,11 @@ class Mage(CommandLineSolver):
if d.is_deleted():
continue
time = d[0]
if lateral.lctype in ["EV"]:
f.write(f"{d[0]:10.3f}{-d[1]:10.3f}\n")
f.write(f"{time:10.3f}{-d[1]:10.3f}\n")
else:
f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n")
f.write(f"{time:10.3f}{d[1]:10.3f}\n")
def _export_RUG(self, study, repertory, qlog, name="0"):
files = []
@@ -686,6 +694,23 @@ class Mage(CommandLineSolver):
return res
def _prepare_additional_file_text(self, path, text):
if os.path.splitext(path)[1].lower() != ".var":
return text
lines = []
for line in text.splitlines(keepends=True):
stripped = line.lstrip()
fields = stripped.split()
if stripped.startswith("$"):
# MAGE only recognizes rule markers in the first column.
line = stripped
elif (not stripped.startswith("*") and len(fields) >= 2
and fields[1] == "W2Z"):
line = "$ " + stripped
lines.append(line)
return "".join(lines)
def _export_VAR(self, study, repertory, qlog, name="0"):
files = []
@@ -736,9 +761,11 @@ class Mage(CommandLineSolver):
name = bhs.name
if name == "":
name = f"HS_{bhs.id:>3}".replace(" ", "0")
else:
name = name.replace(" ", "_")
f.write(
f"${name} clapet"
f"${name} clapet\n"
)
return files
@@ -964,7 +991,7 @@ class Mage8(Mage):
return files
def _export_QSO(self, bounds, repertory, qlog, name="0"):
def _export_QSO(self, study, bounds, repertory, qlog, name="0"):
files = []
if len(bounds) == 0:
@@ -994,7 +1021,8 @@ class Mage8(Mage):
# Data
for d in bound.data:
f.write(f"{d[0]/60:10.3f}{d[1]:10.3f}\n")
time = d[0] / 60
f.write(f"{time:10.3f}{d[1]:10.3f}\n")
return files
@@ -1035,6 +1063,75 @@ class Mage8(Mage):
logger_exception(e)
return False
def export_ensemble_variation(self, study, repertory,
qlog=None, name="0",
samples=None):
self._study = study
name = study.name.replace(" ", "_")
var_samples = self._export_ensemble_RUG(
study, repertory,
qlog, name=name,
samples=samples,
)
return var_samples
def _export_ensemble_RUG(self, study, repertory,
qlog, name="0",
samples={}):
files = []
var_samples = []
for ens in samples:
if ens.target_data is None:
continue
x = next(samples[ens])
var_samples.append(x)
qlog.put(f"*** Variation " +
f"'{ens.target_data}' - {ens.data_type} x {x}")
if ens.data_type == "strickler_minor":
ens.target_data.minor = x
elif ens.data_type == "strickler_medium":
ens.target_data.medium = x
if qlog is not None:
qlog.put("Export variation of RUG file")
# Write header
with mage_file_open(os.path.join(repertory, f"{name}.RUG"), "w+") as f:
files.append(f"{name}.RUG")
edges = study.river.enable_edges()
id = 1
for edge in edges:
frictions = edge.frictions
for friction in frictions.frictions:
if friction.begin_strickler is None:
continue
if friction.is_deleted():
continue
num = f"{id:>3}"
brk = f"{friction.begin_rk:>10.3f}"
erk = f"{friction.end_rk:>10.3f}"
strickler = friction.begin_strickler
coef_1 = f"{strickler.minor:>10.3f}"
coef_2 = f"{strickler.medium:>10.3f}"
f.write(f"K{num} {brk}{erk}{coef_1}{coef_2}\n")
id += 1
return var_samples
###########
# RESULTS #
###########
@@ -1465,6 +1562,171 @@ class Mage8(Mage):
return results
@timer
def read_ensemble_bin(self, study, fname, tables, run_number, qlog=None):
logger.info(f"read_ensemble_bin: Start reading '{fname}' ...")
start = time.time()
with mage_file_open(fname, "r") as f:
def newline(): return np.fromfile(f, dtype=np.int32, count=1)
def endline(): return np.fromfile(f, dtype=np.int32, count=1)
def read_int(size): return np.fromfile(
f, dtype=np.int32, count=size)
def read_float(size): return np.fromfile(
f, dtype=np.float32, count=size)
def read_float64(size): return np.fromfile(
f, dtype=np.float64, count=size)
# Meta data (1st line)
newline()
data = read_int(3)
nb_reach = data[0]
nb_profile = data[1]
mage_version = data[2]
if mage_version <= 80:
msg = (
"Read BIN files: " +
f"Possible incompatible mage version '{mage_version}', " +
"please check your solver configuration..."
)
logger.warning(msg)
if qlog is not None:
qlog.put("[WARNING] " + msg)
endline()
# Reach information (2nd line)
newline()
_ = read_int(2*nb_reach)
endline()
# X (3rd line)
newline()
_ = read_float(nb_profile)
endline()
# Z and Y (4th line)
newline()
_ = read_float(3*nb_profile)
endline()
# Data
newline()
ts = set()
tmp_table = {}
end = False
while not end:
n = read_int(1)[0]
timestamp = read_float64(1)[0]
key = bytearray(
np.fromfile(
f, dtype=np.byte, count=1
)
).decode()
data = read_float(n)
if key not in tmp_table:
tmp_table[key] = []
tmp_table[key].append(data)
ts.add(timestamp)
endline()
end = newline().size <= 0
table = {}
for k in tmp_table:
table[k] = np.array(tmp_table[k])
self.read_ensemble_bin_agregate_data(
table, tables, run_number
)
tables["timestamps"] = ts
logger.info(f"reading time: '{time.time() - start}'")
def read_ensemble_bin_agregate_data(self, tables, res, run_number):
z = tables["Z"]
q = tables["Q"]
if "z_mean" not in res:
res["z_mean"] = z
res["q_mean"] = q
res["z_tmp_M2"] = np.zeros(z.shape)
res["q_tmp_M2"] = np.zeros(q.shape)
res["z_std_derivation"] = np.zeros(z.shape)
res["q_std_derivation"] = np.zeros(q.shape)
res["z_min"] = z
res["z_max"] = z
res["q_min"] = q
res["q_max"] = q
else:
# Get mean results for Z and Q
n = run_number
for x, txt in [(z, "z"), (q, "q")]:
t_mean = f"{txt}_mean"
t_M2 = f"{txt}_tmp_M2"
t_std_deriv = f"{txt}_std_derivation"
delta = x - res[t_mean]
res[t_mean] += delta / n
delta_2 = x - res[t_mean]
res[t_M2] += delta * delta_2
res[t_std_deriv] = np.sqrt(res[t_M2] / n)
# Z and Q min max values
res["z_min"] = np.minimum(z, res["z_min"])
res["z_max"] = np.maximum(z, res["z_max"])
res["q_min"] = np.minimum(q, res["q_min"])
res["q_max"] = np.maximum(q, res["q_max"])
@timer
def results_ensemble(self, study, repertory,
tables, run_number,
qlog=None, name=None):
self._study = study
if name is None:
name = study.name.replace(" ", "_")
fname = os.path.join(repertory, f"{name}.BIN".replace(" ", "_"))
if not os.path.isfile(fname):
logger.info(f"Result file {name}.BIN does not exist")
return None
try:
self.read_ensemble_bin(study, fname, tables, run_number, qlog)
except Exception as e:
logger.error(f"Failed to read results")
logger_exception(e)
return None
return True
def get_ensemble_empty_results(self, study, repertory, name=None):
return Results(
study=study,
solver=self,
repertory=repertory,
name=name,
is_ensemble=True,
)
class MageFake7(Mage8):
_type = "mage_fake7"
+12 -10
View File
@@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp, format_datetime
old_pamhyr_date_to_timestamp, format_datetime, parse_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -94,11 +94,12 @@ class TableModel(PamhyrTableModel):
v = v.replace(',', '.')
value = f"{float(v):.4f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
if self.time_system == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -117,7 +118,7 @@ class TableModel(PamhyrTableModel):
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
@@ -238,17 +239,18 @@ class TableModel(PamhyrTableModel):
if len(data) == 0:
return
try:
rows = [self._data.new_from_data(header, d) for d in data]
except (ValueError, TypeError, IndexError) as error:
logger.warning(error)
return False
self.layoutAboutToBeChanged.emit()
self._undo.push(
PasteCommand(
self._data, row,
list(
map(
lambda d: self._data.new_from_data(header, d),
data
)
)
rows
)
)
+8 -1
View File
@@ -18,6 +18,8 @@
import logging
from Modules import Modules
from tools import timer, trace
from numpy import sqrt
@@ -174,7 +176,7 @@ class EditBoundaryConditionWindow(PamhyrWindow):
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system,
start_date=self._study.date
study=self._study
)
table.setModel(self._table)
@@ -262,6 +264,11 @@ class EditBoundaryConditionWindow(PamhyrWindow):
self._d50sigma.set_d50(self._data.d50)
self._d50sigma.set_sigma(self._data.sigma)
def _propagated_update(self, key=Modules(0)):
if Modules.STUDY in key:
self._table.layoutChanged.emit()
self.plot.update()
def update(self):
self.plot.update()
+1 -1
View File
@@ -124,7 +124,7 @@ class DelCommand(QUndoCommand):
self._bcs.insert(self._tab, row, el)
def redo(self):
self._bcs.delete_i(self._tab, self._rows)
self._bcs.delete(self._tab, [el for _, el in self._bc])
class SortCommand(QUndoCommand):
@@ -21,6 +21,7 @@ import traceback
from datetime import date, time, datetime, timedelta
from tools import parse_datetime
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
@@ -84,11 +85,12 @@ class TableModel(PamhyrTableModel):
if self._data._types[column] == float:
value = f"{v:.4f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
if self.time_system == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -107,7 +109,7 @@ class TableModel(PamhyrTableModel):
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
@@ -169,17 +171,18 @@ class TableModel(PamhyrTableModel):
if len(data) == 0:
return
try:
rows = [self._data.new_from_data(header, d) for d in data]
except (ValueError, TypeError, IndexError) as error:
logger.warning(error)
return False
self.layoutAboutToBeChanged.emit()
self._undo.push(
PasteCommand(
self._data, row,
list(
map(
lambda d: self._data.new_from_data(header, d),
data
)
)
rows
)
)
@@ -18,6 +18,8 @@
import logging
from Modules import Modules
from tools import timer, trace
from View.Tools.PamhyrWindow import PamhyrWindow
@@ -129,7 +131,7 @@ class EditBoundaryConditionWindow(PamhyrWindow):
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system,
start_date=self._study.date
study=self._study
)
table.setModel(self._table)
@@ -162,6 +164,11 @@ class EditBoundaryConditionWindow(PamhyrWindow):
self.find(QAction, "action_sort").triggered.connect(self.sort)
self._table.dataChanged.connect(self.update)
def _propagated_update(self, key=Modules(0)):
if Modules.STUDY in key:
self._table.layoutChanged.emit()
self.plot.update()
def update(self):
self.plot.update()
self.plot.fit_to_data()
@@ -21,6 +21,7 @@ import traceback
from datetime import date, time, datetime, timedelta
from tools import parse_datetime
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
@@ -84,11 +85,12 @@ class TableModel(PamhyrTableModel):
if self._data._types[column] == float:
value = f"{v:.2f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
if self.time_system == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -107,7 +109,7 @@ class TableModel(PamhyrTableModel):
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
@@ -169,17 +171,18 @@ class TableModel(PamhyrTableModel):
if len(data) == 0:
return
try:
rows = [self._data.new_from_data(header, d) for d in data]
except (ValueError, TypeError, IndexError) as error:
logger.warning(error)
return False
self.layoutAboutToBeChanged.emit()
self._undo.push(
PasteCommand(
self._data, row,
list(
map(
lambda d: self._data.new_from_data(header, d),
data
)
)
rows
)
)
@@ -18,6 +18,8 @@
import logging
from Modules import Modules
from tools import timer, trace
from View.Tools.PamhyrWindow import PamhyrWindow
@@ -118,7 +120,7 @@ class EditBoundaryConditionWindow(PamhyrWindow):
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system,
start_date=self._study.date
study=self._study
)
table.setModel(self._table)
@@ -151,6 +153,11 @@ class EditBoundaryConditionWindow(PamhyrWindow):
self.find(QAction, "action_sort").triggered.connect(self.sort)
self._table.dataChanged.connect(self.update)
def _propagated_update(self, key=Modules(0)):
if Modules.STUDY in key:
self._table.layoutChanged.emit()
self.plot.update()
def update(self):
self.plot.update()
self.plot.fit_to_data()
+15
View File
@@ -16,6 +16,8 @@
# -*- coding: utf-8 -*-
import os
import psutil
import logging
import traceback
@@ -120,7 +122,10 @@ class TimerWindow(PamhyrWindow):
parent=parent
)
self._process = psutil.Process(os.getpid())
self.setup_table()
self.setup_label()
self.setup_alarm()
self.setup_connections()
@@ -136,6 +141,15 @@ class TimerWindow(PamhyrWindow):
data=None,
)
def setup_label(self):
label = self.find(QLabel, "label")
label.setText(
f" Memory usage:\t" +
"<b>" +
f"{self._process.memory_info().rss / (1000 * 1000):.3f}" +
"</b> MB"
)
def setup_alarm(self):
self._alarm = QTimer()
self._alarm.start(2000)
@@ -148,6 +162,7 @@ class TimerWindow(PamhyrWindow):
def update(self):
self._table.update_lst()
self.setup_label()
# def reset(self):
# self._table.reset_timers()
+64
View File
@@ -0,0 +1,64 @@
# Translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Ensembles.Translate import EnsemblesTranslate
_translate = QCoreApplication.translate
class EnsembleEditTranslate(EnsemblesTranslate):
def __init__(self):
super(EnsembleEditTranslate, self).__init__()
self._dict["binomial_n"] = _translate("Ensemble", "n")
self._dict["binomial_p"] = _translate("Ensemble", "p")
self._dict["poisson_lam"] = _translate("Ensemble", "Lam")
self._dict["checker_failed_title"] = _translate(
"Ensemble",
"Invalid parameter"
)
self._dict["checker_failed_text"] = _translate(
"Ensemble",
"This parameter is not valid for selected distribution: "
)
uniform = _translate(
"Ensemble", "(High - Low) must be non-negative."
)
self._dict["uniform_low_tips"] = uniform
self._dict["uniform_high_tips"] = uniform
self._dict["normal_scale_tips"] = _translate(
"Ensemble", "Scale must be non-negative."
)
self._dict["binomial_n_tips"] = _translate(
"Ensemble", "N must be non-negative and must be an integers."
)
self._dict["binomial_p_tips"] = _translate(
"Ensemble", "P must be >= 0 and <=1."
)
self._dict["poisson_lam_tips"] = _translate(
"Ensemble", "Lam must be >= 0."
)
+320
View File
@@ -0,0 +1,320 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow, PamhyrDialog
from View.Tools.PamhyrWidget import PamhyrWidget
from View.Tools.PamhyrTree import (
PamhyrModelItem, PamhyrModelListItem,
)
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QSettings,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QGroupBox, QTreeWidget,
)
from View.Ensembles.Edit.Translate import EnsembleEditTranslate
logger = logging.getLogger()
class StricklersCoefModelItem(PamhyrModelItem):
def get_value(self):
return (self._data, self.parent()._data)
class StricklersModelItem(PamhyrModelItem):
def __init__(self, data=None, trad=None, parent=None):
super(StricklersModelItem, self).__init__(
data=data, trad=trad, parent=parent,
)
def _setup_children(self):
for coef in ["strickler_minor", "strickler_medium"]:
self.addChild(
StricklersCoefModelItem(data=coef, parent=self)
)
def get_value(self):
return self._data
class EnsembleEditDialog(PamhyrDialog):
_pamhyr_ui = "EnsembleEditDialog"
_pamhyr_name = "Parameters"
def __init__(self, study=None, ensemble=None,
parent=None):
trad = EnsembleEditTranslate()
super(EnsembleEditDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._study = study
self._ensemble = ensemble
self._target_data = None
self._tree = self.find(QTreeWidget, "treeWidget_target_data")
self._tg_group_box = self.find(QGroupBox, "groupBox_target_data")
self._init_tree()
self._init_default_values()
self._init_connection()
def _init_tree(self):
self._tree.setColumnCount(1)
self._tree.setHeaderLabels(["Name"])
stricklers = PamhyrModelListItem(
name="Stricklers",
lst=self._study.river.stricklers,
child_item_ctor=StricklersModelItem,
parent=self._tree
)
self._target_data = (
self._ensemble.data_type,
self._ensemble.target_data
)
self._tg_group_box.setTitle(
self._format_target_data(self._target_data)
)
item = stricklers.find_item_by_value(self._target_data)
if item:
self._tree.setCurrentItem(item)
def _init_default_values(self):
self.set_line_edit_text("lineEdit_name", self._ensemble.name)
self.combobox_add_items(
"comboBox_distribution",
[self._trad["not_defined"]] +
list(
map(
lambda d: d.name,
self._study._ens_distributions.lst
)
)
)
self._params_widget = None
if self._ensemble.distribution is not None:
self.set_combobox_text("comboBox_distribution",
self._ensemble.distribution.name)
self._set_widget(self._ensemble.distribution)
def _init_connection(self):
self.find(QComboBox, "comboBox_distribution")\
.currentTextChanged.connect(self.update_distribution)
self._tree.itemClicked.connect(self._tree_item_select)
def _tree_item_select(self, item):
if type(item) is StricklersCoefModelItem:
self._target_data = item.get_value()
self._tg_group_box.setTitle(
self._format_target_data(self._target_data)
)
def _format_target_data(self, data):
return f" Selection: {str(data[1])}, {data[0]}"
def update_distribution(self, txt):
# txt = self.get_combobox_text("comboBox_distribution")
distribution = next(
filter(
lambda d: d.name == txt,
self._study._ens_distributions.lst
),
None
)
self._set_widget(distribution)
def _set_widget(self, distribution):
box = self.find(QGroupBox, "groupBox_params")
vbox = self.find(QVBoxLayout, "verticalLayout_params")
if self._params_widget is not None:
vbox.removeWidget(self._params_widget)
self._params_widget.setParent(None)
if distribution is None:
return
size = len(distribution._labels)
if size == 1:
self._params_widget = Ensemble1ParamsWidget(
ensemble=self._ensemble,
distribution=distribution,
trad=self._trad, parent=box,
)
vbox.addWidget(self._params_widget)
elif size == 2:
self._params_widget = Ensemble2ParamsWidget(
ensemble=self._ensemble,
distribution=distribution,
trad=self._trad, parent=box,
)
vbox.addWidget(self._params_widget)
else:
self._params_widget = None
@property
def values(self):
return self._values
def accept(self):
txt = self.get_combobox_text("comboBox_distribution")
distribution = next(
filter(
lambda d: d.name == txt,
self._study._ens_distributions.lst
),
None
)
parameters = self._params_widget.values
if distribution is not None:
ok, tips = distribution.is_valid_parameters(parameters)
if not ok:
self.message_box(
text=self._trad["checker_failed_text"],
informative_text="\n".join(
map(lambda t: " - " + self._trad[t], tips)
),
window_title=self._trad["checker_failed_title"],
)
return
self._values = {
"name": self.get_line_edit_text("lineEdit_name"),
"data_type": self._target_data[0],
"target_data": self._target_data[1],
"distribution": distribution,
"params": parameters,
}
super().accept()
def reject(self):
self.close()
class Ensemble1ParamsWidget(PamhyrWidget):
_pamhyr_ui = "Ensemble1ParamsWidget"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
distribution=None,
trad=None, parent=None):
super(Ensemble1ParamsWidget, self).__init__(
parent=parent
)
self._trad = trad
self._ensemble = ensemble
self._distribution = distribution
self._labels = distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) != 1:
self._values = self._distribution.default_values.copy()
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
@property
def values(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
]
return self._values
class Ensemble2ParamsWidget(PamhyrWidget):
_pamhyr_ui = "Ensemble2ParamsWidget"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
distribution=None,
trad=None, parent=None):
super(Ensemble2ParamsWidget, self).__init__(
parent=parent
)
self._trad = trad
self._ensemble = ensemble
self._distribution = distribution
self._labels = distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
self.set_label_text("label_1", self._trad[self._labels[1]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) != 2:
self._values = self._distribution.default_values.copy()
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
self.set_double_spin_box("doubleSpinBox_1", self._values[1])
@property
def values(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
self.get_double_spin_box("doubleSpinBox_1")
]
return self._values
+268
View File
@@ -0,0 +1,268 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
)
from View.Ensembles.UndoCommand import (
SetNameCommand, SetTypeCommand, SetDataCommand,
SetDistributionCommand,
AddCommand, DelCommand,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.Ensembles.Translate import *
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, study=None,
mode="stricklers", trad=None, parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._study = study
self._trad = trad
self._mode = mode
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
if self._mode == "data_type":
self.editor.addItems(
[
self._trad["not_defined"],
self._trad["strickler_minor"],
self._trad["strickler_medium"],
]
)
elif self._mode == "stricklers":
used = list(
map(
lambda e: (e.data_type, e._target_data),
self._data.lst
)
)
current = self._data.get(index.row())
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda s: str(s),
filter(
# Exclude target data already used for
# this data type
lambda s: (
current.target_data == s or
(current.data_type, s) not in used
),
self._study.river.stricklers.stricklers
)
)
)
)
elif self._mode == "distribution":
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda f: f.name,
self._study._ens_distributions.lst
)
)
)
self.editor.setCurrentText(index.data(Qt.DisplayRole))
return self.editor
def setEditorData(self, editor, index):
value = index.data(Qt.DisplayRole)
self.editor.currentTextChanged.connect(self.currentItemChanged)
def setModelData(self, editor, model, index):
text = str(editor.currentText())
if self._mode == "data_type":
value = next(
filter(
lambda x: self._trad[x] == text,
["strickler_minor", "strickler_medium"],
),
""
)
elif self._mode == "stricklers":
value = next(
filter(
lambda s: str(s) == text,
self._study.river.stricklers.stricklers
),
None
)
elif self._mode == "distribution":
value = next(
filter(
lambda f: f.name == text,
self._study._ens_distributions.lst
),
None
)
else:
value = text
model.setData(index, value)
editor.close()
editor.deleteLater()
def updateEditorGeometry(self, editor, option, index):
r = QRect(option.rect)
if self.editor.windowFlags() & Qt.Popup:
if editor.parent() is not None:
r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
editor.setGeometry(r)
@pyqtSlot()
def currentItemChanged(self):
self.commitData.emit(self.sender())
class EnsembleTableModel(PamhyrTableModel):
def _setup_lst(self):
self._lst = self._data
self._study = self._opt_data
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
if self._headers[column] == "name":
return self._lst.get(row).name
elif self._headers[column] == "type":
return self._trad[self._lst.get(row).data_type]
elif self._headers[column] == "target_data":
value = self._lst.get(row).target_data
if value is None:
return self._trad["not_defined"]
return str(value)
elif self._headers[column] == "distribution":
value = self._lst.get(row).distribution
if value is None:
return self._trad["not_defined"]
return value.name
elif self._headers[column] == "parameters":
value = self._lst.get(row).params
return str(value)
return QVariant()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
try:
if self._headers[column] == "name":
self._undo.push(
SetNameCommand(
self._lst, row, value
)
)
elif self._headers[column] == "type":
self._undo.push(
SetTypeCommand(
self._lst, row, value
)
)
elif self._headers[column] == "target_data":
self._undo.push(
SetDataCommand(
self._lst, row, value
)
)
elif self._headers[column] == "distribution":
self._undo.push(
SetDistributionCommand(
self._lst, row, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
self._undo.push(
AddCommand(
self._lst, row,
)
)
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
self._undo.push(
DelCommand(
self._lst, rows
)
)
self.endRemoveRows()
self.layoutChanged.emit()
def undo(self):
self._undo.undo()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self.layoutChanged.emit()
+50
View File
@@ -0,0 +1,50 @@
# Translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class EnsemblesTranslate(MainTranslate):
def __init__(self):
super(EnsemblesTranslate, self).__init__()
self._dict["Ensembles"] = _translate(
"Ensembles", "Ensembles"
)
self._dict["ensemble"] = _translate(
"Ensembles", "Ensembles scenario"
)
self._dict["strickler_minor"] = _translate(
"Ensembles", "Stricklers minor"
)
self._dict["strickler_medium"] = _translate(
"Ensembles", "Stricklers medium"
)
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"type": _translate("Ensembles", "Type"),
"target_data": _translate("Ensembles", "Target data"),
"distribution": _translate("Ensembles", "Distribution"),
"parameters": _translate("Ensembles", "Parameters"),
}
+178
View File
@@ -0,0 +1,178 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.Friction.Friction import Friction
from Model.Friction.FrictionList import FrictionList
class SetNameCommand(QUndoCommand):
def __init__(self, ensembles, index, new_value):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index).name
self._new = str(new_value)
def undo(self):
self._ensembles.get(self._index).name = self._old
def redo(self):
self._ensembles.get(self._index).name = self._new
class SetTypeCommand(QUndoCommand):
def __init__(self, ensembles, index, new_value):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index)\
.data_type
self._new = new_value
def undo(self):
self._ensembles.get(self._index).data_type = self._old
def redo(self):
self._ensembles.get(self._index).data_type = self._new
class SetDataCommand(QUndoCommand):
def __init__(self, ensembles, index, data):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index)\
.target_data
self._new = data
def undo(self):
self._ensembles.get(self._index).target_data = self._old
def redo(self):
self._ensembles.get(self._index).target_data = self._new
class SetDistributionCommand(QUndoCommand):
def __init__(self, ensembles, index, distribution):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index).distribution
self._old_params = self._ensembles.get(self._index).params
self._new = distribution
def undo(self):
self._ensembles.get(self._index).distribution = self._old
self._ensembles.get(self._index).params = self._old_params
def redo(self):
self._ensembles.get(self._index).distribution = self._new
if self._new is not None:
self._ensembles.get(self._index).params = self._new.default_values
class AddCommand(QUndoCommand):
def __init__(self, ensembles, index):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._new = None
def undo(self):
self._ensembles.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._ensembles.new(self._index)
else:
self._ensembles.insert(self._index, self._new)
class ImportCommand(QUndoCommand):
def __init__(self, ensembles, data, index):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._data = data
self._news = None
def undo(self):
for new in self._news:
new.set_as_deleted()
def redo(self):
if self._news is None:
self._news = list(
map(
lambda d: self._ensembles.new_with_data(
self._index, d
),
self._data,
)
)
else:
for new in self._news:
new.set_as_not_deleted()
class DelCommand(QUndoCommand):
def __init__(self, ensembles, rows):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._rows = rows
self._friction = []
for row in rows:
self._friction.append((row, self._ensembles.get(row)))
self._friction.sort()
def undo(self):
for row, el in self._friction:
self._ensembles.insert(row, el)
def redo(self):
self._ensembles.delete_i(self._rows)
class SetAllCommand(QUndoCommand):
def __init__(self, ensemble, data):
QUndoCommand.__init__(self)
self._ensemble = ensemble
self._old = self._ensemble.get_data_dict()
self._new = data
def undo(self):
self._ensemble.set_data_dict(self._old)
def redo(self):
self._ensemble.set_data_dict(self._new)
+280
View File
@@ -0,0 +1,280 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import json
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow, PamhyrDialog
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QSettings,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
)
from Modules import Modules
from View.Ensembles.Edit.Window import EnsembleEditDialog
from View.Ensembles.Table import (
EnsembleTableModel, ComboBoxDelegate
)
from View.Ensembles.Translate import EnsemblesTranslate
from View.Ensembles.UndoCommand import SetAllCommand, ImportCommand
logger = logging.getLogger()
class EnsemblesWindow(PamhyrWindow):
_pamhyr_ui = "Ensembles"
_pamhyr_name = "Ensembles"
def __init__(self, ensembles=None,
study=None, config=None,
parent=None):
trad = EnsemblesTranslate()
self._ensembles = ensembles
name = (
trad[self._pamhyr_name] + " - " +
study.name
)
super(EnsemblesWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self.setup_table()
self.setup_connections()
def setup_table_delegate(self):
self._delegate_stricklers = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="stricklers",
trad=self._trad,
parent=self
)
self._delegate_data_type = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="data_type",
trad=self._trad,
parent=self
)
self._delegate_distribution = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="distribution",
trad=self._trad,
parent=self
)
return {
"type": self._delegate_data_type,
"target_data": self._delegate_stricklers,
"distribution": self._delegate_distribution,
}
def setup_table(self):
self._table = {}
delegates = self.setup_table_delegate()
editable_headers = [
"name",
]
table = self.find(QTableView, f"tableView")
self._table = EnsembleTableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates=delegates,
data=self._ensembles,
trad=self._trad,
undo=self._undo_stack,
opt_data=self._study
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_connections(self):
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
self.find(QAction, "action_export").triggered.connect(
self.export_to_json
)
self.find(QAction, "action_import").triggered.connect(
self.import_from_json
)
table = self.find(QTableView, f"tableView")
table.selectionModel()\
.selectionChanged\
.connect(self.update)
self._table.dataChanged\
.connect(self.update)
def index_selected_rows(self):
table = self.find(QTableView, f"tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._ensembles) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def edit(self):
rows = self.index_selected_rows()
for row in rows:
try:
ens = self._ensembles.get(row)
dlg = EnsembleEditDialog(
study=self._study,
ensemble=ens,
parent=self
)
if dlg.exec():
self._undo_stack.push(
SetAllCommand(
ens, dlg.values
)
)
except Exception as e:
logger_exception(e)
return
def export_to_json(self):
ens = []
for e in self._ensembles.lst:
ens.append(e.get_data_dict(use_ids=True))
data = {"ensembles": ens}
def aux(filename):
with open(filename[0], 'w') as f:
f.write(json.dumps(data))
self.file_dialog(
select_file=None,
callback=aux,
default_suffix='.json',
file_filter=[self._trad["file_json"]]
)
def import_from_json(self):
def aux(filename):
with open(filename[0], 'r') as f:
return json.load(f)
data = self.file_dialog(
select_file=None,
callback=aux,
default_suffix='.json',
file_filter=[self._trad["file_json"]]
)
dtarget = dict(
map(
lambda s: (s.pamhyr_id, s),
self._study.river.stricklers.stricklers
)
)
ddistr = dict(
map(
lambda d: (d.name, d),
self._study._ens_distributions.lst
)
)
def update(d, values):
d.update(values)
return d
updated = list(
map(
lambda d: update(
d, {"target_data": dtarget[d["target_data"]]}
),
map(
lambda d: update(
d, {"distribution": ddistr[d["distribution"]]}
),
data["ensembles"]
)
)
)
self._undo_stack.push(
ImportCommand(
self._ensembles, updated, 0
)
)
self._table.update()
+7 -4
View File
@@ -112,7 +112,7 @@ class PlotAC(PamhyrPlot):
profile_id = 0
profile = self.data.profile(profile_id)
if profile is None:
if profile is None or self._parent.tableView.isRowHidden(profile_id):
return
station = profile.get_station()
@@ -192,21 +192,24 @@ class PlotAC(PamhyrPlot):
self.plot_selected.set_data([], [])
self.next_plot_selected.set_data([], [])
if 0 <= previous_id < self.data.number_profiles:
if (0 <= previous_id < self.data.number_profiles
and not self._parent.tableView.isRowHidden(previous_id)):
self.previous_plot_selected.set_data(
self.data.profile(previous_id).get_station(),
self.data.profile(previous_id).z()
)
self.previous_plot_selected.set_visible(True)
if 0 <= profile_id < self.data.number_profiles:
if (0 <= profile_id < self.data.number_profiles
and not self._parent.tableView.isRowHidden(profile_id)):
self.plot_selected.set_data(
self.data.profile(profile_id).get_station(),
self.data.profile(profile_id).z()
)
self.plot_selected.set_visible(True)
if 0 <= next_id < self.data.number_profiles:
if (0 <= next_id < self.data.number_profiles
and not self._parent.tableView.isRowHidden(next_id)):
self.next_plot_selected.set_data(
self.data.profile(next_id).get_station(),
self.data.profile(next_id).z()
+17 -16
View File
@@ -20,6 +20,7 @@ import logging
from tools import timer
from View.Tools.PamhyrPlot import PamhyrPlot
from matplotlib.backend_bases import MouseButton
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import (
Qt, QItemSelectionModel,
@@ -64,7 +65,7 @@ class PlotRKZ(PamhyrPlot):
def onpick(self, event):
if event.mouseevent.inaxes != self.canvas.axes:
return
if event.mouseevent.button.value != 1:
if event.mouseevent.button != MouseButton.LEFT:
return
modifiers = QApplication.keyboardModifiers()
@@ -110,9 +111,9 @@ class PlotRKZ(PamhyrPlot):
def _closest_rk(self, event):
s = event.artist.get_segments()
x = [i[0, 0] for i in s]
mx = event.mouseevent.xdata
points = enumerate(x)
points = [(i, segment[0, 0]) for i, segment in enumerate(s)
if len(segment)]
def dist_mouse(point):
x = point[1]
@@ -136,6 +137,7 @@ class PlotRKZ(PamhyrPlot):
if len(ind) == 0:
return
ind = [i for i in ind if not self._table.isRowHidden(i)]
for i in ind:
index.append(QItemSelectionRange(self._table.model().index(i, 0)))
@@ -150,18 +152,7 @@ class PlotRKZ(PamhyrPlot):
self._table.scrollTo(self._table.model().index(ind[-1], 0))
def _select_range_in_table(self, ind1, ind2):
if self._table is not None:
self._table.setFocus()
selection = self._table.selectionModel()
index = QItemSelection(self._table.model().index(ind1, 0),
self._table.model().index(ind2, 0))
selection.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
self._table.scrollTo(self._table.model().index(ind2, 0))
self._select_in_table(list(range(ind1, ind2 + 1)))
@timer
def redraw(self, data):
@@ -213,13 +204,23 @@ class PlotRKZ(PamhyrPlot):
picker=10,
)
if self._table is not None:
segments = self.line_rk_zmin_zmax.get_segments()
self.line_rk_zmin_zmax.set_segments([
segment[:0] if self._table.isRowHidden(row) else segment
for row, segment in enumerate(segments)
])
def color_highlight(self):
rows = sorted(list(
set(
(i.row() for i in self.parent.tableView.selectedIndexes())
)
))
colors = [self.color_plot for row in range(len(self._data))]
colors = [
"darkorange" if profile.interpolated else self.color_plot
for profile in self._data.profiles
]
style = ["-" for row in range(len(self._data))]
if len(rows) > 0:
for row in rows:
+17 -19
View File
@@ -18,6 +18,7 @@
from tools import timer, trace
from View.Tools.PamhyrPlot import PamhyrPlot
from matplotlib.backend_bases import MouseButton
from matplotlib import collections
import numpy as np
@@ -60,7 +61,7 @@ class PlotXY(PamhyrPlot):
def onpick(self, event):
if event.mouseevent.inaxes != self.canvas.axes:
return
if event.mouseevent.button.value != 1:
if event.mouseevent.button != MouseButton.LEFT:
return
modifiers = QApplication.keyboardModifiers()
@@ -136,6 +137,7 @@ class PlotXY(PamhyrPlot):
index = QItemSelection()
if len(ind) == 0:
return
ind = [i for i in ind if not self._table.isRowHidden(i)]
for i in ind:
index.append(QItemSelectionRange(self._table.model().index(i, 0)))
selection.select(
@@ -149,18 +151,7 @@ class PlotXY(PamhyrPlot):
self._table.scrollTo(self._table.model().index(ind[-1], 0))
def _select_range_in_table(self, ind1, ind2):
if self._table is not None:
self._table.setFocus()
selection = self._table.selectionModel()
index = QItemSelection(self._table.model().index(ind1, 0),
self._table.model().index(ind2, 0))
selection.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
self._table.scrollTo(self._table.model().index(ind2, 0))
self._select_in_table(list(range(ind1, ind2 + 1)))
@timer
def redraw(self, data):
@@ -189,10 +180,14 @@ class PlotXY(PamhyrPlot):
def draw_xy(self):
self.line_xy = []
for xy in zip(self.data.get_x(), self.data.get_y()):
self.line_xy.append(np.column_stack(xy))
for row, profile in enumerate(self.data.profiles):
hidden = self._table is not None and self._table.isRowHidden(row)
self.line_xy.append(
np.empty((0, 2)) if hidden or len(profile) == 0
else np.column_stack((profile.x(), profile.y()))
)
self._colors, self._style = self.color_hightlight()
self._colors, self._style = self.color_highlight()
self.line_xy_collection = collections.LineCollection(
self.line_xy,
colors=self._colors,
@@ -201,13 +196,16 @@ class PlotXY(PamhyrPlot):
)
self.canvas.axes.add_collection(self.line_xy_collection)
def color_hightlight(self):
def color_highlight(self):
rows = sorted(list(
set(
(i.row() for i in self.parent.tableView.selectedIndexes())
)
))
colors = [self.color_plot for row in range(len(self._data))]
colors = [
"darkorange" if profile.interpolated else self.color_plot
for profile in self._data.profiles
]
style = ["-" for row in range(len(self._data))]
if len(rows) > 0:
for row in rows:
@@ -320,7 +318,7 @@ class PlotXY(PamhyrPlot):
def update_current(self):
if self._current_data_update:
self._colors, self._style = self.color_hightlight()
self._colors, self._style = self.color_highlight()
self.line_xy_collection.set_colors(self._colors)
self.line_xy_collection.set_linestyle(self._style)
+4 -3
View File
@@ -22,6 +22,7 @@ from math import dist, sqrt
from tools import timer, trace
from View.Tools.PamhyrPlot import PamhyrPlot
from matplotlib.backend_bases import MouseButton
from PyQt5.QtCore import (
Qt, QCoreApplication, QItemSelectionModel,
@@ -111,7 +112,7 @@ class Plot(PamhyrPlot):
def onpick(self, event):
if event.mouseevent.inaxes != self.canvas.axes:
return
if event.mouseevent.button.value != 1:
if event.mouseevent.button != MouseButton.LEFT:
return
modifiers = QApplication.keyboardModifiers()
@@ -126,11 +127,11 @@ class Plot(PamhyrPlot):
def onclick(self, event):
if event.inaxes != self.canvas.axes:
return
if event.button.value == 1:
if event.button not in (MouseButton.MIDDLE, MouseButton.RIGHT):
return
z = self._get_z_from_click(event)
if z < self.data.z_min() or event.button.value == 2:
if z < self.data.z_min() or event.button == MouseButton.MIDDLE:
self.highlight = None
self.draw_highligth()
self.update_idle()
+106 -1
View File
@@ -37,6 +37,11 @@ class SetDataCommand(QUndoCommand):
self._index = index
self._old = old_value
self._new = self.type(new_value)
if self._profile.old_interpolated == "":
if self._profile.interpolated:
self._profile.old_interpolated = "interpolated"
else:
self._profile.old_interpolated = "not_interpolated"
class SetXCommand(SetDataCommand):
@@ -46,10 +51,19 @@ class SetXCommand(SetDataCommand):
def undo(self):
self._profile.point(self._index).x = self._old
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._profile.point(self._index).x = self._new
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -60,10 +74,19 @@ class SetYCommand(SetDataCommand):
def undo(self):
self._profile.point(self._index).y = self._old
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._profile.point(self._index).y = self._new
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -74,10 +97,19 @@ class SetZCommand(SetDataCommand):
def undo(self):
self._profile.point(self._index).z = self._old
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._profile.point(self._index).z = self._new
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -89,9 +121,20 @@ class SetNameCommand(SetDataCommand):
def undo(self):
self._profile.point(self._index).name = self._old
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._profile.point(self._index).name = self._new
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
class AddCommand(QUndoCommand):
@@ -104,6 +147,13 @@ class AddCommand(QUndoCommand):
def undo(self):
self._profile.delete_i([self._index])
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
@@ -111,7 +161,8 @@ class AddCommand(QUndoCommand):
self._point = self._profile.insert(self._index)
else:
self._profile.insert_point(self._index, self._point)
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -125,10 +176,19 @@ class DelCommand(QUndoCommand):
def undo(self):
for point in self._points:
point.set_as_not_deleted()
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._profile.delete_points(self._points)
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -153,10 +213,19 @@ class SortCommand(QUndoCommand):
def undo(self):
self._profile.sort_with_indexes(self._indexes)
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._profile.sort(self._column, self._reverse)
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -173,6 +242,13 @@ class MoveCommand(QUndoCommand):
self._profile.move_up_point(self._i)
else:
self._profile.move_down_point(self._i)
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
@@ -180,6 +256,8 @@ class MoveCommand(QUndoCommand):
self._profile.move_up_point(self._i)
else:
self._profile.move_down_point(self._i)
self._profile.increase_modification_count()
self._profile.interpolated = False
self._profile.modified()
@@ -191,10 +269,19 @@ class ReverseCommand(QUndoCommand):
def undo(self):
self._profile.reverse()
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._profile.reverse()
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -209,10 +296,19 @@ class PurgeCommand(QUndoCommand):
def undo(self):
for point in self._deleted:
point.set_as_not_deleted()
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
self._deleted = self._profile.purge(self._np_purge)
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
@@ -228,9 +324,18 @@ class PasteCommand(QUndoCommand):
def undo(self):
self._profile.delete_points(self._points)
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
self._profile.modified()
def redo(self):
for point in self._points:
self._profile.insert_point(self._row, point)
self._profile.interpolated = False
self._profile.increase_modification_count()
self._profile.modified()
+3
View File
@@ -45,6 +45,9 @@ class GeometryTranslate(MainTranslate):
self._dict["cross_sections"] = _translate("Geometry", "cross-sections")
self._dict["profile"] = _translate("Geometry", "cross-section")
self._dict["profiles"] = _translate("Geometry", "cross-sections")
self._dict["show_interpolated_profiles"] = _translate(
"Geometry", "Show interpolated profiles"
)
self._dict["cross_section_enabled_count"] = _translate(
"Geometry", "Cross-section enabled ({count} disabled)"
)
+33 -4
View File
@@ -41,6 +41,7 @@ class SetDataCommand(QUndoCommand):
self._index = index
self._old = old_value
self._new = self.type(new_value)
self._profile = self._reach.profile(self._index)
class SetNameCommand(SetDataCommand):
@@ -48,24 +49,52 @@ class SetNameCommand(SetDataCommand):
self.type = str
super(SetNameCommand, self).__init__(
reach, index, old_value, new_value)
if self._profile.old_interpolated == "":
if self._profile.interpolated:
self._profile.old_interpolated = "interpolated"
else:
self._profile.old_interpolated = "not_interpolated"
def undo(self):
self._reach.profile(self._index).name = self._old
self._profile.name = self._old
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
def redo(self):
self._reach.profile(self._index).name = self._new
# Name setter will automatically update the interpolated attribute
self._profile.name = self._new
self._profile.increase_modification_count()
class SetRKCommand(SetDataCommand):
def __init__(self, reach, index, old_value, new_value):
self.type = float
super(SetRKCommand, self).__init__(reach, index, old_value, new_value)
if self._profile.old_interpolated == "":
if self._profile.interpolated:
self._profile.old_interpolated = "interpolated"
else:
self._profile.old_interpolated = "not_interpolated"
def undo(self):
self._reach.profile(self._index).rk = self._old
self._profile.rk = self._old
self._profile.decrease_modification_count()
if self._profile.modification_count == 0:
if self._profile.old_interpolated == "interpolated":
self._profile.interpolated = True
else:
self._profile.interpolated = False
self._profile.old_interpolated = ""
def redo(self):
self._reach.profile(self._index).rk = self._new
# RK setter will automatically update the interpolated attribute
self._profile.rk = self._new
self._profile.increase_modification_count()
class SetEnabledCommand(QUndoCommand):
+35
View File
@@ -127,6 +127,9 @@ class GeometryWindow(PamhyrWindow):
table.setAlternatingRowColors(True)
def setup_checkbox(self):
self.find(QCheckBox, "checkBox_show_interpolated").setText(
self._trad["show_interpolated_profiles"]
)
self._checkbox = self.find(QCheckBox, "checkBox_enabled")
self._set_checkbox_state()
@@ -197,6 +200,18 @@ class GeometryWindow(PamhyrWindow):
self._status_label.setText(txt)
def setup_connections(self):
self.find(QCheckBox, "checkBox_show_interpolated").toggled.connect(
self.update_interpolated_profiles_visibility
)
self.find(QCheckBox, "checkBox_show_interpolated").toggled.connect(
self.update_redraw
)
self._table.modelReset.connect(
self.update_interpolated_profiles_visibility)
self._table.rowsInserted.connect(
self.update_interpolated_profiles_visibility)
self._table.rowsRemoved.connect(
self.update_interpolated_profiles_visibility)
if self._study.is_read_only():
actions = {
"action_export": self.export_to_file,
@@ -237,6 +252,24 @@ class GeometryWindow(PamhyrWindow):
def update(self):
self._update()
def update_interpolated_profiles_visibility(self):
show_interpolated = self.find(
QCheckBox, "checkBox_show_interpolated"
).isChecked()
selection = self.tableView.selectionModel()
for row in range(self._table.rowCount()):
hidden = not show_interpolated \
and self._reach.profile(row).interpolated
self.tableView.setRowHidden(row, hidden)
if hidden:
selection.select(
self._table.index(row, 0),
QItemSelectionModel.Deselect | QItemSelectionModel.Rows
)
if selection.currentIndex().row() == row:
selection.setCurrentIndex(QModelIndex(),
QItemSelectionModel.NoUpdate)
def update_redraw(self):
self._update(redraw=True)
@@ -248,6 +281,7 @@ class GeometryWindow(PamhyrWindow):
self.find(QAction, "action_meshing").setEnabled(enabled)
def _update(self, redraw=False, propagate=True):
self.update_interpolated_profiles_visibility()
self.update_meshing_action()
if redraw:
@@ -498,6 +532,7 @@ class GeometryWindow(PamhyrWindow):
data=self._reach,
trad=self._trad,
toolbar=self._toolbar_ac,
parent=self,
)
self._plot_ac.draw()
+2 -1
View File
@@ -18,6 +18,7 @@
from tools import timer
from View.Tools.PamhyrPlot import PamhyrPlot
from matplotlib.backend_bases import MouseButton
from PyQt5.QtCore import (
QCoreApplication, Qt,
@@ -243,7 +244,7 @@ class PlotRKC(PamhyrPlot):
def onpick(self, event):
if event.mouseevent.inaxes != self.canvas.axes:
return
if event.mouseevent.button.value != 1:
if event.mouseevent.button != MouseButton.LEFT:
return
modifiers = QApplication.keyboardModifiers()
+12 -9
View File
@@ -20,6 +20,7 @@ import logging
import traceback
from datetime import date, time, datetime, timedelta
from tools import parse_datetime
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
@@ -73,11 +74,12 @@ class TableModel(PamhyrTableModel):
if self._data.get_type_column(column) == float:
value = f"{v:.4f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
if self.time_system == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -96,7 +98,7 @@ class TableModel(PamhyrTableModel):
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
self._undo.push(
@@ -184,17 +186,18 @@ class TableModel(PamhyrTableModel):
if len(data) == 0:
return
try:
rows = [self._data.new_from_data(header, d) for d in data]
except (ValueError, TypeError, IndexError) as error:
logger.warning(error)
return False
self.layoutAboutToBeChanged.emit()
self._undo.push(
PasteCommand(
self._data, row,
list(
map(
lambda d: self._data.new_from_data(header, d),
data
)
)
rows
)
)
+8 -1
View File
@@ -25,6 +25,8 @@ from PyQt5.QtGui import (
QKeySequence,
)
from Modules import Modules
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel, QCoreApplication,
)
@@ -110,7 +112,7 @@ class EditLateralContributionWindow(PamhyrWindow):
undo=self._undo_stack,
trad=self._trad,
opt_data=self._study.time_system,
start_date=self._study.date
study=self._study
)
table.setModel(self._table)
@@ -146,6 +148,11 @@ class EditLateralContributionWindow(PamhyrWindow):
self._table.dataChanged.connect(self.update)
self._table.layoutChanged.connect(self.update)
def _propagated_update(self, key=Modules(0)):
if Modules.STUDY in key:
self._table.layoutChanged.emit()
self.plot.update()
def update(self):
self.plot.update()
self.plot.fit_to_data()
+3 -3
View File
@@ -127,7 +127,7 @@ class PlotXY(PamhyrPlot):
for xy in zip(self.data.get_x(), self.data.get_y()):
self.line_xy.append(np.column_stack(xy))
self._colors, self._style = self.color_hightlight()
self._colors, self._style = self.color_highlight()
self.line_xy_collection = collections.LineCollection(
self.line_xy,
colors=self._colors,
@@ -136,7 +136,7 @@ class PlotXY(PamhyrPlot):
)
self.canvas.axes.add_collection(self.line_xy_collection)
def color_hightlight(self):
def color_highlight(self):
rk_min, rk_max = (-1, -1)
if self._highlight_data is not None:
rk_min, rk_max = self._highlight_data
@@ -250,7 +250,7 @@ class PlotXY(PamhyrPlot):
def update_current(self):
if self._current_data_update:
self._colors, self._style = self.color_hightlight()
self._colors, self._style = self.color_highlight()
self.line_xy_collection.set_colors(self._colors)
self.line_xy_collection.set_linestyle(self._style)
@@ -20,6 +20,7 @@ import logging
import traceback
from datetime import date, time, datetime, timedelta
from tools import parse_datetime
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
@@ -87,11 +88,12 @@ class TableModel(PamhyrTableModel):
if self._data._types[column] == float:
value = f"{v:.4f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
if self.time_system == "time":
value = timestamp_to_old_pamhyr_date_adists(int(v))
else:
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -110,7 +112,7 @@ class TableModel(PamhyrTableModel):
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
@@ -29,6 +29,8 @@ from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel, QCoreApplication,
)
from Modules import Modules
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
@@ -57,6 +59,15 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
name = trad[self._pamhyr_name]
if self._data is not None:
pollutant_name = next(
(
pollutant.name
for pollutant in study.river.Pollutants.Pollutants_List
if pollutant.id == self._data.pollutant
),
None
)
if self._data.reach is not None:
reach_name = next(filter(
lambda reach: reach.id == self._data.reach,
@@ -75,12 +86,10 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
else:
end_rk = trad['not_associated']
name += (
f" - {study.name} - " +
f"{reach_name} - " +
f"({begin_rk} - " +
f"{end_rk})"
)
name += f" - {study.name}"
if pollutant_name is not None:
name += f" - {pollutant_name}"
name += f" - {reach_name} - ({begin_rk} - {end_rk})"
super(EditLateralContributionAdisTSWindow, self).__init__(
title=name,
@@ -125,7 +134,7 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
undo=self._undo_stack,
trad=self._trad,
opt_data=self._study.time_system,
start_date=self._study.date
study=self._study
)
table.setModel(self._table)
@@ -159,6 +168,11 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow):
self._table.dataChanged.connect(self.update)
def _propagated_update(self, key=Modules(0)):
if Modules.STUDY in key:
self._table.layoutChanged.emit()
self.plot.update()
def update(self):
self.plot.update()
self.plot.fit_to_data()
@@ -0,0 +1,36 @@
# PlotXY.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from View.Geometry.PlotXY import PlotXY as GeometryPlotXY
class PlotXY(GeometryPlotXY):
"""Geometry plot highlighting the lateral-contribution extent."""
def color_highlight(self):
rk_min, rk_max = (-1, -1)
if self.highlight is not None:
rk_min, rk_max = self.highlight
colors = [
self.color_plot_river_bottom for _ in range(len(self.data))
]
for index, rk in enumerate(self.data.get_rk_complete_profiles()):
if rk_min <= rk <= rk_max:
colors[index] = self.color_plot_current
styles = ["-" for _ in range(len(self.data))]
return colors, styles
@@ -45,7 +45,7 @@ from View.LateralContributionsAdisTS.Table import (
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Geometry.PlotXY import PlotXY
from View.LateralContributionsAdisTS.PlotXY import PlotXY
from View.LateralContributionsAdisTS.translate \
import LCTranslate
from View.LateralContributionsAdisTS.Edit.Window \
+145 -32
View File
@@ -61,11 +61,13 @@ from View.WaitingDialog import WaitingDialog
from View.MainWindowTabInfo import WidgetInfo
from View.MainWindowTabChecker import WidgetChecker
from View.MainWindowTabGraph import WidgetGraph
from View.Configure.Window import ConfigureWindow
from View.Study.Window import NewStudyWindow
from View.About.Window import AboutWindow
from View.Scenarios.Window import ScenariosWindow
from View.Ensembles.Window import EnsemblesWindow
from View.Network.Window import NetworkWindow
from View.Geometry.Window import GeometryWindow
from View.BoundaryCondition.Window import BoundaryConditionWindow
@@ -82,11 +84,14 @@ from View.GeoTIFF.Window import GeoTIFFListWindow
from View.REPLines.Window import REPLineListWindow
from View.SolverParameters.Window import SolverParametersWindow
from View.RunSolver.Window import (
EnsembleRunNumberDialog,
SelectSolverWindow,
SolverLogWindow,
SolverLogEnsWindow,
)
from View.Results.Window import ResultsWindow
from View.Results.Window import ResultsWindow, ResultsEnsembleWindow
from View.Results.CompareDialog import (
CompareScenariosWindow
)
@@ -235,6 +240,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
def setup_tab(self):
self.setup_tab_info()
self.setup_tab_graph()
self.setup_tab_checker()
def setup_tab_info(self):
@@ -261,6 +267,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self._trad["tab_checker_name"]
)
def setup_tab_graph(self):
tab_widget = self.findChild(QTabWidget, "tabWidget")
self._tab_widget_graph = WidgetGraph(
study=self._study, trad=self._trad, parent=self
)
tab_widget.insertTab(
0, self._tab_widget_graph,
self._trad["tab_graph_name"]
)
tab_widget.setCurrentWidget(self._tab_widget_graph)
def enable_actions(self, action: str, enable: bool):
"""Enable of disable an action componant
@@ -545,6 +564,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self._do_propagate_update_rec(window, keys)
self._tab_widget_checker.update(modules=keys)
self._tab_widget_graph.update()
def _do_propagate_update_info_tab(self, keys):
modules = Modules.modelling_list()
@@ -570,6 +590,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self._tab_widget_info.study = self._study
self._tab_widget_info.update()
if self._tab_widget_graph.study != self._study:
self._tab_widget_graph.study = self._study
self._tab_widget_graph.update()
if self._tab_widget_checker.study != self._study:
self._tab_widget_checker.study = self._study
self._tab_widget_checker.update(modules=Modules.STUDY)
@@ -898,13 +922,32 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# MSG AND DIALOG #
##################
def msg_select_reach(self):
self.message_box(
window_title=self._trad["Warning"],
text=self._trad["mb_select_reach_title"],
informative_text=self._trad["mb_select_reach_msg"]
def ensure_current_reach(self):
"""Offer a reach selection when needed;
return False on cancellation."""
if self._study is None:
return False
river = self._study.river
if river.has_current_reach():
return True
edges = list(river.edges())
if not edges:
QMessageBox.information(
self, self._trad["Select reach"], self._trad["no_reach"]
)
return False
labels = [f"{edge.name or self._trad['reach']} (#{edge.id})"
for edge in edges]
selected, accepted = QInputDialog.getItem(
self, self._trad["Select reach"],
self._trad["reach"] + ":", labels, 0, False
)
self.open_network()
if not accepted:
return False
river.set_current_reach(edges[labels.index(selected)])
self._tab_widget_graph.update()
self._propagate_update(key=Modules.NETWORK)
return True
def dialog_reopen_study(self):
dlg = QMessageBox(self)
@@ -1287,6 +1330,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
return
self.network = NetworkWindow(study=self._study, parent=self)
self.network.networkChanged.connect(
self._tab_widget_graph.update, Qt.QueuedConnection
)
self.network.show()
def open_scenarios(self):
@@ -1297,6 +1343,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"""
if self._study is None:
return
if self._study.filename is None or self._study.filename == "":
self.message_box(
window_title=self._trad["Warning"],
@@ -1330,13 +1377,36 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self.scenarios = ScenariosWindow(study=self._study, parent=self)
self.scenarios.show()
def open_ensemble_scenario(self):
"""Open ensemble scenario window
Returns:
Nothing
"""
if self._study is None:
return
if self.sub_window_exists(
EnsemblesWindow,
data=[self._study, self.conf]
):
return
ensemble = EnsemblesWindow(
ensembles=self._study.river._ensembles,
study=self._study,
config=self.conf,
parent=self
)
ensemble.show()
def open_geometry(self):
"""Open geometry window
Returns:
Nothing
"""
if (self._study is not None and self._study.river.has_current_reach()):
if self.ensure_current_reach():
reach = self._study.river.current_reach().reach
if self.sub_window_exists(
@@ -1356,9 +1426,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
geometry.show()
return geometry
else:
self.msg_select_reach()
return None
return None
def open_meshing(self):
"""Open the current reach geometry and its meshing dialog."""
@@ -1441,27 +1509,24 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
strick.show()
def open_frictions(self):
if self._study is not None:
if self._study.river.has_current_reach():
reach = self._study.river.current_reach()
if self.ensure_current_reach():
reach = self._study.river.current_reach()
if self.sub_window_exists(
FrictionsWindow,
data=[self._study, self.conf, reach]
):
return
if self.sub_window_exists(
FrictionsWindow,
data=[self._study, self.conf, reach]
):
return
frictions = FrictionsWindow(
study=self._study,
config=self.conf,
parent=self
)
frictions.show()
else:
self.msg_select_reach()
frictions = FrictionsWindow(
study=self._study,
config=self.conf,
parent=self
)
frictions.show()
def open_initial_conditions(self):
if self._study.river.has_current_reach():
if self.ensure_current_reach():
reach = self._study.river.current_reach()
if self.sub_window_exists(
@@ -1477,8 +1542,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
parent=self
)
initial.show()
else:
self.msg_select_reach()
def open_additional_files(self):
if self._study is not None:
@@ -1552,8 +1615,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
return
if not self._study.river.has_current_reach():
self.msg_select_reach()
if not self.ensure_current_reach():
return
reach = self._study.river.current_reach().reach
@@ -1694,6 +1756,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
sol.show()
def solver_log(self, solver):
if self._study.status.scenario.is_ensemble():
return self._solver_log_ensemble(solver)
sol = SolverLogWindow(
study=self._study,
config=self.conf,
@@ -1702,6 +1767,28 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
sol.show()
def _solver_log_ensemble(self, solver):
run_number = 1
dlg = EnsembleRunNumberDialog(
trad=self._trad,
parent=self
)
if dlg.exec():
function = dlg.function
number_of_run = dlg.number
seed = dlg.seed
sol = SolverLogEnsWindow(
number_of_run=number_of_run,
seed=seed, function=function,
study=self._study,
config=self.conf,
solver=solver,
parent=self
)
sol.show()
def msg_box_results_continue(self):
dlg = QMessageBox(self)
@@ -1778,6 +1865,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if not self.msg_box_results_continue():
return
if self._study.status.scenario.is_ensemble():
self.open_solver_ens_results_window(solver, results)
else:
self.open_solver_results_window(solver, results)
def open_solver_results_window(self, solver, results):
# Windows already opened
if self.sub_window_exists(
ResultsWindow,
@@ -1797,6 +1890,26 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
res.show()
def open_solver_ens_results_window(self, solver, results):
# Windows already opened
if self.sub_window_exists(
ResultsEnsembleWindow,
data=[
self._study,
None, # No config,
results._solver,
results._repertory,
results._name]
):
return
res = ResultsEnsembleWindow(
study=self._study,
results=[results],
parent=self
)
res.show()
def msg_open_results_no_results(self):
self.message_box(
window_title=self._trad["Warning"],
+139
View File
@@ -0,0 +1,139 @@
# MainWindowTabGraph.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.
# -*- coding: utf-8 -*-
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.PlotXY import PlotXY
from View.Network.GraphWidget import GraphWidget
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QWidget, QGroupBox, QLabel, QHBoxLayout, QVBoxLayout, QSplitter,
)
class WidgetGraph(QWidget):
"""Read-only network graph displayed in the main window."""
def __init__(self, study=None, trad=None, parent=None):
super(WidgetGraph, self).__init__(parent)
self._study = study
self._trad = trad
self._plot_river = None
layout = QHBoxLayout(self)
self._splitter = QSplitter(Qt.Horizontal, self)
self._splitter.setChildrenCollapsible(False)
layout.addWidget(self._splitter)
self._graph_widget = None
self._graph_group = QGroupBox(
self._trad["graph_network_title"], self
)
self._graph_layout = QVBoxLayout(self._graph_group)
self._graph_title = QLabel(self._graph_group)
self._graph_title.setTextFormat(Qt.PlainText)
self._graph_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self._graph_title.setWordWrap(True)
self._graph_layout.addWidget(self._graph_title)
self._update_graph_title()
self._splitter.addWidget(self._graph_group)
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas_graph")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self,
items=["home", "zoom", "save", "iso", "back/forward", "move"]
)
self._plot_group = QGroupBox(
self._trad["graph_geographic_title"], self
)
self._plot_layout = QVBoxLayout(self._plot_group)
self._plot_layout.addWidget(self.toolbar)
self._plot_layout.addWidget(self.canvas, 1)
self._splitter.addWidget(self._plot_group)
self._splitter.setStretchFactor(0, 1)
self._splitter.setStretchFactor(1, 1)
self._splitter.setSizes([1000, 1000])
self.plot = PlotXY(
canvas=self.canvas,
data=None,
trad=self._trad,
toolbar=self.toolbar,
parent=self
)
@property
def study(self):
return self._study
@study.setter
def study(self, study):
self._study = study
self._update_graph_title()
def _update_graph_title(self):
self._graph_title.setText(
self._study.name if self._study is not None else ""
)
def update(self):
river = self._study.river if self._study is not None else None
view = None
if river is not None and river is self._plot_river:
axes = self.canvas.axes
if axes.has_data():
view = (axes.get_xlim(), axes.get_ylim(),
axes.get_aspect(), axes.get_adjustable())
self._update_graph_title()
if (self._graph_widget is not None
and (self._study is None
or self._graph_widget.graph is not self._study.river)):
self._graph_layout.removeWidget(self._graph_widget)
self._graph_widget.deleteLater()
self._graph_widget = None
if self._study is not None and self._graph_widget is None:
self._graph_widget = GraphWidget(
self._study.river,
parent=self,
only_display=True,
trad=self._trad,
)
self._graph_layout.addWidget(self._graph_widget, 1)
elif self._graph_widget is not None:
self._graph_widget.display_update()
data = None
geotiff = None
if self._study is not None:
data = self._study.river.enable_edges()
geotiff = self._study.river.geotiff
self.plot = PlotXY(
canvas=self.canvas,
data=data,
geotiff=geotiff,
trad=self._trad,
toolbar=self.toolbar,
parent=self
)
self.plot.update()
self._plot_river = river
if view is not None:
x_limits, y_limits, aspect, adjustable = view
self.canvas.axes.set_aspect(aspect, adjustable=adjustable)
self.canvas.axes.set_xlim(x_limits)
self.canvas.axes.set_ylim(y_limits)
self.canvas.draw_idle()
+47 -17
View File
@@ -448,6 +448,7 @@ class NewEdgeLine(QGraphicsItem):
class GraphWidget(QGraphicsView):
changeEdge = pyqtSignal(object)
changeNode = pyqtSignal(object)
currentEdgeChanged = pyqtSignal()
def __init__(self, graph, parent=None,
min_size=(400, 400), max_size=None,
@@ -476,6 +477,7 @@ class GraphWidget(QGraphicsView):
self.m_origin_x = 0.0
self.m_origin_y = 0.0
self.clicked = False
self._panning = False
self.setup_scene(min_size, max_size, size)
@@ -491,8 +493,8 @@ class GraphWidget(QGraphicsView):
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorViewCenter)
self.scale(1, 1)
self.previousScale = 1
self.scale(0.7, 0.7)
self.previousScale = 0.7
self.centerOn(1000.0, 1000.0)
if min_size:
@@ -667,6 +669,13 @@ class GraphWidget(QGraphicsView):
Returns:
Nothing
"""
# Scene items are destroyed by clear(); do not retain hover/selection
# references to them when refreshing another view of the same graph.
self._selected_item = None
self._selected_new_edge_src_node = None
self._current_edge = None
self._current_moved_node = None
self.tmp_line = None
self.scene().clear()
self.create_items()
@@ -875,6 +884,9 @@ class GraphWidget(QGraphicsView):
if previous_edge:
previous_edge.update()
edge.update()
if previous_edge is not edge:
self.currentEdgeChanged.emit()
except Exception as e:
logger.warning(str(e))
@@ -937,6 +949,8 @@ class GraphWidget(QGraphicsView):
def mousePressEvent(self, event):
if self._only_display:
if event.button() == Qt.LeftButton:
self._start_pan(event)
return
if event.button() == Qt.RightButton \
@@ -959,6 +973,12 @@ class GraphWidget(QGraphicsView):
self._selected_new_edge_src_node = None
items = self.items(event.pos())
if event.button() == Qt.LeftButton and not any(
isinstance(item, (NodeItem, EdgeItem, NodeText))
for item in items
):
self._start_pan(event)
return
if items and type(items[0]) is EdgeItem:
edge = items[0]
if edge:
@@ -1007,7 +1027,10 @@ class GraphWidget(QGraphicsView):
def mouseReleaseEvent(self, event):
self.clicked = False
if self._only_display:
if self._panning or self._only_display:
self._panning = False
self.viewport().unsetCursor()
event.accept()
return
locked = self.graph._status.is_read_only()
@@ -1029,7 +1052,28 @@ class GraphWidget(QGraphicsView):
self.update()
super(GraphWidget, self).mouseReleaseEvent(event)
def _start_pan(self, event):
self.clicked = True
self._panning = True
self.m_origin_x = event.x()
self.m_origin_y = event.y()
self.viewport().setCursor(Qt.ClosedHandCursor)
event.accept()
def mouseMoveEvent(self, event):
if self._panning or self._only_display:
if self._panning and event.buttons() & Qt.LeftButton:
dx = event.x() - self.m_origin_x
dy = event.y() - self.m_origin_y
horizontal = self.horizontalScrollBar()
vertical = self.verticalScrollBar()
horizontal.setValue(horizontal.value() - dx)
vertical.setValue(vertical.value() - dy)
self.m_origin_x = event.x()
self.m_origin_y = event.y()
event.accept()
return
pos = self.mapToScene(event.pos())
locked = self.graph._status.is_read_only()
@@ -1061,20 +1105,6 @@ class GraphWidget(QGraphicsView):
# If state is "move"
if self._state == "move":
# Move on scene
if not self.selected_item():
if event.buttons() & Qt.LeftButton:
old_p = self.mapToScene(
int(self.m_origin_x), int(self.m_origin_y)
)
new_p = self.mapToScene(event.pos())
translation = new_p - old_p
self.translate(translation.x(), translation.y())
self.m_origin_x = event.x()
self.m_origin_y = event.y()
# Propagate event
self.update()
super(GraphWidget, self).mouseMoveEvent(event)
+26 -7
View File
@@ -64,7 +64,17 @@ class SelectProfileDialog(PamhyrDialog):
self._reach = reach
self._profile = None
self._profiles = self._reach.reach.enabled_profiles
self._profiles = list(self._reach.reach.profiles)
structure_profiles = [
structure.input_section
for structure in study.river.hydraulic_structures.lst
if structure.input_reach is reach and not structure.is_deleted()
]
self._selectable_indices = [
index for index, profile in enumerate(self._profiles)
if 0 < index < len(self._profiles) - 1
and profile not in structure_profiles
]
self.setup_combobox()
@@ -74,19 +84,28 @@ class SelectProfileDialog(PamhyrDialog):
list(
map(
lambda p: p.display_name(),
self._reach.reach.profiles[1:-1]
self._profiles
)
)
)
if len(self._profiles) == 0:
self.find(QDialogButtonBox, "buttonBox")\
.button(QDialogButtonBox.Ok)\
.setEnabled(False)
combobox = self.find(QComboBox, "comboBox")
for index in range(len(self._profiles)):
if index not in self._selectable_indices:
item = combobox.model().item(index)
item.setFlags(item.flags() &
~(Qt.ItemIsEnabled | Qt.ItemIsSelectable))
combobox.setCurrentIndex(
self._selectable_indices[0] if self._selectable_indices else -1
)
self.find(QDialogButtonBox, "buttonBox")\
.button(QDialogButtonBox.Ok)\
.setEnabled(bool(self._selectable_indices))
def accept(self):
index = self.find(QComboBox, "comboBox").currentIndex()
if not 0 <= index < len(self._profiles):
if index not in self._selectable_indices:
return
self._profile = self._profiles[index]
+4
View File
@@ -58,6 +58,8 @@ _translate = QCoreApplication.translate
class NetworkWindow(PamhyrWindow):
networkChanged = pyqtSignal()
_pamhyr_ui = "Network"
_pamhyr_name = "River network"
@@ -140,6 +142,8 @@ class NetworkWindow(PamhyrWindow):
self._graph_layout.addWidget(self._graph_widget)
def setup_connections(self):
self._undo_stack.indexChanged.connect(self.networkChanged)
self._graph_widget.currentEdgeChanged.connect(self.networkChanged)
self._nodes_model.dataChanged.connect(self.update)
self._reachs_model.dataChanged.connect(
self._graph_widget.display_update)
+21 -9
View File
@@ -26,10 +26,13 @@ from View.Results.translate import ResultsTranslate
class CustomExportDialog(PamhyrDialog):
_pamhyr_ui = "CustomExportAdisDialog"
_pamhyr_name = "Custom Plot Selection"
_pamhyr_ui = "CustomExportDialog"
_pamhyr_name = "Custom Export"
def __init__(self, parent=None):
def __init__(self,
tag_x="values_x",
tag_y="values_y",
parent=None):
trad = ResultsTranslate()
super(CustomExportDialog, self).__init__(
title=trad[self._pamhyr_name],
@@ -38,8 +41,8 @@ class CustomExportDialog(PamhyrDialog):
parent=parent
)
self._available_values_x = self._trad.get_dict("values_x")
self._available_values_y = self._trad.get_dict("values_y")
self._available_values_x = self._trad.get_dict(tag_x)
self._available_values_y = self._trad.get_dict(tag_y)
self.setup_radio_buttons_x()
self.setup_radio_buttons_res()
@@ -66,8 +69,8 @@ class CustomExportDialog(PamhyrDialog):
def setup_radio_buttons_res(self):
self._radio2 = []
layout = self.find(QVBoxLayout, "verticalLayout_pol")
label = self.find(QLabel, "label_3")
layout = self.find(QVBoxLayout, "verticalLayout_solv")
label = self.find(QLabel, "label_solv")
label.setText(self._trad["solver"])
for solver in self.parent._solvers:
@@ -89,7 +92,7 @@ class CustomExportDialog(PamhyrDialog):
layout.addStretch()
def setup_label(self):
self._label = self.find(QLabel, "label_4")
self._label = self.find(QLabel, "label_data")
self._label.setFrameStyle(QFrame.StyledPanel)
self._label.setStyleSheet('background-color: white')
self.set_label()
@@ -163,6 +166,15 @@ class CustomExportDialog(PamhyrDialog):
res = i
break
self.value = x, y, self._envelop.isChecked(), res
self.value = (x, y, self._envelop.isChecked(), res)
super().accept()
class CustomExportEnsembleDialog(CustomExportDialog):
def __init__(self, parent=None):
super(CustomExportEnsembleDialog, self).__init__(
tag_x="values_x_ens",
tag_y="values_y_ens",
parent=parent
)
+261
View File
@@ -0,0 +1,261 @@
# PlotHEnsemble.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from functools import reduce
from datetime import datetime
from tools import timer, trace, logger_exception
from View.Tools.PamhyrPlot import PamhyrPlot
from PyQt5.QtCore import (
QCoreApplication
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class PlotHEnsemble(PamhyrPlot):
def __init__(self, canvas=None, trad=None, toolbar=None,
results=None, reach_id=0, profile_id=[0], res_id=[0],
parent=None):
super(PlotHEnsemble, self).__init__(
canvas=canvas,
trad=trad,
data=results,
toolbar=toolbar,
parent=parent
)
self._mode = "time"
self._parent = parent
self._current_reach_id = reach_id
self._current_profile_id = profile_id
self._current_res_id = res_id
self._timestamps = parent._timestamps
self._current_timestamp = max(self._timestamps)
self.label_x = self._trad["unit_time_s"]
self.label_y = self._trad["unit_discharge"]
self.label_discharge = _translate("Results", "Cross-section discharge")
self.label_discharge_max = _translate("Results", "Max discharge")
self.label_timestamp = _translate("Results", "Current timestamp")
self._isometric_axis = False
# self._auto_relim_update = False
# self._autoscale_update = False
self._line_max = []
self._line = []
self._add_data_lines = []
@property
def results(self):
return self.data
@results.setter
def results(self, results):
self.data = results
self._current_timestamp = max(self._timestamps)
@timer
def draw(self, highlight=None):
self.canvas.axes.cla()
self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
if self.results is None:
return
if len(self._current_res_id) < 1:
return
for res_id in self._current_res_id:
if self.results[res_id] is None:
continue
results = self.results[res_id]
reach = results.river.reach(self._current_reach_id)
if reach.geometry.number_profiles == 0:
self._init = False
return
self.draw_data(res_id)
self.draw_data_std_derivation(res_id)
self.canvas.axes.set_xlabel(
self._trad["unit_time_s"],
color='black', fontsize=10
)
self.canvas.axes.set_ylabel(
self._trad["unit_discharge"],
color='black', fontsize=10
)
self.set_ticks_time_formater()
self.draw_additional_data(0)
self.enable_legend()
self.canvas.draw_idle()
self.update_idle()
self.draw_current()
self._init = True
@timer
def draw_data(self, res_id):
results = self.results[res_id]
reach = results.river.reach(self._current_reach_id)
colors = [
("q_min", self.color_plot_ensemble_min),
("q_mean", self.color_plot_ensemble_mean),
("q_max", self.color_plot_ensemble_max),
]
for t, c in colors:
q = results.get("table")[t]
profile = reach.profile(self._current_profile_id[0])
x = self._timestamps
y = q[:, profile.global_index]
# y = profile.get_key("Q")
if res_id == 2:
label = f"Δ {self.label_discharge} {t} {profile.name}"
else:
label = f"{self.label_discharge} {t} {profile.name}"
line, = self.canvas.axes.plot(
x, y,
label=label,
color=c,
# linestyle=self.linestyle[i % len(self.linestyle)],
**self.plot_default_kargs
)
self._line.append(line)
@timer
def draw_data_std_derivation(self, res_id):
results = self.results[res_id]
reach = results.river.reach(self._current_reach_id)
q = results.get("table")["q_mean"]
std_deriv = results.get("table")["q_std_derivation"]
profile = reach.profile(self._current_profile_id[0])
x = self._timestamps
y = q[:, profile.global_index]
std = std_deriv[:, profile.global_index]
y_b = y - std
y_t = y + std
plot = self.canvas.axes.fill_between(
x, y_b, y_t,
label=f"Standard derivation {profile.name}",
color=self.color_plot_ensemble_std_deriv,
alpha=0.3,
)
def draw_additional_data(self, res_id):
results = self.results[res_id]
self._add_data_lines = []
for data in results.get("additional_data"):
data = data._data
x, y = data['x'], data['y']
legend = data['legend']
unit = data['unit']
if data['type_x'] == 'time' and data['type_y'] == 'discharge':
line, = self.canvas.axes.plot(
x, y, marker="+",
label=legend + ' ' + unit
)
self._add_data_lines.append(line)
self.enable_legend()
self.canvas.draw_idle()
def draw_current(self):
self._current, = self.canvas.axes.plot(
[self._current_timestamp, self._current_timestamp],
self.canvas.axes.get_ylim(),
# label=self.label_timestamp,
color="grey",
linestyle="dashed",
lw=1.,
)
def set_reach(self, reach_id):
self._current_reach_id = reach_id
self._current_profile_id = [0]
self.draw()
def set_profile(self, profile_id):
self._current_profile_id = profile_id
# self.update()
self.draw()
def set_result(self, res_id):
self._current_res_id = res_id
self.draw()
def set_timestamp(self, timestamp):
self._current_timestamp = timestamp
self.update_current()
self.update_idle()
def update(self):
if not self._init:
self.draw()
self.update_current()
self.update_idle()
def update_additional_data(self):
self.draw_additional_data(0)
self.update_idle()
def update_all(self):
self._current_reach_id = self._parent._get_current_reach()
self._current_profile_id = self._parent._get_current_profiles_list()
self._current_res_id = self._parent._get_current_results()
self._current_timestamp = self._parent._get_current_timestamp()
self._init = False
self.update()
def update_current(self):
y = self._current.get_ydata()
self._current.set_data(
[self._current_timestamp, self._current_timestamp],
y
)
+17 -40
View File
@@ -76,11 +76,14 @@ class PlotRKC(PamhyrPlot):
@staticmethod
def reach_geometry(reach):
"""Return geometry values for profiles present in the results."""
# The study's enabled profiles can change after a calculation. Keep
# coordinates in the same order as the result profiles and columns.
profiles = reach.profiles
return (
reach.geometry.get_rk_enabled_profiles(),
reach.geometry.get_z_min_enabled_profiles(),
reach.geometry.get_z_max_enabled_profiles()
)
[profile.rk for profile in profiles],
[profile.geometry.z_min() for profile in profiles],
[profile.geometry.z_max() for profile in profiles],
)
@timer
def draw(self, highlight=None):
@@ -121,12 +124,8 @@ class PlotRKC(PamhyrPlot):
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
zfd = list(
map(
lambda p: table[ts, p.global_index],
reach.profiles
)
)
gi_0, gi_1 = reach.global_index
zfd = table[ts, gi_0:gi_1 + 1]
self.line_bottom, = self.canvas.axes.plot(
rk, zfd,
@@ -186,14 +185,8 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
water_z = list(
map(
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
self.water = self.canvas.axes.plot(
rk, water_z,
@@ -216,16 +209,8 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
water_z = list(
map(
lambda p: np.max(
table[
:, p.global_index
]
),
reach.profiles
)
)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
self.canvas.axes.plot(
rk, water_z, lw=1.,
@@ -358,14 +343,8 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
water_z = list(
map(
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
self.water[0].set_data(
rk, water_z
@@ -408,10 +387,8 @@ class PlotRKC(PamhyrPlot):
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
zfd = list(map(
lambda p: table[ts, p.global_index],
reach.profiles
))
gi_0, gi_1 = reach.global_index
zfd = table[ts, gi_0:gi_1 + 1]
self.line_bottom.remove()
+355
View File
@@ -0,0 +1,355 @@
# PlotRKCEnsemble.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import numpy as np
from functools import reduce
from tools import timer
from View.Tools.PamhyrPlot import PamhyrPlot
from PyQt5.QtCore import (
QCoreApplication
)
logger = logging.getLogger()
class PlotRKCEnsemble(PamhyrPlot):
def __init__(self, canvas=None, trad=None, toolbar=None,
results=None, reach_id=0, profile_id=0,
parent=None):
super(PlotRKCEnsemble, self).__init__(
canvas=canvas,
trad=trad,
data=results,
toolbar=toolbar,
parent=parent
)
self._parent = parent
self._current_reach_id = reach_id
self._current_profile_id = profile_id
self._timestamps = parent._timestamps
self._current_timestamp = max(self._timestamps)
self.label_x = self._trad["unit_rk"]
self.label_y = self._trad["unit_elevation"]
self.label_bottom = self._trad["label_bottom"]
self.label_water = self._trad["label_water"]
self.label_water_mean = self._trad["label_water_mean"]
self.label_water_min = self._trad["label_water_min"]
self.label_water_max = self._trad["label_water_max"]
self.water = []
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
@property
def results(self):
return self.data
@results.setter
def results(self, results):
self.data = results
self._current_timestamp = max(self._timestamps)
@timer
def draw(self, highlight=None):
self.init_axes()
if self.results is None:
return
if self.results[0] is None:
return
results = self.results[0]
reach = results.river.reach(self._current_reach_id)
self.draw_bottom(reach)
self.draw_std_derivation(reach)
self.draw_water_elevation(reach)
self.draw_current(reach)
self.draw_profiles_hs(reach)
self.enable_legend()
self.idle()
self._init = True
def draw_bottom(self, reach):
self.draw_bottom_geometry(reach)
def draw_profiles_hs(self, reach):
results = self.results[0]
lhs = filter(
lambda hs: hs._input_reach.reach is reach.geometry,
filter(
lambda hs: hs._input_reach is not None,
results.study.river.hydraulic_structures.lst
)
)
for hs in lhs:
x = hs.input_section.rk
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
self.canvas.axes.plot(
[x, x],
[min(z_min), max(z_max)],
linestyle="--",
lw=1.,
color=self.color_plot_previous,
)
self.canvas.axes.annotate(
" > " + hs.name,
(x, max(z_max)),
horizontalalignment='left',
verticalalignment='top',
annotation_clip=True,
fontsize=9, color=self.color_plot_previous,
)
def draw_bottom_geometry(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
self.line_rk_zmin = self.canvas.axes.plot(
rk, z_min,
color=self.color_plot_river_bottom,
lw=1.
)
self._river_bottom = z_min
def draw_water_elevation(self, reach):
if len(reach.geometry.profiles) != 0:
result = self.results[0]
rk = reach.geometry.get_rk()
self.water = []
colors = [
("z_min", self.color_plot_ensemble_min),
("z_mean", self.color_plot_ensemble_mean),
("z_max", self.color_plot_ensemble_max),
]
for t, color in colors:
table = result.get("table")[t]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
plot, = self.canvas.axes.plot(
rk, water_z,
lw=1., color=color,
label=f"Elevation: {t} (m)",
)
self.water.append(plot)
def draw_std_derivation(self, reach):
if len(reach.geometry.profiles) != 0:
result = self.results[0]
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
color = self.color_plot_ensemble_std_deriv
table = result.get("table")["z_mean"]
std_deriv = result.get("table")["z_std_derivation"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
std = std_deriv[ts, gi_0:gi_1 + 1]
water_z_b = water_z - std
water_z_t = water_z + std
plot = self.canvas.axes.fill_between(
rk, water_z_b, water_z_t,
lw=1., color=color, alpha=0.3,
label=f"Standard derivation",
)
self.water_std_deriv = plot
def draw_additional_data(self):
results = self.results[0]
self._add_data_lines = []
for data in results.get("additional_data"):
data = data._data
tx, ty = data['type_x'], data['type_y']
x, y = data['x'], data['y']
legend = data['legend']
unit = data['unit']
if tx == 'water_elevation' and ty == 'time':
line, = self.canvas.axes.plot(
x, y, marker="+",
label=legend + ' ' + unit
)
self._add_data_lines.append(line)
self.canvas.draw_idle()
def draw_current(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
self.profile, = self.canvas.axes.plot(
[
rk[self._current_profile_id],
rk[self._current_profile_id]
],
[
z_max[self._current_profile_id],
z_min[self._current_profile_id]
],
color=self.color_plot,
lw=1.
)
def is_overflow_point(self, profile, point):
left_limit = profile.geometry.point(0)
right_limit = profile.geometry.point(
profile.geometry.number_points - 1
)
return (
point == left_limit
or point == right_limit
)
def set_reach(self, reach_id):
self._current_reach_id = reach_id
self._current_profile_id = 0
self.draw()
def set_profile(self, profile_id):
self._current_profile_id = profile_id
self.update_current()
def set_timestamp(self, timestamp):
self._current_timestamp = timestamp
self.update()
def update(self):
if not self._init:
self.draw()
results = self.results[0]
reach = results.river.reach(self._current_reach_id)
self.update_water_elevation()
self.update_std_derivation()
self.update_idle()
def update_all(self):
self._current_reach_id = self._parent._get_current_reach()
self._current_profile_id = self._parent._get_current_profile()
self._current_timestamp = self._parent._get_current_timestamp()
self._init = False
self.update()
def update_water_elevation(self):
result = self.results[0]
reach = result.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
table = result.get("table")["z_mean"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
colors = [
(0, "z_min", self.color_plot_ensemble_min),
(1, "z_mean", self.color_plot_ensemble_mean),
(2, "z_max", self.color_plot_ensemble_max),
]
for i, t, color in colors:
table = result.get("table")[t]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
self.water[i].set_data(rk, water_z)
def update_std_derivation(self):
result = self.results[0]
reach = result.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
table = result.get("table")["z_mean"]
std_deriv = result.get("table")["z_std_derivation"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
std = std_deriv[ts, gi_0:gi_1 + 1]
water_z_b = water_z - std
water_z_t = water_z + std
self.water_std_deriv.set_data(rk, water_z_b, water_z_t)
def update_additional_data(self):
self.draw_additional_data()
self.update_idle()
def update_current(self):
results = self.results[0]
reach = results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
cid = self._current_profile_id
self.profile.set_data(
[rk[cid], rk[cid]],
[z_max[cid], z_min[cid]]
)
self.canvas.figure.canvas.draw_idle()
self.update_idle()
+2 -1
View File
@@ -24,6 +24,7 @@ import numpy as np
from tools import timer, trace, logger_exception
from View.Tools.PamhyrPlot import PamhyrPlot
from matplotlib.backend_bases import MouseButton
from matplotlib import collections
from PyQt5.QtCore import (
@@ -89,7 +90,7 @@ class PlotXY(PamhyrPlot):
def onpick(self, event):
if event.mouseevent.inaxes != self.canvas.axes:
return
if event.mouseevent.button.value != 1:
if event.mouseevent.button != MouseButton.LEFT:
return
modifiers = QApplication.keyboardModifiers()
+6
View File
@@ -61,6 +61,8 @@ class TableModel(PamhyrTableModel):
# _river.reach(0).profiles))
elif self._opt_data == "solver":
self._lst = self._parent._solvers
elif self._opt_data == "runs":
self._lst = self._data[0].get("table")["samples"]
def __init__(self, **kwargs):
self._timestamp = max(kwargs["parent"]._timestamps)
@@ -93,6 +95,10 @@ class TableModel(PamhyrTableModel):
if v is None:
v = self._data[0].solver_name
return str(v)
elif self._opt_data == "runs":
if self._headers[column] == "runs":
p = self._lst[row, :]
return f"{row}: {p}"
elif self._opt_data == "raw_data":
table = self._data[0].get("table")
ts = self._data[0].get_timestamp_id(self._timestamp)
+679 -378
View File
File diff suppressed because it is too large. Load diff
+35 -1
View File
@@ -44,6 +44,7 @@ class ResultsTranslate(MainTranslate):
self._dict['envelop'] = _translate("Results", "Envelop")
self._dict['solver'] = _translate("Results", "Solver")
self._dict['runs'] = _translate("Results", "Runs")
self._dict['x'] = _translate("Results", "X (m)")
self._dict["temperature_map"] = _translate(
@@ -55,10 +56,20 @@ class ResultsTranslate(MainTranslate):
self._dict['label_bottom'] = _translate("Results", "Bottom")
self._dict['label_water'] = _translate("Results", "Water elevation")
self._dict['label_water_min'] = _translate(
"Results",
"Min water elevation"
)
self._dict['label_water_mean'] = _translate(
"Results",
"Mean water elevation"
)
self._dict['label_water_max'] = _translate(
"Results",
"Max water elevation"
)
self._dict["ImageCoordinates"] = _translate(
"Results", "Image coordinates"
)
@@ -87,6 +98,10 @@ class ResultsTranslate(MainTranslate):
"solver": self._dict['solver'],
}
self._sub_dict["table_headers_runs"] = {
"runs": self._dict['runs'],
}
self._sub_dict["table_headers_pollutants"] = {
"name": _translate("Results", "Pollutant name"),
}
@@ -121,6 +136,25 @@ class ResultsTranslate(MainTranslate):
"wet_area": self._dict["unit_wet_area"],
}
self._sub_dict["values_x_ens"] = {
"rk": self._dict["unit_rk"],
"time": self._dict["unit_time_s"],
}
self._sub_dict["values_y_ens"] = {
"z_mean": self._dict["unit_mean_water_elevation"],
"q_mean": self._dict["unit_mean_discharge"],
"z_standard_derivation": self._dict["unit_std_deriv_elevation"],
"q_standard_derivation": self._dict["unit_std_deriv_discharge"],
}
self._sub_dict["values_y_envelop_ens"] = {
"z_min": self._dict["unit_min_water_elevation"],
"z_max": self._dict["unit_max_water_elevation"],
"q_min": self._dict["unit_min_discharge"],
"q_max": self._dict["unit_max_discharge"],
}
if self.pollutants is not None:
self._sub_dict["table_headers_raw_data"] = {
"name": _translate("Results", "Profile"),
@@ -151,7 +185,7 @@ class ResultsTranslate(MainTranslate):
"max_velocity": self._dict["unit_max_velocity"],
"min_depth": self._dict["unit_min_depth"],
"max_depth": self._dict["unit_max_depth"],
}
}
self._sub_dict["values_y_pol"] = {
"unit_C": self._dict["unit_concentration"],
+290 -1
View File
@@ -20,6 +20,8 @@ import os
import logging
import tempfile
import numpy as np
from queue import Queue
from tools import trace, timer, logger_exception
@@ -40,7 +42,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QTextEdit,
QTextEdit, QProgressBar, QCheckBox,
)
from View.RunSolver.Log.Window import SolverLogFileWindow
@@ -57,6 +59,63 @@ _translate = QCoreApplication.translate
logger = logging.getLogger()
class EnsembleRunNumberDialog(PamhyrDialog):
_pamhyr_ui = "EnsembleRunNumberDialog"
_pamhyr_name = "EnsembleRunNumber"
def __init__(self, trad=None, parent=None):
super(EnsembleRunNumberDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._function = "Random"
self._number = 100
self._seed = -1
self._init_default_values()
self._init_connection()
def _init_default_values(self):
self.combobox_add_items("comboBox", ["Random"])
self.set_combobox_text("comboBox", "Random")
self.set_spin_box("spinBox", 100)
def _init_connection(self):
self._checkbox = self.find(QCheckBox, f"checkBox")
self._checkbox.clicked.connect(self._set_random_seed_spin_box_state)
def _set_random_seed_spin_box_state(self):
self.set_spin_box_enable("spinBox_seed", self._checkbox.isChecked())
@property
def number(self):
return self._number
@property
def seed(self):
return self._seed
@property
def function(self):
return self._function
def accept(self):
self._function = self.get_combobox_text("comboBox")
self._number = self.get_spin_box("spinBox")
if self._checkbox.isChecked():
self._seed = self.get_spin_box("spinBox_seed")
super().accept()
def reject(self):
self.close()
class SelectSolverWindow(PamhyrDialog):
_pamhyr_ui = "SelectSolver"
_pamhyr_name = "Select solver"
@@ -427,3 +486,233 @@ class SolverLogWindow(PamhyrWindow):
parent=self,
)
log.show()
class SolverLogEnsWindow(SolverLogWindow):
_pamhyr_ui = "SolverLogEnsemble"
_pamhyr_name = "Solver Log Ensemble"
def __init__(self, number_of_run=100,
seed=-1, function="Random",
study=None, config=None,
solver=None, parent=None):
self._number_of_run = number_of_run
self._last_run = 0
if seed != -1:
np.random.seed(seed)
self._seed = np.random.get_state()[1][0]
logger.info(
"solver: Set ensemble with " +
f"{number_of_run} {function} and " +
f"{self._seed} as random seed"
)
self._samples = {}
for ens in study.river._ensembles.lst:
if ens.distribution is None:
continue
self._samples[ens] = ens.random(self._number_of_run)
self._results = None
self._run_samples = []
self._results_tables = {}
super(SolverLogEnsWindow, self).__init__(
solver=solver,
study=study,
config=config,
parent=parent,
)
def setup_connections(self):
self.setup_progress_bar()
super(SolverLogEnsWindow, self).setup_connections()
def setup_progress_bar(self):
self._progress_bar = self.find(QProgressBar, "progressBar")
self._progress_bar.setRange(0, self._number_of_run)
self._progress_bar.setValue(0)
def _log_int(self, int_code, color=None):
logger.info(f"solver: Returns {int_code}")
color = "blue" if int_code == 0 else "red"
self.find(QTextEdit, "textEdit")\
.append(
f"<font color=\"{color}\">" +
f" *** Finished with code {int_code}" +
"</font>"
)
self.statusbar.showMessage(
("Done" if int_code == 0 else "Failed")
+ f" #{self._last_run}",
3000
)
self._solver.results_ensemble(
self._study, self._workdir,
self._results_tables,
self._last_run,
qlog=self._output,
)
if self._last_run < self._number_of_run:
self.next()
else:
self._results = self.fill_results_from_tables()
self._parent.set_results(self._solver, self._results)
self._progress_bar.setValue(self._last_run)
def fill_results_from_tables(self):
tables = self._results_tables
results = self._solver.get_ensemble_empty_results(
self._study, self._workdir
)
table = {}
for key in ["z", "q"]:
for func in ["min", "mean", "max", "std_derivation"]:
name = f"{key}_{func}"
table[name] = results.new_table_data(name, tables[name])
table["samples"] = results.new_table_data(
"samples", np.array(self._run_samples)
)
results.set("number_of_run", self._number_of_run)
results.set("seed", self._seed) # Keep random seed
results.set("table", table)
# Compute global index for reaches and profiles
gi = 0
for reach in results.river.reachs:
gi_start = gi
for profile in reach.profiles:
gi += 1
reach.set_global_index(range(gi_start, gi))
# Compute timestamps index table
ts = tables["timestamps"]
ts_index = {}
for i, t in enumerate(sorted(ts)):
ts_index[t] = i
results.set("timestamps", ts)
results.set_timestamp_index(ts_index)
return results
def next(self):
self._process = self.new_process(self._parent)
self._log(f" *** Run #{self._last_run}", color="blue")
if self._solver._type in ["mage8"]:
samples = self._solver.export_ensemble_variation(
self._study, self._workdir, qlog=self._output,
samples=self._samples,
)
else:
samples = self.ensemble_variation()
self._solver.export(
self._study, self._workdir, qlog=self._output
)
# Keep samples generators results
self._run_samples.append(samples)
self._solver.run(
self._study,
process=self._process,
output_queue=self._output
)
self.set_running_button_status()
self._last_run += 1
self._progress_bar.setValue(self._last_run - 1)
def ensemble_variation(self):
samples = []
for ens in self._samples:
if ens.target_data is None:
continue
x = next(self._samples[ens])
samples.append(x)
self._log(f"*** Variation #{self._last_run} " +
f"'{ens.target_data}' - {ens.data_type} x {x}",
color="blue")
if ens.data_type == "strickler_minor":
ens.target_data.minor = x
elif ens.data_type == "strickler_medium":
ens.target_data.medium = x
return samples
def run(self):
self._log(f" *** Run solver {self._solver.name}", color="blue")
self.next()
def start(self):
if self._solver.is_stoped():
self._log(f" *** Export study {self._solver.name}", color="blue")
ok = self._solver.export(
self._study, self._workdir, qlog=self._output
)
if not ok:
self._log(f" *** Failed to export", color="red")
self.update()
return
else:
self.update()
self._process = self.new_process(self._parent)
self._log(" *** Start", color="blue")
self._results = None
self.next()
def set_running_button_status(self):
self.find(QAction, "action_start").setEnabled(False)
if _signal:
self.find(QAction, "action_pause").setEnabled(True)
else:
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(True)
self.find(QAction, "action_log_file").setEnabled(False)
self.find(QAction, "action_results").setEnabled(False)
def stop(self):
self._log(" *** Stop", color="blue")
self._solver.kill()
self.find(QAction, "action_start").setEnabled(True)
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(False)
self.find(QAction, "action_results").setEnabled(True)
if self._solver.log_file() != "":
self.find(QAction, "action_log_file").setEnabled(True)
def results(self):
if self._results is None:
return
self._parent.set_results(self._solver, self._results)
self._parent.open_solver_results(self._solver, self._results)
self._solver.has_results_loaded()
+20 -1
View File
@@ -47,11 +47,19 @@ class AbstractMenu(object):
class DefaultMenu(AbstractMenu):
def run(self):
current_scenario = self._graph.status.scenario
if current_scenario.is_ensemble():
return
add_node = self._menu.addAction(self._trad["menu_add_scenario"])
ensemble = self._menu.addAction(self._trad["menu_ens_scenario"])
action = self._exec()
if action == add_node:
self._parent.new_scenario(self._pos)
if action == ensemble:
self._parent.create_new_ensemble_scenario(self._pos)
class ScenarioMenu(AbstractMenu):
@@ -61,9 +69,13 @@ class ScenarioMenu(AbstractMenu):
current_scenario = item.graph._study.status.scenario.id
select = self._menu.addAction(self._trad["menu_select_scenario"])
select = None
duplicate = None
delete = None
edit_ens = None
if len(scenarios) > 1:
select = self._menu.addAction(self._trad["menu_select_scenario"])
if item.scenario.id != 0:
if scenarios.is_leaf(item.scenario):
@@ -74,6 +86,11 @@ class ScenarioMenu(AbstractMenu):
self._trad["menu_dup_scenario"]
)
if item.scenario.is_ensemble():
edit_ens = self._menu.addAction(
self._trad["menu_edit_ens_scenario"]
)
action = self._exec()
if action is None:
return
@@ -83,3 +100,5 @@ class ScenarioMenu(AbstractMenu):
self._parent.delete_scenario(item)
elif action == duplicate:
self._parent.duplicate_scenario(item)
elif action == edit_ens:
self._parent.edit_ensemble_scenario(item)
+50 -1
View File
@@ -100,7 +100,11 @@ class ScenarioItem(QGraphicsTextItem):
)
)
painter.setBrush(QBrush(color))
painter.drawEllipse(self.boundingRect())
if self.scenario.is_ensemble():
painter.drawRect(self.boundingRect())
else:
painter.drawEllipse(self.boundingRect())
super(ScenarioItem, self).paint(painter, option, widget)
@@ -526,6 +530,51 @@ class GraphWidget(QGraphicsView):
self.exec_with_waiting_window(fn, "duplicate_scenario")
self.changeScenario.emit(self.sender())
def create_ensemble_scenario(self, item):
must_save = self.dialog_save()
if must_save == "Cancel":
return
def fn():
self._close_other_window()
if must_save == "Save":
self._study.save()
self._undo.push(
CreateEnsembleScenarioCommand(
self._study,
)
)
self.exec_with_waiting_window(fn, "create_ensemble_scenario")
self.changeScenario.emit(self.sender())
def create_new_ensemble_scenario(self, pos):
must_save = self.dialog_save()
if must_save == "Cancel":
return
def fn():
self._close_other_window()
if must_save == "Save":
self._study.save()
self._undo.push(
CreateEnsembleScenarioCommand(
self._study, pos=pos,
)
)
self.exec_with_waiting_window(fn, "create_ensemble_scenario")
self.changeScenario.emit(self.sender())
def edit_ensemble_scenario(self, item):
self.parent\
.parent\
.open_ensemble_scenario()
def _close_other_window(self):
self.parent\
.parent\
+20
View File
@@ -98,6 +98,26 @@ class DuplicateScenariosCommand(QUndoCommand):
self._study.reload_from_scenario(self._new)
class CreateEnsembleScenarioCommand(QUndoCommand):
def __init__(self, study, pos=None):
QUndoCommand.__init__(self)
self._study = study
self._new = None
self._pos = pos
def undo(self):
self._study.scenarios.delete(self._new.id)
self._study.reload_from_scenario(self._new.parent)
def redo(self):
if self._new is None:
self._new = self._study.create_ensemble_scenario(self._pos)
else:
self._new.set_as_not_deleted()
self._study.reload_from_scenario(self._new)
class SetCommand(QUndoCommand):
def __init__(self, scenario, column, new_value):
QUndoCommand.__init__(self)
+6
View File
@@ -43,6 +43,12 @@ class ScenariosTranslate(MainTranslate):
self._dict["menu_dup_scenario"] = _translate(
"Scenarios", "Duplicate this scenario"
)
self._dict["menu_ens_scenario"] = _translate(
"Scenarios", "Create ensemble scenario from current scenario"
)
self._dict["menu_edit_ens_scenario"] = _translate(
"Scenarios", "Edit this ensemble scenario"
)
self._sub_dict["table_headers_scenarios"] = {
# "id": self._dict['id'],
+51 -1
View File
@@ -45,6 +45,36 @@ logger = logging.getLogger()
_translate = QCoreApplication.translate
class ValueDelegate(QItemDelegate):
"""Use a combo box for solver parameters backed by an index."""
def createEditor(self, parent, option, index):
choices = index.model().value_choices(index)
if choices is None:
return QLineEdit(parent)
editor = QComboBox(parent)
for value, label in choices.items():
editor.addItem(label, value)
return editor
def setEditorData(self, editor, index):
if isinstance(editor, QComboBox):
current = index.model().raw_value(index)
editor.setCurrentIndex(editor.findData(current))
else:
editor.setText(str(index.model().raw_value(index)))
def setModelData(self, editor, model, index):
if isinstance(editor, QComboBox):
model.setData(index, editor.currentData())
else:
model.setData(index, editor.text())
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class TableModel(PamhyrTableModel):
def _setup_lst(self):
self._tab = self._opt_data
@@ -68,6 +98,10 @@ class TableModel(PamhyrTableModel):
elif cname == "value":
value = self._lst.get(row)[cname]
choices = self.value_choices(index)
if choices is not None:
return choices.get(value, value)
if value in self._trad.get_dict("yes_no"):
value = self._trad.get_dict("yes_no")[value]
@@ -79,7 +113,14 @@ class TableModel(PamhyrTableModel):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
choices = self.value_choices(index)
if choices is not None:
reverse_choices = {label: key for key, label in choices.items()}
value = reverse_choices.get(value, value)
if str(self.raw_value(index)) == str(value):
return False
elif self.is_same_data(index, value):
return False
row = index.row()
@@ -103,6 +144,15 @@ class TableModel(PamhyrTableModel):
self.dataChanged.emit(index, index)
return True
def value_choices(self, index):
if not index.isValid():
return None
name = self._lst.get(index.row())["name"]
return self._trad.get_dict("indexed_values").get(name)
def raw_value(self, index):
return self._lst.get(index.row())["value"]
def undo(self):
self._undo.undo()
self.layoutChanged.emit()
+2 -1
View File
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
)
from View.SolverParameters.UndoCommand import *
from View.SolverParameters.Table import TableModel
from View.SolverParameters.Table import TableModel, ValueDelegate
from View.SolverParameters.translate import ParamTranslate
from Solver.Solvers import solver_long_name, solver_type_list
@@ -103,6 +103,7 @@ class SolverParametersWindow(PamhyrWindow):
opt_data=st,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates={"value": ValueDelegate(table)},
trad=self._trad,
undo=self._undo_stack[st],
)
+24
View File
@@ -50,6 +50,30 @@ class ParamTranslate(MainTranslate):
_translate("SolverParameters", "N"): "n",
}
self._sub_dict["indexed_values"] = {
"mage_shields_critique": {
"1": _translate("SolverParameters", "Constant (0.047)"),
"2": _translate("SolverParameters", "Soulsby (1997)"),
"3": _translate("SolverParameters", "Camenen (2012)"),
},
"mage_shields_correction": {
"0": _translate("SolverParameters", "No correction"),
"1": _translate("SolverParameters", "Effective Shields"),
},
"mage_capacite_solide": {
"1": _translate(
"SolverParameters", "Meyer-Peter & Muller (1948)"
),
"2": _translate(
"SolverParameters", "Camenen & Larson (2005)"
),
"3": _translate(
"SolverParameters", "Engelund & Hansen (1967)"
),
"4": _translate("SolverParameters", "Recking (2013)"),
},
}
self._sub_dict["names"] = {
"all_init_time":
_translate("SolverParameters",
+78 -3
View File
@@ -19,7 +19,7 @@
import logging
from datetime import datetime
from tools import get_user_name
from tools import get_user_name, parse_datetime, format_datetime
from Modules import Modules
from Model.Study import Study
@@ -34,7 +34,7 @@ from PyQt5.QtGui import (
from PyQt5.QtWidgets import (
QRadioButton, QLabel, QDateTimeEdit,
QTextEdit, QPlainTextEdit, QTextBrowser,
QPushButton,
QPushButton, QMessageBox,
)
_translate = QCoreApplication.translate
@@ -155,7 +155,80 @@ All right reserved.
self.find(QLabel, "label_date").setEnabled(True)
self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True)
def _close_study_editors(self):
def close_children(parent):
for _, window in list(parent.sub_win_list):
if window is self:
continue
if not close_children(window):
return False
if not window.close():
return False
undo = getattr(window, "_undo_stack", None)
if undo is not None:
undo.clear()
window.deleteLater()
return True
return close_children(self.parent)
def accept(self):
preserve_dates = False
if (
self._study is not None
and self._study.time_system == "date"
and self.get_radio_button("radioButton_date")
):
old_date = parse_datetime(self._study.date)
new_date = parse_datetime(
self.get_datetime_edit("dateTimeEdit_date")
)
if new_date != old_date:
dialog = QMessageBox(self)
dialog.setWindowTitle(_translate(
"Study", "Change simulation start date?"
))
dialog.setText(_translate(
"Study", "How should the data dates change?"
))
dialog.setInformativeText(_translate(
"Study",
"Previous start: {old}\nNew start: {new}\n\n"
"Shift dates: keep elapsed simulation times unchanged.\n"
"Keep dates: adjust elapsed times so calendar dates "
"stay unchanged."
).format(
old=format_datetime(old_date),
new=format_datetime(new_date)
))
shift = dialog.addButton(
_translate("Study", "Shift dates"),
QMessageBox.AcceptRole
)
keep = dialog.addButton(
_translate("Study", "Keep dates"),
QMessageBox.ActionRole
)
cancel = dialog.addButton(QMessageBox.Cancel)
dialog.setDefaultButton(cancel)
dialog.exec()
if dialog.clickedButton() not in (shift, keep):
return
preserve_dates = dialog.clickedButton() is keep
if self._study is not None:
date_mode = self.get_radio_button("radioButton_date")
mode_changed = date_mode != (self._study.time_system == "date")
date_changed = date_mode and (
parse_datetime(self.get_datetime_edit("dateTimeEdit_date"))
!= parse_datetime(self._study.date)
)
if (
(mode_changed or date_changed)
and not self._close_study_editors()
):
return
name = self.get_line_edit_text("lineEdit_name")
description = self.get_plaintext_edit_text("plainTextEdit_description")
@@ -172,7 +245,9 @@ All right reserved.
self._study.description = description
if self.get_radio_button("radioButton_date"):
date = self.get_datetime_edit("dateTimeEdit_date")
self._study.use_date(date)
self._study.change_start_date(
date, preserve_dates=preserve_dates
)
else:
self._study.use_time()
+13 -1
View File
@@ -107,7 +107,7 @@ class WindowToolKit(object):
dialog = QFileDialog(self, options=options)
if select_file == "Existing_file":
if select_file == "ExistingFile":
mode = QFileDialog.FileMode.ExistingFile
elif select_file == "Directory":
mode = QFileDialog.FileMode.Directory
@@ -390,6 +390,18 @@ class ASubWindowFeatures(object):
"""
return self.find(QSpinBox, name).value()
def set_spin_box_enable(self, name: str, value: bool):
"""Set status of spinbox component
Args:
name: The spinbox component name
value: The boolean value
Returns:
Nothing
"""
self.find(QSpinBox, name).setEnabled(value)
def set_double_spin_box(self, name: str, value: int):
"""Set value of spinbox component
+6
View File
@@ -42,6 +42,12 @@ class PamhyrPlot(APlot):
color_plot_river_bottom = "grey"
color_plot_river_water = "blue"
color_plot_river_water_zone = "skyblue"
color_plot_ensemble_min = "green"
color_plot_ensemble_mean = "black"
color_plot_ensemble_max = "red"
color_plot_ensemble_std_deriv = "black"
colors = list(mplcolors.TABLEAU_COLORS)
linestyle = ['solid', 'dashed', 'dashdot', 'dotted']
+18 -3
View File
@@ -62,8 +62,22 @@ class PamhyrTextDelegate(QStyledItemDelegate):
class PamhyrTableModel(QAbstractTableModel):
@property
def time_system(self):
return (
self._study.time_system
if self._study is not None else self._opt_data
)
@property
def start_date(self):
return (
self._study.date
if self._study is not None else self._start_date
)
def date_to_elapsed_seconds(self, value):
"""Convert an entered date to seconds since the study start date."""
"""Convert an accepted calendar date to elapsed simulation seconds."""
try:
entered_date = parse_datetime(value)
except ValueError as error:
@@ -81,7 +95,7 @@ class PamhyrTableModel(QAbstractTableModel):
)
raise error
return (entered_date - self._start_date).total_seconds()
return (entered_date - parse_datetime(self.start_date)).total_seconds()
def _setup_delegates(self):
if self._table_view is None:
@@ -110,7 +124,7 @@ class PamhyrTableModel(QAbstractTableModel):
opt_data=None,
options=["rows_selection"],
parent=None,
start_date=None):
start_date=None, study=None):
super(PamhyrTableModel, self).__init__()
self._table_view = table_view
@@ -128,6 +142,7 @@ class PamhyrTableModel(QAbstractTableModel):
self._undo = undo
self._lst = []
self._start_date = start_date
self._study = study
self._setup_delegates()
self._setup_lst()
+113
View File
@@ -0,0 +1,113 @@
# PamhyrTree.py -- Pamhyr abstract table model
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from PyQt5.QtCore import (
Qt, QVariant, QAbstractItemModel,
QCoreApplication, QModelIndex, pyqtSlot,
)
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem,
)
logger = logging.getLogger()
class PamhyrModelItem(QTreeWidgetItem):
def __init__(self, data=None, trad=None, parent=None):
self._data = data
self._name = self._get_name()
super(PamhyrModelItem, self).__init__(
parent, [self._name],
)
self._setup_children()
def _get_name(self):
return str(self._data)
def _setup_children(self):
return
def get_value(self):
return self._data
def find_item_by_value(self, data):
if self.get_value() == data:
return self
for i in range(self.childCount()):
item = self.child(i)
if item.get_value() == data:
return item
found = item.find_item_by_value(data)
if found:
return found
return None
class PamhyrModelListItem(QTreeWidgetItem):
def __init__(self, name=None, lst=None,
child_item_ctor=PamhyrModelItem,
trad=None, parent=None):
super(PamhyrModelListItem, self).__init__(
parent, [name],
)
self._name = name
self._lst = lst
self._trad = trad
self._child_item_ctor = child_item_ctor
self._setup_children()
def _setup_children(self):
for el in self._lst.lst:
self.addChild(
self._child_item_ctor(
data=el, trad=self._trad, parent=self
)
)
def get_value(self):
return None
def find_item_by_value(self, data):
if self.get_value() == data:
return self
for i in range(self.childCount()):
item = self.child(i)
if item.get_value() == data:
return item
found = item.find_item_by_value(data)
if found:
return found
return None
+36 -8
View File
@@ -40,6 +40,12 @@ class CommonWordTranslate(PamhyrTranslate):
self._dict["time"] = _translate("CommonWord", "Time")
self._dict["date"] = _translate("CommonWord", "Date")
self._dict["low"] = _translate("CommonWord", "Low")
self._dict["high"] = _translate("CommonWord", "High")
self._dict["loc"] = _translate("CommonWord", "Loc")
self._dict["scale"] = _translate("CommonWord", "Scale")
self._dict["reach"] = _translate("CommonWord", "Reach")
self._dict["reaches"] = _translate("CommonWord", "Reaches")
self._dict["Select reach"] = _translate("CommonWord", "Select reach")
@@ -65,6 +71,9 @@ class CommonWordTranslate(PamhyrTranslate):
self._dict["file_csv"] = _translate(
"CommonWord", "CSV file (*.csv)"
)
self._dict["file_json"] = _translate(
"CommonWord", "JSON file (*.json)"
)
class UnitTranslate(CommonWordTranslate):
@@ -103,6 +112,12 @@ class UnitTranslate(CommonWordTranslate):
self._dict["unit_water_elevation"] = _translate(
"Unit", "Water Elevation (m)"
)
self._dict["unit_mean_water_elevation"] = _translate(
"Unit", "Mean water elevation (m)"
)
self._dict["unit_std_deriv_elevation"] = _translate(
"Unit", "Elevation standard derivation (m)"
)
self._dict["unit_water_elevation_envelop"] = _translate(
"Unit", "Water Elevation Envelop (m)"
)
@@ -122,8 +137,16 @@ class UnitTranslate(CommonWordTranslate):
self._dict["unit_min_velocity"] = _translate(
"Unit", "Min Velocity (m/s)"
)
self._dict["unit_discharge"] = _translate(
"Unit", "Discharge") + f" (m³/s)"
self._dict["unit_discharge"] = (
_translate("Unit", "Discharge") + f" (m³/s)"
)
self._dict["unit_mean_discharge"] = (
_translate("Unit", "Mean discharge") + f" (m³/s)"
)
self._dict["unit_std_deriv_discharge"] = (
_translate("Unit", "Discharge standard derivation")
+ f" (m³/s)"
)
self._dict["unit_discharge_envelop"] = _translate(
"Unit", "Discharge Envelop") + " (m³/s)"
self._dict["unit_max_discharge"] = _translate(
@@ -175,6 +198,15 @@ class MainTranslate(UnitTranslate):
self._dict["tab_checker_name"] = _translate(
"MainWindow", "Checks"
)
self._dict["tab_graph_name"] = _translate(
"MainWindow", "Graph"
)
self._dict["graph_network_title"] = _translate(
"MainWindow", "Hydrographic network"
)
self._dict["graph_geographic_title"] = _translate(
"MainWindow", "Geographic view"
)
self._dict["open_debug"] = _translate(
"MainWindow", "Open debug window"
@@ -200,13 +232,9 @@ class MainTranslate(UnitTranslate):
self._dict["Warning"] = _translate(
"MainWindow", "Warning"
)
self._dict["mb_select_reach_title"] = _translate(
"MainWindow", "Please select a reach"
)
self._dict["mb_select_reach_msg"] = _translate(
self._dict["no_reach"] = _translate(
"MainWindow",
"This edition window need a reach selected "
"into the river network to work on it"
"Create a reach in the network before opening this window."
)
self._dict["mb_last_open_title"] = _translate(
+12 -9
View File
@@ -20,6 +20,7 @@ import logging
import traceback
from datetime import date, time, datetime, timedelta
from tools import parse_datetime
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
@@ -74,11 +75,12 @@ class TableModel(PamhyrTableModel):
if self._data.get_type_column(column) == float:
value = f"{v:.4f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
if self.time_system == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = format_datetime(
self._start_date + timedelta(seconds=float(v))
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -97,7 +99,7 @@ class TableModel(PamhyrTableModel):
try:
if (self._data.header[column] == "time"
and self._opt_data == "date"):
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
self._undo.push(
@@ -175,17 +177,18 @@ class TableModel(PamhyrTableModel):
if len(data) == 0:
return
try:
rows = [self._data.new_from_data(header, d) for d in data]
except (ValueError, TypeError, IndexError) as error:
logger.warning(error)
return False
self.layoutAboutToBeChanged.emit()
self._undo.push(
PasteCommand(
self._data, row,
list(
map(
lambda d: self._data.new_from_data(header, d),
data
)
)
rows
)
)
+8 -1
View File
@@ -25,6 +25,8 @@ from PyQt5.QtGui import (
QKeySequence,
)
from Modules import Modules
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel, QCoreApplication,
)
@@ -122,7 +124,7 @@ class EditWeatherParametersWindow(PamhyrWindow):
undo=self._undo_stack,
trad=self._trad,
opt_data=self._study.time_system,
start_date=self._study.date
study=self._study
)
table.setModel(self._table)
@@ -158,6 +160,11 @@ class EditWeatherParametersWindow(PamhyrWindow):
self._table.dataChanged.connect(self.update)
self._table.layoutChanged.connect(self.update)
def _propagated_update(self, key=Modules(0)):
if Modules.STUDY in key:
self._table.layoutChanged.emit()
self.plot.update()
def update(self):
self.plot.update()
self.plot.fit_to_data()
+1 -1
View File
@@ -40,7 +40,7 @@ class WPETranslate(WeatherParametersTranslate):
"Humidity"),
"radiation": _translate("WeatherParameters", "Radiation (W/m²)"),
"wind speed": _translate("WeatherParameters", "Wind Speed (m/s)"),
"albedo": _translate("WeatherParameters", "Albedo"),
"albedo": _translate("WeatherParameters", "Water albedo"),
"shading coefficient": _translate("WeatherParameters",
"Shading Coefficient"),
"cloud cover fraction": _translate("WeatherParameters",
+1 -1
View File
@@ -76,7 +76,7 @@ class WeatherParametersTranslate(MainTranslate):
"Groundwater Temperature (°C)"),
"surface_flow_rate": _translate("WeatherParameters",
"Groundwater Flow Rate (m³/s)"),
"albedo": _translate("WeatherParameters", "Albedo"),
"albedo": _translate("WeatherParameters", "Water albedo"),
"shading_coefficient": _translate("WeatherParameters",
"Shading Coefficient"),
"cloud_cover_fraction": _translate("WeatherParameters",
+111
View File
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>418</width>
<height>220</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_x">
<item>
<widget class="QLabel" name="label_x">
<property name="text">
<string>X axis:</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_y">
<item>
<widget class="QLabel" name="label_y">
<property name="text">
<string>Y axis:</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_3">
<layout class="QVBoxLayout" name="verticalLayout_solv">
<item>
<widget class="QLabel" name="label_solv">
<property name="text">
<string>Solver:</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="5" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_data">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -56,7 +56,7 @@
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>TextLabel</string>
<string></string>
</property>
</widget>
</item>
+7
View File
@@ -18,6 +18,13 @@
<item row="0" column="0">
<widget class="QTableView" name="tableView"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>416</width>
<height>81</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_0">
<property name="text">
<string>@param0</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_0">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+106
View File
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>416</width>
<height>81</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_0">
<property name="text">
<string>@param0</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_0">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_1">
<property name="text">
<string>@param1</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_1">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+157
View File
@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>875</width>
<height>415</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_name"/>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Distribution</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QComboBox" name="comboBox_distribution"/>
</item>
<item row="0" column="4">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>313</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" rowspan="2" colspan="2">
<widget class="QGroupBox" name="groupBox_target_data">
<property name="title">
<string>Target data</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeWidget" name="treeWidget_target_data">
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="2" colspan="3">
<widget class="QGroupBox" name="groupBox_params">
<property name="title">
<string>Parameters</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_params"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="2" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="2" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>152</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Draw function</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox"/>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Number of run</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>Custom seed</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_seed">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>2147483647</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+112
View File
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_del"/>
<addaction name="action_edit"/>
<addaction name="separator"/>
<addaction name="action_export"/>
<addaction name="action_import"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add ensemble</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset>
<normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete selected ensembles</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit ensembles range</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_export">
<property name="icon">
<iconset>
<normaloff>ressources/export.png</normaloff>ressources/export.png</iconset>
</property>
<property name="text">
<string>Export</string>
</property>
<property name="toolTip">
<string>Export all ensemble configuration into json file</string>
</property>
</action>
<action name="action_import">
<property name="icon">
<iconset>
<normaloff>ressources/import.png</normaloff>ressources/import.png</iconset>
</property>
<property name="text">
<string>Import</string>
</property>
<property name="toolTip">
<string>Import ensemble configuration from json file</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
+10
View File
@@ -25,6 +25,16 @@
</property>
<widget class="QWidget" name="horizontalLayoutWidget">
<layout class="QVBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="checkBox_show_interpolated">
<property name="text">
<string notr="true">Show interpolated profiles</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="tableView"/>
</item>
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more