Compare commits

...
271 Commits
Author SHA1 Message Date
Dylan Jeannin 9e5153d63d Merge branch 'master' into merge_rubarbe 2026-10-27 10:22:35 +01:00
Dylan Jeannin 797de09683 InitialConditions: sort rows after each generation (single undo step), and fix undo of generation and sort (avoid STOP7 error on Hogneau example) 2026-10-26 16:55:01 +01:00
Dylan Jeannin ef10588880 BoundaryCondition: refit the edit plot to the data after each change 2026-10-26 16:06:55 +01:00
Dylan Jeannin c2a8e17bf3 HydraulicStructures: always show the reach plot, highlight the selected structure in blue, full zoom toolbars and fitted views 2026-10-26 15:52:46 +01:00
Dylan Jeannin 03979a8bea Frictions: display reach name in addition to the rk in combobox 2026-10-26 11:50:22 +01:00
Dylan Jeannin a4eee962f7 Geometry: fix error on the first right click in the profile plot (water limits not yet cached) 2026-10-26 10:57:29 +01:00
Dylan Jeannin d8d1b4cd1f HydraulicStructures: fix crash when selecting a rk before the reach 2026-10-23 13:37:03 +02:00
Dylan Jeannin cdeca8a2e8 BoundaryCondition: restore back/forward and view buttons in the edit plot toolbar 2026-10-23 13:23:34 +02:00
Dylan Jeannin 4d9c4cb5d0 RubarBE: add checkers running the export checks, with its errors and warnings (boundary conditions, lateral contributions, structures, saves...) 2026-10-22 17:53:29 +02:00
Dylan Jeannin 618477b384 Rubar3: remove it from the interface, RubarBE takes the parameters of Rubar3 studies when they are opened 2026-10-22 12:48:53 +02:00
Dylan Jeannin 70a26d0036 Mage: warn with a non-blocking checker about solid lateral contributions, and rerun lateral contribution checkers when they are modified 2026-10-21 17:04:30 +02:00
Dylan Jeannin c3afc4dd58 LateralContribution: hide the suspension tab, used by no solver (contributions kept in the model), and stop writing them + solid in the Mage LAT file 2026-10-21 15:41:07 +02:00
Dylan Jeannin ef72e41b14 RubarBE: export solid lateral contributions with their D50 and sigma, attached to the liquid contribution of the same zone (hydevs), and no longer read suspension ones 2026-10-21 14:56:44 +02:00
Dylan Jeannin be7b5b892a LateralContribution: add D50 and sigma to solid lateral contributions (database 0.2.11) 2026-10-21 14:23:47 +02:00
Dylan Jeannin 5f64362f65 RubarBE: export weirs and rectangular and circular orifices (ouvrag), and ignore other structure types with a warning 2026-10-20 17:41:32 +02:00
Dylan Jeannin 357b39758c RubarBE: export liquid lateral contributions (hydevs), and warn when lateral overflows parameter is disabled 2026-10-20 16:23:31 +02:00
Dylan Jeannin a2f215984b RubarBE: export an empty upstream solid boundary condition as a supply at equilibrium (negative Qs in hydros), like Mage 'default' 2026-10-20 15:14:15 +02:00
Dylan Jeannin b1cf45497b RubarBE: ignore empty boundary conditions with a warning instead of cancelling the export 2026-10-20 11:08:38 +02:00
Dylan Jeannin ee416e1059 RubarBE: show bed evolution in the results window (cross section and longitudinal profile), from the bed level in profil and the sections of geomac-N 2026-10-19 17:43:18 +02:00
Dylan Jeannin 3fee3b0bb3 RubarBE: export save abscissas (abshyd) and times (tnprof) from two solver parameters, and read results with tnprof save 2026-10-19 16:55:28 +02:00
Dylan Jeannin a06c2eaa7e RubarBE: export initial solid conditions (condins) from the active bed top layers, with no initial solid discharge 2026-10-19 13:45:36 +02:00
Dylan Jeannin 4e767d86d6 RubarBE: export the upstream solid boundary condition (hydros), and refuse unused, missing or invalid solid conditions 2026-10-19 11:45:20 +02:00
Dylan Jeannin f43d108871 Geometry: fix Tabulation type annotations (np.ndarray), which broke pdoc in the doc CI job 2026-10-16 17:10:20 +02:00
Dylan Jeannin 33a2107876 RubarBE: export sediment layers and bed limits in geomac-i, and refuse invalid layers or markers 2026-10-16 17:00:41 +02:00
Dylan Jeannin 7103fdcb46 RubarBE: refuse and disable upstream water level settings (REGIME=1, CONDAM=2) 2026-10-16 10:44:25 +02:00
Dylan Jeannin ace85ccb40 RubarBE: refuse upstream water level settings (REGIME=1, CONDAM=2) 2026-10-16 10:31:09 +02:00
Dylan Jeannin ec81a998cb RubarBE: fix hydraulic export (mail, condin, frot, boundary conditions), and prevent user of mistakes instead of run incorrect calculations 2026-10-16 09:55:40 +02:00
Dylan Jeannin a18accb404 RubarBE: seal study translated in pamhyr db 2026-10-15 17:26:35 +02:00
Dylan Jeannin 657f54ff91 RubarBE: split critical Shields stress (TCADIM) from water viscosity (VISC) 2026-10-15 03:40:52 +02:00
Dylan Jeannin bf2d3ae7a9 Merge branch 'dev_dylan' into merge_rubarbe 2026-10-14 19:35:37 +02:00
Dylan Jeannin 656b337499 RubarBE: notify when attempting to use RubarBE with solid boundary conditions missing 2026-10-14 19:14:51 +02:00
Dylan Jeannin ed2ed0ac2a RubarBE: Integration of the solver in the list 2026-10-11 20:33:25 +02:00
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 ab95f06700 Rubar parameters: prohibit simultaneous selection of REGIME=1 and CONDAM=0 2026-10-08 10:12:26 +02:00
Dylan Jeannin 2a699f46ca Solver: revamp Rubar parameter interface and export validation 2026-10-07 17:11:27 +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 33ad91d1a6 Plot widget: Fix plot selection crash on mouse wheel events 2026-10-06 11:15:14 +02:00
Dylan Jeannin 7d2ef35128 Results Rubar3: fix opening results window from the solver log window 2026-10-05 16:19:41 +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
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 92fb653676 Geometry: Point: Try to create index at each update. 2026-09-04 11:30:30 +02:00
Dylan Jeannin d8e9d786d9 LateralContributionsAdisTS: correctly highlight the profiles in the interval + refacto function name 2026-09-03 16:29:32 +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
Dylan Jeannin 5da607865c Pollutants: line becomes grey when we disable a pollutant + it's mentionned 'disabled' on the view only in the tab 2026-09-03 11:47:08 +02:00
Dylan Jeannin 1f5ce72fb2 Pollutants: fix deletion, didnt work correctly due to relative / flat indexes 2026-09-02 18:00:35 +02:00
Dylan Jeannin b6be245764 BoundaryConditionsAdisTS: display pollutant name in the edit window, to get a reminder of which pol we work on 2026-09-02 16:53:29 +02:00
Dylan Jeannin 46d0f52b3e Solver parameters: fix undo/redo command, double connection broke the normal operation of these commands 2026-09-02 16:20:03 +02:00
Dylan Jeannin 80f0b181d1 AdisTS: remove TEM files generated in results AdisTS (probably due to new solver version, including Temperature) 2026-09-02 14:33:31 +02:00
Dylan Jeannin 9161c54e18 AdisTS: debug results AdisTS with disabled profiles 2026-09-02 12:49:49 +02:00
Dylan Jeannin 64d9e456b7 Fix pep8 2026-09-01 18:13:02 +02:00
Dylan Jeannin 846aaeadf0 Translate: fix an error of translation 2026-09-01 17:57:39 +02:00
Dylan Jeannin eaf8d7364a InitialConditionsTemperature: fix temperature initial condition edit command 2026-09-01 17:53:30 +02:00
Dylan Jeannin 54bfff9623 Disable profile: dont check anymore if the profile we want to delete is attached to an initial condition 2026-09-01 17:47:03 +02:00
Dylan Jeannin 237364a4e2 Profiles: Fix plotting of empty geometry profiles, causing (non-blocking) error in the console 2026-09-01 16:57:50 +02:00
Dylan Jeannin c3288b44bb Meshing: Round interpolated profile RK values (2 decimal digits), to avoid XX.000000000001 values 2026-09-01 16:56:06 +02:00
Dylan Jeannin 70128bbdd1 Profiles: fix loaded profile reach reference (caused a crash during certain operations involving the profile table) 2026-09-01 16:31:33 +02:00
Dylan Jeannin a88a164ead GeometryProfilesXYZ: assignment of interpolated attribute, False by default, True when profile is created with mesh, and for the load of database, False by default, True when name starts with 'interpol' 2026-09-01 16:29:43 +02:00
Dylan Jeannin d6aeb5df21 Hydraulic structure: fix crashing bug happening when we select the section combobox without selecting a reach in the first combobox 2026-09-01 14:53:08 +02:00
Dylan Jeannin 32db5795da Hydraulic structures: Remove obsolete hydraulic structure output fields 2026-09-01 14:02:53 +02:00
Dylan Jeannin a81a641d41 Trad: Traduction of the new terms in french 2026-09-01 12:14:57 +02:00
Dylan Jeannin 53da35d959 Merge branch 'mesh_tab' into dev_dylan 2026-08-31 17:07:16 +02:00
Dylan Jeannin 0f1d1b0797 Time/Date: accept different formats for date, YYYY-MM-DD [HH:MM[:SS]], DD-MM-YYYY [HH:MM[:SS]] and DD/MM/YYYY [HH:MM[:SS]], convert it to classic one used by pamhyr, and prevent user when he's using a wrong one 2026-08-31 16:58:36 +02:00
Dylan Jeannin 6586a7b191 Date/time parameters: fixed the link between the start date in the study settings and the display in the tables 2026-08-31 16:14:28 +02:00
Dylan Jeannin b7aab9f263 Import geometry: debug case when we cannot open profiles after importing a new geometry 2026-08-31 14:42:45 +02:00
Dylan Jeannin ef34fecc31 Export geometry: doesnt export disabled profiles 2026-08-31 14:21:53 +02:00
Dylan Jeannin e9aa980c88 Export geometry: popup displayed before (and not after anymore) the file browser to choose if we export only geometry points, or if we want interpolated ones too 2026-08-31 14:08:09 +02:00
Dylan Jeannin ab495efc65 merge 2026-08-31 13:26:01 +02:00
Dylan Jeannin 14e13d6624 Moving points in network: selected node is now unselected after the end of moving, so clicking anywhere in the graph doesnt change his position anymore without refreshing the window 2026-08-31 11:57:33 +02:00
Dylan Jeannin 8db099c5c7 Moving points in network: selected node is now unselected after the end of moving, so clicking anywhere in the graph doesnt change his position anymore without refreshing the window 2026-08-31 11:56:01 +02:00
Dylan Jeannin b503e54210 Split reach: not displaying the first and the last reach in the combobox 2026-08-31 10:28:45 +02:00
Dylan Jeannin 68e4dfb598 Merge branch 'mesh_tab' into dev_dylan 2026-08-31 10:26:20 +02:00
JeaDylan 63dfab6654 Import INI files: non-blocking error if no profile associated with the RK is found in INI file, notifies the user of the relevant RK 2026-08-27 18:49:22 +02:00
JeaDylan 707577521f Import INI files: tolerate rounded RK values from INI files 2026-08-27 18:32:05 +02:00
JeaDylan 3458a5df9f Import INI files: handle INI RK matching and refresh table after import 2026-08-27 17:19:31 +02:00
JeaDylan e19eeedcc3 Export csv results: fix export flowrate as a function of time, on a study without sediments transport 2026-08-27 13:20:08 +02:00
JeaDylan c95de5e606 Merge branch 'temperature_test' into dev_dylan 2026-08-27 11:39:28 +02:00
Dylan Jeannin 53f7f380ae Result database: fix a problem that happens when we want to open a study with results saved, but results_table_data doesnt exist in database 2026-08-25 12:49:13 +02:00
Dylan Jeannin 2028d84554 Result database: fix a problem that happens when we want to open a study with results saved, but results_table_data doesnt exist in database 2026-08-25 12:48:02 +02:00
Dylan Jeannin da28bae6c5 Fix pep8 2026-08-24 17:08:37 +02:00
Dylan Jeannin 944f99f14b Disable profiles: security to prevent the disable of a profile associated with data (initial conditions, structures, lateral conditions, weather params, init. cond. temperature) 2026-08-24 17:01:16 +02:00
Dylan Jeannin 8978cc3482 Disabled profiles: the reach split is impossible on a disabled profile to avoid any problem 2026-08-24 15:37:03 +02:00
Dylan Jeannin f08d4b1a37 Disable profiles: results only take into account enabled profiles to display results for Mage 2026-08-24 14:17:39 +02:00
Dylan Jeannin 824fbe506e Profiles: disabled profiles now affects results for mage, AdisTS and AdisTT 2026-08-21 17:30:38 +02:00
Dylan Jeannin deb84ca331 Profiles: can now disable some profiles, they still appears in the list, but they are marked as disabled 2026-08-21 16:35:40 +02:00
Dylan Jeannin 5b778af745 Profiles: fix misconnexion between add/remove buttons and tab in actual profile window 2026-08-21 11:30:26 +02:00
JeaDylan 776d9ab610 Profiles: keep security to not open the same profile twice 2026-08-20 23:22:06 +02:00
JeaDylan 587e5412d5 Profiles: new functionnality, navigate between differents profiles (next and previous one) directly in the edit profile window 2026-08-20 22:17:49 +02:00
JeaDylan ef5430ae2c Mesh: give choice to export only handwritten points in geometry, or all points with interpolated ones 2026-08-20 00:01:55 +02:00
JeaDylan 059d7203a9 Mesh: activate button in toolbar only when a study is selected 2026-08-19 23:42:17 +02:00
Dylan Jeannin 82b6cbfd72 Merge branch 'split_reach' into dev_dylan 2026-08-18 16:59:32 +02:00
Dylan Jeannin c170dbc46c Mesh tab: modification of the mesh layout to better integrate it into the modeling process 2026-08-18 16:56:34 +02:00
Dylan Jeannin 890dbd9d32 Network: correct a crashing bug that happened when we tried to right click on the canvas of network after selecting a first point to create an edge 2026-08-18 13:40:02 +02:00
Dylan Jeannin f17b395c4c Bedload: Dynamic display for the riverbed over time in results window + correction of import of .ST files in geometry tab when we import a bedload file 2026-08-17 17:34:32 +02:00
Dylan Jeannin a44dddb51b Temperature results: display of temperature representation along the reach, with a scale of color 2026-08-13 13:00:49 +02:00
Dylan Jeannin 0fdb362313 Ovelapping intervals: setting default values when creating a parameter 2026-08-12 14:49:30 +02:00
Dylan Jeannin 50fa4f7bd8 Overlapping intervals: we cannot select rk if the interval overlaps anohter one, to avoid the confusion of multiple definitions over the same interval for WeatherParameters 2026-08-12 14:21:50 +02:00
Dylan Jeannin 9309aa9d2d Overlapping intervals: we cannot select rk that is already selected, to avoid the confusion of multiple definitions over the same interval 2026-08-12 14:03:17 +02:00
Dylan Jeannin dd31ffc5a2 Split reach: handling the case where the reach is split on a data used to define a data (an initial condition for example) 2026-08-12 12:09:47 +02:00
Dylan Jeannin 3d467c9620 Limit case: we cannot split reaches that contains less than 3 profiles, and not split on a limit profile (first or last one) 2026-08-11 17:32:15 +02:00
Dylan Jeannin f95c646ef3 Split reach: D90 and DIF now spread by the split 2026-08-11 17:32:06 +02:00
Dylan Jeannin c0000bb332 Database: upgrade version for temperature data 2026-08-11 11:47:34 +02:00
Dylan Jeannin cdbcb2b539 Merge branch 'split_reach' into dev_dylan 2026-08-11 11:17:16 +02:00
Dylan Jeannin 252ac257fe AdisTS AdisTT: fix initial conditions in scenario mode, now correctly saved in database, and load correctly, not creating duplicata when two datas have same ID with different scenario_id 2026-07-28 17:33:56 +02:00
Dylan Jeannin afe0217655 PEP8 2026-07-28 15:53:34 +02:00
Dylan Jeannin 516efb64e8 WeatherParam: SetData targets the object, and no longer the line index in the tab, which reduces the risk of error with multiple QUndoCommand, and rewrite of the logic for the other UndoCommand 2026-07-28 13:50:21 +02:00
Dylan Jeannin 4cad251e57 Friction: switch coefficients types or edit the list of application coefficients now send a notify the Friction window to update the list and plot, even if the window is opened already 2026-07-28 10:08:44 +02:00
Dylan Jeannin 48e9c14525 Strickler: dont display duplicated strickler from application database into study data tab 2026-07-27 17:13:16 +02:00
Dylan Jeannin db62af6f71 Friction: application's strickler now appears in combobox to pick a strickler, and create a copy of it in the study 2026-07-27 16:28:14 +02:00
Dylan Jeannin b468a1eb70 Friction: application's stricklers are now read-only in the friction window 2026-07-27 16:04:36 +02:00
Dylan Jeannin b9ab202e3f Application strickler: check that the name is not already in use before committing the change to the talbe, as the name is used as database identifier 2026-07-27 15:38:06 +02:00
Dylan Jeannin 129dde1c57 Strickler: quickfix to correctly save Strickler's name into app model 2026-07-27 15:12:09 +02:00
Dylan Jeannin ff2d3926b1 Strickler: can now add general stricklers relative to application and not only study 2026-07-27 14:41:03 +02:00
Dylan Jeannin fefc6ebc4f Manning: Add parameter to setup manning coef instead of strickler, and switch the value on the view only, but keeps strickler value in the model for the solvers 2026-07-27 13:22:32 +02:00
Dylan Jeannin 0076faf03a Temperature: propagate TEM when splitting reach 2026-07-27 09:31:29 +02:00
Dylan Jeannin bbdd18e8ef LateralContributions: propagate LAT when splitting reach + display only enabled reach in combobox 2026-07-24 15:35:12 +02:00
Dylan Jeannin 87d31c134e Merge branch 'dev_dylan' into split_reach 2026-07-24 14:40:29 +02:00
Dylan Jeannin ce04f5fd1e PEP8 2026-07-24 14:39:39 +02:00
Dylan Jeannin 435ccd1f2d Merge branch 'temperature_test' into dev_dylan 2026-07-24 14:34:50 +02:00
Dylan Jeannin 74c28d40a6 Merge branch 'master' into dev_dylan 2026-07-24 14:33:01 +02:00
Dylan Jeannin 7b2c276643 Mage: reset node name registry for each export + display only Mage/Rubar solver in hydraulic solvers list 2026-07-24 13:53:30 +02:00
Dylan Jeannin 0e1264ca4f Merge branch 'master' into temperature_test 2026-07-24 13:27:07 +02:00
Dylan Jeannin defcb3af84 BoundaryConditionsData: fix multidelete + relative undo/redo 2026-07-24 13:17:55 +02:00
Dylan Jeannin 5cf0a0ec76 Mesh: correction to make meshing thread-safe with tab display 2026-07-24 11:20:33 +02:00
Dylan Jeannin 1c44590def Meshing: unsigned distance to be able to mesh reach with start_rk > end_rk 2026-07-24 10:44:04 +02:00
Dylan Jeannin 53732744f4 Meshing: unsigned distance to be able to mesh reach with start_rk > end_rk 2026-07-24 10:40:52 +02:00
Dylan Jeannin 5e95c286a9 InitialConditionsAdisTS: split correctly reach and InitialConditionsAdisTS + display only enabled reach in tab 2026-07-23 15:57:50 +02:00
Dylan Jeannin 6175e8a03e Reach sediments: avoid crash when trying to open Edit_reach_sediment_layer without any reach selected 2026-07-23 15:39:07 +02:00
Dylan Jeannin 8e3d784e92 HydraulicStructures: split correctly reach and Hydraulic Structures + display only enabled reach in tab 2026-07-23 13:54:25 +02:00
Dylan Jeannin 698e300690 LateralContrib: split correctly reach and lateralContrib + display only enabled reach in tab 2026-07-23 11:54:49 +02:00
Dylan Jeannin e3fbe59585 SQLite debug: files with space in name can now be open 2026-07-23 10:12:01 +02:00
Dylan Jeannin 0c7fecb123 InitialCond: data copy with correct reach affectation after split 2026-07-23 10:10:56 +02:00
Dylan Jeannin 78eed7af13 SolverParams: quickfix for solverparams copy on scenario creation 2026-07-22 15:40:32 +02:00
Dylan Jeannin 6f5743a030 Merge dictionary 2026-07-22 13:31:59 +02:00
Dylan Jeannin d027946fb2 OutputRK: delete all references to this useless feature 2026-07-22 12:54:50 +02:00
Dylan Jeannin 37ea83d42a Lang: new words in french dictionnary 2026-07-22 11:26:30 +02:00
Dylan Jeannin ae39f33131 PEP8 2026-07-21 17:18:59 +02:00
Dylan Jeannin 291f80e904 Results AdisTT: zoom not reset when changing timestamp 2026-07-21 17:16:35 +02:00
Dylan Jeannin 61baaeeffb Scenario structure: adapting better structure for results with scenario mode + default data for weather parameters 2026-07-21 16:58:30 +02:00
Dylan Jeannin 68aabb343d WeatherParameters: informations for default values + no timeseries for ALB, SF and CLD 2026-07-21 15:54:06 +02:00
Dylan Jeannin f2d6e07931 WeatherParameters: better window names to be more clear for user 2026-07-21 15:26:38 +02:00
Dylan Jeannin 123259c2ef Duplicate window: avoid opening duplicate windows for boundary and initial conditions ADISTT 2026-07-21 15:06:44 +02:00
Dylan Jeannin 816d7663b5 AdisTT: new button on interface to open AdisTT results from file 2026-07-21 14:57:30 +02:00
Dylan Jeannin 9b9202c739 AdisTT: new results window, adapted from adists results 2026-07-21 14:00:59 +02:00
Dylan Jeannin 57f82415bd AdisTT: Avoid crash when pasting something unexpected in time series 2026-07-21 11:09:46 +02:00
Dylan Jeannin 36c93a0c93 AdisTT: cleaning code, delete useless functions 2026-07-20 16:22:49 +02:00
Dylan Jeannin 3cd1dbab49 AdisTT: default parameters update to fit better 2026-07-20 16:10:39 +02:00
Dylan Jeannin 4e3da95b1d Plot: Update plots after changing each data in tab 2026-07-20 15:51:05 +02:00
Dylan Jeannin 0d0c4f913e Merge branch 'dev_dylan' into temperature_test 2026-07-20 15:16:41 +02:00
Dylan Jeannin 600f86aeb4 Friction: combobox in tab for rk 2026-07-20 14:48:19 +02:00
Dylan Jeannin c9bb222bff WeatherParameters: Debug crash pamhyr when select a reach without geometry 2026-07-20 13:47:19 +02:00
Dylan Jeannin 456559ffe2 AdisTS & AdisTT: deleted data no more exported in solver files 2026-07-20 12:34:49 +02:00
Dylan Jeannin 6d58c09104 AdisTT: short correction on paste function in edit windows for WeatherParam 2026-07-17 17:47:33 +02:00
Dylan Jeannin bc87d6add1 AdisTT: corrections on writing TEM files 2026-07-17 17:21:52 +02:00
Dylan Jeannin 3fe49b70d0 Merge branch 'monopk-ic-adistt' into temperature_test 2026-07-17 17:04:37 +02:00
Dylan Jeannin 5287f30636 AdisTT: Handling cases where parameters do not have time series 2026-07-17 15:05:21 +02:00
Dylan Jeannin 67433ae325 AdisTS: delete duplicate function 2026-07-17 13:51:18 +02:00
Dylan Jeannin d27ce9d1d8 AdisTT: rewriting reach names in parser + copy the net folder from mage 2026-07-17 13:45:40 +02:00
Dylan Jeannin c8ea9bd774 Merge branch 'temperature_test' into monopk-ic-adistt 2026-07-17 10:12:30 +02:00
Dylan Jeannin 885c8ea502 Merge branch 'master' into temperature_test 2026-07-17 10:09:52 +02:00
Dylan Jeannin c8b32f9877 Merge branch 'master' into temperature_test 2026-07-17 10:05:42 +02:00
Dylan Jeannin cf61fd8d09 IC_temp: temperature set on a single RK rather than a segment 2026-07-17 10:03:46 +02:00
Dylan Jeannin cac20d4f24 AdisTT: user-friendly fixes on interface 2026-07-17 09:31:03 +02:00
Dylan Jeannin 965443a8f9 AdisTT: Managing tab of time series when it's disabled 2026-07-16 16:34:37 +02:00
Dylan Jeannin 02be084faf AdisTT: Fake 0.0 in files to match AdisTS pollutants description 2026-07-16 12:56:46 +02:00
Dylan Jeannin bb2e83b6ce AdisTT: Stable version of files writer for adisTT solver 2026-07-16 10:44:01 +02:00
JeaDylan 4a94c946d1 WeatherParameters: add name of parameter in spec tab 2026-07-09 18:40:27 +02:00
JeaDylan 2dca5d2905 WeatherParameters: automatic selection when we open the window + monoselection in default tab 2026-07-09 15:44:32 +02:00
JeaDylan b5ae352af5 WeatherParameters: deleted variable not useful in default params 2026-07-09 13:47:23 +02:00
JeaDylan 868215705c WeatherParameters: New data in model+db to setup default parameters 2026-07-09 11:43:16 +02:00
Dylan Jeannin 6d16d9dff4 Fix PEP8 2026-07-06 15:45:47 +02:00
Dylan Jeannin b60ab881b1 Weather parameters: time series for each parameter, add/del/sort functions working 2026-07-06 15:37:45 +02:00
Dylan Jeannin f2d76a0d67 WeatherParameters: button connections in another function to clear code 2026-07-06 12:31:01 +02:00
Dylan Jeannin 5dcc82d81e WeatherParameters: Correct add and implement del function on tab_spec 2026-07-06 11:47:37 +02:00
Dylan Jeannin a01e47f205 Weather Parameters: Display relative parameters in spec tab 2026-07-06 10:26:49 +02:00
Dylan Jeannin c87563b7ed Cleaning some code 2026-07-06 09:57:05 +02:00
Dylan Jeannin 246b411114 Weather params: Dual tab for parameters with add functionnality 2026-07-03 17:07:34 +02:00
JeaDylan b2ca1c2ab7 WeatherParameters: Add first parameters view + model 2026-06-30 08:49:35 +02:00
Dylan Jeannin 2cbfaec0c2 Boundary Conditions: interface for temperature's boundary conditions 2026-06-26 19:02:49 +02:00
Dylan Jeannin 95dd648b90 Temperature: Interface for temperature's initial conditions 2026-06-26 17:36:42 +02:00
Dylan Jeannin 333a8a0970 Temperature: New tab in UI 2026-06-25 17:13:02 +02:00
229 changed files with 20633 additions and 3236 deletions

