Compare commits

...
587 Commits
Author SHA1 Message Date
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
parouby 58e3da8942 Ensemble: Add base for RKC ensemble plot. 2026-09-04 17:51:52 +02:00
Dylan Jeannin 2d7409debb Export csv results: fix export flowrate as a function of time, on a study without sediments transport 2026-09-04 16:45:59 +02:00
Dylan Jeannin beece68498 SolverParameters: new combobox instead of index in the value field for critical shields, shields correction and solid capacity 2026-09-04 11:40:04 +02:00
parouby b7c3793a66 Geometry: Point: Fix index creation. 2026-09-04 11:31:23 +02:00
parouby 92fb653676 Geometry: Point: Try to create index at each update. 2026-09-04 11:30:30 +02:00
parouby 2498466a51 Ensemble: Results: Fix runs table. 2026-09-04 10:31:04 +02:00
parouby ef36eba039 Ensemble: Results: Add method to open ensemble results window. 2026-09-04 10:11:46 +02:00
Dylan Jeannin d8e9d786d9 LateralContributionsAdisTS: correctly highlight the profiles in the interval + refacto function name 2026-09-03 16:29:32 +02:00
parouby c40e28ed0f Ensemble: Results: Add ui file for results window. 2026-09-03 16:21:53 +02:00
parouby b21846a513 Ensemble: Results: Add base of 'ResultsEnsembleWindow' obj. 2026-09-03 16:17:49 +02:00
Dylan Jeannin 7ef305e724 Pollutants: ref to pol_name in the title of the lateral_contribution_pollutant 2026-09-03 13:00:53 +02:00
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
parouby 3532f75c2b Solver: Mage: Minor change. 2026-09-02 16:21:31 +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
parouby 6c59919972 Pep8: Fix. 2026-09-01 11:36:35 +02:00
parouby 77d56b8bec Ensemble: Keep results nim, mean and max for each run. 2026-09-01 11:34:37 +02:00
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
parouby 7ef7bc13dc Ensemble: Continue to adapt results. 2026-08-28 16:15:59 +02:00
parouby a1d85274b9 Ensemble: Put ensemble results data into results obj. 2026-08-28 11:10:29 +02:00
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
parouby e7b9bc92a2 Ensemble: Run: Log variation. 2026-08-27 10:10:08 +02:00
parouby 7d8e414fe2 Ensemble: Fix missing requirements 'psutil'. 2026-08-27 09:48:12 +02:00
parouby 476bcc9c55 Ensemble: Made it compatible with other solver than mage8. 2026-08-27 09:44:03 +02:00
parouby ae006f52f4 Ensemble: Table: Fix double assignation for data type and target. 2026-08-26 16:04:58 +02:00
parouby 3583a93ae0 Ensemble: Set ensemble as read only study (except for ensemble). 2026-08-25 17:07:34 +02:00
parouby 212a00a43c Ensemble: Fix missing file. 2026-08-25 14:42:41 +02:00
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
parouby e5dbba362a Debug: Add memory usage to debug timer window. 2026-08-21 16:55:50 +02:00
parouby bdc09addfa Ensemble: Fix results reading. 2026-08-21 16:55:17 +02:00
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
parouby 51b3987a4a Mage: Ensemble: Add bin reading method. 2026-08-21 15:28:17 +02:00
parouby e7290fa81b Ensemble: Fix ensemble study meta data save. 2026-08-21 15:27:17 +02:00
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
parouby b5432498c6 Ensemble: Add sequential runs with RUG variation into mage export. 2026-08-20 16:00:40 +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
parouby 807592eab2 Ensemble: Rename 'range' to 'params'. 2026-08-14 16:33:41 +02:00
parouby 394c0779fc Ensemble: Add min max dialog for range. 2026-08-14 16:30:27 +02:00
parouby 5b3f777100 Ensemble: Minor change and add target data to table. 2026-08-14 09:27:39 +02:00
parouby 8b36905842 Ensemble: Fix some view/model interations. 2026-08-14 09:27:36 +02:00
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
parouby b203e8e062 Ensemble: Fix editing ensemble list. 2026-08-12 16:46:13 +02:00
parouby 4d4df1bde2 Ensemble: Function: Randomize Uniform sample. 2026-08-12 15:19:12 +02:00
parouby 876780a349 Ensembles: Open Ensemble windows from scenarios. 2026-08-12 14:50:58 +02:00
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
parouby 3b83377f3c Ensemble: Draw ensemble scenario as rect and minor fix. 2026-08-07 16:28:13 +02:00
parouby e64c45b621 Ensemble: Create new ensemble scenario. 2026-08-07 15:18:22 +02:00
parouby 70dae9ab1b Ensemble: Prepare view with add base classes. 2026-08-06 15:33:53 +02:00
parouby 0bb3c5bc65 Ensemble: Minor change to prepare view integration. 2026-07-31 16:19:13 +02:00
parouby 54f5c7c548 Ensemble: Switch data pid to data ref, and minor change. 2026-07-30 11:14:33 +02:00
parouby 56217186ff Ensemble: Fix save/load methode. 2026-07-29 16:31:13 +02:00
parouby 82fb2bcc48 Ensemble: Change function to id and name, minor change and fix. 2026-07-29 15:19:36 +02:00
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
parouby 6dc42b637a Ensemble: Function: Some minor fix and add it to study. 2026-07-24 11:45:39 +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
parouby e6d12713e3 Ensemble: Minor change. 2026-07-24 10:04:15 +02:00
parouby 9456cf32fc Ensemble: Function: Add base classes. 2026-07-24 10:02:25 +02:00
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
parouby 9b1fb4e7d2 Ensemble: Add ensemble base classes. 2026-07-23 09:51:17 +02:00
Dylan Jeannin 78eed7af13 SolverParams: quickfix for solverparams copy on scenario creation 2026-07-22 15:40:32 +02:00
Dylan Jeannin 6f5743a030 Merge dictionary 2026-07-22 13:31:59 +02:00
Dylan Jeannin d027946fb2 OutputRK: delete all references to this useless feature 2026-07-22 12:54:50 +02:00
Dylan Jeannin 37ea83d42a Lang: new words in french dictionnary 2026-07-22 11:26:30 +02:00
Dylan Jeannin ae39f33131 PEP8 2026-07-21 17:18:59 +02:00
Dylan Jeannin 291f80e904 Results AdisTT: zoom not reset when changing timestamp 2026-07-21 17:16:35 +02:00
Dylan Jeannin 61baaeeffb Scenario structure: adapting better structure for results with scenario mode + default data for weather parameters 2026-07-21 16:58:30 +02:00
Dylan Jeannin 68aabb343d WeatherParameters: informations for default values + no timeseries for ALB, SF and CLD 2026-07-21 15:54:06 +02:00
Dylan Jeannin f2d6e07931 WeatherParameters: better window names to be more clear for user 2026-07-21 15:26:38 +02:00
Dylan Jeannin 123259c2ef Duplicate window: avoid opening duplicate windows for boundary and initial conditions ADISTT 2026-07-21 15:06:44 +02:00
Dylan Jeannin 816d7663b5 AdisTT: new button on interface to open AdisTT results from file 2026-07-21 14:57:30 +02:00
Dylan Jeannin 9b9202c739 AdisTT: new results window, adapted from adists results 2026-07-21 14:00:59 +02:00
Dylan Jeannin 57f82415bd AdisTT: Avoid crash when pasting something unexpected in time series 2026-07-21 11:09:46 +02:00
Dylan Jeannin 36c93a0c93 AdisTT: cleaning code, delete useless functions 2026-07-20 16:22:49 +02:00
Dylan Jeannin 3cd1dbab49 AdisTT: default parameters update to fit better 2026-07-20 16:10:39 +02:00
Dylan Jeannin 4e3da95b1d Plot: Update plots after changing each data in tab 2026-07-20 15:51:05 +02:00
Dylan Jeannin 0d0c4f913e Merge branch 'dev_dylan' into temperature_test 2026-07-20 15:16:41 +02:00
Dylan Jeannin 600f86aeb4 Friction: combobox in tab for rk 2026-07-20 14:48:19 +02:00
Dylan Jeannin c9bb222bff WeatherParameters: Debug crash pamhyr when select a reach without geometry 2026-07-20 13:47:19 +02:00
Dylan Jeannin 456559ffe2 AdisTS & AdisTT: deleted data no more exported in solver files 2026-07-20 12:34:49 +02:00
Dylan Jeannin 6d58c09104 AdisTT: short correction on paste function in edit windows for WeatherParam 2026-07-17 17:47:33 +02:00
Dylan Jeannin bc87d6add1 AdisTT: corrections on writing TEM files 2026-07-17 17:21:52 +02:00
Dylan Jeannin 3fe49b70d0 Merge branch 'monopk-ic-adistt' into temperature_test 2026-07-17 17:04:37 +02:00
Dylan Jeannin 5287f30636 AdisTT: Handling cases where parameters do not have time series 2026-07-17 15:05:21 +02:00
Dylan Jeannin 67433ae325 AdisTS: delete duplicate function 2026-07-17 13:51:18 +02:00
Dylan Jeannin d27ce9d1d8 AdisTT: rewriting reach names in parser + copy the net folder from mage 2026-07-17 13:45:40 +02:00
Dylan Jeannin c8ea9bd774 Merge branch 'temperature_test' into monopk-ic-adistt 2026-07-17 10:12:30 +02:00
Dylan Jeannin 885c8ea502 Merge branch 'master' into temperature_test 2026-07-17 10:09:52 +02:00
Dylan Jeannin c8b32f9877 Merge branch 'master' into temperature_test 2026-07-17 10:05:42 +02:00
Dylan Jeannin cf61fd8d09 IC_temp: temperature set on a single RK rather than a segment 2026-07-17 10:03:46 +02:00
Dylan Jeannin cac20d4f24 AdisTT: user-friendly fixes on interface 2026-07-17 09:31:03 +02:00
Dylan Jeannin 965443a8f9 AdisTT: Managing tab of time series when it's disabled 2026-07-16 16:34:37 +02:00
Dylan Jeannin 02be084faf AdisTT: Fake 0.0 in files to match AdisTS pollutants description 2026-07-16 12:56:46 +02:00
Dylan Jeannin bb2e83b6ce AdisTT: Stable version of files writer for adisTT solver 2026-07-16 10:44:01 +02:00
parouby c09c62f077 Merge: Results reading optimisation. 2026-07-10 10:18:58 +02:00
parouby 7a2e93a995 Results: Custom plot: Minor change. 2026-07-10 09:16:05 +02:00
JeaDylan 4a94c946d1 WeatherParameters: add name of parameter in spec tab 2026-07-09 18:40:27 +02:00
parouby 5327c29641 Results: Custom plot: Use 'zfd' np table. 2026-07-09 18:05:10 +02:00
parouby be865102a8 Results: Put 'zfd' data into np table. 2026-07-09 16:53:48 +02:00
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
parouby 37f1b43b93 Debug: Fix pep8. 2026-07-09 10:16:23 +02:00
parouby 16f0775783 Pamhyr: Update all copyright. 2026-07-09 10:13:06 +02:00
parouby f2c4285b4c Debug: Minor change. 2026-07-09 09:03:11 +02:00
parouby ee95f0e89e Debug: Add csv export method. 2026-07-08 17:55:31 +02:00
parouby 1306977954 Debug: Add Timer window. 2026-07-08 16:38:41 +02:00
parouby 476fa81bc2 Results: Plot{AC,RKC}: Fix max elevation and minor change. 2026-07-07 21:56:19 +02:00
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
parouby e8f1f0ac43 Results: Add TableData pamhyr object for np table. 2026-07-03 17:03:33 +02:00
Dylan Jeannin cfbc2d96a1 Mage: Fix parsing for .LAT files 2026-07-03 13:07:10 +02:00
parouby 1adf24ce77 Geometry: ProfileXYZ: Minor optimisation of computation. 2026-07-02 15:04:33 +02:00
parouby a61d130baa Solver: Mage: Comment setter Z, Q and V into profile and bufferize and fix indent. 2026-07-02 11:43:00 +02:00
parouby 0a1760cee6 Results: CustomPlot: Continue to switch to np table. 2026-07-02 11:00:51 +02:00
parouby 3e094af325 Results: CustomPlot: Use np table. 2026-07-01 17:54:34 +02:00
parouby 762e4d70a0 Results: Exports: Use np.{min,max}. 2026-07-01 17:54:08 +02:00
parouby 6c4f863b96 Results: Export: Use np table. 2026-07-01 15:39:52 +02:00
parouby b87f82f74d Results: PlotRKC: Use table to get results table. 2026-06-30 16:57:42 +02:00
parouby ffad53a331 Results: Table: Use results tables. 2026-06-30 15:30:44 +02:00
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
parouby 662a218e41 Results: Reach: Keep profiles global min and max. 2026-06-26 16:51:58 +02:00
parouby 0501ae9bec Solver: Mage: read_bin: Use np table for velocity computation. 2026-06-26 16:20:32 +02:00
parouby 7fd38eec00 Results: Table: Minor change. 2026-06-26 14:54:55 +02:00
parouby 4bab4580ad Results: PlotXY: Use table Z to display max elevation. 2026-06-26 14:41:24 +02:00
parouby 21d2e6ad20 Results: PlotAC: Use Z table. 2026-06-26 14:23:00 +02:00
parouby d0a8f369ef Results: PlotH: Use np table. 2026-06-26 14:05:29 +02:00
parouby fef2e1a40c Results: PlotXY: Minor change. 2026-06-26 10:51:36 +02:00
parouby 20c2cb7ea4 Merge branch 'master' into opt-result 2026-06-26 09:29:39 +02:00
Dylan Jeannin 333a8a0970 Temperature: New tab in UI 2026-06-25 17:13:02 +02:00
parouby 64a26e987e ci: Fix again. 2026-06-25 15:48:41 +02:00
parouby eb233847de ci: Fix pipeline for release. 2026-06-25 15:42:46 +02:00
parouby 7bb810d427 Pollutants: Fix 'enabled' column update version. 2026-06-25 15:00:41 +02:00
Dylan Jeannin e49724072b Merge branch 'dev_dylan' into scenarios 2026-06-25 14:41:24 +02:00
Dylan Jeannin 3664ba93e9 Results: Fix open mage results from AdisTS solver 2026-06-25 14:39:45 +02:00
Dylan Jeannin a9903e987b AdisTS: Resolve bug with node ids not matching in config files 2026-06-25 14:05:04 +02:00
Dylan Jeannin f9a88a211c Fix pep8 2026-06-22 17:10:57 +02:00
Dylan Jeannin 32535af3e6 UpdateRK: avoid crash when updating RK with missing points on a section, warning message instead 2026-06-22 17:04:39 +02:00
Dylan Jeannin fa8948c1b6 Meshing: Debug error caused by index out of range when adding an empty transect on a reach 2026-06-22 15:18:08 +02:00
Dylan Jeannin 2d5455c09d Meshing: Debug crashing case when divide by 0 2026-06-22 14:16:33 +02:00
Dylan Jeannin 83680bbe57 Pollutant: quickfix, back to previous version 2026-06-22 12:12:02 +02:00
Dylan Jeannin 1d1a00a0ff Pollutants: merge conflicts 2026-06-22 10:42:29 +02:00
Dylan Jeannin ffd95b9fdf Pollutants: merge conflicts 2026-06-22 10:42:07 +02:00
parouby ecb2544cab Results: Keep timestamps index table. 2026-06-19 15:25:40 +02:00
parouby 9c66021b53 Results: Give access profiles to np table and keep profile global id. 2026-06-19 14:54:26 +02:00
parouby 8255fc3c1b River: Add some timer. 2026-06-19 09:23:42 +02:00
parouby 32e7413aad Solver: Mage: read_bin: Put velocity into np table. 2026-06-18 15:45:11 +02:00
parouby d87db5fd48 Solver: Mage: read_bin: Optimize iprofiles. 2026-06-18 15:02:34 +02:00
parouby 388368adad Solver: Mage: Add data np table to results. 2026-06-18 14:29:50 +02:00
parouby 4d014d5a1d Results: Minor change. 2026-06-18 10:11:17 +02:00
parouby 181a6c226f Geometry: ProfileXYZ: Add 'get_all_water_limits_ac' cache. 2026-06-18 10:09:51 +02:00
parouby 1975ed7eaa Profile: Create cache for 'get_water_limits' results. 2026-06-17 17:41:06 +02:00
parouby 565a7530fb Results: Minor results reading optimisation with async computation. 2026-06-17 17:04:58 +02:00
parouby 32cbc6bc3e Results: Fix additional geotiff display. 2026-06-16 17:28:55 +02:00
parouby 7d7a05c04c Results: Minor change. 2026-06-16 16:28:46 +02:00
parouby 6ac268b668 Results: Add data: Move update to plot class. 2026-06-16 16:24:05 +02:00
parouby 19c72892a8 Results: Add data: Hydrogram: Move drawing to plot. 2026-06-16 16:13:10 +02:00
parouby 9717968e08 Mage: LC: Fix export. 2026-06-10 14:26:05 +02:00
parouby e19c3229bd Solver: Mage: HS: Minor change. 2026-06-05 17:01:22 +02:00
parouby b3c6f1031b Solver: Mage: Minor change. 2026-06-05 16:35:57 +02:00
parouby c2d93b4a4a MainWindow: Finally connect action for the 'Quit' menu action. 2026-06-05 15:56:25 +02:00
parouby 76430322dd Results: Minor change. 2026-06-05 14:59:48 +02:00
parouby 31ae0ab8e0 Study: Add minor comment. 2026-06-05 10:54:51 +02:00
parouby 8182d434ec GeoTIFF: Fix crashes at 'cannot remove artist'. 2026-06-05 10:01:53 +02:00
parouby da78d0cdb1 AdisTS: Fix mage result workdir and add scenario into paths. 2026-06-04 15:21:03 +02:00
parouby 850db144ac WaitingDialog: Fix windows size. 2026-06-04 14:53:43 +02:00
parouby 0f6709075f Scenarios: Merge branch 'master' into scenarios. 2026-06-04 14:03:49 +02:00
parouby e86b84b0b9 AdisTS: Results: Fix db save. 2026-06-03 16:00:12 +02:00
parouby e08d212986 AdisTS: D90: Fix reach pid update. 2026-06-03 15:34:52 +02:00
parouby 7b88d955e5 Geometry: Update RK refactoring and fix modified flag. 2026-06-02 16:46:30 +02:00
parouby d1c34fb437 Pamhyr: Fix PEP8. 2026-06-02 15:40:51 +02:00
parouby 611f9cdfa3 Trad: Minor change. 2026-06-02 15:16:50 +02:00
parouby bd2fc22e96 Results: Fix non valid results save. 2026-06-02 15:11:07 +02:00
parouby efa16837d8 AdisTS: Minor change. 2026-05-28 17:09:02 +02:00
parouby db7aad26fd Geometry: Point: Optimize loading with indexing and update db to 0.2.5. 2026-05-28 17:09:02 +02:00
parouby 229b3da2d1 AdistTS: Fix update with table exists tests. 2026-05-28 17:09:02 +02:00
Dylan Jeannin 48d6728e1e Enable pollutant: fix the list to browse to enable a pol 2026-05-28 15:11:06 +02:00
Dylan Jeannin c51c06e6ec DB version: Upgrade to add enabled in pollutants db 2026-05-28 15:02:40 +02:00
Dylan Jeannin a425c9a6ac BoundaryConditions: delete setter/getter duplicated for d50 and sigma 2026-05-28 15:01:36 +02:00
Dylan Jeannin e79ae8ebf5 Translation: new version of .ts file, with type of pollutants added 2026-05-28 13:01:48 +02:00
Dylan Jeannin e3020d15ba Merge branch 'scenarios' into dev_dylan 2026-05-28 11:56:00 +02:00
Dylan Jeannin 83e0ecbdc7 PEP8 fix 2026-05-28 11:54:44 +02:00
Dylan Jeannin 3087023caa Pollutants: Add translation for type of pol 2026-05-28 11:52:43 +02:00
parouby 7e080767da AdistTS: d90: Fix update to 0.2.*. 2026-05-28 10:27:05 +02:00
parouby 44c3a91d72 AdisTS: Fix db version update (again) for 0.0.11 2026-05-28 10:27:05 +02:00
Dylan Jeannin 26dc7d8761 Merge branch 'scenarios' of https://gitlab.com/pamhyr/pamhyr2 into scenarios 2026-05-27 09:35:46 +02:00
Dylan Jeannin e573fa4197 Pollutants: specific way to check if we modified the data in the type combobox 2026-05-27 09:35:10 +02:00
parouby 7aa0cf4361 AdisTS: Fix db update. 2026-05-26 18:00:05 +02:00
parouby c3faf45f75 SolverLogFileWindow: Add exeption to fix crash for #48. 2026-05-26 16:36:27 +02:00
Dylan Jeannin fb8b5aa77f Translation: back to old fr.ts version which broke translation* 2026-05-26 14:56:31 +02:00
Dylan Jeannin 8183a284af Merge branch 'pollutant_type_combobox' into dev_dylan 2026-05-26 12:27:44 +02:00
Dylan Jeannin 09997a0074 Tables: Skip table undo commands when data is unchanged (avoid data duplicates in db) 2026-05-26 12:27:41 +02:00
Dylan Jeannin c44597dfbd Merge branch 'dev_dylan' into scenarios 2026-05-26 11:15:32 +02:00
Dylan Jeannin b1632eaea5 Fix PEP8 2026-05-26 11:14:57 +02:00
Dylan Jeannin a2461af084 Scenario: avoid marking loaded scenario data as modified, that created unwanted copies in database 2026-05-26 11:12:36 +02:00
parouby 5c366839f3 Results: Fix db save where results do not correspond to scenario. 2026-05-26 10:28:41 +02:00
parouby 311df46a2c Friction: Fix save and delete method. 2026-05-26 10:28:41 +02:00
Dylan Jeannin a235f61044 PEP8 fixes 2026-05-26 08:15:04 +02:00
Dylan Jeannin 0b80b86d31 Merge branch 'dev_dylan' into scenarios 2026-05-26 08:10:34 +02:00
Dylan Jeannin 0c7ede5f43 Network: Add missing () on method to check if study is editable 2026-05-26 08:06:07 +02:00
Dylan Jeannin 445c1ac7ba Scenario: Add ⛔ symbol on missing windows when we are not in a leaf scenario 2026-05-26 07:42:08 +02:00
Dylan Jeannin c687e05d8f Pollutant types: Add type combobox with readable names 2026-05-22 18:36:28 +02:00
Dylan Jeannin 5fb6fecc24 Scenarios: Set parent to EDIT when deleting current scenario 2026-05-22 17:25:44 +02:00
Dylan Jeannin 3dfbdc328e PEP8 fix 2026-05-22 16:54:35 +02:00
Dylan Jeannin 97d87f2455 Merge branch 'scenarios' of https://gitlab.com/pamhyr/pamhyr2 into scenarios 2026-05-22 16:52:50 +02:00
Dylan Jeannin a78be47e2b AdisTS: Fix the possibility to edit AdisTS windows on a parent scenario 2026-05-22 16:47:02 +02:00
Dylan Jeannin eed2ce4243 Pollutants: Fix setters from undo/redo commands on edition 2026-05-22 15:25:11 +02:00
parouby 78ba7b70b4 AdisTS: OutputRK: Fix save methode. 2026-05-22 14:53:38 +02:00
Dylan Jeannin f86cd4602c Merge branch 'scenarios' into dev_dylan 2026-05-22 14:38:52 +02:00
Dylan Jeannin ee28afac86 HydraulicStructures: Fix hydraulic structures plot with missing input section 2026-05-22 12:57:34 +02:00
Dylan Jeannin 81ff6372e9 LateralSources: Debug of crash on RK selection 2026-05-22 12:45:27 +02:00
parouby 61156f1c3e Scenario: Fix scenario duplication. 2026-05-22 11:51:18 +02:00
parouby e26b170b05 Network: Fix reach selection where the study is read only. 2026-05-22 10:57:49 +02:00
Dylan Jeannin ddbaa0fb94 Pollutants: Fixed pollutant deletion removing the absolute index instead of the relative index 2026-05-22 10:53:07 +02:00
parouby 64d2b2678a Geometry: Profiles: Fix shift methode. 2026-05-22 10:39:10 +02:00
parouby 3a3b5ea2dd PamhyrDB: Minor change. 2026-05-22 10:09:34 +02:00
parouby 7cb3ac8a80 Clean code: Remove some print function call. 2026-05-22 09:42:32 +02:00
parouby 190ca5d171 Scenario: Add some save dialog. 2026-05-22 09:22:35 +02:00
parouby 5d8ec51417 Scenario: Remove posibility to remove non leaf scenario. 2026-05-22 09:21:55 +02:00
parouby d922436ada Scenario: Add Discard option to scenario switching dialog. 2026-05-21 16:26:51 +02:00
parouby 4537a829af WaitingDialog: Try fix deadlock. 2026-05-21 16:26:47 +02:00
parouby 363b9c7132 Scenario: Add HS to memory clean and minor comment change. 2026-05-21 15:07:18 +02:00
parouby f9d83ac68c Scenario: Add memory clean for Pamhyr list, dict and reaches. 2026-05-21 11:17:45 +02:00
Dylan Jeannin 63b5cbaba7 debug pollutants model and save in db 2026-05-20 17:17:51 +02:00
Dylan Jeannin 1989914aef error in the name of a db table 2026-05-20 10:48:37 +02:00
Dylan Jeannin 2c0b155baf error in the name of a db table 2026-05-20 10:40:11 +02:00
Dylan Jeannin 4d20c2457a pep8 fix 2026-05-20 10:19:52 +02:00
Dylan Jeannin 2dd9003721 merge scenario (missing tables) 2026-05-20 10:18:56 +02:00
Dylan Jeannin 94ceb44fd4 debug load study after deleting a node in the child scenario network 2026-05-20 09:51:38 +02:00
Dylan Jeannin ab50d2adf4 little fixes with variable names 2026-05-19 18:00:39 +02:00
Dylan Jeannin 0eb979256f little fixes with variable names 2026-05-19 17:59:30 +02:00
parouby 5ce94a63d3 Scenario: Clean DB add minimal comment. 2026-05-19 17:54:28 +02:00
parouby 42ea50d62e Study: Minor change. 2026-05-19 16:51:57 +02:00
parouby 0df4cf0fd5 Scenario: Clean DB. 2026-05-19 16:13:30 +02:00
Dylan Jeannin ccf3da114b fix duplication of same objects in db with when saving a child scenario 2026-05-19 16:13:21 +02:00
Dylan Jeannin 348417977d add missing tables in drop_all function, called on a scenario deletion 2026-05-19 15:58:37 +02:00
parouby 914a1f8866 Scenario: Add DB clean methode (not tested yet). 2026-05-19 12:22:20 +02:00
Dylan Jeannin cbbbeeac0b correct error that made InitialCondition addition bug 2026-05-19 11:40:03 +02:00
Dylan Jeannin c9ea726bfb correct error that made LateralContrib deletion bug 2026-05-19 11:30:38 +02:00
parouby fa88e05d91 AdisTS: BC: Fix dual sub classes dependencies. 2026-05-19 10:15:23 +02:00
parouby a6915020cb BC: Minor change. 2026-05-19 09:46:46 +02:00
Dylan Jeannin ea2287215e fix pep8 before push 2026-05-18 15:33:36 +02:00
Dylan Jeannin e940dead70 Merge branch 'dev_dylan' into scenarios 2026-05-18 15:04:37 +02:00
Theophile Terraz b6a6d54a22 Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios 2026-05-12 16:17:32 +02:00
Theophile Terraz 11a95cb391 debug 2026-05-12 16:16:56 +02:00
Dylan Jeannin 084c22836b debug undo after deletion of BCA 2026-05-12 13:48:14 +02:00
Dylan Jeannin 726b81bf12 debug undo after add LateralContributionAdisTS 2026-05-12 08:40:35 +02:00
Dylan Jeannin 7c3de520d9 delete old references to pollutants in BoundaryConditionsAdisTS window 2026-05-12 08:39:58 +02:00
Dylan Jeannin b450796df5 stab window InitialCond AdisTS (undo / redo / add / delete / save / load), all should work correctly 2026-05-11 16:14:35 +02:00
Dylan Jeannin b1daadf9f2 rename variables for DIF to keep consistency 2026-05-11 14:59:48 +02:00
Dylan Jeannin c38e3ee446 correct pep8 rules 2026-05-11 14:59:14 +02:00
Dylan Jeannin 606b9919e3 stab window D90 AdisTS (undo / redo / add / delete / save / load), all should work correctly 2026-05-11 14:58:42 +02:00
Dylan Jeannin e3de7e7100 rename variable to keep consistency 2026-05-11 14:07:38 +02:00
Dylan Jeannin 92fae0e13f rename variable to keep consistency 2026-05-11 14:06:07 +02:00
Dylan Jeannin a8b260ad38 debug crash when double clic on PK combobox when no reach is selected (DIF window) 2026-05-11 13:49:54 +02:00
Dylan Jeannin faff877c80 stab window DIF AdisTS (undo / redo / add / delete / save / load), all should work correctly 2026-05-11 12:52:33 +02:00
Theophile Terraz 0a0eeb3c3f debug 2026-05-07 17:01:22 +02:00
parouby cdcbbc2cf4 doc: Remove watermark. 2026-05-06 11:03:56 +02:00
Dylan Jeannin 7565d8ee95 fix pep8 2026-05-05 17:51:49 +02:00
Dylan Jeannin 11e1302e05 fix add/del/undo/redo in edit BoundaryConditionsAdisTS + quick fix in edit LateralContributionsAdisTS 2026-05-05 17:50:45 +02:00
Dylan Jeannin 99cb2883a1 display seconds in LateralContributionAdisTS data (edition window) 2026-05-05 12:34:44 +02:00
Dylan Jeannin e61dede7f8 add Dylan in contributors 2026-05-05 12:33:41 +02:00
Dylan Jeannin cb4dfab482 conflicts resolution before merge 2026-05-05 10:50:42 +02:00
Dylan Jeannin 377c508038 correction pep8 2026-05-05 10:29:13 +02:00
Dylan Jeannin 30e34b6847 debug add function and save/load in db for edit LCAdisTS window 2026-05-05 10:28:30 +02:00
Dylan Jeannin 102702e1df fix delete function for BCAdisTS 2026-05-04 14:52:31 +02:00
Dylan Jeannin c014f0ef34 erase old and useless methods 2026-05-04 14:51:34 +02:00
Dylan Jeannin e615a6b9d4 debug index matching in delete function for LCAdisTS 2026-05-04 13:03:18 +02:00
Dylan Jeannin f1dca727c6 rename edition BCadisTS and LCadisTS windows 2026-05-04 11:11:23 +02:00
Dylan Jeannin 2db1f4c7f7 fix tab elts related to pollutants, and fix add/del functionnality for LateralContributionAdisTS 2026-04-30 15:56:11 +02:00
Dylan Jeannin b3d631943c correction bug with saving data for lateral contributions adisTS 2026-04-29 15:33:00 +02:00
Dylan Jeannin b6895ae99c correction bug with saving data for lateral contributions adisTS 2026-04-29 14:09:34 +02:00
Dylan Jeannin 9f26a7323c correction bug with saving data for lateral contributions adisTS 2026-04-29 14:03:58 +02:00
Dylan Jeannin 2a7e322ee5 fix name of window InitCondAdisTS to mention pollutant name 2026-04-28 16:44:33 +02:00
Dylan Jeannin 13a44264ac fix pep8 rules 2026-04-28 16:05:40 +02:00
Dylan Jeannin 6165719829 warning popup when export format for geometry reach is not .st 2026-04-28 14:30:40 +02:00
Dylan Jeannin 51dd6d27c5 try fix ci pipeline 2026-04-28 13:31:39 +02:00
Dylan Jeannin cea4dcd9cb refacto variable names for D90 2026-04-28 11:06:57 +02:00
Dylan Jeannin e1c1f411c3 fix bug when add button didnt refresh the tab list in Initial conditions AdisTS and d90 2026-04-28 11:06:57 +02:00
Dylan Jeannin 4550fc95d7 debug delete functionnality in InitialConditionsAdisTS 2026-04-28 11:06:57 +02:00
Dylan Jeannin 1f97af9c03 clean useless data 2026-04-28 11:06:57 +02:00
Dylan Jeannin df9e1ec42d fix delete for pollutants D90 2026-04-28 11:06:57 +02:00
Dylan Jeannin ac79fae1fb debug save db d90 2026-04-28 11:06:57 +02:00
Dylan Jeannin fe39f07981 consistency with called functions in the file 2026-04-28 11:06:57 +02:00
Dylan Jeannin b578e0c50c désactivation temporaire de undo dans la fenêtre résultat qui fait crash Pamhyr 2026-04-28 11:06:57 +02:00
Dylan Jeannin 4408a8f368 debug save BoundaryConditionsAdisTS 2026-04-28 11:06:57 +02:00
Dylan Jeannin d3e922af6e desactivation du clic sur le graph du réseau hors de l'onglet d'édition du réseau (ex. BoundaryConditions...) 2026-04-28 11:06:57 +02:00
Dylan Jeannin 8adf878584 garde le zoom sur un graph après rafraichissement des données 2026-04-28 11:06:57 +02:00
Dylan Jeannin aa6973813f debug fonction SAVE des figures, qui utilisait une méthode Matplotlib, plutôt qu'une méthode native Qt 2026-04-28 11:06:57 +02:00
Dylan Jeannin e21a64fcad zoom plus intuitif avec translation du widget pour garder une cohérence dans le zoom 2026-04-28 11:06:57 +02:00
Dylan Jeannin 8b6308d3c9 inversion du sens de zoom avec la molette sur les graphs (contrintuitif auparavant) 2026-04-28 11:06:57 +02:00
Dylan Jeannin bc3ea9c4b9 ajout automatique de l'extansion .st lorsque l'on ne le stipule pas manuellement dans l'export des tronçons 2026-04-28 11:06:57 +02:00
Dylan Jeannin 04ea55e21a can use '.' caracter and not only ',' in Shift Geometry window 2026-04-28 11:06:57 +02:00
Dylan Jeannin a68270ff67 update model + view to display only BCAdisTS related to pollutant, and not the global list, todo: reconnect add/del buttons 2026-04-28 11:06:57 +02:00
Dylan Jeannin c498ed2d96 add d50/sigma in database + load/save data in UI 2026-04-28 11:06:57 +02:00
Dylan Jeannin e6d059bc19 correction erreur identifiant bdd 2026-04-28 11:06:57 +02:00
Dylan Jeannin cc500ceccf debug model InitialConditionsAdisTSSpec 2026-04-28 11:06:57 +02:00
Dylan Jeannin 7a91969f41 refacto variable names for D90 2026-04-28 10:27:19 +02:00
Theophile Terraz 6bd3605d10 update doc links 2026-04-28 10:13:43 +02:00
Theophile Terraz 6c6da90952 debug QSO export 2026-04-28 10:09:49 +02:00
Theophile Terraz 539b63f765 update doc links 2026-04-28 10:09:24 +02:00
Dylan Jeannin e755459004 Merge branch 'scenarios' into dev_dylan 2026-04-27 16:54:52 +02:00
Dylan Jeannin a8f95e2386 fix bug when add button didnt refresh the tab list in Initial conditions AdisTS and d90 2026-04-27 16:45:36 +02:00
Dylan Jeannin 55b200a48e debug delete functionnality in InitialConditionsAdisTS 2026-04-27 16:32:41 +02:00
Dylan Jeannin 6b8d7c2df2 clean useless data 2026-04-27 15:44:57 +02:00
Dylan Jeannin eb7c4895ca fix delete for pollutants D90 2026-04-27 15:43:32 +02:00
Dylan Jeannin a060dbd461 debug save db d90 2026-04-27 12:00:39 +02:00
Dylan Jeannin 9379f7a27d consistency with called functions in the file 2026-04-27 10:07:36 +02:00
Theophile Terraz 19fedc2278 debug QSO export 2026-04-23 09:36:36 +02:00
Theophile Terraz 260efc17a9 pep8 + debug 2026-04-23 09:27:29 +02:00
Dylan Jeannin a4026d6386 désactivation temporaire de undo dans la fenêtre résultat qui fait crash Pamhyr 2026-04-22 13:45:15 +02:00
Dylan Jeannin d6f7755d12 debug save BoundaryConditionsAdisTS 2026-04-22 12:05:28 +02:00
Theophile Terraz 00b89635f3 add a warning window when opening Adis-TS results from file if wrong directory 2026-04-22 11:46:25 +02:00
Theophile Terraz f54dc07432 add warning windows in compare results 2026-04-22 11:38:51 +02:00
Theophile Terraz 3bbc55b4be add a warning window when opening Adis-TS results from file if wrong directory 2026-04-22 11:26:05 +02:00
Dylan Jeannin b58d3e005d desactivation du clic sur le graph du réseau hors de l'onglet d'édition du réseau (ex. BoundaryConditions...) 2026-04-21 16:55:04 +02:00
Dylan Jeannin 870bb41ff7 garde le zoom sur un graph après rafraichissement des données 2026-04-21 16:02:37 +02:00
Dylan Jeannin 0701531d37 debug fonction SAVE des figures, qui utilisait une méthode Matplotlib, plutôt qu'une méthode native Qt 2026-04-21 10:41:34 +02:00
Dylan Jeannin 43eb486700 zoom plus intuitif avec translation du widget pour garder une cohérence dans le zoom 2026-04-21 10:20:24 +02:00
Dylan Jeannin 82eeb9a478 inversion du sens de zoom avec la molette sur les graphs (contrintuitif auparavant) 2026-04-21 01:22:08 +02:00
Dylan Jeannin 432c263cdf ajout automatique de l'extansion .st lorsque l'on ne le stipule pas manuellement dans l'export des tronçons 2026-04-20 23:01:04 +02:00
Dylan Jeannin c2d28b0110 can use '.' caracter and not only ',' in Shift Geometry window 2026-04-16 16:44:14 +02:00
Dylan Jeannin 23361d142e update model + view to display only BCAdisTS related to pollutant, and not the global list, todo: reconnect add/del buttons 2026-04-16 15:14:28 +02:00
Dylan Jeannin 63836527c7 add d50/sigma in database + load/save data in UI 2026-04-13 13:10:50 +02:00
Dylan Jeannin 4f6e8249e0 correction erreur identifiant bdd 2026-04-08 14:33:02 +02:00
Dylan Jeannin ab2f775b95 debug model InitialConditionsAdisTSSpec 2026-04-08 12:34:05 +02:00
Theophile Terraz bb0e176956 debug 2026-04-07 11:48:36 +02:00
Theophile Terraz c0d4aced5e debug 2026-04-07 11:47:45 +02:00
Theophile Terraz 4ed865759a debug 2026-04-03 15:23:47 +02:00
Theophile Terraz 70e09d3b3a debug export csv 2026-04-03 14:42:58 +02:00
Theophile Terraz 2bcfa4d121 debug compare results with bedload 2026-04-03 14:42:58 +02:00
Theophile Terraz 5b497d61f0 add zfd in results 2026-04-03 14:41:30 +02:00
Theophile Terraz 242df7f648 start work on results buffers 2026-04-03 14:30:11 +02:00
Theophile Terraz 421e6797f6 debug export csv 2026-04-03 14:18:29 +02:00
Theophile Terraz 0886956ec8 debug compare results with bedload 2026-04-03 13:51:15 +02:00
Theophile Terraz 9670bde56f add zfd in results 2026-04-03 12:30:58 +02:00
Theophile Terraz 3277cf551b start work on results buffers 2026-04-02 11:54:55 +02:00
Theophile Terraz ad5cc831a3 debug bed elevation comparison 2026-03-26 11:02:46 +01:00
Theophile Terraz b00cca953e debug bed elevation comparison 2026-03-26 10:51:21 +01:00
Theophile Terraz 1f35020e3f update release to python3.12 2026-03-25 13:55:18 +01:00
Theophile Terraz f0994dd801 debug requirements 2026-03-25 10:21:09 +01:00
Theophile Terraz d002eebd49 debug requirements 2026-03-25 10:07:42 +01:00
Theophile Terraz e3ffac9ce0 test python3.12 2026-03-25 09:58:30 +01:00
Theophile Terraz e8a4d5eeed test debug for linux runner 2026-03-24 17:10:22 +01:00
Theophile Terraz fc090dadce test debug for linux runner 2026-03-24 16:00:49 +01:00
Theophile Terraz 25e4786484 test debug for linux runner 2026-03-24 15:55:07 +01:00
Theophile Terraz 81e85ab488 test debug for linux runner 2026-03-24 15:47:52 +01:00
Theophile Terraz 201b1099bd test debug for linux runner 2026-03-24 14:39:40 +01:00
Theophile Terraz b6054dd633 test debug for linux runner 2026-03-24 13:29:55 +01:00
Theophile Terraz 912843be25 test debug for windows runner 2026-03-24 12:04:01 +01:00
Theophile Terraz bf49a7fc87 test debug for windows runner 2026-03-24 11:47:08 +01:00
Theophile Terraz d77be00bcc force matplotlib < 3.10 to avoid a bug (removing artists) 2026-03-24 09:18:40 +01:00
Theophile Terraz ff0fa8ae8e optimize results visu 2026-03-10 11:47:13 +01:00
Theophile Terraz 92f4793181 debug open results from file 2026-03-10 11:39:27 +01:00
Theophile Terraz 5eaac204d5 add talweg in update_rk
(cherry picked from commit 674247db09)
2026-03-10 11:32:05 +01:00
Theophile Terraz 7ef41ad886 optimize results visu 2026-03-06 11:12:43 +01:00
Theophile Terraz ada396f26d debug 2026-03-05 16:46:22 +01:00
Theophile Terraz 674247db09 add talweg in update_rk 2026-03-04 11:14:05 +01:00
Theophile Terraz 5fecd29666 Claude François 2026-02-26 16:57:42 +01:00
Theophile Terraz 71e1324a0f debug open results from file 2026-02-24 17:19:59 +01:00
Theophile Terraz 07cae542b0 pep8 2026-02-24 09:18:47 +01:00
Theophile Terraz b693f67b99 debug RubarBE 2026-02-24 09:15:31 +01:00
Theophile Terraz 18a3d51d75 add adists with rubar3 + rubar3 in release 2026-02-23 10:46:05 +01:00
Theophile Terraz 118a39bd5c debug Rubar3 (from scenario branch) 2026-02-09 14:24:09 +01:00
Theophile Terraz 5357eb9735 debug geometry 2026-02-09 10:06:24 +01:00
Theophile Terraz 0946212c4b debug 2026-01-30 14:51:56 +01:00
Theophile Terraz d6ab44d18e pep8 2026-01-30 12:08:10 +01:00
Theophile Terraz 1db1a543c7 add default rubar3 in config 2026-01-30 12:00:19 +01:00
Theophile Terraz 2009e8223b debug adists view 2026-01-30 11:32:06 +01:00
Theophile Terraz bdaa38ea5c Merge branch 'master' of gitlab.com:pamhyr/pamhyr2 2026-01-13 08:58:46 +01:00
Lionel Pénard 713732b1dd Fix doc links in README 2026-01-12 11:03:56 +01:00
Theophile Terraz f053539cdd debug 2025-12-19 11:29:02 +01:00
Theophile Terraz fc6916bb3e lot of debug 2025-12-12 14:18:09 +01:00
Theophile Terraz 97c26cb4fb debug HS 2025-12-12 11:33:13 +01:00
Theophile Terraz 10c98bb96e release for Jerome 2025-12-09 14:25:38 +01:00
Theophile Terraz 3e92a0c40d debug IC 2025-12-05 16:58:54 +01:00
Theophile Terraz 7c2d9891d0 debug IC 2025-12-05 16:51:39 +01:00
Theophile Terraz 3c3ac1f1df pep8 2025-12-05 10:19:43 +01:00
Theophile Terraz 80af278a0f add label in custom export results 2025-12-05 10:18:38 +01:00
Theophile Terraz f3c461cbd9 debug AdisTS 2025-12-02 13:47:56 +01:00
Theophile Terraz f254eaee25 debug adists 2025-12-02 09:31:07 +01:00
Theophile Terraz 012408d19e translate 2025-11-28 16:25:51 +01:00
Theophile Terraz f7e563e71a prevent scenario window to open if study is not saved 2025-11-28 16:08:54 +01:00
Theophile Terraz 81c6d39c1a debug GL in resluts 2025-11-28 15:32:15 +01:00
Theophile Terraz 5adb0bdee5 debug GL in resluts 2025-11-28 15:31:07 +01:00
Theophile Terraz d682443e1d cleaning 2025-11-28 14:23:32 +01:00
Theophile Terraz 64c99d46d4 debug named points in meshed profiles 2025-11-28 14:12:06 +01:00
Theophile Terraz 0f0defb36f Revert "debug ci"
This reverts commit 1e1ee67d4b.
2025-11-27 16:10:00 +01:00
Theophile Terraz 1e1ee67d4b debug ci 2025-11-27 16:03:09 +01:00
Theophile Terraz 5bc514c565 pep8 2025-11-26 11:58:58 +01:00
Theophile Terraz 1da3461dfe debug meshing 2025-11-26 11:16:19 +01:00
Theophile Terraz dd5eecb1e2 pep8 2025-11-24 15:18:28 +01:00
399 changed files with 27691 additions and 4642 deletions

No files matched your search

+1
View File
@@ -44,6 +44,7 @@ __old__
mage8/
adists/
rubarbe/
rubar/
linux/
windows/
+8 -8
View File
@@ -180,13 +180,13 @@ unittest:
- job: set-version
artifacts: true
script:
- python3 -m venv venv
- python3.12 -m venv venv
- . venv/bin/activate
- pip3 install -U pip
- pip3 install -r ./full-requirements.txt
- pip3 install -U -r ./full-requirements.txt
- cd src
- python3 -m unittest discover -t .
- python3.12 -m unittest discover -t .
test-pep8:
stage: test
@@ -198,7 +198,7 @@ test-pep8:
- job: unittest
script:
# Setup virtual env
- python3 -m venv venv
- python3.12 -m venv venv
- . venv/bin/activate
- pip3 install -U pip
- pip3 install -r ./requirements.txt
@@ -276,7 +276,7 @@ build-developers-doc:
- ./build.sh
# Create html documentation
- cd ../../src
- python3 -m venv doc-venv
- python3.12 -m venv doc-venv
- . doc-venv/bin/activate
- pip3 install -U pip
- pip3 install -r ../full-requirements.txt
@@ -326,13 +326,13 @@ build-linux:
- mkdir -p linux
- cd linux
# Setup virtual env
- python3 -m venv linux-venv
- python3.12 -m venv linux-venv
- . linux-venv/bin/activate
- pip3 install -U pip
- pip3 install -r ../requirements.txt
- pip3 install -U -r ../requirements.txt
# Run Pyinstaller
- pyinstaller -y $HIDDEN_IMPORTS --paths linux-venv/lib/python3.8/site-packages ../src/pamhyr.py
- pyinstaller -y $HIDDEN_IMPORTS --paths linux-venv/lib/python3.12/site-packages ../src/pamhyr.py
# Create directory
- mkdir -p pamhyr
- mkdir -p pamhyr/_internal
@@ -638,7 +638,7 @@ shadow-release:
- job: pkg-hash
artifacts: true
rules:
- if: $CI_COMMIT_BRANCH != 'master' && $CI_COMMIT_TAG
- if: $CI_COMMIT_TAG && $CI_COMMIT_TAG !~ /^v/
artifacts:
paths:
- linux/pamhyr-gnulinux.tar.xz
@@ -672,7 +672,7 @@ tag-release:
- job: pkg-hash
artifacts: true
rules:
- if: $CI_COMMIT_BRANCH == 'master' && $CI_COMMIT_TAG
- if: $CI_COMMIT_TAG =~ /^v/
artifacts:
paths:
- linux/pamhyr-gnulinux.tar.xz
+2 -1
View File
@@ -1,3 +1,4 @@
Sylvain COULIBALY, INRAE, 2020 - 2022
Théophile TERRAZ, INRAE, 2022 - 2024
Pierre-Antoine ROUBY, INRAE, 2023 - 2025
Pierre-Antoine ROUBY, INRAE, 2023 - 2026
Dylan JEANNIN, INRAE, 2026
+8 -8
View File
@@ -3,7 +3,7 @@
Pamhyr is a free and open source graphical user interface for 1D hydro-sedimentary
modelling of rivers.
![logo](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/raw/master/src/View/ui/ressources/Pamhyr2_logo.png)
![logo](https://gitlab.com/pamhyr/pamhyr2/-/raw/master/src/View/ui/ressources/Pamhyr2_logo.png)
## Features
@@ -21,9 +21,9 @@ modelling of rivers.
+ Create and edit study alternative scenario tree
Let see the
[documentation](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home)
([:fr:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home-fr),
[:gb:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home-en))
[documentation](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home)
([:fr:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home-fr),
[:gb:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home-en))
for more details.
## Install
@@ -31,11 +31,11 @@ for more details.
### GNU/Linux
See documentation:
- [French :fr:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/fr/Install%20on%20GNULinux)
- [English :gb:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/en/Install%20on%20GNULinux)
- [French :fr:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/fr/Install%20on%20GNULinux)
- [English :gb:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/en/Install%20on%20GNULinux)
### Windows
See documentation:
- [French :fr:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/fr/Install%20on%20Windows)
- [English :gb:](https://gitlab.irstea.fr/theophile.terraz/pamhyr/-/wikis/home/en/Install%20on%20Windows)
- [French :fr:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/fr/Install%20on%20Windows)
- [English :gb:](https://gitlab.com/pamhyr/pamhyr2/-/wikis/home/en/Install%20on%20Windows)
+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

+6 -6
View File
@@ -157,12 +157,12 @@
%%
%% FIXME: Delete me for the first realease
\usepackage{draftwatermark}
\SetWatermarkLightness{0.8}
\SetWatermarkAngle{25}
\SetWatermarkScale{3}
\SetWatermarkFontSize{1cm}
\SetWatermarkText{Work in progress}
%% \usepackage{draftwatermark}
%% \SetWatermarkLightness{0.8}
%% \SetWatermarkAngle{25}
%% \SetWatermarkScale{3}
%% \SetWatermarkFontSize{1cm}
%% \SetWatermarkText{Work in progress}
%% Icons
+4 -3
View File
@@ -1,10 +1,10 @@
PyQt5==5.15.4
PyQt5==5.15.11
PyQt5-Qt5==5.15.2
PyQt5-sip==12.12.2
PyQt5-sip==12.18.0
PyQtWebEngine==5.15.6
QScintilla>=2.14.1
pyqtgraph>=0.12.1
matplotlib>=3.7.1
matplotlib==3.10.8
numpy>=1.24.2
colorama>=0.4.3
pyinstaller>=5.11.0
@@ -14,3 +14,4 @@ platformdirs>=4.2.0
pyshp>=2.3.1
rasterio==1.3.11
#fortranformat==2.0.3
psutil>=7.2.2
+1 -1
View File
@@ -20,4 +20,4 @@
"python-pyqtgraph" "python-qscintilla"
"python-numpy@1" "python-shapely" "python-pyshp"
"python-matplotlib" "python-colorama"
"python-rasterio"))
"python-rasterio" "python-psutil"))
+1 -1
View File
@@ -48,7 +48,7 @@ copy /y ..\mage8-windows\libbief.dll pamhyr\mage8\
rem rubar
mkdir pamhyr\rubar
copy /y ..\adists-windows\rubarbe.exe pamhyr\rubar\
copy /y ..\rubar-windows\rubarbe.exe pamhyr\rubar\
rem adists
mkdir pamhyr\adists
+4 -3
View File
@@ -1,10 +1,10 @@
PyQt5==5.15.4
PyQt5==5.15.11
PyQt5-Qt5==5.15.2
PyQt5-sip==12.12.2
PyQt5-sip==12.18.0
#PyQtWebEngine==5.15.6
QScintilla>=2.14.1
pyqtgraph>=0.12.1
matplotlib>=3.7.1
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
@@ -1,5 +1,5 @@
# Checker.py -- Pamhyr abstract checker class
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+6 -3
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr MAGE checkers
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
+8 -4
View File
@@ -1,5 +1,5 @@
# Study.py -- Pamhyr study checkers
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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"
+1 -1
View File
@@ -1,5 +1,5 @@
# AMeshingTool.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+128 -86
View File
@@ -1,5 +1,5 @@
# Internal.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -57,6 +57,13 @@ class InternalMeshing(AMeshingTool):
# we never modify original profiles, we work on the copies
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,8 +72,13 @@ class InternalMeshing(AMeshingTool):
return new_profiles
def st_to_m(self, profiles, guide_list):
@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)
max_values_index = [0] * (len(guide_list) - 1)
@@ -85,11 +97,13 @@ class InternalMeshing(AMeshingTool):
profiles[isect+1],
guide_list[i],
guide_list[i+1])
for isect in reversed(range(0, max_values_index[i])):
self.compl_sect(profiles[isect+1],
profiles[isect],
guide_list[i],
guide_list[i+1])
return profiles
def interpolate_transversal_step(self,
@@ -123,12 +137,14 @@ 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)
return new_profiles
def compl_sect(self, sect1, sect2, tag1, tag2):
@@ -139,19 +155,11 @@ class InternalMeshing(AMeshingTool):
# len1 and 2: number of intervals
# len1: target len
if tag1 == '': # left end point
start1 = 0
start2 = 0
else:
start1 = sect1.named_point_index(tag1)
start2 = sect2.named_point_index(tag1)
start1 = sect1.named_point_index(tag1)
start2 = sect2.named_point_index(tag1)
if tag2 == '': # right end point
end1 = sect1.nb_points-1
end2 = sect2.nb_points-1
else:
end1 = sect1.named_point_index(tag2)
end2 = sect2.named_point_index(tag2)
end1 = sect1.named_point_index(tag2)
end2 = sect2.named_point_index(tag2)
len1 = end1 - start1
len2 = end2 - start2
@@ -164,15 +172,14 @@ 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 = ''
for i in range(len1):
ltot1 += sect1.point(start1+i).dist(sect1.point(start1+i+1))
alpha.append(ltot1)
alpha = list(map(lambda x: x/ltot1, alpha)) # target ratios
for i in range(len2):
ltot2 += sect2.point(start2+i).dist(sect2.point(start2+i+1))
beta.append(ltot2)
@@ -183,81 +190,116 @@ class InternalMeshing(AMeshingTool):
p = sect2.point(start2).copy()
sect2.insert_point(start2, p)
len2 += 1
if tag1 == '': # left end point
sect2.point(start2).name = ''
if tag2 == '': # left end point
sect2.point(start2+1).name = ''
len2 += 1
elif ltot1 < 0.0001:
sect2.add_npoints(len1-len2)
len2 = len1
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:
print(f"ratio négatif {ratio}")
# on double le point a gauche
p = sect2.point(start2+j0-1).copy()
sect2.insert_point(start2+j0-1, p)
sect2.point(start2+j0-1).name = ''
beta[j0] = beta[j0-1]
else:
p = sect2.point(start2+j0).copy()
sect2.insert_point(start2+j0, p)
sect2.point(start2+j0+1).name = ''
sect2.point(start2+j0+1).x = \
(1.0-ratio) * sect2.point(start2+j0).x + \
ratio * sect2.point(start2+j0+2).x
sect2.point(start2+j0+1).y = \
(1.0-ratio) * sect2.point(start2+j0).y + \
ratio * sect2.point(start2+j0+2).y
sect2.point(start2+j0+1).z = \
(1.0-ratio) * sect2.point(start2+j0).z + \
ratio * sect2.point(start2+j0+2).z
beta[j0] = (1.-ratio)*beta[j0-1] \
+ ratio*beta[j0+1]
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 tag1 != "un":
sect2.point(start2).name = tag1
if tag2 != "np":
sect2.point(start2+len2).name = tag2
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 = ''
sect2.point(start2).name = tag1
sect2.point(start2+len2).name = tag2
sect2.modified()
def _match_indices(self, u_ref, v_target):
# appariement optimal (ordre croissant) par programmation dynamique
"""
Trouve, pour chaque v_target[j], un indice i_j dans u_ref tel que
i_0 < i_1 < ... < i_{m-1} et
Sum |u_ref[i_j] - v_target[j]| soit minimal.
Retourne la liste des indices i_j (longueur m),
avec i_0 = 0 et i_{m-1} = n-1 imposés
(les extrémités se correspondent forcément).
"""
n = len(u_ref)
m = len(v_target)
# Les extrémités sont fixées.
fixed_first, fixed_last = 0, n - 1
# DP sur les indices disponibles entre fixed_first+1 et fixed_last-1
# dp[j][i] = coût minimal pour apparier v_target[1..j] aux indices
# se terminant à l'indice i (i croissant strict), j de 1 à m-2.
if m == 2:
return [fixed_first, fixed_last]
# indices internes possibles
candidates = list(range(fixed_first + 1, fixed_last))
inner_targets = v_target[1:-1] # cibles internes (j = 1..m-2)
INF = float("inf")
# dp[j] : dict indice_candidat -> (coût_min, provenance)
dp = [{} for _ in range(len(inner_targets))]
for j, vt in enumerate(inner_targets):
for idx_pos, i in enumerate(candidates):
cost_here = abs(u_ref[i] - vt)
if j == 0:
# doit être > fixed_first
dp[j][i] = (cost_here, fixed_first)
else:
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],
+1 -1
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# __init__.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# test_Model.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# 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
@@ -1,5 +1,5 @@
# BoundaryCondition.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -232,6 +232,8 @@ class BoundaryCondition(SQLSubModel):
type TEXT NOT NULL,
tab TEXT NOT NULL,
node INTEGER,
d50 REAL DEFAULT 0.002,
sigma REAL DEFAULT 1,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id),
@@ -258,6 +260,17 @@ class BoundaryCondition(SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and int(minor) <= 2:
if int(release) < 4:
execute(
"ALTER TABLE boundary_condition " +
"ADD COLUMN d50 REAL DEFAULT 0.002"
)
execute(
"ALTER TABLE boundary_condition " +
"ADD COLUMN sigma REAL DEFAULT 1"
)
return cls._update_submodel(execute, version, data)
@classmethod
@@ -316,7 +329,8 @@ class BoundaryCondition(SQLSubModel):
return new
table = execute(
"SELECT pamhyr_id, deleted, name, type, node, scenario " +
"SELECT pamhyr_id, deleted, name, type, node, d50, sigma, " +
"scenario " +
"FROM boundary_condition " +
f"WHERE tab = '{tab}' " +
f"AND scenario = {scenario.id} " +
@@ -331,6 +345,8 @@ class BoundaryCondition(SQLSubModel):
name = next(it)
t = next(it)
node = next(it)
d50 = next(it)
sigma = next(it)
owner_scenario = next(it)
ctor = cls._get_ctor_from_type(t)
@@ -340,9 +356,14 @@ class BoundaryCondition(SQLSubModel):
status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
if t == "SL":
bc.d50 = d50
bc.sigma = sigma
bc.node = None
if node != -1:
bc.node = next(filter(lambda n: n.id == node, nodes), None)
@@ -375,15 +396,22 @@ class BoundaryCondition(SQLSubModel):
if self._node is not None:
node = self._node.id
d50 = 0.002
sigma = 1
if self._type == "SL":
d50 = self._d50
sigma = self._sigma
execute(
"INSERT INTO " +
"boundary_condition(" +
"pamhyr_id, deleted, name, type, tab, node, scenario" +
"pamhyr_id, deleted, name, type, tab, node, d50, sigma, scenario" +
") " +
"VALUES (" +
f"{self._pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', '{tab}', {node}, " +
f"{d50}, {sigma}, " +
f"{self._status.scenario_id}" +
")"
)
@@ -468,6 +496,24 @@ class BoundaryCondition(SQLSubModel):
def has_node(self):
return self._node is not None
@property
def d50(self):
return self._d50
@d50.setter
def d50(self, value):
self._d50 = float(value)
self.modified()
@property
def sigma(self):
return self._sigma
@sigma.setter
def sigma(self, value):
self._sigma = float(value)
self.modified()
@property
def header(self):
return self._header.copy()
@@ -1,5 +1,5 @@
# BoundaryConditionList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# BoundaryConditionTypes.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# BoundaryConditionsAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -70,9 +70,8 @@ class Data(SQLSubModel):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
if major == "0" and minor == "0":
if int(release) < 11:
cls._db_create(execute)
created = True
@@ -143,8 +142,9 @@ class Data(SQLSubModel):
table = execute(
"SELECT pamhyr_id, deleted, data0, data1, scenario FROM " +
"boundary_condition_data_adists " +
f"WHERE bca = {bca.id} " +
f"AND scenario = {scenario.id}"
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:
@@ -187,8 +187,9 @@ class Data(SQLSubModel):
execute(
"INSERT INTO " +
"boundary_condition_data_adists(" +
"pamhyr_id, data0, data1, bca, scenario) " +
f"VALUES ({self.id}, '{data0}', {data1}, {bca.id}, " +
"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})"
)
@@ -247,15 +248,13 @@ class BoundaryConditionAdisTS(SQLSubModel):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "boundary_condition_adists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created:
return cls._update_submodel(execute, version, data)
@@ -300,6 +299,14 @@ class BoundaryConditionAdisTS(SQLSubModel):
if pol_id == -1:
continue
if pol_id not in pid_pol:
# ⚠️ cas important : probablement déjà migré
logger.warning(
f"pol_id {pol_id} not in pid_pol " +
"→ probably already migrated"
)
continue
execute(
f"UPDATE boundary_condition_adists " +
f"SET pollutant = {pid_pol[pol_id]} " +
@@ -312,17 +319,21 @@ class BoundaryConditionAdisTS(SQLSubModel):
status = data['status']
nodes = data['nodes']
scenario = data["scenario"]
pollutant = data.get("pollutant")
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, pollutant, type, node, scenario " +
sql = (
"SELECT pamhyr_id, deleted, pollutant, type, node, scenario "
"FROM boundary_condition_adists " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
f"WHERE scenario = {scenario.id} "
)
if pollutant is not None:
sql += f"AND pollutant = {pollutant.id} "
table = execute(sql)
if table is not None:
for row in table:
@@ -347,7 +358,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
bc.type = bc_type
bc.node = None
if row[3] != -1:
if node != -1:
tmp = next(
filter(
lambda n: n.id == node, nodes
@@ -426,7 +437,14 @@ class BoundaryConditionAdisTS(SQLSubModel):
d._data_traversal(predicate, modifier, data)
def __len__(self):
return len(self._data)
return len(
list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
)
@classmethod
def time_convert(cls, data):
@@ -451,7 +469,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@node.setter
def node(self, node):
self._node = node
self._status.modified()
self.modified()
@property
def header(self):
@@ -460,7 +478,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@header.setter
def header(self, header):
self._header = header
self._status.modified()
self.modified()
@property
def pollutant(self):
@@ -469,7 +487,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@pollutant.setter
def pollutant(self, pollutant):
self._pollutant = pollutant
self._status.modified()
self.modified()
@property
def type(self):
@@ -478,11 +496,16 @@ class BoundaryConditionAdisTS(SQLSubModel):
@type.setter
def type(self, type):
self._type = type
self._status.modified()
self.modified()
@property
def data(self):
return self._data.copy()
return list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property
def _default_0(self):
@@ -508,36 +531,42 @@ 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 = (self._default_0, self._default_1)
value = Data(
self._default_0, self._default_1,
status=self._status
)
self._data.insert(index, value)
self._status.modified()
self.modified()
return value
def insert(self, index: int, value):
self._data.insert(index, value)
self._status.modified()
if value in self._data:
value.set_as_not_deleted()
else:
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
self._data = list(
list(
map(
lambda e: e[1],
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] not in indexes,
lambda e: e[0] in indexes,
enumerate(self.data)
)
)
)
self._status.modified()
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._status.modified()
self.modified()
def index(self, bc):
self._data.index(bc)
@@ -552,10 +581,10 @@ class BoundaryConditionAdisTS(SQLSubModel):
return lst
def _set_i_c_v(self, index, column, value):
v = list(self._data[index])
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = tuple(v)
self._status.modified()
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
@@ -1,5 +1,5 @@
# BoundaryConditionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -38,9 +38,13 @@ class BoundaryConditionsAdisTSList(PamhyrModelList):
if data is None:
data = {}
new._lst = BoundaryConditionAdisTS._db_load(
execute, data
)
previous_pollutant = data.get("pollutant")
data.pop("pollutant", None)
new._lst = BoundaryConditionAdisTS._db_load(execute, data)
if previous_pollutant is not None:
data["pollutant"] = previous_pollutant
return new
@@ -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)
@@ -0,0 +1,71 @@
# BoundaryConditionsTemperatureList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Except import NotImplementedMethodeError
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
class BoundaryConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
BoundaryConditionTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = BoundaryConditionTemperature._db_load(execute, data)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM boundary_condition_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for bca in self._lst:
bca._db_save(execute, data=data)
return True
def new(self, index):
n = BoundaryConditionTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
@property
def BCs_Temperature_List(self):
return self.lst
+46 -7
View File
@@ -1,5 +1,5 @@
# D90AdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -73,15 +73,13 @@ class D90AdisTS(SQLSubModel):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 6:
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "d90_adists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created:
return cls._update_submodel(execute, version, data)
@@ -277,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()
+5 -1
View File
@@ -1,5 +1,5 @@
# D90AdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
+26 -16
View File
@@ -1,5 +1,5 @@
# D90AdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -70,16 +70,12 @@ class D90AdisTSSpec(SQLSubModel):
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == 0 and minor < 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
if cls.is_table_exists(execute, "d90_spec"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@@ -87,6 +83,7 @@ class D90AdisTSSpec(SQLSubModel):
def _db_update_to_0_2_0(cls, execute, data):
table = "d90_spec"
table_new = "d90_adists_spec"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
@@ -95,8 +92,10 @@ class D90AdisTSSpec(SQLSubModel):
execute(
f"INSERT INTO {table_new}_tmp " +
"(pamhyr_id, name, d90, enabled, scenario) " +
"SELECT pamhyr_id, name, d90, enabled, scenario " +
"(pamhyr_id, d90_default, name, reach, " +
" start_rk, end_rk, d90, enabled, scenario) " +
"SELECT pamhyr_id, d90_default, name, reach, " +
" start_rk, end_rk, d90, enabled, scenario " +
f"FROM {table}"
)
@@ -104,12 +103,13 @@ class D90AdisTSSpec(SQLSubModel):
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
cls._db_update_to_0_2_0_set_d90_pid(execute, data)
cls._db_update_to_0_2_0_set_reach_pid(execute, table_new, reachs)
@classmethod
def _db_update_to_0_2_0_set_d90_pid(cls, execute, data):
pid_d90 = data["id2pid"]["d90_adists"]
els = execute(
f"SELECT pamhyr_id, d90 FROM d90_adists_spec"
f"SELECT pamhyr_id, d90_default FROM d90_adists_spec"
)
for row in els:
@@ -121,8 +121,8 @@ class D90AdisTSSpec(SQLSubModel):
continue
execute(
f"UPDATE d90_adists_spec" +
f"SET d90 = {pid_d90[d90_id]} " +
f"UPDATE d90_adists_spec " +
f"SET d90_default = {pid_d90[d90_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@@ -167,7 +167,7 @@ class D90AdisTSSpec(SQLSubModel):
owner_scenario=owner_scenario
)
if deleted:
new_spec.is_deleted()
new_spec.set_as_deleted()
new_spec.reach = reach
new_spec.start_rk = start_rk
@@ -196,12 +196,12 @@ class D90AdisTSSpec(SQLSubModel):
"d90_default, name, reach, " +
"start_rk, end_rk, d90, enabled, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{d90_default}, " +
f"'{self._db_format(self._name_section)}', " +
f"{self._reach}, " +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._d90}, {self._enabled}" +
f"{self._d90}, {self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
@@ -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
+56 -17
View File
@@ -1,5 +1,5 @@
# DIFAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -79,15 +79,13 @@ class DIFAdisTS(SQLSubModel):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 6:
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "dif_adists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created:
return cls._update_submodel(execute, version, data)
@@ -142,24 +140,24 @@ class DIFAdisTS(SQLSubModel):
enabled = (next(it) == 1)
owner_scenario = next(it)
dif = cls(
DIF = cls(
id=pid,
name=name,
status=status,
owner_scenario=owner_scenario,
)
dif.method = method
dif.dif = dif
dif.b = b
dif.c = c
dif.enabled = enabled
DIF.method = method
DIF.dif = dif
DIF.b = b
DIF.c = c
DIF.enabled = enabled
data['dif_default_id'] = pid
dif._data = DIFAdisTSSpec._db_load(execute, data)
DIF._data = DIFAdisTSSpec._db_load(execute, data)
loaded.add(pid)
new.append(dif)
new.append(DIF)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
@@ -190,7 +188,7 @@ class DIFAdisTS(SQLSubModel):
b = self.b
c = -1.
if self.dif is not None:
if self.c is not None:
c = self.c
sql = (
@@ -201,7 +199,7 @@ class DIFAdisTS(SQLSubModel):
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"'{self._db_format(self._method)}', " +
f"{dif.id}, {b}, {c}, {self._enabled}, " +
f"{dif}, {b}, {c}, {self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
@@ -331,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()
+5 -1
View File
@@ -1,5 +1,5 @@
# DIFAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
+26 -21
View File
@@ -1,5 +1,5 @@
# DIFAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -33,16 +33,14 @@ class DIFAdisTSSpec(SQLSubModel):
_id_cnt = 0
def __init__(self, id: int = -1, method: str = "",
status=None):
super(DIFAdisTSSpec, self).__init__()
status=None, owner_scenario=-1):
super(DIFAdisTSSpec, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if id == -1:
self.id = DIFAdisTSSpec._id_cnt
else:
self.id = id
self._method = method
self._reach = None
self._start_rk = None
@@ -52,8 +50,6 @@ class DIFAdisTSSpec(SQLSubModel):
self._c = None
self._enabled = True
DIFAdisTSSpec._id_cnt = max(DIFAdisTSSpec._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
@@ -81,16 +77,12 @@ class DIFAdisTSSpec(SQLSubModel):
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == 0 and minor < 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
if cls.is_table_exists(execute, "dif_spec"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@@ -195,7 +187,7 @@ class DIFAdisTSSpec(SQLSubModel):
new_spec.c = c
new_spec.enabled = enabled
loaded.add(pid)
loaded.add(id)
new.append(new_spec)
data["scenario"] = scenario.parent
@@ -210,22 +202,23 @@ class DIFAdisTSSpec(SQLSubModel):
dif_default = data['dif_default_id']
execute(
sql = (
"INSERT INTO " +
"dif_adists_spec(pamhyr_id, deleted, " +
"dif_default, method, reach, " +
"start_rk, end_rk, dif, b, c, enabled, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{dif_default}, " +
f"'{self._db_format(self._method)}', " +
f"{self._reach}, " +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._dif}, {self._b}, {self._c}, " +
f"{self._enabled}" +
f"{self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
@@ -300,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
+307
View File
@@ -0,0 +1,307 @@
# Distribution.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import struct
import logging
import itertools
import numpy as np
from tools import logger_exception
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
logger = logging.getLogger()
class Distribution(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Distribution, self).__init__(
id=id, status=status,
owner_scenario=0
)
self._name = name
self._type = "generic"
self._script = script
self._labels = []
self._default_values = []
self._params_checkers = []
self._params_tips = []
@property
def name(self):
return self._name
@property
def labels(self):
return self._labels
@property
def default_values(self):
return self._default_values
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble_distribution{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
script TEXT NOT NULL,
PRIMARY KEY(pamhyr_id)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, script " +
"FROM ensemble_distribution " +
f"WHERE pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
script = next(it)
new_distribution = cls(
id, name=name, script=script,
status=data["status"]
)
if deleted:
new_distribution.set_as_deleted()
loaded.add(id)
new.append(new_distribution)
return new
def _db_save(self, execute, data=None):
# Only save custom distribution
if self._type != "custom":
return True
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"ensemble_distribution(pamhyr_id, deleted, " +
" name, script) " +
"VALUES (?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._script
)
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
def is_valid_parameters(self, parameters):
valid = True
tips = []
for i, checker in enumerate(self._params_checkers):
ok = checker(parameters)
if not ok:
tips.append(self._params_tips[i])
valid = False
return (valid, tips)
def check_params(self, params):
return True
def random(self, r):
raise NotImplementedMethodeError(cls, cls._load)
###############################
# DISTRIBUTION IMPLEMENTATION #
###############################
class Uniform(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "uniform", script: str = "",
status=None):
super(Uniform, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["low", "high"]
self._default_values = [20.0, 40.0]
self._params_checkers = [self.check_params, self.check_params]
self._params_tips = ["uniform_low_tips", "uniform_high_tips"]
def check_params(self, params):
return (params[0] < params[1])
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.uniform(*lst)
class Normal(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "normal", script: str = "",
status=None):
super(Normal, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["loc", "scale"]
self._default_values = [30.0, 5.0]
self._params_checkers = [self.check_params, self.check_scale]
self._params_tips = ["normal_loc_tips", "normal_scale_tips"]
def check_scale(self, params):
return 0 <= params[1]
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.normal(*lst)
class Binomial(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "binomial", script: str = "",
status=None):
super(Binomial, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["binomial_n", "binomial_p"]
self._default_values = [100.0, 0.3]
self._params_checkers = [self.check_n, self.check_p]
self._params_tips = ["binomial_n_tips", "binomial_p_tips"]
def check_n(self, params):
n = params[0]
return (0 <= n) and (n == round(n))
def check_p(self, params):
p = params[1]
return 0 <= p <= 1
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.binomial(*lst)
class Poisson(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "poisson", script: str = "",
status=None):
super(Poisson, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["poisson_lam"]
self._default_values = [5.0]
self._params_checkers = [self.check_lam]
self._params_tips = ["poisson_lam_tips"]
def check_lam(self, params):
lam = params[0]
return 0 <= lam
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.poisson(*lst)
class Custom(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Custom, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["custom"]
self._type = "custom"
self._default_values = [""]
self._params_checkers = [self.check_params]
self._params_tips = ["custom_params_tips"]
def random(self, params, nb):
try:
# Run script to (re)define the sample distribution
f = eval(self._script)
return f(params, nb)
except Exception as e:
logger_exception(e)
return None # TODO: Raise helpful exception
+62
View File
@@ -0,0 +1,62 @@
# DistributionList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Distribution import *
class DistributionList(PamhyrModelList):
_sub_classes = [Distribution]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
# Default distributions
d = {"status": data['status']}
new._lst = [
Uniform(**d),
Normal(**d),
Binomial(**d),
Poisson(**d),
]
# DB custom distributions
new._lst += Distribution._db_load(
execute, data
)
return new
def _db_save(self, execute, data={}):
execute(
"DELETE FROM ensemble_distribution"
)
for fun in self._lst:
fun._db_save(execute, data)
return True
def new(self, index):
r = Custom(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
+279
View File
@@ -0,0 +1,279 @@
# Ensemble.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import struct
import logging
import itertools
import numpy as np
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Ensembles.Distribution import Uniform
logger = logging.getLogger()
class Ensemble(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
distribution=None, params=[0., 100.],
data_type=None, target_data=None,
status=None, owner_scenario=-1):
super(Ensemble, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._data_type = data_type
self._distribution = distribution
self._params = params
self._target_data = target_data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type VARCHAR(8) NOT NULL,
distribution INTEGER NOT NULL,
distribution_name VARCHAR(32) NOT NULL,
params BLOB NOT NULL,
params_len INTEGER NOT NULL,
data_pid INTEGER NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
distributions = data["ens_distributions"]
stricklers = data["stricklers"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"distribution, distribution_name, params, params_len, data_pid, " +
"scenario " +
"FROM ensemble " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
data_type = next(it)
fid = next(it)
fname = next(it)
params = next(it)
params_len = next(it)
data_pid = next(it)
owner_scenario = next(it)
def fn(f): return f._name == fname
if fid != -1:
def fn(f): return f._pamhyr_id == fid
distribution = next(
filter(fn, distributions.lst),
None
)
target_data = next(
filter(
lambda s: s._pamhyr_id == data_pid,
stricklers.lst
),
None
)
rdata = list(cls._decode_params(params, params_len))
new_ensemble = cls(
id, name=name,
distribution=distribution,
params=rdata,
data_type=data_type,
target_data=target_data,
status=data["status"],
owner_scenario=owner_scenario
)
if deleted:
new_ensemble.set_as_deleted()
loaded.add(id)
new.append(new_ensemble)
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM ensemble " +
f"WHERE pamhyr_id = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
bparams, length = self._encode_params()
if self._distribution is None:
fid = -1
fname = "NULL"
else:
fid = self._distribution._pamhyr_id
fname = self._distribution._name
if self._distribution._type == "generic":
fid = -1
data_pid = -1
if self._target_data is not None:
data_pid = self._target_data._pamhyr_id
execute(
"INSERT INTO " +
"ensemble(pamhyr_id, deleted, name, type, " +
" distribution, distribution_name, params, params_len, " +
" data_pid, scenario) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._data_type,
fid, fname, bparams, length,
data_pid, self._status.scenario_id
)
return True
def _encode_params(self):
length = len(self._params)
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.pack(data_format, *self._params), length
@classmethod
def _decode_params(cls, bparams, length):
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.unpack(data_format, bparams)
def random(self, nb):
return np.nditer(
self._distribution.random(
self._params, nb
)
)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
@property
def distribution(self):
return self._distribution
@distribution.setter
def distribution(self, distribution):
self._distribution = distribution
self._status.modified()
@property
def params(self):
return self._params
@params.setter
def params(self, params):
self._params = params
self._status.modified()
@property
def data_type(self):
return self._data_type
@data_type.setter
def data_type(self, data_type):
self._data_type = data_type
self._status.modified()
@property
def target_data(self):
return self._target_data
@target_data.setter
def target_data(self, target_data):
self._target_data = target_data
self._status.modified()
def get_data_dict(self, use_ids=False):
target_data = self.target_data
distribution = self.distribution
if use_ids:
if self.target_data:
target_data = self.target_data.pamhyr_id
if self.distribution:
distribution = self.distribution.name
return {
"name": self.name,
"data_type": self.data_type,
"target_data": target_data,
"distribution": distribution,
"params": self.params,
}
def set_data_dict(self, data):
self.name = data["name"]
self.data_type = data["data_type"]
self.target_data = data["target_data"]
self.distribution = data["distribution"]
self.params = data["params"]
@@ -1,5 +1,5 @@
# OutputRKListAdists.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
# EnsembleList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -18,42 +18,45 @@
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.OutputRKAdists.OutputRKAdists import OutputRKAdists
from Model.Ensembles.Ensemble import Ensemble
class OutputRKAdistsList(PamhyrModelList):
_sub_classes = [OutputRKAdists]
class EnsembleList(PamhyrModelList):
_sub_classes = [Ensemble]
@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)
new._lst = Ensemble._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 ensemble " +
f"WHERE scenario = {self._status.scenario_id}"
)
for sl in self._lst:
ok &= sl._db_save(execute, data)
for ens in self._lst:
ens._db_save(execute, data=data)
return ok
return True
@property
def OutputRK_List(self):
return self.lst
def new(self, lst, index):
n = OutputRKAdists(status=self._status)
self._lst.insert(index, n)
def new(self, index):
r = Ensemble(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return n
return r
def new_with_data(self, index, data):
r = Ensemble(status=self._status)
r.set_data_dict(data)
self._lst.insert(index, r)
self._status.modified()
return r
+1 -1
View File
@@ -1,5 +1,5 @@
# Except.py -- Pamhyr model exceptions
# Copyright (C) 2023-2025 INRAE
# 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
+11 -4
View File
@@ -1,5 +1,5 @@
# Friction.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -228,9 +228,10 @@ class Friction(SQLSubModel):
ssi = self._status.scenario_id
return (
self._begin_strickler._owner_scenario == ssi
# or self._end_strickler._owner_scenario == ssi
or super(Friction, self).must_be_saved()
self._begin_strickler is not None
and (self._begin_strickler._owner_scenario == ssi
# or self._end_strickler._owner_scenario == ssi
or super(Friction, self).must_be_saved())
)
def _db_save(self, execute, data=None):
@@ -364,3 +365,9 @@ class Friction(SQLSubModel):
# self.end_strickler.medium])
return minor, medium
def update_rks(self, begin_rk, end_rk):
self._begin_rk = begin_rk
self._end_rk = end_rk
self.modified()
+14 -8
View File
@@ -1,5 +1,5 @@
# FrictionList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
+4 -1
View File
@@ -1,5 +1,5 @@
# Point.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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):
"""
+1 -1
View File
@@ -1,5 +1,5 @@
# PointXY.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+28 -1
View File
@@ -1,5 +1,5 @@
# PointXYZ.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -66,6 +66,11 @@ class PointXYZ(Point):
)
""")
execute(
"CREATE INDEX idx_point " +
"ON geometry_pointXYZ(profile, scenario);"
)
return cls._create_submodel(execute)
@classmethod
@@ -90,6 +95,12 @@ class PointXYZ(Point):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
# Try to create index at each update
execute(
"CREATE INDEX IF NOT EXISTS idx_point " +
"ON geometry_pointXYZ(profile, scenario);"
)
return cls._update_submodel(execute, version, data)
@classmethod
@@ -317,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):
@@ -326,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):
@@ -335,8 +352,18 @@ 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
self.y += y
self.z += z
self.modified()
def get_coordinate(self):
return (self._x, self._y, self._z)
+66 -13
View File
@@ -1,5 +1,5 @@
# Profile.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
@@ -80,6 +82,9 @@ class Profile(object):
return self.points[0]
elif name == "np":
return self.points[-1]
elif name == "talweg":
zm = self.z_min()
return next((p for p in self.points if p.z == zm), None)
else:
return next((p for p in self.points if p.name == name), None)
@@ -88,6 +93,12 @@ class Profile(object):
return 0
elif name == "np":
return len(self.points) - 1
elif name == "talweg":
zm = self.z_min()
return next(
(p for p in enumerate(self.points) if p[1].z == zm),
None
)[0]
else:
return next(
(p for p in enumerate(self.points) if p[1].name == name),
@@ -151,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):
@@ -164,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):
@@ -208,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.
@@ -223,7 +276,7 @@ class Profile(object):
else:
self._points.insert(index, point)
self.modified()
self.content_modified()
def delete_i(self, indexes: list):
list(
@@ -236,7 +289,7 @@ class Profile(object):
)
)
self.modified()
self.content_modified()
def delete_points(self, points):
"""Delete some elements in profile list
@@ -253,7 +306,7 @@ class Profile(object):
points
)
)
self.modified()
self.content_modified()
# Move
@@ -263,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:
@@ -271,7 +324,7 @@ class Profile(object):
p = self._points
p[index], p[prev] = p[prev], p[index]
self.modified()
self.content_modified()
# Sort
@@ -288,7 +341,7 @@ class Profile(object):
key=predicate,
reverse=is_reversed
)
self.modified()
self.content_modified()
@timer
def sort_with_indexes(self, indexes: list):
@@ -304,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
+154 -58
View File
@@ -1,5 +1,5 @@
# ProfileXYZ.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -24,7 +24,7 @@ from typing import List
from functools import reduce
from dataclasses import dataclass
from tools import timer, flatten
from tools import trace, timer, flatten
from shapely import geometry
from Model.Tools.PamhyrDB import SQLSubModel
@@ -39,9 +39,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 +58,8 @@ class ProfileXYZ(Profile, SQLSubModel):
num=0,
nb_point: int = 0,
code1: int = 0, code2: int = 0,
status=None, owner_scenario=-1):
status=None, owner_scenario=-1,
enabled=True, interpolated=False):
"""ProfileXYZ constructor
Args:
@@ -90,6 +91,11 @@ 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 = {}
@classmethod
def _db_create(cls, execute, ext=""):
@@ -105,6 +111,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),
@@ -149,6 +157,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
@@ -234,7 +263,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} " +
@@ -255,6 +284,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,
@@ -262,7 +293,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()
@@ -315,12 +348,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)}" +
")"
)
@@ -387,7 +422,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()
@@ -396,7 +433,6 @@ class ProfileXYZ(Profile, SQLSubModel):
for point in self._points:
p = point.copy()
print(p)
new_p._points.append(p)
new_p.modified()
@@ -408,7 +444,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()
@@ -521,7 +559,7 @@ class ProfileXYZ(Profile, SQLSubModel):
)
self._points.append(pt)
self.modified()
self.content_modified()
def get_point_i(self, index: int) -> PointXYZ:
"""Get point at index.
@@ -586,7 +624,7 @@ class ProfileXYZ(Profile, SQLSubModel):
)
self.points.append(point_xyz)
self.modified()
self.content_modified()
def insert(self, index: int):
"""Insert a new point at index.
@@ -605,7 +643,7 @@ class ProfileXYZ(Profile, SQLSubModel):
point = PointXYZ(x=x, y=y, z=z, profile=self, status=self._status)
self._points.insert(index, point)
self.modified()
self.content_modified()
return point
@@ -620,6 +658,7 @@ class ProfileXYZ(Profile, SQLSubModel):
"""
return [x for x in lst if not np.isnan(x)]
@timer
def speed(self, q, z):
area = self.wet_area(z)
@@ -671,6 +710,7 @@ class ProfileXYZ(Profile, SQLSubModel):
length += line.length
return length
@timer
def compute_wet_area(self, z):
area = 0.0
if len(self.tab.L) > 0:
@@ -711,6 +751,7 @@ class ProfileXYZ(Profile, SQLSubModel):
if len(line.coords) > 2:
poly = geometry.Polygon(line)
area += poly.area
return area
def wet_radius(self, z):
@@ -733,6 +774,7 @@ class ProfileXYZ(Profile, SQLSubModel):
line = geometry.LineString(list(zip(station, zz)))
return line
@timer
def wet_lines(self, z):
points = self._points
if len(points) < 3:
@@ -809,7 +851,6 @@ class ProfileXYZ(Profile, SQLSubModel):
return points
def get_nb_wet_areas(self, z):
n_zones = 0
points = self._points
if points[0].z <= z:
@@ -821,10 +862,20 @@ 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):
"""
Determine all water limits for z elevation.
"""
if z in self._get_water_limits_ac_cache:
return self._get_water_limits_ac_cache[z]
points = self._points
if len(points) < 3:
@@ -857,69 +908,94 @@ class ProfileXYZ(Profile, SQLSubModel):
logger.error(f"ERROR in get_all_water_limits_ac")
return [], []
return start, list(reversed(end))
res = start, list(reversed(end))
self._get_water_limits_ac_cache[z] = res
return res
@timer
def get_water_limits(self, z):
if z in self._get_water_limits_cache:
return self._get_water_limits_cache[z]
v = self.get_water_limits_compute(z)
return v
def get_water_limits_compute(self, z):
"""
Determine left and right limits of water elevation.
"""
# Get the index of first point with elevation lesser than water
# elevation (for the right and left river side)
i_left = -1
i_right = -1
i_left = next(
filter(
lambda i: self.point(i).z <= z,
range(self.number_points)
),
-1
)
for i in range(self.number_points):
if self.point(i).z <= z:
i_left = i
break
for i in reversed(range(self.number_points)):
if self.point(i).z <= z:
i_right = i
break
i_right = next(
filter(
lambda i: self.point(i).z <= z,
reversed(range(self.number_points))
),
-1
)
# Interpolate points at river left side
if (i_left > 0):
if abs(self.point(i_left - 1).z - self.point(i_left).z) < 1e-20:
pt_left = self.point(i_left)
pi = self.point(i_left)
pim1 = self.point(i_left - 1)
if abs(pim1.z - pi.z) < 1e-20:
pt_left = pi
else:
fact = (z - self.point(i_left).z) / \
(self.point(i_left - 1).z - self.point(i_left).z)
x = self.point(i_left).x + fact * \
(self.point(i_left - 1).x - self.point(i_left).x)
y = self.point(i_left).y + fact * \
(self.point(i_left - 1).y - self.point(i_left).y)
fact = (z - pi.z) / (pim1.z - pi.z)
x = pi.x + fact * (pim1.x - pi.x)
y = pi.y + fact * (pim1.y - pi.y)
pt_left = PointXYZ(x=x, y=y, z=z, name="wl_left")
else:
pt_left = self.point(0)
# Interpolate points at river right side
if (i_right < self.number_points - 1):
if abs(self.point(i_right + 1).z - self.point(i_right).z) < 1e-20:
pt_right = self.point(i_right)
pi = self.point(i_right)
pip1 = self.point(i_right + 1)
if abs(pip1.z - pi.z) < 1e-20:
pt_right = pi
else:
fact = (z - self.point(i_right).z) / \
(self.point(i_right + 1).z - self.point(i_right).z)
x = self.point(i_right).x + fact * \
(self.point(i_right + 1).x - self.point(i_right).x)
y = self.point(i_right).y + fact * \
(self.point(i_right + 1).y - self.point(i_right).y)
fact = (z - pi.z) / (pip1.z - pi.z)
x = pi.x + fact * (pip1.x - pi.x)
y = pi.y + fact * (pip1.y - pi.y)
pt_right = PointXYZ(x=x, y=y, z=z, name="wl_right")
else:
pt_right = self.point(self.number_points - 1)
return pt_left, pt_right
v = (pt_left, pt_right)
# Put results in cache
self._get_water_limits_cache[z] = v
# Create a generator to improve results data reading speed
yield v
@timer
def compute_tabulation(self):
sorted_points = sorted(self._points, key=lambda p: p.z)
self.tab.z = np.array([p.z for p in sorted_points], np.float64)
self.tab.L = np.zeros(len(self.tab.z), np.float64)
self.tab.A = np.zeros(len(self.tab.z), np.float64)
for i in range(1, len(self.tab.z)):
self.tab.L[i] = self.compute_wet_width(self.tab.z[i])
dx = (self.tab.L[i] + self.tab.L[i-1])/2
dz = self.tab.z[i] - self.tab.z[i-1]
self.tab.A[i] = self.tab.A[i-1] + dz * dx
self.tab_up_to_date = True
@@ -934,7 +1010,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
@@ -1074,16 +1150,10 @@ class ProfileXYZ(Profile, SQLSubModel):
def shift(self, x, y, z):
for p in self._points:
p.x = p.x + x
p.y = p.y + y
p.z = p.z + z
p.shift(x, y, z)
self.modified()
def modified(self):
self.tab_up_to_date = False
self.station_up_to_date = False
def add_npoints(self, npoints):
# add npoints in a profile
for k in range(npoints):
@@ -1104,10 +1174,14 @@ class ProfileXYZ(Profile, SQLSubModel):
p = ProfileXYZ(name=self.name,
rk=self.rk,
reach=self.reach,
status=self._status)
for i, k in enumerate(self.points):
p.insert_point(i, k.copy())
status=self._status,
enabled=self.is_enabled,
interpolated=self.interpolated)
for i, k in enumerate(self.points):
p.insert_point(i, k.cloned_for(p))
p._interpolated = self.interpolated
return p
def purge_aligned_points(self):
@@ -1137,3 +1211,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()
+114 -16
View File
@@ -1,5 +1,5 @@
# Reach.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -52,6 +52,11 @@ class Reach(SQLSubModel):
self._guidelines_is_valid = False
self._guidelines = {}
super(Reach, self).__init__(
id=parent.pamhyr_id,
status=status,
)
@property
def pamhyr_id(self):
return self._parent.pamhyr_id
@@ -60,6 +65,10 @@ class Reach(SQLSubModel):
def _pamhyr_id(self):
return self._parent.pamhyr_id
@_pamhyr_id.setter
def _pamhyr_id(self, id):
return
@classmethod
def _db_create(cls, execute):
return cls._create_submodel(execute)
@@ -71,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
)
@@ -103,6 +114,9 @@ class Reach(SQLSubModel):
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for p in self._profiles:
p._data_traversal(predicate, modifier, data)
@@ -145,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(
@@ -197,13 +219,14 @@ class Reach(SQLSubModel):
self.undelete([profile])
profile.num = index
else:
profile.num = index
self._profiles.insert(index, profile)
gi = self.get_global_profil_index(index)
profile.num = gi
self._profiles.insert(gi, profile)
self.modified()
def delete(self, indexes):
"""Delete some elements in profile list
"""Set some elements as deleted in profile list
Args:
indexes: The list of index to delete
@@ -211,11 +234,12 @@ class Reach(SQLSubModel):
Returns:
Nothing.
"""
gi = [self.get_global_profil_index(i) for i in indexes]
list(
map(
lambda p: p[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
lambda e: e[0] in gi,
enumerate(self._profiles)
)
)
@@ -231,11 +255,12 @@ class Reach(SQLSubModel):
Returns:
Nothing.
"""
gi = [self.get_global_profil_index(i) for i in indexes]
self._profiles = list(
map(
lambda p: p[1],
filter(
lambda e: e[0] not in indexes,
lambda e: e[0] not in gi,
enumerate(self._profiles)
)
)
@@ -243,7 +268,7 @@ class Reach(SQLSubModel):
self.modified()
def delete_profiles(self, profiles):
"""Delete some elements in profile list
"""Set profiles list as deleted
Args:
profiles: The list of profile to delete
@@ -352,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
@@ -360,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(
@@ -471,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)),
@@ -509,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):
@@ -737,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
@@ -931,3 +1002,30 @@ class Reach(SQLSubModel):
nr2._profiles = list(map(lambda p: p.cloned_for(nr2), nr2_profiles))
return nr1, nr2
def get_local_profil_index(self, index):
local_index = -1
for i, p in enumerate(self._profiles):
if not p.is_deleted():
local_index += 1
if i == index:
break
return local_index
def get_global_profil_index(self, index):
local_index = -1
for i, p in enumerate(self._profiles):
if not p.is_deleted():
local_index += 1
if local_index == index:
global_index = i
break
return global_index
def update_rks(self, rks):
for rk, profile in zip(rks, self.profiles):
profile.rk = rk
self.modified()
+1 -1
View File
@@ -1,5 +1,5 @@
# Vector_1d.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# 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
@@ -1,5 +1,5 @@
# HydraulicStructures.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -183,7 +183,6 @@ class BasicHS(SQLSubModel):
bhs._data = BHSValue._db_load(
execute, data
)
print(f"{bhs_pid} : {deleted}")
if deleted:
bhs.set_as_deleted()
@@ -278,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
+1 -1
View File
@@ -1,5 +1,5 @@
# Types.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+9 -1
View File
@@ -1,5 +1,5 @@
# Value.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
)
@@ -1,5 +1,5 @@
# HydraulicStructures.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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)
@@ -274,6 +265,9 @@ class HydraulicStructure(SQLSubModel):
return hs
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM hydraulic_structures " +
f"WHERE pamhyr_id = {self.pamhyr_id} " +
@@ -284,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 (" +
@@ -309,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}"
")"
)
@@ -368,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
@@ -384,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
@@ -411,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
@@ -1,5 +1,5 @@
# HydraulicStructuresList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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()
@@ -1,5 +1,5 @@
# InitialConditions.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
@@ -225,6 +226,8 @@ class Data(SQLSubModel):
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ind = data["ind"]
execute(
@@ -254,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
@@ -396,7 +410,7 @@ class InitialConditions(SQLSubModel):
@reach.setter
def reach(self, new):
self._reach = reach
self._reach = new
self.modified()
@property
@@ -426,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
@@ -503,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()
@@ -537,7 +578,10 @@ class InitialConditions(SQLSubModel):
strickler = 25.0
if not compute_discharge:
discharge = data_discharge[profile.rk]
if profile.rk in data_discharge:
discharge = data_discharge[profile.rk]
else:
discharge = 0.0
else:
discharge = (
# ((width * 0.8)
@@ -583,8 +627,6 @@ class InitialConditions(SQLSubModel):
for data in self._data:
data_height[data["rk"].rk] = data["height"]
print(data_height)
incline = self._reach.reach.get_incline_median_mean()
logger.debug(f"incline = {incline}")
@@ -605,7 +647,10 @@ class InitialConditions(SQLSubModel):
strickler = 25.0
if not compute_height:
height = data_height[profile.rk]
if profile.rk in data_height:
height = data_height[profile.rk]
else:
height = 0.0
else:
if abs(incline) <= 0:
height = 0.0
@@ -656,7 +701,10 @@ class InitialConditions(SQLSubModel):
for profile in profiles:
if not compute_discharge:
d = data_discharge[profile.rk]
if profile.rk in data_discharge:
d = data_discharge[profile.rk]
else:
d = 0.0
else:
d = discharge
elevation = interp(profile.rk,
@@ -1,5 +1,5 @@
# InitialConditionsDict.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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)
)
@@ -1,5 +1,5 @@
# InitialConditionsAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -79,16 +79,12 @@ class InitialConditionsAdisTS(SQLSubModel):
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
if cls.is_table_exists(execute, "initial_conditions_adists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@@ -267,7 +263,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def pollutant(self):
@@ -276,7 +272,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@pollutant.setter
def pollutant(self, pollutant):
self._pollutant = pollutant
self._status.modified()
self.modified()
@property
def concentration(self):
@@ -285,7 +281,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@concentration.setter
def concentration(self, concentration):
self._concentration = concentration
self._status.modified()
self.modified()
@property
def eg(self):
@@ -294,7 +290,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@eg.setter
def eg(self, eg):
self._eg = eg
self._status.modified()
self.modified()
@property
def em(self):
@@ -303,7 +299,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@em.setter
def em(self, em):
self._em = em
self._status.modified()
self.modified()
@property
def ed(self):
@@ -312,7 +308,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@ed.setter
def ed(self, ed):
self._ed = ed
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -321,28 +317,102 @@ class InitialConditionsAdisTS(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
def new(self, index):
n = ICAdisTSSpec(status=self._status)
self._data.insert(index, n)
self._status.modified()
self.modified()
return n
def delete(self, data):
self._data = list(
filter(
lambda x: x not in data,
self._data
list(
map(
lambda x: x.set_as_deleted(),
data
)
)
self._status.modified()
self.modified()
def delete_i(self, indexes):
for ind in indexes:
del self._data[ind]
self._status.modified()
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):
self._data.insert(index, data)
self._status.modified()
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):
split_rk = profile.rk
reach_rks = reach.reach.get_rk()
if len(reach_rks) == 0:
return
lower_reach_rk = min(reach_rks)
upper_reach_rk = max(reach_rks)
parts = [
(reach1.id, lower_reach_rk, split_rk),
(reach2.id, split_rk, upper_reach_rk),
]
specifications = [
specification
for specification in self._data
if (not specification.is_deleted() and
specification.reach == reach.id)
]
for specification in specifications:
begin = specification.start_rk
end = specification.end_rk
if begin is None or end is None:
continue
lower, upper = sorted((begin, end))
reverse = begin > end
for new_reach, part_lower, part_upper in parts:
clipped_lower = max(lower, part_lower)
clipped_upper = min(upper, part_upper)
if clipped_lower >= clipped_upper:
continue
start_rk = clipped_upper if reverse else clipped_lower
end_rk = clipped_lower if reverse else clipped_upper
self._data.append(specification.cloned_for(
new_reach, start_rk, end_rk
))
if len(specifications) != 0:
self.modified()
def get_specs_for_enabled_reaches(self, reaches):
enabled_reaches = {
reach.id
for reach in reaches
if not reach.is_deleted() and reach.is_enable()
}
return [
specification
for specification in self._data
if (not specification.is_deleted() and
(specification.reach in (None, -1) or
specification.reach in enabled_reaches))
]
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# 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
@@ -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
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -31,10 +31,12 @@ class ICAdisTSSpec(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=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
@@ -77,16 +79,12 @@ class ICAdisTSSpec(SQLSubModel):
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == 0 and minor < 1:
if int(release) < 6:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
if cls.is_table_exists(execute, "initial_conditions_adists_spec"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@@ -194,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
@@ -211,7 +209,7 @@ class ICAdisTSSpec(SQLSubModel):
sql = (
"INSERT INTO " +
"initial_conditions_adists_spec(id, deleted, " +
"initial_conditions_adists_spec(pamhyr_id, deleted, " +
"ic_default, name, reach, " +
"start_rk, end_rk, concentration, eg, em, ed, rate, " +
"enabled, scenario) " +
@@ -238,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):
@@ -247,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):
@@ -256,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):
@@ -265,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):
@@ -274,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):
@@ -283,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):
@@ -292,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):
@@ -301,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):
@@ -310,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):
@@ -319,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
@@ -1,5 +1,5 @@
# LateralContribution.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -147,9 +147,9 @@ class Data(SQLSubModel):
it = iter(v)
pid = next(it)
delete = next(it)
data0 = bc._types[0](next(it))
data1 = bc._types[1](next(it))
deleted = next(it)
data0 = lc._types[0](next(it))
data1 = lc._types[1](next(it))
owner_scenario = next(it)
nd = cls(
@@ -196,6 +196,13 @@ class Data(SQLSubModel):
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
def cloned(self):
return Data(
self[0], self[1],
types=self._types,
status=self._status
)
class LateralContribution(SQLSubModel):
_sub_classes = [Data]
@@ -511,6 +518,15 @@ class LateralContribution(SQLSubModel):
def has_reach(self):
return self._reach is not None
def cloned_for(self, reach, begin_section, end_section):
new = type(self)(name=self._name, status=self._status)
new._reach = reach
new._begin_section = begin_section
new._end_section = end_section
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def begin_rk(self):
if self._begin_section is None:
@@ -1,5 +1,5 @@
# LateralContributionList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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()
@@ -1,5 +1,5 @@
# LateralContributionTypes.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
# LateralContributionAdisTS.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -69,14 +69,12 @@ class Data(SQLSubModel):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
if cls.is_table_exists(
execute, "lateral_contribution_data_adists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@@ -132,11 +130,13 @@ class Data(SQLSubModel):
if scenario is None:
return new
lca = data["lca"]
table = execute(
"SELECT pamhyr_id, deleted, data0, data1 " +
"SELECT pamhyr_id, deleted, data0, data1, scenario " +
"FROM lateral_contribution_data_adists " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
f"AND lca = '{lca.id}'"
)
@@ -150,16 +150,16 @@ class Data(SQLSubModel):
data1 = next(it)
owner_scenario = next(it)
data = cls(
lc = cls(
data0, data1,
id=pid, status=status,
owner_scenario=owner_scenario
)
if deleted:
data.set_as_deleted()
lc.set_as_deleted()
loaded.add(pid)
new.append(data)
new.append(lc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
@@ -175,7 +175,7 @@ class Data(SQLSubModel):
execute(
"INSERT INTO " +
"lateral_contribution_adists(pamhyr_id, deleted," +
"lateral_contribution_data_adists(pamhyr_id, deleted," +
"data0, data1, lca, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
@@ -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]
@@ -241,15 +248,13 @@ class LateralContributionAdisTS(SQLSubModel):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "lateral_contribution_adists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created:
return cls._update_submodel(execute, version, data)
@@ -372,16 +377,18 @@ class LateralContributionAdisTS(SQLSubModel):
execute(
"INSERT INTO " +
"lateral_contribution_adists(id, deleted," +
"lateral_contribution_adists(pamhyr_id, deleted, " +
"pollutant, reach, begin_rk, end_rk, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._pollutant}, {self.reach}, " +
f"{self._begin_rk}, {self._end_rk}" +
f"{self._begin_rk}, {self._end_rk}, " +
f"{self._status.scenario_id}" +
")"
)
data["lca"] = self
ind = 0
for d in self._data:
data["ind"] = ind
@@ -406,7 +413,7 @@ class LateralContributionAdisTS(SQLSubModel):
return len(
list(
filter(
lambda el: not el.is_deleted(),
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@@ -452,7 +459,12 @@ class LateralContributionAdisTS(SQLSubModel):
@property
def data(self):
return self._data.copy()
return list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property
def begin_rk(self):
@@ -471,3 +483,69 @@ class LateralContributionAdisTS(SQLSubModel):
def end_rk(self, end_rk):
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)
@property
def _default_1(self):
return self._types[1](0.0)
def add(self, index: int):
value = Data(self._default_0, self._default_1, status=self._status)
self._data.insert(index, value)
self.modified()
return value
def insert(self, index: int, data: Data):
self._data.insert(index, data)
self.modified()
def delete_i(self, indexes):
self._data = list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] not in indexes,
enumerate(self.data)
)
)
)
self.modified()
def index(self, bc):
self._data.index(bc)
def get_i(self, index: int):
return self._data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index: int, column: int, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)
@@ -1,5 +1,5 @@
# LateralContributionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# 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
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
# Edge.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Graph.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Node.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# Point.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# __init__.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+1 -1
View File
@@ -1,5 +1,5 @@
# test_Network.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# 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
-212
View File
@@ -1,212 +0,0 @@
# OutputRKAdists.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class OutputRKAdists(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, reach=None,
rk=None, title: str = "", status=None,
owner_scenario=-1):
super(OutputRKAdists, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._reach = reach
self._rk = rk
self._title = str(title)
self._enabled = True
@property
def reach(self):
return self._reach
@reach.setter
def reach(self, reach_id):
self._reach = reach_id
self._status.modified()
@property
def rk(self):
return self._rk
@rk.setter
def rk(self, profile_id):
self._rk = profile_id
self._status.modified()
@property
def title(self):
return self._title
@title.setter
def title(self, title):
self._title = title
self._status.modified()
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE output_rk_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
reach INTEGER NOT NULL,
rk REAL NOT NULL,
title TEXT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "OutputRKAdists"
table_new = "output_rk_adists"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table_new}_tmp " +
"(pamhyr_id, reach, rk, title, scenario) " +
"SELECT pamhyr_id, reach, rk, title, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table_new, reachs)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
# reach = data["reach"]
# profile = data["profile"]
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, deleted, reach, rk, title, scenario " +
f"FROM output_rk_adists"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
id_reach = next(it)
id_rk = next(it)
title = next(it)
owner_scenario = next(it)
new_output = cls(
id=pid, reach=id_reach,
rk=id_rk, title=title,
status=status,
owner_scenario=owner_scenario,
)
if deleted:
new_output.set_as_deleted()
loaded.add(pid)
new.append(new_output)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM output_rk_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"INSERT INTO " +
"output_rk_adists(pamhyr_id, deleted, " +
"reach, rk, title, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}" +
f"{self._reach}, {self._rk}, " +
f"'{self._db_format(self._title)}', " +
f"{self._status.scenario_id}" +
")"
)
return True
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
+81 -20
View File
@@ -1,5 +1,5 @@
# Pollutants.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -28,6 +28,9 @@ from tools import (
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS import (
BoundaryConditionAdisTS,
)
logger = logging.getLogger()
@@ -101,6 +104,12 @@ class PollutantCharacteristics(SQLSubModel):
self._ac = value
elif key == 8:
self._bc = value
self.propagate_modified()
def propagate_modified(self):
self.modified()
if hasattr(self, "_pollutant") and self._pollutant:
self._pollutant.modified()
@classmethod
def _db_create(cls, execute, ext=""):
@@ -129,16 +138,12 @@ class PollutantCharacteristics(SQLSubModel):
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
if cls.is_table_exists(execute, "Pollutants_characteristics"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@@ -253,6 +258,12 @@ class PollutantCharacteristics(SQLSubModel):
if not self.must_be_saved():
return True
execute(
"DELETE FROM pollutants_characteristics " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
execute(
"INSERT INTO " +
"pollutants_characteristics(pamhyr_id, deleted, " +
@@ -275,7 +286,10 @@ class PollutantCharacteristics(SQLSubModel):
class Pollutants(SQLSubModel):
_sub_classes = [PollutantCharacteristics]
_sub_classes = [
PollutantCharacteristics,
# BoundaryConditionAdisTS
]
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
@@ -293,6 +307,11 @@ class Pollutants(SQLSubModel):
self._enabled = True
self._data = []
self._boundary_conditions_adists = []
@property
def boundary_conditions_adists(self):
return self._boundary_conditions_adists
@property
def name(self):
@@ -301,18 +320,37 @@ class Pollutants(SQLSubModel):
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def data(self):
return self._data
@data.setter
def data(self, data):
self._data = [
PollutantCharacteristics(
type=int(data[0]),
diametre=float(data[1]),
rho=float(data[2]),
porosity=float(data[3]),
cdc_riv=float(data[4]),
cdc_cas=float(data[5]),
apd=float(data[6]),
ac=float(data[7]),
bc=float(data[8]),
pollutant=self,
status=self._status
)
]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE pollutants{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
name TEXT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}
@@ -329,14 +367,20 @@ class Pollutants(SQLSubModel):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) <= 1:
if int(release) < 7:
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "Pollutants"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
created = True
if major == "0" and int(minor) < 2:
if not created:
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "2":
if int(release) < 6:
execute(f"ALTER TABLE pollutants " +
f"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE")
# cls._db_update_to_0_2_5(execute, data)
# # created = True
if not created:
return cls._update_submodel(execute, version, data)
@@ -375,7 +419,8 @@ class Pollutants(SQLSubModel):
return new
table = execute(
"SELECT pamhyr_id, deleted, name FROM pollutants " +
"SELECT pamhyr_id, deleted, enabled, name, scenario " +
"FROM pollutants " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
@@ -386,18 +431,30 @@ class Pollutants(SQLSubModel):
pid = next(it)
deleted = (next(it) == 1)
enabled = (next(it) == 1)
name = next(it)
owner_scenario = next(it)
new_pollutant = cls(
id=pid, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
if deleted:
new_pollutant.set_as_deleted()
if not enabled:
new_pollutant.enabled = False
data["pollutant"] = new_pollutant
new_pollutant._data = PollutantCharacteristics._db_load(
execute, data.copy()
execute, data=data
)
new_pollutant._boundary_conditions_adists = (
BoundaryConditionAdisTS._db_load(
execute,
data=data
)
)
loaded.add(pid)
@@ -421,16 +478,17 @@ class Pollutants(SQLSubModel):
f"AND scenario = {self._status.scenario_id}"
)
execute(
f"DELETE FROM pollutants_characteristics " +
"DELETE FROM pollutants_characteristics " +
f"WHERE pollutant = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
execute(
"INSERT INTO " +
"pollutants(pamhyr_id, deleted, name, scenario) " +
"pollutants(pamhyr_id, deleted, enabled, name, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._db_format(self._enabled)}, " +
f"'{self._db_format(self._name)}', " +
f"{self._status.scenario_id}" +
")"
@@ -439,6 +497,9 @@ class Pollutants(SQLSubModel):
for d in self._data:
ok &= d._db_save(execute, data)
for bc in self._boundary_conditions_adists:
ok &= bc._db_save(execute, data)
return ok
def _data_traversal(self,
+1 -1
View File
@@ -19,7 +19,7 @@
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Pollutants.Pollutants import Pollutants
+2 -4
View File
@@ -1,5 +1,5 @@
# Reservoir.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -263,8 +263,6 @@ class Reservoir(SQLSubModel):
cls._db_update_to_0_2_0_set_node_pid(execute, table, nodes)
Scenario.update_db_add_scenario(execute, table)
print(execute(f"SELECT * FROM {table}"))
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
@@ -314,7 +312,7 @@ class Reservoir(SQLSubModel):
new_reservoir._node = next(
filter(
lambda n: n.id == node_id, data["nodes"]
)
), None
)
data["reservoir"] = new_reservoir
+1 -1
View File
@@ -1,5 +1,5 @@
# ReservoirList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+289 -10
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@ import numpy as np
from copy import deepcopy
from datetime import datetime
from tools import timer
from Model.Scenario import Scenario
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Results.River.River import River
@@ -155,11 +157,183 @@ class AdditionalData(SQLSubModel):
return True
class TableData(SQLSubModel):
_sub_classes = []
def __init__(self, id=-1, study=None,
legend="", data=None,
owner_scenario=-1):
super(TableData, self).__init__(
id=id, status=study.status,
owner_scenario=owner_scenario
)
self._study = study
self._legend = legend
self._data = data
@property
def data(self):
return self._data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results_table_data{ext} (
{cls.create_db_add_pamhyr_id()},
result INTEGER NOT NULL,
legend TEXT NOT NULL,
unit TEXT NOT NULL,
data_shape_x INTEGER NOT NULL,
data_shape_y INTEGER NOT NULL,
data_shape_z INTEGER DEFAULT -1,
data BLOB NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(result) REFERENCES results(pamhyr_id),
PRIMARY KEY(pamhyr_id, result, legend, scenario)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
if not cls.is_table_exists(execute, "results_table_data"):
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
res = {}
study = data['study']
status = data['status']
scenario = data["scenario"]
result = data["result"]
rid = data["result_pid"]
table = execute(
"SELECT pamhyr_id, " +
"legend, unit, " +
"data_shape_x, data_shape_y, data_shape_z, " +
"data, " +
"scenario " +
"FROM results_table_data " +
f"WHERE scenario = {scenario.id} " +
f"AND result = {rid}"
)
for v in table:
it = iter(v)
pid = next(it)
legend = next(it)
unit = next(it)
data_shape_x = next(it)
data_shape_y = next(it)
data_shape_z = next(it)
data = next(it)
owner_scenario = next(it)
dtype = cls._unit_to_dtype(unit)
np_data = np.frombuffer(data, dtype=dtype)
if data_shape_y == -1:
np_data = np.reshape(np_data, data_shape_x)
elif data_shape_z == -1:
np_data = np.reshape(np_data, (data_shape_x,
data_shape_y))
else:
np_data = np.reshape(np_data, (data_shape_x,
data_shape_y,
data_shape_z))
new_table = cls(study=study,
legend=legend, data=np_data,
owner_scenario=owner_scenario)
res[legend] = new_table
return res
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
return
shape_x = -1
shape_y = -1
shape_z = -1
shape = self._data.shape
if len(shape) == 2:
shape_x, shape_y = shape
elif len(shape) == 1:
shape_x, = shape
elif len(shape) == 3:
shape_x, shape_y, shape_z = shape
else:
logger.error(
f"Failed to parse numpy shape: {shape}"
)
execute(
"INSERT INTO " +
"results_table_data (pamhyr_id, result, " +
"legend, unit, " +
"data_shape_x, data_shape_y, data_shape_z, " +
"data, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, data["result"],
self._legend, self._dtype_to_unit(self._data.dtype),
shape_x, shape_y, shape_z,
self._data,
self._owner_scenario
)
return True
@classmethod
def _unit_to_dtype(cls, unit):
t = {
"i32": np.int32, "i64": np.int64,
"u32": np.uint32, "u64": np.uint64,
"f32": np.float32, "f64": np.float64,
}
return t[unit]
def _dtype_to_unit(self, dtype):
t = {
np.dtype(np.int32): "i32", np.dtype(np.int64): "i64",
np.dtype(np.uint32): "u32", np.dtype(np.uint64): "u64",
np.dtype(np.float32): "f32", np.dtype(np.float64): "f64",
}
return t[dtype]
def table(self):
return self._data
def __len__(self):
return len(self._data)
def __getitem__(self, key):
if isinstance(key, tuple):
return self._data[key[0], key[1]]
self._data[key]
class Results(SQLSubModel):
_sub_classes = [River, AdditionalData]
_sub_classes = [River, TableData, AdditionalData]
def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0"):
repertory="", name="0", is_ensemble=False):
super(Results, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
@@ -167,21 +341,30 @@ class Results(SQLSubModel):
self._name = name
self._study = study
self._river = River(self._study)
# Solver readers add their own reaches; ensembles need them upfront.
self._river = River(self._study, self, init=is_ensemble)
self._solver = solver
self._repertory = repertory
self._is_ensemble = is_ensemble
self._meta_data = {
# Keep results creation date
"creation_date": datetime.now(),
"study_revision": study.status.version,
"additional_data": [],
"table": {},
}
self._ts_index = {}
if solver is not None:
self.set("solver_type", solver._type)
def is_ensemble(self):
return self._is_ensemble
@property
def date(self):
date = self._meta_data["creation_date"]
@@ -195,6 +378,7 @@ class Results(SQLSubModel):
def study(self):
return self._study
@timer
def set(self, key, value):
self._meta_data[key] = value
@@ -212,6 +396,16 @@ class Results(SQLSubModel):
def get(self, key):
return self._meta_data[key]
def set_timestamp_index(self, index):
self._ts_index = index
def get_timestamp_id(self, ts):
return self._ts_index[ts]
def compute_timestamp_index(self):
for i, ts in enumerate(self._meta_data["timestamps"]):
self._ts_index[ts] = i
def reload(self):
return self._solver.results(
self._study,
@@ -219,6 +413,11 @@ class Results(SQLSubModel):
qlog=None,
)
def bufferize(self, key):
if self.is_valid:
for reach in self._river._reachs:
reach.bufferize(self._meta_data["timestamps"], key)
def timestamps_to_struct(self):
ts = self._meta_data["timestamps"]
sf = ">" + ''.join(itertools.repeat("d", len(ts)))
@@ -233,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)
@@ -261,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
@@ -273,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}"
@@ -293,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)
@@ -303,12 +523,24 @@ 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
table_data = TableData._db_load(execute, data)
new_results.set("table", table_data)
sf = ">" + ''.join(itertools.repeat("d", nb_timestamps))
ts = struct.unpack(sf, timestamps_bytes)
new_results.set("timestamps", ts)
data["timestamps"] = sorted(ts)
new_results.compute_timestamp_index()
data["parent"] = new_results
new_results._river = River._db_load(execute, data)
new_results.set(
@@ -316,6 +548,20 @@ class Results(SQLSubModel):
AdditionalData._db_load(execute, data)
)
# Update global index
i = 0
for reach in new_results._river.reachs:
nb = len(reach.profiles)
reach.set_global_index(range(i, i + nb))
i += nb
if "Z" in table_data:
for ts in data["timestamps"]:
new_results._river.update_water_limits(
ts,
table_data["Z"][new_results.get_timestamp_id(ts), :]
)
yield (solver_type, new_results)
def _db_save_clear(self, execute, solver_type, data=None):
@@ -338,15 +584,26 @@ class Results(SQLSubModel):
f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}"
)
execute(
"DELETE FROM results_table_data " +
f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}"
)
execute(
"DELETE FROM results_add_data " +
f"WHERE scenario = {self._owner_scenario} " +
f"AND result = {pid}"
)
def _is_valid(self):
return "timestamps" in self._meta_data
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
return
return True
if not self._is_valid():
return False
pid = self._pamhyr_id
if self._solver is None:
@@ -361,21 +618,43 @@ 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
self._river._db_save(execute, data)
tables = self.get("table")
for legend in tables:
tables[legend]._db_save(execute, data)
for add_data in self.get("additional_data"):
add_data._db_save(execute, data)
return True
def new_table_data(self, legend, data):
return TableData(
study=self._study,
legend=legend,
data=data,
owner_scenario=self._owner_scenario
)
+6 -2
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,12 +21,13 @@ import os
from copy import deepcopy
from datetime import datetime
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Results.River.RiverAdisTS import River
logger = logging.getLogger()
class Results(object):
class Results(SQLSubModel):
def __init__(self, study=None, solver=None,
repertory="", name="0", type_pol=None):
self._study = study
@@ -90,3 +91,6 @@ class Results(object):
self._repertory,
qlog=None,
)
def _db_save(self, execute, data=None):
return False
+116 -28
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,11 +14,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import types
import struct
import logging
import itertools
import numpy as np
from tools import flatten
from tools import flatten, timer
from functools import reduce
from datetime import datetime
@@ -29,15 +31,17 @@ logger = logging.getLogger()
class Profile(SQLSubModel):
def __init__(self, profile, study):
def __init__(self, profile, study, parent):
super(Profile, self).__init__(
id=-1, status=study.status,
owner_scenario=study.status.scenario.id
)
self._parent = parent
self._study = study
self._profile = profile # Source profile in the study
self._data = {} # Dict of dict {<ts>: {<key>: <value>, ...}, ...}
self._global_index = -1
def __len__(self):
return len(self._data)
@@ -54,15 +58,26 @@ class Profile(SQLSubModel):
def geometry(self):
return self._profile
@property
def global_index(self):
return self._global_index
@global_index.setter
def global_index(self, id):
self._global_index = id
@timer
def set(self, timestamp, key, data):
if timestamp not in self._data:
self._data[timestamp] = {}
self._data[timestamp][key] = data
@timer
def get_ts(self, timestamp):
return self._data[timestamp]
@timer
def get_key(self, key):
res = list(
map(lambda ts: self._data[ts][key],
@@ -70,15 +85,30 @@ class Profile(SQLSubModel):
)
return res
@timer
def get_ts_key(self, timestamp, key):
if timestamp in self._data:
if key in self._data[timestamp]:
return self._data[timestamp][key]
v = self._data[timestamp][key]
# If is a generator, compute value(s)
if isinstance(v, types.GeneratorType):
v = next(v)
self._data[timestamp][key] = v
return v
return None
def has_sediment(self):
def has_sediment_layers(self):
return any(map(lambda ts: "sl" in self._data[ts], self._data))
def has_bedload(self):
# 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=""):
execute(f"""
@@ -110,9 +140,9 @@ class Profile(SQLSubModel):
@classmethod
def _db_load(cls, execute, data=None):
new = {}
status = data['status']
parent = data['parent']
study = data['study']
reach = data['reach']
profile = data['profile']
@@ -120,11 +150,14 @@ class Profile(SQLSubModel):
loaded = data['loaded_pid']
timestamps = data['timestamps']
new = cls(profile, study, parent)
values = execute(
"SELECT pamhyr_id, result, key, " +
"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}"
)
@@ -139,12 +172,6 @@ class Profile(SQLSubModel):
data = next(it)
owner_scenario = next(it)
if profile not in new:
new_data = cls(profile, study)
new[profile] = new_data
else:
new_data = new[profile]
if key in ["Z", "Q", "V"]:
sf = ">" + ''.join(itertools.repeat("f", len_data))
len_values = len(values)
@@ -157,11 +184,11 @@ class Profile(SQLSubModel):
)
for timestamp, value in zip(timestamps, values):
new_data.set(timestamp, key, value)
new.set(timestamp, key, value)
if key == "Z":
new_data.update_water_limits(timestamp, value)
new.update_water_limits(timestamp, value)
return list(new.values())
return new
@classmethod
def _db_load_data_sl_format(cls, values, len_data, timestamps):
@@ -253,20 +280,23 @@ class Profile(SQLSubModel):
class Reach(SQLSubModel):
_sub_classes = [Profile]
def __init__(self, reach, study, 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
)
self._parent = parent
self._study = study
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),
reach.profiles
lambda p: Profile(p, self._study, self._parent),
profiles
)
)
@@ -276,6 +306,10 @@ class Reach(SQLSubModel):
)
)
self._global_index = (-1, -1)
self._buffers = {}
def __len__(self):
return len(self._profiles)
@@ -295,14 +329,42 @@ class Reach(SQLSubModel):
def profile_mask(self):
return self._profile_mask
@property
def buffers(self):
return self._buffers
def profile(self, id):
return self._profiles[id]
def set_global_index(self, indexs):
self._global_index = (indexs[0], indexs[-1])
for profile, ind in zip(self._profiles, indexs):
profile.global_index = ind
@property
def global_index(self):
return self._global_index
@timer
def set(self, profile_id, timestamp, key, data):
self._profiles[profile_id].set(timestamp, key, data)
def has_sediment(self):
return any(map(lambda profile: profile.has_sediment(), self._profiles))
return any(map(lambda profile: profile.has_sediment_layers(),
self._profiles))
def has_bedload(self):
return any(map(lambda profile: profile.has_bedload(), self._profiles))
def bufferize(self, timestamps, key):
self._buffers[key] = np.zeros((len(timestamps), len(self)))
for i, p in enumerate(self._profiles):
self._buffers[key][:, i] = p.get_key(key)
def update_water_limits(self, timestamp, z_data):
for profile, z in zip(self._profiles, z_data):
profile.update_water_limits(timestamp, z)
@classmethod
def _db_create(cls, execute, ext=""):
@@ -317,12 +379,20 @@ class Reach(SQLSubModel):
reach = data["reach"]
new_reach = cls(
data["reach"], data["study"], with_init=False
data["reach"], data["study"], data["parent"],
with_init=False
)
for i, profile in enumerate(reach.profiles):
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
@@ -336,16 +406,20 @@ class Reach(SQLSubModel):
class River(SQLSubModel):
_sub_classes = [Reach]
def __init__(self, study):
def __init__(self, study, parent, init=True):
super(River, self).__init__(
id=-1, status=study.status,
owner_scenario=study.status.scenario.id
)
self._study = study
self._parent = parent
# Dict with timestamps as key
self._reachs = []
if init:
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
def __len__(self):
return len(self._reachs)
@@ -360,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)
new = Reach(
reachs[reach_id].reach, self._study, self._parent,
profiles=profiles
)
self._reachs.append(new)
return new
@@ -376,6 +453,15 @@ class River(SQLSubModel):
)
)
def update_water_limits(self, timestamp, data):
for reach in self._reachs:
ind = reach.global_index
reach.update_water_limits(
timestamp,
data[ind[0]:ind[1] + 1]
)
@classmethod
def _db_create(cls, execute, ext=""):
return cls._create_submodel(execute)
@@ -387,13 +473,15 @@ class River(SQLSubModel):
@classmethod
def _db_load(cls, execute, data=None):
study = data["study"]
new_river = cls(study)
parent = data["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
+11 -5
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
+150 -18
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr river model
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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,7 +58,18 @@ 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
logger = logging.getLogger()
@@ -180,6 +190,9 @@ class RiverNode(Node):
return nodes
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"INSERT OR REPLACE INTO river_node(" +
"pamhyr_id, deleted, name, x, y, scenario" +
@@ -361,7 +374,7 @@ class RiverReach(Edge):
)
new.enable(enable=enabled)
if deleted:
nd.set_as_deleted()
new.set_as_deleted()
data["reach"] = new
new._reach = Reach._db_load(execute, data)
@@ -377,6 +390,9 @@ class RiverReach(Edge):
return reachs
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"INSERT OR REPLACE INTO " +
"river_reach(" +
@@ -386,7 +402,7 @@ class RiverReach(Edge):
"VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"{self._db_format(self.is_enable())},"
f"{self._db_format(self.is_enable())}," +
f"{self.node1.pamhyr_id}, {self.node2.pamhyr_id}, " +
f"{self._status.scenario_id}" +
")"
@@ -462,14 +478,17 @@ class River(Graph):
HydraulicStructureList,
AddFileList,
REPLineList,
OutputRKAdistsList,
PollutantsList,
InitialConditionsAdisTSList,
BoundaryConditionsAdisTSList,
LateralContributionsAdisTSList,
D90AdisTSList,
DIFAdisTSList,
InitialConditionsTemperatureList,
BoundaryConditionsTemperatureList,
WeatherParametersList,
GeoTIFFList,
EnsembleList,
Results
]
@@ -496,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)
@@ -506,9 +524,19 @@ 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)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {}
@classmethod
@@ -543,7 +571,7 @@ class River(Graph):
new._nodes = RiverNode._db_load(
execute, data
)
data["nodes"] = new.nodes()
data["nodes"] = new._nodes
data['loaded_pid'] = set()
new._edges = RiverReach._db_load(
@@ -604,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)
@@ -621,8 +645,18 @@ 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)
return new
def _db_load_results(self, execute, data=None):
@@ -648,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)
@@ -656,8 +689,14 @@ 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)
for solv_type in self.results:
objs.append(self.results[solv_type])
@@ -728,13 +767,15 @@ 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._geotiff,
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
self._WeatherParameters,
self._geotiff, self._ensembles,
]
for solver in self._parameters:
@@ -839,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
@@ -867,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]
@@ -923,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
@@ -939,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
+137 -44
View File
@@ -17,21 +17,63 @@
# -*- coding: utf-8 -*-
import os
import logging
from tools import logger_exception
from tools import logger_exception, timer, flatten
from Model.Tools.PamhyrDB import SQLSubModel
logger = logging.getLogger()
class Scenario(SQLSubModel):
_id_cnt = 0
_sub_classes = []
tables_with_deleted_column = [
# Adists
"boundary_condition_adists", "boundary_condition_data_adists",
"lateral_contribution_adists", "lateral_contribution_data_adists",
"initial_conditions_adists", "initial_conditions_adists_spec",
"d90_adists", "d90_adists_spec",
"dif_adists_spec",
"pollutants", "pollutants_characteristics",
# Hydraulic
"additional_files",
"boundary_condition", "boundary_condition_data",
"lateral_contribution", "lateral_contribution_data",
"friction", "stricklers",
"hydraulic_structures",
"hydraulic_structures_basic",
"initial_conditions",
"sedimentary_layer", "sedimentary_layer_layer",
"reservoir", "reservoir_data",
"rep_lines",
"geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node",
"geotiff", "reservoir", "reservoir_data",
# Temperature
"boundary_condition_temperature",
"boundary_condition_data_temperature",
"initial_conditions_temperature",
"initial_conditions_temperature_spec",
"weather_parameters", "weather_parameters_data",
"ensemble",
]
related_tables = tables_with_deleted_column + [
"dif_adists",
"solver_parameter",
"hydraulic_structures_basic_value",
"results", "results_data", "results_add_data",
]
def __init__(self,
id: int = -1,
name: str = "",
description: str = "",
x: int = 0.0, y: int = 0.0,
x: int = 1000.0, y: int = 1000.0,
is_ensemble: bool = False,
revision: int = 0,
parent=None):
super(Scenario, self).__init__()
@@ -41,6 +83,7 @@ class Scenario(SQLSubModel):
self._x, self._y = x, y
self._name = name
self._description = description
self._is_ensemble = is_ensemble
self._revision = revision
self._parent = parent
@@ -63,6 +106,7 @@ class Scenario(SQLSubModel):
y REAL NOT NULL DEFAULT 1000,
name TEXT NOT NULL,
description TEXT NOT NULL,
is_ensemble BOOL DEFAULT FALSE,
revision INTEGER NOT NULL,
parent_id INTEGER REFERENCES scenario(id)
)
@@ -98,6 +142,13 @@ class Scenario(SQLSubModel):
cls._db_create(execute)
cls._db_add_default(execute)
if major == "0" and int(minor) == 2:
if int(release) < 8:
execute(
f"ALTER TABLE scenario " +
"ADD COLUMN is_ensemble BOOL DEFAULT FALSE"
)
return True
@classmethod
@@ -118,7 +169,8 @@ class Scenario(SQLSubModel):
scenarios = {}
table = execute(
"SELECT id, x, y, name, description, revision, parent_id " +
"SELECT id, x, y, name, description, " +
" is_ensemble, revision, parent_id " +
"FROM scenario " +
"ORDER BY id ASC"
)
@@ -130,6 +182,7 @@ class Scenario(SQLSubModel):
x, y = next(it), next(it)
name = next(it)
desc = next(it)
is_ens = next(it)
revi = next(it)
parent = next(it)
@@ -139,7 +192,9 @@ class Scenario(SQLSubModel):
new = cls(
id=id, x=x, y=y,
name=name, description=desc,
revision=revi, parent=parent
is_ensemble=is_ens,
revision=revi,
parent=parent
)
scenarios[id] = new
@@ -149,21 +204,18 @@ class Scenario(SQLSubModel):
if self.is_deleted():
return self.drop_all(execute)
parent = 'NULL'
parent = None
if self.parent is not None:
parent = self.parent._id
execute(
"INSERT OR REPLACE INTO " +
"scenario(id, x, y, name, description, revision, parent_id) " +
"VALUES (" +
f"{self._id}, " +
f"{self.x}, {self.y}, " +
f"'{self._db_format(self.name)}', " +
f"'{self._db_format(self.description)}', " +
f"{self._revision}, " +
f"{parent}" +
")"
"scenario(id, x, y, name, description, " +
" is_ensemble, revision, parent_id) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
self._id, self.x, self.y,
self.name, self.description, self._is_ensemble,
self._revision, parent
)
return True
@@ -171,29 +223,7 @@ class Scenario(SQLSubModel):
def drop_all(self, execute):
execute(f"DELETE FROM scenario WHERE id = {self.id}")
tables = [
# Adists
"output_rk_adists",
"boundary_condition_adists", "boundary_condition_data_adists",
"lateral_contribution_adists", "lateral_contribution_data_adists",
"initial_conditions_adists",
"d90_adists", "dif_adists",
"pollutants", "pollutants_characteristics",
# Hydraulic
"additional_files",
"boundary_condition", "boundary_condition_data",
"lateral_contribution", "lateral_contribution_data",
"friction", "stricklers",
"hydraulic_structures",
"hydraulic_structures_basic", "hydraulic_structures_basic_value",
"initial_conditions",
"sedimentary_layer", "sedimentary_layer_layer",
"reservoir", "reservoir_data",
"rep_lines",
"solver_parameter",
"geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node",
]
tables = self.related_tables
for table in tables:
execute(
@@ -203,6 +233,67 @@ class Scenario(SQLSubModel):
return True
def get_parent_branch(self):
def aux(scenario, acc):
if scenario is None:
return acc
return aux(scenario.parent, [scenario.id] + acc)
return aux(self, [])
@timer
def clean_deleted_data(self, execute):
tables = self.tables_with_deleted_column
branch = self.get_parent_branch()
all_ids = []
for table in tables:
if self.parent is None:
# This scenario is the default scenario, so we can
# delete all data marked as deleted
execute(
f"DELETE FROM {table} " +
f"WHERE scenario = {self.id} " +
"AND deleted = TRUE"
)
continue
# Select pamhyr_id for each deleted data in this scenario
# who do not exists into parents scenarios
ids = execute(
f"""
SELECT pamhyr_id FROM {table}
WHERE deleted = TRUE
AND scenario = {self.id}
AND pamhyr_id NOT IN (
SELECT pamhyr_id FROM {table}
WHERE scenario IN
({', '.join(map(str, branch[:-1]))})
)
""", fetch_one=False
)
if ids is None or len(ids) == 0:
continue
ids = flatten(ids)
logger.debug(
f"({self.name}) Drop deleted data into '{table}' : {ids}"
)
execute(
f"DELETE FROM {table} " +
f"WHERE scenario = {self.id} " +
"AND pamhyr_id IN " +
f"({', '.join(map(str, ids))})"
)
all_ids += ids
return all_ids
@property
def id(self):
return self._id
@@ -249,6 +340,9 @@ class Scenario(SQLSubModel):
def parent(self):
return self._parent
def is_ensemble(self):
return self._is_ensemble
def set_pos(self, x, y):
self._x = x
self._y = y
@@ -266,17 +360,16 @@ class Scenario(SQLSubModel):
return self.name
if key == "description":
return self.description
if key == "is_ensemble":
return self.is_ensemble()
if key == "parent":
return self.parent
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)
+16 -2
View File
@@ -16,6 +16,8 @@
# -*- coding: utf-8 -*-
from functools import reduce
from tools import logger_exception
from Model.Tools.PamhyrDict import PamhyrModelDict
@@ -55,8 +57,11 @@ class Scenarios(PamhyrModelDict):
return None
def new(self, parent):
new = Scenario(parent=parent)
def new(self, parent, is_ensemble=False):
new = Scenario(
parent=parent,
is_ensemble=is_ensemble
)
self.set(new._id, new)
return new
@@ -78,3 +83,12 @@ class Scenarios(PamhyrModelDict):
self._dict[key].set_as_deleted()
self._status.modified()
def is_leaf(self, scenario):
return not reduce(
lambda acc, s: (
acc or (not s.is_deleted() and s.parent is scenario)
),
self._dict.values(),
False
)
+1 -1
View File
@@ -1,5 +1,5 @@
# Serializable.py -- Pamhyr pickle abstract class
# Copyright (C) 2023-2025 INRAE
# 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
@@ -1,5 +1,5 @@
# SolverParametersList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -216,6 +216,9 @@ class Parameter(SQLSubModel):
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
ind = data["ind"]
solver = data["solver"]
@@ -312,6 +315,14 @@ 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
+14 -1
View File
@@ -1,5 +1,5 @@
# Status.py -- Pamhyr model status class
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -34,6 +34,7 @@ class StudyStatus(object):
self._status = Status.EDIT
self._scenario = scenario
self._saved = True
self._loading = False
@property
def scenario_id(self):
@@ -86,6 +87,15 @@ class StudyStatus(object):
def is_saved(self):
return self._saved
def is_loading(self):
return self._loading
def start_loading(self):
self._loading = True
def stop_loading(self):
self._loading = False
def save(self):
logger.debug("model status set as saved")
self._saved = True
@@ -97,6 +107,9 @@ class StudyStatus(object):
self._saved = True
def modified(self):
if self._loading:
return
if self._saved:
self.version += 1
+1 -1
View File
@@ -1,5 +1,5 @@
# Stricklers.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
+5 -2
View File
@@ -1,5 +1,5 @@
# StricklersList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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()
+174 -20
View File
@@ -1,5 +1,5 @@
# Study.py -- Pamhyr Study class
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,15 +21,26 @@ 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
from Model.Tools.PamhyrDict import PamhyrModelDict
from Model.Tools.PamhyrDB import SQLModel
from Model.Scenarios import Scenarios
from Model.Scenario import Scenario
from Model.Status import StudyStatus
from Model.Except import NotImplementedMethodeError
from Model.River import River
from Model.TimeShift import shift_river_times, shift_database_times
from Model.Geometry.Reach import Reach
from Model.Ensembles.DistributionList import DistributionList
from Model.HydraulicStructures.HydraulicStructures import (
HydraulicStructure
)
from Model.HydraulicStructures.Basic.HydraulicStructures import (
BasicHS
)
from Checker.Study import *
@@ -37,10 +48,11 @@ logger = logging.getLogger()
class Study(SQLModel):
_version = "0.2.3"
_version = "0.2.10"
_sub_classes = [
Scenario,
DistributionList,
River,
]
@@ -63,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:
@@ -75,6 +88,9 @@ class Study(SQLModel):
self.scenarios = Scenarios(status=self.status)
self.scenarios[0] = s0
self.status.scenario = s0
self._ens_distributions = DistributionList(status=self.status)
self._river = River(status=self.status)
else:
self._init_db_file(filename, is_new=False)
@@ -189,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
@@ -208,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
@@ -281,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
@@ -291,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;"
@@ -379,12 +411,19 @@ class Study(SQLModel):
lambda a, s: a or (s.parent is scenario),
new.scenarios.lst,
False
):
) or scenario.is_ensemble():
new.status.set_as_read_only()
data["scenario"] = scenario
data["loaded_pid"] = set()
# Get ensemble distribution
new._ens_distributions = DistributionList._db_load(
sql_exec, data=data
)
data["ens_distributions"] = new._ens_distributions
# Load river data
new._river = River._db_load(
sql_exec, data=data
@@ -445,12 +484,94 @@ class Study(SQLModel):
)
progress()
if self._pending_time_offset:
self._db.execute('SAVEPOINT shift_time_origin')
try:
shift_database_times(self._db, self._pending_time_offset)
self._db.execute('RELEASE SAVEPOINT shift_time_origin')
except Exception:
self._db.execute('ROLLBACK TO SAVEPOINT shift_time_origin')
self._db.execute('RELEASE SAVEPOINT shift_time_origin')
raise
self._pending_time_offset = 0.0
if self.status.scenario.is_ensemble():
lst = [
self.scenarios, self._ens_distributions,
self._river._ensembles,
]
for solv in self._river._results:
lst.append(self._river._results[solv])
self._save_submodel(
lst, data=progress
)
progress()
self.commit()
return
self._save_submodel(
[self.scenarios, self._river],
[
self.scenarios, self._ens_distributions,
self._river
],
data=progress
)
# Clear DB for current scenario
ids = self.status.scenario.clean_deleted_data(self.execute)
self.memory_clean(ids)
progress()
self.commit()
@timer
def memory_clean(self, ids):
if len(ids) == 0:
return
reach_class = Reach
hs_classes = [HydraulicStructure, BasicHS]
list_classes = set(PamhyrModelList.__subclasses__())
dict_classes = set(PamhyrModelDict.__subclasses__())
def modifier(obj, data):
t = type(obj)
if t is reach_class:
obj._profiles = list(
filter(
lambda el: el.id not in ids,
obj._profiles
)
)
elif t in hs_classes:
obj._data = list(
filter(
lambda el: el.id not in ids,
obj._data
)
)
elif t in dict_classes:
new = {}
for key in obj._dict:
if obj._dict[key].id not in ids:
new[key] = obj._dict[key]
obj._dict = new
elif t in list_classes:
obj._lst = list(
filter(
lambda el: el.id not in ids,
obj._lst
)
)
self.river._data_traversal(
lambda o, d: True, modifier, {}
)
def sql_save_request_count(self, *args, **kargs):
return self._count()
@@ -459,7 +580,10 @@ class Study(SQLModel):
[self.scenarios, self._river]
)
logger.debug(cnt)
return cnt + 7
# HACK: 8 is the number of SQL request do and dont counted in
# submodel save_count (let's count sql execute into the
# '_save' method)
return cnt + 8
def close(self):
"""Close db connection
@@ -485,7 +609,8 @@ class Study(SQLModel):
self.status.scenario = scenario
if reduce(
lambda a, s: a or (s.parent is scenario),
lambda a, s: a or (s.parent is scenario
and not s.is_deleted()),
self.scenarios.lst,
False
):
@@ -508,18 +633,27 @@ class Study(SQLModel):
data = {
"status": self.status,
"loaded_pid": set(),
"scenario": scenario
"scenario": scenario,
"ens_distributions": self._ens_distributions,
}
# Load river data
river = River._db_load(
sql_exec, data=data
)
data["study"] = self
river._db_load_results(sql_exec, data=data)
self.status.start_loading()
try:
# Load river data
river = River._db_load(
sql_exec, data=data
)
data["study"] = self
river._db_load_results(sql_exec, data=data)
finally:
self.status.stop_loading()
self.status.scenario = old_scenario
self._river_scenario_cache[scenario] = river
self.status.scenario = old_scenario
if self._pending_time_offset:
shift_river_times(river, self._pending_time_offset)
# FIXME: Disable scenario cache to save memory usage
# self._river_scenario_cache[scenario] = river
return river
@@ -577,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(
@@ -588,6 +723,25 @@ class Study(SQLModel):
self.status.set_as_editable()
return new
def create_ensemble_scenario(self, pos=None, switch=True):
new = self.scenarios.new(
self.status.scenario,
is_ensemble=True
)
new.name = self.status.scenario.name + " (ensemble)"
if pos is not None:
new.set_pos(pos.x(), pos.y())
else:
new.set_pos(self.status.scenario.x + 100,
self.status.scenario.y)
if switch:
self.status.scenario = new
self.status.set_as_read_only()
return new
@property
def results(self):
return self._river.results
+82
View File
@@ -0,0 +1,82 @@
"""Shift temporal series when preserving calendar dates across a new origin."""
from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition
from Model.LateralContribution.LateralContribution import LateralContribution
from Model.WeatherParameters.WeatherParameters import WeatherParameters
from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS import (
BoundaryConditionAdisTS
)
from Model.LateralContributionsAdisTS.LateralContributionAdisTS import (
LateralContributionAdisTS
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature import (
BoundaryConditionTemperature
)
def shift_river_times(river, offset):
changes = []
seen = set()
def collect(obj, data):
temporal = isinstance(obj, (
BoundaryConditionAdisTS,
LateralContributionAdisTS,
BoundaryConditionTemperature
))
if isinstance(obj, (
BoundaryCondition, LateralContribution, WeatherParameters
)):
temporal = bool(obj.header) and obj.header[0] == 'time'
if temporal:
for point in obj._data:
if id(point) not in seen:
changes.append((point, float(point[0]) + offset))
seen.add(id(point))
river._data_traversal(modifier=collect)
for point, value in changes:
# This is a global change of origin, not a scenario-specific edit.
point[0] = value
def shift_database_times(connection, offset):
tables = [
(
'boundary_condition_data', 'boundary_condition',
'bc', BoundaryCondition
),
(
'lateral_contribution_data', 'lateral_contribution',
'lc', LateralContribution
),
(
'weather_parameters_data', 'weather_parameters',
'wp', WeatherParameters
),
('boundary_condition_data_adists', None, None, None),
('lateral_contribution_data_adists', None, None, None),
('boundary_condition_data_temperature', None, None, None),
]
for table, parent, fk, model in tables:
if model is None:
rows = connection.execute(
f'SELECT rowid, data0 FROM {table}'
).fetchall()
else:
rows = []
for kind, in connection.execute(
f'SELECT DISTINCT type FROM {parent}'
):
headers = model._get_ctor_from_type(kind)().header
if not headers or headers[0] != 'time':
continue
rows.extend(connection.execute(
f'SELECT d.rowid, d.data0 FROM {table} d JOIN {parent} p '
f'ON p.pamhyr_id=d.{fk} AND p.scenario=d.scenario '
f'WHERE p.type=?',
(kind,)
).fetchall())
connection.executemany(
f'UPDATE {table} SET data0=? WHERE rowid=?',
[(float(value) + offset, rowid) for rowid, value in rows]
)
+31 -3
View File
@@ -1,5 +1,5 @@
# PamhyrDB.py -- Pamhyr abstract model database classes
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -26,6 +26,8 @@ from functools import reduce
from SQL import SQL
from Model.Except import NotImplementedMethodeError
from tools import trace
from Model.Tools.PamhyrID import PamhyrID
logger = logging.getLogger()
@@ -48,11 +50,9 @@ class SQLModel(SQL):
self._cur = self._db.cursor()
if is_new:
logger.info("Create database")
self._create() # Create db
# self._save() # Save
else:
logger.info("Update database")
self._update() # Update db scheme if necessary
# self._load() # Load data
@@ -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.
@@ -202,6 +209,8 @@ class SQLSubModel(PamhyrID):
"""
if self._status is None:
return
if self._status.is_loading():
return
self._owner_scenario = self._status.scenario_id
self._status.modified()
@@ -218,6 +227,8 @@ class SQLSubModel(PamhyrID):
"""
if self._status is None:
return
if self._status.is_loading():
return
self._owner_scenario = self._status.scenario_id
self._status.modified()
@@ -297,6 +308,15 @@ class SQLSubModel(PamhyrID):
"""
raise NotImplementedMethodeError(cls, cls._db_update)
@classmethod
def is_table_exists(cls, execute, table):
return any(
execute(
"SELECT name FROM sqlite_master " +
f"WHERE type='table' AND name='{table}'"
)
)
@classmethod
def _db_update_to_0_2_0_set_node_pid(cls, execute, table, nodes):
els = execute(
@@ -311,6 +331,14 @@ class SQLSubModel(PamhyrID):
if node_id == -1:
continue
if node_id not in nodes:
# ⚠️ cas important : probablement déjà migré
logger.warning(
f"node_id {node_id} not in nodes " +
"→ probably already migrated"
)
continue
execute(
f"UPDATE {table} " +
f"SET node = {nodes[node_id]} " +
+4 -1
View File
@@ -1,5 +1,5 @@
# PamhyrDict.py -- Pamhyr abstract dict for model classes
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -97,6 +97,9 @@ class PamhyrModelDict(SQLSubModel):
Nothing
"""
if predicate(self, data):
modifier(self, data)
for key in self._dict:
self._dict[key]\
._data_traversal(predicate, modifier, data)
+4 -1
View File
@@ -1,5 +1,5 @@
# PamhyrList.py -- Pamhyr Abstract List object for the Model
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -100,6 +100,9 @@ class PamhyrModelList(SQLSubModel):
Nothing
"""
if predicate(self, data):
modifier(self, data)
for el in self._lst:
el._data_traversal(predicate, modifier, data)
+5 -2
View File
@@ -1,5 +1,5 @@
# PamhyrList.py -- Pamhyr Abstract List object for the Model
# Copyright (C) 2023-2025 INRAE
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -107,6 +107,9 @@ class PamhyrModelList(SQLSubModel):
Returns:
Nothing
"""
if predicate(self, data):
modifier(self, data)
for el in self._lst:
el._data_traversal(predicate, modifier, data)
@@ -168,7 +171,7 @@ class PamhyrModelList(SQLSubModel):
lambda x: x[1],
filter(
lambda x: x[0] in indexes,
enumerate(self._lst)
enumerate(self.lst)
)
)
)
@@ -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]
Loaded 100 of 399 files, more files were not shown because too many files have changed in this diff. Show more