No files matched your search

+1
View File
@@ -44,6 +44,7 @@ __old__
mage8/
adists/
rubarbe/
rubar/
linux/
windows/
+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
@@ -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
+48 -2
View File
@@ -229,7 +229,11 @@ for each reach"
gls = []
for edge in edges:
comp, incomp = edge.reach.compute_guidelines()
profiles = edge.reach.enabled_profiles
comp, incomp = edge.reach.compute_guidelines(
profiles=profiles,
update_cache=False
)
if len(incomp) != 0:
self._status = STATUS.WARNING
self._summary = "incomplete_guideline"
@@ -237,7 +241,6 @@ for each reach"
gls.append(comp)
profiles = edge.reach.profiles
for profile in profiles:
if not profile.has_standard_named_points():
self._status = STATUS.WARNING
@@ -251,3 +254,46 @@ for each reach"
return False
return ok
class MageLateralSolidChecker(AbstractModelChecker):
"""Mage has no solid lateral contribution (notice: only upstream
solid discharges, QSO): the solid ones are not exported (LAT holds
liquid discharges only). Non blocking warning"""
def __init__(self, version="mage8"):
super(MageLateralSolidChecker, self).__init__()
self._name = _translate(
"Checker", "Mage solid lateral contributions checker")
self._description = _translate(
"Checker",
"Check if the study has solid lateral contributions, which "
"Mage does not take into account"
)
self._solver = version
self._modules = Modules.LATERAL_CONTRIBUTION
def run(self, study):
if not self.basic_check(study):
return False
edges = study.river.enable_edges()
names = [
lc.name
for lc in study.river.lateral_contribution.get_tab("solid")
if not lc.is_deleted() and lc.reach in edges and len(lc) > 0
]
if len(names) == 0:
self._status = STATUS.OK
self._summary = "ok"
return True
self._status = STATUS.WARNING
self._summary = _translate(
"Checker",
"Solid lateral contributions not taken into account by Mage: "
"{names}"
).format(names=", ".join(names))
return False
+255
View File
@@ -0,0 +1,255 @@
# RubarBE.py -- Pamhyr RubarBE checkers
# 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 -*-
"""RubarBE checkers. Each one runs a step of the checks of the RubarBE
export (Solver/RubarBE.py), without writing any file, so that they
always give the messages of the export: an error where the export would
be cancelled, a warning for the data it ignores (empty boundary
conditions, lateral contributions not read, solid lateral contributions
without liquid one, structures without RubarBE equivalent...)."""
import logging
from PyQt5.QtCore import QCoreApplication
from Modules import Modules
from tools import logger_exception
from Checker.Checker import AbstractModelChecker, STATUS
_translate = QCoreApplication.translate
logger = logging.getLogger()
class RubarBEChecker(AbstractModelChecker):
"""Base class: _check(solver, study, context, warnings) runs the
export step, raising RubarExportError (error) or appending to
warnings (warning). Steps that need the reach are not run when the
study has not a single valid reach: RubarBEReachChecker reports it"""
_needs_reach = True
def __init__(self, name, description, modules, version="rubarbe"):
super(RubarBEChecker, self).__init__()
self._name = name
self._description = description
self._solver = version
self._modules = modules
@staticmethod
def _context(solver, study):
# Read the parameters without creating them (get_params would
# add default ones to the study)
river = study.river
if solver.type in river._parameters:
values = solver._parameter_values(study)
else:
values = {k: str(v).strip()
for k, v in solver.default_parameters()}
return {
"values": values,
"trased": values.get("rubarbe_trased", "n").lower()
in ("y", "o"),
}
def _check(self, solver, study, context, warnings):
raise NotImplementedError
def run(self, study):
from Solver.RubarBE import RubarBE, RubarExportError
if not self.basic_check(study):
return False
solver = RubarBE("checker")
warnings = []
try:
context = self._context(solver, study)
if self._needs_reach:
try:
edge, profiles = solver._single_reach(study.river)
except RubarExportError:
self._status = STATUS.OK
self._summary = "ok"
return True
context["hydraulics"] = {
"edge": edge,
"profiles": profiles,
"tinit": solver._seconds(
context["values"].get("rubarbe_tinit", "0")),
"warnings": warnings,
}
self._check(solver, study, context, warnings)
except (RubarExportError, ValueError) as error:
self._status = STATUS.ERROR
self._summary = str(error)
return False
except Exception as error:
# Never stop the checker thread
logger_exception(error)
self._status = STATUS.UNKNOWN
self._summary = str(error)
return False
if warnings:
self._status = STATUS.WARNING
self._summary = " | ".join(warnings)
return False
self._status = STATUS.OK
self._summary = "ok"
return True
class RubarBEReachChecker(RubarBEChecker):
_needs_reach = False
def __init__(self, version="rubarbe"):
super(RubarBEReachChecker, self).__init__(
_translate("Checker", "RubarBE reach checker"),
_translate(
"Checker",
"Check that the study has a single enabled reach, with at "
"least two profiles of increasing rk"
),
Modules.GEOMETRY, version,
)
def _check(self, solver, study, context, warnings):
solver._single_reach(study.river)
class RubarBEGeometryChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEGeometryChecker, self).__init__(
_translate("Checker", "RubarBE geometry and sediment checker"),
_translate(
"Checker",
"Check the main channel and active bed limits of the "
"profiles and, with sediment transport, their sediment "
"layers"
),
# Rerun on changes of the geometry, which impact the sediment
# layers
Modules.SEDIMENT_LAYER, version,
)
def _check(self, solver, study, context, warnings):
solver._geometry_data(context["hydraulics"]["edge"],
context["trased"])
class RubarBEBoundaryChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEBoundaryChecker, self).__init__(
_translate("Checker", "RubarBE boundary conditions checker"),
_translate(
"Checker",
"Check the liquid and solid boundary conditions used by "
"the boundary settings of the solver parameters"
),
Modules.BOUNDARY_CONDITION, version,
)
def _check(self, solver, study, context, warnings):
values = context["values"]
edge = context["hydraulics"]["edge"]
solver._check_regime(values)
solver._check_boundaries(study.river, edge, values, warnings)
solver._solid_boundary(study.river, edge, values,
context["trased"], warnings)
class RubarBEFrictionChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEFrictionChecker, self).__init__(
_translate("Checker", "RubarBE friction checker"),
_translate("Checker", "Check that the reach has frictions"),
Modules.FRICTION, version,
)
def _check(self, solver, study, context, warnings):
solver._interpolated_frictions(context["hydraulics"]["edge"])
class RubarBEInitialConditionsChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEInitialConditionsChecker, self).__init__(
_translate("Checker", "RubarBE initial conditions checker"),
_translate(
"Checker", "Check the initial conditions of the reach"),
Modules.INITIAL_CONDITION, version,
)
def _check(self, solver, study, context, warnings):
solver._initial_conditions(study.river,
context["hydraulics"]["edge"])
class RubarBELateralChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBELateralChecker, self).__init__(
_translate("Checker", "RubarBE lateral contributions checker"),
_translate(
"Checker",
"Check the lateral contributions: read only with lateral "
"overflows, solid ones attached to the liquid contribution "
"of the same sections"
),
Modules.LATERAL_CONTRIBUTION, version,
)
def _check(self, solver, study, context, warnings):
solver._lateral_data(study.river, context["hydraulics"],
context["values"], context["trased"], warnings)
class RubarBEStructureChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEStructureChecker, self).__init__(
_translate("Checker", "RubarBE hydraulic structures checker"),
_translate(
"Checker",
"Check the hydraulic structures: weirs and rectangular and "
"circular orifices only"
),
Modules.HYDRAULIC_STRUCTURES, version,
)
def _check(self, solver, study, context, warnings):
solver._structure_data(study.river, context["hydraulics"], warnings)
class RubarBESavesChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBESavesChecker, self).__init__(
_translate("Checker", "RubarBE saves checker"),
_translate(
"Checker",
"Check the save abscissas (ABSHYD) and times (TNPROF) of "
"the solver parameters"
),
# The solver parameters have no module: rerun on any change
Modules.NONE, version,
)
def _check(self, solver, study, context, warnings):
solver._save_data(context["values"], context["hydraulics"])
+7 -3
View File
@@ -87,7 +87,7 @@ class StudyGeometryChecker(AbstractModelChecker):
return False
for edge in edges:
if len(edge.reach.profiles) < 2:
if len(edge.reach.enabled_profiles) < 2:
summary = "no_geometry_defined"
status = STATUS.ERROR
ok = False
@@ -132,8 +132,12 @@ class StudyInitialConditionsChecker(AbstractModelChecker):
return ok
ic = river.initial_conditions[reach]
len_ic = len(ic)
len_reach = len(reach)
enabled_profiles = set(reach.enabled_profiles)
len_ic = sum(
data["section"] in enabled_profiles
for data in ic.data
)
len_reach = len(enabled_profiles)
if len_ic < len_reach:
self._summary = "initial_condition_missing_profile"
+114 -64
View File
@@ -57,6 +57,13 @@ class InternalMeshing(AMeshingTool):
# we never modify original profiles, we work on the copies
m_profiles = self.st_to_m(profiles, guide_list)
# Adapt the spacing to the kilometre-point direction. The value from
# the dialog is a distance, so only its magnitude is significant.
step = self.signed_step(
step, m_profiles[0].rk, m_profiles[-1].rk
)
new_profiles = self.interpolate_transversal_step(m_profiles, step)
for new_profiles2 in new_profiles:
@@ -65,6 +72,12 @@ class InternalMeshing(AMeshingTool):
return new_profiles
@staticmethod
def signed_step(step, begin_rk, end_rk):
if end_rk < begin_rk:
return -abs(step)
return abs(step)
def st_to_m(self, profiles, guide_list):
guide_list = ["un"] + guide_list + ["np"]
max_values = [0] * (len(guide_list) - 1)
@@ -124,10 +137,11 @@ class InternalMeshing(AMeshingTool):
p.points[k].z = pt1[k].z + \
dj*(pt2[k].z -
pt1[k].z)
p.rk = profiles[i].rk + \
dj*(profiles[i+1].rk -
profiles[i].rk)
p.rk = round(profiles[i].rk +
dj*(profiles[i+1].rk -
profiles[i].rk), 2)
p.name = f'interpol{p.rk}'
p.interpolated = True
new_profiles2.append(p)
new_profiles.append(new_profiles2)
@@ -158,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 = ''
@@ -182,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":
@@ -250,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):
@@ -0,0 +1,481 @@
# BoundaryConditionsTemperature.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class Data(SQLSubModel):
_sub_classes = []
def __init__(self,
data0, data1,
id: int = -1,
types=[float, float],
status=None,
owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._types = types
self._data = [data0, data1]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_data_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
bca INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(bca)
REFERENCES boundary_condition_temperature(pamhyr_id)
)
""")
return True
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and minor == "0":
if int(release) < 11:
cls._db_create(execute)
created = True
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute,
"boundary_condition_data_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
bca = data["bca"]
table = execute(
"SELECT pamhyr_id, deleted, data0, data1, scenario FROM " +
"boundary_condition_data_temperature " +
f"WHERE scenario = {scenario.id} " +
# f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
f"AND bca = {bca.id}"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
data0 = next(it)
data1 = next(it)
owner_scenario = next(it)
bc = cls(
data0, data1,
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
bca = data["bca"]
data0 = self._db_format(self._data[0])
data1 = self._db_format(self._data[1])
execute(
"INSERT INTO " +
"boundary_condition_data_temperature(" +
"pamhyr_id, deleted, data0, data1, bca, scenario) " +
f"VALUES ({self.id}, {self._db_format(self.is_deleted())}, " +
f"'{data0}', {data1}, {bca.id}, " +
f"{self._status.scenario_id})"
)
return True
def __getitem__(self, key):
return self._types[key](self._data[key])
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
class BoundaryConditionTemperature(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1, status=None, owner_scenario=-1):
super(BoundaryConditionTemperature, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = ""
self._node = None
self._data = []
self._header = []
self._types = [self.time_convert, float]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
node INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and (int(minor) < 2 or (
int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute,
"boundary_condition_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
nodes = data['nodes']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
sql = (
"SELECT pamhyr_id, deleted, name, node, scenario "
"FROM boundary_condition_temperature " +
f"WHERE scenario = {scenario.id} "
)
table = execute(sql)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
bc_name = next(it)
node = next(it)
owner_scenario = next(it)
bc = cls(
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
bc.name = bc_name
bc.node = None
if node != -1:
tmp = next(
filter(
lambda n: n.id == node, nodes
),
None
)
if tmp is not None:
bc.node = tmp.id
else:
bc.node = -1
data["bca"] = bc
bc._data = Data._db_load(execute, data=data)
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM boundary_condition_temperature " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE bca = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
node = -1
if self._node is not None:
node = self._node
execute(
"INSERT INTO " +
"boundary_condition_temperature(" +
"pamhyr_id, deleted, name, " +
"node, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', {node}, " +
f"{self._status.scenario_id}" +
")"
)
data["bca"] = self
ind = 0
for d in self._data:
data["ind"] = ind
d._db_save(execute, data)
ind += 1
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for d in self._data:
d._data_traversal(predicate, modifier, data)
def __len__(self):
return len(
list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
)
@classmethod
def time_convert(cls, data):
if type(data) is str:
if data.count("-") == 2:
return date_iso_to_timestamp(data)
if data.count("/") == 2:
return date_dmy_to_timestamp(data)
if data.count(":") == 3:
return old_pamhyr_date_to_timestamp(data)
if data.count(":") == 2:
return old_pamhyr_date_to_timestamp("00:" + data)
if data.count(".") == 1:
return round(float(data))
return int(data)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def node(self):
return self._node
@node.setter
def node(self, node):
self._node = node
self.modified()
@property
def header(self):
return self._header.copy()
@header.setter
def header(self, header):
self._header = header
self.modified()
@property
def data(self):
return list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property
def _default_0(self):
return self._types[0](0)
@property
def _default_1(self):
return self._types[1](0.0)
def new_from_data(self, header, data):
new_0 = self._default_0
new_1 = self._default_1
if len(header) != 0:
for i in [0, 1]:
for j in range(len(header)):
if self._header[i] == header[j]:
if i == 0:
new_0 = self._types[i](data[j].replace(",", "."))
else:
new_1 = self._types[i](data[j].replace(",", "."))
else:
new_0 = self._types[0](data[0].replace(",", "."))
new_1 = self._types[1](data[1].replace(",", "."))
return Data(new_0, new_1, types=self._types, status=self._status)
def add(self, index: int):
value = Data(
self._default_0, self._default_1,
status=self._status
)
self._data.insert(index, value)
self.modified()
return value
def insert(self, index: int, value):
if value in self._data:
value.set_as_not_deleted()
else:
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self.data)
)
)
)
self.modified()
def sort(self, _reverse=False, key=None):
if key is None:
self._data.sort(reverse=_reverse)
else:
self._data.sort(reverse=_reverse, key=key)
self.modified()
def index(self, bc):
self._data.index(bc)
def get_i(self, index):
return self.data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index, column, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)
@@ -1,5 +1,5 @@
# OutputRKListAdists.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
# BoundaryConditionsTemperatureList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,44 +16,56 @@
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.OutputRKAdists.OutputRKAdists import OutputRKAdists
from Model.Except import NotImplementedMethodeError
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
class OutputRKAdistsList(PamhyrModelList):
_sub_classes = [OutputRKAdists]
class BoundaryConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
BoundaryConditionTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"])
new = cls(status=data['status'])
new._lst = OutputRKAdists._db_load(execute, data)
if data is None:
data = {}
new._lst = BoundaryConditionTemperature._db_load(execute, data)
return new
def _db_save(self, execute, data=None):
ok = True
# Delete previous data
execute(
"DELETE FROM output_rk_adists " +
"DELETE FROM boundary_condition_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
for sl in self._lst:
ok &= sl._db_save(execute, data)
if data is None:
data = {}
return ok
for bca in self._lst:
bca._db_save(execute, data=data)
@property
def OutputRK_List(self):
return self.lst
return True
def new(self, lst, index):
n = OutputRKAdists(status=self._status)
def new(self, index):
n = BoundaryConditionTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
@property
def BCs_Temperature_List(self):
return self.lst
+41
View File
@@ -275,3 +275,44 @@ class D90AdisTS(SQLSubModel):
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
parts = [
(reach1.id, min(reach_rks), split_rk),
(reach2.id, split_rk, max(reach_rks)),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
+4
View File
@@ -61,6 +61,10 @@ class D90AdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for d90 in self.lst:
d90.split_reach(reach, profile, reach1, reach2)
@property
def D90_AdisTS_List(self):
return self.lst
+10
View File
@@ -261,3 +261,13 @@ class D90AdisTSSpec(SQLSubModel):
def enabled(self, enabled):
self._enabled = enabled
self.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = D90AdisTSSpec(name=self._name_section, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._d90 = self._d90
new._enabled = self._enabled
new.modified()
return new
+41
View File
@@ -329,3 +329,44 @@ class DIFAdisTS(SQLSubModel):
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
parts = [
(reach1.id, min(reach_rks), split_rk),
(reach2.id, split_rk, max(reach_rks)),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
+4
View File
@@ -61,6 +61,10 @@ class DIFAdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for dif in self.lst:
dif.split_reach(reach, profile, reach1, reach2)
@property
def DIF_AdisTS_List(self):
return self.lst
+12
View File
@@ -293,3 +293,15 @@ class DIFAdisTSSpec(SQLSubModel):
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = DIFAdisTSSpec(method=self._method, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._dif = self._dif
new._b = self._b
new._c = self._c
new._enabled = self._enabled
new.modified()
return new
@@ -1,4 +1,4 @@
# Function.py -- Pamhyr
# Distribution.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
@@ -30,13 +30,13 @@ from Model.Scenario import Scenario
logger = logging.getLogger()
class Function(SQLSubModel):
class Distribution(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Function, self).__init__(
super(Distribution, self).__init__(
id=id, status=status,
owner_scenario=0
)
@@ -44,15 +44,27 @@ class Function(SQLSubModel):
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_function{ext} (
CREATE TABLE ensemble_distribution{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
@@ -85,7 +97,7 @@ class Function(SQLSubModel):
table = execute(
"SELECT pamhyr_id, deleted, name, script " +
"FROM ensemble_function " +
"FROM ensemble_distribution " +
f"WHERE pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
@@ -97,20 +109,20 @@ class Function(SQLSubModel):
name = next(it)
script = next(it)
new_function = cls(
new_distribution = cls(
id, name=name, script=script,
status=data["status"]
)
if deleted:
new_function.set_as_deleted()
new_distribution.set_as_deleted()
loaded.add(id)
new.append(new_function)
new.append(new_distribution)
return new
def _db_save(self, execute, data=None):
# Only save custom function
# Only save custom distribution
if self._type != "custom":
return True
@@ -119,7 +131,7 @@ class Function(SQLSubModel):
execute(
"INSERT INTO " +
"ensemble_function(pamhyr_id, deleted, " +
"ensemble_distribution(pamhyr_id, deleted, " +
" name, script) " +
"VALUES (?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
@@ -135,15 +147,30 @@ class Function(SQLSubModel):
if predicate(self, data):
modifier(self, data)
def get_sample(self, r):
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)
###########################
# FUNCTION IMPLEMENTATION #
###########################
###############################
# DISTRIBUTION IMPLEMENTATION #
###############################
class Uniform(Function):
class Uniform(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
@@ -154,19 +181,106 @@ class Uniform(Function):
name=name, script=script
)
def get_sample(self, ens_range, nb):
b = ens_range[0]
t = ens_range[1]
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"]
step = (t - b) / nb
def check_params(self, params):
return (params[0] < params[1])
r = np.arange(b, t, step)
np.random.shuffle(r)
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return r
return np.random.uniform(*lst)
class Custom(Function):
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,
@@ -177,13 +291,17 @@ class Custom(Function):
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 get_sample(self, ens_range, nb):
def random(self, params, nb):
try:
# Run script to (re)define the sample function
# Run script to (re)define the sample distribution
f = eval(self._script)
return f(ens_range, nb)
return f(params, nb)
except Exception as e:
logger_exception(e)
return None # TODO: Raise helpful exception
@@ -1,4 +1,4 @@
# FunctionList.py -- Pamhyr
# DistributionList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
@@ -19,23 +19,27 @@
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Function import *
from Model.Ensembles.Distribution import *
class FunctionList(PamhyrModelList):
_sub_classes = [Function]
class DistributionList(PamhyrModelList):
_sub_classes = [Distribution]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
# Default functions
# Default distributions
d = {"status": data['status']}
new._lst = [
Uniform(status=data['status']),
Uniform(**d),
Normal(**d),
Binomial(**d),
Poisson(**d),
]
# DB custom functions
new._lst += Function._db_load(
# DB custom distributions
new._lst += Distribution._db_load(
execute, data
)
@@ -43,7 +47,7 @@ class FunctionList(PamhyrModelList):
def _db_save(self, execute, data={}):
execute(
"DELETE FROM ensemble_function"
"DELETE FROM ensemble_distribution"
)
for fun in self._lst:
+48 -23
View File
@@ -24,7 +24,7 @@ import numpy as np
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Ensembles.Function import Uniform
from Model.Ensembles.Distribution import Uniform
logger = logging.getLogger()
@@ -33,7 +33,7 @@ class Ensemble(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
function=None, params=[0., 100.],
distribution=None, params=[0., 100.],
data_type=None, target_data=None,
status=None, owner_scenario=-1):
super(Ensemble, self).__init__(
@@ -43,7 +43,7 @@ class Ensemble(SQLSubModel):
self._name = name
self._data_type = data_type
self._function = function
self._distribution = distribution
self._params = params
self._target_data = target_data
@@ -55,8 +55,8 @@ class Ensemble(SQLSubModel):
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type VARCHAR(8) NOT NULL,
function INTEGER NOT NULL,
function_name VARCHAR(32) 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,
@@ -86,13 +86,13 @@ class Ensemble(SQLSubModel):
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
functions = data["ens_functions"]
distributions = data["ens_distributions"]
stricklers = data["stricklers"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"function, function_name, params, params_len, data_pid, " +
"distribution, distribution_name, params, params_len, data_pid, " +
"scenario " +
"FROM ensemble " +
f"WHERE scenario = {scenario.id} " +
@@ -117,8 +117,8 @@ class Ensemble(SQLSubModel):
if fid != -1:
def fn(f): return f._pamhyr_id == fid
function = next(
filter(fn, functions.lst),
distribution = next(
filter(fn, distributions.lst),
None
)
@@ -130,11 +130,11 @@ class Ensemble(SQLSubModel):
None
)
rdata = cls._decode_params(params, params_len)
rdata = list(cls._decode_params(params, params_len))
new_ensemble = cls(
id, name=name,
function=function,
distribution=distribution,
params=rdata,
data_type=data_type,
target_data=target_data,
@@ -161,14 +161,14 @@ class Ensemble(SQLSubModel):
bparams, length = self._encode_params()
if self._function is None:
if self._distribution is None:
fid = -1
fname = "NULL"
else:
fid = self._function._pamhyr_id
fname = self._function._name
fid = self._distribution._pamhyr_id
fname = self._distribution._name
if self._function._type == "generic":
if self._distribution._type == "generic":
fid = -1
data_pid = -1
@@ -178,7 +178,7 @@ class Ensemble(SQLSubModel):
execute(
"INSERT INTO " +
"ensemble(pamhyr_id, deleted, name, type, " +
" function, function_name, params, params_len, " +
" distribution, distribution_name, params, params_len, " +
" data_pid, scenario) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
@@ -201,9 +201,9 @@ class Ensemble(SQLSubModel):
return struct.unpack(data_format, bparams)
def get_sample(self, nb):
def random(self, nb):
return np.nditer(
self._function.get_sample(
self._distribution.random(
self._params, nb
)
)
@@ -218,12 +218,12 @@ class Ensemble(SQLSubModel):
self._status.modified()
@property
def function(self):
return self._function
def distribution(self):
return self._distribution
@function.setter
def function(self, function):
self._function = function
@distribution.setter
def distribution(self, distribution):
self._distribution = distribution
self._status.modified()
@property
@@ -252,3 +252,28 @@ class Ensemble(SQLSubModel):
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"]
+9
View File
@@ -51,3 +51,12 @@ class EnsembleList(PamhyrModelList):
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
+13 -7
View File
@@ -109,16 +109,22 @@ class FrictionList(PamhyrModelList):
((999 ** 100), -(999 ** 100))
)
frictions = list(
filter(
lambda f: ((rk_min <= f.begin_rk <= rk_max) or
(rk_min <= f.end_rk <= rk_max)),
self._lst))
for friction in self.lst:
lower, upper = sorted((friction.begin_rk, friction.end_rk))
clipped_lower = max(lower, rk_min)
clipped_upper = min(upper, rk_max)
if clipped_lower >= clipped_upper:
continue
for ind, friction in enumerate(frictions):
n = friction.cloned_for(reach)
if friction.begin_rk > friction.end_rk:
n.begin_rk = clipped_upper
n.end_rk = clipped_lower
else:
n.begin_rk = clipped_lower
n.end_rk = clipped_upper
new._lst.insert(ind, n)
new._lst.append(n)
new._status.modified()
return new
+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):
"""
+9
View File
@@ -328,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):
@@ -337,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):
@@ -346,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
+90 -17
View File
@@ -17,6 +17,7 @@
# -*- coding: utf-8 -*-
import queue
import types
import random
import logging
import numpy as np
@@ -39,9 +40,9 @@ logger = logging.getLogger()
@dataclass
class Tabulation:
z: np.array([])
A: np.array([])
L: np.array([])
z: np.ndarray
A: np.ndarray
L: np.ndarray
class ProfileXYZ(Profile, SQLSubModel):
@@ -58,7 +59,8 @@ class ProfileXYZ(Profile, SQLSubModel):
num=0,
nb_point: int = 0,
code1: int = 0, code2: int = 0,
status=None, owner_scenario=-1):
status=None, owner_scenario=-1,
enabled=True, interpolated=False):
"""ProfileXYZ constructor
Args:
@@ -90,6 +92,8 @@ class ProfileXYZ(Profile, SQLSubModel):
self.time_l = 0.0
self._station = []
self.station_up_to_date = False
self._enabled = bool(enabled)
self._interpolated = bool(interpolated)
self._get_water_limits_cache = {}
self._get_water_limits_ac_cache = {}
@@ -108,6 +112,8 @@ class ProfileXYZ(Profile, SQLSubModel):
code1 INTEGER NOT NULL,
code2 INTEGER NOT NULL,
sl INTEGER,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interpolated BOOLEAN NOT NULL DEFAULT FALSE,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
@@ -152,6 +158,27 @@ class ProfileXYZ(Profile, SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and int(minor) <= 2:
if int(release) < 10:
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE"
)
if major == "0" and (int(minor) < 2 or
(minor == "2" and int(release) < 10)):
if minor != "0":
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN interpolated BOOLEAN NOT NULL DEFAULT FALSE"
)
execute(
"UPDATE geometry_profileXYZ " +
"SET interpolated = CASE " +
"WHEN LOWER(name) LIKE 'interpol%' THEN TRUE " +
"ELSE FALSE END"
)
return cls._update_submodel(execute, version, data)
@classmethod
@@ -237,7 +264,7 @@ class ProfileXYZ(Profile, SQLSubModel):
table = execute(
"SELECT pamhyr_id, ind, deleted, name, rk, num, " +
"code1, code2, sl, scenario " +
"code1, code2, sl, scenario, enabled, interpolated " +
"FROM geometry_profileXYZ " +
f"WHERE reach = {reach.id} " +
f"AND scenario = {scenario.id} " +
@@ -258,6 +285,8 @@ class ProfileXYZ(Profile, SQLSubModel):
code2 = next(it)
sl = next(it)
owner_scenario = next(it)
enabled = (next(it) == 1)
interpolated = (next(it) == 1)
profile = cls(
id=pid, ind=ind, num=num,
@@ -265,7 +294,9 @@ class ProfileXYZ(Profile, SQLSubModel):
code1=code1, code2=code2,
reach=reach,
status=status,
owner_scenario=owner_scenario
owner_scenario=owner_scenario,
enabled=enabled,
interpolated=interpolated
)
if deleted:
profile.set_as_deleted()
@@ -318,12 +349,14 @@ class ProfileXYZ(Profile, SQLSubModel):
execute(
"INSERT OR REPLACE INTO " +
"geometry_profileXYZ(pamhyr_id, deleted, ind, name, reach, " +
"rk, num, code1, code2, sl, scenario) " +
"rk, num, code1, code2, sl, scenario, enabled, interpolated) " +
"VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(self._name)}', " +
f"{self.reach.pamhyr_id}, {self.rk}, {self.num}, " +
f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}" +
f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, " +
f"{self._db_format(self._enabled)}, " +
f"{self._db_format(self._interpolated)}" +
")"
)
@@ -390,7 +423,9 @@ class ProfileXYZ(Profile, SQLSubModel):
name=self.name,
rk=self.rk,
reach=self.reach,
status=self._status
status=self._status,
enabled=self.is_enabled,
interpolated=self.interpolated
)
if self.is_deleted():
new_p.set_as_deleted()
@@ -410,7 +445,9 @@ class ProfileXYZ(Profile, SQLSubModel):
name=self.name,
rk=self.rk,
reach=new_reach,
status=self._status
status=self._status,
enabled=self.is_enabled,
interpolated=self.interpolated
)
if self.is_deleted():
new_p.set_as_deleted()
@@ -523,7 +560,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.
@@ -588,7 +625,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.
@@ -607,7 +644,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
@@ -806,7 +843,11 @@ class ProfileXYZ(Profile, SQLSubModel):
return poly
def wet_points(self, z):
left, right = self.get_water_limits(z)
limits = self.get_water_limits(z)
if isinstance(limits, types.GeneratorType):
limits = next(limits)
left, right = limits
points = list(filter(lambda p: p.z < z, self._points))
points = [left] + points + [right]
@@ -826,6 +867,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):
"""
@@ -967,7 +1015,7 @@ class ProfileXYZ(Profile, SQLSubModel):
Projection of the points of the profile on a plane.
"""
if self.nb_points < 2:
return [0.0]
return [0.0] * self.nb_points
else:
if self.station_up_to_date:
return self._station
@@ -1131,11 +1179,14 @@ class ProfileXYZ(Profile, SQLSubModel):
p = ProfileXYZ(name=self.name,
rk=self.rk,
reach=self.reach,
status=self._status)
status=self._status,
enabled=self.is_enabled,
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):
@@ -1165,3 +1216,25 @@ class ProfileXYZ(Profile, SQLSubModel):
break
self._points = points
self.modified()
@property
def is_enabled(self):
return self._enabled
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, value):
self._enabled = bool(value)
self.modified()
@property
def interpolated(self):
return self._interpolated
@interpolated.setter
def interpolated(self, value):
self._interpolated = bool(value)
self.modified()
+65 -9
View File
@@ -80,11 +80,13 @@ class Reach(SQLSubModel):
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"], parent=data["reach"])
profile_data = data.copy()
profile_data["reach"] = new
new._profiles = sorted(
ProfileXYZ._db_load(
execute,
data=data.copy()
data=profile_data
),
key=lambda p: p._db_ind
)
@@ -157,6 +159,14 @@ class Reach(SQLSubModel):
def profiles(self, profiles):
self._profiles = profiles
@property
def enabled_profiles(self):
return list(filter(lambda p: p.is_enabled, self.profiles))
@property
def number_enabled_profiles(self):
return len(self.enabled_profiles)
def get_profiles_from_rk(self, rk):
return list(
filter(
@@ -367,6 +377,28 @@ class Reach(SQLSubModel):
)
)
def get_z_min_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_min(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_z_max_enabled_profiles(self):
return list(
map(
lambda profile: profile.z_max(),
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk(self):
"""List of profiles rk
@@ -375,6 +407,17 @@ class Reach(SQLSubModel):
"""
return [profile.rk for profile in self.profiles]
def get_rk_enabled_profiles(self):
return list(
map(
lambda profile: profile.rk,
filter(
lambda profile: len(profile) > 0 and profile.is_enabled,
self.profiles
)
)
)
def get_rk_complete_profiles(self):
return list(
map(
@@ -486,14 +529,22 @@ class Reach(SQLSubModel):
)
@timer
def compute_guidelines(self):
def compute_guidelines(self, profiles=None, update_cache=True):
"""Compute reach guidelines
Args:
profiles: Profiles used to compute the guidelines. If omitted,
all non-deleted profiles of the reach are used.
update_cache: Whether to update the reach guidelines cache.
Returns:
Tuple of complete and incomplete guidelines name.
"""
if profiles is None:
profiles = self.profiles
# Get all point contained into a guideline
named_points = [profile.named_points() for profile in self.profiles]
named_points = [profile.named_points() for profile in profiles]
points_name = list(
map(
lambda lst: list(map(lambda p: p.name, lst)),
@@ -524,11 +575,13 @@ class Reach(SQLSubModel):
complete = guide_set - incomplete
# Compute guideline and put data in cache
self._compute_guidelines_cache(guide_set, named_points,
complete, incomplete)
if update_cache:
# Compute guideline and put data in cache
self._compute_guidelines_cache(
guide_set, named_points, complete, incomplete
)
self.modified()
self.modified()
return (complete, incomplete)
def _map_guidelines_points(self, func, full=False):
@@ -752,8 +805,11 @@ class Reach(SQLSubModel):
list_profile.append(list_point_profile)
list_point_profile = []
else:
line.append("")
list_point_profile.append(line[:3])
# MAGE-8 ST files can append hydraulic data
# after the regular x, y, z and point-name
# fields. ProfileXYZ still expects exactly
# those first four fields.
list_point_profile.append(line[:4])
else:
pass
@@ -277,3 +277,10 @@ class BasicHS(SQLSubModel):
def convert(self, new_type):
return new_type(id=self.id, name=self.name, status=self._status)
def cloned(self):
new = type(self)(name=self._name, status=self._status)
new._enabled = self._enabled
new._data = [value.cloned() for value in self._data]
new.modified()
return new
@@ -208,3 +208,11 @@ class BHSValue(SQLSubModel):
def value(self, value):
self._value = self._type(value)
self.modified()
def cloned(self):
return BHSValue(
name=self._name,
type=self._type,
value=self._value,
status=self._status
)
@@ -47,9 +47,7 @@ class HydraulicStructure(SQLSubModel):
self._name = name
self._input_section = None
self._output_section = None
self._input_reach = None
self._output_reach = None
self._enabled = True
self._data = []
@@ -62,17 +60,12 @@ class HydraulicStructure(SQLSubModel):
name TEXT NOT NULL,
enabled BOOLEAN NOT NULL,
input_reach INTEGER,
output_reach INTEGER,
input_section INTEGER,
output_section INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(input_reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(output_reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(input_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
FOREIGN KEY(output_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
@@ -92,13 +85,12 @@ class HydraulicStructure(SQLSubModel):
return True
if rl < 11:
for v in ["input", "output"]:
execute(
f"""
ALTER TABLE hydraulic_structures
RENAME COLUMN {v}_kp TO {v}_rk
"""
)
execute(
"""
ALTER TABLE hydraulic_structures
RENAME COLUMN input_kp TO input_rk
"""
)
cls._db_update_to_0_2_0(execute, data)
@@ -112,8 +104,28 @@ class HydraulicStructure(SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and (int(minor) < 2 or
(minor == "2" and rl < 10)):
cls._db_update_to_0_2_10(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_10(cls, execute):
table = "hydraulic_structures"
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, deleted, name, enabled, input_section, " +
"input_reach, scenario) " +
"SELECT pamhyr_id, deleted, name, enabled, input_section, " +
f"input_reach, scenario FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "hydraulic_structures"
@@ -131,11 +143,10 @@ class HydraulicStructure(SQLSubModel):
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, name, enabled, input_section, output_section, " +
"input_reach, output_reach, scenario) " +
"(pamhyr_id, name, enabled, input_section, " +
"input_reach, scenario) " +
"SELECT pamhyr_id, name, enabled, " +
"input_section, output_section, " +
"input_reach, output_reach, scenario " +
"input_section, input_reach, scenario " +
f"FROM {table}"
)
@@ -145,44 +156,38 @@ class HydraulicStructure(SQLSubModel):
@classmethod
def _db_update_to_0_2_0_set_reach_pid(cls, execute, table, reachs):
els = execute(
f"SELECT pamhyr_id, input_reach, output_reach FROM {table}"
f"SELECT pamhyr_id, input_reach FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
in_reach_id = next(it)
out_reach_id = next(it)
if in_reach_id == -1:
return
if out_reach_id == -1:
out_reach_id = in_reach_id
execute(
f"UPDATE {table} " +
f"SET input_reach = {reachs[in_reach_id]}, " +
f"output_reach = {reachs[out_reach_id]} " +
f"SET input_reach = {reachs[in_reach_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_update_to_0_1_1(cls, execute, data,
origin_version="0.1.0"):
for v in ["input", "output"]:
execute(
"ALTER TABLE hydraulic_structures " +
f"ADD COLUMN {v}_section INTEGER"
)
execute(
"ALTER TABLE hydraulic_structures " +
"ADD COLUMN input_section INTEGER"
)
cls._db_update_to_0_1_1_assoc_section_from_rk(
execute, "hydraulic_structures",
reach_column=f"{v}_reach",
rk_column=f"{v}_rk",
section_column=f"{v}_section",
origin_version=origin_version
)
cls._db_update_to_0_1_1_assoc_section_from_rk(
execute, "hydraulic_structures",
reach_column="input_reach",
rk_column="input_rk",
section_column="input_section",
origin_version=origin_version
)
@classmethod
def _db_load(cls, execute, data=None):
@@ -196,8 +201,7 @@ class HydraulicStructure(SQLSubModel):
table = execute(
"SELECT pamhyr_id, deleted, name, enabled, " +
"input_section, output_section, " +
"input_reach, output_reach, scenario " +
"input_section, input_reach, scenario " +
"FROM hydraulic_structures " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
@@ -227,12 +231,7 @@ class HydraulicStructure(SQLSubModel):
input_section_id = (
-1 if input_section_id is None else input_section_id
)
output_section_id = next(it)
output_section_id = (
-1 if output_section_id is None else output_section_id
)
input_reach_id = next(it)
output_reach_id = next(it)
owner_scenario = next(it)
hs = cls(
@@ -243,27 +242,19 @@ class HydraulicStructure(SQLSubModel):
hs.set_as_deleted()
hs.enabled = enabled
hs.input_reach, hs.output_reach = reduce(
lambda acc, n: (
n if n.pamhyr_id == input_reach_id else acc[0],
n if n.pamhyr_id == output_reach_id else acc[1]
),
hs.input_reach = reduce(
lambda acc, n: n if n.pamhyr_id == input_reach_id else acc,
data["edges"],
[None, None]
None
)
sections = []
if hs.input_reach is not None:
sections += hs.input_reach.reach.profiles
if hs.output_reach is not None:
sections += hs.output_reach.reach.profiles
hs.input_section, hs.output_section = reduce(
lambda acc, s: (
s if s.pamhyr_id == input_section_id else acc[0],
s if s.pamhyr_id == output_section_id else acc[1]
),
hs.input_section = reduce(
lambda acc, s: s if s.pamhyr_id == input_section_id else acc,
sections,
[None, None]
None
)
loaded.add(hs_id)
@@ -287,24 +278,15 @@ class HydraulicStructure(SQLSubModel):
if self._input_reach is not None:
input_reach_id = self._input_reach.pamhyr_id
output_reach_id = -1
if self._output_reach is not None:
output_reach_id = self._output_reach.pamhyr_id
input_section = 'NULL'
if self.input_section is not None:
input_section = self.input_section.pamhyr_id
output_section = 'NULL'
if self.output_section is not None:
output_section = self.output_section.pamhyr_id
execute(
"INSERT INTO " +
"hydraulic_structures(" +
" pamhyr_id, deleted, name, enabled, " +
" input_section, output_section, " +
" input_reach, output_reach, " +
" input_section, input_reach, " +
" scenario" +
") " +
"VALUES (" +
@@ -312,8 +294,7 @@ class HydraulicStructure(SQLSubModel):
f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"{self._db_format(self.enabled)}, " +
f"{input_section}, {output_section}, " +
f"{input_reach_id}, {output_reach_id}, " +
f"{input_section}, {input_reach_id}, " +
f"{self._status.scenario_id}"
")"
)
@@ -371,13 +352,6 @@ class HydraulicStructure(SQLSubModel):
return self._input_section
@property
def output_rk(self):
if self._output_section is None:
return None
return self._output_section
@property
def input_section(self):
return self._input_section
@@ -387,15 +361,6 @@ class HydraulicStructure(SQLSubModel):
self._input_section = input_section
self.modified()
@property
def output_section(self):
return self._output_section
@output_section.setter
def output_section(self, output_section):
self._output_section = output_section
self.modified()
@property
def enabled(self):
return self._enabled
@@ -414,19 +379,19 @@ class HydraulicStructure(SQLSubModel):
self._input_reach = input_reach
self.modified()
@property
def output_reach(self):
return self._output_reach
@output_reach.setter
def output_reach(self, output_reach):
self._output_reach = output_reach
self.modified()
@property
def basic_structures(self):
return self.lst.copy()
def cloned_for(self, input_reach, input_section):
new = HydraulicStructure(name=self._name, status=self._status)
new._enabled = self._enabled
new._input_reach = input_reach
new._input_section = input_section
new._data = [structure.cloned() for structure in self.lst]
new.modified()
return new
def basic_structure(self, index: int):
if len(self._data) == 0:
return None
@@ -61,6 +61,64 @@ class HydraulicStructureList(PamhyrModelList):
self.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
sections1 = dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
sections2 = dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
def split_endpoint(endpoint_reach, section):
if endpoint_reach is not reach:
return endpoint_reach, section
if section not in profiles:
return endpoint_reach, section
index = profiles.index(section)
if index < split_index:
return reach1, sections1[section]
return reach2, sections2[section]
structures = [
structure
for structure in self.lst
if structure.input_reach is reach
]
for structure in structures:
input_reach, input_section = split_endpoint(
structure.input_reach,
structure.input_section
)
self._lst.append(structure.cloned_for(
input_reach,
input_section
))
if len(structures) != 0:
self.modified()
@property
def enabled_reaches_list(self):
return [
structure
for structure in self.lst
if (structure.input_reach is None or
(not structure.input_reach.is_deleted() and
structure.input_reach.is_enable()))
]
def get_for_enabled_reaches(self, index):
structures = self.enabled_reaches_list
if not 0 <= index < len(structures):
return None
return structures[index]
def __copy__(self):
new = HydraulicStructureList()
@@ -19,6 +19,7 @@
import logging
from copy import copy, deepcopy
from math import isclose
from tools import trace, timer
from functools import reduce
from numpy import interp
@@ -256,6 +257,17 @@ class Data(SQLSubModel):
)
return new
def cloned_for(self, reach, section):
return Data(
name=self._name,
comment=self._comment,
section=section,
discharge=self._discharge,
height=self._height,
reach=reach,
status=self._status,
)
@property
def name(self):
return self._name
@@ -428,15 +440,28 @@ class InitialConditions(SQLSubModel):
def new_from_data(self, rk, discharge, elevation):
n = Data(reach=self._reach, status=self._status)
# Values read from Mage INI files are strings, whereas profile RK
# values are stored as floats. Normalize the imported value before
# looking up its associated profile.
rk = float(rk)
section = reduce(
lambda acc, s: (
s if s.rk == rk else acc
# Mage INI files store RK values with two decimal places.
# Allow for the rounding introduced by that format.
s if isclose(s.rk, rk, rel_tol=0.0, abs_tol=0.0051)
else acc
),
self._reach.reach.profiles,
None
)
if section is None:
raise ValueError(
f"No profile with RK {rk} exists in reach "
f"{self._reach.name}"
)
n['section'] = section
n['discharge'] = discharge
n['elevation'] = elevation
@@ -505,6 +530,20 @@ class InitialConditions(SQLSubModel):
for data in self._data:
data.set_as_deleted()
def _splited_for(self, reach, sections):
new = InitialConditions(reach=reach, status=self._status)
new._data = [
data.cloned_for(reach, sections[data["section"]])
for data in self.data
if data["section"] in sections
]
if len(new._data) != 0:
new.modified()
return new
def generate_growing_constant_depth(self, height: float,
compute_discharge: bool):
profiles = self._reach.reach.profiles.copy()
@@ -88,3 +88,28 @@ class InitialConditionsDict(PamhyrModelDict):
new = InitialConditions(reach=reach, status=self._status)
self.set(reach, new)
return new
def split_reach(self, reach, profile, reach1, reach2):
if reach not in self._dict:
return
profiles = reach.reach.profiles
split_index = profiles.index(profile)
sections1 = dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
sections2 = dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
self.set(
reach1,
self._dict[reach]._splited_for(reach1, sections1)
)
self.set(
reach2,
self._dict[reach]._splited_for(reach2, sections2)
)
@@ -359,3 +359,60 @@ class InitialConditionsAdisTS(SQLSubModel):
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
lower_reach_rk = min(reach_rks)
upper_reach_rk = max(reach_rks)
parts = [
(reach1.id, lower_reach_rk, split_rk),
(reach2.id, split_rk, upper_reach_rk),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
def get_specs_for_enabled_reaches(self, reaches):
enabled_reaches = {
reach.id
for reach in reaches
if not reach.is_deleted() and reach.is_enable()
}
return [
specification
for specification in self._data
if (not specification.is_deleted() and
(specification.reach in (None, -1) or
specification.reach in enabled_reaches))
]
@@ -62,6 +62,12 @@ class InitialConditionsAdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for initial_condition in self.lst:
initial_condition.split_reach(
reach, profile, reach1, reach2
)
@property
def Initial_Conditions_List(self):
return self.lst
@@ -31,10 +31,12 @@ class ICAdisTSSpec(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=None):
super(ICAdisTSSpec, self).__init__()
self._status = status
status=None, owner_scenario=-1):
super(ICAdisTSSpec, self).__init__(
id=id,
status=status,
owner_scenario=owner_scenario
)
self._name_section = name
self._reach = None
@@ -190,7 +192,7 @@ class ICAdisTSSpec(SQLSubModel):
new_spec.rate = rate
new_spec.enabled = enabled
# loaded.add(pid)
loaded.add(id)
new.append(new_spec)
data["scenario"] = scenario.parent
@@ -234,7 +236,8 @@ class ICAdisTSSpec(SQLSubModel):
@name.setter
def name(self, name):
self._name_section = name
self._status.modified()
self.modified()
self.modified()
@property
def reach(self):
@@ -243,7 +246,8 @@ class ICAdisTSSpec(SQLSubModel):
@reach.setter
def reach(self, reach):
self._reach = reach
self._status.modified()
self.modified()
self.modified()
@property
def start_rk(self):
@@ -252,7 +256,8 @@ class ICAdisTSSpec(SQLSubModel):
@start_rk.setter
def start_rk(self, start_rk):
self._start_rk = start_rk
self._status.modified()
self.modified()
self.modified()
@property
def end_rk(self):
@@ -261,7 +266,8 @@ class ICAdisTSSpec(SQLSubModel):
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self._status.modified()
self.modified()
self.modified()
@property
def concentration(self):
@@ -270,7 +276,8 @@ class ICAdisTSSpec(SQLSubModel):
@concentration.setter
def concentration(self, concentration):
self._concentration = concentration
self._status.modified()
self.modified()
self.modified()
@property
def eg(self):
@@ -279,7 +286,8 @@ class ICAdisTSSpec(SQLSubModel):
@eg.setter
def eg(self, eg):
self._eg = eg
self._status.modified()
self.modified()
self.modified()
@property
def em(self):
@@ -288,7 +296,8 @@ class ICAdisTSSpec(SQLSubModel):
@em.setter
def em(self, em):
self._em = em
self._status.modified()
self.modified()
self.modified()
@property
def ed(self):
@@ -297,7 +306,8 @@ class ICAdisTSSpec(SQLSubModel):
@ed.setter
def ed(self, ed):
self._ed = ed
self._status.modified()
self.modified()
self.modified()
@property
def rate(self):
@@ -306,7 +316,8 @@ class ICAdisTSSpec(SQLSubModel):
@rate.setter
def rate(self, rate):
self._rate = rate
self._status.modified()
self.modified()
self.modified()
@property
def enabled(self):
@@ -315,4 +326,18 @@ class ICAdisTSSpec(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = ICAdisTSSpec(name=self._name_section, status=self._status)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._concentration = self._concentration
new._eg = self._eg
new._em = self._em
new._ed = self._ed
new._rate = self._rate
new._enabled = self._enabled
new.modified()
return new
@@ -0,0 +1,272 @@
# InitialConditionsTemperature.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from functools import reduce
from tools import trace, timer, old_pamhyr_date_to_timestamp
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
from Model.InitialConditionsTemperature.InitialConditionsTemperatureSpec \
import ICTemperatureSpec
logger = logging.getLogger()
class InitialConditionsTemperature(SQLSubModel):
_sub_classes = [
ICTemperatureSpec,
]
def __init__(self, id: int = -1, name: str = "default",
status=None, owner_scenario=-1):
super(InitialConditionsTemperature, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = name
self._temperature = None
self._data = []
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
temperature REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7):
table_name = "initial_conditions_temperature"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, " +
"name, temperature, scenario " +
"FROM initial_conditions_temperature " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
temperature = next(it)
owner_scenario = next(it)
ic = cls(
id=pid,
name=name,
status=status,
owner_scenario=owner_scenario
)
if deleted:
ic.set_as_deleted()
ic.temperature = temperature
data['ic_default_id'] = pid
ic._data = ICTemperatureSpec._db_load(execute, data)
loaded.add(pid)
new.append(ic)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM initial_conditions_temperature " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
temperature = self._temperature if \
self._temperature is not None else 0.0
sql = (
"INSERT INTO " +
"initial_conditions_temperature(" +
"pamhyr_id, deleted, name, temperature, " +
"scenario" +
") " +
"VALUES (" +
f"{self.id}, {self.is_deleted()}, " +
f"'{self._db_format(self._name)}', " +
f"{temperature}, {self._status.scenario_id}" +
")"
)
execute(sql)
data['ic_default_id'] = self.id
execute(
"DELETE FROM initial_conditions_temperature_spec " +
f"WHERE ic_default = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
for ic_spec in self._data:
ic_spec._db_save(execute, data)
return True
def __len__(self):
return len(self.data)
@property
def data(self):
return list(filter(lambda item: not item.is_deleted(), self._data))
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temperature):
self._temperature = temperature
self.modified()
def new(self, index):
n = ICTemperatureSpec(status=self._status)
self._data.insert(index, n)
self.modified()
return n
def delete(self, data):
list(
map(
lambda x: x.set_as_deleted(),
data
)
)
self.modified()
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self._data)
)
)
)
self.modified()
def insert(self, index, data):
if data in self._data:
self.undelete([data])
else:
self._data.insert(index, data)
self.modified()
def undelete(self, lst):
for x in lst:
x.set_as_not_deleted()
self.modified()
def split_reach(self, reach, profile, reach1, reach2):
parts = []
for new_reach in (reach1, reach2):
rks = new_reach.reach.get_rk()
if rks:
parts.append((new_reach.id, min(rks), max(rks)))
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
clones = []
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
clones.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if clones:
self._data.extend(clones)
self.modified()
@@ -0,0 +1,73 @@
# InitialConditionsTemperatureList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.InitialConditionsTemperature.InitialConditionsTemperature \
import InitialConditionsTemperature
class InitialConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
InitialConditionsTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = InitialConditionsTemperature._db_load(
execute, data
)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM initial_conditions_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for ic in self._lst:
ic._db_save(execute, data=data)
return True
def new(self, index):
n = InitialConditionsTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
for initial_condition in self.lst:
initial_condition.split_reach(
reach, profile, reach1, reach2
)
@property
def Initial_Conditions_List(self):
return self.lst
@@ -0,0 +1,215 @@
# InitialConditionsAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class ICTemperatureSpec(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(ICTemperatureSpec, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name_section = name
self._reach = None
self._start_rk = None
self._end_rk = None
self._temperature = None
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_temperature_spec{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ic_default INTEGER NOT NULL,
name TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
temperature REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(ic_default)
REFERENCES initial_conditions_temperature(pamhyr_id),
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0":
if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7):
table_name = "initial_conditions_temperature_spec"
if not cls.is_table_exists(execute, table_name):
cls._db_create(execute)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
reachs = data["edges"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, name, reach, start_rk, end_rk, " +
"temperature, scenario " +
"FROM initial_conditions_temperature_spec " +
f"WHERE ic_default = {data['ic_default_id']} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
if table is not None:
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
reach = next(it)
start_rk = next(it)
end_rk = next(it)
temperature = next(it)
owner_scenario = next(it)
new_spec = cls(
id=id, name=name,
status=status,
owner_scenario=owner_scenario
)
if deleted:
new_spec.set_as_deleted()
new_spec.reach = reach
new_spec.start_rk = start_rk
new_spec.end_rk = end_rk
new_spec.temperature = temperature
loaded.add(id)
new.append(new_spec)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ic_default = data['ic_default_id']
sql = (
"INSERT INTO " +
"initial_conditions_temperature_spec(pamhyr_id, deleted, " +
"ic_default, name, reach, " +
"start_rk, end_rk, temperature, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{ic_default}, " +
f"'{self._db_format(self._name_section)}', " +
f"{self._reach}, " +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._temperature}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
@property
def name(self):
return self._name_section
@name.setter
def name(self, name):
self._name_section = name
self.modified()
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach):
self._reach = reach
self.modified()
@property
def start_rk(self):
return self._start_rk
@start_rk.setter
def start_rk(self, start_rk):
self._start_rk = start_rk
self.modified()
@property
def end_rk(self):
return self._end_rk
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self.modified()
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temperature):
self._temperature = temperature
self._status.modified()
def cloned_for(self, reach, start_rk, end_rk):
new = ICTemperatureSpec(
name=self._name_section,
status=self._status
)
new._reach = reach
new._start_rk = start_rk
new._end_rk = end_rk
new._temperature = self._temperature
new.modified()
return new
@@ -196,6 +196,13 @@ class Data(SQLSubModel):
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class LateralContribution(SQLSubModel):
_sub_classes = [Data]
@@ -217,6 +224,9 @@ class LateralContribution(SQLSubModel):
self._header = []
self._types = [float, float]
self._d50 = 0.002
self._sigma = 1.0
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
@@ -229,6 +239,8 @@ class LateralContribution(SQLSubModel):
reach INTEGER,
begin_section INTEGER,
end_section INTEGER,
d50 REAL DEFAULT 0.002,
sigma REAL DEFAULT 1,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
@@ -279,8 +291,32 @@ class LateralContribution(SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and (int(minor) < 2 or
(minor == "2" and int(release) < 11)):
cls._db_update_to_0_2_11(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_11(cls, execute):
# The table may already have been recreated with these columns
# by _db_update_to_0_2_0
columns = [
row[0] for row in execute(
"SELECT name FROM pragma_table_info('lateral_contribution')"
)
]
if "d50" not in columns:
execute(
"ALTER TABLE lateral_contribution " +
"ADD COLUMN d50 REAL DEFAULT 0.002"
)
if "sigma" not in columns:
execute(
"ALTER TABLE lateral_contribution " +
"ADD COLUMN sigma REAL DEFAULT 1"
)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "lateral_contribution"
@@ -332,7 +368,7 @@ class LateralContribution(SQLSubModel):
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"reach, begin_section, end_section, scenario " +
"reach, begin_section, end_section, d50, sigma, scenario " +
"FROM lateral_contribution " +
f"WHERE tab = '{tab}'" +
f"AND scenario = {scenario.id} " +
@@ -349,6 +385,8 @@ class LateralContribution(SQLSubModel):
reach = next(it)
b_section = next(it)
e_section = next(it)
d50 = next(it)
sigma = next(it)
owner_scenario = next(it)
ctor = cls._get_ctor_from_type(t)
@@ -360,6 +398,10 @@ class LateralContribution(SQLSubModel):
lc.reach = None
lc._begin_section = None
lc._end_section = None
if d50 is not None:
lc._d50 = float(d50)
if sigma is not None:
lc._sigma = float(sigma)
if row[3] != -1:
lc.reach = next(
@@ -419,12 +461,13 @@ class LateralContribution(SQLSubModel):
"INSERT INTO " +
"lateral_contribution(" +
"pamhyr_id, deleted, name, type, tab, " +
"reach, begin_section, end_section, scenario) " +
"reach, begin_section, end_section, d50, sigma, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', '{tab}', {reach}, " +
f"{begin_section}, {end_section}, " +
f"{self._d50}, {self._sigma}, " +
f"{self._status.scenario_id}" +
")"
)
@@ -511,6 +554,35 @@ class LateralContribution(SQLSubModel):
def has_reach(self):
return self._reach is not None
def cloned_for(self, reach, begin_section, end_section):
new = type(self)(name=self._name, status=self._status)
new._reach = reach
new._begin_section = begin_section
new._end_section = end_section
new._d50 = self._d50
new._sigma = self._sigma
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def d50(self):
return self._d50
@d50.setter
def d50(self, value):
self._d50 = float(value)
self.modified()
@property
def sigma(self):
return self._sigma
@sigma.setter
def sigma(self, value):
self._sigma = float(value)
self.modified()
@property
def begin_rk(self):
if self._begin_section is None:
@@ -71,6 +71,89 @@ class LateralContributionList(PamhyrModelListWithTab):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
parts = [
(
reach1,
0,
split_index,
dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
),
(
reach2,
split_index,
len(profiles) - 1,
dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
),
]
for tab in self._tabs:
contributions = [
contribution
for contribution in self.get_tab(tab)
if contribution.reach is reach
]
for contribution in contributions:
if (contribution.begin_section not in profiles or
contribution.end_section not in profiles):
continue
begin = profiles.index(contribution.begin_section)
end = profiles.index(contribution.end_section)
lower, upper = sorted((begin, end))
reverse = begin > end
clones = []
for new_reach, part_lower, part_upper, sections in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
begin_index = (
clipped_upper if reverse else clipped_lower
)
end_index = (
clipped_lower if reverse else clipped_upper
)
clones.append(contribution.cloned_for(
new_reach,
sections[profiles[begin_index]],
sections[profiles[end_index]]
))
if len(clones) == 0:
continue
self._tabs[tab].extend(clones)
self._status.modified()
def get_tab_for_enabled_reaches(self, tab):
return [
contribution
for contribution in self.get_tab(tab)
if (contribution.reach is None or
(not contribution.reach.is_deleted() and
contribution.reach.is_enable()))
]
def get_for_enabled_reaches(self, tab, index):
contributions = self.get_tab_for_enabled_reaches(tab)
if not 0 <= index < len(contributions):
return None
return contributions[index]
def __copy__(self):
new = LateralContributionList()
@@ -192,6 +192,13 @@ class Data(SQLSubModel):
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class LateralContributionAdisTS(SQLSubModel):
_sub_classes = [Data]
@@ -477,6 +484,18 @@ class LateralContributionAdisTS(SQLSubModel):
self._end_rk = end_rk
self.modified()
def cloned_for(self, reach, begin_rk, end_rk):
new = LateralContributionAdisTS(
pollutant=self._pollutant,
status=self._status
)
new._reach = reach
new._begin_rk = begin_rk
new._end_rk = end_rk
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def _default_0(self):
return self._types[0](0)
@@ -68,6 +68,46 @@ class LateralContributionsAdisTSList(PamhyrModelList):
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
parts = []
for new_reach in (reach1, reach2):
rks = new_reach.reach.get_rk()
if len(rks) == 0:
continue
parts.append((new_reach.id, min(rks), max(rks)))
contributions = [
contribution
for contribution in self.lst
if contribution.reach == reach.id
]
clones = []
for contribution in contributions:
begin = contribution.begin_rk
end = contribution.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
begin_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
clones.append(contribution.cloned_for(
new_reach, begin_rk, end_rk
))
if len(clones) != 0:
self._lst.extend(clones)
self._status.modified()
@property
def Lat_Cont_List(self):
return self.lst
-208
View File
@@ -1,208 +0,0 @@
# OutputRKAdists.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class OutputRKAdists(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, reach=None,
rk=None, title: str = "", status=None,
owner_scenario=-1):
super(OutputRKAdists, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._reach = reach
self._rk = rk
self._title = str(title)
self._enabled = True
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach_id):
self._reach = reach_id
self._status.modified()
@property
def rk(self):
return self._rk
@rk.setter
def rk(self, profile_id):
self._rk = profile_id
self._status.modified()
@property
def title(self):
return self._title
@title.setter
def title(self, title):
self._title = title
self._status.modified()
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE output_rk_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
reach INTEGER NOT NULL,
rk REAL NOT NULL,
title TEXT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "OutputRKAdists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "OutputRKAdists"
table_new = "output_rk_adists"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table_new}_tmp " +
"(pamhyr_id, reach, rk, title, scenario) " +
"SELECT pamhyr_id, reach, rk, title, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table_new, reachs)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
# reach = data["reach"]
# profile = data["profile"]
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, reach, rk, title, scenario " +
f"FROM output_rk_adists"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
id_reach = next(it)
id_rk = next(it)
title = next(it)
owner_scenario = next(it)
new_output = cls(
id=pid, reach=id_reach,
rk=id_rk, title=title,
status=status,
owner_scenario=owner_scenario,
)
if deleted:
new_output.set_as_deleted()
loaded.add(pid)
new.append(new_output)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM output_rk_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"INSERT INTO " +
"output_rk_adists(pamhyr_id, deleted, " +
"reach, rk, title, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._reach}, {self._rk}, " +
f"'{self._db_format(self._title)}', " +
f"{self._status.scenario_id}" +
")"
)
return True
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
+46 -16
View File
@@ -202,13 +202,8 @@ class TableData(SQLSubModel):
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
create = False
if major == "0" and int(minor) == 2:
if int(release) < 7 and not create:
cls._db_create(execute)
create = True
if not cls.is_table_exists(execute, "results_table_data"):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@@ -277,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(
@@ -346,7 +341,8 @@ 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
@@ -436,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)
@@ -464,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
@@ -476,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}"
@@ -496,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)
@@ -506,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
@@ -532,7 +552,7 @@ class Results(SQLSubModel):
i = 0
for reach in new_results._river.reachs:
nb = len(reach.profiles)
reach.set_global_index(range(i, i + nb + 1))
reach.set_global_index(range(i, i + nb))
i += nb
if "Z" in table_data:
@@ -598,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
+31 -14
View File
@@ -104,7 +104,10 @@ class Profile(SQLSubModel):
return any(map(lambda ts: "sl" in self._data[ts], self._data))
def has_bedload(self):
return any(map(lambda ts: "sl" in self._data[ts], self._data))
# Sediment layers (Mage) or bed level of the section (RubarBE)
return any(map(lambda ts: ("sl" in self._data[ts] or
"zfd" in self._data[ts]),
self._data))
@classmethod
def _db_create(cls, execute, ext=""):
@@ -154,6 +157,7 @@ class Profile(SQLSubModel):
"len_data, data, scenario " +
"FROM results_data " +
f"WHERE scenario = {scenario.id} " +
f"AND result = {data['result_pid']} " +
f"AND reach = {reach.pamhyr_id} " +
f"AND section = {profile.pamhyr_id}"
)
@@ -276,7 +280,7 @@ class Profile(SQLSubModel):
class Reach(SQLSubModel):
_sub_classes = [Profile]
def __init__(self, reach, study, parent, with_init=True):
def __init__(self, reach, study, parent, with_init=True, profiles=None):
super(Reach, self).__init__(
id=-1, status=study.status,
owner_scenario=study.status.scenario.id
@@ -287,10 +291,12 @@ class Reach(SQLSubModel):
self._reach = reach # Source reach in the study
self._profiles = []
if with_init:
if profiles is None:
profiles = reach.profiles
self._profiles = list(
map(
lambda p: Profile(p, self._study, self._parent),
reach.profiles
profiles
)
)
@@ -379,7 +385,14 @@ class Reach(SQLSubModel):
for i, profile in enumerate(reach.profiles):
data["profile"] = profile
new_reach._profiles += [Profile._db_load(execute, data)]
result_profile = Profile._db_load(execute, data)
if len(result_profile) > 0:
new_reach._profiles.append(result_profile)
new_reach._profile_mask = [
profile.name[0:8] != 'interpol'
for profile in new_reach._profiles
]
return new_reach
@@ -393,7 +406,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
@@ -404,8 +417,9 @@ class River(SQLSubModel):
# Dict with timestamps as key
self._reachs = []
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
if init:
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
def __len__(self):
return len(self._reachs)
@@ -420,10 +434,13 @@ class River(SQLSubModel):
def has_reach(self, id):
return 0 <= id < len(self._reachs)
def add(self, reach_id):
def add(self, reach_id, profiles=None):
reachs = self._study.river.enable_edges()
new = Reach(reachs[reach_id].reach, self._study, self._parent)
new = Reach(
reachs[reach_id].reach, self._study, self._parent,
profiles=profiles
)
self._reachs.append(new)
return new
@@ -442,7 +459,7 @@ class River(SQLSubModel):
reach.update_water_limits(
timestamp,
data[ind[0]:ind[1]]
data[ind[0]:ind[1] + 1]
)
@classmethod
@@ -458,13 +475,13 @@ 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
new_river._reachs.append(
Reach._db_load(execute, data)
)
result_reach = Reach._db_load(execute, data)
if len(result_reach) > 0:
new_river._reachs.append(result_reach)
return new_river
+10 -4
View File
@@ -64,13 +64,15 @@ class Profile(object):
class Reach(object):
def __init__(self, reach, study):
def __init__(self, reach, study, profiles=None):
self._study = study
self._reach = reach # Source reach in the study
if profiles is None:
profiles = reach.profiles
self._profiles = list(
map(
lambda p: Profile(p, self._study),
reach.profiles
profiles
)
)
@@ -89,6 +91,10 @@ class Reach(object):
def profiles(self):
return self._profiles.copy()
def get_rk(self):
"""Return the PK values for profiles present in the results."""
return [profile.rk for profile in self._profiles]
def profile(self, id):
return self._profiles[id]
@@ -119,10 +125,10 @@ class River(object):
def has_reach(self, id):
return 0 <= id < len(self._reachs)
def add(self, reach_id):
def add(self, reach_id, profiles=None):
reachs = self._study.river.enable_edges()
new = Reach(reachs[reach_id].reach, self._study)
new = Reach(reachs[reach_id].reach, self._study, profiles=profiles)
self._reachs.append(new)
return new
+130 -13
View File
@@ -49,7 +49,6 @@ from Model.REPLine.REPLineList import REPLineList
from Solver.Solvers import solver_type_list
from Model.OutputRKAdists.OutputRKListAdists import OutputRKAdistsList
from Model.Pollutants.PollutantsList import PollutantsList
from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList \
import InitialConditionsAdisTSList
@@ -59,6 +58,16 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
import LateralContributionsAdisTSList
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
import InitialConditionsTemperatureList
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
from Model.WeatherParameters.WeatherParametersList \
import WeatherParametersList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results
@@ -469,13 +478,15 @@ class River(Graph):
HydraulicStructureList,
AddFileList,
REPLineList,
OutputRKAdistsList,
PollutantsList,
InitialConditionsAdisTSList,
BoundaryConditionsAdisTSList,
LateralContributionsAdisTSList,
D90AdisTSList,
DIFAdisTSList,
InitialConditionsTemperatureList,
BoundaryConditionsTemperatureList,
WeatherParametersList,
GeoTIFFList,
EnsembleList,
Results
@@ -504,7 +515,6 @@ class River(Graph):
)
self._additional_files = AddFileList(status=self._status)
self._rep_lines = REPLineList(status=self._status)
self._Output_rk_adists = OutputRKAdistsList(status=self._status)
self._Pollutants = PollutantsList(status=self._status)
self._InitialConditionsAdisTS = InitialConditionsAdisTSList(
status=self._status)
@@ -514,6 +524,13 @@ class River(Graph):
status=self._status)
self._D90AdisTS = D90AdisTSList(status=self._status)
self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._InitialConditionsTemperature = (
InitialConditionsTemperatureList(status=self._status)
)
self._BoundaryConditionsTemperature = (
BoundaryConditionsTemperatureList(status=self._status)
)
self._WeatherParameters = (WeatherParametersList(status=self._status))
self._geotiff = GeoTIFFList(status=self._status)
@@ -615,10 +632,6 @@ class River(Graph):
new._Pollutants = PollutantsList._db_load(execute, data)
new._Output_rk_adists = OutputRKAdistsList._db_load(
execute, data
)
new._InitialConditionsAdisTS = InitialConditionsAdisTSList._db_load(
execute, data)
@@ -632,6 +645,14 @@ class River(Graph):
new._DIFAdisTS = DIFAdisTSList._db_load(execute, data)
new._InitialConditionsTemperature = \
InitialConditionsTemperatureList._db_load(execute, data)
new._BoundaryConditionsTemperature = \
BoundaryConditionsTemperatureList._db_load(execute, data)
new._WeatherParameters = WeatherParametersList._db_load(execute, data)
new._geotiff = GeoTIFFList._db_load(execute, data)
new._ensembles = EnsembleList._db_load(execute, data)
@@ -661,7 +682,6 @@ class River(Graph):
for solver in self._parameters:
objs.append(self._parameters[solver])
objs.append(self._Output_rk_adists)
objs.append(self._Pollutants)
objs.append(self._InitialConditionsAdisTS)
objs.append(self._BoundaryConditionsAdisTS)
@@ -669,6 +689,10 @@ class River(Graph):
objs.append(self._D90AdisTS)
objs.append(self._DIFAdisTS)
objs.append(self._InitialConditionsTemperature)
objs.append(self._BoundaryConditionsTemperature)
objs.append(self._WeatherParameters)
objs.append(self._geotiff)
objs.append(self._ensembles)
@@ -743,12 +767,14 @@ class River(Graph):
self._hydraulic_structures,
self._additional_files,
self._rep_lines,
self._Output_rk_adists,
self._Pollutants,
self._InitialConditionsAdisTS,
self._BoundaryConditionsAdisTS,
self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS,
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
self._WeatherParameters,
self._geotiff, self._ensembles,
]
@@ -854,10 +880,6 @@ Last export at: @date."""
def parameters(self):
return self._parameters
@property
def Output_rk_adists(self):
return self._Output_rk_adists
@property
def Pollutants(self):
return self._Pollutants
@@ -882,6 +904,67 @@ Last export at: @date."""
def dif_adists(self):
return self._DIFAdisTS
@property
def ic_temperature(self):
return self._InitialConditionsTemperature
@property
def boundary_conditions_temperature(self):
return self._BoundaryConditionsTemperature
@property
def weather_parameters(self):
return self._WeatherParameters
def profile_dependencies(self, profile):
"""Return model data which directly reference *profile*.
PK-based intervals are deliberately not included: only explicit
references to the profile object prevent it from being disabled.
"""
dependencies = []
def add(kind, item, association):
dependencies.append({
"kind": kind,
"item": item,
"association": association,
})
for structure in self._hydraulic_structures.lst:
if structure.input_section is profile:
add("hydraulic_structure", structure, "")
for tab in self._lateral_contribution._tabs_list:
for contribution in self._lateral_contribution.get_tab(tab):
if contribution.begin_section is profile:
add("lateral_contribution", contribution, "begin_section")
if contribution.end_section is profile:
add("lateral_contribution", contribution, "end_section")
for parameter in self._WeatherParameters.lst:
if parameter.begin_section is profile:
add("weather_parameter", parameter, "begin_section")
if parameter.end_section is profile:
add("weather_parameter", parameter, "end_section")
reach_id = profile.reach.pamhyr_id
for initial_condition in self._InitialConditionsTemperature.lst:
for specification in initial_condition.data:
same_reach = specification.reach == reach_id
same_rk = (
specification.start_rk == profile.rk or
specification.end_rk == profile.rk
)
if same_reach and same_rk:
add(
"initial_condition_temperature",
specification,
"cross_section_pk",
)
return dependencies
def get_params(self, solver):
if solver in self._parameters:
return self._parameters[solver]
@@ -938,6 +1021,12 @@ Last export at: @date."""
self._results[solv_type] = results
def _split_reach(self, reach, profile):
profiles = reach.reach.profiles
if not profiles or profile not in profiles[1:-1]:
raise ValueError(
"A reach cannot be split at its first or last profile"
)
node1 = reach.node1
node2 = reach.node2
@@ -954,4 +1043,32 @@ Last export at: @date."""
self._add_edge(r1)
self._add_edge(r2)
self._initial_conditions.split_reach(
reach, profile, r1, r2
)
self._lateral_contribution.split_reach(
reach, profile, r1, r2
)
self._hydraulic_structures.split_reach(
reach, profile, r1, r2
)
self._InitialConditionsAdisTS.split_reach(
reach, profile, r1, r2
)
self._LateralContributionsAdisTS.split_reach(
reach, profile, r1, r2
)
self._InitialConditionsTemperature.split_reach(
reach, profile, r1, r2
)
self._WeatherParameters.split_reach(
reach, profile, r1, r2
)
self._D90AdisTS.split_reach(
reach, profile, r1, r2
)
self._DIFAdisTS.split_reach(
reach, profile, r1, r2
)
return r1, r2
+10 -8
View File
@@ -32,7 +32,6 @@ class Scenario(SQLSubModel):
tables_with_deleted_column = [
# Adists
"output_rk_adists",
"boundary_condition_adists", "boundary_condition_data_adists",
"lateral_contribution_adists", "lateral_contribution_data_adists",
"initial_conditions_adists", "initial_conditions_adists_spec",
@@ -53,6 +52,12 @@ class Scenario(SQLSubModel):
"geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node",
"geotiff", "reservoir", "reservoir_data",
# Temperature
"boundary_condition_temperature",
"boundary_condition_data_temperature",
"initial_conditions_temperature",
"initial_conditions_temperature_spec",
"weather_parameters", "weather_parameters_data",
"ensemble",
]
@@ -363,11 +368,8 @@ class Scenario(SQLSubModel):
return None
def workdir(self):
srep = ""
if self.id != 0:
srep = os.path.join(
self.parent.workdir(),
"scenario_" + str(self.id)
)
current = "scenario_" + str(self.id)
if self.parent is None or self.parent.id == 0:
return current
return srep
return os.path.join(self.parent.workdir(), current)
@@ -19,6 +19,7 @@
from copy import copy
from tools import trace, timer
from Solver.Solvers import solver_type_list
from Solver.RubarBE import Rubar3
from Model.Scenario import Scenario
from Model.Tools.PamhyrDB import SQLSubModel
@@ -216,6 +217,9 @@ class Parameter(SQLSubModel):
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ind = data["ind"]
solver = data["solver"]
@@ -293,14 +297,20 @@ class SolverParametersList(PamhyrModelList):
scenario = data["scenario"]
loaded = data['loaded_pid']
solvers = execute("SELECT DISTINCT solver FROM solver_parameter")
solvers = [
row[0] for row in
execute("SELECT DISTINCT solver FROM solver_parameter")
]
new = {}
if scenario is None:
return new
for solver in solvers:
solver = solver[0]
if solver == "rubar3" and "rubarbe" not in solvers:
new["rubarbe"] = cls._db_load_rubar3(execute, data)
continue
if solver not in solver_type_list:
continue
@@ -312,10 +322,44 @@ class SolverParametersList(PamhyrModelList):
n._lst = list(map(lambda t: t[1], sorted(lst)))
# Parameters added to the solver after the study was saved
names = {p.name for p in n._lst}
n._lst += [
Parameter(name=name, value=value, status=status)
for name, value in st.default_parameters()
if name not in names
]
new[solver] = n
return new
@classmethod
def _db_load_rubar3(cls, execute, data):
"""Rubar3 is no longer offered: RubarBE, its generic version,
takes the parameters of a Rubar3 study (same rubarbe_* keys,
RubarBE reads them the same way). Missing parameters take the
Rubar3 defaults (no sediment transport), so that the
computation is unchanged. New parameters (new ids): saved as
RubarBE ones with the study, the Rubar3 rows are then ignored"""
status = data["status"]
n = cls(solver_type=solver_type_list["rubarbe"], status=status)
data['solver'] = "rubar3"
lst = Parameter._db_load(execute, data)
n._lst = [
Parameter(name=p.name, value=p.value, status=status)
for _, p in sorted(lst, key=lambda t: t[0])
]
names = {p.name for p in n._lst}
n._lst += [
Parameter(name=name, value=value, status=status)
for name, value in Rubar3.default_parameters()
if name not in names
]
return n
def _db_save(self, execute, data=None):
t = self._solver._type
execute(
+4 -1
View File
@@ -60,9 +60,12 @@ class StricklersList(PamhyrModelList):
return self.lst
def new(self, index):
owner_scenario = (
self._status.scenario_id if self._status is not None else -1
)
s = Stricklers(
status=self._status,
owner_scenario=self._status.scenario_id
owner_scenario=owner_scenario
)
self.insert(index, s)
self.modified()
+55 -19
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,8 +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.FunctionList import FunctionList
from Model.Ensembles.DistributionList import DistributionList
from Model.HydraulicStructures.HydraulicStructures import (
HydraulicStructure
)
@@ -47,11 +48,11 @@ logger = logging.getLogger()
class Study(SQLModel):
_version = "0.2.8"
_version = "0.2.11"
_sub_classes = [
Scenario,
FunctionList,
DistributionList,
River,
]
@@ -74,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:
@@ -87,7 +89,7 @@ class Study(SQLModel):
self.scenarios[0] = s0
self.status.scenario = s0
self._ens_functions = FunctionList(status=self.status)
self._ens_distributions = DistributionList(status=self.status)
self._river = River(status=self.status)
else:
@@ -203,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
@@ -222,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
@@ -295,7 +306,14 @@ class Study(SQLModel):
if major == "0" and int(minor) < 2:
self._add_into_info_if_not_exists('current_scenario', '0')
if major == "0" and int(minor) < 2:
needs_foreign_keys_disabled = (
major == "0" and (
int(minor) < 2 or
(minor == "2" and int(release) < 10)
)
)
if needs_foreign_keys_disabled:
# Need to temporary disable the sqlite foreign keys
# checking to update db dans change the table id fk to
# table pamhyr_id fk
@@ -305,7 +323,7 @@ class Study(SQLModel):
ok = self._update_submodel(version[0], data={})
if major == "0" and int(minor) < 2:
if needs_foreign_keys_disabled:
# Reactivate foreign keys checking
self.execute(
"PRAGMA foreign_keys = ON;"
@@ -399,12 +417,12 @@ class Study(SQLModel):
data["scenario"] = scenario
data["loaded_pid"] = set()
# Get ensemble function
new._ens_functions = FunctionList._db_load(
# Get ensemble distribution
new._ens_distributions = DistributionList._db_load(
sql_exec, data=data
)
data["ens_functions"] = new._ens_functions
data["ens_distributions"] = new._ens_distributions
# Load river data
new._river = River._db_load(
@@ -466,9 +484,20 @@ 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_functions,
self.scenarios, self._ens_distributions,
self._river._ensembles,
]
for solv in self._river._results:
@@ -484,7 +513,7 @@ class Study(SQLModel):
self._save_submodel(
[
self.scenarios, self._ens_functions,
self.scenarios, self._ens_distributions,
self._river
],
data=progress
@@ -605,7 +634,7 @@ class Study(SQLModel):
"status": self.status,
"loaded_pid": set(),
"scenario": scenario,
"ens_functions": self._ens_functions,
"ens_distributions": self._ens_distributions,
}
self.status.start_loading()
@@ -620,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
@@ -679,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(
@@ -690,20 +723,23 @@ class Study(SQLModel):
self.status.set_as_editable()
return new
def create_ensemble_scenario(self, switch=True):
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)"
new.set_pos(self.status.scenario.x + 100,
self.status.scenario.y)
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_editable()
self.status.set_as_read_only()
return new
@property
+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.
@@ -0,0 +1,775 @@
# WeatherParameters.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class Data(SQLSubModel):
_sub_classes = []
def __init__(self,
data0, data1,
id: int = -1,
types=[float, float],
status=None,
owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._types = types
self._data = [data0, data1]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE weather_parameters_data{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
wp INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(wp) REFERENCES weather_parameters(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute, "air_temperature_data"):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
wp = data["wp"]
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
values = execute(
"SELECT pamhyr_id, deleted, " +
"data0, data1, scenario " +
"FROM weather_parameters_data " +
f"WHERE wp = {wp._pamhyr_id} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
"ORDER BY ind ASC"
)
for v in values:
it = iter(v)
pid = next(it)
deleted = next(it)
data0 = wp._types[0](next(it))
data1 = wp._types[1](next(it))
owner_scenario = next(it)
nd = cls(
data0, data1,
id=pid,
types=wp._types,
status=status,
owner_scenario=owner_scenario
)
if deleted:
nd.set_as_deleted()
loaded.add(pid)
new.append(nd)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
pid = self._pamhyr_id
ind = data["ind"]
data0 = self._db_format(str(self[0]))
data1 = self._db_format(str(self[1]))
wp = data["wp"]
execute(
"INSERT INTO " +
"weather_parameters_data (pamhyr_id, deleted, ind, " +
"data0, data1, wp, scenario) " +
f"VALUES ({pid}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(data0)}', {self._db_format(data1)}, " +
f"{wp._pamhyr_id}, {self._status.scenario_id}" +
")"
)
return True
def __getitem__(self, key):
return self._types[key](self._data[key])
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class WeatherParametersDefault(SQLSubModel):
def __init__(self, type: str, value: float = 0.0,
id: int = -1, status=None, owner_scenario=-1):
super(WeatherParametersDefault, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._type = type
self._value = float(value)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, type, value, scenario " +
"FROM weather_parameters_default " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
for row in table:
it = iter(row)
pid = next(it)
type = next(it)
value = next(it)
owner_scenario = next(it)
wp_default = cls(
type=type,
value=value,
id=pid,
status=status,
owner_scenario=owner_scenario
)
loaded.add(pid)
new.append(wp_default)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"weather_parameters_default(" +
"pamhyr_id, type, value, scenario) " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._type)}', {self._value}, " +
f"{self._status.scenario_id}" +
")"
)
return True
@property
def type(self):
return self._type
@property
def value(self):
return self._value
@value.setter
def value(self, value):
self._value = float(value)
self.modified()
class WeatherParameters(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1,
name: str = "",
status=None, owner_scenario=-1):
super(WeatherParameters, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._type = ""
self._reach = None
self._begin_section = None
self._end_section = None
self._data = []
self._header = []
self._types = [float, float]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE weather_parameters{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type TEXT NOT NULL,
reach INTEGER,
begin_section INTEGER,
end_section INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(begin_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
FOREIGN KEY(end_section)
REFERENCES geometry_profileXYZ(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
execute(f"""
CREATE TABLE weather_parameters_default{ext}(
{cls.create_db_add_pamhyr_id()},
type TEXT NOT NULL,
value FLOAT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
primary KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _get_ctor_from_type(cls, t):
from Model.WeatherParameters.WeatherParametersTypes import (
NotDefined, WeatherParam, AirTemperature, SpecificHumidity,
GlobalRadiation, ReferenceWindSpeed, GroundwaterTemperature,
GroundwaterFlowRate, Albedo, ShadingCoefficient, CloudCoverFraction
)
res = NotDefined
if t == "WP":
res = WeatherParam
elif t == "AT":
res = AirTemperature
elif t == "SH":
res = SpecificHumidity
elif t == "GR":
res = GlobalRadiation
elif t == "RWS":
res = ReferenceWindSpeed
elif t == "GT":
res = GroundwaterTemperature
elif t == "GFR":
res = GroundwaterFlowRate
elif t == "ALB":
res = Albedo
elif t == "SC":
res = ShadingCoefficient
elif t == "CCF":
res = CloudCoverFraction
else:
res = NotDefined
return res
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 10)):
if not cls.is_table_exists(execute, "air_temperature"):
cls._db_create(execute)
created = True
if not cls.is_table_exists(execute, "weather_parameters_default"):
execute(f"""
CREATE TABLE weather_parameters_default(
{cls.create_db_add_pamhyr_id()},
type TEXT NOT NULL,
value FLOAT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
primary KEY(pamhyr_id, scenario)
)
""")
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
edges = data["edges"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"reach, begin_section, end_section, scenario " +
"FROM weather_parameters " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
for row in table:
it = iter(row)
pid = next(it)
deleted = next(it)
name = next(it)
t = next(it)
reach = next(it)
b_section = next(it)
e_section = next(it)
owner_scenario = next(it)
ctor = cls._get_ctor_from_type(t)
wp = ctor(
id=pid, name=name,
status=status,
owner_scenario=owner_scenario
)
wp.reach = None
wp._begin_section = None
wp._end_section = None
if reach not in (-1, None):
wp.reach = next(
(edge for edge in edges if edge.id == reach), None
)
profiles = (
wp.reach.reach.profiles
if wp.reach is not None else []
)
wp._begin_section = next(
(profile for profile in profiles
if profile.id == b_section), None
)
wp._end_section = next(
(profile for profile in profiles
if profile.id == e_section), None
)
data["wp"] = wp
wp._data = Data._db_load(execute, data=data)
loaded.add(pid)
new.append(wp)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
data["wp"] = self
execute(
"DELETE FROM weather_parameters_data " +
f"WHERE wp = {self._pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
reach = None
begin_section = None
end_section = None
if self._reach is not None:
reach = self._reach._pamhyr_id
begin_section = self._begin_section._pamhyr_id
end_section = self._end_section._pamhyr_id
execute(
"INSERT INTO " +
"weather_parameters(" +
"pamhyr_id, deleted, name, type, " +
"reach, begin_section, end_section, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', {reach}, " +
f"{begin_section}, {end_section}, " +
f"{self._status.scenario_id}" +
")"
)
ind = 0
for d in self._data:
data["ind"] = ind
d._db_save(execute, data)
ind += 1
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for d in self._data:
d._data_traversal(predicate, modifier, data)
def __len__(self):
return len(
list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
)
@classmethod
def compatibility(cls):
return ["liquid", "solid", "suspenssion"]
@classmethod
def time_convert(cls, data):
if type(data) is str:
if data.count("-") == 2:
return date_iso_to_timestamp(data)
if data.count("/") == 2:
return date_dmy_to_timestamp(data)
if data.count(":") == 3:
return old_pamhyr_date_to_timestamp(data)
if data.count(":") == 2:
return old_pamhyr_date_to_timestamp("00:" + data)
if data.count(".") == 1:
return round(float(data))
return int(data)
@property
def name(self):
if self._name == "":
return f"LC #{self.id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def type(self):
return self._type
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach):
self._reach = reach
self._begin_section = None
self._end_section = None
if reach is not None:
profiles = reach.reach.profiles
if profiles:
self._begin_section = profiles[0]
self._end_section = profiles[-1]
self.modified()
def has_reach(self):
return self._reach is not None
@property
def begin_rk(self):
if self._begin_section is None:
return 0
return self._begin_section.rk
@property
def begin_section(self):
return self._begin_section
@begin_section.setter
def begin_section(self, section):
self._begin_section = section
self.modified()
@property
def end_rk(self):
if self._end_section is None:
return 0
return self._end_section.rk
@property
def end_section(self):
return self._end_section
@end_section.setter
def end_section(self, section):
self._end_section = section
self.modified()
def cloned_for(self, reach, begin_section, end_section):
new = type(self)(name=self._name, status=self._status)
new._reach = reach
new._begin_section = begin_section
new._end_section = end_section
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def header(self):
return self._header.copy()
@property
def data(self):
return list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
def get_type_column(self, column):
if 0 <= column < 2:
return self._types[column]
return None
@property
def _default_0(self):
return self._types[0](0)
@property
def _default_1(self):
return self._types[1](0.0)
def is_define(self):
return self._data is not None
def new_from_data(self, header, data):
new_0 = self._default_0
new_1 = self._default_1
def convert(column, value, default):
if value.strip() == "":
return default
return self._types[column](value.replace(",", "."))
if len(header) != 0:
for i in [0, 1]:
for j in range(len(header)):
if self._header[i] == header[j] and j < len(data):
if i == 0:
new_0 = convert(i, data[j], new_0)
else:
new_1 = convert(i, data[j], new_1)
else:
if len(data) > 0:
new_0 = convert(0, data[0], new_0)
if len(data) > 1:
new_1 = convert(1, data[1], new_1)
return Data(new_0, new_1, status=self._status)
def add(self, index: int):
value = Data(self._default_0, self._default_1, status=self._status)
self.insert(index, value)
return value
def insert(self, index: int, value):
visible_data = self.data
if index < len(visible_data):
index = self._data.index(visible_data[index])
else:
index = len(self._data)
self._data.insert(index, value)
self.modified()
def set_data_value(self, value, column, data):
value[column] = self._types[column](data)
self.modified()
def set_deleted(self, values, deleted):
for value in values:
if deleted:
value.set_as_deleted()
else:
value.set_as_not_deleted()
self.modified()
def reorder(self, values):
values = iter(values)
self._data = [
value if value.is_deleted() else next(values)
for value in self._data
]
self.modified()
def delete_i(self, indexes):
self._data = list(
map(
lambda e: e[1],
filter(
lambda e: e[0] not in indexes,
enumerate(self.data)
)
)
)
self.modified()
def set_as_deleted_i(self, indexes):
for i in indexes:
self._data[i].set_as_deleted()
self.modified()
def set_as_not_deleted_i(self, indexes):
for i in indexes:
self._data[i].set_as_not_deleted()
self.modified()
def delete(self, els):
self._data = list(
filter(
lambda e: e not in els,
self.data
)
)
self.modified()
def sort(self, _reverse=False, key=None):
if key is None:
self._data.sort(reverse=_reverse)
else:
self._data.sort(reverse=_reverse, key=key)
self.modified()
def get_i(self, index):
return self.data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index, column, value):
self.set_data_value(self.get_i(index), column, value)
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)
@timer
def convert(self, cls):
new = cls(name=self.name, status=self._status)
new.reach = self.reach
new.begin_section = self.begin_section
new.end_section = self.end_section
for i, _ in enumerate(self.data):
new.add(i)
for i in [0, 1]:
for j in [0, 1]:
if self._header[i] == new.header[j]:
for ind, v in self.data:
try:
new._set_i_c_v(ind, j, v[i])
except Exception as e:
logger.info(e)
self.modified()
return new
def move_up(self, index):
if 0 < index < len(self):
data = self.data
data[index - 1], data[index] = data[index], data[index - 1]
self.reorder(data)
def move_down(self, index):
if 0 <= index < len(self) - 1:
data = self.data
data[index], data[index + 1] = data[index + 1], data[index]
self.reorder(data)
@@ -0,0 +1,234 @@
# WeatherParametersList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Except import NotImplementedMethodeError
from Model.WeatherParameters.WeatherParameters import (
WeatherParameters,
WeatherParametersDefault,
)
from Model.WeatherParameters.WeatherParametersTypes import (
NotDefined, WeatherParam, AirTemperature, SpecificHumidity,
GlobalRadiation, ReferenceWindSpeed, GroundwaterTemperature,
GroundwaterFlowRate, Albedo, ShadingCoefficient, CloudCoverFraction
)
class WeatherParametersList(PamhyrModelList):
_sub_classes = [
WeatherParameters,
]
_default_types = [
"AT", "SH", "GR", "RWS", "GT", "GFR", "ALB", "SC", "CCF"
]
_default_values = {
"AT": 20.0,
"SH": 0.001,
"GR": 10.0,
"RWS": 1.0,
"GT": 0.0,
"GFR": 0.0,
"ALB": 0.06,
"SC": 0.15,
"CCF": 0.5,
}
def __init__(self, status=None):
super(WeatherParametersList, self).__init__(status=status)
self._default = []
self._init_missing_default_values()
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = WeatherParameters._db_load(
execute, data
)
data['loaded_pid'] = set()
new._default = WeatherParametersDefault._db_load(
execute, data
)
new._init_missing_default_values()
new._sort_default_values()
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM weather_parameters " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM weather_parameters_data " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM weather_parameters_default " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for wp in self._lst:
wp._db_save(execute, data=data)
for wp_default in self._default:
wp_default._db_save(execute, data=data)
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for el in self._lst:
el._data_traversal(predicate, modifier, data)
for el in self._default:
el._data_traversal(predicate, modifier, data)
def _init_missing_default_values(self):
existing = [wp_default.type for wp_default in self.default]
for type in self._default_types:
if type not in existing:
self._default.append(
WeatherParametersDefault(
type=type,
value=self._default_values[type],
status=self._status,
)
)
self._sort_default_values()
def _sort_default_values(self):
self._default.sort(
key=lambda wp_default: self._default_types.index(wp_default.type)
if wp_default.type in self._default_types
else len(self._default_types)
)
def new(self, index, type="ND"):
if type == "AT":
n = AirTemperature(status=self._status)
elif type == "SH":
n = SpecificHumidity(status=self._status)
elif type == "GR":
n = GlobalRadiation(status=self._status)
elif type == "RWS":
n = ReferenceWindSpeed(status=self._status)
elif type == "GT":
n = GroundwaterTemperature(status=self._status)
elif type == "GFR":
n = GroundwaterFlowRate(status=self._status)
elif type == "ALB":
n = Albedo(status=self._status)
elif type == "SC":
n = ShadingCoefficient(status=self._status)
elif type == "CCF":
n = CloudCoverFraction(status=self._status)
else:
n = NotDefined(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
def split_reach(self, reach, profile, reach1, reach2):
profiles = reach.reach.profiles
split_index = profiles.index(profile)
parts = [
(
reach1,
0,
split_index,
dict(zip(
profiles[:split_index + 1],
reach1.reach.profiles
))
),
(
reach2,
split_index,
len(profiles) - 1,
dict(zip(
profiles[split_index:],
reach2.reach.profiles
))
),
]
weather_parameters = [
weather_parameter
for weather_parameter in self.lst
if weather_parameter.reach is reach
]
clones = []
for weather_parameter in weather_parameters:
if (weather_parameter.begin_section not in profiles or
weather_parameter.end_section not in profiles):
continue
begin = profiles.index(weather_parameter.begin_section)
end = profiles.index(weather_parameter.end_section)
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper, sections in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
begin_index = clipped_upper if reverse else clipped_lower
end_index = clipped_lower if reverse else clipped_upper
clones.append(weather_parameter.cloned_for(
new_reach,
sections[profiles[begin_index]],
sections[profiles[end_index]]
))
if clones:
self._lst.extend(clones)
self._status.modified()
@property
def Weather_Parameters_List(self):
return self.lst
@property
def default(self):
return self._default.copy()
def default_for_type(self, type):
for wp_default in self.default:
if wp_default.type == type:
return wp_default
return None
@@ -0,0 +1,171 @@
# LateralContributionTypes.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from Model.Except import NotImplementedMethodeError
from Model.WeatherParameters.WeatherParameters import WeatherParameters
class NotDefined(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(NotDefined, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "ND"
self._header = ["x", "y"]
@property
def _default_0(self):
return 0.0
class WeatherParam(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(WeatherParam, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "WP"
self._header = ["time", "value"]
self._types = [WeatherParam.time_convert, float]
@classmethod
def compatibility(cls):
return ["liquid"]
class AirTemperature(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(AirTemperature, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "AT"
self._header = ["time", "temperature"]
self._types = [AirTemperature.time_convert, float]
class SpecificHumidity(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(SpecificHumidity, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "SH"
self._header = ["time", "humidity"]
self._types = [SpecificHumidity.time_convert, float]
class GlobalRadiation(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(GlobalRadiation, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "GR"
self._header = ["time", "radiation"]
self._types = [GlobalRadiation.time_convert, float]
class ReferenceWindSpeed(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(ReferenceWindSpeed, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "RWS"
self._header = ["time", "wind speed"]
self._types = [ReferenceWindSpeed.time_convert, float]
class GroundwaterTemperature(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(GroundwaterTemperature, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "GT"
self._header = ["time", "temperature"]
self._types = [GroundwaterTemperature.time_convert, float]
class GroundwaterFlowRate(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(GroundwaterFlowRate, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "GFR"
self._header = ["time", "discharge"]
self._types = [GroundwaterFlowRate.time_convert, float]
class Albedo(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(Albedo, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "ALB"
self._header = ["time", "albedo"]
self._types = [Albedo.time_convert, float]
class ShadingCoefficient(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(ShadingCoefficient, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "SC"
self._header = ["time", "shading coefficient"]
self._types = [ShadingCoefficient.time_convert, float]
class CloudCoverFraction(WeatherParameters):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(CloudCoverFraction, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "CCF"
self._header = ["time", "cloud cover fraction"]
self._types = [CloudCoverFraction.time_convert, float]
+113
View File
@@ -23,6 +23,8 @@ import tempfile
from Model.Status import StudyStatus
from Model.Study import Study
from Model.River import River
from Model.LateralContribution.LateralContributionTypes import LateralContrib
from Model.HydraulicStructures.Basic.Types import DischargeWeir
class StudyTestCase(unittest.TestCase):
@@ -62,6 +64,31 @@ class StudyTestCase(unittest.TestCase):
study = Study.new("foo", "bar")
self.assertNotEqual(study.river, None)
def test_save_open_study_weather_parameters_default(self):
study = Study.new("foo", "bar")
defaults = study.river.weather_parameters.default
self.assertEqual(len(defaults), 9)
self.assertEqual(
[default.value for default in defaults],
[20.0, 0.001, 10.0, 1.0, 0.0, 0.0, 0.06, 0.15, 0.5]
)
defaults[0].value = 12.5
dir = tempfile.mkdtemp()
f = os.path.join(dir, "foo.pamhyr")
study.filename = f
study.save()
study.close()
study = Study.open(f)
defaults = study.river.weather_parameters.default
self.assertEqual(len(defaults), 9)
self.assertEqual(defaults[0].type, "AT")
self.assertEqual(defaults[0].value, 12.5)
class StudyScenarioTestCase(unittest.TestCase):
def test_create_study(self):
@@ -262,3 +289,89 @@ class RiverTestCase(unittest.TestCase):
edges = river.edges()
self.assertEqual(edges[0], e0)
self.assertEqual(edges[1], e1)
def test_split_reach_splits_initial_conditions(self):
status = StudyStatus()
river = River(status=status)
node1 = river.add_node()
node2 = river.add_node(x=40.0)
reach = river.add_edge(node1, node2)
profiles = []
for index in range(5):
profile = reach.reach.insert(index)
profile.rk = index * 10.0
profile.insert(0)
profiles.append(profile)
initial_conditions = river.initial_conditions.get(reach)
for index, profile in enumerate(profiles):
data = initial_conditions.new_from_data(
profile.rk,
discharge=100.0 + index,
elevation=10.0 + index,
)
initial_conditions.insert(index, data)
reach1, reach2 = river._split_reach(reach, profiles[2])
conditions1 = river.initial_conditions.get(reach1).data
conditions2 = river.initial_conditions.get(reach2).data
self.assertEqual(
[data["discharge"] for data in conditions1],
[100.0, 101.0, 102.0]
)
self.assertEqual(
[data["discharge"] for data in conditions2],
[102.0, 103.0, 104.0]
)
self.assertEqual(
[data["section"] for data in conditions1],
reach1.reach.profiles
)
self.assertEqual(
[data["section"] for data in conditions2],
reach2.reach.profiles
)
self.assertTrue(all(
data._reach is reach1 for data in conditions1
))
self.assertTrue(all(
data._reach is reach2 for data in conditions2
))
def test_split_reach_keeps_partial_initial_conditions_in_scope(self):
status = StudyStatus()
river = River(status=status)
node1 = river.add_node()
node2 = river.add_node(x=40.0)
reach = river.add_edge(node1, node2)
profiles = []
for index in range(5):
profile = reach.reach.insert(index)
profile.rk = index * 10.0
profile.insert(0)
profiles.append(profile)
initial_conditions = river.initial_conditions.get(reach)
for index in (1, 3):
data = initial_conditions.new_from_data(
profiles[index].rk,
discharge=100.0 + index,
elevation=10.0 + index,
)
initial_conditions.insert(len(initial_conditions.data), data)
reach1, reach2 = river._split_reach(reach, profiles[2])
self.assertEqual(
[data["discharge"]
for data in river.initial_conditions.get(reach1).data],
[101.0]
)
self.assertEqual(
[data["discharge"]
for data in river.initial_conditions.get(reach2).data],
[103.0]
)
-2
View File
@@ -55,7 +55,6 @@ class Modules(IterableFlag):
RESERVOIR = auto()
SEDIMENT_LAYER = auto()
ADDITIONAL_FILES = auto()
OUTPUT_RK = auto()
GEOTIFF = auto()
# Results
@@ -81,7 +80,6 @@ class Modules(IterableFlag):
cls.ADDITIONAL_FILES,
cls.RESULTS,
cls.WINDOW_LIST,
cls.OUTPUT_RK,
cls.GEOTIFF
]
+8 -2
View File
@@ -25,6 +25,7 @@ from PyQt5.QtCore import QProcess
from Scripts.AScript import AScript
from Model.Study import Study
from View.RunSolver.Translate import RunSolverTranslate
logger = logging.getLogger()
@@ -75,7 +76,8 @@ class ScriptRun(AScript):
os.path.dirname(study.filename),
"_PAMHYR_",
study.name.replace(" ", "_"),
solver.name.replace(" ", "_")
solver.name.replace(" ", "_"),
study.status.scenario.workdir(),
)
os.makedirs(workdir, exist_ok=True)
logger.info(f"Set working dir to {workdir}")
@@ -88,12 +90,16 @@ class ScriptRun(AScript):
# Export and Run
logger.info(f"~Export~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
solver.export(study, workdir, qlog=self._q)
ok = solver.export(study, workdir, qlog=self._q)
while self._q.qsize() != 0:
s = self._q.get()
logger.info(s)
if not ok:
logger.warning(RunSolverTranslate()["export_failed"])
return
if command == "run":
logger.info(f"~Run~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
solver.run(
+1 -1
View File
@@ -79,7 +79,7 @@ class AbstractSolver(object):
def default_parameters(cls):
lst = [
("all_init_time", "000:00:00:00"),
("all_final_time", "999:99:00:00"),
("all_final_time", "10:00:00:00"),
("all_timestep", "300.0"),
]
+83 -60
View File
@@ -35,10 +35,6 @@ from Solver.CommandLine import CommandLineSolver
from Model.Results.ResultsAdisTS import Results
from Model.Results.River.River import River, Reach, Profile
from Checker.Adists import (
AdistsOutputRKChecker,
)
from itertools import chain
logger = logging.getLogger()
@@ -90,11 +86,7 @@ class AdisTS(CommandLineSolver):
@classmethod
def checkers(cls):
lst = [
AdistsOutputRKChecker(),
]
return lst
return []
##########
# Export #
@@ -117,9 +109,6 @@ class AdisTS(CommandLineSolver):
_nodes_names = {}
_nodes_views = set()
def get_reach_name(self, reach):
return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0")
def get_node_name(self, node):
"""Generate a 3 char name for node
@@ -182,7 +171,7 @@ class AdisTS(CommandLineSolver):
# Write header
edges = study.river.enable_edges()
for edge in edges:
name = f"Reach_{edge.id + 1:>3}".replace(" ", "0")
name = self.get_reach_name(edge)
with adists_file_open(
os.path.join(repertory, "net", f"{name}.ST"),
@@ -191,7 +180,7 @@ class AdisTS(CommandLineSolver):
files.append(str(os.path.join("net", f"{name}.ST")))
cnt_num = 1
for profile in edge.reach.profiles:
for profile in edge.reach.enabled_profiles:
self._export_ST_profile_header(
f, files, profile, cnt_num
)
@@ -277,7 +266,7 @@ class AdisTS(CommandLineSolver):
edges = study.river.enable_edges()
for e in edges:
name = f"Reach_{e.id + 1:>3}".replace(" ", "0")
name = self.get_reach_name(e)
id = name
n1 = f"{self.get_node_name(e.node1):3}".replace(" ", "x")
@@ -346,7 +335,7 @@ class AdisTS(CommandLineSolver):
return coeff.minor, coeff.medium
for j, profile in enumerate(edge.reach.profiles):
for j, profile in enumerate(edge.reach.enabled_profiles):
coef_min, coef_moy = get_stricklers_from_rk(profile.rk,
lst)
f.write(
@@ -366,13 +355,19 @@ class AdisTS(CommandLineSolver):
if qlog is not None:
qlog.put("Export REP file")
mage_path = (mage_rep if os.path.isabs(mage_rep) else os.path.join(
os.path.abspath(os.path.join(repertory, os.pardir)),
mage_rep,
))
relative_mage_path = os.path.relpath(mage_path, repertory)
# Write header
with adists_file_open(
os.path.join(
repertory, f"{name}.REP"
), "w+"
) as f:
path = os.path.join("..", mage_rep, name)
path = os.path.join(relative_mage_path, name)
f.write(f"NET {name}.NET\n")
f.write(f"REP {path}.REP\n")
@@ -387,9 +382,7 @@ class AdisTS(CommandLineSolver):
# fake mage_fin.ini:
path_mage = os.path.join(os.path.abspath(
os.path.join(repertory, os.pardir)), mage_rep)
self._export_fake_INI(study, path_mage,
self._export_fake_INI(study, mage_path,
qlog, name=name)
# path_mage_net = os.path.join(os.path.abspath(
@@ -415,6 +408,41 @@ class AdisTS(CommandLineSolver):
# RESULTS #
###########
@staticmethod
def _remove_temperature_results(repertory):
"""Remove temperature outputs generated but unused by AdisTS."""
results_directory = os.path.join(repertory, "resultats")
if not os.path.isdir(results_directory):
results_directory = repertory
if not os.path.isdir(results_directory):
return
for filename in os.listdir(results_directory):
if not (filename.startswith("TEM.") or
filename.startswith("TEM_")):
continue
filepath = os.path.join(results_directory, filename)
if os.path.isfile(filepath):
try:
os.remove(filepath)
logger.debug(
f"Removed unused AdisTS result '{filepath}'"
)
except OSError as error:
logger.warning(
f"Unable to remove AdisTS result '{filepath}': "
f"{error}"
)
def run_output_data_fomater(self, study):
# AdisTS generates temperature results that are not used by the solver,
# but they are still present in the output directory.
# Remove them to avoid confusion.
self._remove_temperature_results(self._process.workingDirectory())
return super(AdisTS, self).run_output_data_fomater(study)
def read_bin(self, study, repertory, results, qlog=None, name="0"):
return
@@ -453,7 +481,8 @@ class AdisTS(CommandLineSolver):
_nodes_views = set()
def get_reach_name(self, reach):
return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0")
index = self._study.river.get_edge_id(reach) + 1
return f"Reach_{index:03d}"
#################################
# Adis-TS in weak coupling mode #
@@ -507,12 +536,13 @@ class AdisTSwc(AdisTS):
study, pollutant._data, f, qlog
)
POL_ICs = next(filter(
lambda ic: ic.pollutant == pollutant.id,
POL_ICs = next((
ic for ic in
study.river.ic_adists.Initial_Conditions_List
))
if ic.pollutant == pollutant.id
), None)
if POL_ICs.concentration is not None:
if POL_ICs is not None and POL_ICs.concentration is not None:
f.write(f"file_ini = {name}.INI\n")
self._export_ICs_AdisTS(
study, repertory, POL_ICs, qlog, name
@@ -552,15 +582,18 @@ class AdisTSwc(AdisTS):
os.path.join(repertory, f"{POL_name}.ALD"), "w+"
) as f:
for LC in POL_LC:
reach = next(filter(
lambda edge: edge.id == LC.edge, study.river.enable_edges()
)) # .name
reach = next((
edge for edge in study.river.enable_edges()
if edge.id == LC.reach
), None)
if reach is None:
continue
reach_name = self.get_reach_name(reach)
f.write(f"${reach_name} {LC.begin_rk} {LC.end_rk}\n")
f.write(f"*temps |débit massique (kg/s)\n")
f.write(f"*---------++++++++++\n")
for LC_data in LC._data:
for LC_data in LC.data:
tmp = timestamp_to_old_pamhyr_date_adists(int(LC_data[0]))
f.write(" ".join((f"{tmp}",
f"{LC_data[1]}\n")))
@@ -577,9 +610,10 @@ class AdisTSwc(AdisTS):
repertory, f"{POL_name}.CDT"), "w+"
) as f:
for BC in POL_BC:
node = next(filter(
lambda x: x.id == BC.node, study.river._nodes
))
node = next((node for node in study.river.nodes()
if node.id == BC.node), None)
if node is None:
continue
f.write(f"${self.get_node_name(node)}\n")
if BC.type == "Concentration":
@@ -591,7 +625,7 @@ class AdisTSwc(AdisTS):
f.write(f"*JJ:HH:MM | (kg/s)\n")
f.write(f"*---------++++++++++\n")
for BC_data in BC._data:
for BC_data in BC.data:
tmp = timestamp_to_old_pamhyr_date_adists(int(BC_data[0]))
f.write(" ".join((f"{tmp}",
f"{BC_data[1]}\n")))
@@ -625,6 +659,9 @@ class AdisTSwc(AdisTS):
edges = study.river.enable_edges()
for ic_spec in pol_ics_spec_data:
if ic_spec.is_deleted():
continue
id_reach = ic_spec.reach
reach = next((x for x in edges if x.id == id_reach), None)
if reach is None:
@@ -648,7 +685,9 @@ class AdisTSwc(AdisTS):
list_characteristics = ["type", "diametre", "rho", "porosity",
"cdc_riv", "cdc_cas", "apd", "ac", "bc"]
if len(list_characteristics) <= (len(pol_data[0])):
pol_data = [data for data in pol_data if not data.is_deleted()]
if pol_data and len(list_characteristics) <= len(pol_data[0]):
for i in range(len(list_characteristics)):
f.write(f"{list_characteristics[i]} = {pol_data[0][i]}\n")
@@ -677,6 +716,9 @@ class AdisTSwc(AdisTS):
def _export_d90_spec(self, study, d90_spec_data, f, qlog, name="0"):
for d90_spec in d90_spec_data:
if d90_spec.is_deleted():
continue
if (d90_spec.name is None
or d90_spec.reach is None
or d90_spec.start_rk is None
@@ -736,6 +778,9 @@ class AdisTSwc(AdisTS):
def _export_dif_spec(self, study, dif_spec_data, f, qlog, name="0"):
for dif_spec in dif_spec_data:
if dif_spec.is_deleted():
continue
if (dif_spec.reach is None
or dif_spec.start_rk is None
or dif_spec.end_rk is None
@@ -803,33 +848,8 @@ class AdisTSwc(AdisTS):
if name != "command_line_arguments":
f.write(f"{dict_names[name]} = {value}\n")
outputrks = study.river.Output_rk_adists.OutputRK_List
for outputrk in outputrks:
self._export_outputrk(study, outputrk, f, qlog)
return files
def _export_outputrk(self, study, outputrk, f, qlog, name="0"):
if (outputrk.reach is None
or outputrk.rk is None
or outputrk.title is None):
return
edges = study.river.enable_edges()
id_edges = list(map(lambda x: x.id, edges))
id_reach = outputrk.reach
reach = next((x for x in edges if x.id == id_reach), None)
rk = outputrk.rk
title = outputrk.title
if reach is None:
return
f.write(f"output = {study.river.get_edge_id(reach)+1} {rk} {title}\n")
@timer
def read_bin(self, study, repertory, results, qlog=None, name="0"):
@@ -876,7 +896,10 @@ class AdisTSwc(AdisTS):
for i in range(ibmax):
# Add results reach to reach list
r = results.river.add(i)
geometry_reach = study.river.enable_edges()[i].reach
r = results.river.add(
i, profiles=geometry_reach.enabled_profiles
)
reachs.append(r)
is1[i] = data[2 * i] - 1 # first section of reach i
+1042
View File
File diff suppressed because it is too large. Load diff
+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 = []
+88 -48
View File
@@ -28,6 +28,7 @@ from tools import timer, trace, logger_exception
from Solver.CommandLine import CommandLineSolver
from Checker.Mage import (
MageNetworkGraphChecker, MageGeometryGuideLineChecker,
MageLateralSolidChecker,
)
from Model.Results.Results import Results
@@ -65,6 +66,8 @@ class Mage(CommandLineSolver):
self._cmd_solver = "@path @input -o @output"
self._cmd_output = ""
self._reset_node_names()
@classmethod
def default_parameters(cls):
lst = super(Mage, cls).default_parameters()
@@ -104,6 +107,7 @@ class Mage(CommandLineSolver):
MageNetworkGraphChecker(connectivity=True, version=cls._type),
MageNetworkGraphChecker(connectivity=False, version=cls._type),
MageGeometryGuideLineChecker(version=cls._type),
MageLateralSolidChecker(version=cls._type),
]
return lst
@@ -125,12 +129,15 @@ class Mage(CommandLineSolver):
_l_alph = len(_alph)
_nodes_cnt = 0
_nodes_names = {}
_nodes_views = set()
def _reset_node_names(self):
"""Reset the node-name registry for a new export."""
self._nodes_cnt = 0
self._nodes_names = {}
self._nodes_views = set()
def get_reach_name(self, reach):
return f"Reach_{reach.pamhyr_id:>3}".replace(" ", "0")
index = self._study.river.get_edge_id(reach) + 1
return f"Reach_{index:03d}"
def get_node_name(self, node):
"""Generate a 3 char name for node
@@ -210,7 +217,7 @@ class Mage(CommandLineSolver):
# Write header
edges = study.river.enable_edges()
for edge in edges:
name = f"Reach_{edge.pamhyr_id:>3}".replace(" ", "0")
name = self.get_reach_name(edge)
with mage_file_open(
os.path.join(repertory, "net", f"{name}.ST"),
@@ -219,7 +226,7 @@ class Mage(CommandLineSolver):
files.append(str(os.path.join("net", f"{name}.ST")))
cnt_num = 1
for profile in edge.reach.profiles:
for profile in edge.reach.enabled_profiles:
self._export_ST_profile_header(
f, files, profile, cnt_num
)
@@ -298,7 +305,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:
@@ -328,7 +335,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")
@@ -360,10 +367,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
@@ -379,7 +393,7 @@ class Mage(CommandLineSolver):
qlog.put(f"Export LAT file")
files.append(f"{name}.LAT")
for tab in ["liquid", "solid", "suspenssion"]:
for tab in ["liquid"]:
for lateral in lst.get_tab(tab):
self._export_LC(study, lateral, f, qlog)
@@ -417,10 +431,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 = []
@@ -484,7 +499,12 @@ class Mage(CommandLineSolver):
if cond.is_deleted():
continue
data = cond.data
enabled_profiles = set(reach.reach.enabled_profiles)
data = [
item for item in cond.data
if not item.is_deleted()
and item["section"] in enabled_profiles
]
if len(data) == 0:
continue
@@ -492,9 +512,6 @@ class Mage(CommandLineSolver):
id_sec = 1
for d in data:
if d.is_deleted():
continue
IR = f"{id}"
IS = f"{id_sec}"
discharge = f"{d['discharge']:>10.5f}"
@@ -679,6 +696,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 = []
@@ -729,9 +763,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
@@ -957,7 +993,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:
@@ -987,7 +1023,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
@@ -1004,6 +1041,7 @@ class Mage8(Mage):
@timer
def export(self, study, repertory, qlog=None, name="0"):
self._study = study
self._reset_node_names()
name = study.name.replace(" ", "_")
# Define GRA file name
@@ -1160,7 +1198,10 @@ class Mage8(Mage):
for i in range(nb_reach):
# Add results reach to reach list
r = results.river.reach(i)
geometry_reach = study.river.enable_edges()[i].reach
r = results.river.add(
i, profiles=geometry_reach.enabled_profiles
)
reachs.append(r)
# ID of first and last reach profiles
@@ -1465,7 +1506,10 @@ class Mage8(Mage):
zfd_lst = []
for r in reachs:
z_min = r.geometry.get_z_min()
z_min = [
profile.geometry.z_min()
for profile in r.profiles
]
sls = map(
lambda p: p.get_ts_key(ts_list[0], "sl")[0],
r.profiles
@@ -1621,25 +1665,33 @@ class Mage8(Mage):
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
res["res_z_min"] = []
res["res_z_mean"] = []
res["res_z_max"] = []
res["res_q_min"] = []
res["res_q_mean"] = []
res["res_q_max"] = []
res["traces"] = []
else:
# Get mean results for Z and Q
res["z_mean"] = (res["z_mean"] + z) / 2
res["q_mean"] = (res["q_mean"] + q) / 2
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"])
@@ -1647,18 +1699,6 @@ class Mage8(Mage):
res["q_min"] = np.minimum(q, res["q_min"])
res["q_max"] = np.maximum(q, res["q_max"])
# Keep results min, mean and max
res["res_z_min"].append((run_number, np.min(z, axis=0)))
res["res_z_mean"].append((run_number, np.mean(z, axis=0)))
res["res_z_max"].append((run_number, np.max(z, axis=0)))
res["res_q_min"].append((run_number, np.min(q, axis=0)))
res["res_q_mean"].append((run_number, np.mean(q, axis=0)))
res["res_q_max"].append((run_number, np.max(q, axis=0)))
# FIXME: Keep all results ?
res["traces"].append((run_number, tables))
@timer
def results_ensemble(self, study, repertory,
tables, run_number,
+1388 -389
View File
File diff suppressed because it is too large. Load diff
+5 -4
View File
@@ -23,6 +23,7 @@ from Solver.Mage import (
Mage7, Mage8, MageFake7,
)
from Solver.AdisTS import AdisTSwc
from Solver.AdisTT import AdisTTwc
from Solver.RubarBE import Rubar3, RubarBE
_translate = QCoreApplication.translate
@@ -33,8 +34,8 @@ solver_long_name = {
"mage8": "Mage v8",
# "mage_fake7": "Mage fake v7",
"adistswc": "Adis-TS_WC",
# "rubarbe": "RubarBE",
"rubar3": "Rubar3",
"adisttwc": "Adis-TT_WC",
"rubarbe": "RubarBE",
}
solver_type_list = {
@@ -43,6 +44,6 @@ solver_type_list = {
"mage8": Mage8,
# "mage_fake7": MageFake7,
"adistswc": AdisTSwc,
# "rubarbe": RubarBE,
"rubar3": Rubar3,
"adisttwc": AdisTTwc,
"rubarbe": RubarBE,
}
+1 -1
View File
@@ -54,7 +54,7 @@ class Plot(PamhyrPlot):
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
self._autoscale_update = True
def custom_ticks(self):
if self.data.header[0] != "time":
+49 -19
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
old_pamhyr_date_to_timestamp, format_datetime, parse_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -94,10 +94,13 @@ 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 = str(datetime.fromtimestamp(v))
value = format_datetime(
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -114,6 +117,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
self._undo.push(
@@ -143,21 +150,43 @@ class TableModel(PamhyrTableModel):
self.update()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
if not rows:
return
rows = list(map(
lambda r: self.get_true_data_row(r),
rows))
rows = sorted(set(rows))
rows = [
self.get_true_data_row(row)
for row in rows
]
self._undo.push(
DelCommand(
self._data, rows
# A multiple selection may contain non-contiguous rows. A model reset
# accurately represents this atomic undo command, unlike one
# beginRemoveRows() call spanning rows that are not all removed.
self.beginResetModel()
try:
self._undo.push(
DelCommand(
self._data, rows
)
)
)
self.endRemoveRows()
finally:
self.endResetModel()
self.update()
def undo(self):
self.beginResetModel()
try:
self._undo.undo()
finally:
self.endResetModel()
def redo(self):
self.beginResetModel()
try:
self._undo.redo()
finally:
self.endResetModel()
def sort(self, _reverse, parent=QModelIndex()):
self.layoutAboutToBeChanged.emit()
@@ -210,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
)
)
+13 -18
View File
@@ -97,12 +97,11 @@ class DelCommand(QUndoCommand):
QUndoCommand.__init__(self)
self._data = data
self._rows = rows
self._bc = []
for row in rows:
self._bc.append(self._data.get_i(row))
self._bc.sort()
self._rows = sorted(set(rows))
self._bc = [
self._data.get_i(row)
for row in self._rows
]
def undo(self):
for el in self._bc:
@@ -120,13 +119,17 @@ class SortCommand(QUndoCommand):
self._data = data
self._reverse = _reverse
self._old = self._data.data
self._indexes = None
# Keep the order of the complete underlying list, including soft
# deleted items. ``data`` only exposes visible items and therefore
# cannot reliably restore a sort after a delete/undelete operation.
self._old_positions = {
id(item): index
for index, item in enumerate(self._data._data)
}
def undo(self):
ll = self._data.data
self._data.sort(
key=lambda x: self._indexes[ll.index(x)]
key=lambda item: self._old_positions[id(item)]
)
def redo(self):
@@ -134,14 +137,6 @@ class SortCommand(QUndoCommand):
_reverse=self._reverse,
key=lambda x: x[0]
)
if self._indexes is None:
self._indexes = list(
map(
lambda p: self._old.index(p),
self._data.data
)
)
self._old = None
class MoveCommand(QUndoCommand):
+13 -2
View File
@@ -18,6 +18,8 @@
import logging
from Modules import Modules
from tools import timer, trace
from numpy import sqrt
@@ -173,7 +175,8 @@ class EditBoundaryConditionWindow(PamhyrWindow):
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system
opt_data=self._study.time_system,
study=self._study
)
table.setModel(self._table)
@@ -185,7 +188,10 @@ class EditBoundaryConditionWindow(PamhyrWindow):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self
self.canvas, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.verticalLayout.addWidget(self.toolbar)
self.verticalLayout.addWidget(self.canvas)
@@ -261,6 +267,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,10 +21,11 @@ import traceback
from datetime import date, time, datetime, timedelta
from tools import parse_datetime
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
old_pamhyr_date_to_timestamp, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
@@ -84,10 +85,13 @@ 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 = str(datetime.fromtimestamp(v))
value = format_datetime(
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
@@ -104,6 +108,10 @@ class TableModel(PamhyrTableModel):
column = index.column()
try:
if (self._data.header[column] == "time"
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
self._undo.push(
@@ -163,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
@@ -64,14 +66,22 @@ class EditBoundaryConditionWindow(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
)
n = self._data.node
node_name = next(filter(
lambda x: x.id == n, study.river._nodes
)).name
name += (
f" - {study.name} " +
f"({node_name})"
)
name += f" - {study.name}"
if pollutant_name is not None:
name += f" - {pollutant_name}"
name += f" ({node_name})"
super(EditBoundaryConditionWindow, self).__init__(
title=name,
@@ -120,7 +130,8 @@ class EditBoundaryConditionWindow(PamhyrWindow):
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system
opt_data=self._study.time_system,
study=self._study
)
table.setModel(self._table)
@@ -153,8 +164,14 @@ 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()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
@@ -0,0 +1,105 @@
# Plot.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from datetime import datetime
from tools import timer, trace
from View.Tools.PamhyrPlot import PamhyrPlot
from PyQt5.QtCore import (
QCoreApplication
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class Plot(PamhyrPlot):
def __init__(self, mode="time", data=None,
trad=None, canvas=None, toolbar=None,
parent=None):
super(Plot, self).__init__(
canvas=canvas,
trad=trad,
data=data,
toolbar=toolbar,
parent=parent
)
self._table_headers = self._trad.get_dict("table_headers")
header = self.data.header
self.label_x = self._table_headers[header[0]]
self.label_y = self._table_headers[header[1]]
self._mode = mode
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
def custom_ticks(self):
if self.data.header[0] != "time":
return
self.set_ticks_time_formater()
@timer
def draw(self):
self.init_axes()
if len(self.data) == 0:
self._init = False
return
self.draw_data()
self.custom_ticks()
self.idle()
self._init = True
def draw_data(self):
# Plot data
x = list(map(lambda v: v[0], self.data.data))
y = list(map(lambda v: v[1], self.data.data))
self._line, = self.canvas.axes.plot(
x, y,
color=self.color_plot,
**self.plot_default_kargs
)
@timer
def update(self, ind=None):
if not self._init:
self.draw()
return
self.update_data()
self.update_idle()
def update_data(self):
x = list(map(lambda v: v[0], self.data.data))
y = list(map(lambda v: v[1], self.data.data))
self._line.set_data(x, y)
@@ -0,0 +1,194 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from datetime import date, time, datetime, timedelta
from tools import parse_datetime
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp, format_datetime
)
from View.Tools.PamhyrTable import PamhyrTableModel
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QTime, QDateTime,
)
from PyQt5.QtWidgets import (
QTableView, QAbstractItemView, QSpinBox,
QTimeEdit, QDateTimeEdit, QItemDelegate,
)
from View.BoundaryConditionsTemperature.Edit.UndoCommand import (
AddCommand, DelCommand, SetDataCommand, PasteCommand, SortCommand,
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class TableModel(PamhyrTableModel):
def get_true_data_row(self, row):
if len(self._data.data) > 0:
bc = self._data.data[row]
else:
return 0
return next(
map(
lambda e: e[0],
filter(
lambda e: e[1] == bc,
enumerate(self._data._data)
)
), 0
)
def data(self, index, role):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter | Qt.AlignVCenter
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
value = QVariant()
if 0 <= column < 2:
row = self.get_true_data_row(row)
v = self._data._data[row][column]
if self._data._types[column] == float:
value = f"{v:.2f}"
elif self._data.header[column] == "time":
if self.time_system == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = format_datetime(
parse_datetime(self.start_date)
+ timedelta(seconds=float(v))
)
else:
value = f"{v}"
return value
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
try:
if (self._data.header[column] == "time"
and self.time_system == "date"):
value = self.date_to_elapsed_seconds(value)
row = self.get_true_data_row(row)
self._undo.push(
SetDataCommand(
self._data, row, column, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
self._undo.push(
AddCommand(
self._data, row
)
)
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
_rows = list(
map(
lambda r: self.get_true_data_row(r),
rows
)
)
self._undo.push(
DelCommand(
self._data, _rows
)
)
self.endRemoveRows()
self.layoutChanged.emit()
def sort(self, _reverse, parent=QModelIndex()):
self.layoutAboutToBeChanged.emit()
self._undo.push(
SortCommand(
self._data, _reverse
)
)
self.layoutAboutToBeChanged.emit()
self.update()
def paste(self, row, header, data):
if len(data) == 0:
return
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,
rows
)
)
self.layoutAboutToBeChanged.emit()
self.update()
def update(self):
# self.auto_sort()
self.layoutChanged.emit()
@@ -0,0 +1,137 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
logger = logging.getLogger()
class SetDataCommand(QUndoCommand):
def __init__(self, data, index, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._index = index
self._column = column
self._old = self._data.get_i(self._index)[self._column]
self._new = new_value
def undo(self):
self._data._set_i_c_v(self._index, self._column, self._old)
def redo(self):
self._data._set_i_c_v(self._index, self._column, self._new)
class AddCommand(QUndoCommand):
def __init__(self, data, index):
QUndoCommand.__init__(self)
self._data = data
self._index = index
self._new = None
def undo(self):
del self._data._data[self._index]
def redo(self):
if self._new is None:
self._new = self._data.add(self._index)
else:
self._data.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, data, rows):
QUndoCommand.__init__(self)
self._data = data
self._rows = rows
self._bc = []
for row in rows:
self._bc.append((row, self._data._data[row]))
self._bc.sort()
def undo(self):
for row, el in self._bc:
el.set_as_not_deleted()
def redo(self):
for row, el in self._bc:
el.set_as_deleted()
class PasteCommand(QUndoCommand):
def __init__(self, data, row, bcs):
QUndoCommand.__init__(self)
self._data = data
self._row = row
self._bcs = bcs
self._bcs.reverse()
def undo(self):
self._data.delete_i(
range(self._row, self._row + len(self._bcs))
)
def redo(self):
for bc in self._bcs:
self._data.insert(self._row, bc)
class SortCommand(QUndoCommand):
def __init__(self, data, _reverse):
QUndoCommand.__init__(self)
self._data = data
self._reverse = _reverse
self._old = self._data.data
self._indexes = None
def undo(self):
ll = self._data.data
self._data.sort(
key=lambda x: self._indexes[ll.index(x)]
)
def redo(self):
self._data.sort(
_reverse=self._reverse,
key=lambda x: x[0]
)
if self._indexes is None:
self._indexes = list(
map(
lambda p: self._old.index(p),
self._data.data
)
)
self._old = None
@@ -0,0 +1,238 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from Modules import Modules
from tools import timer, trace
from View.Tools.PamhyrWindow import PamhyrWindow
from View.Tools.PamhyrWidget import PamhyrWidget
from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtCore
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel, QCoreApplication,
pyqtSlot, pyqtSignal,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QHeaderView, QDoubleSpinBox, QVBoxLayout,
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.BoundaryConditionsTemperature.Edit.translate import BCETranslate
from View.BoundaryConditionsTemperature.Edit.Table import TableModel
from View.BoundaryConditionsTemperature.Edit.Plot import Plot
_translate = QCoreApplication.translate
logger = logging.getLogger()
class EditBoundaryConditionWindow(PamhyrWindow):
_pamhyr_ui = "EditBoundaryConditionsTemperature"
_pamhyr_name = "Edit Boundary Conditions Temperature"
def __init__(self, data=None, study=None, config=None, parent=None):
self._data = data
trad = BCETranslate()
name = trad[self._pamhyr_name]
if self._data is not None:
n = self._data.node
node_name = next(filter(
lambda x: x.id == n, study.river._nodes
)).name
name += (
f" - {study.name} " +
f"({node_name})"
)
super(EditBoundaryConditionWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._hash_data.append(data)
self.setup_table()
self.setup_plot()
self.setup_connections()
def setup_table(self):
table_headers = self._trad.get_dict("table_headers")
if self._study.is_editable():
editable_headers = self._trad.get_dict("table_headers")
else:
editable_headers = []
self._data.header = ["time", "temperature"]
headers = {}
for h in self._data.header:
headers[h] = table_headers[h]
self._delegate_time = PamhyrExTimeDelegate(
data=self._data,
mode=self._study.time_system,
parent=self
)
table = self.find(QTableView, "tableView")
self._table = TableModel(
table_view=table,
table_headers=headers,
editable_headers=editable_headers,
delegates={
# "time": self._delegate_time,
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system,
study=self._study
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_plot(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self
)
self.verticalLayout.addWidget(self.toolbar)
self.verticalLayout.addWidget(self.canvas)
self.plot = Plot(
canvas=self.canvas,
data=self._data,
mode=self._study.time_system,
trad=self._trad,
toolbar=self.toolbar,
)
self.plot.draw()
def setup_connections(self):
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_sort").triggered.connect(self.sort)
self._table.dataChanged.connect(self.update)
def _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()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
return table.selectionModel()\
.selectedRows()[0]\
.row()
def index_selected_rows(self):
table = self.find(QTableView, "tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._data) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
self.plot.update()
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
self.plot.update()
def sort(self):
self._table.sort(False)
self.plot.update()
def _copy(self):
rows = self.index_selected_rows()
table = []
table.append(self._data.header)
data = self._data.data
for row in rows:
table.append(list(data[row]))
self.copyTableIntoClipboard(table)
def _paste(self):
header, data = self.parseClipboardTable()
logger.debug(f"paste: h:{header}, d:{data}")
if len(data) == 0:
return
row = 0
rows = self.index_selected_rows()
if len(rows) != 0:
row = rows[0]
self._table.paste(row, header, data)
self.plot.update()
def _undo(self):
self._table.undo()
self.plot.update()
def _redo(self):
self._table.redo()
self.plot.update()
@@ -1,5 +1,5 @@
# translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -20,21 +20,21 @@ from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
from View.BoundaryCondition.translate import BCTranslate
_translate = QCoreApplication.translate
class OutputRKAdisTSTranslate(MainTranslate):
class BCETranslate(BCTranslate):
def __init__(self):
super(OutputRKAdisTSTranslate, self).__init__()
super(BCETranslate, self).__init__()
self._dict["Output RK"] = _translate(
"OutputRKAdisTS", "Output RK"
self._dict["Edit Boundary Conditions Temperature"] = _translate(
"BoundaryConditionsTemperature",
"Edit boundary conditions Temperature"
)
self._dict["x"] = _translate("OutputRKAdisTS", "X (m)")
self._sub_dict["table_headers"] = {
"title": self._dict["title"],
"reach": self._dict["reach"],
"rk": self._dict["unit_rk"],
"time": self._dict["time"],
"temperature": self._dict["unit_temperature"],
}
@@ -1,5 +1,5 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -36,60 +36,36 @@ from PyQt5.QtWidgets import (
from View.Tools.PamhyrTable import PamhyrTableModel
from View.OutputRKAdisTS.UndoCommand import (
SetTitleCommand, SetReachCommand, SetRKCommand,
SetEnabledCommand, AddCommand, DelCommand,
from View.BoundaryConditionsTemperature.UndoCommand import (
SetNodeCommand, SetNameCommand,
AddCommand, DelCommand
)
from functools import reduce
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, trad=None, parent=None, mode="reaches"):
def __init__(self, data=None, mode="node", tab="",
trad=None, parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._trad = trad
self._mode = mode
self._tab = tab
self._trad = trad
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
val = []
if self._mode == "rk":
reach_id = self._data.Output_rk_adists \
.get(index.row()) \
.reach
if reach_id is not None:
reach = next(
filter(
lambda edge: edge.id == reach_id, self._data.edges()
)
)
val = list(
map(
lambda rk: str(rk), reach.reach.get_rk()
)
)
else:
val = list(
map(
lambda n: n.name, self._data.edges()
)
if self._mode == "node":
self.editor.addItems(
[self._trad["not_associated"]] +
self._data.nodes_names()
)
self.editor.addItems(
[self._trad['not_associated']] +
val
)
self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
self.editor.setCurrentText(index.data(Qt.DisplayRole))
return self.editor
def setEditorData(self, editor, index):
@@ -115,10 +91,27 @@ class ComboBoxDelegate(QItemDelegate):
class TableModel(PamhyrTableModel):
def _setup_lst(self):
self._lst = self._data._Output_rk_adists
def __init__(self, bc_list=None, trad=None, **kwargs):
self._trad = trad
self._bc_list = bc_list
def rowCount(self, parent):
super(TableModel, self).__init__(trad=trad, **kwargs)
def _setup_lst(self):
self._lst = self._bc_list.lst
def get(self, row):
if row < 0 or row >= len(self._lst):
return None
return self._lst[row]
def _global_row(self, row):
bc = self.get(row)
if bc is None:
return None
return self._bc_list.index(bc)
def rowCount(self, parent=QModelIndex()):
return len(self._lst)
def data(self, index, role):
@@ -128,20 +121,20 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
if self._headers[column] == "title":
return self._lst.get(row).title
elif self._headers[column] == "reach":
n = self._lst.get(row).reach
if n is None:
return self._trad['not_associated']
return next(filter(
lambda edge: edge.id == n, self._data.edges())
).name
elif self._headers[column] == "rk":
n = self._lst.get(row).rk
if n is None:
return self._trad['not_associated']
if self._headers[column] == "name":
n = self._lst[row].name
if n is None or n == "":
return self._trad["default"]
return n
elif self._headers[column] == "node":
n = self._lst[row].node
if n is None:
return self._trad["not_associated"]
tmp = next(filter(lambda x: x.id == n, self._data._nodes), None)
if tmp is not None:
return tmp.name
else:
return self._trad["not_associated"]
return QVariant()
@@ -154,76 +147,79 @@ class TableModel(PamhyrTableModel):
row = index.row()
column = index.column()
na = self._trad['not_associated']
try:
if self._headers[column] == "title":
if self._headers[column] == "name":
global_row = self._global_row(row)
self._undo.push(
SetTitleCommand(
self._lst, row, value
SetNameCommand(
self._bc_list, global_row, value
)
)
elif self._headers[column] == "node":
global_row = self._global_row(row)
node = next(
filter(lambda n: n.name == value, self._data.nodes()),
None
)
self._undo.push(
SetNodeCommand(
self._bc_list, global_row, node
)
)
elif self._headers[column] == "reach":
if value == na:
value = None
self._undo.push(
SetReachCommand(
self._lst, row, self._data.edge(value)
)
)
elif self._headers[column] == "rk":
if value == na:
value = None
self._undo.push(
SetRKCommand(
self._lst, row, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
if self._headers[column] == "node" and self.rowCount() > 0:
self.dataChanged.emit(
self.index(0, column),
self.index(self.rowCount() - 1, column)
)
else:
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
row = len(self._lst)
self.beginInsertRows(parent, row, row)
self._undo.push(
AddCommand(
self._lst, row
self._bc_list, len(self._bc_list)
)
)
self._setup_lst()
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
row_by_bc = {
id(bc): row for row, bc in enumerate(self._bc_list._lst)
}
global_rows = [
row_by_bc[id(self._lst[row])]
for row in rows
if 0 <= row < len(self._lst)
]
self._undo.push(
DelCommand(
self._lst, rows
self._bc_list, global_rows
)
)
self._setup_lst()
self.endRemoveRows()
self.layoutChanged.emit()
def enabled(self, row, enabled, parent=QModelIndex()):
self._undo.push(
SetEnabledCommand(
self._lst, row, enabled
)
)
self.layoutChanged.emit()
def undo(self):
self._undo.undo()
self._setup_lst()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self._setup_lst()
self.layoutChanged.emit()
@@ -0,0 +1,111 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
class SetNodeCommand(QUndoCommand):
def __init__(self, bcs, index, node):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._old = self._bcs.get(self._index).node
self._new = node.id if node is not None else None
self._previous = next(
(
bc for i, bc in enumerate(self._bcs.lst)
if i != self._index and bc.node == self._new
),
None
)
def undo(self):
self._bcs.get(self._index).node = self._old
if self._previous is not None:
self._previous.node = self._new
def redo(self):
self._bcs.get(self._index).node = self._new
if self._previous is not None:
self._previous.node = None
class SetNameCommand(QUndoCommand):
def __init__(self, bcs, index, name):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._name = name
self._old = self._bcs.get(self._index).name
self._new = self._name
def undo(self):
self._bcs.get(self._index).name = self._old
def redo(self):
self._bcs.get(self._index).name = self._new
class AddCommand(QUndoCommand):
def __init__(self, bcs, index):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._new = None
def undo(self):
self._bcs.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._bcs.new(self._index)
else:
self._bcs.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, bcs, rows):
QUndoCommand.__init__(self)
self._bcs = bcs
self._rows = rows
self._bc = []
for row in rows:
self._bc.append((row, self._bcs._lst[row]))
self._bc.sort()
def undo(self):
for row, el in self._bc:
self._bcs.insert(row, el)
def redo(self):
self._bcs.delete_i(self._rows)
@@ -0,0 +1,192 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QWidget,
)
from View.BoundaryConditionsTemperature.Table import (
TableModel, ComboBoxDelegate
)
from View.Network.GraphWidget import GraphWidget
from View.BoundaryConditionsTemperature.translate import BCTemperatureTranslate
from View.BoundaryConditionsTemperature.Edit.Window \
import EditBoundaryConditionWindow
_translate = QCoreApplication.translate
logger = logging.getLogger()
class BoundaryConditionsTemperatureWindow(PamhyrWindow):
_pamhyr_ui = "BoundaryConditionsTemperature"
_pamhyr_name = "Boundary conditions temperature"
def __init__(self, data=None, study=None,
config=None, parent=None):
self._data = data
trad = BCTemperatureTranslate()
name = (
trad[self._pamhyr_name] +
" - " + study.name
)
super(BoundaryConditionsTemperatureWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._bcs = self._study.river.boundary_conditions_temperature
self.setup_graph()
self.setup_table()
self.setup_connections()
def setup_table(self):
self._delegate_node = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
mode="node",
parent=self
)
if self._study.is_editable():
editable_headers = self._trad.get_dict("table_headers")
else:
editable_headers = []
table = self.find(QTableView, f"tableView")
self._table = TableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates={
"node": self._delegate_node,
},
trad=self._trad,
bc_list=self._study.river.boundary_conditions_temperature,
undo=self._undo_stack,
data=self._study.river
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_graph(self):
self.graph_widget = GraphWidget(
self._study.river,
min_size=None, size=(200, 200),
only_display=True,
parent=self
)
self.graph_layout = self.find(QVBoxLayout, "verticalLayout")
self.graph_layout.addWidget(self.graph_widget)
def setup_connections(self):
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
def index_selected_row(self):
table = self.find(QTableView, f"tableView")
return table.selectionModel()\
.selectedRows()[0]\
.row()
def index_selected_rows(self):
table = self.find(QTableView, f"tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
self._table.add(self._table.rowCount())
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _copy(self):
logger.info("TODO: copy")
def _paste(self):
logger.info("TODO: paste")
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def edit(self):
rows = self.index_selected_rows()
for row in rows:
data = self._table.get(row)
if data.node is None:
continue
if self.sub_window_exists(
EditBoundaryConditionWindow,
data=[self._study, None, data]
):
continue
win = EditBoundaryConditionWindow(
data=data,
study=self._study,
parent=self
)
win.show()
@@ -1,5 +1,5 @@
# Adists.py -- Pamhyr study checkers
# Copyright (C) 2023-2026 INRAE
# translate.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,37 +16,25 @@
# -*- coding: utf-8 -*-
import time
import logging
from functools import reduce
from PyQt5.QtCore import QCoreApplication
from Modules import Modules
from Checker.Checker import AbstractModelChecker, STATUS
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
logger = logging.getLogger()
class AdistsOutputRKChecker(AbstractModelChecker):
class BCTemperatureTranslate(MainTranslate):
def __init__(self):
super(AdistsOutputRKChecker, self).__init__()
super(BCTemperatureTranslate, self).__init__()
self._name = _translate("Checker", "AdisTS output RK checker")
self._description = _translate(
"Checker", "Check output RK"
self._dict["Boundary conditions temperature"] = _translate(
"BoundaryConditionsTemperature", "Boundary conditions temperature"
)
self._dict["default"] = _translate(
"BoundaryConditionsTemperature", "default"
)
self._modules = Modules.OUTPUT_RK
def run(self, study):
ok = True
nerror = 0
self._summary = "ok"
self._status = STATUS.OK
if not self.basic_check(study):
return False
return ok
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"node": _translate("BoundaryCondition", "Node"),
}
+12 -3
View File
@@ -53,12 +53,13 @@ class CheckListWindowAdisTS(PamhyrWindow):
def __init__(self, autorun: bool = True,
study=None, config=None,
solver=None, parent=None, mage_rep=None):
solver=None, parent=None, mage_rep=None, mode="adists"):
trad = CheckListTranslate()
self._autorun = autorun
self._solver = solver
self._mage_rep = mage_rep
self._mode = mode
name = trad[self._pamhyr_name] + " - " + study.name
@@ -73,6 +74,8 @@ class CheckListWindowAdisTS(PamhyrWindow):
# Add solver to hash computation data
self._hash_data.append(self._solver)
self._hash_data.append(self._mage_rep)
self._hash_data.append(self._mode)
self._checker_list = (
self._study.checkers() +
@@ -169,7 +172,7 @@ class CheckListWindowAdisTS(PamhyrWindow):
if not errors:
self.find(QPushButton, "pushButton_ok").setEnabled(True)
if self._autorun:
self._parent.solver_log_adists(self._solver, self._mage_rep)
self.open_solver_log()
self.end()
self.update_statusbar()
@@ -196,5 +199,11 @@ class CheckListWindowAdisTS(PamhyrWindow):
self.end()
def accept(self):
self._parent.solver_log_adists(self._solver, self._mage_rep)
self.open_solver_log()
# self.end()
def open_solver_log(self):
if self._mode == "adistt":
self._parent.solver_log_adistt(self._solver, self._mage_rep)
else:
self._parent.solver_log_adists(self._solver, self._mage_rep)
+20
View File
@@ -43,6 +43,7 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QComboBox, QUndoStack, QShortcut, QHeaderView,
QCheckBox,
)
from Modules import Modules
@@ -119,11 +120,17 @@ class ConfigureWindow(PamhyrDialog):
editable_headers=["name", "comment", "minor", "medium"],
data=self._stricklers,
undo=self._undo_stack,
trad=self._trad_stricklers,
display_manning=self._config.display_manning,
unique_names=True,
)
def setup_data(self):
# Backup
self.set_check_box("checkBox_backup", self._config.backup_enable)
self.set_check_box(
"checkBox_display_manning", self._config.display_manning
)
# self.set_line_edit_text("lineEdit_backup_path",
# self._config.backup_path)
self.set_time_edit("timeEdit_backup_frequence",
@@ -167,6 +174,10 @@ class ConfigureWindow(PamhyrDialog):
for button in buttons:
self.find(QPushButton, button).clicked.connect(buttons[button])
self.find(QCheckBox, "checkBox_display_manning").stateChanged.connect(
self.update_stricklers_display
)
def accept(self):
# Solvers
self._config.solvers = self.solver_table_model._data.copy()
@@ -178,6 +189,9 @@ class ConfigureWindow(PamhyrDialog):
self._config.backup_frequence = self.get_time_edit(
"timeEdit_backup_frequence"
)
self._config.display_manning = self.get_check_box(
"checkBox_display_manning"
)
# self._config.backup_max = self.get_spin_box("spinBox_backup_max")
# Stricklers
@@ -237,6 +251,12 @@ class ConfigureWindow(PamhyrDialog):
# Stricklers
def update_stricklers_display(self):
self._stricklers_table.display_manning = self.get_check_box(
"checkBox_display_manning"
)
self._stricklers_table.layoutChanged.emit()
def index_selected_rows_strikclers(self):
table = self.find(QTableView, f"tableView_stricklers")
return list(
+9 -2
View File
@@ -78,7 +78,7 @@ class ComboBoxDelegate(QItemDelegate):
else:
val = list(
map(
lambda n: n.name, self._data.edges()
lambda n: n.name, self._data.enable_edges()
)
)
@@ -123,9 +123,16 @@ class D90TableModel(PamhyrTableModel):
self._undo.indexChanged.connect(lambda _: self.update())
def _setup_lst(self):
enabled_reaches = {
reach.id for reach in self._river.enable_edges()
}
self._lst = list(
filter(
lambda d90: d90._deleted is False,
lambda d90: (
d90._deleted is False
and (d90.reach in (None, -1)
or d90.reach in enabled_reaches)
),
self._data._data
)
)
+9 -2
View File
@@ -80,7 +80,7 @@ class ComboBoxDelegate(QItemDelegate):
else:
val = list(
map(
lambda n: n.name, self._data.edges()
lambda n: n.name, self._data.enable_edges()
)
)
@@ -125,9 +125,16 @@ class DIFTableModel(PamhyrTableModel):
self._undo.indexChanged.connect(lambda _: self.update())
def _setup_lst(self):
enabled_reaches = {
reach.id for reach in self._river.enable_edges()
}
self._lst = list(
filter(
lambda dif: dif._deleted is False,
lambda dif: (
dif._deleted is False
and (dif.reach in (None, -1)
or dif.reach in enabled_reaches)
),
self._data._data
)
)
+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
+9 -9
View File
@@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (
from View.Ensembles.UndoCommand import (
SetNameCommand, SetTypeCommand, SetDataCommand,
SetFunctionCommand,
SetDistributionCommand,
AddCommand, DelCommand,
)
@@ -96,13 +96,13 @@ class ComboBoxDelegate(QItemDelegate):
)
)
)
elif self._mode == "function":
elif self._mode == "distribution":
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda f: f.name,
self._study._ens_functions.lst
self._study._ens_distributions.lst
)
)
)
@@ -133,11 +133,11 @@ class ComboBoxDelegate(QItemDelegate):
),
None
)
elif self._mode == "function":
elif self._mode == "distribution":
value = next(
filter(
lambda f: f.name == text,
self._study._ens_functions.lst
self._study._ens_distributions.lst
),
None
)
@@ -182,8 +182,8 @@ class EnsembleTableModel(PamhyrTableModel):
if value is None:
return self._trad["not_defined"]
return str(value)
elif self._headers[column] == "function":
value = self._lst.get(row).function
elif self._headers[column] == "distribution":
value = self._lst.get(row).distribution
if value is None:
return self._trad["not_defined"]
return value.name
@@ -222,9 +222,9 @@ class EnsembleTableModel(PamhyrTableModel):
self._lst, row, value
)
)
elif self._headers[column] == "function":
elif self._headers[column] == "distribution":
self._undo.push(
SetFunctionCommand(
SetDistributionCommand(
self._lst, row, value
)
)
+2 -2
View File
@@ -1,4 +1,4 @@
# translate.py -- Pamhyr
# Translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
@@ -45,6 +45,6 @@ class EnsemblesTranslate(MainTranslate):
"name": self._dict["name"],
"type": _translate("Ensembles", "Type"),
"target_data": _translate("Ensembles", "Target data"),
"function": _translate("Ensembles", "Function"),
"distribution": _translate("Ensembles", "Distribution"),
"parameters": _translate("Ensembles", "Parameters"),
}
+53 -6
View File
@@ -77,20 +77,24 @@ class SetDataCommand(QUndoCommand):
self._ensembles.get(self._index).target_data = self._new
class SetFunctionCommand(QUndoCommand):
def __init__(self, ensembles, index, function):
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).function
self._new = function
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).function = self._old
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).function = self._new
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):
@@ -111,6 +115,34 @@ class AddCommand(QUndoCommand):
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)
@@ -129,3 +161,18 @@ class DelCommand(QUndoCommand):
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)
+82 -36
View File
@@ -17,6 +17,7 @@
# -*- coding: utf-8 -*-
import logging
import json
from tools import trace, timer, logger_exception
@@ -42,10 +43,12 @@ from PyQt5.QtWidgets import (
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()
@@ -94,10 +97,10 @@ class EnsemblesWindow(PamhyrWindow):
parent=self
)
self._delegate_function = ComboBoxDelegate(
self._delegate_distribution = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="function",
mode="distribution",
trad=self._trad,
parent=self
)
@@ -105,7 +108,7 @@ class EnsemblesWindow(PamhyrWindow):
return {
"type": self._delegate_data_type,
"target_data": self._delegate_stricklers,
"function": self._delegate_function,
"distribution": self._delegate_distribution,
}
def setup_table(self):
@@ -114,7 +117,7 @@ class EnsemblesWindow(PamhyrWindow):
delegates = self.setup_table_delegate()
editable_headers = [
"name", "type", "function", "target_data"
"name",
]
table = self.find(QTableView, f"tableView")
@@ -138,6 +141,12 @@ class EnsemblesWindow(PamhyrWindow):
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()\
@@ -184,51 +193,88 @@ class EnsemblesWindow(PamhyrWindow):
for row in rows:
try:
dlg = MinMaxDialog(
ensemble=self._ensembles.get(row),
trad=self._trad,
ens = self._ensembles.get(row)
dlg = EnsembleEditDialog(
study=self._study,
ensemble=ens,
parent=self
)
if dlg.exec():
self._ensembles.get(row).params = dlg._minmax
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))
class MinMaxDialog(PamhyrDialog):
_pamhyr_ui = "MinMaxDialog"
_pamhyr_name = "Minmax"
data = {"ensembles": ens}
def __init__(self, ensemble=None,
trad=None, parent=None):
super(MinMaxDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
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"]]
)
self._ensemble = ensemble
def import_from_json(self):
def aux(filename):
with open(filename[0], 'r') as f:
return json.load(f)
self._init_default_values()
data = self.file_dialog(
select_file=None,
callback=aux,
default_suffix='.json',
file_filter=[self._trad["file_json"]]
)
def _init_default_values(self):
self._minmax = self._ensemble.params
if len(self._minmax) > 0:
self.set_double_spin_box("doubleSpinBox_min", self._minmax[0])
self.set_double_spin_box("doubleSpinBox_max", self._minmax[1])
dtarget = dict(
map(
lambda s: (s.pamhyr_id, s),
self._study.river.stricklers.stricklers
)
)
@property
def minmax(self):
return self._minmax
ddistr = dict(
map(
lambda d: (d.name, d),
self._study._ens_distributions.lst
)
)
def accept(self):
self._minmax = [
self.get_double_spin_box("doubleSpinBox_min"),
self.get_double_spin_box("doubleSpinBox_max")
]
super().accept()
def update(d, values):
d.update(values)
return d
def reject(self):
self.close()
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()
+13 -4
View File
@@ -20,6 +20,7 @@ import logging
from tools import timer, flatten
from View.Tools.PamhyrPlot import PamhyrPlot
from View.Tools.CoefficientDisplay import display_coefficient
from PyQt5.QtCore import (
QCoreApplication
@@ -32,7 +33,7 @@ _translate = QCoreApplication.translate
class PlotStricklers(PamhyrPlot):
def __init__(self, data=None, canvas=None, trad=None,
toolbar=None, parent=None):
toolbar=None, display_manning=True, parent=None):
super(PlotStricklers, self).__init__(
canvas=canvas,
trad=trad,
@@ -42,7 +43,11 @@ class PlotStricklers(PamhyrPlot):
)
self.label_x = self._trad["rk"]
self.label_y = self._trad["strickler_plot"]
self._display_manning = display_manning
self.label_y = (
self._trad["manning_plot"] if display_manning
else self._trad["strickler_plot"]
)
self.line_rk_elevation = [None, None]
@@ -113,8 +118,12 @@ class PlotStricklers(PamhyrPlot):
)
)
coef_minor = list(map(lambda s: s.minor, coef))
coef_medium = list(map(lambda s: s.medium, coef))
coef_minor = [
display_coefficient(s.minor, self._display_manning) for s in coef
]
coef_medium = [
display_coefficient(s.medium, self._display_manning) for s in coef
]
self.line_rk_elevation[0] = self.canvas.axes.plot(
rk, coef_minor,
+91 -16
View File
@@ -42,6 +42,7 @@ from View.Frictions.UndoCommand import (
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.Tools.CoefficientDisplay import display_strickler
from View.Frictions.translate import *
@@ -51,30 +52,74 @@ _translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, study=None,
mode="stricklers", trad=None, parent=None):
def __init__(self, data=None, study=None, application_stricklers=None,
mode="stricklers", trad=None, display_manning=True,
parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._study = study
self._application_stricklers = application_stricklers or []
self._trad = trad
self._mode = mode
self._display_manning = display_manning
@staticmethod
def same_strickler(first, second):
return (
first.name == second.name
and first.comment == second.comment
and first.minor == second.minor
and first.medium == second.medium
)
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
if self._mode == "stricklers":
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda s: str(s),
self._study.river.stricklers.stricklers
self.editor.addItem(self._trad["not_defined"], None)
study_stricklers = self._study.river.stricklers.stricklers
for strickler in study_stricklers:
self.editor.addItem(
display_strickler(strickler, self._display_manning),
strickler
)
application_stricklers = [
application_strickler
for application_strickler in self._application_stricklers
if not any(
self.same_strickler(
application_strickler, study_strickler
)
for study_strickler in study_stricklers
)
]
if application_stricklers:
self.editor.insertSeparator(self.editor.count())
for strickler in application_stricklers:
self.editor.addItem(
display_strickler(
strickler, self._display_manning
),
strickler
)
elif self._mode == "rk":
if self._data is None:
self.editor.addItems(
["-"]
)
else:
self.editor.addItems(
list(
map(
lambda p: p.display_name(),
self._data.reach.profiles
)
)
)
)
self.editor.setCurrentText(index.data(Qt.DisplayRole))
self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
return self.editor
def setEditorData(self, editor, index):
@@ -82,8 +127,9 @@ class ComboBoxDelegate(QItemDelegate):
self.editor.currentTextChanged.connect(self.currentItemChanged)
def setModelData(self, editor, model, index):
text = str(editor.currentText())
model.setData(index, text)
value = editor.currentData() if self._mode == "stricklers" else \
str(editor.currentText())
model.setData(index, value)
editor.close()
editor.deleteLater()
@@ -100,10 +146,33 @@ class ComboBoxDelegate(QItemDelegate):
class FrictionTableModel(PamhyrTableModel):
def __init__(self, *args, display_manning=False, **kwargs):
self.display_manning = display_manning
super(FrictionTableModel, self).__init__(*args, **kwargs)
def _setup_lst(self):
self._lst = self._data.frictions
self._study = self._opt_data
def study_strickler(self, strickler):
if strickler is None:
return None
study_stricklers = self._study.river.stricklers
if any(s is strickler for s in study_stricklers.stricklers):
return strickler
for current in study_stricklers.stricklers:
if ComboBoxDelegate.same_strickler(current, strickler):
return current
copied = study_stricklers.new(len(study_stricklers))
copied.name = strickler.name
copied.comment = strickler.comment
copied.minor = strickler.minor
copied.medium = strickler.medium
return copied
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
@@ -121,12 +190,12 @@ class FrictionTableModel(PamhyrTableModel):
value = self._lst.get(row).begin_strickler
if value is None:
return self._trad["not_defined"]
return str(value)
return display_strickler(value, self.display_manning)
elif self._headers[column] == "end_strickler":
value = self._lst.get(row).end_strickler
if value is None:
return self._trad["not_defined"]
return str(value)
return display_strickler(value, self.display_manning)
return QVariant()
@@ -160,15 +229,21 @@ class FrictionTableModel(PamhyrTableModel):
)
)
elif self._headers[column] == "begin_strickler":
strickler = value if hasattr(value, "minor") else \
self._study.river.strickler(value)
strickler = self.study_strickler(strickler)
self._undo.push(
SetBeginStricklerCommand(
self._lst, row, self._study.river.strickler(value)
self._lst, row, strickler
)
)
elif self._headers[column] == "end_strickler":
strickler = value if hasattr(value, "minor") else \
self._study.river.strickler(value)
strickler = self.study_strickler(strickler)
self._undo.push(
SetEndStricklerCommand(
self._lst, row, self._study.river.strickler(value)
self._lst, row, strickler
)
)
except Exception as e:
+65 -8
View File
@@ -67,6 +67,9 @@ class FrictionsWindow(PamhyrWindow):
def __init__(self, reach=None, study=None, config=None, parent=None):
trad = FrictionsTranslate()
if config is None and parent is not None:
config = getattr(parent, "conf", None)
if reach is not None:
self._reach = reach
else:
@@ -101,8 +104,20 @@ class FrictionsWindow(PamhyrWindow):
self._delegate_stricklers = ComboBoxDelegate(
data=self._reach,
study=self._study,
application_stricklers=(
self._config.stricklers.stricklers
if self._config is not None else []
),
mode="stricklers",
trad=self._trad,
display_manning=getattr(self._config, "display_manning", False),
parent=self
)
self._delegate_rk = ComboBoxDelegate(
data=self._reach,
study=self._study,
mode="rk",
trad=self._trad,
parent=self
)
@@ -115,18 +130,27 @@ class FrictionsWindow(PamhyrWindow):
editable_headers = []
table = self.find(QTableView, f"tableView")
table_headers = self._trad.get_dict("table_headers").copy()
table_headers["begin_strickler"] = (
self._trad["manning"]
if getattr(self._config, "display_manning", False)
else self._trad["strickler"]
)
self._table = FrictionTableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
table_headers=table_headers,
editable_headers=editable_headers,
delegates={
"begin_rk": self._delegate_rk,
"end_rk": self._delegate_rk,
"begin_strickler": self._delegate_stricklers,
"end_strickler": self._delegate_stricklers,
# "end_strickler": self._delegate_stricklers,
},
data=self._reach,
trad=self._trad,
undo=self._undo_stack,
opt_data=self._study
opt_data=self._study,
display_manning=getattr(self._config, "display_manning", False)
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
@@ -158,7 +182,8 @@ class FrictionsWindow(PamhyrWindow):
canvas=self.canvas_2,
data=self._reach,
trad=self._trad,
toolbar=None
toolbar=None,
display_manning=getattr(self._config, "display_manning", False)
)
self.plot_2.draw()
@@ -198,9 +223,41 @@ class FrictionsWindow(PamhyrWindow):
# self._propagate_update(key=Modules.FRICTION)
def _propagated_update(self, key=Modules(0)):
if Modules.GEOMETRY not in key:
return
if Modules.CONFIG in key:
self.update_config()
if Modules.GEOMETRY in key:
self.update_plot()
for _, window in self.sub_win_list:
window._propagated_update(key=key)
def update_config(self):
display_manning = getattr(
self._config, "display_manning", False
)
self._delegate_stricklers._application_stricklers = (
self._config.stricklers.stricklers
if self._config is not None else []
)
self._delegate_stricklers._display_manning = display_manning
self._table.display_manning = display_manning
self._table._table_headers["begin_strickler"] = (
self._trad["manning"] if display_manning
else self._trad["strickler"]
)
self._table.headerDataChanged.emit(
Qt.Horizontal, 0, self._table.columnCount() - 1
)
self._table.layoutChanged.emit()
self.plot_2._display_manning = display_manning
self.plot_2.label_y = (
self._trad["manning_plot"] if display_manning
else self._trad["strickler_plot"]
)
self.update_plot()
def _set_current_reach(self):
@@ -265,13 +322,13 @@ class FrictionsWindow(PamhyrWindow):
def edit_stricklers(self):
if self.sub_window_exists(
StricklersWindow,
data=[self._study, self.parent.conf]
data=[self._study, self._config]
):
return
strick = StricklersWindow(
study=self._study,
config=self.parent.conf,
config=self._config,
parent=self
)
strick.show()
Loaded 100 of 229 files, more files were not shown because too many files have changed in this diff. Show more