Compare commits

..
816 Commits
Author SHA1 Message Date
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
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
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
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
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
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
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
parouby bf6d40f398 Ensemble: Results: PlotRKC: Add legend. 2026-09-08 17:50:51 +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
parouby 97fe038d9b Ensemble: Fix open results. 2026-09-08 15:08:11 +02:00
parouby 58e3da8942 Ensemble: Add base for RKC ensemble plot. 2026-09-04 17:51:52 +02:00
parouby b7c3793a66 Geometry: Point: Fix index creation. 2026-09-04 11:31:23 +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
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
parouby 3532f75c2b Solver: Mage: Minor change. 2026-09-02 16:21:31 +02:00
parouby 6c59919972 Pep8: Fix. 2026-09-01 11:36:35 +02:00
parouby 77d56b8bec Ensemble: Keep results nim, mean and max for each run. 2026-09-01 11:34:37 +02:00
parouby 7ef7bc13dc Ensemble: Continue to adapt results. 2026-08-28 16:15:59 +02:00
parouby a1d85274b9 Ensemble: Put ensemble results data into results obj. 2026-08-28 11:10:29 +02:00
parouby e7b9bc92a2 Ensemble: Run: Log variation. 2026-08-27 10:10:08 +02:00
parouby 7d8e414fe2 Ensemble: Fix missing requirements 'psutil'. 2026-08-27 09:48:12 +02:00
parouby 476bcc9c55 Ensemble: Made it compatible with other solver than mage8. 2026-08-27 09:44:03 +02:00
parouby ae006f52f4 Ensemble: Table: Fix double assignation for data type and target. 2026-08-26 16:04:58 +02:00
parouby 3583a93ae0 Ensemble: Set ensemble as read only study (except for ensemble). 2026-08-25 17:07:34 +02:00
parouby 212a00a43c Ensemble: Fix missing file. 2026-08-25 14:42:41 +02:00
parouby e5dbba362a Debug: Add memory usage to debug timer window. 2026-08-21 16:55:50 +02:00
parouby bdc09addfa Ensemble: Fix results reading. 2026-08-21 16:55:17 +02:00
parouby 51b3987a4a Mage: Ensemble: Add bin reading method. 2026-08-21 15:28:17 +02:00
parouby e7290fa81b Ensemble: Fix ensemble study meta data save. 2026-08-21 15:27:17 +02:00
parouby b5432498c6 Ensemble: Add sequential runs with RUG variation into mage export. 2026-08-20 16:00:40 +02:00
parouby 807592eab2 Ensemble: Rename 'range' to 'params'. 2026-08-14 16:33:41 +02:00
parouby 394c0779fc Ensemble: Add min max dialog for range. 2026-08-14 16:30:27 +02:00
parouby 5b3f777100 Ensemble: Minor change and add target data to table. 2026-08-14 09:27:39 +02:00
parouby 8b36905842 Ensemble: Fix some view/model interations. 2026-08-14 09:27:36 +02:00
parouby b203e8e062 Ensemble: Fix editing ensemble list. 2026-08-12 16:46:13 +02:00
parouby 4d4df1bde2 Ensemble: Function: Randomize Uniform sample. 2026-08-12 15:19:12 +02:00
parouby 876780a349 Ensembles: Open Ensemble windows from scenarios. 2026-08-12 14:50:58 +02:00
parouby 3b83377f3c Ensemble: Draw ensemble scenario as rect and minor fix. 2026-08-07 16:28:13 +02:00
parouby e64c45b621 Ensemble: Create new ensemble scenario. 2026-08-07 15:18:22 +02:00
parouby 70dae9ab1b Ensemble: Prepare view with add base classes. 2026-08-06 15:33:53 +02:00
parouby 0bb3c5bc65 Ensemble: Minor change to prepare view integration. 2026-07-31 16:19:13 +02:00
parouby 54f5c7c548 Ensemble: Switch data pid to data ref, and minor change. 2026-07-30 11:14:33 +02:00
parouby 56217186ff Ensemble: Fix save/load methode. 2026-07-29 16:31:13 +02:00
parouby 82fb2bcc48 Ensemble: Change function to id and name, minor change and fix. 2026-07-29 15:19:36 +02:00
parouby 6dc42b637a Ensemble: Function: Some minor fix and add it to study. 2026-07-24 11:45:39 +02:00
parouby e6d12713e3 Ensemble: Minor change. 2026-07-24 10:04:15 +02:00
parouby 9456cf32fc Ensemble: Function: Add base classes. 2026-07-24 10:02:25 +02:00
parouby 9b1fb4e7d2 Ensemble: Add ensemble base classes. 2026-07-23 09:51:17 +02:00
parouby c09c62f077 Merge: Results reading optimisation. 2026-07-10 10:18:58 +02:00
parouby 7a2e93a995 Results: Custom plot: Minor change. 2026-07-10 09:16:05 +02:00
parouby 5327c29641 Results: Custom plot: Use 'zfd' np table. 2026-07-09 18:05:10 +02:00
parouby be865102a8 Results: Put 'zfd' data into np table. 2026-07-09 16:53:48 +02:00
parouby 37f1b43b93 Debug: Fix pep8. 2026-07-09 10:16:23 +02:00
parouby 16f0775783 Pamhyr: Update all copyright. 2026-07-09 10:13:06 +02:00
parouby f2c4285b4c Debug: Minor change. 2026-07-09 09:03:11 +02:00
parouby ee95f0e89e Debug: Add csv export method. 2026-07-08 17:55:31 +02:00
parouby 1306977954 Debug: Add Timer window. 2026-07-08 16:38:41 +02:00
parouby 476fa81bc2 Results: Plot{AC,RKC}: Fix max elevation and minor change. 2026-07-07 21:56:19 +02:00
parouby e8f1f0ac43 Results: Add TableData pamhyr object for np table. 2026-07-03 17:03:33 +02:00
Dylan Jeannin cfbc2d96a1 Mage: Fix parsing for .LAT files 2026-07-03 13:07:10 +02:00
parouby 1adf24ce77 Geometry: ProfileXYZ: Minor optimisation of computation. 2026-07-02 15:04:33 +02:00
parouby a61d130baa Solver: Mage: Comment setter Z, Q and V into profile and bufferize and fix indent. 2026-07-02 11:43:00 +02:00
parouby 0a1760cee6 Results: CustomPlot: Continue to switch to np table. 2026-07-02 11:00:51 +02:00
parouby 3e094af325 Results: CustomPlot: Use np table. 2026-07-01 17:54:34 +02:00
parouby 762e4d70a0 Results: Exports: Use np.{min,max}. 2026-07-01 17:54:08 +02:00
parouby 6c4f863b96 Results: Export: Use np table. 2026-07-01 15:39:52 +02:00
parouby b87f82f74d Results: PlotRKC: Use table to get results table. 2026-06-30 16:57:42 +02:00
parouby ffad53a331 Results: Table: Use results tables. 2026-06-30 15:30:44 +02:00
parouby 662a218e41 Results: Reach: Keep profiles global min and max. 2026-06-26 16:51:58 +02:00
parouby 0501ae9bec Solver: Mage: read_bin: Use np table for velocity computation. 2026-06-26 16:20:32 +02:00
parouby 7fd38eec00 Results: Table: Minor change. 2026-06-26 14:54:55 +02:00
parouby 4bab4580ad Results: PlotXY: Use table Z to display max elevation. 2026-06-26 14:41:24 +02:00
parouby 21d2e6ad20 Results: PlotAC: Use Z table. 2026-06-26 14:23:00 +02:00
parouby d0a8f369ef Results: PlotH: Use np table. 2026-06-26 14:05:29 +02:00
parouby fef2e1a40c Results: PlotXY: Minor change. 2026-06-26 10:51:36 +02:00
parouby 20c2cb7ea4 Merge branch 'master' into opt-result 2026-06-26 09:29:39 +02:00
parouby 64a26e987e ci: Fix again. 2026-06-25 15:48:41 +02:00
parouby eb233847de ci: Fix pipeline for release. 2026-06-25 15:42:46 +02:00
parouby 7bb810d427 Pollutants: Fix 'enabled' column update version. 2026-06-25 15:00:41 +02:00
Dylan Jeannin e49724072b Merge branch 'dev_dylan' into scenarios 2026-06-25 14:41:24 +02:00
Dylan Jeannin 3664ba93e9 Results: Fix open mage results from AdisTS solver 2026-06-25 14:39:45 +02:00
Dylan Jeannin a9903e987b AdisTS: Resolve bug with node ids not matching in config files 2026-06-25 14:05:04 +02:00
Dylan Jeannin f9a88a211c Fix pep8 2026-06-22 17:10:57 +02:00
Dylan Jeannin 32535af3e6 UpdateRK: avoid crash when updating RK with missing points on a section, warning message instead 2026-06-22 17:04:39 +02:00
Dylan Jeannin fa8948c1b6 Meshing: Debug error caused by index out of range when adding an empty transect on a reach 2026-06-22 15:18:08 +02:00
Dylan Jeannin 2d5455c09d Meshing: Debug crashing case when divide by 0 2026-06-22 14:16:33 +02:00
Dylan Jeannin 83680bbe57 Pollutant: quickfix, back to previous version 2026-06-22 12:12:02 +02:00
Dylan Jeannin 1d1a00a0ff Pollutants: merge conflicts 2026-06-22 10:42:29 +02:00
Dylan Jeannin ffd95b9fdf Pollutants: merge conflicts 2026-06-22 10:42:07 +02:00
parouby ecb2544cab Results: Keep timestamps index table. 2026-06-19 15:25:40 +02:00
parouby 9c66021b53 Results: Give access profiles to np table and keep profile global id. 2026-06-19 14:54:26 +02:00
parouby 8255fc3c1b River: Add some timer. 2026-06-19 09:23:42 +02:00
parouby 32e7413aad Solver: Mage: read_bin: Put velocity into np table. 2026-06-18 15:45:11 +02:00
parouby d87db5fd48 Solver: Mage: read_bin: Optimize iprofiles. 2026-06-18 15:02:34 +02:00
parouby 388368adad Solver: Mage: Add data np table to results. 2026-06-18 14:29:50 +02:00
parouby 4d014d5a1d Results: Minor change. 2026-06-18 10:11:17 +02:00
parouby 181a6c226f Geometry: ProfileXYZ: Add 'get_all_water_limits_ac' cache. 2026-06-18 10:09:51 +02:00
parouby 1975ed7eaa Profile: Create cache for 'get_water_limits' results. 2026-06-17 17:41:06 +02:00
parouby 565a7530fb Results: Minor results reading optimisation with async computation. 2026-06-17 17:04:58 +02:00
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 c065b19ef8 debug 2025-11-26 11:46:38 +01:00
Theophile Terraz 1da3461dfe debug meshing 2025-11-26 11:16:19 +01:00
Theophile Terraz b20a4c3c3d debug meshing again... 2025-11-25 17:41:26 +01:00
Theophile Terraz d66e75c183 debug mesh AGAIN 2025-11-25 16:27:50 +01:00
Theophile Terraz 31b4c47379 debug CI 2025-11-25 13:20:57 +01:00
Theophile Terraz 71bba499e0 Revert "debug CI"
This reverts commit 7a08fa42d9.
2025-11-25 13:19:04 +01:00
Theophile Terraz 7a08fa42d9 debug CI 2025-11-25 12:03:16 +01:00
Theophile Terraz 18a008462a debug CI 2025-11-24 18:00:57 +01:00
Theophile Terraz d81da59398 debug CI 2025-11-24 17:54:33 +01:00
Theophile Terraz 3ab1eabe75 debug CI 2025-11-24 17:38:03 +01:00
Theophile Terraz cf4f860db8 debug CI 2025-11-24 17:25:51 +01:00
Theophile Terraz c68e3337ef debug CI 2025-11-24 17:09:00 +01:00
Theophile Terraz dd5eecb1e2 pep8 2025-11-24 15:18:28 +01:00
Theophile Terraz db1ae613d6 test release2 2025-11-21 16:12:53 +01:00
Theophile Terraz a800816cda test release 2025-11-21 16:01:59 +01:00
Theophile Terraz d4ef06c7c1 debug CI 2025-11-21 14:52:46 +01:00
Theophile Terraz 95d1d231c9 pep8 2025-11-21 14:07:26 +01:00
Theophile Terraz 10b278e26f Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios 2025-11-21 14:05:25 +01:00
Theophile Terraz 0550eef2eb add rubarBE in release 2025-11-21 14:05:13 +01:00
parouby 73eb9919eb Merge remote-tracking branch 'origin/master' into scenarios 2025-11-21 11:23:46 +01:00
parouby 9116009b0b Geotiff: Disable add and delete for readonly mode. 2025-11-21 11:19:43 +01:00
parouby 4e4ceb5074 Geotiff: Edit: Fix readonly mode. 2025-11-21 11:14:29 +01:00
parouby d79e2ff187 Merge branch 'master' into scenarios 2025-11-21 10:55:51 +01:00
parouby d4a2594239 Wiki: Add geotiff screenshot. 2025-11-21 10:20:44 +01:00
parouby b666162bdf Merge branch 'scenario-dev-pa' into scenarios. 2025-11-20 15:44:52 +01:00
parouby d705accaed Wiki: Add scenarios screenshot. 2025-11-20 09:58:30 +01:00
Theophile Terraz d93917b081 debug internal mesher 2025-11-19 15:27:33 +01:00
Theophile Terraz ceff5f3083 debug internal mesher 2025-11-19 15:26:09 +01:00
Theophile Terraz 97762756f8 debug 2025-11-19 13:40:12 +01:00
parouby 2c49e991b4 GeoTIFF: Fix coordinates. 2025-11-19 11:46:22 +01:00
Theophile Terraz c2a8f38ef5 quick fix for AdisTS 2025-11-18 17:25:48 +01:00
parouby f8a41fce08 GeoTIFF: Add red rectangle at GeoTIFF selected. 2025-11-17 15:01:59 +01:00
parouby 624ae826eb GeoTIFF: Fix plot display where geotiff is deleted and fix update propagation. 2025-11-17 10:08:06 +01:00
parouby d47dc0687e GeoTIFF: Minor change. 2025-11-14 15:56:24 +01:00
parouby 117e5222e4 GeoTIFF: Edit: Fix event connection. 2025-11-14 15:50:24 +01:00
parouby b1c7a77f37 GeoTIFF: Fix pep8. 2025-11-14 15:18:39 +01:00
parouby 7b833390f1 Results: Add study geotiff in plot xy. 2025-11-14 15:05:00 +01:00
parouby ae08642116 GeoTIFF: Factorise draw geotiff function. 2025-11-14 15:04:31 +01:00
Theophile Terraz c1905aa7e1 debug rubarBE format 2025-11-14 11:48:02 +01:00
parouby 97ece018aa Reservoirs: Fix loading without nodes. 2025-11-14 10:01:56 +01:00
parouby 7f0102a881 GeoTIFF: Add geotiff to mainwindow tab info and fix bounds at file import. 2025-11-14 09:56:46 +01:00
parouby 786923bdbf GeoTIFF: Add missing ui files. 2025-11-13 15:08:50 +01:00
parouby deb9b2069f GeoTIFF: Integrate geotiff menu to mainwindow and fix bounds update. 2025-11-13 14:56:38 +01:00
parouby 9308a73e8e GeoTIFF: Minor change. 2025-11-13 11:47:51 +01:00
parouby 869e116ad0 GeoTIFF: Minor change. 2025-11-13 10:49:22 +01:00
parouby a2f3d22001 GeoTIFF: Add geometry and geotiff diplay next to the geotiff list. 2025-11-13 10:44:23 +01:00
parouby 2e360943b2 GeoTIFF: Switch PlotXY to display all river reaches. 2025-11-12 11:42:46 +01:00
parouby 8976f054c7 GeoTIFF: Save geotiff into study db. 2025-11-12 11:16:28 +01:00
parouby 7bce725c63 GeoTIFF: Fix pep8. 2025-11-12 10:43:20 +01:00
parouby 5c83d67865 GeoTIFF: Continue intergation. 2025-11-12 10:41:45 +01:00
parouby 6e52b1681e GeoTIFF: Continue view integration. 2025-11-11 12:26:50 +01:00
parouby f27b2cc586 GeoTIFF: Prepare model/view integration. 2025-11-10 17:10:01 +01:00
parouby 1ed5d69bf4 GeoTIFF: Prepare view integration. 2025-11-10 16:28:21 +01:00
parouby 5bb6cc40fe GeoTIFF: Add to modules list. 2025-11-10 16:04:18 +01:00
parouby 4c0a12dcf9 GeoTIFF: Integrate to River submodel. 2025-11-10 10:21:40 +01:00
parouby a308af41e0 GeoTIFF: Prepare add geotiff into pamhyr2 db. 2025-11-07 17:12:27 +01:00
Theophile Terraz 02db91f3d7 debug 2025-11-06 15:13:20 +01:00
parouby 4cf4015579 Results: Set import data methode default dir as results dir. 2025-11-06 09:30:29 +01:00
parouby 291b97ac9b Results: Select solver for last results also if the results path exists. 2025-11-04 16:38:35 +01:00
parouby b04e367e72 Results: Add InputDialog to select solver. 2025-11-04 16:22:11 +01:00
parouby 81d58122d6 MainWindow: Fix save callback. 2025-11-04 14:34:32 +01:00
parouby 874f592cf4 Results: Fix add data settings. 2025-11-04 14:18:00 +01:00
parouby 9b0bdd1e63 Results: Fix plot add data 'Q(t)'. 2025-11-04 10:15:37 +01:00
Theophile Terraz d934b5e381 add waiting dialog while meshing 2025-11-04 09:30:40 +01:00
parouby 14549330b9 Results: Add additional data to SQL study save. 2025-11-03 16:55:36 +01:00
parouby 2487bec6d6 Results: Minor change. 2025-11-03 15:04:05 +01:00
parouby 4e1acfecdc Results: Refacto read csv. 2025-11-03 14:55:42 +01:00
parouby c63c776989 MainWindow: Add message box at results opening error. 2025-11-03 10:03:49 +01:00
parouby 44a8be6be3 WaitingDialog: Minor change. 2025-10-31 17:27:47 +01:00
parouby a5a64f2080 MainWindow: Minor change and fix. 2025-10-31 15:41:25 +01:00
parouby 16ee5a90e4 Results: Fix update table creation. 2025-10-31 15:05:38 +01:00
parouby 7ca6e69526 Pamhyr2: Add save action (ctrl+s) in each pamhyr window. 2025-10-31 14:43:01 +01:00
Theophile Terraz 279fa0bf5e debug get_water_limits 2025-10-31 11:01:43 +01:00
parouby abe24611b3 Merge branch 'scenario-dev-pa' into scenarios 2025-10-31 10:44:04 +01:00
parouby 17a18dde18 Results: Fix compare dialog and remove dead code. 2025-10-31 10:40:46 +01:00
parouby 044818072e Scenario: Fix scenario reload. 2025-10-31 10:27:10 +01:00
Theophile Terraz 8bd791b2c4 Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios 2025-10-31 09:38:15 +01:00
Theophile Terraz c8d0d0c212 fix typo 2025-10-31 09:37:56 +01:00
parouby d31be61af8 Merge branch 'scenario-dev-pa' into scenarios 2025-10-31 09:14:50 +01:00
Theophile Terraz a8a408b5d5 fix RubarBE friction export + extrapolation 2025-10-30 16:16:35 +01:00
parouby ff9d25412c Results: Fix results_data delete. 2025-10-30 16:14:41 +01:00
parouby de7f3e63b1 BC: Fix BC undo command. 2025-10-30 09:57:05 +01:00
Theophile Terraz 879c151524 pep8 2025-10-29 17:13:09 +01:00
Theophile Terraz 7990a60d8b take care of frictions in update_rk 2025-10-29 17:09:42 +01:00
Theophile Terraz 8aa0e25da2 pep8 2025-10-28 17:21:10 +01:00
parouby 1e6a578890 Results: Minor fix. 2025-10-28 17:01:37 +01:00
Theophile Terraz 1dca9b98df debug rubarBE 2025-10-28 15:56:53 +01:00
parouby de180b3635 Pamhyr2: Fix pep8. 2025-10-28 11:12:45 +01:00
parouby 5ca763ac8c Results: Disable simple compare between solvers to solvers/scenarios. 2025-10-28 11:09:48 +01:00
parouby 188486f33a Results: Compare scenarios. 2025-10-27 17:14:29 +01:00
parouby bc08d06bb1 Study: Add copy method to prepare scenario comparison. 2025-10-27 14:50:08 +01:00
parouby 05e039011a Scenario: Fix workdir path for last result opening. 2025-10-27 09:29:33 +01:00
Theophile Terraz 7640123299 merge 2025-10-24 16:41:34 +02:00
Theophile Terraz e6dd983f97 more work on compare results 2025-10-24 16:36:06 +02:00
parouby 002ebc5928 Solver: Mage: Fix path name with whitespaces. 2025-10-24 15:18:31 +02:00
Theophile Terraz 84486c03ed more work on import data results 2025-10-24 15:11:11 +02:00
parouby dc7dfb2414 Results: Compare: Minor change. 2025-10-24 15:02:53 +02:00
parouby ee01beb844 Results: Compare: Simplify compare method and add message box on error. 2025-10-24 11:23:43 +02:00
Theophile Terraz a31e2127e7 debug old .INI file read 2025-10-23 16:32:04 +02:00
Theophile Terraz 68b62a5630 debug water limits 2025-10-23 15:30:41 +02:00
Theophile Terraz 990d44dd07 again 2025-10-23 14:50:06 +02:00
parouby 819ae41c00 Results: Add posibility to save multiple results for different solver. 2025-10-23 11:23:44 +02:00
Theophile Terraz bf95b7f7e6 work on import data in result window 2025-10-22 16:22:59 +02:00
Theophile Terraz 861b78b928 debug read rubar3 results 2025-10-22 15:01:10 +02:00
parouby a29df8f904 Network: Center on previous size. 2025-10-21 17:28:16 +02:00
parouby 08c414a66e Network: Resize network zone. 2025-10-21 17:01:39 +02:00
parouby 97ebf9324a Merge branch 'master' of gitlab.com:pamhyr/pamhyr2 2025-10-21 15:42:01 +02:00
parouby 0112ad63d9 Network: Resize network zone. 2025-10-21 15:38:12 +02:00
Theophile Terraz bcbb440ad4 debug import friction 2025-10-21 15:11:28 +02:00
Theophile Terraz 57a80d00aa work on import data in results 2025-10-21 15:01:16 +02:00
parouby 70eae5f7d2 Results: Try add explicite import for rasterio. 2025-10-21 14:23:40 +02:00
parouby e2a65e2cdd git: CI: Use HIDDEN_IMPORTS variable and try collect-submodules. 2025-10-21 11:04:22 +02:00
parouby f03b74614e git: CI: Add hiddenimport on windows pyinstaller command. 2025-10-21 10:11:22 +02:00
parouby 532b3fd7fd Results: Fix pep8 comment. 2025-10-21 09:51:47 +02:00
parouby 64df4129d4 Results: Reactivate geotiff if package is imported. 2025-10-21 09:50:40 +02:00
parouby f6e35e2045 git: CI: Try add rasterio.sample into hiddenimport. 2025-10-21 09:23:47 +02:00
parouby a97717a624 Pamhyr: Window: Fix title without study opened. 2025-10-20 18:48:40 +02:00
parouby 3f650de52f git: CI: Add rasterio at hidden import. 2025-10-20 17:51:59 +02:00
parouby 1f4bda7976 Results: Minor change. 2025-10-20 16:48:49 +02:00
parouby a4423e45f8 Git: Update guix manifest. 2025-10-20 15:23:25 +02:00
parouby 1386c592bf Geotiff: Try load geotiff otherelse disable features. 2025-10-20 15:18:20 +02:00
Theophile Terraz 016f3fc73c Merge branch 'master' into scenarios 2025-10-14 11:52:44 +02:00
Theophile Terraz 7ef3184699 debug + disable rasterio 2025-10-14 11:52:28 +02:00
Theophile Terraz 5f6c823301 debug results 2025-10-14 11:45:45 +02:00
Theophile Terraz a42c46f4d2 speed up internal meshing 2025-10-14 09:41:51 +02:00
Theophile Terraz fb09cf254e speed up internal meshing 2025-10-14 09:40:43 +02:00
Theophile Terraz 315b913fd0 Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios 2025-10-14 09:01:16 +02:00
Theophile Terraz 0383dc18ae pep8 2025-10-13 17:50:15 +02:00
Theophile Terraz f16856d23b debug mesh + update_rk (verry slow mesh) 2025-10-13 17:48:48 +02:00
parouby e640fa1bc0 Solver: RubarBE: Fix results reading. 2025-10-10 16:44:16 +02:00
parouby df3b3b604d Solver: RubarBE: Factorise some code. 2025-10-10 16:44:16 +02:00
parouby 21e6c410e2 Solver: RubarBE: Add read results method. 2025-10-10 16:44:16 +02:00
Theophile Terraz 19045329dc merge master 2025-10-10 15:09:44 +02:00
parouby 4586ef79f7 Solver: RubarBE: Fix study export and minor change. 2025-10-09 10:54:42 +02:00
Theophile Terraz bc4557e21f keep selection after meshing 2025-10-08 09:45:35 +02:00
parouby f97d4ce3c5 Solver: RubarBE: Add devers file. 2025-10-07 16:26:06 +02:00
Theophile Terraz f6e08f203c purge + pep8 2025-10-07 15:28:36 +02:00
Theophile Terraz 5dcbc6b4e1 test clean mage rep 2025-10-07 11:56:01 +02:00
Theophile Terraz ebe906fce0 switch from MailleurTT to internal meshing 2025-10-07 11:52:31 +02:00
parouby cd4f626218 Solver: Rubarbe: Add export abshyd. 2025-10-07 11:16:31 +02:00
parouby c1eb2cf447 Solver: Rubar3: Minor change. 2025-10-06 17:20:12 +02:00
parouby 5fec95cc4d Network: Fix missing file and pep8. 2025-10-06 09:26:46 +02:00
parouby 46b943c800 Results: Message box where study version and results version are different. 2025-10-03 11:12:18 +02:00
parouby fa8fe508a7 Scenario: Save dialog at scenarios switch. 2025-10-02 11:14:26 +02:00
Theophile Terraz 911a2bbbff mesh on selection 2025-10-01 10:29:14 +02:00
parouby 2a926a8d73 Frictions: Split frictions list at reach split. 2025-09-30 15:54:00 +02:00
parouby d222842313 Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios 2025-09-30 13:57:09 +02:00
parouby 2942b6e442 Network: Add split reach method. 2025-09-30 13:53:37 +02:00
parouby 926cade437 River: Add reach split methods. 2025-09-30 10:25:26 +02:00
Theophile Terraz 13b4413b36 merge master 2025-09-29 16:08:06 +02:00
Theophile Terraz 706a900c8e work on rubar3 2025-09-29 15:25:35 +02:00
Theophile Terraz da03f39425 debug 2025-09-29 11:23:46 +02:00
parouby f516db0e48 Results: Fix db update. 2025-09-26 16:36:14 +02:00
parouby 8744e1d73e Results: Fix tests. 2025-09-26 16:08:19 +02:00
parouby 6113d9f4cd Scenario: Results: Fix scenario load and save results. 2025-09-26 15:58:42 +02:00
parouby 1dd92cb089 Results: Fix db update from 0.2.0. 2025-09-26 15:12:19 +02:00
parouby 8f6255c2cd Results: Update to async db load for results. 2025-09-26 14:27:19 +02:00
parouby 6c4f6e269c Results: Clear db load code. 2025-09-26 10:46:57 +02:00
parouby 82b0837d8c Results: Minor change. 2025-09-26 10:35:24 +02:00
parouby 137489cc1f Results: Fix timestamps order. 2025-09-26 10:23:55 +02:00
parouby 4dc4dfdda0 Results: Minor change. 2025-09-25 17:46:01 +02:00
Theophile Terraz 7d42c164fc debug 2025-09-25 17:01:01 +02:00
parouby ae15311471 Results: Save sediment layers to db save. 2025-09-25 16:58:49 +02:00
parouby dbcdace348 Results: Fix db save results open and display. 2025-09-25 10:58:29 +02:00
parouby 9375827949 Pamhyr2: Fix PEP8. 2025-09-25 09:29:52 +02:00
parouby 739f401fac Model: Study: Fix db save without results. 2025-09-25 09:28:15 +02:00
parouby 5f5a47679d Result: Fix db load profiles list. 2025-09-23 17:09:11 +02:00
parouby 26d1d63edb Result: Fix solver name. 2025-09-23 16:08:38 +02:00
Theophile Terraz 1b2f98494b pep8 2025-09-23 15:53:49 +02:00
parouby 381bc37d13 Results: Fix save method. 2025-09-23 15:47:37 +02:00
Theophile Terraz e7b63fe270 debug 2025-09-23 14:55:31 +02:00
Theophile Terraz 80ae4c36ad import lateral contributions from file 2025-09-23 14:17:59 +02:00
parouby 936bfc753a Results: Simplify db load. 2025-09-23 10:23:36 +02:00
parouby f0ccf1f4f7 Results: Continue db load method. 2025-09-23 10:21:44 +02:00
parouby 3a3c4d9d73 Results: Prepare result save. 2025-09-22 17:20:31 +02:00
Theophile Terraz 41cffd13e0 debug 2025-09-19 16:33:06 +02:00
Theophile Terraz 9270dd0ff1 add HHLab test case 2025-09-19 16:04:10 +02:00
Theophile Terraz ba8a50e0b9 debug frictions 2025-09-19 16:03:23 +02:00
parouby 9c69f2ce6d Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios 2025-09-18 17:10:34 +02:00
parouby e7a27c798a LC: Fix undo command. 2025-09-18 17:09:20 +02:00
Theophile Terraz ddc612328e Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios 2025-09-18 11:34:52 +02:00
Theophile Terraz d05d1e6914 merge master 2025-09-18 11:34:47 +02:00
parouby 8bcf776813 Pamhyr: Fix PEP8. 2025-09-18 11:26:20 +02:00
parouby 589e591d3b Strickler: Fix undo command. 2025-09-18 11:19:30 +02:00
parouby e2e8c5ec9e AddFile: Fix undo commands. 2025-09-18 10:45:31 +02:00
Theophile Terraz fce034419d cleaning 2025-09-18 10:11:49 +02:00
parouby d9aea9eb3c REPLines: Fix undo commands. 2025-09-18 09:54:35 +02:00
parouby 4cddd43f5a BHS: Fix undo commands. 2025-09-16 15:09:42 +02:00
parouby becc1696cb HS: Fix undo commands. 2025-09-16 14:08:08 +02:00
parouby cf4957551f LC: Fix undo commands. 2025-09-16 11:08:26 +02:00
parouby 794b06d55a BC: Fix command rows. 2025-09-15 17:42:32 +02:00
parouby f51f742e00 Scenario: Fix contextual menu. 2025-09-15 15:43:13 +02:00
Theophile Terraz 31fe765e51 load frictions from file 2025-09-15 15:40:17 +02:00
parouby 40cc188815 SL: Fix save for scenario. 2025-09-15 14:13:24 +02:00
parouby 303f6bb35b Scenario: Duplicate: Fix data traversal and duplicate method. 2025-09-15 10:11:44 +02:00
parouby 9043b6e2a3 Scenario: Add duplicate command. 2025-09-12 17:36:22 +02:00
parouby 0608b9175e Scenario: Fix mistake. 2025-09-12 16:35:26 +02:00
parouby ea7fda6b28 Scenario: Fix scenario duplicattion. 2025-09-12 16:24:59 +02:00
parouby 090326b75c Pamhyr2: Model: Add data traversal method on each pamhyr object. 2025-09-12 15:09:05 +02:00
Theophile Terraz b582e133a1 load boundary conditions from file 2025-09-12 14:22:46 +02:00
parouby 78e6f7e5a7 River: Data traversal methode. 2025-09-12 11:13:57 +02:00
parouby 8173eb8de9 Pamhyr2: Create new generic methode '_data_traversal' for scenario duplicattion. 2025-09-12 10:52:41 +02:00
parouby 591cd8950c Rubar3: Minor change. 2025-09-12 09:33:58 +02:00
Theophile Terraz 7c4c772f43 load initial state from .INI 2025-09-11 17:15:48 +02:00
parouby 3a78d6d6f7 MainWindow: Create a study file copy in current dire before update. 2025-09-11 14:39:28 +02:00
parouby bbf85a6048 BC: Edit: Minor change. 2025-09-11 14:38:58 +02:00
parouby 6dfa876809 Pamhyr2: Fix pep8. 2025-09-11 11:35:08 +02:00
parouby aff86a2ffa CI: Add release job without create gitlab release. 2025-09-11 11:08:11 +02:00
parouby 1eb925f3dd Merge branch 'scenarios' of gitlab.com:pamhyr/pamhyr2 into scenarios. 2025-09-11 10:09:44 +02:00
parouby 701d76240e Results: Fix hs display. 2025-09-11 10:06:15 +02:00
parouby 37ea9e9083 Geometry: Reverte purge algorithme to previous version (modified). 2025-09-11 10:01:18 +02:00
Theophile Terraz 6457b3ad25 fix Hogneau step3 + bug fixes 2025-09-11 09:53:47 +02:00
Theophile Terraz cba5b948de fix Hogneau step2 2025-09-09 16:14:08 +02:00
parouby 4ba232a7e4 Doc: Minor change. 2025-09-09 11:16:00 +02:00
parouby 05f3f0fc7e Reservoir: Fix index for add and delete undo command. 2025-09-09 10:44:40 +02:00
parouby 89a5d78ced Reservoir: Fix delete reservoir (load and display). 2025-09-09 09:39:44 +02:00
parouby 1b70e22a58 IC: Fix modification method and display. 2025-09-08 16:02:19 +02:00
parouby 22aeda105e Geometry: Minor change. 2025-09-08 14:19:49 +02:00
parouby 7181122a3a Geometry: Refacto and fix purge method. 2025-09-08 11:26:42 +02:00
parouby 76ce34c4ea Geometry: Continue to fix ponts operation. 2025-09-08 10:34:46 +02:00
parouby c58620fdc2 Geometry: Fix section modification command. 2025-09-05 16:57:12 +02:00
parouby a6341dedd2 Geometry: Fix move up method. 2025-09-05 15:13:49 +02:00
parouby ba7171326c Geometry: Section: Fix move up and down. 2025-09-05 11:07:10 +02:00
parouby 78b4269a69 Geometry: Section: Fix paste row position. 2025-09-05 10:46:25 +02:00
parouby a30bf8e364 Geometry: Section: Fix sort undo. 2025-09-05 10:28:09 +02:00
parouby bd1edbaf30 Pollutant: Fix db create. 2025-09-05 10:07:13 +02:00
parouby bdf9264963 MainWindow: Minor change. 2025-09-05 09:38:48 +02:00
parouby befe1a550d Geometry: Profile: Fix undo insert point. 2025-09-04 17:06:18 +02:00
parouby f4d3f2b28f Geometry: Fix profile copy/paste. 2025-09-04 16:41:07 +02:00
parouby 2a1c8c1e57 MainWindow: Try fix backup saved for multi scenario. 2025-09-04 14:53:20 +02:00
parouby a3da09cf5b Mainwindow: Add backup file dialog. 2025-09-04 09:59:24 +02:00
parouby 017f26c03d Geometry: Point: Fix modified status for SL setter and name setter. 2025-09-04 09:26:50 +02:00
parouby 1e2a123693 Model: Tools: Prepare scenario duplication. 2025-09-02 17:20:09 +02:00
parouby 2916afb974 Model: Tools: Minor change. 2025-09-02 16:57:08 +02:00
parouby 37963b7b21 Mainwindow: Fix save as for multiple scenarios. 2025-09-02 14:25:08 +02:00
parouby e5b4c998cb Scenario: Graph: Fix mouse button event. 2025-09-02 10:53:50 +02:00
parouby 4bf0ad40b1 IC, BC: Some fixes. 2025-09-02 10:47:50 +02:00
parouby 420d766ae5 Solver: Mage: Fix mage export. 2025-09-01 16:57:41 +02:00
parouby 645a76b0e8 Solver: Mage: Fix export where data is deleted. 2025-09-01 14:00:01 +02:00
parouby 6288f039d2 Pamhyr: Fix pep8. 2025-09-01 10:37:32 +02:00
parouby 3585b29b03 Solver: Mage: Minor change. 2025-09-01 10:36:13 +02:00
parouby 96bac8ee1f Graph: Minor change. 2025-09-01 10:35:07 +02:00
parouby e7d14e7aa3 BC: Fix status propagation to Data object. 2025-09-01 09:53:55 +02:00
parouby 6b14ed7efe tests_cases: Hogneau: Minor change. 2025-08-29 17:16:07 +02:00
parouby 3d40f599d6 tests_cases: Add Hogneau study with multiple scenario. 2025-08-29 16:28:17 +02:00
parouby 6ec677a047 Friction, Reservoir, Geometry: Some fixes and minor change. 2025-08-29 14:44:51 +02:00
parouby 7bb0c9d57e HS: Fix loading deleted HS. 2025-08-29 10:15:03 +02:00
parouby ec2a1da6b6 HS: Basic: Fix display. 2025-08-28 16:58:54 +02:00
parouby a072a28929 HS: Fix hs section set and display. 2025-08-28 15:23:33 +02:00
parouby fc7d732e0a Geometry: Profile: Optimize loading with reduce size of loaded pid set. 2025-08-28 11:09:12 +02:00
parouby cc5d86328b Reach: Fix deleted profile loading and manipulation. 2025-08-28 10:43:20 +02:00
parouby e3cbe3cf1f IC: Fix initial conditions generation. 2025-08-26 15:00:45 +02:00
parouby 9482aaf523 Geometry: Point: Fix points order at DB loading. 2025-08-26 14:23:31 +02:00
parouby e0cee0fbef Meshing: Fix meshing M file import. 2025-08-26 11:50:01 +02:00
parouby f9a8512477 Geometry: Points: Fix point insertions pamhyr_id creation and default value. 2025-08-26 10:03:54 +02:00
parouby 1457db721b Geometry: Fix profile modifications. 2025-08-26 09:31:23 +02:00
parouby 7774d2d7af Scenario: Minor fix at scenario creation and deletion. 2025-08-25 13:27:37 +02:00
parouby e6d3564d9b Scenarios: Fix scenarios table list after delete action. 2025-08-25 11:32:22 +02:00
parouby d62e703439 tests: Minor change. 2025-08-22 15:07:08 +02:00
parouby 0346080e16 Scenario: Simplify scenario update and minor change. 2025-08-22 14:14:22 +02:00
parouby 76559f6af8 tests: Add nodes scenario tests. 2025-08-22 11:07:50 +02:00
parouby 04ce79462c Adists: OutputRK: Fix mistake in save method. 2025-08-22 09:44:15 +02:00
parouby ffa065a3e6 Adists: OutputRK: Fix save and table display. 2025-08-22 09:36:54 +02:00
parouby 0e1e3665a6 Adists: Pollutants, BC: Fix update and save. 2025-08-21 15:30:33 +02:00
parouby c64bcd1ff7 Adists: BC: Put data into separate class. 2025-08-21 14:45:24 +02:00
parouby dd84b83302 Adists: Dif, LC: Minor fix. 2025-08-21 14:45:15 +02:00
parouby 084223037c tests: AdisTS: Add open MassZero study test. 2025-08-21 13:47:41 +02:00
parouby 072840a18c Adists: Fix update. 2025-08-21 13:47:14 +02:00
parouby 3ef13666dc Adists: dif: Fix update. 2025-08-21 11:35:39 +02:00
parouby e344678ac5 Adists: d90: Fix update pipeline. 2025-08-21 11:14:05 +02:00
parouby 1f79e6510d Adists: LC: Put lca data into separate class (to setup correct pamhyr_id). 2025-08-21 10:29:34 +02:00
parouby b94c37dd3b Adists: BC, IC: Some fixes. 2025-08-21 10:28:21 +02:00
parouby b61c4da8f1 AdisTS: Pollutant: Define 'characteristics' into a separate class. 2025-08-19 16:53:00 +02:00
parouby ba60585550 AdisTS: OutputRK, IC, LC: Add deleted tag in DB. 2025-08-19 14:27:46 +02:00
parouby 0ed7660e72 Adists: d90, dif, BC: Add deleted tag in DB. 2025-08-19 11:34:51 +02:00
parouby 8b4ff41b96 Adists: LC: View: Minor change. 2025-08-18 17:07:23 +02:00
parouby 8769fa260d View: IC: Fix RK table display. 2025-08-18 16:29:04 +02:00
parouby 2667737490 Adists: LC: Rename some 'edge' to 'reach' and fix plotXY error. 2025-08-18 15:56:15 +02:00
parouby 7cb3982a1f Pamhyr2: Update licence date. 2025-08-18 15:34:58 +02:00
parouby 2336dd22f3 tools: Update licence emacs script. 2025-08-18 15:32:16 +02:00
parouby f13ab03192 Adists: BC, IC, LC: Fix pollutant pamhyr_id update. 2025-08-18 14:47:31 +02:00
parouby 0df813b0d2 Adists: Fix update from id to pamhyr_id. 2025-08-18 13:59:12 +02:00
parouby 2faebfd402 Adists: d90, dif: Fix update for scenario. 2025-08-18 11:32:24 +02:00
parouby 7094ff3e62 Pollutants: Fix scenario update and minor fix. 2025-08-18 10:26:09 +02:00
parouby c6ab2647c1 Adists: Minor pep8 fix. 2025-08-18 09:56:24 +02:00
parouby b62d1429c0 Friction: Fix friction loading. 2025-08-18 09:39:52 +02:00
parouby 23a0bc33fa Adists: D90, DIF: Fix update for scenario. 2025-08-15 14:41:58 +02:00
parouby 93340a48b1 Adists: LC: Fix update for scenario. 2025-08-15 14:31:28 +02:00
parouby 0379a8ee02 Adists: BC: Fix update for scenario. 2025-08-15 14:31:09 +02:00
parouby 6cce02650b Adists: IC: Fix update for scenario. 2025-08-15 14:11:21 +02:00
parouby 6e79fe4cd5 Pollutants: Fix update for sceanrio. 2025-08-15 13:54:34 +02:00
parouby f66d8c7a34 OutputRKAdists: Fix update and DB table name. 2025-08-15 13:39:16 +02:00
parouby 826e532605 Pollutants: Fix delete sql request. 2025-08-15 13:10:25 +02:00
parouby fc35f2fbcc SQL: Minor sql loading request optimization. 2025-08-15 11:01:17 +02:00
parouby 216239bfce Scenario: Fix profile saving and loading for scenario. 2025-08-15 10:08:26 +02:00
parouby 739eb56006 Adists: Pollutants: Update for scenario. 2025-08-14 17:49:21 +02:00
parouby 468405d4ea Adists: Fix missing and update outputrk for scenario. 2025-08-14 16:32:22 +02:00
parouby 31af83ba02 Adists: DIF: Update for scenario. 2025-08-14 16:07:01 +02:00
parouby 45fddb9459 Adists: D90: Update for scenario. 2025-08-14 16:06:21 +02:00
parouby 05dcd3bcca Adists: BC: Update for scenario. 2025-08-14 15:08:32 +02:00
parouby 89a898410f Adists: LC: Update for scenario. 2025-08-14 10:52:47 +02:00
parouby 4a0456cbce Adists: IC: Update load and save for scenario. 2025-08-14 10:31:17 +02:00
parouby a525d8a2b0 Adists: Scenario: Minor fix. 2025-08-13 16:23:46 +02:00
parouby d7fcf62e12 Adists: Scenario: Some minor fix. 2025-08-13 15:03:25 +02:00
parouby 4ac86b9acb Study: Set version to 0.2.0. 2025-08-12 17:00:09 +02:00
parouby 00c9ecde52 Scenarios: Change update version number for scenarios from 0.1.0 to 0.2.0. 2025-08-12 16:23:19 +02:00
parouby ae49908a42 Adists: Continue to prepare scenario. 2025-08-12 16:09:24 +02:00
parouby 37cf42c524 Adists: Prepare for scenarios update (commit code is not working). 2025-08-12 15:36:19 +02:00
parouby c43a26870e Adists: LC: Prepare update for scenarios. 2025-08-12 10:44:58 +02:00
parouby 5a6574921a Adists: OutputRK: Prepare update for scenarios. 2025-08-12 10:44:20 +02:00
parouby 7fa86c29e7 Geometry: Fix profile modifications update status. 2025-08-12 10:43:17 +02:00
parouby a3f20796d7 Modules: Add Scenarios to modelling list. 2025-08-12 09:53:10 +02:00
parouby 835d73789e Scenarios: Update data to delete. 2025-08-12 09:40:44 +02:00
parouby f0ac170d2f Pamhyr: Fix tests and update adists obj id management. 2025-08-11 15:38:31 +02:00
parouby b154e3d03b Pamhyr: Fix pep8. 2025-08-11 14:30:23 +02:00
parouby 633e5a53dc tests: Update tests scripts. 2025-08-11 14:24:40 +02:00
parouby 3f98cf8625 DB: Fix DB update and set DB version to '0.1.7'. 2025-08-11 13:57:08 +02:00
parouby 36fe9a51c4 AdisTS: Update '_db_update' for the new methode prototype. 2025-08-11 11:41:45 +02:00
parouby 74ab3aa522 Merge: branch 'master' into 'scenarios'. 2025-08-11 11:12:59 +02:00
parouby cfdf175f9d git: Add Guix manifest and run script. 2025-08-11 09:38:45 +02:00
Pierre-Antoine Rouby 1b8f9b2abc .gitignore: Update. 2024-10-15 16:04:00 +02:00
Pierre-Antoine Rouby f3ad4414b6 doc: paper: 2023: simhydro: Add missing image. 2024-10-15 16:04:00 +02:00
Pierre-Antoine Rouby 8930c39fbe doc: dev: scenarios: Add gitkeep for images. 2024-10-15 16:04:00 +02:00
Pierre-Antoine Rouby 7c2908569e doc: paper: 2023: simhydro: Add presentation. 2024-10-15 16:04:00 +02:00
Pierre-Antoine Rouby 5598464081 doc: dev: Add note about scenarios. 2024-10-15 16:04:00 +02:00
Pierre-Antoine Rouby 40d90424a3 Merge branch 'master' into scenarios. 2024-09-24 15:54:10 +02:00
Pierre-Antoine Rouby bb33e8077a Results: Minor change. 2024-09-24 15:41:32 +02:00
Pierre-Antoine Rouby 2859d0f4ff Scenario: Add scenario separated repertory. 2024-09-23 17:33:11 +02:00
Pierre-Antoine Rouby cab1bceae8 Network: Fix tests. 2024-09-20 19:20:00 +02:00
Pierre-Antoine Rouby 48c389a27c Network: Add scenario support. 2024-09-20 18:35:43 +02:00
Pierre-Antoine Rouby 4199eb6ac5 Geometry: Add scenario support. 2024-09-20 16:10:00 +02:00
Pierre-Antoine Rouby 02c6430ed2 Geometry: Point: Add scenario support. 2024-09-20 11:15:00 +02:00
Pierre-Antoine Rouby 05ee0e86b1 SL: Fix modification status. 2024-09-18 13:58:36 +02:00
Pierre-Antoine Rouby 1ed6274f8b SL: Fix DB cleaning. 2024-09-18 11:25:47 +02:00
Pierre-Antoine Rouby 7857a2ea61 SL: Add scenario support. 2024-09-18 11:19:17 +02:00
Pierre-Antoine Rouby c01effbf4c LC: Add scenario support. 2024-09-17 16:14:06 +02:00
Pierre-Antoine Rouby a2ff3ecd9a IC: Add scenario support. 2024-09-17 12:01:02 +02:00
Pierre-Antoine Rouby a5ecc2383c BC: Add scenario support. 2024-09-16 16:25:47 +02:00
Pierre-Antoine Rouby e73f76024e Friction: Fix scenarios support. 2024-09-16 11:40:20 +02:00
Pierre-Antoine Rouby cd7a81d828 Reservoir: Add scenario support. 2024-09-06 15:33:20 +02:00
Pierre-Antoine Rouby 81264fa63e Friction: Add scenario support. 2024-09-06 14:41:25 +02:00
Pierre-Antoine Rouby 5e3dbc18c3 REPLines: Add scenario support. 2024-09-06 14:19:58 +02:00
Pierre-Antoine Rouby e43c20c1e6 Scenarios: Merge remote-tracking branch 'origin/master'. 2024-09-06 10:58:05 +02:00
Pierre-Antoine Rouby f65039d748 Pamhyr2: Fix pep8. 2024-09-06 10:26:19 +02:00
Pierre-Antoine Rouby d0e70ddddc SL: Add read only support. 2024-09-06 10:22:38 +02:00
Pierre-Antoine Rouby 3de96a8d9d BC, LC: Add read only mode support. 2024-09-06 10:04:21 +02:00
Pierre-Antoine Rouby b3fd673f33 Pamhyr2: Fix pep8. 2024-09-04 17:12:56 +02:00
Pierre-Antoine Rouby fb12f498a7 Pamhyr2: Extend RO mode to Friction, Strickler, HS, SP and REPLines. 2024-09-04 17:11:15 +02:00
Pierre-Antoine Rouby fd54116d55 Pamhyr2: Fix pep8. 2024-09-04 16:52:42 +02:00
Pierre-Antoine Rouby 83ecdc9a88 Study: Scenario: Fix stupidity. 2024-09-04 16:46:08 +02:00
Pierre-Antoine Rouby be1e868fc4 AddFiles: Add support to read only mode. 2024-09-04 16:29:27 +02:00
Pierre-Antoine Rouby a1e6e646f9 Scenario: Dont allow delete the default scenario. 2024-09-04 16:13:33 +02:00
Pierre-Antoine Rouby 29df56c8b4 Scenario: Add delete scenario command and use undo command. 2024-09-04 15:00:28 +02:00
Pierre-Antoine Rouby 064b3efa13 Pamhyr2: Fix pep8. 2024-09-03 15:57:08 +02:00
Pierre-Antoine Rouby 5cc5628956 Geometry: Add read only support. 2024-09-03 15:55:06 +02:00
Pierre-Antoine Rouby 8afa3d1e30 Pamhyr2, Scenario: Add Read only mode. 2024-09-03 15:25:29 +02:00
Pierre-Antoine Rouby 8f0535dc5c Scenario: Minor changes. 2024-09-03 11:47:46 +02:00
Pierre-Antoine Rouby a412f2f18f Scenario: Add waiting dialog. 2024-09-03 11:30:44 +02:00
Pierre-Antoine Rouby 8f4d233965 Scenario: Minor change. 2024-09-03 11:17:03 +02:00
Pierre-Antoine Rouby cc84b96024 Scenario: Add context menu with add scenario and select senario action. 2024-09-03 09:52:49 +02:00
Pierre-Antoine Rouby 4ff81a0b63 Scenario: Allow to move on board with mouse left click. 2024-09-03 09:22:55 +02:00
Pierre-Antoine Rouby 9b40b7eb39 Scenario: Add protected tag in front of name and alternative color. 2024-09-02 16:23:37 +02:00
Pierre-Antoine Rouby 5c3464a303 Scenario: Add some colors! 2024-09-02 16:13:43 +02:00
Pierre-Antoine Rouby 71bea2eea4 Scenario: Minor change. 2024-09-02 15:42:56 +02:00
Pierre-Antoine Rouby ed0a549027 Scenario: Some minor fix. 2024-09-02 15:02:02 +02:00
Pierre-Antoine Rouby 267f30849f Scenario: Minor fixes. 2024-09-02 11:49:54 +02:00
Pierre-Antoine Rouby b8ed3d8954 Scenario: Add X, Y position for tree display. 2024-09-02 11:47:37 +02:00
Pierre-Antoine Rouby b634acec67 HS, Stricklers: Minor change. 2024-09-02 11:46:51 +02:00
Pierre-Antoine Rouby 3035ea8d8b Scenario: Fix line display. 2024-09-02 11:11:43 +02:00
Pierre-Antoine Rouby abe7046d4f Scenario: Continue adapt network window to scenario. 2024-08-30 16:18:03 +02:00
Pierre-Antoine Rouby b60b922fef HS: Fixes pep8. 2024-08-30 11:05:08 +02:00
Pierre-Antoine Rouby 85d3345043 Scenarios: Add window scheme (from copy of network window). 2024-08-30 10:09:00 +02:00
Pierre-Antoine Rouby 60ee889794 HS: Add to scenarios. 2024-08-28 16:04:19 +02:00
Pierre-Antoine Rouby 9ec21d815d Stricklers: Add to scenarios. 2024-08-28 10:03:25 +02:00
Pierre-Antoine Rouby ac32995e01 AddFile: Add deleted flags. 2024-08-27 17:08:40 +02:00
Pierre-Antoine Rouby 8c99268bb8 SolverParameters: Switch to PamhyrID and add scenarios management. 2024-08-27 11:56:53 +02:00
Pierre-Antoine Rouby 237b2670f6 Pamhyr: Save: Minor fix. 2024-08-26 17:02:47 +02:00
Pierre-Antoine Rouby d50f617a85 tests: Add AddFiles scenario tests. 2024-08-26 15:10:36 +02:00
Pierre-Antoine Rouby 1cfc7e6638 AddFile: Activate scenarios. 2024-08-26 15:03:05 +02:00
Pierre-Antoine Rouby 8b9bb68ac5 Study: Add current scenario into study info. 2024-08-26 11:20:26 +02:00
Pierre-Antoine Rouby 71b631fbe4 Status: Rename SavedStatus to StudyStatus and add scenario to StudyStatus. 2024-08-23 14:35:04 +02:00
Pierre-Antoine Rouby 7274ff5e2d Pamhyr: Some fixe for db update to 0.1.1. 2024-08-21 15:10:39 +02:00
Pierre-Antoine Rouby 770ecdfa99 Pamhyr2: Pep8. 2024-08-21 14:12:48 +02:00
Pierre-Antoine Rouby 4023ae73ba IC: Use section ID instead of RK. 2024-08-21 12:03:55 +02:00
Pierre-Antoine Rouby 7f87d22249 HS: Use section ID instead of RK. 2024-08-21 11:31:43 +02:00
Pierre-Antoine Rouby 6225f8940e Study: DB: Set version to 0.1.0. 2024-08-20 15:57:45 +02:00
Pierre-Antoine Rouby 6a5183cc0c WaitingDialog: Minor change. 2024-08-20 15:37:40 +02:00
Pierre-Antoine Rouby 037f5ecfa5 WaitingDialog: Minor fixes. 2024-08-20 15:17:30 +02:00
Pierre-Antoine Rouby 63657151ea WaitingDialog: Minor change. 2024-08-20 15:15:18 +02:00
Pierre-Antoine Rouby c4e1a4c485 Results: Delete ReadingResultsDialog (switch to WaitingDialog). 2024-08-20 15:08:49 +02:00
Pierre-Antoine Rouby 4222503961 Pamhyr: Add waiting dialog at file loading. 2024-08-20 15:07:30 +02:00
Pierre-Antoine Rouby af779a1880 BC: Fix save. 2024-08-20 13:51:57 +02:00
Pierre-Antoine Rouby 84a5bed66d Point: Delete missing points at 0.1.0 update. 2024-08-19 15:19:28 +02:00
Pierre-Antoine Rouby fa9ffccb19 Solver: Mage: Fix node name generator. 2024-08-09 17:10:24 +02:00
Pierre-Antoine Rouby 4a333ff988 Pamhyr2: Some minor fixes. 2024-08-09 10:32:15 +02:00
Pierre-Antoine Rouby ce8c4ebd91 DB: Some fixes for db update 0.1.0. 2024-08-09 10:16:19 +02:00
Pierre-Antoine Rouby bc157fcde7 DB: Some fixes for db update 0.1.0. 2024-08-07 17:20:49 +02:00
Pierre-Antoine Rouby 9a0476aa46 DB: Some fixes for db update 0.1.0. 2024-08-07 15:00:03 +02:00
Pierre-Antoine Rouby c7b5b40eda DB: Some fixes for db update 0.1.0. 2024-08-07 11:11:32 +02:00
Pierre-Antoine Rouby 74c61262b7 DB: Some fixes for db update to 0.1.0. 2024-08-07 10:46:44 +02:00
Pierre-Antoine Rouby 2290e2eb6d Pamhyr2: Fix pep8. 2024-08-06 16:06:08 +02:00
Pierre-Antoine Rouby 0d33072a7e Pamhyr2: Some fixes. 2024-08-06 16:05:04 +02:00
Pierre-Antoine Rouby 19d9a29459 LC, BC: Fix save for update db 0.1.0. 2024-08-06 15:04:42 +02:00
Pierre-Antoine Rouby d2e5eecf22 DB: Some fixes for update 0.1.0. 2024-08-05 18:00:32 +02:00
Pierre-Antoine Rouby b9ca6bf4a9 Geometry: Profile: Fix id at save. 2024-08-05 17:35:54 +02:00
Pierre-Antoine Rouby aa68acea3d DB: Minor change to 0.1.0 update. 2024-08-05 17:05:42 +02:00
Pierre-Antoine Rouby bf7b18f158 DB: Prepare update to 0.1.0. 2024-08-05 15:40:06 +02:00
Pierre-Antoine Rouby 3675dc5b9d DB: Prepare to update 0.1.0. 2024-08-05 14:55:26 +02:00
Pierre-Antoine Rouby 1dfdcb0413 DB: Continue to prepare update 0.1.0. 2024-08-05 12:00:10 +02:00
Pierre-Antoine Rouby 32cda58064 DB: Update: Minor fixes. 2024-07-31 15:36:34 +02:00
Pierre-Antoine Rouby 4081f96d68 DB: Fixes some table fk to prepare update to 0.1.0. 2024-07-31 15:13:12 +02:00
Pierre-Antoine Rouby 16350b4dec Pamhyr2: Some fixes to prepare db update to 0.1.0. 2024-07-31 11:23:43 +02:00
Pierre-Antoine Rouby d8fe1d8b81 Pamhyr2: Some fixes for db update to 0.1.0. 2024-07-31 10:29:04 +02:00
Pierre-Antoine Rouby 5525fff1fe pamhyr: Fix pep8. 2024-07-30 09:00:51 +02:00
Pierre-Antoine Rouby 328f5656bf DB: Fixes for db 0.1.0 creation. 2024-07-29 16:28:46 +02:00
Pierre-Antoine Rouby 9ec080a107 HS: Prepare update db 0.1.0. 2024-07-29 15:47:17 +02:00
Pierre-Antoine Rouby 1bae755f1c Geometry: Prepare update db 0.1.0. 2024-07-29 14:00:14 +02:00
Pierre-Antoine Rouby 9deccb465e SL: Prepare update db 0.1.0. 2024-07-29 11:27:24 +02:00
Pierre-Antoine Rouby 2ff635d0cd Friction, Strickers: Prepare db 0.1.0. 2024-07-26 11:47:40 +02:00
Pierre-Antoine Rouby 9f4b297d23 IC: Add prepare db 0.1.0. 2024-07-24 11:15:29 +02:00
Pierre-Antoine Rouby aea308a5a2 Reservoir: Prepare db update 0.1.0. 2024-07-23 15:04:05 +02:00
Pierre-Antoine Rouby 5c4948bd6d Pamhyr: Database: Update data dictionary and some change to prepare db
0.1.0.
2024-07-23 14:15:46 +02:00
Pierre-Antoine Rouby d78d9167a0 BC: Prepare db version 0.1.0, add pamhyr_id and scenario. 2024-07-22 15:31:32 +02:00
Pierre-Antoine Rouby 9c9fcc3dd8 REPFiles: Add Scenarios and 'pamhyr_id' and update DB to 0.0.14. 2024-07-22 14:17:38 +02:00
Pierre-Antoine Rouby 4271fbb85f SolverParameters: Add Scenario reference. 2024-07-22 11:49:33 +02:00
Pierre-Antoine Rouby ba4603e850 pamhyr: Fix Pep8. 2024-07-22 10:53:22 +02:00
Pierre-Antoine Rouby f2bc31c97b AddFile: Fix version update. 2024-07-19 16:46:33 +02:00
Pierre-Antoine Rouby 62a1726ef1 PamhyrDB, PamhyrID, AddFile: Fix some bugs. 2024-07-19 16:29:47 +02:00
Pierre-Antoine Rouby f19941f6da PamhyrDB: Add PamhyrID at parent class and add PamhyrID and
Scenario to AdditionalFiles.
2024-07-19 15:27:10 +02:00
Pierre-Antoine Rouby 3917b84b56 Status: Minor change. 2024-07-17 17:01:46 +02:00
Pierre-Antoine Rouby 3ecf97ac82 Scenarios: Add scenarios table in db and default scenario in study. 2024-07-17 16:04:49 +02:00
414 changed files with 26770 additions and 5618 deletions

No files matched your search

+9 -3
View File
@@ -41,8 +41,14 @@ __old__
*-venv
mage8
adists
mage8/
adists/
rubarbe/
linux/
windows/
install/
### END CUSTOM ###
@@ -624,7 +630,7 @@ TSWLatexianTemp*
# auto folder when using emacs and auctex
./auto/*
*.el
# *.el
# expex forward references with \gathertags
*-tags.tex
+88 -17
View File
@@ -1,5 +1,5 @@
# .gitlab-ci.yml -- Pamhyr gitlab-ci
# Copyright (C) 2023-2024 INRAE
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -24,6 +24,13 @@ stages:
- antivirus
- release
#########################
# PYINSTALLER VARIABLES #
#########################
variables:
HIDDEN_IMPORTS: "--hiddenimport rasterio --collect-submodules rasterio"
#############
# DOWNLOADS #
#############
@@ -31,6 +38,7 @@ stages:
variables:
MAGE_8_VERSION: "permalink/latest"
ADISTS_VERSION: "permalink/latest"
RUBAR_VERSION: "permalink/latest"
dl-mage8-doc:
stage: downloads
@@ -60,10 +68,6 @@ dl-mage8-linux:
artifacts:
paths:
- mage8-linux/mage
- mage8-linux/mage_as7
- mage8-linux/mage_extraire
- mage8-linux/mailleurTT
- mage8-linux/libbief.so
dl-mage8-windows:
stage: downloads
@@ -79,10 +83,6 @@ dl-mage8-windows:
artifacts:
paths:
- mage8-windows/mage.exe
- mage8-windows/mage_as7.exe
- mage8-windows/mage_extraire.exe
- mage8-windows/mailleurTT.exe
- mage8-windows/libbief.dll
dl-adists-linux:
stage: downloads
@@ -112,6 +112,36 @@ dl-adists-windows:
paths:
- adists-windows/adists.exe
dl-rubar-linux:
stage: downloads
tags:
- linux
rules:
- if: $CI_COMMIT_BRANCH == 'ci-test' || $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG
script:
- mkdir -p rubar-linux
- cd rubar-linux
- curl -L -o rubar.tgz https://forge.inrae.fr/river-hydraulics/rubarbe/-/releases/$RUBAR_VERSION/downloads/packages/rubar_linux.tgz
- tar xf rubar.tgz
artifacts:
paths:
- rubar-linux/rubarbe
dl-rubar-windows:
stage: downloads
tags:
- linux
rules:
- if: $CI_COMMIT_BRANCH == 'ci-test' || $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG
script:
- mkdir -p rubar-windows
- cd rubar-windows
- curl -L -o rubar.tgz https://forge.inrae.fr/river-hydraulics/rubarbe/-/releases/$RUBAR_VERSION/downloads/packages/rubar_windows.tgz
- tar xf rubar.tgz
artifacts:
paths:
- rubar-windows/rubarbe.exe
#############
# CONFIGURE #
#############
@@ -150,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
@@ -168,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
@@ -246,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
@@ -274,6 +304,8 @@ build-linux:
needs:
- job: dl-adists-linux
artifacts: true
- job: dl-rubar-linux
artifacts: true
- job: dl-mage8-linux
artifacts: true
- job: dl-mage8-doc
@@ -294,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 --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
@@ -315,6 +347,9 @@ build-linux:
# Copy adists
- mkdir -p pamhyr/adists
- cp -v ../adists-linux/* pamhyr/adists/
# Copy rubar
- mkdir -p pamhyr/rubar
- cp -v ../rubar-linux/* pamhyr/rubar/
# Copy Pamhyr
- cp -r dist/pamhyr/* pamhyr/
# Pamhyr script to force x11
@@ -380,6 +415,8 @@ build-windows:
artifacts: true
- job: dl-adists-windows
artifacts: true
- job: dl-rubar-windows
artifacts: true
- job: dl-mage8-doc
artifacts: true
- job: set-version
@@ -403,7 +440,7 @@ build-windows:
- python -m pip install -U -r ..\requirements.txt
# Run Pyinstaller
# - pyinstaller --noconsole -y ..\src\pamhyr.py
- pyinstaller -i ../src/View/ui/ressources/icon.ico --hide-console hide-early -y ..\src\pamhyr.py
- pyinstaller -i ../src/View/ui/ressources/icon.ico $HIDDEN_IMPORTS --hide-console hide-early -y ..\src\pamhyr.py
# Create directory
- mkdir pamhyr
- dir
@@ -582,6 +619,40 @@ pkg-check-clamav:
# RELEASE #
###########
shadow-release:
stage: release
tags:
- release
- linux
needs:
- job: linux-package-tar
artifacts: true
- job: windows-package-zip
artifacts: true
- job: windows-package-exe
artifacts: true
# - job: build-users-doc
# artifacts: true
- job: build-developers-doc
artifacts: true
- job: pkg-hash
artifacts: true
rules:
- if: $CI_COMMIT_TAG && $CI_COMMIT_TAG !~ /^v/
artifacts:
paths:
- linux/pamhyr-gnulinux.tar.xz
- windows/pamhyr-windows.zip
- windows/pamhyr-windows.exe
- linux/pamhyr-gnulinux.tar.xz.sha256
- windows/pamhyr-windows.zip.sha256
- windows/pamhyr-windows.exe.sha256
- doc/dev/documentation.pdf
# - doc/users/documentation.pdf
script:
- cd packages
tag-release:
stage: release
tags:
@@ -601,7 +672,7 @@ tag-release:
- job: pkg-hash
artifacts: true
rules:
- if: $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 - 2024
Pierre-Antoine ROUBY, INRAE, 2023 - 2026
Dylan JEANNIN, INRAE, 2026
+9 -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
@@ -18,11 +18,12 @@ modelling of rivers.
+ Define river sedimentary layers
+ Save the study in a unique file
+ Run Mage 8 on a study and visualize results
+ 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
@@ -30,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)
+20 -15
View File
@@ -1,18 +1,23 @@
#! /bin/sh
echo " PDF documentation.org"
emacs -nw --batch \
--eval "(require 'org)" \
--eval "(require 'ox-latex)" \
--eval "(load-file \"../tools/setup.el\")" \
--file ./documentation.org \
-f org-latex-export-to-pdf
FILES="documentation.org scenario.org"
echo " HTML documentation.org"
emacs -nw --batch \
--eval "(require 'org)" \
--eval "(require 'ox-latex)" \
--eval "(require 'ox-html)" \
--eval "(load-file \"../tools/setup.el\")" \
--file ./documentation.org \
-f org-html-export-to-html
for file in $FILES
do
echo " PDF $file"
emacs -nw --batch \
--eval "(require 'org)" \
--eval "(require 'ox-latex)" \
--eval "(load-file \"../tools/setup.el\")" \
--file ./$file \
-f org-latex-export-to-pdf \
echo " HTML $file"
emacs -nw --batch \
--eval "(require 'org)" \
--eval "(require 'ox-latex)" \
--eval "(require 'ox-html)" \
--eval "(load-file \"../tools/setup.el\")" \
--file ./$file \
-f org-html-export-to-html
done
+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
+661
View File
@@ -0,0 +1,661 @@
# senario.org -- Pamhyr developers documentation
# Copyright (C) 2023-2024 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 -*-
#+STARTUP: indent show2levels
#+INCLUDE: ../tools/macro.org
#+INCLUDE: ../tools/latex.org
#+TITLE: Scenario implementation in Pamhyr2: \textbf{Preparatory work}
#+SUBTITLE: Version: {{{version}}}
#+AUTHOR: {{{INRAE}}}
#+OPTIONS: toc:t
#+LANGUAGE: UKenglish
#+latex: \newpage
* COMMENT tools
#+begin_src emacs-lisp :export none
(add-to-list 'org-structure-template-alist
'("p" "#+begin_src python :python python3 :results output\n\n#+end_src"))
#+end_src
#+RESULTS:
#+begin_example
((p #+begin_src python :python python3 :results output
,#+end_src) (d . #+name: graph-XXX
,#+header: :results drawer
,#+header: :exports results
,#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-XXX", caption="Graph XXX", float="t")
,#+begin_src dot :file "images/graph-XXX.png" :cache no
digraph {
bgcolor="transparent";
node[colorscheme=set19,shape=box,style="filled",fillcolor=9];
}
,#+end_src) (p . src python :python python3 :results output :noweb yes
src) (t . EXPORT latex
\begin{table}
\centering
\resizebox{0.6\linewidth}{!}{
}
%\vspace{0.5pt}
%\caption{Number of tests by errors types}
\end{table}
,#+E) (B . src shell :session *shell* :results output :exports both) (a . export ascii) (c . center) (C . comment) (e . example) (E . export) (h . export html) (l . export latex) (q . quote) (s . src) (v . verse))
#+end_example
#+name: dbg-execute
#+begin_src python :python python3 :results output :noweb yes
def execute(db, query):
print(query)
return db.execute(query)
#+end_src
#+RESULTS: dbg-execute
#+name: execute
#+begin_src python :python python3 :results output :noweb yes
def execute(db, query):
return db.execute(query)
#+end_src
#+RESULTS: execute
* Principe
** Scenario
A scenario is defined by a name, a description and a parent
scenario. If a scenario as no parent, parent value is =None=. So, in
Pamhyr2 the list of scenario must by a tree with a root defined by a
default scenarios with no parent.
#+begin_src sql
CREATE TABLE scenario(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, -- Name of the scenario
description TEXT NOT NULL, -- Rich text description
parent_id INTEGER REFERENCES scenario(id) -- Recursive references
-- for parent scenario
)
#+end_src
** Data
Each data is associated with one and only one scenario. This senario
is defined by the =id=. So, each data structure who can saved in DB
must add the following lines in table définition:
#+begin_src sql
scenario_id INTEGER NOT NULL,
FOREIGN KEY(scenario_id) REFERENCES scenario(id)
#+end_src
To avoid, at least, memory usage exploitation, by default, a scenario
must saved only the modified data from its parent. This contrains
require to wrap any data modification to update the data scenario id
to current scenario id in source code. In addition, we need to modify
the data loading méthode to take in considération this recursive
dependencies.
#+CAPTION: Recursive loading method
#+begin_src python :python python3 :results output :noweb yes
def rec_load(cls, db, current_scenario):
# Default case, end of recursion
if current_scenario is None:
return [] # Default value
table = db.execute(f"<MY SELECT> WHERE scenario_id = {current_scenario}")
# If no data for this scenario, recursion
if len(table) == 0:
parent_id = db.get_parent_id(current_scenario)
return cls.rec_load(db, parent_id)
# Otherelse, parse data and return...
#+end_src
With this method, modify a data in parent scenario, modify this data
in child scenario too. Unless is value has been previously modified in
the child scenario. Deny tree node modification, except for leaf.
This method work, but it dont allow to delete data un child scenarios,
because if we dont found data for this scenario we search for parent
scenario. So, we need a condition to stop recursion in case of data
deletion in child scenario. To stop the recurtion, we can use a dummy
data flag. By default, this flags is =false=, but if this flags is
=true= the data is ignored at loading.
So, we can use this flags for each data structure who can saved in DB:
#+begin_src sql
is_dummy BOOLEAN, -- True if this data as been deleted
#+end_src
Modify the loading method:
#+CAPTION: Recursive loading method with possible dummy data
#+begin_src python :python python3 :results output :noweb yes
def rec_load_2(cls, db, current_scenario):
# Default case, end of recursion
if current_scenario is None:
return [] # Default value
table = db.execute(f"<MY SELECT> WHERE scenario_id = {current_scenario}")
# If no data for this scenario, recursion
if len(table) == 0:
parent_id = db.get_parent_id(current_scenario)
return cls.rec_load(db, parent_id)
for data in table:
if is_dummy(data):
continue
# Otherelse, parse data ...
# return ...
#+end_src
#+RESULTS:
And add a data reduction method:
#+caption: Data reduction method for data subtree
#+begin_src python :python python3 :results output :noweb yes
def reduce_change(self):
# Get the greater scenario
scenar = reduce(
lambda acc, x: max(acc, x._scenario),
self._values,
self._scenario
)
# Apply change on this subtree
self._scenario = scenar
if is_dummy(self): # There are no more subtree
self.values = []
else: # Update data subtree scenario id
for value in self._values:
value.set_scenario(self._scenario)
# Dummy data is no more necessary in this subtree
self._values = list(
filter(
lambda v: not is_dummy(v),
self._values
)
)
# Keep data id unique for all subtree
self.get_new_db_id()
#+end_src
#+RESULTS:
#+latex: \newpage
* Prototype
** Scenario
#+name: scenario
#+begin_src python :python python3 :results output :noweb yes
class Scenario():
def __init__(self, id, name, parent):
self._id = id
self._name = name
self._parent = parent
@classmethod
def create_db(cls, db):
execute(
db, """
CREATE TABLE scenario(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES scenario(id)
)
"""
)
@classmethod
def load_db(cls, db):
cur = execute(
db, f"SELECT * FROM scenario"
)
table = cur.fetchall()
new = []
for values in table:
new.append(
cls(
values[0], values[1], values[2],
)
)
return new
def save(self, db):
sid = 'NULL' if self._parent is None else self._parent
execute(
db,
"INSERT INTO scenario (id, name, parent_id) " +
f"VALUES ({self._id}, '{self._name}', {sid})"
)
def __repr__(self):
return f"Scenario {{ {self._id}, {self._name}, parent {self._parent} }}"
def __str__(self):
return f"{self._name}({self._id})"
#+end_src
#+RESULTS: scenario
** Data
#+name: data-b
#+begin_src python :python python3 :results output :noweb yes
class B():
def __init__(self, value, scenario):
self._value = value
self._scenario = scenario
self._dummy = False
@classmethod
def create_db(cls, db):
execute(db, """
CREATE TABLE b(
id INTEGER PRIMARY KEY AUTOINCREMENT,
dummy_data BOOLEAN,
x INTEGER,
a_id INTEGER NOT NULL,
scenario_id INTEGER NOT NULL,
FOREIGN KEY(a_id) REFERENCES a(id),
FOREIGN KEY(scenario_id) REFERENCES scenario(id)
)
""")
def save(self, db, a_id):
execute(
db,
"INSERT INTO b (x, dummy_data, a_id, scenario_id) " +
f"VALUES ({self._value}, {self._dummy}, {a_id}, {self._scenario})"
)
@classmethod
def load_db(cls, db, a_id, senar):
if senar is None:
return []
cur = execute(
db,
"SELECT x, scenario_id, dummy_data FROM b " +
f"WHERE scenario_id = {senar} AND a_id = {a_id}"
)
table = cur.fetchall()
if len(table) == 0:
parent = execute(
db,
f"SELECT parent_id FROM scenario WHERE id = {senar}"
).fetchone()
return cls.load_db(db, parent[0])
new = []
for values in table:
if values[2]: # Is dummy
continue
new.append(cls(values[0], values[1]))
return new
def set_value(self, value, scenario):
self._value = value
self._scenario = scenario
def set_dummy(self, scenario):
self._scenario = scenario
self._dummy = True
def __repr__(self):
if self._dummy:
return ""
return f"{self._value}"
def __str__(self):
if self._dummy:
return ""
return f"{self._value}"
#+end_src
#+RESULTS: data-b
#+name: data-a
#+begin_src python :python python3 :results output :noweb yes
class A():
def __init__(self, id, values, scenario):
self._id = id
self._values = values
self._scenario = scenario
self._dummy = False
@classmethod
def create_db(cls, db):
execute(db, """
CREATE TABLE a(
id INTEGER PRIMARY KEY,
dummy_data BOOLEAN,
scenario_id INTEGER,
FOREIGN KEY(scenario_id) REFERENCES scenario(id)
)
""")
def save(self, db):
self.reduce_change()
execute(db, f"DELETE FROM a WHERE scenario_id = {self._scenario}")
execute(
db,
"INSERT INTO a (id, dummy_data, scenario_id) " +
f"VALUES ({self._id}, {self._dummy}, {self._scenario})"
)
execute(
db,
"DELETE FROM b " +
f"WHERE scenario_id = {self._scenario} " +
f"AND a_id = {self._id}"
)
for value in self._values:
value.save(db, self._id)
def reduce_change(self):
diff_scenar = reduce(
lambda acc, x: max(acc, x._scenario),
self._values,
self._scenario
)
print(f" ~> Reduce: {self._scenario} to {diff_scenar}")
self._scenario = diff_scenar
# Reduce new scenario value on each value
for value in self._values:
value._scenario = self._scenario
# Delete useless dummy data
self._values = list(
filter(
lambda v: not v._dummy,
self._values
)
)
# HACK: keep id unique
self._id = self._id * 100 + diff_scenar
@classmethod
def load_db(cls, db, senar):
if senar is None:
return []
cur = execute(
db,
f"SELECT id, scenario_id, dummy_data FROM a WHERE scenario_id = {senar}"
)
table = cur.fetchall()
if len(table) == 0:
parent = execute(
db,
f"SELECT parent_id FROM scenario WHERE id = {senar}"
).fetchone()
return cls.load_db(db, parent[0])
new = []
for values in table:
if values[2]:
continue
bb = B.load_db(db, values[0], senar)
new.append(cls(values[0], bb, values[1]))
return new
def set_dummy(self, scenario):
self._scenario = scenario
self._dummy = True
def add(self, b):
self._values.append(b)
def delete(self, index, scenario):
self._values[index].set_dummy(scenario)
self._scenario = scenario
def __repr__(self):
if self._dummy:
return "{ }"
return f"{{ {self._values}, ({self._scenario}) }}"
def __str__(self):
if self._dummy:
return "{ }"
return f"{{ {list(map(str, self._values))}, ({self._scenario}) }}"
#+end_src
#+RESULTS: data-a
** Full prototype
#+begin_src python :python python3 :results output :exports both :noweb yes :cache yes
from functools import reduce
def execute(db, query):
# print(query)
return db.execute(query)
def get_tree(db):
from treelib import Node, Tree
tree = Tree()
for senar in db.execute("SELECT * FROM scenario").fetchall():
if senar[1] is None:
tree.create_node(
str(Scenario(senar[0], senar[1], senar[2])) +
" : " + str(A.load_db(db, senar[0])[0]),
senar[0]
)
else:
tree.create_node(
str(Scenario(senar[0], senar[1], senar[2])) +
" : " + str(A.load_db(db, senar[0])[0]),
senar[0],
parent=senar[2]
)
return tree
### --- Scenario ---
<<scenario>>
### --- class B ---
<<data-b>>
### --- class A ---
<<data-a>>
### --- Script ---
import sqlite3
import pprint
pp = pprint.PrettyPrinter(width=79, compact=True, depth=6)
with sqlite3.connect(":memory:") as db:
cur = db.cursor()
Scenario.create_db(cur)
A.create_db(cur)
B.create_db(cur)
print("--- Default scenario (s0) ---")
# One scenario
s0 = Scenario(0, "s0", None)
a0 = A(
1,
[
B(0, s0._id),
B(1, s0._id),
B(2, s0._id),
],
s0._id
)
s0.save(cur)
a0.save(cur)
print("--- New scenario (s1) -- copy without modification ---")
# New scenario
s1 = Scenario(1, "s1", s0._id)
s1.save(cur)
print("--- New scenario (s2) -- data modification ---")
# New scenario 2 with data
s2 = Scenario(2, "s2", s0._id)
s2.save(cur)
a2 = a0
bb = list(map(
lambda b: b.set_value(b._value + 1, s2._id),
a2._values
))
a2.save(cur)
print("--- New scenario (s3) -- delete data ---")
# New scenario 3 with data
s3 = Scenario(3, "s3", s0._id)
s3.save(cur)
a3 = A.load_db(cur, s3._id)[0]
a3._values[0].set_dummy(s3._id)
a3.save(cur)
print("--- New scenario (s4) -- new data ---")
s4 = Scenario(4, "s4", s3._id)
s4.save(cur)
a4 = A.load_db(cur, s3._id)[0]
a4.add(B(666, s4._id))
a4.save(cur)
print("--- Scenario tree ---")
tree = get_tree(db)
print(tree)
print("--- Modify s0 data ---")
aa = A.load_db(db, s0._id)[0]
bb = list(map(
lambda b: b.set_value(b._value * 100 - 1, s0._id),
aa._values
))
aa.save(db)
print("--- Scenario tree ---")
tree = get_tree(db)
print(tree)
print("--- DB ---")
ss = db.execute("SELECT * FROM scenario").fetchall()
print(f"+ scenario ({len(ss)}):")
pp.pprint(ss)
aa = db.execute("SELECT * FROM a").fetchall()
print(f"+ a ({len(aa)}):")
pp.pprint(aa)
bb = db.execute("SELECT * FROM b").fetchall()
print(f"+ b ({len(bb)}):")
pp.pprint(bb)
#+end_src
#+RESULTS[96c84f23be557425ae2822ef7f174175ef69f9b9]:
#+begin_example
--- Default scenario (s0) ---
~> Reduce: 0 to 0
--- New scenario (s1) -- copy without modification ---
--- New scenario (s2) -- data modification ---
~> Reduce: 0 to 2
--- New scenario (s3) -- delete data ---
~> Reduce: 0 to 3
--- New scenario (s4) -- new data ---
~> Reduce: 3 to 4
--- Scenario tree ---
s0(0) : { ['0', '1', '2'], (0) }
├── s1(1) : { ['0', '1', '2'], (0) }
├── s2(2) : { ['1', '2', '3'], (2) }
└── s3(3) : { ['1', '2'], (3) }
└── s4(4) : { ['1', '2', '666'], (4) }
--- Modify s0 data ---
~> Reduce: 0 to 0
--- Scenario tree ---
s0(0) : { ['-1', '99', '199'], (0) }
├── s1(1) : { ['-1', '99', '199'], (0) }
├── s2(2) : { ['1', '2', '3'], (2) }
└── s3(3) : { ['1', '2'], (3) }
└── s4(4) : { ['1', '2', '666'], (4) }
--- DB ---
+ scenario (5):
[(0, 's0', None), (1, 's1', 0), (2, 's2', 0), (3, 's3', 0), (4, 's4', 3)]
+ a (4):
[(10000, 0, 0), (10002, 0, 2), (10003, 0, 3), (1000304, 0, 4)]
+ b (14):
[(1, 0, 0, 100, 0), (2, 0, 1, 100, 0), (3, 0, 2, 100, 0), (4, 0, 1, 10002, 2),
(5, 0, 2, 10002, 2), (6, 0, 3, 10002, 2), (7, 0, 1, 10003, 3),
(8, 0, 2, 10003, 3), (9, 0, 1, 1000304, 4), (10, 0, 2, 1000304, 4),
(11, 0, 666, 1000304, 4), (12, 0, -1, 10000, 0), (13, 0, 99, 10000, 0),
(14, 0, 199, 10000, 0)]
#+end_example
View File
Whitespace-only changes.
+525
View File
@@ -0,0 +1,525 @@
# presentation.org -- Pamhyr
# Copyright (C) 2023 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 -*-
#+STARTUP: indent hideblocks content beamer
#+TITLE: \textbf{Pamhyr2}
#+SUBTITLE: Scenarios managements
# #+AUTHOR: Pierre-Antoine Rouby
#+LATEX_HEADER: \author[Pierre-Antoine Rouby]{\textbf{Pierre-Antoine Rouby}}
#+EMAIL: pierre-antoine.rouby@inrae.fr
#+INSTITUTE: INRAE Lyon-Villerbanne, teams River Hydraulics
#+LATEX_HEADER: \institute{INRAE Lyon-Villerbanne, River Hydraulics team}
#+DATE: 9th November
#+KEYWORDS: g ui, hydraulics, sedimentary, hydro-sedimentary, 1D modelling, freesoftware, foss
#+OPTIONS: H:2 toc:t num:t author:nil
#+LATEX_CLASS: beamer
#+LATEX_CLASS_OPTIONS: [slideopt,A4,showboxes,svgnames,aspectratio=169]
# #+LATEX_CLASS_OPTIONS: [presentation]
#+BEAMER_THEME: default
#+COLUMNS: %45ITEM %10BEAMER_ENV(Env) %10BEAMER_ACT(Act) %4BEAMER_COL(Col)
#+LANGUAGE: fr
#+LATEX_HEADER: \usepackage[frenchb]{babel}
#+BEGIN_SRC emacs-lisp :results silent :exports none
(setq org-latex-pdf-process (list
"latexmk -pdflatex='lualatex -shell-escape -interaction nonstopmode' -pdf -f %f"))
(add-to-list 'org-latex-packages-alist '("" "minted"))
(setq org-latex-listings 'minted)
(setq org-src-fontify-natively t)
(org-add-link-type
"cite" 'ebib
(lambda (path desc format)
(cond
((eq format 'html)
(format "(<cite>%s</cite>)" path))
((eq format 'text)
(format "[%s]" path))
((eq format 'latex)
(if (or (not desc) (equal 0 (search "cite:" desc)))
(format "\\cite{%s}" path)
(format "\\cite[%s][%s]{%s}"
(cadr (split-string desc ";"))
(car (split-string desc ";")) path))))))
#+END_SRC
# Wrapper
#+NAME: attr_wrap
#+HEADER: :var width="\\textwidth"
#+HEADER: :var caption=""
#+HEADER: :var smallcaption=""
#+HEADER: :var name=""
#+HEADER: :var data=""
#+HEADER: :var float="nil"
#+BEGIN_SRC sh :results output :exports none
echo "#+CAPTION[$smallcaption]: $caption"
echo "#+NAME: $name"
echo "#+ATTR_LATEX: :width $width :float $float"
echo "$data"
#+END_SRC
#+LATEX_HEADER: \setbeamertemplate{footline}[frame number]
#+LATEX_HEADER: \setbeamertemplate{headline}{}
#+LATEX_HEADER: \usepackage{multirow}
#+LATEX_HEADER: \usepackage{fontawesome5}
#+LATEX_HEADER: \usepackage{tcolorbox}
#+LATEX_HEADER: \usepackage{tikz}
#+LATEX_HEADER: \logo{\includegraphics[width=.1\textwidth]{../../../src/View/ui/ressources/Logo-INRAE.png}}
#+LATEX_HEADER: \newcommand{\R}[1]{\rotatebox[origin=c]{90}{#1}}
# Beamer colors
#+LATEX_HEADER: \setbeamersize{text margin left=0.5cm,text margin right=0.5cm}
#+LATEX_HEADER: \setbeamerfont{alerted text}{series=\bfseries}
#+LATEX_HEADER: \setbeamerfont{example text}{series=\bfseries}
#+LATEX_HEADER: \definecolor{inrae_1}{RGB}{102,193,191}
#+LATEX_HEADER: \definecolor{inrae_2}{RGB}{0,150,152}
#+LATEX_HEADER: \definecolor{inrae_3}{RGB}{39,86,98}
#+LATEX_HEADER: \setbeamercolor{alerted text}{fg=inrae_1}
#+LATEX_HEADER: \setbeamercolor{structure}{fg=inrae_2}
#+LATEX_HEADER: \setbeamercolor{normal text}{fg=inrae_3}
#+LATEX_HEADER: \setbeamercolor*{author}{fg=inrae_3}
#+LATEX_HEADER: \usepackage[absolute,showboxes,overlay]{textpos}
#+LATEX_HEADER: \TPshowboxesfalse
#+LATEX_HEADER: \textblockorigin{5mm}{0mm}
# Biblio
#+LATEX_HEADER: \usepackage{natbib}
# #+LATEX_HEADER: \bibliographystyle{apalike}
#+LATEX_HEADER: \setbeamertemplate{bibliography item}{}
#+LATEX_HEADER: \renewcommand\bibfont{\scriptsize}
#+LATEX_HEADER: \setbeamertemplate{frametitle continuation}[from second]
#+LATEX_HEADER: \setbeamercolor*{bibliography entry title}{fg=inrae_3}
#+LATEX_HEADER: \setbeamercolor*{bibliography entry author}{fg=inrae_3}
#+LATEX_HEADER: \setbeamercolor*{bibliography entry location}{fg=inrae_3}
#+LATEX_HEADER: \usepackage{bbding}
#+LATEX_HEADER: \usepackage{pdfpages}
# Renew title page
#+LATEX_HEADER: \defbeamertemplate*{title page}{customized}[1][]
#+LATEX_HEADER: {
# #+LATEX_HEADER: {
# #+LATEX_HEADER: \usebeamerfont{date}\usebeamercolor[fg]{date}
# #+LATEX_HEADER: \tikz[overlay,remember picture]
# #+LATEX_HEADER: \node[xshift=-1cm,yshift=-1cm,text width=10cm] at (current page.north east) {
# #+LATEX_HEADER: 7$^{th}$ International Conference\\
# #+LATEX_HEADER: 8TH-10TH November\\
# #+LATEX_HEADER: EDF Lab - CHATOU, FRANCE
# #+LATEX_HEADER: }
# #+LATEX_HEADER: }
#+LATEX_HEADER: {
#+LATEX_HEADER: \usebeamerfont{title}\inserttitle\par
#+LATEX_HEADER: \usebeamerfont{title}\usebeamercolor[fg]{title}
#+LATEX_HEADER: }
#+LATEX_HEADER: \bigskip
#+LATEX_HEADER: \begin{center}
#+LATEX_HEADER: \usebeamerfont{subtitle}\usebeamercolor[fg]{subtitle}
#+LATEX_HEADER: {
#+LATEX_HEADER: \LARGE\insertsubtitle\par
#+LATEX_HEADER: }
#+LATEX_HEADER: \bigskip
#+LATEX_HEADER: \usebeamerfont{author}\usebeamercolor[fg]{author}\insertauthor\par
#+LATEX_HEADER: \bigskip
#+LATEX_HEADER: \usebeamerfont{institute}\usebeamercolor[fg]{institute}\insertinstitute\par
#+LATEX_HEADER: \bigskip
#+LATEX_HEADER: \usebeamerfont{date}\usebeamercolor[fg]{date}\insertdate\par
#+LATEX_HEADER: \usebeamercolor[fg]{titlegraphic}\inserttitlegraphic
#+LATEX_HEADER: \end{center}
#+LATEX_HEADER: }
# #+LATEX_HEADER: \newcommand{\tred}[1]{\textcolor{rouge_inrae}{#1}}
#+TODO: TODO WIP TOREVIEW | DONE
#+MACRO: pamhyr \textsc{PAMHyR}
#+MACRO: pamhyr2 \textsc{Pamhyr2}
#+MACRO: pause \pause
#+MACRO: framebreak \framebreak
#+MACRO: no
# #+MACRO: OK \checkmark
#+MACRO: OK \faIcon{check}
#+MACRO: bf \textbf{$1}
# #+latex: \frame{\tocpage}
* Problématique
** Problématique
*** Utilisateur
- Définire une version modifier de l'étude (changement dans les
données)
- Comparé des résultats entre une variante et l'étude
{{{pause}}}
**** COMMENT Développeur
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
- Un unique fichier
- Limité la duplication de donnée à sauvegarder
- Garder une bonne cohérence entre données et résultats
{{{pause}}}
*** Scénarios
:PROPERTIES:
:BEAMER_COL: 1
:BEAMER_ENV: definition
:END:
Un scénario est une variate d'une étude.
** Problématique -- Avancer
*** Utilisateur
- Définire une version modifier d'un senarios
- Comparé des résultats entre plusieurs senarios
{{{pause}}}
**** COMMENT Développeur
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
- Un unique fichier
- Limité la duplication de donnée à sauvegarder
- Garder une bonne cohérence entre données et résultats
{{{pause}}}
*** Scénarios
:PROPERTIES:
:BEAMER_COL: 1
:BEAMER_ENV: definition
:END:
Un scénario peut être l'étude ou une variate d'un autre scénario.
** Problématique -- Encore plus avancer
*** Utilisateur
- Définire un ensemble fini de variante correspondant a un ensemble de
valeur pour un(des) paramètre(s) et executer un solveur sur
l'ensemble des variantes
- Pouvoir comparé des metrics sur un ensemble de résultats trop grand
pour une comparaisont à la mains
{{{pause}}}
**** COMMENT Développeur
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
- Un unique fichier
- Limité la duplication de donnée à sauvegarder
- Garder une bonne cohérence entre données et résultats
{{{pause}}}
*** Scénarios
:PROPERTIES:
:BEAMER_COL: 1
:BEAMER_ENV: definition
:END:
Un scénario peut être l'étude, une variate d'un scénarios ou un
ensemble fini de variantes d'un scénarios.
** Problématique -- Encore plus plus avancer
*** Utilisateur
- Définire un ensemble fini de variante correspondant a un ensemble de
valeur pour un(des) paramètre(s) et faire un nombre fini de tirage
aléatoire dans ces paramètre pour executer un solveur ces paramètres
- Pouvoir comparé des metrics sur un ensemble de résultats trop grand
pour une comparaisont à la mains
{{{pause}}}
*** Scénarios
:PROPERTIES:
:BEAMER_COL: 1
:BEAMER_ENV: definition
:END:
Un scénario peut être l'étude, une variate d'un scénarios ou un
ensemble fini ou un tirage aléatoire dans un ensemble de variantes
d'un scénarios.
** Problématique -- Développeur
*** Développeur
- Permetre a l'utilisateur de *garder une bonne cohérence entre
données et résultats* de façon claire
- Permetre a l'utilisateur de *géré les scénarios* de façon claire
(représentation graphique)
- Garder *un unique fichier* (=.pamhyr=)
- *Limité la duplication* de donnée à sauvegarder
- Permetre à l'utilisateur de garder les *résultats pertinant* sans
exploser la mémoire de l'ordinateur
* Proposition
** Proposition -- Concept
*** {{{no}}}
\vspace{-1.2cm}
**** Deux type de scénarios
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
- Scénarios simple
- Scénarios d'éxecution d'ensemble
{{{pause}}}
**** Scénarios simple
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
Est composer:
- d'un identifiant unique
- d'un nom (définie par l'utilisateur)
- d'une description (définie par l'utilisateur)
- d'un parent (définie par l'utilisateur)
{{{pause}}}
\vspace{-0.5cm}
*** Scénarios d'ensembles
**** {{{no}}}
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
\vspace{-1cm}
Est composer:
- d'un identifiant unique
- d'un nom (définie par l'utilisateur)
- d'une description (définie par l'utilisateur)
- d'un parent (définie par l'utilisateur)
- d'un ou plusieurs ensemble de valeur
**** {{{no}}}
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
\vspace{-0.5cm}
Un ensemble de valeurs est definie par:
- une valeurs de départ
- une valeurs de fin
- un pas de valeur
- un élément d'origine (élément définie dans l'étude)
- une fonctions (modification de l'élément à l'aide d'une valeur de
l'ensemble)
** Proposition -- Limitation
*** Scénarios simple
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
Si le scénarios est le *parent d'un autre scénarios*, il ne *peut plus
être modifier* sans risquer des comportement difficilement
comprehensible pour l'utilisateur.
{{{pause}}}
*** Scénarios d'ensembles
:PROPERTIES:
:BEAMER_COL: 0.5
:BEAMER_ENV: block
:END:
- Un scénarios d'ensemble *ne peut pas être parent* d'un autre
scénarios
- Le nombre d'exécution peut très rapidement grossir
** Proposition -- Affichage
#+name: graph-scenarios
#+header: :results drawer
#+header: :exports results
#+header: :post attr_wrap(width="7cm", data=*this*, name="graph-scenarios", caption="Exemple of scenarios graph", float="t")
#+begin_src dot :file "images/graph-scenarios.png" :cache no
digraph {
bgcolor="transparent";
node[colorscheme=pastel19,shape=ellipse,style="filled",fillcolor=9];
b[fillcolor=2];
aa[shape=box,fillcolor=2];
aa2[label="aa'", shape=box, fillcolor=2];
ad[shape=box, fillcolor=2];
re[shape=box, fillcolor=2];
rf[shape=box, fillcolor=2];
default -> a;
default -> b;
default -> c;
a -> aa;
a -> aa2;
a -> f -> rf;
c -> d -> ad;
c -> e -> re;
}
#+end_src
#+RESULTS[80ed2484b9cc71143f800ff23c10846fd04697c3]: graph-scenarios
:results:
#+CAPTION[]: Exemple of scenarios graph
#+NAME: graph-scenarios
#+ATTR_LATEX: :width 7cm :float t
[[file:images/graph-scenarios.png]]
:end:
* Solution technique
** Technique -- scénarios
#+begin_src sql
CREATE TABLE scenario(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, -- Name of the scenario
description TEXT NOT NULL, -- Rich text description
parent_id INTEGER REFERENCES scenario(id) -- Recursive references
-- for parent scenario
)
#+end_src
** Technique -- Données -- Enregistrement
#+begin_src sql
-- Unique ID (Must be unused, to delete)
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- New ID (not unique)
pamhyr_id INTEGER NOT NULL,
-- (Optional) Flag for set there is no data in this scenario and
-- ignore parent scenario
dummy BOOLEAN NOT NULL,
-- Corresponding scenario
scenario_id INTEGER NOT NULL,
PRIMARY KEY(pamhyr_id, scenario_id),
FOREIGN KEY(scenario_id) REFERENCES scenario(id),
#+end_src
** Technique -- Données -- Chargement
#+begin_src python :python python3 :results output :noweb yes
def load(cls, db, current_scenario):
# Default case, end of recursion
if current_scenario is None:
return [] # Default value
table = db.execute(f"<MY SELECT> WHERE scenario_id = {current_scenario}")
# If no data for this scenario, recursion
if len(table) == 0:
parent_id = db.get_parent_id(current_scenario)
return cls.load(db, parent_id)
# Otherelse, parse data and return...
#+end_src
** Technique -- Données -- Modification
#+begin_src python :python python3 :results output :noweb yes
def set_modified(self, current_scenario):
self.scenario = current_scenario
#+end_src
#+begin_src python :python python3 :results output :noweb yes
def delete(self, current_scenario):
self.deleted = True
self.set_modified(current_scenario)
#+end_src
** Technique -- Exécution d'ensemble
#+begin_src sql
CREATE TABLE range_set(
id INTEGER PRIMARY KEY AUTOINCREMENT,
start REAL NOT NULL, -- Range start
end REAL NOT NULL, -- Range end
step REAL NOT NULL, -- Range step
generator_name TEXT NOT NULL, -- Generator function name (ex:
-- 'minor_strickler')
original_data_id INTEGER, -- (optional) Ref to data who apply
-- this range (ex: a strickler id)
scenario_id INTEGER NOT NULL,
FOREIGN KEY(scenario_id) REFERENCES scenario(id)
)
#+end_src
* Sauvegarde des résultats
** Sauvegarde des résultats
*** Scénarios simple
Sauvegarde d'un seul résultats complet par scénarios (et par solver).
*** Scénarios d'ensemble
Sauvegarde de metrics récupéré sur un ensemble de résultats (pas de
sauvegarde de tous les résultats) ou certain résultats particuliers.
* Question & remarque
** Question & remarque
- Solution satisfaisante ?
- Cas d'usage intéressent non couvert ?
- Contre exemple ?
- Meilleur solution ?
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 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
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
guix shell --manifest=manifest.scm -- python3 ./src/pamhyr.py $@
+23
View File
@@ -0,0 +1,23 @@
;; manifest.scm -- Pamhyr2 Guix manifest
;; Copyright (C) 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/>.
(specifications->manifest
(list "python" "python-platformdirs" "python-lxml"
"python-pyqt@5" "python-pyqt5-sip"
"python-pyqtgraph" "python-qscintilla"
"python-numpy@1" "python-shapely" "python-pyshp"
"python-matplotlib" "python-colorama"
"python-rasterio" "python-psutil"))
+4
View File
@@ -46,6 +46,10 @@ copy /y ..\mage8-windows\mage_extraire.exe pamhyr\mage8\
copy /y ..\mage8-windows\mailleurTT.exe pamhyr\mage8\
copy /y ..\mage8-windows\libbief.dll pamhyr\mage8\
rem rubar
mkdir pamhyr\rubar
copy /y ..\rubar-windows\rubarbe.exe pamhyr\rubar\
rem adists
mkdir pamhyr\adists
copy /y ..\adists-windows\adists.exe 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 @@
# Adists.py -- Pamhyr study checkers
# Copyright (C) 2023-2024 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 @@
# Checker.py -- Pamhyr abstract checker class
# Copyright (C) 2023-2024 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 @@
# Mage.py -- Pamhyr MAGE checkers
# Copyright (C) 2023-2024 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 @@
# Study.py -- Pamhyr study checkers
# Copyright (C) 2023-2024 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 @@
# AMeshingTool.py -- Pamhyr
# Copyright (C) 2023-2024 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
+364
View File
@@ -0,0 +1,364 @@
# Internal.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from numpy import sign, interp
from copy import deepcopy
from tools import logger_color_red, logger_color_reset, logger_exception
from Meshing.AMeshingTool import AMeshingTool
from Model.Geometry.ProfileXYZ import ProfileXYZ
logger = logging.getLogger()
class InternalMeshing(AMeshingTool):
def __init__(self):
super(InternalMeshing, self).__init__()
###########
# Meshing #
###########
def meshing(self, reach,
step: float = 50,
limites=[-1, -1],
linear: bool = True):
# pre process
profiles = self.get_profiles(reach, limites)
if profiles is None:
return []
for p in profiles:
if len(p) < 1:
return []
guide_list = self.get_guide_list(reach, profiles)
if guide_list is None:
return []
# we never modify original profiles, we work on the copies
m_profiles = self.st_to_m(profiles, guide_list)
new_profiles = self.interpolate_transversal_step(m_profiles, step)
for new_profiles2 in new_profiles:
for p in new_profiles2:
p.hard_purge_aligned_points()
return new_profiles
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)
# we work between each couple of guidelines
for j, p in enumerate(profiles):
for i in range(len(guide_list) - 1):
index1 = p.named_point_index(guide_list[i])
index2 = p.named_point_index(guide_list[i+1])
if index2 - index1 > max_values[i]:
max_values[i] = index2 - index1
max_values_index[i] = j
for i in range(len(max_values)):
for isect in range(max_values_index[i], len(profiles)-1):
self.compl_sect(profiles[isect],
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,
profiles,
step):
# calcul number of intermediate profiles
np = []
for i in range(len(profiles)-1):
np.append(int((profiles[i+1].rk -
profiles[i].rk) / step) - 1)
if np[-1] < 0:
np[-1] = 0
new_profiles = []
for i in range(len(profiles)-1):
new_profiles2 = []
d = 1.0/float(np[i]+1)
for j in range(np[i]):
p = profiles[i].copy()
# RATIO entre les deux sections initiales
dj = float(j+1)*d
pt1 = profiles[i].points
pt2 = profiles[i+1].points
for k in range(len(pt1)):
p.points[k].x = pt1[k].x + \
dj*(pt2[k].x -
pt1[k].x)
p.points[k].y = pt1[k].y + \
dj*(pt2[k].y -
pt1[k].y)
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.name = f'interpol{p.rk}'
new_profiles2.append(p)
new_profiles.append(new_profiles2)
return new_profiles
def compl_sect(self, sect1, sect2, tag1, tag2):
# sect1: reference section
# sect2: section to complete
# start1 and 2: indices of the first point of the bed to complete
# end = start + len
# len1 and 2: number of intervals
# len1: target len
start1 = sect1.named_point_index(tag1)
start2 = sect2.named_point_index(tag1)
end1 = sect1.named_point_index(tag2)
end2 = sect2.named_point_index(tag2)
len1 = end1 - start1
len2 = end2 - start2
if len1 < len2:
self.compl_sect(sect2, sect1, tag1, tag2)
return
elif len1 == len2:
return
ltot1 = 0.0
ltot2 = 0.0
alpha = []
beta = []
# 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)
for i in range(len2):
ltot2 += sect2.point(start2+i).dist(sect2.point(start2+i+1))
beta.append(ltot2)
if len2 < 1 or ltot2 < 0.0001:
# unique point (copy len1 times)
# we are at an edge of the profile
for i in range(len1-len2):
p = sect2.point(start2).copy()
sect2.insert_point(start2, p)
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
if trouve:
len2 += 1
for i in reversed(range(j0, len2)):
beta[i+1] = beta[i]
if (beta[j0] == beta[j0-1]):
# rien
pass
else:
ratio = (alpha[j0] - beta[j0-1]) \
/ (beta[j0] - beta[j0-1])
if ratio < 0.0:
logger.warning(f"ratio négatif {ratio}")
# on double le point a gauche
p = sect2.point(start2+j0-1).copy()
sect2.insert_point(start2+j0-1, p)
sect2.point(start2+j0-1).name = ''
beta[j0] = beta[j0-1]
else:
p = sect2.point(start2+j0).copy()
sect2.insert_point(start2+j0, p)
sect2.point(start2+j0+1).name = ''
sect2.point(start2+j0+1).x = \
(1.0-ratio) * sect2.point(start2+j0).x + \
ratio * sect2.point(start2+j0+2).x
sect2.point(start2+j0+1).y = \
(1.0-ratio) * sect2.point(start2+j0).y + \
ratio * sect2.point(start2+j0+2).y
sect2.point(start2+j0+1).z = \
(1.0-ratio) * sect2.point(start2+j0).z + \
ratio * sect2.point(start2+j0+2).z
beta[j0] = (1.-ratio)*beta[j0-1] \
+ ratio*beta[j0+1]
if len1 > len2:
for i in range(len1 - len2):
p = sect2.point(start2+len2).copy()
sect2.insert_point(start2+len2, p)
sect2.point(start2+len2).name = ''
if tag1 != "un":
sect2.point(start2).name = tag1
if tag2 != "np":
sect2.point(start2+len2).name = tag2
sect2.modified()
def update_rk(self, reach, begin_rk, end_rk,
step: float = 50,
limites=[-1, -1],
origin=0,
directrices=['un', 'np'],
lplan: bool = False,
lm: int = 3,
linear: bool = False,
origin_value=0.0,
orientation=0):
if reach is None or len(reach.profiles) == 0:
return []
nprof = reach.number_profiles
if origin >= nprof or origin < 0:
logger.warning(
f"Origin outside reach"
)
return reach.get_rk()
# orientation is the orintation of the bief:
# 1: up -> downstream
# 2: down -> upstream
# else: keep current orientation
old_orientation = sign(reach.profiles[-1].rk - reach.profiles[0].rk)
if orientation == 1:
sgn = 1.0
elif orientation == 2:
sgn = -1.0
else:
sgn = old_orientation
if abs(sgn) < 0.5:
sgn = 1.0
new_rk = [0.0]*nprof
new_rk[origin] = origin_value
frictions = reach._parent.frictions
old_begin_rk = list(map(lambda x: x * old_orientation,
begin_rk))
old_end_rk = list(map(lambda x: x * old_orientation,
end_rk))
old_rk = list(map(lambda x: x * old_orientation,
reach.get_rk()))
if origin < nprof - 1:
for i in range(origin+1, nprof):
# 2D
d1 = reach.profiles[i-1].named_point(
directrices[0]
).dist_2d(reach.profiles[i].named_point(directrices[0]))
d2 = reach.profiles[i-1].named_point(
directrices[1]
).dist_2d(reach.profiles[i].named_point(directrices[1]))
new_rk[i] = new_rk[i-1] + (sgn * (d1 + d2) / 2)
if origin > 0:
for i in reversed(range(0, origin)):
# 2D
d1 = reach.profiles[i+1].named_point(
directrices[0]
).dist_2d(reach.profiles[i].named_point(directrices[0]))
d2 = reach.profiles[i+1].named_point(
directrices[1]
).dist_2d(reach.profiles[i].named_point(directrices[1]))
new_rk[i] = new_rk[i+1] - (sgn * (d1 + d2) / 2)
new_begin_rk = interp(old_begin_rk, old_rk, new_rk)
new_end_rk = interp(old_end_rk, old_rk, new_rk)
if abs(sgn - old_orientation) > 0.1: # orientation changed
return new_rk, new_end_rk, new_begin_rk
else:
return new_rk, new_begin_rk, new_end_rk
def get_guide_list(self, reach, profiles):
gl = reach.compute_guidelines()
# we make sure that the lines are in the left-to-right order
guide_list = [
[
x.name
for x in p.named_points()
if x.name in gl[0]
]
for p in profiles]
# we make sure the lines never cross
for gl in guide_list:
for i in range(len(guide_list[0])):
if gl[i] != guide_list[0][i]:
return None
return guide_list[0]
def get_profiles(self, reach, limites):
if reach is None or len(reach.profiles) == 0:
return None
if limites[0] > limites[1]:
lim = limites[1]
limites[1] = limites[0]
limites[0] = lim
elif limites[0] == limites[1]:
return None
profiles = [reach.profile(i).copy()
for i in range(limites[0], limites[1]+1)
]
return profiles
+8 -3
View File
@@ -1,5 +1,5 @@
# Mage.py -- Pamhyr
# Copyright (C) 2023-2024 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 os
import logging
import tempfile
from numpy import sign
from ctypes import (
cdll,
@@ -343,7 +344,9 @@ class MeshingWithMageMailleurTT(AMeshingTool):
)
)
)
logger.info("Waiting ...")
proc.waitForFinished()
logger.info(f"Waiting ... Done (status : {proc.exitCode()})")
if proc.exitCode() != 0:
logger.error(
@@ -352,7 +355,7 @@ class MeshingWithMageMailleurTT(AMeshingTool):
f"{logger_color_reset()}"
)
outputs = proc.readAllStandardOutput()
logger.debug(outputs)
logger.warning(outputs)
errors = proc.readAllStandardError()
logger.error(
@@ -418,7 +421,9 @@ class MeshingWithMageMailleurTT(AMeshingTool):
)
)
)
logger.debug("Waiting ...")
proc.waitForFinished()
logger.debug(f"Waiting ... Done (status : {proc.exitCode()})")
if proc.exitCode() != 0:
logger.error(
@@ -427,7 +432,7 @@ class MeshingWithMageMailleurTT(AMeshingTool):
f"{logger_color_reset()}"
)
outputs = proc.readAllStandardOutput()
logger.debug(outputs)
logger.warning(outputs)
errors = proc.readAllStandardError()
logger.error(
+1 -1
View File
@@ -1,5 +1,5 @@
# __init__.py -- Pamhyr
# Copyright (C) 2023-2024 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 -2
View File
@@ -1,5 +1,5 @@
# test_Model.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +21,6 @@ import unittest
import warnings
import tempfile
from Model.Saved import SavedStatus
from Model.Study import Study
from Model.River import River
+85 -34
View File
@@ -1,5 +1,5 @@
# AddFile.py -- Pamhyr
# Copyright (C) 2024 INRAE
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,31 +22,25 @@ from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
class AddFile(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, enabled=True,
name="", path="", text="",
status=None):
super(AddFile, self).__init__()
if id == -1:
self.id = AddFile._id_cnt
else:
self.id = id
self._status = status
status=None, owner_scenario=-1):
super(AddFile, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._enabled = enabled
self._name = f"File {self.id}" if name == "" else name
self._name = f"File #{self._pamhyr_id}" if name == "" else name
self._path = path
self._text = text
AddFile._id_cnt = max(id, AddFile._id_cnt+1)
def __getitem__(self, key):
value = None
@@ -71,7 +65,7 @@ class AddFile(SQLSubModel):
elif key == "text":
self._text = value
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -80,7 +74,7 @@ class AddFile(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
def is_enabled(self):
return self._enabled
@@ -92,7 +86,7 @@ class AddFile(SQLSubModel):
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def path(self):
@@ -101,7 +95,7 @@ class AddFile(SQLSubModel):
@path.setter
def path(self, path):
self._path = path
self._status.modified()
self.modified()
@property
def text(self):
@@ -110,38 +104,80 @@ class AddFile(SQLSubModel):
@text.setter
def text(self, text):
self._text = text
self._status.modified()
self.modified()
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE additional_files(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE additional_files{ext} (
{cls.create_db_add_pamhyr_id()},
enabled BOOLEAN NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
path TEXT NOT NULL,
text TEXT NOT NULL
text TEXT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
release = int(release)
if major == minor == "0":
if int(release) < 8:
if release < 8:
cls._db_create(execute)
return True
if 8 < release < 13:
cls._db_update_to_0_0_13(execute, data)
if major == "0" and minor == "1":
if release < 2:
execute(
"ALTER TABLE additional_files " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return True
@classmethod
def _db_update_to_0_0_13(cls, execute, data):
table = "additional_files"
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
"INSERT INTO additional_files_tmp " +
"(pamhyr_id, enabled, name, path, text, scenario) " +
"SELECT pamhyr_id, enabled, name, path, text, scenario " +
"FROM additional_files"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT id, enabled, name, path, text " +
"FROM additional_files"
"SELECT pamhyr_id, enabled, deleted, name, path, text, scenario " +
"FROM additional_files " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
@@ -149,30 +185,45 @@ class AddFile(SQLSubModel):
id = next(it)
enabled = (next(it) == 1)
deleted = (next(it) == 1)
name = next(it)
path = next(it)
text = next(it)
owner_scenario = next(it)
f = cls(
id=id, enabled=enabled, name=name, path=path, text=text,
status=data['status']
status=data['status'], owner_scenario=owner_scenario
)
if deleted:
f.set_as_deleted()
loaded.add(id)
new.append(f)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
sql = (
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"additional_files(id, enabled, name, path, text) " +
"additional_files(pamhyr_id, enabled, deleted, " +
"name, path, text, scenario) " +
"VALUES (" +
f"{self.id}, {self._enabled}, " +
f"{self._pamhyr_id}, " +
f"{self._db_format(self._enabled)}, " +
f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._path)}', " +
f"'{self._db_format(self._text)}'" +
f"'{self._db_format(self._text)}', " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
+6 -3
View File
@@ -1,5 +1,5 @@
# AddFileList.py -- Pamhyr
# Copyright (C) 2024 INRAE
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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.AdditionalFile.AddFile import AddFile
@@ -38,7 +38,10 @@ class AddFileList(PamhyrModelList):
ok = True
# Delete previous data
execute("DELETE FROM additional_files")
execute(
"DELETE FROM additional_files " +
f"WHERE scenario = {self._status.scenario_id}"
)
for af in self._lst:
ok &= af._db_save(execute, data)
+405 -94
View File
@@ -1,5 +1,5 @@
# BoundaryCondition.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -27,24 +27,193 @@ from tools import (
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class BoundaryCondition(SQLSubModel):
class Data(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "",
status=None):
super(BoundaryCondition, self).__init__()
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._status = status
self._types = types
self._data = [data0, data1]
if id == -1:
self.id = BoundaryCondition._id_cnt
else:
self.id = id
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_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,
bc INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(bc) REFERENCES boundary_condition(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) < 1:
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE boundary_condition_data " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "boundary_condition_data"
bcs = data['id2pid']['boundary_condition']
cls.update_db_add_pamhyr_id(
execute, table,
data=data
)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, ind, data0, data1, bc, scenario) " +
"SELECT pamhyr_id, ind, data0, data1, bc, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_bc_pid(execute, table, bcs)
@classmethod
def _db_update_to_0_2_0_set_bc_pid(cls, execute, table, bcs):
els = execute(
f"SELECT pamhyr_id, bc FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
bc_id = next(it)
execute(
f"UPDATE {table} " +
f"SET bc = {bcs[bc_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
bc = data["bc"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
values = execute(
"SELECT pamhyr_id, deleted, ind, " +
"data0, data1, scenario " +
"FROM boundary_condition_data " +
f"WHERE bc = {bc._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) == 1)
ind = next(it)
data0 = bc._types[0](next(it))
data1 = bc._types[1](next(it))
owner_scenario = next(it)
nd = cls(
data0, data1,
id=pid,
types=bc._types,
status=data['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):
if not self.must_be_saved():
return True
pid = self._pamhyr_id
ind = data["ind"]
data0 = self._db_format(str(self[0]))
data1 = self._db_format(str(self[1]))
bc = data["bc"]
execute(
"INSERT INTO " +
"boundary_condition_data(pamhyr_id, deleted, ind, " +
"data0, data1, bc, scenario) " +
f"VALUES (" +
f"{pid}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(data0)}', {self._db_format(data1)}, " +
f"{bc._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)
class BoundaryCondition(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1,
name: str = "",
status=None,
owner_scenario=-1):
super(BoundaryCondition, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._type = ""
@@ -53,37 +222,79 @@ class BoundaryCondition(SQLSubModel):
self._header = []
self._types = [float, float]
BoundaryCondition._id_cnt = max(BoundaryCondition._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE boundary_condition(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type TEXT NOT NULL,
tab TEXT NOT NULL,
node INTEGER,
FOREIGN KEY(node) REFERENCES river_node(id)
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),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
execute("""
CREATE TABLE boundary_condition_data(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
ind INTEGER NOT NULL,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
bc INTEGER,
FOREIGN KEY(bc) REFERENCES boundary_condition(id)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
return True
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE friction " +
"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
def _db_update_to_0_2_0(cls, execute, data):
table = "boundary_condition"
nodes = data['id2pid']['river_node']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
# Copy table
execute(
f"INSERT INTO {table}_tmp " +
f"(pamhyr_id, name, type, tab, node, scenario) " +
"SELECT pamhyr_id, name, type, tab, node, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_node_pid(execute, table, nodes)
@classmethod
def _get_ctor_from_type(cls, t):
@@ -110,83 +321,132 @@ class BoundaryCondition(SQLSubModel):
def _db_load(cls, execute, data=None):
new = []
tab = data["tab"]
nodes = data["nodes"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT id, name, type, node " +
"SELECT pamhyr_id, deleted, name, type, node, d50, sigma, " +
"scenario " +
"FROM boundary_condition " +
f"WHERE tab = '{tab}'"
f"WHERE tab = '{tab}' " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
for row in table:
t = row[2]
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
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)
bc = ctor(
id=row[0],
name=row[1],
status=data['status']
id=pid, name=name,
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 row[3] != -1:
bc.node = next(filter(lambda n: n.id == row[3], data["nodes"]),
None)
if node != -1:
bc.node = next(filter(lambda n: n.id == node, nodes), None)
values = execute(
"SELECT ind, data0, data1 FROM boundary_condition_data " +
f"WHERE bc = '{bc.id}'"
)
# Create dummy data list
for _ in values:
bc.add(0)
# Write data
for v in values:
ind = v[0]
data0 = bc._types[0](v[1])
data1 = bc._types[1](v[2])
# Replace data at pos ind
bc._data[ind] = (data0, data1)
data["bc"] = 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
tab = data["tab"]
execute(f"DELETE FROM boundary_condition WHERE id = {self.id}")
execute(f"DELETE FROM boundary_condition_data WHERE bc = {self.id}")
execute(
"DELETE FROM boundary_condition_data " +
f"WHERE bc = {self._pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
node = -1
if self._node is not None:
node = self._node.id
sql = (
d50 = 0.002
sigma = 1
if self._type == "SL":
d50 = self._d50
sigma = self._sigma
execute(
"INSERT INTO " +
"boundary_condition(id, name, type, tab, node) " +
"boundary_condition(" +
"pamhyr_id, deleted, name, type, tab, node, d50, sigma, scenario" +
") " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', '{tab}', {node}" +
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}" +
")"
)
execute(sql)
data["bc"] = self
ind = 0
for d in self._data:
data0 = self._db_format(str(d[0]))
data1 = self._db_format(str(d[1]))
data["ind"] = ind
d._db_save(execute, data)
sql = (
"INSERT INTO " +
"boundary_condition_data(ind, data0, data1, bc) " +
f"VALUES ({ind}, '{data0}', {data1}, {self.id})"
)
execute(sql)
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 el in self._data:
el._data_traversal(predicate, modifier, data)
def __len__(self):
return len(self._data)
return len(
list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
)
@classmethod
def compatibility(cls):
@@ -211,14 +471,14 @@ class BoundaryCondition(SQLSubModel):
@property
def name(self):
if self._name == "":
return f"B{self.id + 1}"
return f"B #{self.pamhyr_id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def bctype(self):
@@ -231,18 +491,41 @@ class BoundaryCondition(SQLSubModel):
@node.setter
def node(self, node):
self._node = node
self._status.modified()
self.modified()
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()
@property
def data(self):
return self._data.copy()
return list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
def get_type_column(self, column):
if 0 <= column < 2:
@@ -276,51 +559,79 @@ class BoundaryCondition(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,
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)
def delete_i(self, indexes):
self.modified()
def hard_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 delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self.data)
)
)
)
self._status.modified()
self.modified()
def delete(self, els):
self._data = list(
filter(
lambda e: e not in els,
self.data
)
)
self._status.modified()
for el in els:
el.set_as_deleted()
self.modified()
def undelete(self, els):
for el in els:
el.set_as_not_deleted()
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)
def get_i(self, index):
return self.data[index]
if len(self._data) == 0:
return None
return self._data[index]
def get_range(self, _range):
lst = []
@@ -329,10 +640,10 @@ class BoundaryCondition(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)
@@ -364,14 +675,14 @@ class BoundaryCondition(SQLSubModel):
next = index - 1
d = self._data
d[index], d[next] = d[next], d[index]
self._status.modified()
self.modified()
def move_down(self, index):
if index >= 0:
prev = index + 1
d = self._data
d[index], d[prev] = d[prev], d[index]
self._status.modified()
self.modified()
def reach(self, river):
r = []
@@ -1,5 +1,5 @@
# BoundaryConditionList.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelListWithTab
from Model.Tools.PamhyrListExt import PamhyrModelListWithTab
from Model.Except import NotImplementedMethodeError
from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition
@@ -41,9 +41,6 @@ class BoundaryConditionList(PamhyrModelListWithTab):
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
for tab in new._tabs:
data["tab"] = tab
new._tabs[tab] = BoundaryCondition._db_load(
@@ -53,10 +50,10 @@ class BoundaryConditionList(PamhyrModelListWithTab):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM boundary_condition")
if data is None:
data = {}
execute(
"DELETE FROM boundary_condition " +
f"WHERE scenario = {self._status.scenario_id}"
)
for tab in self._tabs:
data["tab"] = tab
@@ -1,5 +1,5 @@
# BoundaryConditionTypes.py -- Pamhyr
# Copyright (C) 2023-2024 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,8 +22,12 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition
class NotDefined(BoundaryCondition):
def __init__(self, id: int = -1, name: str = "", status=None):
super(NotDefined, self).__init__(id=id, name=name, status=status)
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"]
@@ -34,9 +38,12 @@ class NotDefined(BoundaryCondition):
class PonctualContribution(BoundaryCondition):
def __init__(self, id: int = -1, name: str = "", status=None):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(PonctualContribution, self).__init__(
id=id, name=name, status=status)
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "PC"
self._header = ["time", "discharge"]
@@ -48,8 +55,12 @@ class PonctualContribution(BoundaryCondition):
class TimeOverZ(BoundaryCondition):
def __init__(self, id: int = -1, name: str = "", status=None):
super(TimeOverZ, self).__init__(id=id, name=name, status=status)
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(TimeOverZ, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "TZ"
self._header = ["time", "z"]
@@ -61,9 +72,12 @@ class TimeOverZ(BoundaryCondition):
class TimeOverDischarge(BoundaryCondition):
def __init__(self, id: int = -1, name: str = "", status=None):
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(TimeOverDischarge, self).__init__(
id=id, name=name, status=status)
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "TD"
self._header = ["time", "discharge"]
@@ -75,8 +89,12 @@ class TimeOverDischarge(BoundaryCondition):
class ZOverDischarge(BoundaryCondition):
def __init__(self, id: int = -1, name: str = "", status=None):
super(ZOverDischarge, self).__init__(id=id, name=name, status=status)
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(ZOverDischarge, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "ZD"
self._header = ["z", "discharge"]
@@ -92,8 +110,12 @@ class ZOverDischarge(BoundaryCondition):
class Solid(BoundaryCondition):
def __init__(self, id: int = -1, name: str = "", status=None):
super(Solid, self).__init__(id=id, name=name, status=status)
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(Solid, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self.d50 = 0.002
self.sigma = 1
@@ -1,5 +1,5 @@
# BoundaryConditionsAdisTS.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -27,25 +27,194 @@ from tools import (
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class BoundaryConditionAdisTS(SQLSubModel):
class Data(SQLSubModel):
_sub_classes = []
_id_cnt = 0
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_adists{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_adists(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:
if not created:
cls._db_update_to_0_2_0(execute, data)
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "boundary_condition_data_adists"
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}_tmp " +
"(pamhyr_id, data0, data1, bca, scenario) " +
"SELECT pamhyr_id, data0, data1, bc, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_bca_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_bca_pid(cls, execute, data):
pid_bca = data["id2pid"]["boundary_condition_adists"]
els = execute(
"SELECT pamhyr_id, bca " +
"FROM boundary_condition_data_adists"
)
for row in els:
it = iter(row)
pid = next(it)
bca_id = next(it)
if bca_id == -1:
continue
execute(
f"UPDATE boundary_condition_data_adists " +
f"SET bca = {pid_bca[bca_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@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_adists " +
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_adists(" +
"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 BoundaryConditionAdisTS(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1,
pollutant: int = -1, status=None):
super(BoundaryConditionAdisTS, self).__init__()
pollutant: int = -1, status=None,
owner_scenario=-1):
super(BoundaryConditionAdisTS, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if id == -1:
self.id = BoundaryConditionAdisTS._id_cnt
else:
self.id = id
self._type = ""
self._node = None
self._pollutant = pollutant
@@ -53,119 +222,229 @@ class BoundaryConditionAdisTS(SQLSubModel):
self._header = []
self._types = [self.time_convert, float]
BoundaryConditionAdisTS._id_cnt = max(
BoundaryConditionAdisTS._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE boundary_condition_adists(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
pollutant INTEGER NOT NULL,
type TEXT NOT NULL,
node INTEGER,
FOREIGN KEY(pollutant) REFERENCES Pollutants(id),
FOREIGN KEY(node) REFERENCES river_node(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(pollutant) REFERENCES Pollutants(pamhyr_id),
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id)
)
""")
execute("""
CREATE TABLE boundary_condition_data_adists(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
bc INTEGER,
FOREIGN KEY(bc) REFERENCES boundary_condition_adists(id)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
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 not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "boundary_condition_adists"
nodes = data['id2pid']['river_node']
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}_tmp " +
"(pamhyr_id, pollutant, type, node, scenario) " +
"SELECT pamhyr_id, pollutant, type, node, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_node_pid(execute, table, nodes)
cls._db_update_to_0_2_0_set_pollutants_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_pollutants_pid(cls, execute, data):
pid_pol = data["id2pid"]["Pollutants"]
els = execute(
f"SELECT pamhyr_id, pollutant " +
"FROM boundary_condition_adists"
)
for row in els:
it = iter(row)
pid = next(it)
pol_id = next(it)
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]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
nodes = data['nodes']
scenario = data["scenario"]
pollutant = data.get("pollutant")
loaded = data['loaded_pid']
table = execute(
"SELECT id, pollutant, type, node " +
"FROM boundary_condition_adists"
if scenario is None:
return new
sql = (
"SELECT pamhyr_id, deleted, pollutant, type, node, scenario "
"FROM boundary_condition_adists " +
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:
bc = cls(
id=row[0],
pollutant=row[1],
status=data['status']
)
it = iter(row)
bc.type = row[2]
pid = next(it)
deleted = (next(it) == 1)
pollutant = next(it)
bc_type = next(it)
node = next(it)
owner_scenario = next(it)
bc = cls(
id=pid,
pollutant=pollutant,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
bc.type = bc_type
bc.node = None
if row[3] != -1:
tmp = next(filter(
lambda n: n.id == row[3], data["nodes"]),
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
values = execute(
"SELECT data0, data1 FROM " +
"boundary_condition_data_adists " +
f"WHERE bc = '{bc.id}'"
)
# Write data
for v in values:
data0 = bc._types[0](v[0])
data1 = bc._types[1](v[1])
# Replace data at pos ind
bc._data.append((data0, data1))
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(f"DELETE FROM boundary_condition_adists WHERE id = {self.id}")
execute(f"DELETE FROM boundary_condition_data_adists" +
f" WHERE bc = {self.id}")
execute(
"DELETE FROM boundary_condition_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"DELETE FROM boundary_condition_data_adists " +
f"WHERE bca = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
node = -1
if self._node is not None:
node = self._node
sql = (
execute(
"INSERT INTO " +
"boundary_condition_adists(id, pollutant, type, node) " +
"boundary_condition_adists(" +
"pamhyr_id, deleted, pollutant, type, " +
"node, scenario) " +
"VALUES (" +
f"{self.id}, {self._pollutant}, " +
f"'{self._db_format(self._type)}', {node}" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._pollutant}, " +
f"'{self._db_format(self._type)}', {node}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
data["bca"] = self
ind = 0
for d in self._data:
data0 = self._db_format(str(d[0]))
data1 = self._db_format(str(d[1]))
data["ind"] = ind
sql = (
"INSERT INTO " +
"boundary_condition_data_adists(data0, data1, bc) " +
f"VALUES ('{data0}', {data1}, {self.id})"
)
execute(sql)
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(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):
@@ -190,7 +469,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@node.setter
def node(self, node):
self._node = node
self._status.modified()
self.modified()
@property
def header(self):
@@ -199,7 +478,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@header.setter
def header(self, header):
self._header = header
self._status.modified()
self.modified()
@property
def pollutant(self):
@@ -208,7 +487,7 @@ class BoundaryConditionAdisTS(SQLSubModel):
@pollutant.setter
def pollutant(self, pollutant):
self._pollutant = pollutant
self._status.modified()
self.modified()
@property
def type(self):
@@ -217,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):
@@ -250,33 +534,36 @@ class BoundaryConditionAdisTS(SQLSubModel):
return (new_0, new_1)
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()
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)
@@ -291,10 +578,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-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Except import NotImplementedMethodeError
from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS \
@@ -38,21 +38,31 @@ 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
def _db_save(self, execute, data=None):
execute("DELETE FROM boundary_condition_adists")
execute("DELETE FROM boundary_condition_data_adists")
execute(
"DELETE FROM boundary_condition_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM boundary_condition_data_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for bc in self._lst:
bc._db_save(execute, data=data)
for bca in self._lst:
bca._db_save(execute, data=data)
return True
+144 -63
View File
@@ -1,5 +1,5 @@
# D90AdisTS.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -23,6 +23,7 @@ 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.D90AdisTS.D90AdisTSSpec import D90AdisTSSpec
@@ -33,107 +34,160 @@ class D90AdisTS(SQLSubModel):
_sub_classes = [
D90AdisTSSpec,
]
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "default",
status=None):
super(D90AdisTS, self).__init__()
status=None, owner_scenario=-1):
super(D90AdisTS, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if id == -1:
self.id = D90AdisTS._id_cnt
else:
self.id = id
self._name = name
self._d90 = None
self._enabled = True
self._data = []
D90AdisTS._id_cnt = max(
D90AdisTS._id_cnt + 1,
self.id
)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE d90_adists(
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
d90 REAL NOT NULL,
enabled BOOLEAN NOT NULL
)
""")
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE d90_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
d90 REAL NOT NULL,
enabled BOOLEAN NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == 0 and minor < 1:
if int(release) < 6:
created = False
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 not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "d90_adists"
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}_tmp " +
"(pamhyr_id, name, d90, enabled, scenario) " +
"SELECT pamhyr_id, name, d90, enabled, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@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 id, name, d90, enabled " +
"FROM d90_adists"
"SELECT pamhyr_id, deleted, name, d90, enabled, scenario " +
"FROM d90_adists " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
if table is not None:
for row in table:
d90_id = row[0]
name = row[1]
d90 = row[2]
enabled = (row[3] == 1)
it = iter(row)
D90 = cls(
id=d90_id,
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
value_d90 = next(it)
enabled = (next(it) == 1)
owner_scenario = next(it)
d90 = cls(
id=pid,
name=name,
status=data['status']
status=status,
owner_scenario=owner_scenario
)
if deleted:
d90.set_as_deleted()
D90.d90 = d90
D90.enabled = enabled
d90.d90 = value_d90
d90.enabled = enabled
data['d90_default_id'] = d90_id
D90._data = D90AdisTSSpec._db_load(execute, data)
data['d90_default_id'] = pid
d90._data = D90AdisTSSpec._db_load(execute, data)
new.append(D90)
loaded.add(pid)
new.append(d90)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
execute(f"DELETE FROM d90_adists WHERE id = {self.id}")
if not self.must_be_saved():
return True
execute(
f"DELETE FROM d90_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
d90 = -1.
if self.d90 is not None:
d90 = self.d90
sql = (
execute(
"INSERT INTO " +
"d90_adists(" +
"id, name, d90, enabled" +
"pamhyr_id, deleted, " +
"name, d90, enabled, scenario" +
") " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"{d90}, {self._enabled}" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"{d90}, {self._enabled}, {self._status.scenario_id}" +
")"
)
execute(sql)
data['d90_default_id'] = self.id
execute(
"DELETE FROM d90_spec " +
f"WHERE d90_default = {self.id}"
"DELETE FROM d90_adists_spec " +
f"WHERE d90_default = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
for d90_spec in self._data:
@@ -141,6 +195,16 @@ class D90AdisTS(SQLSubModel):
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(self._data)
@@ -151,7 +215,7 @@ class D90AdisTS(SQLSubModel):
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def d90(self):
@@ -160,7 +224,7 @@ class D90AdisTS(SQLSubModel):
@d90.setter
def d90(self, d90):
self._d90 = d90
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -169,28 +233,45 @@ class D90AdisTS(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
def new(self, index):
n = D90AdisTSSpec(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()
+6 -3
View File
@@ -1,5 +1,5 @@
# D90AdisTSList.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.D90AdisTS.D90AdisTS import D90AdisTS
@@ -42,7 +42,10 @@ class D90AdisTSList(PamhyrModelList):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM d90_adists")
execute(
"DELETE FROM d90_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
+130 -60
View File
@@ -1,5 +1,5 @@
# D90AdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2024 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,25 +22,21 @@ 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 D90AdisTSSpec(SQLSubModel):
_sub_classes = [
]
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "",
status=None):
super(D90AdisTSSpec, self).__init__()
self._status = status
if id == -1:
self.id = D90AdisTSSpec._id_cnt
else:
self.id = id
status=None, owner_scenario=-1):
super(D90AdisTSSpec, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name_section = name
self._reach = None
@@ -49,62 +45,129 @@ class D90AdisTSSpec(SQLSubModel):
self._d90 = None
self._enabled = True
D90AdisTSSpec._id_cnt = max(D90AdisTSSpec._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE d90_spec(
id INTEGER NOT NULL PRIMARY KEY,
d90_default INTEGER NOT NULL,
name TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
d90 REAL NOT NULL,
enabled BOOLEAN NOT NULL,
FOREIGN KEY(d90_default) REFERENCES d90_adists(id),
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE d90_adists_spec{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
d90_default INTEGER NOT NULL,
name TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
d90 REAL NOT NULL,
enabled BOOLEAN NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(d90_default) REFERENCES d90_adists(pamhyr_id),
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == 0 and minor < 1:
if int(release) < 6:
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "d90_spec"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "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)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table_new}_tmp " +
"(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}"
)
execute(f"DROP TABLE {table}")
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_default FROM d90_adists_spec"
)
for row in els:
it = iter(row)
pid = next(it)
d90_id = next(it)
if d90_id == -1:
continue
execute(
f"UPDATE d90_adists_spec " +
f"SET d90_default = {pid_d90[d90_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@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 id, d90_default, name, reach, start_rk, end_rk, " +
"d90, enabled " +
"FROM d90_spec " +
f"WHERE d90_default = {data['d90_default_id']} "
"SELECT pamhyr_id, deleted, " +
f"name, reach, start_rk, end_rk, " +
"d90, enabled, scenario " +
"FROM d90_adists_spec " +
f"WHERE d90_default = {data['d90_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:
id = row[0]
name = row[2]
reach = row[3]
start_rk = row[4]
end_rk = row[5]
d90 = row[6]
enabled = (row[7] == 1)
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
reach = next(it)
start_rk = next(it)
end_rk = next(it)
d90 = next(it)
enabled = (next(it) == 1)
owner_scenario = next(it)
new_spec = cls(
id=id,
id=pid,
name=name,
status=data['status']
status=status,
owner_scenario=owner_scenario
)
if deleted:
new_spec.set_as_deleted()
new_spec.reach = reach
new_spec.start_rk = start_rk
@@ -112,29 +175,36 @@ class D90AdisTSSpec(SQLSubModel):
new_spec.d90 = d90
new_spec.enabled = enabled
loaded.add(pid)
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
d90_default = data['d90_default_id']
sql = (
execute(
"INSERT INTO " +
"d90_spec(id, d90_default, name, reach, " +
"start_rk, end_rk, d90, enabled) " +
"d90_adists_spec(pamhyr_id, deleted, " +
"d90_default, name, reach, " +
"start_rk, end_rk, d90, enabled, scenario) " +
"VALUES (" +
f"{self.id}, " +
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}, " +
f"{self._end_rk}, " +
f"{self._d90}, " +
f"{self._enabled}" +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._d90}, {self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
@@ -145,7 +215,7 @@ class D90AdisTSSpec(SQLSubModel):
@name.setter
def name(self, name):
self._name_section = name
self._status.modified()
self.modified()
@property
def reach(self):
@@ -154,7 +224,7 @@ class D90AdisTSSpec(SQLSubModel):
@reach.setter
def reach(self, reach):
self._reach = reach
self._status.modified()
self.modified()
@property
def start_rk(self):
@@ -163,7 +233,7 @@ class D90AdisTSSpec(SQLSubModel):
@start_rk.setter
def start_rk(self, start_rk):
self._start_rk = start_rk
self._status.modified()
self.modified()
@property
def end_rk(self):
@@ -172,7 +242,7 @@ class D90AdisTSSpec(SQLSubModel):
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self._status.modified()
self.modified()
@property
def d90(self):
@@ -181,7 +251,7 @@ class D90AdisTSSpec(SQLSubModel):
@d90.setter
def d90(self, d90):
self._d90 = d90
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -190,4 +260,4 @@ class D90AdisTSSpec(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
+142 -64
View File
@@ -1,5 +1,5 @@
# DIFAdisTS.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -23,6 +23,7 @@ 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.DIFAdisTS.DIFAdisTSSpec import DIFAdisTSSpec
@@ -33,19 +34,16 @@ class DIFAdisTS(SQLSubModel):
_sub_classes = [
DIFAdisTSSpec,
]
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "default",
status=None):
super(DIFAdisTS, self).__init__()
status=None, owner_scenario=-1):
super(DIFAdisTS, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if id == -1:
self.id = DIFAdisTS._id_cnt
else:
self.id = id
self._name = name
self._method = None
self._dif = None
@@ -55,59 +53,98 @@ class DIFAdisTS(SQLSubModel):
self._types = ["iwasa", "fisher", "elder", "constante", "generique"]
self._data = []
DIFAdisTS._id_cnt = max(
DIFAdisTS._id_cnt + 1,
self.id
)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE dif_adists(
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
method TEXT NOT NULL,
dif REAL NOT NULL,
b REAL,
c REAL,
enabled BOOLEAN NOT NULL
)
""")
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE dif_adists{ext}(
{cls.create_db_add_pamhyr_id()},
name TEXT NOT NULL,
method TEXT NOT NULL,
dif REAL NOT NULL,
b REAL,
c REAL,
enabled BOOLEAN NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == 0 and minor < 1:
if int(release) < 6:
created = False
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 not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "dif_adists"
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}_tmp " +
"(pamhyr_id, name, method, dif, b, c, enabled, scenario) " +
"SELECT pamhyr_id, name, method, dif, b, c, enabled, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@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 id, name, method, dif, b, c, enabled " +
"FROM dif_adists"
"SELECT pamhyr_id, name, method, dif, b, c, enabled, scenario " +
"FROM dif_adists " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
if table is not None:
for row in table:
dif_id = row[0]
name = row[1]
method = row[2]
dif = row[3]
b = row[4]
c = row[5]
enabled = (row[6] == 1)
it = iter(row)
pid = next(it)
name = next(it)
method = next(it)
dif = next(it)
b = next(it)
c = next(it)
enabled = (next(it) == 1)
owner_scenario = next(it)
DIF = cls(
id=dif_id,
id=pid,
name=name,
status=data['status']
status=status,
owner_scenario=owner_scenario,
)
DIF.method = method
@@ -116,15 +153,27 @@ class DIFAdisTS(SQLSubModel):
DIF.c = c
DIF.enabled = enabled
data['dif_default_id'] = dif_id
data['dif_default_id'] = pid
DIF._data = DIFAdisTSSpec._db_load(execute, data)
loaded.add(pid)
new.append(DIF)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
execute(f"DELETE FROM dif_adists WHERE id = {self.id}")
if not self.must_be_saved():
return True
execute(
"DELETE FROM dif_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
method = ""
if self.method is not None:
@@ -139,18 +188,19 @@ class DIFAdisTS(SQLSubModel):
b = self.b
c = -1.
if self.dif is not None:
if self.c is not None:
c = self.c
sql = (
"INSERT INTO " +
"dif_adists(" +
"id, name, method, dif, b, c, enabled" +
"pamhyr_id, name, method, dif, b, c, enabled, scenario" +
") " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"'{self._db_format(self._method)}', " +
f"{dif}, {b}, {c}, {self._enabled}" +
f"{dif}, {b}, {c}, {self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
@@ -158,8 +208,9 @@ class DIFAdisTS(SQLSubModel):
data['dif_default_id'] = self.id
execute(
"DELETE FROM dif_spec " +
f"WHERE dif_default = {self.id}"
"DELETE FROM dif_adists_spec " +
f"WHERE dif_default = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
for dif_spec in self._data:
@@ -167,6 +218,16 @@ class DIFAdisTS(SQLSubModel):
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(self._data)
@@ -177,7 +238,7 @@ class DIFAdisTS(SQLSubModel):
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def method(self):
@@ -186,7 +247,7 @@ class DIFAdisTS(SQLSubModel):
@method.setter
def method(self, method):
self._method = method
self._status.modified()
self.modified()
@property
def types(self):
@@ -199,7 +260,7 @@ class DIFAdisTS(SQLSubModel):
@dif.setter
def dif(self, dif):
self._dif = dif
self._status.modified()
self.modified()
@property
def b(self):
@@ -208,7 +269,7 @@ class DIFAdisTS(SQLSubModel):
@b.setter
def b(self, b):
self._b = b
self._status.modified()
self.modified()
@property
def c(self):
@@ -217,7 +278,7 @@ class DIFAdisTS(SQLSubModel):
@c.setter
def c(self, c):
self._c = c
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -226,28 +287,45 @@ class DIFAdisTS(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
def new(self, index):
n = DIFAdisTSSpec(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()
+6 -3
View File
@@ -1,5 +1,5 @@
# DIFAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.DIFAdisTS.DIFAdisTS import DIFAdisTS
@@ -42,7 +42,10 @@ class DIFAdisTSList(PamhyrModelList):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM dif_adists")
execute(
"DELETE FROM dif_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
+127 -53
View File
@@ -1,5 +1,5 @@
# DIFAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 @@ from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
@@ -32,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
@@ -51,66 +50,134 @@ 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):
execute("""
CREATE TABLE dif_spec(
id INTEGER NOT NULL PRIMARY KEY,
dif_default INTEGER NOT NULL,
method TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
dif REAL NOT NULL,
b REAL,
c REAL,
enabled BOOLEAN NOT NULL,
FOREIGN KEY(dif_default) REFERENCES dif_adists(id),
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE dif_adists_spec{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
dif_default INTEGER NOT NULL,
method TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
dif REAL NOT NULL,
b REAL,
c REAL,
enabled BOOLEAN NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(dif_default) REFERENCES dif_adists(id),
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == 0 and minor < 1:
if int(release) < 6:
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "dif_spec"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "dif_spec"
table_new = "dif_adists_spec"
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, dif_default, " +
"method, reach, start_rk, end_rk, " +
"dif, b, c, scenario) " +
"SELECT pamhyr_id, dif_default, " +
"method, reach, start_rk, end_rk, " +
"dif, b, c, 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)
cls._db_update_to_0_2_0_set_dif_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_dif_pid(cls, execute, data):
pid_dif = data["id2pid"]["dif_adists"]
els = execute(
"SELECT pamhyr_id, dif_default FROM dif_adists_spec"
)
for row in els:
it = iter(row)
pid = next(it)
dif_id = next(it)
if dif_id == -1:
continue
execute(
f"UPDATE dif_adists_spec" +
f"SET dif_default = {pid_dif[dif_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@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 id, dif_default, method, reach, start_rk, end_rk, " +
"dif, b, c, enabled " +
"FROM dif_spec " +
f"WHERE dif_default = {data['dif_default_id']} "
"SELECT pamhyr_id, deleted, method, reach, start_rk, end_rk, " +
"dif, b, c, enabled, scenario " +
"FROM dif_adists_spec " +
f"WHERE dif_default = {data['dif_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:
id = row[0]
method = row[2]
reach = row[3]
start_rk = row[4]
end_rk = row[5]
dif = row[6]
b = row[7]
c = row[8]
enabled = (row[9] == 1)
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
method = next(it)
reach = next(it)
start_rk = next(it)
end_rk = next(it)
dif = next(it)
b = next(it)
c = next(it)
enabled = (next(it) == 1)
owner_scenario = next(it)
new_spec = cls(
id=id,
method=method,
status=data['status']
status=status,
owner_scenario=owner_scenario
)
if deleted:
new_spec.set_as_deleted()
new_spec.reach = reach
new_spec.start_rk = start_rk
@@ -120,28 +187,35 @@ class DIFAdisTSSpec(SQLSubModel):
new_spec.c = c
new_spec.enabled = enabled
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
dif_default = data['dif_default_id']
sql = (
"INSERT INTO " +
"dif_spec(id, dif_default, method, reach, " +
"start_rk, end_rk, dif, b, c, enabled) " +
"dif_adists_spec(pamhyr_id, deleted, " +
"dif_default, method, reach, " +
"start_rk, end_rk, dif, b, c, enabled, scenario) " +
"VALUES (" +
f"{self.id}, " +
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}, " +
f"{self._end_rk}, " +
f"{self._dif}, " +
f"{self._b}, " +
f"{self._c}, " +
f"{self._enabled}" +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._dif}, {self._b}, {self._c}, " +
f"{self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
+307
View File
@@ -0,0 +1,307 @@
# Distribution.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import struct
import logging
import itertools
import numpy as np
from tools import logger_exception
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
logger = logging.getLogger()
class Distribution(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Distribution, self).__init__(
id=id, status=status,
owner_scenario=0
)
self._name = name
self._type = "generic"
self._script = script
self._labels = []
self._default_values = []
self._params_checkers = []
self._params_tips = []
@property
def name(self):
return self._name
@property
def labels(self):
return self._labels
@property
def default_values(self):
return self._default_values
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble_distribution{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
script TEXT NOT NULL,
PRIMARY KEY(pamhyr_id)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, script " +
"FROM ensemble_distribution " +
f"WHERE pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
script = next(it)
new_distribution = cls(
id, name=name, script=script,
status=data["status"]
)
if deleted:
new_distribution.set_as_deleted()
loaded.add(id)
new.append(new_distribution)
return new
def _db_save(self, execute, data=None):
# Only save custom distribution
if self._type != "custom":
return True
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"ensemble_distribution(pamhyr_id, deleted, " +
" name, script) " +
"VALUES (?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._script
)
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
def is_valid_parameters(self, parameters):
valid = True
tips = []
for i, checker in enumerate(self._params_checkers):
ok = checker(parameters)
if not ok:
tips.append(self._params_tips[i])
valid = False
return (valid, tips)
def check_params(self, params):
return True
def random(self, r):
raise NotImplementedMethodeError(cls, cls._load)
###############################
# DISTRIBUTION IMPLEMENTATION #
###############################
class Uniform(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "uniform", script: str = "",
status=None):
super(Uniform, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["low", "high"]
self._default_values = [20.0, 40.0]
self._params_checkers = [self.check_params, self.check_params]
self._params_tips = ["uniform_low_tips", "uniform_high_tips"]
def check_params(self, params):
return (params[0] < params[1])
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.uniform(*lst)
class Normal(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "normal", script: str = "",
status=None):
super(Normal, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["loc", "scale"]
self._default_values = [30.0, 5.0]
self._params_checkers = [self.check_params, self.check_scale]
self._params_tips = ["normal_loc_tips", "normal_scale_tips"]
def check_scale(self, params):
return 0 <= params[1]
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.normal(*lst)
class Binomial(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "binomial", script: str = "",
status=None):
super(Binomial, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["binomial_n", "binomial_p"]
self._default_values = [100.0, 0.3]
self._params_checkers = [self.check_n, self.check_p]
self._params_tips = ["binomial_n_tips", "binomial_p_tips"]
def check_n(self, params):
n = params[0]
return (0 <= n) and (n == round(n))
def check_p(self, params):
p = params[1]
return 0 <= p <= 1
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.binomial(*lst)
class Poisson(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "poisson", script: str = "",
status=None):
super(Poisson, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["poisson_lam"]
self._default_values = [5.0]
self._params_checkers = [self.check_lam]
self._params_tips = ["poisson_lam_tips"]
def check_lam(self, params):
lam = params[0]
return 0 <= lam
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.poisson(*lst)
class Custom(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Custom, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["custom"]
self._type = "custom"
self._default_values = [""]
self._params_checkers = [self.check_params]
self._params_tips = ["custom_params_tips"]
def random(self, params, nb):
try:
# Run script to (re)define the sample distribution
f = eval(self._script)
return f(params, nb)
except Exception as e:
logger_exception(e)
return None # TODO: Raise helpful exception
+62
View File
@@ -0,0 +1,62 @@
# DistributionList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Distribution import *
class DistributionList(PamhyrModelList):
_sub_classes = [Distribution]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
# Default distributions
d = {"status": data['status']}
new._lst = [
Uniform(**d),
Normal(**d),
Binomial(**d),
Poisson(**d),
]
# DB custom distributions
new._lst += Distribution._db_load(
execute, data
)
return new
def _db_save(self, execute, data={}):
execute(
"DELETE FROM ensemble_distribution"
)
for fun in self._lst:
fun._db_save(execute, data)
return True
def new(self, index):
r = Custom(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
+279
View File
@@ -0,0 +1,279 @@
# Ensemble.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import struct
import logging
import itertools
import numpy as np
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Ensembles.Distribution import Uniform
logger = logging.getLogger()
class Ensemble(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
distribution=None, params=[0., 100.],
data_type=None, target_data=None,
status=None, owner_scenario=-1):
super(Ensemble, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._data_type = data_type
self._distribution = distribution
self._params = params
self._target_data = target_data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type VARCHAR(8) NOT NULL,
distribution INTEGER NOT NULL,
distribution_name VARCHAR(32) NOT NULL,
params BLOB NOT NULL,
params_len INTEGER NOT NULL,
data_pid INTEGER NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
distributions = data["ens_distributions"]
stricklers = data["stricklers"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"distribution, distribution_name, params, params_len, data_pid, " +
"scenario " +
"FROM ensemble " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
data_type = next(it)
fid = next(it)
fname = next(it)
params = next(it)
params_len = next(it)
data_pid = next(it)
owner_scenario = next(it)
def fn(f): return f._name == fname
if fid != -1:
def fn(f): return f._pamhyr_id == fid
distribution = next(
filter(fn, distributions.lst),
None
)
target_data = next(
filter(
lambda s: s._pamhyr_id == data_pid,
stricklers.lst
),
None
)
rdata = list(cls._decode_params(params, params_len))
new_ensemble = cls(
id, name=name,
distribution=distribution,
params=rdata,
data_type=data_type,
target_data=target_data,
status=data["status"],
owner_scenario=owner_scenario
)
if deleted:
new_ensemble.set_as_deleted()
loaded.add(id)
new.append(new_ensemble)
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM ensemble " +
f"WHERE pamhyr_id = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
bparams, length = self._encode_params()
if self._distribution is None:
fid = -1
fname = "NULL"
else:
fid = self._distribution._pamhyr_id
fname = self._distribution._name
if self._distribution._type == "generic":
fid = -1
data_pid = -1
if self._target_data is not None:
data_pid = self._target_data._pamhyr_id
execute(
"INSERT INTO " +
"ensemble(pamhyr_id, deleted, name, type, " +
" distribution, distribution_name, params, params_len, " +
" data_pid, scenario) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._data_type,
fid, fname, bparams, length,
data_pid, self._status.scenario_id
)
return True
def _encode_params(self):
length = len(self._params)
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.pack(data_format, *self._params), length
@classmethod
def _decode_params(cls, bparams, length):
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.unpack(data_format, bparams)
def random(self, nb):
return np.nditer(
self._distribution.random(
self._params, nb
)
)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
@property
def distribution(self):
return self._distribution
@distribution.setter
def distribution(self, distribution):
self._distribution = distribution
self._status.modified()
@property
def params(self):
return self._params
@params.setter
def params(self, params):
self._params = params
self._status.modified()
@property
def data_type(self):
return self._data_type
@data_type.setter
def data_type(self, data_type):
self._data_type = data_type
self._status.modified()
@property
def target_data(self):
return self._target_data
@target_data.setter
def target_data(self, target_data):
self._target_data = target_data
self._status.modified()
def get_data_dict(self, use_ids=False):
target_data = self.target_data
distribution = self.distribution
if use_ids:
if self.target_data:
target_data = self.target_data.pamhyr_id
if self.distribution:
distribution = self.distribution.name
return {
"name": self.name,
"data_type": self.data_type,
"target_data": target_data,
"distribution": distribution,
"params": self.params,
}
def set_data_dict(self, data):
self.name = data["name"]
self.data_type = data["data_type"]
self.target_data = data["target_data"]
self.distribution = data["distribution"]
self.params = data["params"]
+62
View File
@@ -0,0 +1,62 @@
# EnsembleList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Ensemble import Ensemble
class EnsembleList(PamhyrModelList):
_sub_classes = [Ensemble]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
new._lst = Ensemble._db_load(
execute, data
)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM ensemble " +
f"WHERE scenario = {self._status.scenario_id}"
)
for ens in self._lst:
ens._db_save(execute, data=data)
return True
def new(self, index):
r = Ensemble(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
def new_with_data(self, index, data):
r = Ensemble(status=self._status)
r.set_data_dict(data)
self._lst.insert(index, r)
self._status.modified()
return r
+1 -1
View File
@@ -1,5 +1,5 @@
# Except.py -- Pamhyr model exceptions
# Copyright (C) 2023-2024 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
+194 -84
View File
@@ -1,5 +1,5 @@
# Friction.py -- Pamhyr
# Copyright (C) 2023-2024 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,6 +21,7 @@ import logging
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from numpy import interp
@@ -28,49 +29,49 @@ logger = logging.getLogger()
class Friction(SQLSubModel):
_id_cnt = 0
def __init__(self, id: int = -1,
reach=None,
status=None,
owner_scenario=-1):
super(Friction, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
def __init__(self, id: int = -1, name: str = "",
status=None):
super(Friction, self).__init__()
self._status = status
if id == -1:
self.id = Friction._id_cnt
else:
self.id = id
Friction._id_cnt = max(self.id, Friction._id_cnt+1)
self._name = name
self._edge = None
self._reach = reach
self._begin_rk = 0.0
self._end_rk = 0.0
self._begin_strickler = None
self._end_strickler = None
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE friction(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE friction{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
begin_rk REAL NOT NULL,
end_rk REAL NOT NULL,
reach INTEGER NOT NULL,
begin_strickler INTEGER NOT NULL,
end_strickler INTEGER NOT NULL,
FOREIGN KEY(reach) REFERENCES river_reach(id),
FOREIGN KEY(begin_strickler) REFERENCES stricklers(id),
FOREIGN KEY(end_strickler) REFERENCES stricklers(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(begin_strickler) REFERENCES stricklers(pamhyr_id),
FOREIGN KEY(end_strickler) REFERENCES stricklers(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return True
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 11:
@@ -78,49 +79,165 @@ class Friction(SQLSubModel):
"RENAME COLUMN begin_kp TO begin_rk")
execute("ALTER TABLE friction RENAME COLUMN end_kp TO end_rk")
return True
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE friction " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "friction"
reachs = data['id2pid']['river_reach']
stricklers = data['id2pid']['stricklers']
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}_tmp " +
"(pamhyr_id, ind, begin_rk, end_rk, reach, " +
"begin_strickler, end_strickler, scenario) " +
"SELECT pamhyr_id, ind, begin_rk, end_rk, reach, " +
"begin_strickler, end_strickler, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table, reachs)
cls._db_update_to_0_2_0_set_stricklers_pid(execute, table, stricklers)
@classmethod
def _db_update_to_0_2_0_set_stricklers_pid(cls, execute, table,
stricklers):
els = execute(
f"SELECT pamhyr_id, begin_strickler, end_strickler FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
b_s_id = next(it)
e_s_id = next(it)
b_s = stricklers[b_s_id] if b_s_id != -1 else -1
e_s = stricklers[e_s_id] if e_s_id != -1 else -1
execute(
f"UPDATE {table} " +
f"SET begin_strickler = {b_s}, " +
f"end_strickler = {e_s} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
reach = data["reach"]
status = data["status"]
stricklers = data["stricklers"].stricklers
table = execute(
"SELECT id, ind, begin_rk, end_rk, " +
"begin_strickler, end_strickler " +
f"FROM friction WHERE reach = {reach.id}"
"SELECT pamhyr_id, deleted, begin_rk, end_rk, " +
"begin_strickler, end_strickler, scenario " +
"FROM friction " +
f"WHERE reach = {reach.pamhyr_id} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
"ORDER BY ind ASC"
)
for row in table:
id = row[0]
ind = row[1]
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
begin_rk = next(it)
end_rk = next(it)
begin_strickler_pid = int(next(it))
end_strickler_pid = int(next(it))
owner_scenario = next(it)
# Get stricklers
if int(row[4]) == -1:
bs = None
else:
bs = next(filter(lambda s: s.id == row[4], stricklers))
begin_strickler = None
if begin_strickler_pid != -1:
begin_strickler = next(
filter(
lambda s: s.id == begin_strickler_pid,
stricklers
)
)
if int(row[5]) == -1:
es = None
else:
es = next(filter(lambda s: s.id == row[5], stricklers))
end_strickler = None
if end_strickler_pid != -1:
end_strickler = next(
filter(
lambda s: s.id == begin_strickler_pid,
stricklers
)
)
# Create friction
sec = cls(status=status)
sec.edge = reach
sec.begin_rk = row[2]
sec.end_rk = row[3]
sec.begin_strickler = bs
sec.end_strickler = es
new_friction = cls(
id=pid, status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
f.set_as_deleted()
new.append((ind, sec))
new_friction.reach = reach
new_friction.begin_rk = begin_rk
new_friction.end_rk = end_rk
new_friction.begin_strickler = begin_strickler
new_friction.end_strickler = begin_strickler
loaded.add(pid)
new.append(new_friction)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def cloned_for(self, reach):
new = Friction(reach=reach, status=self._status)
new.reach = reach
new.begin_rk = self._begin_rk
new.end_rk = self._end_rk
new.begin_strickler = self._begin_strickler
new.end_strickler = self._begin_strickler
return new
def must_be_saved(self):
ssi = self._status.scenario_id
return (
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):
if not self.must_be_saved():
return True
ind = data["ind"]
b_s_id = -1
@@ -133,47 +250,34 @@ class Friction(SQLSubModel):
execute(
"INSERT INTO " +
"friction(id, ind, begin_rk, end_rk, " +
"reach, begin_strickler, end_strickler) " +
"friction(pamhyr_id, deleted, ind, begin_rk, end_rk, " +
"reach, begin_strickler, end_strickler, scenario) " +
"VALUES (" +
f"{self.id}, {ind}, " +
f"{self._begin_rk}, {self._end_rk}, " +
f"{self._edge.id}, " +
f"{b_s_id}, {e_s_id}" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, {self._begin_rk}, {self._end_rk}, " +
f"{self._reach.id}, {b_s_id}, {e_s_id}, " +
f"{self._status.scenario_id}" +
")"
)
return True
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
@property
def edge(self):
return self._edge
@property
def reach(self):
return self._edge
return self._reach
@edge.setter
def edge(self, edge):
self._edge = edge
if (edge is not None and
@reach.setter
def reach(self, reach):
self._reach = reach
if (reach is not None and
self._begin_rk == 0.0 and
self._end_rk == 0.0):
self._begin_rk = self._edge.reach.get_rk_min()
self._end_rk = self._edge.reach.get_rk_max()
self._begin_rk = self._reach.reach.get_rk_min()
self._end_rk = self._reach.reach.get_rk_max()
self._status.modified()
def has_edge(self):
return self._edge is not None
def has_reach(self):
return self._reach is not None
def has_coefficient(self):
return (
@@ -181,7 +285,7 @@ class Friction(SQLSubModel):
)
def is_full_defined(self):
return self.has_edge() and self.has_coefficient()
return self.has_reach() and self.has_coefficient()
@property
def begin_rk(self):
@@ -189,11 +293,11 @@ class Friction(SQLSubModel):
@begin_rk.setter
def begin_rk(self, begin_rk):
if self._edge is None:
if self._reach is None:
self._begin_rk = begin_rk
else:
_min = self._edge.reach.get_rk_min()
_max = self._edge.reach.get_rk_max()
_min = self._reach.reach.get_rk_min()
_max = self._reach.reach.get_rk_max()
if _min <= begin_rk <= _max:
self._begin_rk = begin_rk
@@ -206,11 +310,11 @@ class Friction(SQLSubModel):
@end_rk.setter
def end_rk(self, end_rk):
if self._edge is None:
if self._reach is None:
self._end_rk = end_rk
else:
_min = self._edge.reach.get_rk_min()
_max = self._edge.reach.get_rk_max()
_min = self._reach.reach.get_rk_min()
_max = self._reach.reach.get_rk_max()
if _min <= end_rk <= _max:
self._end_rk = end_rk
@@ -261,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()
+38 -18
View File
@@ -1,5 +1,5 @@
# FrictionList.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -20,9 +20,10 @@ import logging
from copy import copy
from tools import trace, timer
from functools import reduce
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Friction.Friction import Friction
@@ -39,30 +40,30 @@ class FrictionList(PamhyrModelList):
execute("ALTER TABLE `section` RENAME TO `friction`")
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
if version == "0.0.0":
logger.info(f"Update friction TABLE from {version}")
cls._db_update_0_0_1(execute, version)
return cls._update_submodel(execute, version)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
ilst = Friction._db_load(
new._lst = Friction._db_load(
execute, data
)
new._lst = list(map(lambda x: x[1], sorted(ilst)))
return new
def _db_save(self, execute, data=None):
frictions = self.lst
reach = data["reach"]
execute(f"DELETE FROM friction WHERE reach = {reach.id}")
execute(
f"DELETE FROM friction WHERE reach = {reach.id} " +
f"AND scenario = {self._status.scenario_id}"
)
ok = True
ind = 0
@@ -86,19 +87,38 @@ class FrictionList(PamhyrModelList):
)
)
@property
def lst(self):
if not isinstance(self._lst, list):
self._lst = self._get_frictions_list()
return self._lst
@property
def frictions(self):
return self.lst
def new(self, index):
n = Friction(status=self._status)
def new(self, reach, index):
n = Friction(reach=reach, status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
def _splited_for(self, reach):
new = FrictionList(status=self._status)
rk_min, rk_max = reduce(
lambda mm, p: (
min(mm[0], p.rk),
max(mm[1], p.rk)
),
reach.reach.profiles,
((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 ind, friction in enumerate(frictions):
n = friction.cloned_for(reach)
new._lst.insert(ind, n)
new._status.modified()
return new
+368
View File
@@ -0,0 +1,368 @@
# GeoTIFF.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import os
import struct
import logging
from functools import reduce
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
try:
import rasterio
import rasterio.control
import rasterio.crs
import rasterio.sample
import rasterio.vrt
import rasterio._features
from rasterio.io import MemoryFile
_rasterio_loaded = True
except Exception as e:
print(f"Module 'rasterio' is not available: {e}")
_rasterio_loaded = False
logger = logging.getLogger()
class GeoTIFF(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, enabled=True,
name="", description="",
path="", coordinates=None,
status=None, owner_scenario=-1):
super(GeoTIFF, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._enabled = enabled
self._name = f"GeoTIFF #{self._pamhyr_id}" if name == "" else name
self._description = description
self._file_bytes = b''
if coordinates is None:
self._coordinates = {
"bottom": 0.0,
"top": 0.0,
"left": 0.0,
"right": 0.0,
}
else:
self._coordinates = coordinates
self._file_name = ""
if path != "":
self.read_file(path)
self._memfile = None
def __getitem__(self, key):
value = None
if key == "enabled":
value = self._enabled
elif key == "name":
value = self.name
elif key == "description":
value = self.description
elif key == "file_name":
value = self.file_name
elif key == "coordinates":
value = self.coordinates
elif key == "coordinates_bottom":
value = self.coordinates["bottom"]
elif key == "coordinates_top":
value = self.coordinates["top"]
elif key == "coordinates_left":
value = self.coordinates["left"]
elif key == "coordinates_right":
value = self.coordinates["right"]
elif key == "memfile":
value = self.memfile
return value
def __setitem__(self, key, value):
if key == "enabled":
self.enabled = value
elif key == "name":
self.name = value
elif key == "description":
self.description = value
elif key == "file_name":
if self._file_name != value:
self.read_file(value)
elif key == "coordinates":
self.coordinates = value
elif key == "coordinates_bottom" or key == "bottom":
self.coordinates["bottom"] = value
elif key == "coordinates_top" or key == "top":
self.coordinates["top"] = value
elif key == "coordinates_left" or key == "left":
self.coordinates["left"] = value
elif key == "coordinates_right" or key == "right":
self.coordinates["right"] = value
self.modified()
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self.modified()
def is_enabled(self):
return self._enabled
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def description(self):
return self._description
@description.setter
def description(self, description):
self._description = description
self.modified()
@property
def file_name(self):
return self._file_name
@file_name.setter
def file_name(self, file_name):
self._file_name = file_name
self.modified()
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
self.modified()
@property
def coord_bottom(self):
if self._coordinates is None:
return 0.0
return self._coordinates["bottom"]
@property
def coord_top(self):
if self._coordinates is None:
return 0.0
return self._coordinates["top"]
@property
def coord_left(self):
if self._coordinates is None:
return 0.0
return self._coordinates["left"]
@property
def coord_right(self):
if self._coordinates is None:
return 0.0
return self._coordinates["right"]
@property
def memfile(self):
if not _rasterio_loaded:
return None
if self._file_bytes == b'':
return None
if self._memfile is None:
self._memfile = MemoryFile()
self._memfile.write(self._file_bytes)
return self._memfile
def read_file(self, path):
logger.debug(f"Read GeoTIFF file at : '{path}'")
self._file_name = path
self._file_bytes = b''
self._memfile = None
nbytes = 0
with open(path, "rb") as f:
while True:
data = f.read(4096)
if not data:
break
nbytes += len(data)
self._file_bytes += data
logger.debug(f"Read GeoTIFF: {nbytes} bytes readed")
def write_file(self, path):
with open(path, "w+b") as f:
f.write(self._file_bytes)
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE geotiff{ext} (
{cls.create_db_add_pamhyr_id()},
enabled BOOLEAN NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
description TEXT NOT NULL,
file_name TEXT NOT NULL,
file_bytes BLOB NOT NULL,
coordinates_bottom REAL NOT NULL,
coordinates_top REAL NOT NULL,
coordinates_left REAL NOT NULL,
coordinates_right REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
if major == "0" and int(minor) == 2:
if int(release) < 3:
cls._db_create(execute)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT pamhyr_id, enabled, deleted, " +
"name, description, file_name, file_bytes, " +
"coordinates_bottom, coordinates_top, " +
"coordinates_left, coordinates_right, " +
"scenario " +
"FROM geotiff " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
if table is not None:
for row in table:
it = iter(row)
id = next(it)
enabled = (next(it) == 1)
deleted = (next(it) == 1)
name = next(it)
description = next(it)
file_name = next(it)
file_bytes = next(it)
coordinates_bottom = next(it)
coordinates_top = next(it)
coordinates_left = next(it)
coordinates_right = next(it)
owner_scenario = next(it)
f = cls(
id=id, enabled=enabled, name=name,
description=description, coordinates={
"bottom": coordinates_bottom,
"top": coordinates_top,
"left": coordinates_left,
"right": coordinates_right,
},
status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
f.set_as_deleted()
f._file_bytes = file_bytes
loaded.add(id)
new.append(f)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"INSERT INTO geotiff (" +
"pamhyr_id, enabled, deleted, " +
"name, description, file_name, file_bytes, " +
"coordinates_bottom, coordinates_top, " +
"coordinates_left, coordinates_right, " +
"scenario) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
self._pamhyr_id,
self._enabled,
self.is_deleted(),
self.name,
self.description,
self.file_name,
self._file_bytes,
self.coordinates['bottom'],
self.coordinates['top'],
self.coordinates['left'],
self.coordinates['right'],
self._status.scenario_id,
)
return True
+59
View File
@@ -0,0 +1,59 @@
# GeoTIFFList.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.GeoTIFF.GeoTIFF import GeoTIFF
class GeoTIFFList(PamhyrModelList):
_sub_classes = [GeoTIFF]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"])
new._lst = GeoTIFF._db_load(execute, data)
return new
def _db_save(self, execute, data=None):
ok = True
# Delete previous data
execute(
"DELETE FROM geotiff " +
f"WHERE scenario = {self._status.scenario_id}"
)
for gt in self._lst:
ok &= gt._db_save(execute, data)
return ok
@property
def files(self):
return self.lst
def new(self, index):
n = GeoTIFF(status=self._status)
self.insert(index, n)
self._status.modified()
return n
+14 -8
View File
@@ -1,5 +1,5 @@
# Point.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -16,14 +16,18 @@
# -*- coding: utf-8 -*-
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
class Point(object):
def __init__(self, name: str = "", profile=None, status=None):
super(Point, self).__init__()
self._status = status
class Point(SQLSubModel):
def __init__(self, id: int = -1, name: str = "",
profile=None, status=None,
owner_scenario=-1):
super(Point, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._profile = profile
@@ -43,6 +47,8 @@ class Point(object):
else:
self._sl = sl
self.modified()
@property
def name(self):
return self._name
@@ -50,9 +56,9 @@ class Point(object):
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
def point_is_named(self):
def is_named(self):
"""
Returns:
True if the point is named.
+1 -1
View File
@@ -1,5 +1,5 @@
# PointXY.py -- Pamhyr
# Copyright (C) 2023-2024 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
+236 -40
View File
@@ -1,5 +1,5 @@
# PointXYZ.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -16,30 +16,40 @@
# -*- coding: utf-8 -*-
import logging
from math import dist
import numpy as np
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Geometry.Point import Point
logger = logging.getLogger()
class PointXYZ(Point, SQLSubModel):
class PointXYZ(Point):
_sub_classes = []
def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0,
name: str = "", profile=None, status=None):
def __init__(self, id: int = -1,
x: float = 0.0, y: float = 0.0, z: float = 0.0,
name: str = "", profile=None,
status=None, owner_scenario=-1):
super(PointXYZ, self).__init__(
name=name, profile=profile, status=status)
id=id, name=name, profile=profile, status=status,
owner_scenario=owner_scenario
)
self._x = float(x)
self._y = float(y)
self._z = float(z)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE geometry_pointXYZ(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE geometry_pointXYZ{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
name TEXT,
x INTEGER NOT NULL,
@@ -47,17 +57,24 @@ class PointXYZ(Point, SQLSubModel):
z INTEGER NOT NULL,
profile INTEGER NOT NULL,
sl INTEGER,
FOREIGN KEY(profile) REFERENCES geometry_profileXYZ(id),
FOREIGN KEY(sl) REFERENCES sedimentary_layer(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(profile)
REFERENCES geometry_profileXYZ(pamhyr_id),
FOREIGN KEY(sl) REFERENCES sedimentary_layer(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
execute(
"CREATE INDEX idx_point " +
"ON geometry_pointXYZ(profile, scenario);"
)
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
cls._update_submodel(execute, version)
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 2:
@@ -68,63 +85,180 @@ class PointXYZ(Point, SQLSubModel):
REFERENCES sedimentary_layer(id)
"""
)
return True
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE geometry_pointXYZ " +
"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
def _db_update_to_0_2_0(cls, execute, data):
table = "geometry_pointXYZ"
id2pid = data['id2pid']
profiles = id2pid['geometry_profileXYZ']
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}_tmp " +
"(pamhyr_id, ind, name, x, y, z, profile, sl, scenario) " +
"SELECT pamhyr_id, ind, name, x, y, z, profile, sl, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_profile_pid(execute, table, profiles)
if 'sedimentary_layer' in id2pid:
sl = id2pid['sedimentary_layer']
cls._db_update_to_0_2_0_set_sl_pid(execute, table, sl)
@classmethod
def _db_update_to_0_2_0_set_profile_pid(cls, execute, table, profiles):
els = execute(
f"SELECT pamhyr_id, profile FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
profile_id = next(it)
if profile_id not in profiles:
logger.warning(
f"This point as no associated profile ({profile_id})"
)
continue
execute(
f"UPDATE {table} " +
f"SET profile = {profiles[profile_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_update_to_0_2_0_set_sl_pid(cls, execute, table, sl):
els = execute(
f"SELECT pamhyr_id, sl FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
sl_id = next(it)
if sl_id == -1:
continue
execute(
f"UPDATE {table} " +
f"SET sl = {sl[sl_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data["status"]
profile = data["profile"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT ind, name, x, y, z, sl " +
"SELECT pamhyr_id, ind, deleted, " +
"name, x, y, z, sl, scenario " +
"FROM geometry_pointXYZ " +
f"WHERE profile = {profile.id}"
f"WHERE profile = {profile.pamhyr_id} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
"ORDER BY ind ASC"
)
# Fill points list with new point
for row in table:
ind = row[0]
name = row[1]
x = row[2]
y = row[3]
z = row[4]
sl = row[5]
it = iter(row)
new = cls(
pid = next(it)
ind = next(it)
deleted = (next(it) == 1)
name = next(it)
x = next(it)
y = next(it)
z = next(it)
sl = next(it)
owner_scenario = next(it)
point = cls(
id=pid,
name=name,
x=x, y=y, z=z,
profile=profile,
status=status
status=status,
owner_scenario=owner_scenario
)
if deleted:
point.set_as_deleted()
if sl == -1 or sl is None:
new._sl = None
point._sl = None
else:
new._sl = next(
point._sl = next(
filter(
lambda s: s.id == sl,
lambda s: s.pamhyr_id == sl,
data["sediment_layers_list"].sediment_layers
)
)
yield ind, new
loaded.add(pid)
new.append((ind, point))
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
profile = data["profile"]
ind = data["ind"]
sl = self._sl.id if self._sl is not None else -1
sl = self._sl.pamhyr_id if self._sl is not None else -1
sql = (
execute(
"INSERT INTO " +
"geometry_pointXYZ(ind, name, x, y, z, profile, sl) " +
"geometry_pointXYZ(pamhyr_id, deleted, ind, name, " +
"x, y, z, profile, sl, scenario) " +
"VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(self._name)}', " +
f"{self.x}, {self.y}, {self.z}, " +
f"{profile.id}, {sl}" +
f"{profile.pamhyr_id}, {sl}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
@@ -150,9 +284,44 @@ class PointXYZ(Point, SQLSubModel):
return point
def copy(self):
new_p = PointXYZ(
id=-1,
name=self.name,
x=self.x, y=self.y, z=self.z,
profile=self._profile,
status=self._status
)
if self.is_deleted():
new_p.set_as_deleted()
new_p._sl = self.sl
new_p.modified()
return new_p
def cloned_for(self, new_profile):
new_p = PointXYZ(
id=-1,
name=self.name,
x=self.x, y=self.y, z=self.z,
profile=new_profile,
status=self._status
)
if self.is_deleted():
new_p.set_as_deleted()
new_p._sl = self.sl
new_p.modified()
return new_p
def __repr__(self):
return f"({self._x}, {self._y}, {self._z}, {self._name})"
def __gt__(self, other):
return self.y > other.y
@property
def x(self):
return self._x
@@ -160,7 +329,7 @@ class PointXYZ(Point, SQLSubModel):
@x.setter
def x(self, value):
self._x = float(value)
self._status.modified()
self.modified()
@property
def y(self):
@@ -169,7 +338,7 @@ class PointXYZ(Point, SQLSubModel):
@y.setter
def y(self, value):
self._y = float(value)
self._status.modified()
self.modified()
@property
def z(self):
@@ -178,7 +347,17 @@ class PointXYZ(Point, SQLSubModel):
@z.setter
def z(self, value):
self._z = float(value)
self._status.modified()
self.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)
def is_nan(self):
"""
@@ -226,5 +405,22 @@ class PointXYZ(Point, SQLSubModel):
a = PointXYZ.distance(p1, p2)
b = PointXYZ.distance(p2, p3)
c = PointXYZ.distance(p3, p1)
s = (a + b + c) / 2
return (s*(s-a) * (s-b)*(s-c)) ** 0.5
s = float((a + b + c) / 2)
res = abs(
s * (s - a) * (s - b) * (s - c)
) ** 0.5
return res
def dist_from_seg(self, p1, p2):
a2 = pow(PointXYZ.distance(self, p1), 2)
b2 = pow(PointXYZ.distance(self, p2), 2)
c2 = pow(PointXYZ.distance(p1, p2), 2)
if c2 < 0.00000001:
# si les deux points sont confondus
d = 0.0
else:
d = np.sqrt(abs(a2 - pow((c2 - b2 + a2) / (2*np.sqrt(c2)), 2)))
return d
+76 -57
View File
@@ -1,5 +1,5 @@
# Profile.py -- Pamhyr
# Copyright (C) 2023-2024 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,23 +28,15 @@ logger = logging.getLogger()
class Profile(object):
_id_cnt = 0
def __init__(self, id: int = -1, num: int = 0,
rk: float = 0.0, name: str = "",
code1: int = 0, code2: int = 0,
_type: str = "", reach=None,
status=None):
super(Profile, self).__init__()
self._status = status
if id == -1:
self.id = Profile._id_cnt
else:
self.id = id
Profile._id_cnt = max(self.id, Profile._id_cnt+1)
status=None, owner_scenario=-1):
super(Profile, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._num = int(num)
self._code1 = int(code1)
@@ -66,28 +58,51 @@ class Profile(object):
return len(self.points)
def _get_points_list(self):
# Points list generator is type (int, Point) with the first
# element the index of the Point in list
return list(
map(
lambda p: p[1],
sorted(
self._points,
key=lambda p: p[0]
)
)
)
return list(self._points)
@property
def points(self):
if not isinstance(self._points, list):
self._points = self._get_points_list()
return self._points
return list(
filter(
lambda p: not p.is_deleted(),
self._points
)
)
def point(self, index):
return self.points[index]
def named_point(self, name):
if name == "un":
return self.points[0]
elif name == "np":
return self.points[-1]
elif name == "talweg":
zm = self.z_min()
return next((p for p in self.points if p.z == zm), None)
else:
return next((p for p in self.points if p.name == name), None)
def named_point_index(self, name):
if name == "un":
return 0
elif name == "np":
return len(self.points) - 1
elif name == "talweg":
zm = self.z_min()
return next(
(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),
None
)[0]
@property
def reach(self):
return self._reach
@@ -103,7 +118,7 @@ class Profile(object):
@num.setter
def num(self, value: int):
self._num = int(value)
self._status.modified()
self.modified()
@property
def code1(self):
@@ -116,7 +131,7 @@ class Profile(object):
@code1.setter
def code1(self, value: int):
self._code1 = int(value)
self._status.modified()
self.modified()
@property
def code2(self):
@@ -129,7 +144,7 @@ class Profile(object):
@code2.setter
def code2(self, value: int):
self._code2 = int(value)
self._status.modified()
self.modified()
@property
def nb_points(self):
@@ -146,7 +161,7 @@ class Profile(object):
@rk.setter
def rk(self, value: float):
self._rk = float(value)
self._status.modified()
self.modified()
@property
def name(self):
@@ -159,7 +174,7 @@ class Profile(object):
@name.setter
def name(self, value: str):
self._name = value.strip()
self._status.modified()
self.modified()
@property
def sl(self):
@@ -172,7 +187,7 @@ class Profile(object):
@sl.setter
def sl(self, value):
self._sl = value
self._status.modified()
self.modified()
@property
def profile_type(self):
@@ -185,7 +200,7 @@ class Profile(object):
@profile_type.setter
def profile_type(self, value: str):
self._profile_type = value
self._status.modified()
self.modified()
def point(self, i: int):
if i < len(self.points):
@@ -200,33 +215,37 @@ class Profile(object):
The list of named point
"""
return [point for point in self.points
if point.point_is_named()]
if point.is_named()]
def insert_point(self, index: int, point: Point):
"""Insert point at index.
Args:
index: The index of new profile.
index: The index of new point.
point: The point.
Returns:
Nothing.
"""
self.points.insert(index, point)
self._status.modified()
if point in self._points:
point.set_as_not_deleted()
else:
self._points.insert(index, point)
self.modified()
def delete_i(self, indexes: list):
self._points = 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.points)
)
)
)
self._status.modified()
self.modified()
def delete_points(self, points):
"""Delete some elements in profile list
@@ -237,31 +256,31 @@ class Profile(object):
Returns:
Nothing.
"""
self._points = list(
filter(
lambda p: p not in points,
self.points
list(
map(
lambda p: p.set_as_deleted(),
points
)
)
self._status.modified()
self.modified()
# Move
def move_up_point(self, index: int):
if index < len(self.points):
if index < len(self._points):
next = index - 1
p = self.points
p = self._points
p[index], p[next] = p[next], p[index]
self._status.modified()
self.modified()
def move_down_point(self, index: int):
if index >= 0:
prev = index + 1
p = self.points
p = self._points
p[index], p[prev] = p[prev], p[index]
self._status.modified()
self.modified()
# Sort
@@ -274,32 +293,32 @@ class Profile(object):
def predicate(p): return p.z
self._points = sorted(
self.points,
self._points,
key=predicate,
reverse=is_reversed
)
self._status.modified()
self.modified()
@timer
def sort_with_indexes(self, indexes: list):
if len(self.points) != len(indexes):
if len(self._points) != len(indexes):
logger.critical("Indexes list do not correspond to point list")
self._points = list(
map(
lambda x: x[1],
sorted(
enumerate(self.points),
enumerate(self._points),
key=lambda x: indexes[x[0]]
)
)
)
self._status.modified()
self.modified()
@timer
def reverse(self):
self._points.reverse()
self._status.modified()
self.modified()
# Sediment Layers
+434 -144
View File
@@ -1,5 +1,5 @@
# ProfileXYZ.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -16,17 +16,20 @@
# -*- coding: utf-8 -*-
import queue
import random
import logging
import numpy as np
from typing import List
from functools import reduce
from dataclasses import dataclass
from tools import timer
from tools import trace, timer, flatten
from shapely import geometry
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import ClipboardFormatError
from Model.Scenario import Scenario
from Model.Geometry.Profile import Profile
from Model.Geometry.PointXYZ import PointXYZ
from Model.Geometry.Vector_1d import Vector1d
@@ -48,13 +51,14 @@ class ProfileXYZ(Profile, SQLSubModel):
def __init__(self,
id: int = -1,
ind: int = -1,
name: str = "",
rk: float = 0.,
reach=None,
num=0,
nb_point: int = 0,
code1: int = 0, code2: int = 0,
status=None):
status=None, owner_scenario=-1):
"""ProfileXYZ constructor
Args:
@@ -69,15 +73,16 @@ class ProfileXYZ(Profile, SQLSubModel):
"""
super(ProfileXYZ, self).__init__(
id=id,
num=num,
name=name,
rk=rk,
num=num, rk=rk,
code1=code1, code2=code2,
_type="XYZ",
reach=reach,
status=status,
owner_scenario=owner_scenario
)
self._db_ind = ind
self.tab = Tabulation([], [], [])
self.tab_up_to_date = False
self.time_z = 0.0
@@ -86,11 +91,15 @@ class ProfileXYZ(Profile, SQLSubModel):
self._station = []
self.station_up_to_date = False
self._get_water_limits_cache = {}
self._get_water_limits_ac_cache = {}
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE geometry_profileXYZ(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE geometry_profileXYZ{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
name TEXT,
reach INTEGER NOT NULL,
@@ -99,15 +108,21 @@ class ProfileXYZ(Profile, SQLSubModel):
code1 INTEGER NOT NULL,
code2 INTEGER NOT NULL,
sl INTEGER,
FOREIGN KEY(reach) REFERENCES river_reach(id),
FOREIGN KEY(sl) REFERENCES sedimentary_layer(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(sl) REFERENCES sedimentary_layer(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
rl = int(release)
@@ -128,83 +143,215 @@ class ProfileXYZ(Profile, SQLSubModel):
"""
)
return cls._update_submodel(execute, version)
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "geometry_profileXYZ"
id2pid = data['id2pid']
reachs = id2pid['river_reach']
cls._db_update_cleanup_points(execute)
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}_tmp " +
"(pamhyr_id, ind, name, reach, rk, " +
"num, code1, code2, sl, scenario) " +
"SELECT pamhyr_id, ind, name, reach, rk, " +
"num, code1, code2, sl, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table, reachs)
if 'sedimentary_layer' in id2pid:
sl = id2pid['sedimentary_layer']
cls._db_update_to_0_2_0_set_sl_pid(execute, table, sl)
@classmethod
def _db_update_cleanup_points(cls, execute):
profiles = set(
map(
str,
flatten(
execute(
"SELECT id FROM geometry_profileXYZ"
)
)
)
)
execute(
"DELETE FROM geometry_pointXYZ " +
f"WHERE profile NOT IN ({', '.join(profiles)})"
)
@classmethod
def _db_update_to_0_2_0_set_sl_pid(cls, execute, table, sl):
els = execute(
f"SELECT pamhyr_id, sl FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
sl_id = next(it)
if sl_id == -1:
continue
execute(
f"UPDATE {table} " +
f"SET sl = {sl[sl_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
profiles = []
new = []
status = data["status"]
scenario = data["scenario"]
loaded = data['loaded_pid']
reach = data["reach"]
if scenario is None:
return new
table = execute(
"SELECT id, ind, name, rk, num, code1, code2, sl " +
"SELECT pamhyr_id, ind, deleted, name, rk, num, " +
"code1, code2, sl, scenario " +
"FROM geometry_profileXYZ " +
f"WHERE reach = {reach.id}"
f"WHERE reach = {reach.id} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
"ORDER BY ind ASC"
)
for row in table:
id = row[0]
ind = row[1]
name = row[2]
rk = row[3]
num = row[5]
code1 = row[5]
code2 = row[6]
sl = row[7]
it = iter(row)
new = cls(
id=id, num=num,
pid = next(it)
ind = next(it)
deleted = (next(it) == 1)
name = next(it)
rk = next(it)
num = next(it)
code1 = next(it)
code2 = next(it)
sl = next(it)
owner_scenario = next(it)
profile = cls(
id=pid, ind=ind, num=num,
name=name, rk=rk,
code1=code1, code2=code2,
reach=reach,
status=status
status=status,
owner_scenario=owner_scenario
)
if deleted:
profile.set_as_deleted()
if sl == -1 or sl is None:
new._sl = None
profile._sl = None
else:
new._sl = next(
profile._sl = next(
filter(
lambda s: s.id == sl,
data["sediment_layers_list"].sediment_layers
), None)
),
None
)
data["profile"] = new
new._points = PointXYZ._db_load(execute, data.copy())
loaded_save = data['loaded_pid']
data['loaded_pid'] = set()
data["profile"] = profile
yield ind, new
profile._points = list(
map(
lambda ip: ip[1],
sorted(
PointXYZ._db_load(execute, data.copy()),
key=lambda ip: ip[0]
)
)
)
data['loaded_pid'] = loaded_save
loaded.add(pid)
new.append(profile)
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
ok = True
ind = data["ind"]
sl = self._sl.id if self._sl is not None else -1
sl = self._sl.pamhyr_id if self._sl is not None else -1
sql = (
execute(
"INSERT OR REPLACE INTO " +
"geometry_profileXYZ(id, ind, name, reach, " +
"rk, num, code1, code2, sl) " +
"geometry_profileXYZ(pamhyr_id, deleted, ind, name, reach, " +
"rk, num, code1, code2, sl, scenario) " +
"VALUES (" +
f"{self.id}, {ind}, '{self._db_format(self._name)}', " +
f"{self.reach.id}, {self.rk}, {self.num}, " +
f"{self.code1}, {self.code1}, {sl}" +
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}" +
")"
)
execute(sql)
points = self.points
data["profile"] = self
execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}")
execute(
"DELETE FROM geometry_pointXYZ " +
f"WHERE profile = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
ind = 0
for point in points:
for point in self._points:
data["ind"] = ind
ok &= point._db_save(execute, data)
ind += 1
return ok
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for p in self._points:
p._data_traversal(predicate, modifier, data)
@classmethod
def from_data(cls, header, data):
profile = None
@@ -237,6 +384,46 @@ class ProfileXYZ(Profile, SQLSubModel):
return profile
def copy(self):
new_p = ProfileXYZ(
id=-1,
name=self.name,
rk=self.rk,
reach=self.reach,
status=self._status
)
if self.is_deleted():
new_p.set_as_deleted()
new_p._sl = self.sl
for point in self._points:
p = point.copy()
new_p._points.append(p)
new_p.modified()
return new_p
def cloned_for(self, new_reach):
new_p = ProfileXYZ(
id=-1,
name=self.name,
rk=self.rk,
reach=new_reach,
status=self._status
)
if self.is_deleted():
new_p.set_as_deleted()
new_p._sl = self.sl
for point in self._points:
p = point.cloned_for(new_p)
new_p._points.append(p)
new_p.modified()
return new_p
def point_from_data(self, header, data):
def float_format(s: str):
return float(
@@ -277,6 +464,12 @@ class ProfileXYZ(Profile, SQLSubModel):
return name
def modified(self):
super(ProfileXYZ, self).modified()
self.tab_up_to_date = False
self.station_up_to_date = False
def x(self):
return [point.x for point in self.points]
@@ -317,11 +510,20 @@ class ProfileXYZ(Profile, SQLSubModel):
Nothing.
"""
for point in list_points:
pt = PointXYZ(*point, profile=self, status=self._status)
self.points.append(pt)
self._status.modified()
self.tab_up_to_date = False
self.station_up_to_date = False
named_args = {
"x": point[0],
"y": point[1],
"z": point[2],
"name": point[3],
}
pt = PointXYZ(
id=-1, **named_args, profile=self,
status=self._status
)
self._points.append(pt)
self.modified()
def get_point_i(self, index: int) -> PointXYZ:
"""Get point at index.
@@ -380,11 +582,13 @@ class ProfileXYZ(Profile, SQLSubModel):
Returns:
Nothing.
"""
point_xyz = PointXYZ(0., 0., 0., profile=self, status=self._status)
point_xyz = PointXYZ(
x=0., y=0., z=0., profile=self,
status=self._status
)
self.points.append(point_xyz)
self._status.modified()
self.tab_up_to_date = False
self.station_up_to_date = False
self.modified()
def insert(self, index: int):
"""Insert a new point at index.
@@ -395,11 +599,16 @@ class ProfileXYZ(Profile, SQLSubModel):
Returns:
The new point.
"""
point = PointXYZ(0., 0., 0., profile=self, status=self._status)
self.points.insert(index, point)
self._status.modified()
self.tab_up_to_date = False
self.station_up_to_date = False
x, y, z = (0., 0., 0.)
if 0 < len(self._points) >= index:
x, y, z = self._points[index - 1].get_coordinate()
point = PointXYZ(x=x, y=y, z=z, profile=self, status=self._status)
self._points.insert(index, point)
self.modified()
return point
def filter_isnan(self, lst):
@@ -413,6 +622,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)
@@ -464,6 +674,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:
@@ -504,6 +715,7 @@ class ProfileXYZ(Profile, SQLSubModel):
if len(line.coords) > 2:
poly = geometry.Polygon(line)
area += poly.area
return area
def wet_radius(self, z):
@@ -526,6 +738,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:
@@ -542,8 +755,8 @@ class ProfileXYZ(Profile, SQLSubModel):
if zz[i] >= z and zz[i+1] < z:
y = np.interp(
z,
[zz[i], zz[i+1]],
[station[i], station[i+1]]
[zz[i+1], zz[i]],
[station[i+1], station[i]]
)
line.append([y, z])
@@ -602,7 +815,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:
@@ -614,10 +826,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:
@@ -632,11 +854,8 @@ class ProfileXYZ(Profile, SQLSubModel):
for i in range(self.number_points-1):
if zz[i] > z and zz[i+1] <= z:
y = np.interp(
z,
[zz[i], zz[i+1]],
[station[i], station[i+1]]
)
fact = (z - zz[i]) / (zz[i+1] - zz[i])
y = station[i] + fact * (station[i+1] - station[i])
start.append(y)
end = []
@@ -645,82 +864,102 @@ class ProfileXYZ(Profile, SQLSubModel):
for i in reversed(range(self.number_points-1)):
if zz[i] <= z and zz[i+1] > z:
y = np.interp(
z,
[zz[i], zz[i+1]],
[station[i], station[i+1]]
)
fact = (z - zz[i]) / (zz[i+1] - zz[i])
y = station[i] + fact * (station[i+1] - station[i])
end.append(y)
if len(start) != len(end):
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):
x = np.interp(
z,
[self.point(i_left).z, self.point(i_left - 1).z],
[self.point(i_left).x, self.point(i_left - 1).x]
)
y = np.interp(
z,
[self.point(i_left).z, self.point(i_left - 1).z],
[self.point(i_left).y, self.point(i_left - 1).y]
)
pt_left = PointXYZ(x, y, z, name="wl_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 - 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):
x = np.interp(
z,
[self.point(i_right).z, self.point(i_right + 1).z],
[self.point(i_right).x, self.point(i_right + 1).x]
)
y = np.interp(
z,
[self.point(i_right).z, self.point(i_right + 1).z],
[self.point(i_right).y, self.point(i_right + 1).y]
)
pt_right = PointXYZ(x, y, z, name="wl_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 - 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
@@ -754,13 +993,13 @@ class ProfileXYZ(Profile, SQLSubModel):
last_point_not_nan = self._last_point_not_nan(points)
for index, point in enumerate(points):
if point.point_is_named():
if point.is_named():
index_first_named_point = index
first_named_point = point
break
for point in reversed(points):
if point.point_is_named():
if point.is_named():
last_named_point = point
break
@@ -831,54 +1070,105 @@ class ProfileXYZ(Profile, SQLSubModel):
"""
Remove points to keep at most np_purge points.
"""
if (self.nb_points <= np_purge):
if self.nb_points <= np_purge:
return
nb_named = 2 # we consider the first and last point as named
area = [0.0]
nb_named = 0 # we consider the first and last point as named
area = []
for i in range(1, self.nb_points-1):
if self.point(i).point_is_named():
area.append(9999999.999)
for i, p in enumerate(self.points):
if p.is_named() or i == 0 or i == self.nb_points - 1:
area.append((9999999.999, p))
nb_named += 1
else:
area.append(
area.append((
PointXYZ.areatriangle3d(
self.point(i-1),
self.point(i),
self.point(i+1))
)
area.append(0.0)
p,
self.point(i+1)
),
p
))
while self.nb_points > max(np_purge, nb_named):
to_rm = np.argmin(area[1:self.nb_points - 1]) + 1
ind, (min_area, p) = min(
enumerate(area),
key=lambda t: t[1][0]
)
self.delete_i([to_rm])
area.pop(to_rm)
p.set_as_deleted()
area.pop(ind)
for i in [to_rm-1, to_rm]:
if (i == 0):
continue
for i in [ind-1, ind]:
p = self.point(i)
if (i == self.nb_points - 1):
continue
if self.point(i).point_is_named():
area[i] = 9999999.999
if p.is_named() or i == 0 or i == self.nb_points - 1:
area[i] = (9999999.999, p)
else:
area[i] = PointXYZ.areatriangle3d(
self.point(i-1),
self.point(i),
self.point(i+1)
area[i] = (
PointXYZ.areatriangle3d(
self.point(i-1), p, self.point(i+1)
),
p
)
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
for p in self._points:
p.shift(x, y, z)
def modified(self):
self.tab_up_to_date = False
self.station_up_to_date = False
self.modified()
def add_npoints(self, npoints):
# add npoints in a profile
for k in range(npoints):
disti = 0.0
i = 1
for j in range(len(self.points)-1):
distj = self.point(j).dist(self.point(j+1))
if distj > disti:
disti = distj
i = j
self.insert_point(i, self.point(i).copy())
self.point(i+1).name = ''
self.point(i+1).x = 0.5 * self.point(i).x + 0.5 * self.point(i+2).x
self.point(i+1).y = 0.5 * self.point(i).y + 0.5 * self.point(i+2).y
self.point(i+1).z = 0.5 * self.point(i).z + 0.5 * self.point(i+2).z
def copy(self):
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())
return p
def purge_aligned_points(self):
align = True
while (align):
align = False
for i in range(1, self.number_points - 1):
d = self.point(i).dist_from_seg(self.point(i-1),
self.point(i+1))
if d < 0.00001 and self.point(i).name == "":
align = True
self.delete_i([i])
break
def hard_purge_aligned_points(self):
align = True
points = self.points
while (align):
align = False
for i in range(1, len(points) - 1):
d = points[i].dist_from_seg(points[i-1],
points[i+1])
if d < 0.00001 and points[i].name == "":
align = True
points.pop(i)
break
self._points = points
self.modified()
+168 -89
View File
@@ -1,5 +1,5 @@
# Reach.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -45,7 +45,6 @@ class Reach(SQLSubModel):
]
def __init__(self, status=None, parent=None):
self.id = parent.id if parent is not None else 0
self._status = status
self._parent = parent
self._profiles: List[Profile] = []
@@ -53,41 +52,71 @@ 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
@property
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)
@classmethod
def _db_update(cls, execute, version):
return cls._update_submodel(execute, version)
def _db_update(cls, execute, version, data=None):
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"], parent=data["reach"])
new._profiles = ProfileXYZ._db_load(
execute,
data=data.copy()
new._profiles = sorted(
ProfileXYZ._db_load(
execute,
data=data.copy()
),
key=lambda p: p._db_ind
)
return new
def _db_save(self, execute, data=None):
profiles = self.profiles
ok = True
# Delete old data
execute(f"DELETE FROM geometry_profileXYZ WHERE reach = {self.id}")
if data is None:
data = {}
execute(
"DELETE FROM geometry_profileXYZ " +
f"WHERE reach = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
ind = 0
for profile in profiles:
for profile in self._profiles:
data["ind"] = ind
profile._db_save(execute, data)
ok &= profile._db_save(execute, data)
ind += 1
return True
return ok
def _data_traversal(self,
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)
def profile(self, i):
"""Returns profile at index i
@@ -111,31 +140,18 @@ class Reach(SQLSubModel):
return self._parent.name
def _get_profiles_list(self):
# Profiles list generator is type (int, Point) with the first
# element the index of the Point in list
logger.debug(f"Load profiles from reach {self.name}")
return list(
map(
lambda p: p[1],
sorted(
self._profiles,
key=lambda p: p[0]
)
)
)
return list(self._profiles)
def __len__(self):
if not isinstance(self._profiles, list):
self._profiles = self._get_profiles_list()
return len(self._profiles)
return len(self.profiles)
@property
def profiles(self):
if not isinstance(self._profiles, list):
self._profiles = self._get_profiles_list()
return self._profiles
return list(filter(lambda p: not p.is_deleted(), self._profiles))
@profiles.setter
def profiles(self, profiles):
@@ -163,15 +179,6 @@ class Reach(SQLSubModel):
"""
return self.profiles
def _update_profile_numbers(self):
"""Update profiles index
Returns:
Nothing.
"""
for ind, profile in enumerate(self.get_geometry()):
profile.num = ind + 1
def insert(self, index: int):
"""Insert new profile in list
@@ -183,10 +190,9 @@ class Reach(SQLSubModel):
"""
profile = ProfileXYZ(reach=self, status=self._status)
self.profiles.insert(index, profile)
self._update_profile_numbers()
self._profiles.insert(index, profile)
self._status.modified()
self.modified()
return profile
@@ -199,11 +205,38 @@ class Reach(SQLSubModel):
Returns:
Nothing.
"""
self.profiles.insert(index, profile)
self._update_profile_numbers()
self._status.modified()
if profile in self._profiles:
self.undelete([profile])
profile.num = index
else:
gi = self.get_global_profil_index(index)
profile.num = gi
self._profiles.insert(gi, profile)
self.modified()
def delete(self, indexes):
"""Set some elements as deleted in profile list
Args:
indexes: The list of index to delete
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 gi,
enumerate(self._profiles)
)
)
)
self.modified()
def hard_delete(self, indexes):
"""Delete some elements in profile list
Args:
@@ -212,27 +245,20 @@ class Reach(SQLSubModel):
Returns:
Nothing.
"""
profiles = set(
gi = [self.get_global_profil_index(i) for i in indexes]
self._profiles = list(
map(
lambda e: e[1],
lambda p: p[1],
filter(
lambda e: e[0] in indexes,
enumerate(self.profiles)
lambda e: e[0] not in gi,
enumerate(self._profiles)
)
)
)
self._profiles = list(
filter(
lambda p: p not in profiles,
self.profiles
)
)
self._update_profile_numbers()
self._status.modified()
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
@@ -240,35 +266,41 @@ class Reach(SQLSubModel):
Returns:
Nothing.
"""
self._profiles = list(
filter(
lambda p: p not in profiles,
self.profiles
list(
map(
lambda p: p.set_as_deleted(),
profiles
)
)
self._update_profile_numbers()
self._status.modified()
self.modified()
def undelete(self, lst):
for el in lst:
el.set_as_not_deleted()
self.modified()
def purge(self):
self._profiles = []
self._update_profile_numbers()
self._status.modified()
for el in self._profiles:
el.set_as_deleted()
self.modified()
def move_up_profile(self, index: int):
if index < len(self.profiles):
if index < len(self._profiles):
next = index - 1
p = self.profiles
p = self._profiles
p[index], p[next] = p[next], p[index]
self._status.modified()
self.modified()
def move_down_profile(self, index: int):
if index >= 0:
prev = index + 1
p = self.profiles
p = self._profiles
p[index], p[prev] = p[prev], p[index]
self._status.modified()
self.modified()
def get_x(self):
return list(
@@ -496,7 +528,7 @@ class Reach(SQLSubModel):
self._compute_guidelines_cache(guide_set, named_points,
complete, incomplete)
self._status.modified()
self.modified()
return (complete, incomplete)
def _map_guidelines_points(self, func, full=False):
@@ -537,28 +569,28 @@ class Reach(SQLSubModel):
@timer
def sort(self, is_reversed: bool = False):
self.profiles = sorted(
self.profiles,
self._profiles = sorted(
self._profiles,
key=lambda profile: profile.rk,
reverse=is_reversed
)
self._status.modified()
self.modified()
@timer
def sort_with_indexes(self, indexes: list):
if len(self.profiles) != len(indexes):
if len(self._profiles) != len(indexes):
logger.critical("Indexes list do not correspond to profile list")
self.profiles = list(
self._profiles = list(
map(
lambda x: x[1],
sorted(
enumerate(self.profiles),
enumerate(self._profiles),
key=lambda x: indexes[x[0]]
)
)
)
self._status.modified()
self.modified()
# Import/Export
@@ -614,8 +646,7 @@ class Reach(SQLSubModel):
profiles.append(prof)
self.profiles = profiles + self.profiles
self._update_profile_numbers()
self._profiles = profiles + self._profiles
self._recompute_rk()
return profiles
@@ -633,6 +664,8 @@ class Reach(SQLSubModel):
"""
imported_profiles = []
logger.info(f"import reach from {file_path_name}")
try:
list_profile, list_header = self.read_file_st(str(file_path_name))
profile_header = ["num", "code1", "code2",
@@ -645,12 +678,15 @@ class Reach(SQLSubModel):
d[profile_header[i]] = data
prof = ProfileXYZ(
**d, reach=self, status=self._status
id=-1,
**d,
reach=self,
status=self._status
)
prof.import_points(profile)
imported_profiles.append(prof)
self._status.modified()
self.modified()
except FileNotFoundError as e:
logger.error(e)
exception_message_box(e)
@@ -658,9 +694,8 @@ class Reach(SQLSubModel):
logger.error(e)
e.alert()
finally:
self.profiles = imported_profiles + self.profiles
self._profiles = self._profiles + imported_profiles
self._update_profile_numbers()
return imported_profiles
@timer
@@ -894,3 +929,47 @@ class Reach(SQLSubModel):
profile.rk = previous.rk + dist
previous = profile
def _split(self, profile, reach1, reach2):
if profile is None:
return
nr1 = Reach(status=self._status, parent=reach1)
nr2 = Reach(status=self._status, parent=reach2)
ind = self.profiles.index(profile)
nr1_profiles = self.profiles[:ind+1]
nr2_profiles = self.profiles[ind:]
nr1._profiles = list(map(lambda p: p.cloned_for(nr1), nr1_profiles))
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-2024 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-2024 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,6 +21,7 @@ import logging
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Except import NotImplementedMethodeError
from Model.HydraulicStructures.Basic.Value import (
@@ -34,49 +35,97 @@ class BasicHS(SQLSubModel):
_sub_classes = [
BHSValue,
]
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "",
status=None):
super(BasicHS, self).__init__()
self._status = status
if id == -1:
self.id = BasicHS._id_cnt
else:
self.id = id
status=None, owner_scenario=-1):
super(BasicHS, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._type = ""
self._enabled = True
self._data = []
BasicHS._id_cnt = max(BasicHS._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE hydraulic_structures_basic(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE hydraulic_structures_basic{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type TEXT NOT NULL,
enabled BOOLEAN NOT NULL,
hs INTEGER,
FOREIGN KEY(hs) REFERENCES hydraulic_structures(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(hs) REFERENCES hydraulic_structures(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 6:
cls._db_create(execute)
return True
else:
cls._db_update_to_0_2_0(execute, data)
return True
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE hydraulic_structures_basic " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "hydraulic_structures_basic"
hs = data['id2pid']['hydraulic_structures']
cls.update_db_add_pamhyr_id(execute, table, data)
cls._db_update_to_0_2_0_set_hs_pid(execute, table, hs)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, name, type, enabled, hs, scenario) " +
"SELECT pamhyr_id, name, type, enabled, hs, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_update_to_0_2_0_set_hs_pid(cls, execute, table, hs):
els = execute(
f"SELECT pamhyr_id, hs FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
hs_id = next(it)
execute(
f"UPDATE {table} " +
f"SET hs = {hs[hs_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _get_ctor_from_type(cls, t):
@@ -94,58 +143,84 @@ class BasicHS(SQLSubModel):
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data["status"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return []
table = execute(
"SELECT id, name, type, enabled, hs " +
"SELECT pamhyr_id, deleted, name, type, enabled, hs, scenario " +
"FROM hydraulic_structures_basic " +
f"WHERE hs = {data['hs_id']} "
f"WHERE hs = {data['hs_id']} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
bhs_id = row[0]
name = row[1]
type = row[2]
enabled = (row[3] == 1)
hs_id = row[4]
it = iter(row)
bhs_pid = next(it)
deleted = (next(it) == 1)
name = next(it)
type = next(it)
enabled = (next(it) == 1)
hs_id = next(it)
owner_scenario = next(it)
ctor = cls._get_ctor_from_type(type)
bhs = ctor(
id=bhs_id,
name=name,
status=data['status']
id=bhs_pid, name=name,
status=status, owner_scenario=owner_scenario
)
if deleted:
bhs.set_as_deleted()
bhs.enabled = enabled
data['bhs_id'] = bhs_id
data['bhs_id'] = bhs_pid
bhs._data = BHSValue._db_load(
execute, data
)
if deleted:
bhs.set_as_deleted()
new.append(bhs)
loaded.add(bhs_pid)
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
hs_id = data['hs_id']
sql = (
execute(
"INSERT INTO " +
"hydraulic_structures_basic(id, name, type, enabled, hs) " +
"hydraulic_structures_basic(pamhyr_id, deleted, " +
"name, type, enabled, hs, scenario) " +
"VALUES (" +
f"{self.id}, " +
f"{self.pamhyr_id}, " +
f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', " +
f"{self._db_format(self.enabled)}, " +
f"{hs_id} " +
f"{hs_id}, " +
f"{self._status.scenario_id}"
")"
)
execute(sql)
data['bhs_id'] = self.id
data['bhs_id'] = self.pamhyr_id
execute(
"DELETE FROM hydraulic_structures_basic_value " +
f"WHERE bhs = {self.id}"
f"WHERE bhs = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
for values in self._data:
@@ -153,20 +228,30 @@ class BasicHS(SQLSubModel):
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(self._data)
@property
def name(self):
if self._name == "":
return f"{self._type}{self.id + 1}"
return f"{self._type} #{self.pamhyr_id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def type(self):
@@ -175,7 +260,7 @@ class BasicHS(SQLSubModel):
@type.setter
def type(self, type):
self._type = type
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -184,7 +269,7 @@ class BasicHS(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
@property
def parameters(self):
+86 -74
View File
@@ -1,5 +1,5 @@
# Types.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -27,10 +27,11 @@ from Model.HydraulicStructures.Basic.Value import (
class NotDefined(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(NotDefined, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "ND"
@@ -39,197 +40,208 @@ class NotDefined(BasicHS):
class DischargeWeir(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(DischargeWeir, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "S1"
self._data = [
BHSValue("width", float, 1.0, status=status),
BHSValue("elevation", float, 1.0, status=status),
BHSValue("discharge_coefficient", float, 0.4, status=status),
BHSValue(-1, "width", float, 1.0, status=status),
BHSValue(-1, "elevation", float, 1.0, status=status),
BHSValue(-1, "discharge_coefficient", float, 0.4, status=status),
]
class TrapezoidalWeir(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(TrapezoidalWeir, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "S2"
self._data = [
BHSValue("width", float, 1.0, status=status),
BHSValue("elevation", float, 1.0, status=status),
BHSValue("loading_elevation", float, 9999.999, status=status),
BHSValue("discharge_coefficient", float, 0.4, status=status),
BHSValue("half-angle_tangent", float, 0.0, status=status),
BHSValue(-1, "width", float, 1.0, status=status),
BHSValue(-1, "elevation", float, 1.0, status=status),
BHSValue(-1, "loading_elevation", float, 9999.999, status=status),
BHSValue(-1, "discharge_coefficient", float, 0.4, status=status),
BHSValue(-1, "half-angle_tangent", float, 0.0, status=status),
]
class TriangularWeir(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(TriangularWeir, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "S3"
self._data = [
BHSValue("elevation", float, 1.0, status=status),
BHSValue("loading_elevation", float, 9999.999, status=status),
BHSValue("discharge_coefficient", float, 0.31, status=status),
BHSValue("half-angle_tangent", float, 1.0, status=status),
BHSValue(-1, "elevation", float, 1.0, status=status),
BHSValue(-1, "loading_elevation", float, 9999.999, status=status),
BHSValue(-1, "discharge_coefficient", float, 0.31, status=status),
BHSValue(-1, "half-angle_tangent", float, 1.0, status=status),
]
class RectangularOrifice(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(RectangularOrifice, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "OR"
self._data = [
BHSValue("width", float, 0.0, status=status),
BHSValue("elevation", float, 0.0, status=status),
BHSValue("loading_elevation", float, 9999.999,
BHSValue(-1, "width", float, 0.0, status=status),
BHSValue(-1, "elevation", float, 0.0, status=status),
BHSValue(-1, "loading_elevation", float, 9999.999,
status=status),
BHSValue("discharge_coefficient", float, 0.4, status=status),
BHSValue("maximal_loading_elevation", float, 9999.999,
BHSValue(-1, "discharge_coefficient", float, 0.4, status=status),
BHSValue(-1, "maximal_loading_elevation", float, 9999.999,
status=status),
]
class CircularOrifice(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(CircularOrifice, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "OC"
self._data = [
BHSValue("diameter", float, 0.0, status=status),
BHSValue("elevation", float, 0.0, status=status),
BHSValue("siltation_height", float, 0.0, status=status),
BHSValue("discharge_coefficient", float, 0.4, status=status),
BHSValue(-1, "diameter", float, 0.0, status=status),
BHSValue(-1, "elevation", float, 0.0, status=status),
BHSValue(-1, "siltation_height", float, 0.0, status=status),
BHSValue(-1, "discharge_coefficient", float, 0.4, status=status),
]
class VaultedOrifice(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(VaultedOrifice, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "OV"
self._data = [
BHSValue("elevation", float, 1.0, status=status),
BHSValue("width", float, 1.0, status=status),
BHSValue("top_of_the_vault", float, 0.0, status=status),
BHSValue("bottom_of_the_vault", float, 0.0, status=status),
BHSValue("discharge_coefficient", float, 0.4, status=status),
BHSValue(-1, "elevation", float, 1.0, status=status),
BHSValue(-1, "width", float, 1.0, status=status),
BHSValue(-1, "top_of_the_vault", float, 0.0, status=status),
BHSValue(-1, "bottom_of_the_vault", float, 0.0, status=status),
BHSValue(-1, "discharge_coefficient", float, 0.4, status=status),
]
class RectangularGate(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(RectangularGate, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "V1"
self._data = [
BHSValue("width", float, 1.0, status=status),
BHSValue("elevation", float, 0.0, status=status),
BHSValue("discharge coefficient", float, 0.4, status=status),
BHSValue("opening", float, 1.0, status=status),
BHSValue("maximal_opening", float, 1.0, status=status),
BHSValue(-1, "width", float, 1.0, status=status),
BHSValue(-1, "elevation", float, 0.0, status=status),
BHSValue(-1, "discharge coefficient", float, 0.4, status=status),
BHSValue(-1, "opening", float, 1.0, status=status),
BHSValue(-1, "maximal_opening", float, 1.0, status=status),
]
class SimplifiedRectangularGate(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(SimplifiedRectangularGate, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "V2"
self._data = [
BHSValue("width", float, 1.0, status=status),
BHSValue("elevation", float, 0.0, status=status),
BHSValue("discharge coefficient", float, 0.4, status=status),
BHSValue("opening", float, 1.0, status=status),
BHSValue("maximal_opening", float, 1.0, status=status),
BHSValue(-1, "width", float, 1.0, status=status),
BHSValue(-1, "elevation", float, 0.0, status=status),
BHSValue(-1, "discharge coefficient", float, 0.4, status=status),
BHSValue(-1, "opening", float, 1.0, status=status),
BHSValue(-1, "maximal_opening", float, 1.0, status=status),
]
class Borda(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(Borda, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "BO"
self._data = [
BHSValue("step_space", float, 0.1, status=status),
BHSValue("weir", float, 0.15, status=status),
BHSValue("coefficient", float, 0.4, status=status),
BHSValue(-1, "step_space", float, 0.1, status=status),
BHSValue(-1, "weir", float, 0.15, status=status),
BHSValue(-1, "coefficient", float, 0.4, status=status),
]
class CheckValve(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(CheckValve, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "CV"
self._data = [
BHSValue("width", float, 0.0, status=status),
BHSValue("elevation", float, 0.0, status=status),
BHSValue("loading_elevation", float, 9999.999,
BHSValue(-1, "width", float, 0.0, status=status),
BHSValue(-1, "elevation", float, 0.0, status=status),
BHSValue(-1, "loading_elevation", float, 9999.999,
status=status),
BHSValue("discharge_coefficient", float, 0.4, status=status),
BHSValue("maximal_loading_elevation", float, 9999.999,
BHSValue(-1, "discharge_coefficient", float, 0.4, status=status),
BHSValue(-1, "maximal_loading_elevation", float, 9999.999,
status=status),
]
class UserDefined(BasicHS):
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=-1):
super(UserDefined, self).__init__(
id=id, name=name,
status=status
status=status,
owner_scenario=owner_scenario
)
self._type = "UD"
self._data = [
BHSValue("parameter_1", float, 0.0, status=status),
BHSValue("parameter_2", float, 0.0, status=status),
BHSValue("parameter_3", float, 0.0, status=status),
BHSValue("parameter_4", float, 0.0, status=status),
BHSValue("parameter_5", float, 0.0, status=status),
BHSValue(-1, "parameter_1", float, 0.0, status=status),
BHSValue(-1, "parameter_2", float, 0.0, status=status),
BHSValue(-1, "parameter_3", float, 0.0, status=status),
BHSValue(-1, "parameter_4", float, 0.0, status=status),
BHSValue(-1, "parameter_5", float, 0.0, status=status),
]
+92 -27
View File
@@ -1,5 +1,5 @@
# Value.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -17,45 +17,92 @@
# -*- coding: utf-8 -*-
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
class BHSValue(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, name: str = "", type=float, value=0.0,
status=None):
super(BHSValue, self).__init__()
self._status = status
def __init__(self, id: int = -1, name: str = "",
type=float, value=0.0,
status=None, owner_scenario=-1):
super(BHSValue, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._type = type
self._value = type(value)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE hydraulic_structures_basic_value(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE hydraulic_structures_basic_value{ext} (
{cls.create_db_add_pamhyr_id()},
name TEXT NOT NULL,
type TEXT NOT NULL,
value TEXT NOT NULL,
bhs INTEGER,
FOREIGN KEY(bhs) REFERENCES hydraulic_structures_basic(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(bhs) REFERENCES hydraulic_structures_basic(id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 6:
cls._db_create(execute)
return True
else:
cls._db_update_to_0_2_0(execute, data)
return True
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "hydraulic_structures_basic_value"
bhs = data['id2pid']['hydraulic_structures_basic']
cls.update_db_add_pamhyr_id(execute, table, data)
cls._db_update_to_0_2_0_set_bhs_pid(execute, table, bhs)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, name, type, value, bhs, scenario) " +
"SELECT pamhyr_id, name, type, value, bhs, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_update_to_0_2_0_set_bhs_pid(cls, execute, table, bhs):
els = execute(
f"SELECT pamhyr_id, bhs FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
bhs_id = next(it)
execute(
f"UPDATE {table} " +
f"SET bhs = {bhs[bhs_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _str_to_type(cls, type):
@@ -86,44 +133,62 @@ class BHSValue(SQLSubModel):
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data["status"]
scenario = data["scenario"]
loaded = data['loaded_pid']
bhs_id = data["bhs_id"]
if scenario is None:
return []
table = execute(
"SELECT name, type, value " +
"SELECT pamhyr_id, name, type, value, scenario " +
"FROM hydraulic_structures_basic_value " +
f"WHERE bhs = '{bhs_id}'"
f"WHERE bhs = '{bhs_id}' " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
name = row[0]
type = cls._str_to_type(row[1])
value = row[2]
it = iter(row)
pid = next(it)
name = next(it)
type = cls._str_to_type(next(it))
value = next(it)
owner_scenario = next(it)
val = cls(
name=name,
type=type,
value=value,
status=data['status']
id=pid, name=name,
type=type, value=value,
status=status, owner_scenario=owner_scenario
)
new.append(val)
loaded.add(pid)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
bhs_id = data["bhs_id"]
sql = (
execute(
"INSERT INTO " +
"hydraulic_structures_basic_value(name, type, value, bhs) " +
"hydraulic_structures_basic_value" +
"(pamhyr_id, name, type, value, bhs, scenario) " +
"VALUES (" +
f"{self._pamhyr_id}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type_to_str(self._type))}', " +
f"'{self._db_format(self._value)}', " +
f"{bhs_id}" +
f"{bhs_id}, " +
f"{self._status.scenario_id}"
")"
)
execute(sql)
return True
@@ -142,4 +207,4 @@ class BHSValue(SQLSubModel):
@value.setter
def value(self, value):
self._value = self._type(value)
self._status.modified()
self.modified()
@@ -1,5 +1,5 @@
# HydraulicStructures.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 @@ from functools import reduce
from tools import trace, timer, old_pamhyr_date_to_timestamp
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Except import NotImplementedMethodeError
from Model.HydraulicStructures.Basic.HydraulicStructures import BasicHS
@@ -36,162 +37,292 @@ class HydraulicStructure(SQLSubModel):
_sub_classes = [
BasicHS,
]
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "",
status=None):
super(HydraulicStructure, self).__init__()
self._status = status
if id == -1:
self.id = HydraulicStructure._id_cnt
else:
self.id = id
status=None, owner_scenario=-1):
super(HydraulicStructure, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._input_rk = None
self._output_rk = None
self._input_section = None
self._output_section = None
self._input_reach = None
self._output_reach = None
self._enabled = True
self._data = []
HydraulicStructure._id_cnt = max(
HydraulicStructure._id_cnt + 1,
self.id
)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE hydraulic_structures(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE hydraulic_structures{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
enabled BOOLEAN NOT NULL,
input_rk REAL NOT NULL,
output_rk REAL NOT NULL,
input_reach INTEGER,
output_reach INTEGER,
FOREIGN KEY(input_reach) REFERENCES river_reach(id),
FOREIGN KEY(output_reach) REFERENCES river_reach(id)
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)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
rl = int(release)
rl = int(release)
if major == minor == "0":
if rl < 6:
cls._db_create(execute)
return True
if rl < 11:
for v in ["input", "output"]:
execute(
f"""
ALTER TABLE hydraulic_structures
RENAME COLUMN {v}_kp TO {v}_rk
"""
)
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if rl < 1:
cls._db_update_to_0_1_1(execute, data)
if rl < 2:
execute(
"""
ALTER TABLE hydraulic_structures
RENAME COLUMN input_kp TO input_rk
"""
)
execute(
"""
ALTER TABLE hydraulic_structures
RENAME COLUMN output_kp TO output_rk
"""
"ALTER TABLE hydraulic_structures " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return True
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "hydraulic_structures"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
cls._db_update_to_0_2_0_set_reach_pid(execute, table, reachs)
cls._db_update_to_0_1_1(
execute, data,
origin_version="0.0.*"
)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, name, enabled, input_section, output_section, " +
"input_reach, output_reach, scenario) " +
"SELECT pamhyr_id, name, enabled, " +
"input_section, output_section, " +
"input_reach, output_reach, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@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}"
)
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"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"
)
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
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data["status"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return []
table = execute(
"SELECT id, name, enabled, " +
"input_rk, output_rk, " +
"input_reach, output_reach " +
"FROM hydraulic_structures "
"SELECT pamhyr_id, deleted, name, enabled, " +
"input_section, output_section, " +
"input_reach, output_reach, scenario " +
"FROM hydraulic_structures " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
new.append(cls._db_load_new(execute, data, row))
hs_id = next(it)
name = next(it)
enabled = (next(it) == 1)
input_rk = next(it)
output_rk = next(it)
input_reach_id = next(it)
output_reach_id = next(it)
hs = cls(
id=hs_id,
name=name,
status=data['status']
)
hs.enabled = enabled
hs.input_rk = input_rk if input_rk != -1 else None
hs.output_rk = output_rk if output_rk != -1 else None
hs.input_reach, hs.output_reach = reduce(
lambda acc, n: (
n if n.id == input_reach_id else acc[0],
n if n.id == output_reach_id else acc[1]
),
data["edges"],
[None, None]
)
data['hs_id'] = hs_id
hs._data = BasicHS._db_load(execute, data)
new.append(hs)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
@classmethod
def _db_load_new(cls, execute, data, row):
it = iter(row)
status = data["status"]
scenario = data["scenario"]
loaded = data['loaded_pid']
hs_id = next(it)
deleted = (next(it) == 1)
name = next(it)
enabled = (next(it) == 1)
input_section_id = next(it)
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(
id=hs_id, name=name, status=status,
owner_scenario=owner_scenario
)
if deleted:
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]
),
data["edges"],
[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]
),
sections,
[None, None]
)
loaded.add(hs_id)
data['hs_id'] = hs_id
hs._data = BasicHS._db_load(execute, data)
return hs
def _db_save(self, execute, data=None):
execute(f"DELETE FROM hydraulic_structures WHERE id = {self.id}")
if not self.must_be_saved():
return True
execute(
"DELETE FROM hydraulic_structures " +
f"WHERE pamhyr_id = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
input_reach_id = -1
if self._input_reach is not None:
input_reach_id = self._input_reach.id
input_reach_id = self._input_reach.pamhyr_id
output_reach_id = -1
if self._output_reach is not None:
output_reach_id = self._output_reach.id
output_reach_id = self._output_reach.pamhyr_id
input_rk = -1
if self.input_rk is not None:
input_rk = self.input_rk
input_section = 'NULL'
if self.input_section is not None:
input_section = self.input_section.pamhyr_id
output_rk = -1
if self.output_rk is not None:
output_rk = self.output_rk
output_section = 'NULL'
if self.output_section is not None:
output_section = self.output_section.pamhyr_id
sql = (
execute(
"INSERT INTO " +
"hydraulic_structures(" +
" id, name, enabled, input_rk, output_rk, " +
" input_reach, output_reach" +
" pamhyr_id, deleted, name, enabled, " +
" input_section, output_section, " +
" input_reach, output_reach, " +
" scenario" +
") " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"{self.pamhyr_id}, " +
f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"{self._db_format(self.enabled)}, " +
f"{input_rk}, {output_rk}, " +
f"{input_reach_id}, {output_reach_id}" +
f"{input_section}, {output_section}, " +
f"{input_reach_id}, {output_reach_id}, " +
f"{self._status.scenario_id}"
")"
)
execute(sql)
data['hs_id'] = self.id
data['hs_id'] = self.pamhyr_id
execute(
"DELETE FROM hydraulic_structures_basic " +
f"WHERE hs = {self.id}"
f"WHERE hs = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
for basic in self._data:
@@ -199,41 +330,71 @@ class HydraulicStructure(SQLSubModel):
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(self._data)
return len(self.lst)
@property
def lst(self):
return list(
filter(
lambda bhs: not bhs.is_deleted(),
self._data,
)
)
@property
def name(self):
if self._name == "":
return f"HS{self.id + 1}"
return f"HS #{self.pamhyr_id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def input_rk(self):
return self._input_rk
if self._input_section is None:
return None
@input_rk.setter
def input_rk(self, input_rk):
if input_rk is not None:
input_rk = float(input_rk)
self._input_rk = input_rk
self._status.modified()
return self._input_section
@property
def output_rk(self):
return self._output_rk
if self._output_section is None:
return None
@output_rk.setter
def output_rk(self, output_rk):
self._output_rk = output_rk
self._status.modified()
return self._output_section
@property
def input_section(self):
return self._input_section
@input_section.setter
def input_section(self, input_section):
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):
@@ -242,7 +403,7 @@ class HydraulicStructure(SQLSubModel):
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
self._status.modified()
self.modified()
@property
def input_reach(self):
@@ -251,7 +412,7 @@ class HydraulicStructure(SQLSubModel):
@input_reach.setter
def input_reach(self, input_reach):
self._input_reach = input_reach
self._status.modified()
self.modified()
@property
def output_reach(self):
@@ -260,26 +421,34 @@ class HydraulicStructure(SQLSubModel):
@output_reach.setter
def output_reach(self, output_reach):
self._output_reach = output_reach
self._status.modified()
self.modified()
@property
def basic_structures(self):
return self._data.copy()
return self.lst.copy()
def basic_structure(self, index: int):
return self._data[index]
if len(self._data) == 0:
return None
return self.lst[index]
def add(self, index: int):
value = NotDefined(status=self._status)
self._data.insert(index, value)
self._status.modified()
self.modified()
return value
def insert(self, index: int, value: BasicHS):
self._data.insert(index, value)
self._status.modified()
if value in self._data:
value.set_as_not_deleted()
else:
self._data.insert(index, value)
def delete_i(self, indexes):
self.modified()
def hard_delete_i(self, indexes):
self._data = list(
map(
lambda e: e[1],
@@ -289,20 +458,30 @@ class HydraulicStructure(SQLSubModel):
)
)
)
self._status.modified()
self.modified()
def delete(self, els):
self._data = list(
filter(
lambda e: e not in els,
self._data
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self.lst)
)
)
)
self._status.modified()
self.modified()
def delete(self, els):
for el in els:
el.set_as_deleted()
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()
@@ -1,5 +1,5 @@
# HydraulicStructuresList.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.HydraulicStructures.HydraulicStructures import HydraulicStructure
@@ -42,10 +42,10 @@ class HydraulicStructureList(PamhyrModelList):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM hydraulic_structures")
if data is None:
data = {}
execute(
"DELETE FROM hydraulic_structures " +
f"WHERE scenario = {self._status.scenario_id}"
)
for hs in self._lst:
hs._db_save(execute, data=data)
@@ -53,9 +53,12 @@ class HydraulicStructureList(PamhyrModelList):
return True
def new(self, lst, index):
n = HydraulicStructure(status=self._status)
n = HydraulicStructure(
status=self._status,
owner_scenario=self._status.scenario_id
)
self._lst.insert(index, n)
self._status.modified()
self.modified()
return n
def __copy__(self):
+246 -94
View File
@@ -1,5 +1,5 @@
# InitialConditions.py -- Pamhyr
# Copyright (C) 2023-2024 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,32 +24,34 @@ from functools import reduce
from numpy import interp
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
logger = logging.getLogger()
class Data(SQLSubModel):
def __init__(self, name: str = "",
comment: str = "", reach=None,
rk: float = 0.0, discharge: float = 0.0,
def __init__(self, id: int = -1,
name: str = "", comment: str = "",
reach=None, section=None,
discharge: float = 0.0,
height: float = 0.0,
status=None):
super(Data, self).__init__()
self._status = status
status=None,
owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._reach = reach
self._name = name
self._comment = comment
self._rk = rk
self._section = section
self._discharge = discharge
self._speed = 0.0
self._elevation = 0.0
self._height = height
if self._rk != 0.0:
if self._section is not None:
self._update_from_rk()
if self._height != 0.0:
self._update_from_height()
@@ -57,25 +59,29 @@ class Data(SQLSubModel):
self._update_from_discharge()
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE initial_conditions(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
name TEXT NOT NULL,
comment TEXT NOT NULL,
reach INTEGER,
rk REAL NOT NULL,
section INTEGER,
discharge REAL NOT NULL,
height REAL NOT NULL,
FOREIGN KEY(reach) REFERENCES river_reach(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 11:
@@ -83,56 +89,156 @@ class Data(SQLSubModel):
"ALTER TABLE initial_conditions RENAME COLUMN kp TO rk"
)
return cls._update_submodel(execute, version)
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 1:
cls._db_update_to_0_1_1(execute, data)
if int(release) < 2:
execute(
"ALTER TABLE initial_conditions " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "initial_conditions"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
cls._db_update_to_0_2_0_set_reach_pid(execute, table, reachs)
cls._db_update_to_0_1_1(
execute, data,
origin_version="0.0.*"
)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, ind, name, comment, reach, section, " +
"discharge, height, scenario) " +
"SELECT pamhyr_id, ind, name, comment, reach, section, " +
"discharge, height, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_update_to_0_1_1(cls, execute, data,
origin_version="0.1.0"):
execute(
"ALTER TABLE initial_conditions " +
f"ADD COLUMN section INTEGER"
)
cls._db_update_to_0_1_1_assoc_section_from_rk(
execute, "initial_conditions",
origin_version=origin_version
)
@classmethod
def _db_load(cls, execute, data=None):
id = data["reach"].id
new = []
reach = data["reach"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
id = reach.pamhyr_id
table = execute(
"SELECT ind, name, comment, rk, discharge, height " +
"SELECT pamhyr_id, deleted, ind, name, comment, " +
"section, discharge, height, scenario " +
"FROM initial_conditions " +
f"WHERE reach = {id}"
f"WHERE reach = {id} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
"ORDER BY ind ASC"
)
new = []
for _ in table:
new.append(None)
ok = True
for row in table:
ind = row[0]
name = row[1]
comment = row[2]
rk = row[3]
discharge = row[4]
height = row[5]
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
ind = next(it)
name = next(it)
comment = next(it)
section_id = next(it)
discharge = next(it)
height = next(it)
owner_scenario = next(it)
section = reduce(
lambda acc, s: (
s if s.pamhyr_id == section_id else acc
),
reach.reach.profiles,
None
)
if section is None:
logger.debug(
f"Section ID {section_id} do not exist in reach " +
f"{reach.name} ({reach.pamhyr_id})"
)
ok = False
continue
d = cls(
id=pid,
reach=data["reach"],
status=data["status"],
name=name,
comment=comment,
rk=rk,
section=section,
discharge=discharge,
height=height,
owner_scenario=owner_scenario
)
if deleted:
d.set_as_deleted()
loaded.add(pid)
new.append(d)
if not ok:
logger.info(
"Please regenerate/defined the inital conditions " +
f"for reach {reach.name} ({reach.pamhyr_id})"
)
new[ind] = d
# Recution only if no data found
if len(new) == 0:
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
ind = data["ind"]
execute(
"INSERT INTO " +
"initial_conditions(ind, name, comment, rk, " +
"discharge, height, reach) " +
"initial_conditions(pamhyr_id, deleted, ind, name, comment, " +
"section, discharge, height, reach, scenario) " +
"VALUES (" +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(self.name)}', " +
f"'{self._db_format(self._comment)}', " +
f"{self._rk}, {self._discharge}, {self._height}, " +
f"{self._reach.id}" +
f"{self._section.id}, {self._discharge}, {self._height}, " +
f"{self._reach.id}, {self._status.scenario_id}" +
")"
)
@@ -142,7 +248,7 @@ class Data(SQLSubModel):
new = Data(
name=self.name,
comment=self._comment,
rk=self._rk,
section=self._section,
discharge=self._discharge,
height=self._height,
reach=self._reach,
@@ -161,8 +267,8 @@ class Data(SQLSubModel):
val = self._name
elif key == "comment":
val = self._comment
elif key == "rk":
val = self._rk
elif key == "rk" or key == "section":
val = self._section
elif key == "speed":
val = self._speed
elif key == "discharge":
@@ -175,9 +281,9 @@ class Data(SQLSubModel):
return val
def _update_get_min(self):
profile = self._reach.reach.get_profiles_from_rk(self._rk)
if len(profile) > 0:
min = profile[0].z_min()
profile = self._section
if profile is not None:
min = profile.z_min()
else:
min = 0.0
@@ -204,8 +310,8 @@ class Data(SQLSubModel):
self._name = str(value)
elif key == "comment":
self._comment = str(value)
elif key == "rk":
self._rk = float(value)
elif key == "rk" or key == "section":
self._section = value
self._update_from_rk()
elif key == "speed":
# Not supposed to be modified
@@ -220,7 +326,7 @@ class Data(SQLSubModel):
self._height = float(value)
self._update_from_height()
self._status.modified()
self.modified()
class InitialConditions(SQLSubModel):
@@ -241,8 +347,8 @@ class InitialConditions(SQLSubModel):
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
return cls._update_submodel(execute, version)
def _db_update(cls, execute, version, data=None):
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
@@ -270,8 +376,18 @@ class InitialConditions(SQLSubModel):
return ok
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(self._data)
return len(self.data)
def lst(self):
return self._data
@@ -282,50 +398,70 @@ class InitialConditions(SQLSubModel):
@reach.setter
def reach(self, new):
self._reach = reach
self._status.modified()
self._reach = new
self.modified()
@property
def data(self):
return self._data.copy()
return list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
@data.setter
def data(self, data):
self._data = data
def get(self, index):
return self._data[index]
return self.data[index]
def set(self, index, data):
self._data.insert(index, data)
self._status.modified()
self.modified()
def new(self, index):
n = Data(reach=self._reach, status=self._status)
self._data.insert(index, n)
self._status.modified()
self.modified()
def new_from_data(self, rk, discharge, elevation):
n = Data(reach=self._reach, status=self._status)
n['rk'] = rk
section = reduce(
lambda acc, s: (
s if s.rk == rk else acc
),
self._reach.reach.profiles,
None
)
n['section'] = section
n['discharge'] = discharge
n['elevation'] = elevation
return n
def insert(self, index, data):
self._data.insert(index, data)
self._status.modified()
if data in self._data:
data.set_as_not_deleted()
else:
self._data.insert(index, data)
self.modified()
def delete(self, data):
self._data = list(
filter(
lambda x: x not in data,
self._data
list(
map(
lambda x: x.set_as_deleted(),
filter(
lambda x: x in data,
self._data
)
)
)
self._status.modified()
self.modified()
def delete_i(self, indexes):
data = list(
@@ -341,18 +477,23 @@ class InitialConditions(SQLSubModel):
def sort(self, reverse=False, key=None):
self._data.sort(reverse=reverse, key=key)
self._status.modified()
self.modified()
def _data_get(self, key):
return list(
map(
lambda d: d[key],
self._data
self.data
)
)
def get_rk(self):
return self._data_get("rk")
return list(
map(
lambda d: d["rk"].rk,
self.data
)
)
def get_elevation(self):
return self._data_get("elevation")
@@ -360,12 +501,13 @@ class InitialConditions(SQLSubModel):
def get_discharge(self):
return self._data_get("discharge")
def clear_data(self):
for data in self._data:
data.set_as_deleted()
def generate_growing_constant_depth(self, height: float,
compute_discharge: bool):
profiles = self._reach.reach.profiles.copy()
profiles.reverse()
previous_elevation = -99999.99
data_discharge = {}
@@ -375,12 +517,14 @@ class InitialConditions(SQLSubModel):
data_discharge[profile.rk] = 0.0
else:
for data in self._data:
data_discharge[data["rk"]] = data["discharge"]
data_discharge[data["rk"].rk] = data["discharge"]
incline = self._reach.reach.get_incline_median_mean()
logger.debug(f"incline = {incline}")
self._data = []
for profile in profiles:
self.clear_data()
for profile in reversed(profiles):
width = profile.wet_width(profile.z_min() + height)
area = profile.wet_area(profile.z_min() + height)
radius = profile.wet_radius(profile.z_min() + height)
@@ -395,7 +539,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)
@@ -418,21 +565,17 @@ class InitialConditions(SQLSubModel):
logger.debug(f" discharge = {discharge}")
new = Data(reach=self._reach, status=self._status)
new["rk"] = profile.rk
new["rk"] = profile
new["discharge"] = discharge
new["elevation"] = elevation
previous_elevation = elevation
self._data.append(new)
self._data.reverse()
def generate_discharge(self, discharge: float, compute_height: bool):
profiles = self._reach.reach.profiles.copy()
if profiles is None:
return None
profiles.reverse()
previous_elevation = -99999.99
@@ -443,33 +586,41 @@ class InitialConditions(SQLSubModel):
data_height[profile.rk] = 0.0
else:
for data in self._data:
data_height[data["rk"]] = data["height"]
data_height[data["rk"].rk] = data["height"]
incline = self._reach.reach.get_incline_median_mean()
logger.debug(f"incline = {incline}")
self._data = []
for profile in profiles:
self.clear_data()
for profile in reversed(profiles):
width = profile.width_approximation()
frictions = self._reach.frictions.frictions
strickler = None
if frictions is not None:
if len(frictions) >= 1:
for f in frictions:
if f.contains_rk(profile.rk):
strickler = f.get_friction(profile.rk)[0]
if strickler is None:
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
else:
height = (
discharge
/
((width * 0.8) * strickler * (abs(incline) ** (0.5)))
/ ((width * 0.8)
* strickler
* (abs(incline) ** (0.5)))
) ** (0.6)
elevation = max(
@@ -483,15 +634,13 @@ class InitialConditions(SQLSubModel):
logger.debug(f" height = {height}")
new = Data(reach=self._reach, status=self._status)
new["rk"] = profile.rk
new["section"] = profile
new["discharge"] = discharge
new["elevation"] = elevation
previous_elevation = elevation
self._data.append(new)
self._data.reverse()
def generate_height(self,
elevation1: float,
elevation2: float,
@@ -507,20 +656,23 @@ class InitialConditions(SQLSubModel):
data_discharge[profile.rk] = 0.0
else:
for data in self._data:
data_discharge[data["rk"]] = data["discharge"]
data_discharge[data["rk"].rk] = data["discharge"]
self._data = []
self.clear_data()
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,
[upstream_rk, downstream_rk],
[elevation1, elevation2])
new = Data(reach=self._reach, status=self._status)
new["rk"] = profile.rk
new["rk"] = profile
new["discharge"] = d
new["elevation"] = elevation
self._data.append(new)
@@ -1,5 +1,5 @@
# InitialConditionsDict.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +64,8 @@ class InitialConditionsDict(PamhyrModelDict):
execute(
"DELETE FROM initial_conditions " +
f"WHERE reach = '{reach.id}'"
f"WHERE reach = '{reach.id}' " +
f"AND scenario = {self._status.scenario_id}"
)
ok &= self._dict[reach]._db_save(execute, data)
@@ -1,5 +1,5 @@
# InitialConditionsAdisTS.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -23,6 +23,7 @@ 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.InitialConditionsAdisTS.InitialConditionsAdisTSSpec \
import ICAdisTSSpec
@@ -34,19 +35,17 @@ class InitialConditionsAdisTS(SQLSubModel):
_sub_classes = [
ICAdisTSSpec,
]
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "default",
pollutant: int = -1, status=None):
super(InitialConditionsAdisTS, self).__init__()
pollutant: int = -1, status=None,
owner_scenario=-1):
super(InitialConditionsAdisTS, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if id == -1:
self.id = InitialConditionsAdisTS._id_cnt
else:
self.id = id
self._name = name
self._pollutant = pollutant
self._concentration = None
@@ -56,81 +55,155 @@ class InitialConditionsAdisTS(SQLSubModel):
self._enabled = True
self._data = []
InitialConditionsAdisTS._id_cnt = max(
InitialConditionsAdisTS._id_cnt + 1,
self.id
)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE initial_conditions_adists(
id INTEGER NOT NULL PRIMARY KEY,
pollutant INTEGER NOT NULL,
name TEXT NOT NULL,
concentration REAL NOT NULL,
eg REAL NOT NULL,
em REAL NOT NULL,
ed REAL NOT NULL,
enabled BOOLEAN NOT NULL,
FOREIGN KEY(pollutant) REFERENCES Pollutants(id)
)
""")
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
pollutant INTEGER NOT NULL,
name TEXT NOT NULL,
concentration REAL NOT NULL,
eg REAL NOT NULL,
em REAL NOT NULL,
ed REAL NOT NULL,
enabled BOOLEAN NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(pollutant) REFERENCES Pollutants(pamhyr_id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == 0 and minor < 1:
if int(release) < 6:
if major == "0" and int(minor) < 2:
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
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "initial_conditions_adists"
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}_tmp " +
"(pamhyr_id, pollutant, name, concentration, " +
"eg, em, ed, enabled, scenario) " +
"SELECT pamhyr_id, pollutant, name, concentration, " +
"eg, em, ed, enabled, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_pollutants_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_pollutants_pid(cls, execute, data):
pid_pol = data["id2pid"]["Pollutants"]
els = execute(
f"SELECT pamhyr_id, pollutant " +
"FROM initial_conditions_adists"
)
for row in els:
it = iter(row)
pid = next(it)
pol_id = next(it)
if pol_id == -1:
continue
execute(
f"UPDATE initial_conditions_adists " +
f"SET pollutant = {pid_pol[pol_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@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 id, pollutant, name, concentration, eg, em, ed, " +
"enabled " +
"FROM initial_conditions_adists"
"SELECT pamhyr_id, deleted, " +
"pollutant, name, concentration, eg, em, ed, " +
"enabled, scenario " +
"FROM initial_conditions_adists " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
if table is not None:
for row in table:
IC_id = row[0]
pollutant = row[1]
name = row[2]
concentration = row[3]
eg = row[4]
em = row[5]
ed = row[6]
enabled = (row[7] == 1)
it = iter(row)
IC = cls(
id=IC_id,
pid = next(it)
deleted = (next(it) == 1)
pollutant = next(it)
name = next(it)
concentration = next(it)
eg = next(it)
em = next(it)
ed = next(it)
enabled = (next(it) == 1)
owner_scenario = next(it)
ic = cls(
id=pid,
name=name,
status=data['status']
status=status,
owner_scenario=owner_scenario
)
if deleted:
ic.set_as_deleted()
IC.pollutant = pollutant
IC.concentration = concentration
IC.eg = eg
IC.em = em
IC.ed = ed
IC.enabled = enabled
ic.pollutant = pollutant
ic.concentration = concentration
ic.eg = eg
ic.em = em
ic.ed = ed
ic.enabled = enabled
data['ic_default_id'] = IC_id
IC._data = ICAdisTSSpec._db_load(execute, data)
data['ic_default_id'] = pid
ic._data = ICAdisTSSpec._db_load(execute, data)
new.append(IC)
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):
execute(f"DELETE FROM initial_conditions_adists WHERE id = {self.id}")
if not self.must_be_saved():
return True
execute(
"DELETE FROM initial_conditions_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
pollutant = -1
if self.pollutant is not None:
@@ -155,12 +228,14 @@ class InitialConditionsAdisTS(SQLSubModel):
sql = (
"INSERT INTO " +
"initial_conditions_adists(" +
"id, pollutant, name, concentration, " +
"eg, em, ed, enabled" +
"pamhyr_id, deleted, pollutant, name, concentration, " +
"eg, em, ed, enabled, scenario" +
") " +
"VALUES (" +
f"{self.id}, {pollutant}, '{self._db_format(self._name)}', " +
f"{concentration}, {eg}, {em}, {ed}, {self._enabled}" +
f"{self.id}, {self.is_deleted()}, " +
f"{pollutant}, '{self._db_format(self._name)}', " +
f"{concentration}, {eg}, {em}, {ed}, {self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
@@ -168,8 +243,9 @@ class InitialConditionsAdisTS(SQLSubModel):
data['ic_default_id'] = self.id
execute(
"DELETE FROM initial_conditions_spec " +
f"WHERE ic_default = {self.id}"
"DELETE FROM initial_conditions_adists_spec " +
f"WHERE ic_default = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
for ic_spec in self._data:
@@ -187,7 +263,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def pollutant(self):
@@ -196,7 +272,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@pollutant.setter
def pollutant(self, pollutant):
self._pollutant = pollutant
self._status.modified()
self.modified()
@property
def concentration(self):
@@ -205,7 +281,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@concentration.setter
def concentration(self, concentration):
self._concentration = concentration
self._status.modified()
self.modified()
@property
def eg(self):
@@ -214,7 +290,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@eg.setter
def eg(self, eg):
self._eg = eg
self._status.modified()
self.modified()
@property
def em(self):
@@ -223,7 +299,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@em.setter
def em(self, em):
self._em = em
self._status.modified()
self.modified()
@property
def ed(self):
@@ -232,7 +308,7 @@ class InitialConditionsAdisTS(SQLSubModel):
@ed.setter
def ed(self, ed):
self._ed = ed
self._status.modified()
self.modified()
@property
def enabled(self):
@@ -241,28 +317,45 @@ 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()
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSList.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.InitialConditionsAdisTS.InitialConditionsAdisTS \
import InitialConditionsAdisTS
@@ -43,7 +43,10 @@ class InitialConditionsAdisTSList(PamhyrModelList):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM initial_conditions_adists")
execute(
"DELETE FROM initial_conditions_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
@@ -1,5 +1,5 @@
# InitialConditionsAdisTSSpec.py -- Pamhyr
# Copyright (C) 2023-2024 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,26 +22,20 @@ 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 ICAdisTSSpec(SQLSubModel):
_sub_classes = [
]
_id_cnt = 0
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
status=None):
status=None, owner_scenario=None):
super(ICAdisTSSpec, self).__init__()
self._status = status
if id == -1:
self.id = ICAdisTSSpec._id_cnt
else:
self.id = id
self._name_section = name
self._reach = None
self._start_rk = None
@@ -53,73 +47,138 @@ class ICAdisTSSpec(SQLSubModel):
self._rate = None
self._enabled = True
ICAdisTSSpec._id_cnt = max(ICAdisTSSpec._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE initial_conditions_spec(
id INTEGER NOT NULL PRIMARY KEY,
ic_default INTEGER NOT NULL,
name TEXT NOT NULL,
reach INTEGER NOT NULL,
start_rk REAL NOT NULL,
end_rk REAL NOT NULL,
concentration REAL NOT NULL,
eg REAL NOT NULL,
em REAL NOT NULL,
ed REAL NOT NULL,
rate REAL NOT NULL,
enabled BOOLEAN NOT NULL,
FOREIGN KEY(ic_default) REFERENCES initial_conditions_adists(id),
FOREIGN KEY(reach) REFERENCES river_reach(id)
)
""")
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE initial_conditions_adists_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,
concentration REAL NOT NULL,
eg REAL NOT NULL,
em REAL NOT NULL,
ed REAL NOT NULL,
rate REAL NOT NULL,
enabled BOOLEAN NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(ic_default)
REFERENCES initial_conditions_adists(pamhyr_id),
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == 0 and minor < 1:
if int(release) < 6:
if major == "0" and int(minor) < 2:
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
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "initial_conditions_spec"
table_new = "initial_conditions_adists_spec"
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, ic_default, name, reach, start_rk, end_rk, " +
"concentration, eg, em, ed, rate, enabled, scenario) " +
"SELECT pamhyr_id, ic_default, name, reach, start_rk, end_rk, " +
"concentration, eg, em, ed, rate, enabled, 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)
cls._db_update_to_0_2_0_set_ica_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_ica_pid(cls, execute, data):
pid_ica = data["id2pid"]["initial_conditions_adist"]
els = execute(
"SELECT pamhyr_id, ic_default " +
"FROM initial_conditions_adists_spec"
)
for row in els:
it = iter(row)
pid = next(it)
ica_id = next(it)
if ica_id == -1:
continue
execute(
f"UPDATE initial_conditions_adists_spec " +
f"SET ic_default = {pid_ica[ica_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@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 id, ic_default, name, reach, start_rk, end_rk, " +
"concentration, eg, em, ed, rate, enabled " +
"FROM initial_conditions_spec " +
f"WHERE ic_default = {data['ic_default_id']} "
"SELECT pamhyr_id, deleted, name, reach, start_rk, end_rk, " +
"concentration, eg, em, ed, rate, enabled, scenario " +
"FROM initial_conditions_adists_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:
id = row[0]
name = row[2]
reach = row[3]
start_rk = row[4]
end_rk = row[5]
concentration = row[6]
eg = row[7]
em = row[8]
ed = row[9]
rate = row[10]
enabled = (row[11] == 1)
it = iter(row)
# new_spec = [name, reach, start_rk, end_rk,
# concentration, eg, em, ed, rate, enabled]
id = next(it)
deleted = (next(it) == 1)
name = next(it)
reach = next(it)
start_rk = next(it)
end_rk = next(it)
concentration = next(it)
eg = next(it)
em = next(it)
ed = next(it)
rate = next(it)
enabled = (next(it) == 1)
owner_scenario = next(it)
new_spec = cls(
id=id,
name=name,
status=data['status']
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
@@ -131,30 +190,37 @@ class ICAdisTSSpec(SQLSubModel):
new_spec.rate = rate
new_spec.enabled = enabled
# loaded.add(pid)
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_spec(id, ic_default, name, reach, " +
"start_rk, end_rk, concentration, eg, em, ed, rate, enabled) " +
"initial_conditions_adists_spec(pamhyr_id, deleted, " +
"ic_default, name, reach, " +
"start_rk, end_rk, concentration, eg, em, ed, rate, " +
"enabled, scenario) " +
"VALUES (" +
f"{self.id}, " +
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}, " +
f"{self._end_rk}, " +
f"{self._start_rk}, {self._end_rk}, " +
f"{self._concentration}, " +
f"{self._eg}, " +
f"{self._em}, " +
f"{self._ed}, " +
f"{self._rate}, " +
f"{self._enabled}" +
f"{self._eg}, {self._em}, {self._ed}, " +
f"{self._rate}, {self._enabled}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
@@ -1,5 +1,5 @@
# LateralContribution.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -27,66 +27,226 @@ from tools import (
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class LateralContribution(SQLSubModel):
class Data(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "", status=None):
super(LateralContribution, self).__init__()
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._status = status
if id == -1:
self.id = LateralContribution._id_cnt
else:
self.id = id
self._name = name
self._type = ""
self._edge = None
self._begin_rk = 0.0
self._end_rk = 0.0
self._data = []
self._header = []
self._types = [float, float]
LateralContribution._id_cnt = max(
LateralContribution._id_cnt + 1, self.id)
self._types = types
self._data = [data0, data1]
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE lateral_contribution(
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
tab TEXT NOT NULL,
edge INTEGER,
begin_rk REAL NOT NULL,
end_rk REAL NOT NULL,
FOREIGN KEY(edge) REFERENCES river_reach(id)
)
""")
execute("""
CREATE TABLE lateral_contribution_data(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE lateral_contribution_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,
lc INTEGER,
FOREIGN KEY(lc) REFERENCES lateral_contribution(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(lc) REFERENCES lateral_contribution(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE boundary_condition_data " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data=None):
table = "lateral_contribution_data"
lcs = data['id2pid']['lateral_contribution']
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} " +
"(pamhyr_id, ind, data0, data1, lc, scenario) " +
"SELECT pamhyr_id, ind, data0, data1, lc, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_lc_pid(execute, table, lcs)
@classmethod
def _db_update_to_0_2_0_set_lc_pid(cls, execute, table, lcs):
els = execute(
f"SELECT pamhyr_id, lc FROM {table}"
)
for row in els:
it = iter(row)
pid = next(it)
lc_id = next(it)
execute(
f"UPDATE {table} " +
f"SET lc = {lcs[lc_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
lc = data["lc"]
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 lateral_contribution_data " +
f"WHERE lc = {lc._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 = lc._types[0](next(it))
data1 = lc._types[1](next(it))
owner_scenario = next(it)
nd = cls(
data0, data1,
id=pid,
types=lc._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]))
lc = data["lc"]
execute(
"INSERT INTO " +
"lateral_contribution_data (pamhyr_id, deleted, ind, " +
"data0, data1, lc, scenario) " +
f"VALUES ({pid}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(data0)}', {self._db_format(data1)}, " +
f"{lc._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)
class LateralContribution(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1,
name: str = "",
status=None, owner_scenario=-1):
super(LateralContribution, 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 lateral_contribution{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type TEXT NOT NULL,
tab 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)
)
""")
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 == minor == "0":
if int(release) < 11:
@@ -103,7 +263,45 @@ class LateralContribution(SQLSubModel):
"""
)
return True
execute(
"""
ALTER TABLE lateral_contribution
RENAME COLUMN edge TO reach
"""
)
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE lateral_contribution " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "lateral_contribution"
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} " +
"(pamhyr_id, name, type, tab, reach, scenario) " +
"SELECT pamhyr_id, name, type, tab, reach, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table, reachs)
@classmethod
def _get_ctor_from_type(cls, t):
@@ -124,84 +322,142 @@ class LateralContribution(SQLSubModel):
def _db_load(cls, execute, data=None):
new = []
tab = data["tab"]
status = data['status']
edges = data["edges"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT id, name, type, edge, begin_rk, end_rk " +
f"FROM lateral_contribution WHERE tab = '{tab}'"
"SELECT pamhyr_id, deleted, name, type, " +
"reach, begin_section, end_section, scenario " +
"FROM lateral_contribution " +
f"WHERE tab = '{tab}'" +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
for row in table:
t = row[2]
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)
lc = ctor(
id=row[0],
name=row[1],
status=data['status']
id=pid, name=name,
status=status,
owner_scenario=owner_scenario
)
lc.edge = None
lc.reach = None
lc._begin_section = None
lc._end_section = None
if row[3] != -1:
lc.edge = next(filter(lambda e: e.id == row[3], data["edges"]))
lc._begin_rk = row[4]
lc._end_rk = row[5]
lc.reach = next(
filter(
lambda e: e.id == reach,
edges
)
)
lc._begin_section = next(
filter(
lambda p: p.id == b_section,
lc.reach.reach.profiles
)
)
lc._end_section = next(
filter(
lambda p: p.id == e_section,
lc.reach.reach.profiles
)
)
values = execute(
"SELECT ind, data0, data1 FROM lateral_contribution_data " +
f"WHERE lc = '{lc.id}'"
)
# Create dummy data list
for _ in values:
lc.add(0)
# Write data
for v in values:
ind = v[0]
data0 = lc._types[0](v[1])
data1 = lc._types[1](v[2])
# Replace data at pos ind
lc._data[ind] = (data0, data1)
data["lc"] = lc
lc._data = Data._db_load(execute, data=data)
loaded.add(pid)
new.append(lc)
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
tab = data["tab"]
data["lc"] = self
execute(f"DELETE FROM lateral_contribution WHERE id = {self.id}")
execute(f"DELETE FROM lateral_contribution_data WHERE lc = {self.id}")
execute(
"DELETE FROM lateral_contribution_data " +
f"WHERE lc = {self._pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
edge = -1
if self._edge is not None:
edge = self._edge.id
reach = -1
begin_section = -1
end_section = -1
sql = (
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 " +
"lateral_contribution(id, name, type, tab, " +
"edge, begin_rk, end_rk) " +
"lateral_contribution(" +
"pamhyr_id, deleted, name, type, tab, " +
"reach, begin_section, end_section, scenario) " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', '{tab}', {edge}, " +
f"{self._begin_rk}, {self._end_rk}" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', '{tab}', {reach}, " +
f"{begin_section}, {end_section}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
ind = 0
for d in self._data:
data0 = self._db_format(str(d[0]))
data1 = self._db_format(str(d[1]))
data["ind"] = ind
d._db_save(execute, data)
sql = (
"INSERT INTO " +
"lateral_contribution_data(ind, data0, data1, lc) " +
f"VALUES ({ind}, '{data0}', {data1}, {self.id})"
)
execute(sql)
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(self._data)
return len(
list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
)
@classmethod
def compatibility(cls):
@@ -226,67 +482,66 @@ class LateralContribution(SQLSubModel):
@property
def name(self):
if self._name == "":
return f"L{self.id + 1}"
return f"LC #{self.id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def lctype(self):
return self._type
@property
def edge(self):
return self._edge
def reach(self):
return self._reach
@edge.setter
def edge(self, edge):
self._edge = edge
if edge is not None:
self._begin_rk = self._edge.reach.get_rk_min()
self._end_rk = self._edge.reach.get_rk_max()
self._status.modified()
@reach.setter
def reach(self, reach):
self._reach = reach
if reach is not None:
self._begin_section = self._reach.reach.profiles[0]
self._end_section = self._reach.reach.profiles[-1]
def has_edge(self):
return self._edge is not None
self.modified()
def has_reach(self):
return self._reach is not None
@property
def begin_rk(self):
return self._begin_rk
if self._begin_section is None:
return 0
@begin_rk.setter
def begin_rk(self, begin_rk):
if self._edge is None:
self._begin_rk = begin_rk
else:
_min = self._edge.reach.get_rk_min()
_max = self._edge.reach.get_rk_max()
return self._begin_section.rk
if _min <= begin_rk <= _max:
self._begin_rk = begin_rk
@property
def begin_section(self):
return self._begin_section
self._status.modified()
@begin_section.setter
def begin_section(self, section):
self._begin_section = section
self.modified()
@property
def end_rk(self):
return self._end_rk
if self._end_section is None:
return 0
@end_rk.setter
def end_rk(self, end_rk):
if self._edge is None:
self._end_rk = end_rk
else:
_min = self._edge.reach.get_rk_min()
_max = self._edge.reach.get_rk_max()
return self._end_section.rk
if _min <= end_rk <= _max:
self._end_rk = end_rk
@property
def end_section(self):
return self._end_section
self._status.modified()
@end_section.setter
def end_section(self, section):
self._end_section = section
self.modified()
@property
def header(self):
@@ -294,7 +549,12 @@ class LateralContribution(SQLSubModel):
@property
def data(self):
return self._data.copy()
return list(
filter(
lambda el: not el.is_deleted(),
self._data
)
)
def get_type_column(self, column):
if 0 <= column < 2:
@@ -328,17 +588,17 @@ class LateralContribution(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, 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()
self.modified()
def delete_i(self, indexes):
self._data = list(
@@ -350,7 +610,7 @@ class LateralContribution(SQLSubModel):
)
)
)
self._status.modified()
self.modified()
def delete(self, els):
self._data = list(
@@ -359,14 +619,14 @@ class LateralContribution(SQLSubModel):
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 get_i(self, index):
return self.data[index]
@@ -378,10 +638,10 @@ class LateralContribution(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)
@@ -392,9 +652,9 @@ class LateralContribution(SQLSubModel):
@timer
def convert(self, cls):
new = cls(name=self.name, status=self._status)
new.edge = self.edge
new.begin_rk = self.begin_rk
new.end_rk = self.end_rk
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)
@@ -408,7 +668,7 @@ class LateralContribution(SQLSubModel):
except Exception as e:
logger.info(e)
self._status.modified()
self.modified()
return new
def move_up(self, index):
@@ -416,11 +676,11 @@ class LateralContribution(SQLSubModel):
next = index - 1
d = self._data
d[index], d[next] = d[next], d[index]
self._status.modified()
self.modified()
def move_down(self, index):
if index >= 0:
prev = index + 1
d = self._data
d[index], d[prev] = d[prev], d[index]
self._status.modified()
self.modified()
@@ -1,5 +1,5 @@
# LateralContributionList.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelListWithTab
from Model.Tools.PamhyrListExt import PamhyrModelListWithTab
from Model.Except import NotImplementedMethodeError
from Model.LateralContribution.LateralContribution import LateralContribution
@@ -50,7 +50,10 @@ class LateralContributionList(PamhyrModelListWithTab):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM lateral_contribution")
execute(
"DELETE FROM lateral_contribution " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
@@ -1,5 +1,5 @@
# LateralContributionTypes.py -- Pamhyr
# Copyright (C) 2023-2024 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,8 +22,12 @@ from Model.LateralContribution.LateralContribution import LateralContribution
class NotDefined(LateralContribution):
def __init__(self, id: int = -1, name: str = "", status=None):
super(NotDefined, self).__init__(id=id, name=name, status=status)
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"]
@@ -34,8 +38,12 @@ class NotDefined(LateralContribution):
class LateralContrib(LateralContribution):
def __init__(self, id: int = -1, name: str = "", status=None):
super(LateralContrib, self).__init__(id=id, name=name, status=status)
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(LateralContrib, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "LC"
self._header = ["time", "discharge"]
@@ -47,8 +55,12 @@ class LateralContrib(LateralContribution):
class Rain(LateralContribution):
def __init__(self, id: int = -1, name: str = "", status=None):
super(Rain, self).__init__(id=id, name=name, status=status)
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(Rain, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "RA"
self._header = ["time", "discharge"]
@@ -60,8 +72,12 @@ class Rain(LateralContribution):
class Evaporation(LateralContribution):
def __init__(self, id: int = -1, name: str = "", status=None):
super(Evaporation, self).__init__(id=id, name=name, status=status)
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(Evaporation, self).__init__(
id=id, name=name, status=status,
owner_scenario=owner_scenario
)
self._type = "EV"
self._header = ["time", "discharge"]
@@ -1,5 +1,5 @@
# LateralContributionAdisTS.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -27,137 +27,390 @@ from tools import (
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class LateralContributionAdisTS(SQLSubModel):
class Data(SQLSubModel):
_sub_classes = []
_id_cnt = 0
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 lateral_contribution_data_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
lca INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(lca) REFERENCES lateral_contribution_adists(pamhyr_id)
)
""")
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and int(minor) < 2:
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
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "lateral_contribution_data_adists"
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}_tmp " +
"(pamhyr_id, data0, data1, lca, scenario) " +
"SELECT pamhyr_id, data0, data1, lc, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_lca_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_lca_pid(cls, execute, data):
pid_lca = data["id2pid"]["lateral_contribution_adists"]
els = execute(
"SELECT pamhyr_id, lca " +
"FROM lateral_contribution_data_adists"
)
for row in els:
it = iter(row)
pid = next(it)
lca_id = next(it)
if lca_id == -1:
continue
execute(
f"UPDATE lateral_contribution_data_adists " +
f"SET lca = {pid_lca[lca_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@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
lca = data["lca"]
table = execute(
"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 lca = '{lca.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)
lc = cls(
data0, data1,
id=pid, status=status,
owner_scenario=owner_scenario
)
if deleted:
lc.set_as_deleted()
loaded.add(pid)
new.append(lc)
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
lca = data["lca"]
execute(
"INSERT INTO " +
"lateral_contribution_data_adists(pamhyr_id, deleted," +
"data0, data1, lca, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._data[0]}, {self._data[1]}, " +
f"{lca.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)
class LateralContributionAdisTS(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1, pollutant: int = -1,
name: str = "", status=None):
super(LateralContributionAdisTS, self).__init__()
name: str = "", status=None,
owner_scenario=-1):
super(LateralContributionAdisTS, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if id == -1:
self.id = LateralContributionAdisTS._id_cnt
else:
self.id = id
self._pollutant = pollutant
self._edge = None
self._reach = None
self._begin_rk = 0.0
self._end_rk = 0.0
self._data = []
self._header = ["time", "rate"]
self._types = [self.time_convert, float]
LateralContributionAdisTS._id_cnt = max(
LateralContributionAdisTS._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE lateral_contribution_adists(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE lateral_contribution_adists{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
pollutant INTEGER NOT NULL,
edge INTEGER NOT NULL,
reach INTEGER NOT NULL,
begin_rk REAL NOT NULL,
end_rk REAL NOT NULL,
FOREIGN KEY(pollutant) REFERENCES Pollutants(id),
FOREIGN KEY(edge) REFERENCES river_reach(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(pollutant) REFERENCES Pollutants(pamhyr_id),
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id)
)
""")
execute("""
CREATE TABLE lateral_contribution_data_adists(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
lc INTEGER,
FOREIGN KEY(lc) REFERENCES lateral_contribution(id)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
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 not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "lateral_contribution_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}_tmp " +
"(pamhyr_id, pollutant, reach, begin_rk, end_rk, scenario) " +
"SELECT pamhyr_id, pollutant, edge, begin_rk, end_rk, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table, reachs)
cls._db_update_to_0_2_0_set_pollutants_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_pollutants_pid(cls, execute, data):
pid_pol = data["id2pid"]["Pollutants"]
els = execute(
f"SELECT pamhyr_id, pollutant " +
"FROM lateral_contribution_adists"
)
for row in els:
it = iter(row)
pid = next(it)
pol_id = next(it)
if pol_id == -1:
continue
execute(
f"UPDATE lateral_contribution_adists " +
f"SET pollutant = {pid_pol[pol_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@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 id, pollutant, edge, begin_rk, end_rk " +
"FROM lateral_contribution_adists"
"SELECT pamhyr_id, deleted, pollutant, reach, " +
"begin_rk, end_rk, scenario " +
"FROM lateral_contribution_adists " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
if table is not None:
for row in table:
lc = cls(
id=row[0],
pollutant=row[1],
status=data['status']
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
pollutant = next(it)
reach = next(it)
brk = next(it)
erk = next(it)
owner_scenario = next(it)
lca = cls(
id=pid,
pollutant=pollutant,
status=status,
owner_scenario=owner_scenario
)
if deleted:
lca.set_as_deleted()
lc.edge = row[2]
lc.begin_rk = row[3]
lc.end_rk = row[4]
lca.reach = reach
lca.begin_rk = brk
lca.end_rk = erk
values = execute(
"SELECT data0," +
" data1 FROM lateral_contribution_data_adists " +
f"WHERE lc = '{lc.id}'"
)
data["lca"] = lca
lca._data = Data._db_load(execute, data=data)
# Write data
for v in values:
data0 = lc._types[0](v[0])
data1 = lc._types[1](v[1])
# Replace data at pos ind
lc._data.append((data0, data1))
loaded.add(pid)
new.append(lca)
new.append(lc)
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(f"DELETE FROM lateral_contribution_adists" +
f" WHERE id = {self.id}")
execute(f"DELETE FROM lateral_contribution_data_adists" +
f" WHERE lc = {self.id}")
execute(
f"DELETE FROM lateral_contribution_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
execute(
f"DELETE FROM lateral_contribution_data_adists " +
f"WHERE lca = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
sql = (
execute(
"INSERT INTO " +
"lateral_contribution_adists(id, " +
"pollutant, edge, begin_rk, end_rk) " +
"lateral_contribution_adists(pamhyr_id, deleted, " +
"pollutant, reach, begin_rk, end_rk, scenario) " +
"VALUES (" +
f"{self.id}, {self._pollutant}, {self.edge}, " +
f"{self._begin_rk}, {self._end_rk}" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._pollutant}, {self.reach}, " +
f"{self._begin_rk}, {self._end_rk}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
data["lca"] = self
ind = 0
for d in self._data:
data0 = self._db_format(str(d[0]))
data1 = self._db_format(str(d[1]))
data["ind"] = ind
sql = (
"INSERT INTO " +
"lateral_contribution_data_adists(data0, data1, lc) " +
f"VALUES ('{data0}', {data1}, {self.id})"
)
execute(sql)
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(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):
@@ -176,13 +429,13 @@ class LateralContributionAdisTS(SQLSubModel):
return int(data)
@property
def edge(self):
return self._edge
def reach(self):
return self._reach
@edge.setter
def edge(self, edge):
self._edge = edge
self._status.modified()
@reach.setter
def reach(self, reach):
self._reach = reach
self.modified()
@property
def header(self):
@@ -191,7 +444,7 @@ class LateralContributionAdisTS(SQLSubModel):
@header.setter
def header(self, header):
self._header = header
self._status.modified()
self.modified()
@property
def pollutant(self):
@@ -199,7 +452,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):
@@ -208,7 +466,7 @@ class LateralContributionAdisTS(SQLSubModel):
@begin_rk.setter
def begin_rk(self, begin_rk):
self._begin_rk = begin_rk
self._status.modified()
self.modified()
@property
def end_rk(self):
@@ -217,4 +475,58 @@ class LateralContributionAdisTS(SQLSubModel):
@end_rk.setter
def end_rk(self, end_rk):
self._end_rk = end_rk
self._status.modified()
self.modified()
@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-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Except import NotImplementedMethodeError
from Model.LateralContributionsAdisTS.LateralContributionAdisTS \
@@ -45,8 +45,14 @@ class LateralContributionsAdisTSList(PamhyrModelList):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM lateral_contribution_adists")
execute("DELETE FROM lateral_condition_data_adists")
execute(
"DELETE FROM lateral_contribution_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM lateral_contribution_data_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
+16 -22
View File
@@ -1,5 +1,5 @@
# Edge.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -16,28 +16,18 @@
# -*- coding: utf-8 -*-
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Network.Node import Node
class Edge(object):
_id_cnt = 0
def __init__(self, id: int, name: str,
class Edge(SQLSubModel):
def __init__(self, id: int = -1, name: str = "",
node1: Node = None,
node2: Node = None,
status=None):
super(Edge, self).__init__()
self._status = status
if id == -1:
type(self)._id_cnt += 1
self.id = type(self)._id_cnt
else:
self.id = id
**kwargs):
super(Edge, self).__init__(id=id, **kwargs)
self._name = name
self.node1 = node1
self.node2 = node2
@@ -49,7 +39,7 @@ class Edge(object):
if name == "name":
ret = self.name
elif name == "id":
ret = self.id
ret = self.pamhyr_id
elif name == "node1":
ret = self.node1
elif name == "node2":
@@ -63,7 +53,7 @@ class Edge(object):
if name == "name":
self._name = value
elif name == "id":
self.id = value
self.pamhyr_id = value
elif name == "node1":
self.node1 = value
elif name == "node2":
@@ -71,7 +61,11 @@ class Edge(object):
elif name == "enable":
self._enable = value
self._status.modified()
self.modified()
@property
def id(self):
return self.pamhyr_id
@property
def name(self):
@@ -85,14 +79,14 @@ class Edge(object):
def enable(self, enable=True):
self._enable = enable
self._status.modified()
self.modified()
def disable(self):
self._enable = False
self._status.modified()
self.modified()
def reverse(self):
tmp = self.node1
self.node1 = self.node2
self.node2 = tmp
self._status.modified()
self.modified()
+40 -34
View File
@@ -1,5 +1,5 @@
# Graph.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -18,15 +18,15 @@
from functools import reduce
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Network.Node import Node
from Model.Network.Edge import Edge
class Graph(object):
def __init__(self, status=None):
super(Graph, self).__init__()
self._status = status
class Graph(SQLSubModel):
def __init__(self, **kwargs):
super(Graph, self).__init__(**kwargs)
self._node_ctor = Node
self._edge_ctor = Edge
@@ -38,13 +38,13 @@ class Graph(object):
return f"Graph {{nodes: {self._nodes}, edges: {self._edges}}}"
def nodes(self):
return self._nodes
return list(filter(lambda n: not n.is_deleted(), self._nodes))
def nodes_names(self):
return list(map(lambda n: n.name, self._nodes))
return list(map(lambda n: n.name, self.nodes()))
def edges(self):
return self._edges
return list(filter(lambda e: not e.is_deleted(), self._edges))
def enable_edges(self):
return list(
@@ -54,44 +54,44 @@ class Graph(object):
def _enable_edges(self):
"""Return a generator"""
return filter(
lambda e: e.is_enable(),
self._edges
lambda e: e.is_enable() and not e.is_deleted(),
self.edges()
)
def edges_names(self):
return list(map(lambda e: e.name, self._edges))
return list(map(lambda e: e.name, self.edges()))
def nodes_counts(self):
return len(self._nodes)
return len(self.nodes())
def edges_counts(self):
return len(self._edges)
return len(self.edges())
def enable_nodes_counts(self):
return reduce(
lambda acc, n: acc + 1 if self.is_enable_node(n) else acc,
self._nodes,
self.nodes(),
0
)
def enable_edges_counts(self):
return reduce(
lambda acc, e: acc + 1 if e.is_enable() else acc,
self._edges,
self.edges(),
0
)
def is_node_exists(self, node_name):
return reduce(
lambda acc, n: (acc or (n.name == node_name)),
self._nodes,
self.nodes(),
False
)
def is_edge_exists(self, edge_name):
return reduce(
lambda acc, e: (acc or (e.name == edge_name)),
self._edges,
self.edges(),
False
)
@@ -99,7 +99,7 @@ class Graph(object):
node = list(
filter(
lambda n: n.name == node_name,
self._nodes
self.nodes()
)
)
@@ -112,7 +112,7 @@ class Graph(object):
edge = list(
filter(
lambda e: e.name == edge_name,
self._edges
self.edges()
)
)
@@ -121,18 +121,23 @@ class Graph(object):
return edge[0]
def reach(self, reach_name: str):
return self.edge(reach_name)
def _create_node(self, x: float, y: float):
node = self._node_ctor(
-1,
"",
x=x, y=y,
id=-1, name="", x=x, y=y,
status=self._status
)
return node
def _add_node(self, node):
self._nodes.append(node)
self._status.modified()
if node in self._nodes:
node.set_as_not_deleted()
else:
self._nodes.append(node)
self.modified()
return node
def add_node(self, x: float = 0.0, y: float = 0.0):
@@ -143,9 +148,9 @@ class Graph(object):
return self._add_node(node)
def remove_node(self, node):
self._nodes.remove(node)
node.set_as_deleted()
self._remove_associated_edge(node)
self._status.modified()
self.modified()
def _remove_associated_edge(self, node: str):
edges = list(
@@ -164,8 +169,7 @@ class Graph(object):
def _create_edge(self, n1: Node, n2: Node):
edge = self._edge_ctor(
-1,
"", n1, n2,
node1=n1, node2=n2,
status=self._status
)
return edge
@@ -176,10 +180,12 @@ class Graph(object):
# e.node2 == edge.node2),
# self._edges)):
# return None
if edge in self._edges:
edge.set_as_not_deleted()
else:
self._edges.append(edge)
self._edges.append(edge)
self._status.modified()
self.modified()
return edge
def add_edge(self, n1: Node, n2: Node):
@@ -193,8 +199,8 @@ class Graph(object):
return self._create_edge(n1, n2)
def remove_edge(self, edge):
self._edges.remove(edge)
self._status.modified()
edge.set_as_deleted()
self.modified()
def is_upstream_node(self, node):
return reduce(
+10 -19
View File
@@ -1,5 +1,5 @@
# Node.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -16,24 +16,15 @@
# -*- coding: utf-8 -*-
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Network.Point import Point
class Node(object):
_id_cnt = 0
def __init__(self, id: int, name: str,
class Node(SQLSubModel):
def __init__(self, id: int = -1, name: str = "",
x: float = 0.0, y: float = 0.0,
status=None):
super(Node, self).__init__()
self._status = status
if id == -1:
type(self)._id_cnt += 1
self.id = type(self)._id_cnt
else:
self.id = id
**kwargs):
super(Node, self).__init__(id=id, **kwargs)
self._name = name
self.pos = Point(x, y)
@@ -44,7 +35,7 @@ class Node(object):
if key == "name":
ret = self._name
elif key == "id":
ret = self.id
ret = self.pamhyr_id
elif key == "pos":
ret = f"({self.pos.x},{self.pos.y})"
@@ -54,9 +45,9 @@ class Node(object):
if key == "name":
self._name = value
elif key == "id":
self.id = value
self.pamhyr_id = value
self._status.modified()
self.modified()
@property
def name(self):
@@ -76,4 +67,4 @@ class Node(object):
def setPos(self, x, y):
self.pos.x = x
self.pos.y = y
self._status.modified()
self.modified()
+1 -1
View File
@@ -1,5 +1,5 @@
# Point.py -- Pamhyr
# Copyright (C) 2023-2024 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-2024 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 @@
# test_Network.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -20,7 +20,7 @@ import os
import unittest
import tempfile
from Model.Saved import SavedStatus
from Model.Status import StudyStatus
from Model.Network.Graph import Graph
from Model.Network.Edge import Edge
@@ -28,7 +28,7 @@ from Model.Network.Node import Node
def new_graph():
status = SavedStatus()
status = StudyStatus()
g = Graph(status=status)
return g
@@ -60,6 +60,9 @@ class GraphTestCase(unittest.TestCase):
e0 = g.add_edge(n0, n1)
self.assertEqual(g.nodes_counts(), 2)
self.assertEqual(g.edges_counts(), 1)
g.remove_edge(e0)
self.assertEqual(g.nodes_counts(), 2)
+96 -47
View File
@@ -1,5 +1,5 @@
# OutputRKAdists.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -27,33 +27,27 @@ from tools import (
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class OutputRKAdists(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, reach=None,
rk=None, title: str = "", status=None):
super(OutputRKAdists, self).__init__()
self._status = status
if id == -1:
self.id = OutputRKAdists._id_cnt
else:
self.id = id
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
OutputRKAdists._id_cnt = max(
OutputRKAdists._id_cnt + 1, self.id)
@property
def reach(self):
return self._reach
@@ -82,70 +76,125 @@ class OutputRKAdists(SQLSubModel):
self._status.modified()
@classmethod
def _db_create(cls, execute):
sql = (
"CREATE TABLE OutputRKAdists(" +
"id INTEGER NOT NULL PRIMARY KEY, " +
"reach INTEGER NOT NULL, " +
"rk REAL NOT NULL, " +
"title TEXT NOT NULL, " +
"FOREIGN KEY(reach) REFERENCES river_reach(id)" +
")"
)
execute(sql)
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):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "OutputRKAdists"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "OutputRKAdists"
table_new = "output_rk_adists"
reachs = data['id2pid']['river_reach']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table_new}_tmp " +
"(pamhyr_id, reach, rk, title, scenario) " +
"SELECT pamhyr_id, reach, rk, title, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
cls._db_update_to_0_2_0_set_reach_pid(execute, table_new, reachs)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
# reach = data["reach"]
# profile = data["profile"]
status = data["status"]
if scenario is None:
return new
table = execute(
"SELECT id, reach, rk, title " +
f"FROM OutputRKAdists"
"SELECT pamhyr_id, deleted, reach, rk, title, scenario " +
f"FROM output_rk_adists"
)
if table is not None:
for row in table:
id = row[0]
id_reach = row[1]
id_rk = row[2]
title = row[3]
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=id, reach=id_reach,
id=pid, reach=id_reach,
rk=id_rk, title=title,
status=status
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(f"DELETE FROM OutputRKAdists WHERE id = {self.id}")
sql = (
"INSERT INTO " +
"OutputRKAdists(id, reach, rk, title) " +
"VALUES (" +
f"{self.id}, {self._reach}, {self._rk}, " +
f"'{self._db_format(self._title)}'" +
")"
execute(
"DELETE FROM output_rk_adists " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(sql)
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
@@ -1,5 +1,5 @@
# OutputRKListAdists.py -- Pamhyr
# Copyright (C) 2024 INRAE
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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.OutputRKAdists.OutputRKAdists import OutputRKAdists
@@ -38,7 +38,10 @@ class OutputRKAdistsList(PamhyrModelList):
ok = True
# Delete previous data
execute("DELETE FROM OutputRKAdists")
execute(
"DELETE FROM output_rk_adists " +
f"WHERE scenario = {self._status.scenario_id}"
)
for sl in self._lst:
ok &= sl._db_save(execute, data)
+423 -80
View File
@@ -1,5 +1,5 @@
# Pollutants.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -27,60 +27,96 @@ 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()
class Pollutants(SQLSubModel):
class PollutantCharacteristics(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "", status=None):
super(Pollutants, self).__init__()
def __init__(self, id: int = -1, type=-1,
diametre=-1, rho=-1,
porosity=-1,
cdc_riv=-1, cdc_cas=-1,
apd=-1, ac=-1, bc=-1,
pollutant=None,
status=None,
owner_scenario=-1):
super(PollutantCharacteristics, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if id == -1:
self.id = Pollutants._id_cnt
else:
self.id = id
self._type = type
self._diametre = diametre
self._rho = rho
self._porosity = porosity
self._cdc_riv = cdc_riv
self._cdc_cas = cdc_cas
self._apd = apd
self._ac = ac
self._bc = bc
self._pollutant = pollutant
if name is None or name == "":
self.name = f"pol{self.id}"
else:
self._name = str(name)
self._enabled = True
# FIXME: This four next method is dirty work, just an hack to keep
# the View API working... Must be refactoring
def to_list(self):
return [
self._type,
self._diametre,
self._rho,
self._porosity,
self._cdc_riv,
self._cdc_cas,
self._apd,
self._ac,
self._bc,
]
self._data = []
def __len__(self):
return len(self.to_list())
Pollutants._id_cnt = max(
Pollutants._id_cnt + 1, self.id)
def __getitem__(self, key):
return self.to_list()[key]
@property
def name(self):
return self._name
def __setitem__(self, key, value):
if key == 0:
self._type = value
elif key == 1:
self._diametre = value
elif key == 2:
self._rho = value
elif key == 3:
self._porosity = value
elif key == 4:
self._cdc_riv = value
elif key == 5:
self._cdc_cas = value
elif key == 6:
self._apd = value
elif key == 7:
self._ac = value
elif key == 8:
self._bc = value
self.propagate_modified()
@name.setter
def name(self, name):
self._name = name
self._status.modified()
@property
def data(self):
return self._data.copy()
def propagate_modified(self):
self.modified()
if hasattr(self, "_pollutant") and self._pollutant:
self._pollutant.modified()
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE Pollutants(
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
)
""")
execute("""
CREATE TABLE Pollutants_characteristics(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE pollutants_characteristics{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
type INTEGER NOT NULL,
diametre REAL NOT NULL,
rho REAL NOT NULL,
@@ -91,83 +127,391 @@ class Pollutants(SQLSubModel):
ac REAL NOT NULL,
bc REAL NOT NULL,
pollutant INTEGER NOT NULL,
FOREIGN KEY(pollutant) REFERENCES Pollutants(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(pollutant) REFERENCES pollutants(pamhyr_id)
)
""")
return cls._create_submodel(execute)
return True
@classmethod
def _db_update(cls, execute, version):
return True
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
if cls.is_table_exists(execute, "Pollutants_characteristics"):
cls._db_update_to_0_2_0(execute, data)
else:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "Pollutants_characteristics"
table_new = "pollutants_characteristics"
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, type, diametre, rho, porosity, " +
"cdc_riv, cdc_cas, apd, ac, bc, pollutant, scenario) " +
"SELECT pamhyr_id, type, diametre, rho, porosity, " +
"cdc_riv, cdc_cas, apd, ac, bc, pollutant, 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_pollutants_pid(execute, data)
@classmethod
def _db_update_to_0_2_0_set_pollutants_pid(cls, execute, data):
pid_pol = data["id2pid"]["Pollutants"]
els = execute(
f"SELECT pamhyr_id, pollutant " +
"FROM pollutants_characteristics"
)
for row in els:
it = iter(row)
pid = next(it)
pol_id = next(it)
if pol_id == -1:
continue
execute(
f"UPDATE pollutants_characteristics " +
f"SET pollutant = {pid_pol[pol_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data["status"]
scenario = data["scenario"]
loaded = data['loaded_pid']
pollutant = data['pollutant']
if scenario is None:
return new
table = execute(
"SELECT id, name " +
f"FROM Pollutants"
"SELECT pamhyr_id, deleted, type, diametre, rho, porosity, " +
"cdc_riv, cdc_cas, apd, ac, bc, scenario " +
"FROM pollutants_characteristics " +
f"WHERE pollutant = {pollutant.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:
id = row[0]
name = row[1]
it = iter(row)
new_pollutant = cls(
id=id, name=name,
status=status
pid = next(it)
deleted = (next(it) == 1)
type = next(it)
diametre = next(it)
rho = next(it)
porosity = next(it)
cdc_riv = next(it)
cdc_cas = next(it)
apd = next(it)
ac = next(it)
bc = next(it)
owner_scenario = next(it)
pc = cls(
id=pid, type=type,
diametre=diametre,
rho=rho,
porosity=porosity,
cdc_riv=cdc_riv, cdc_cas=cdc_cas,
apd=apd, ac=ac, bc=bc,
pollutant=pollutant,
status=status,
owner_scenario=owner_scenario
)
if deleted:
pc.set_as_deleted()
new_data = []
table = execute(
"SELECT * " +
"FROM Pollutants_characteristics " +
f"WHERE pollutant = {id}"
)
loaded.add(pid)
new.append(pc)
if table is not None:
for t in table:
new_data = t[1:-1]
new_pollutant._data.append(new_data)
new.append(new_pollutant)
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(f"DELETE FROM Pollutants WHERE id = {self.id}")
execute(f"DELETE FROM Pollutants_characteristics" +
f" WHERE pollutant = {self.id}")
execute(
"DELETE FROM pollutants_characteristics " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
sql = (
execute(
"INSERT INTO " +
"Pollutants(id, name) " +
"pollutants_characteristics(pamhyr_id, deleted, " +
"type, diametre, rho, porosity, " +
"cdc_riv, cdc_cas, apd, ac, bc, " +
"pollutant, scenario) " +
"VALUES (" +
f"{self.id}, " +
f"'{self._db_format(self._name)}'" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"{self._type}, " +
f"{self._diametre}, {self._rho}, " +
f"{self._porosity}, " +
f"{self._cdc_riv}, {self._cdc_cas}, " +
f"{self._apd}, {self._ac}, {self._bc}, " +
f"{self._pollutant.id}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
for d in self._data:
sql = (
"INSERT INTO " +
"Pollutants_characteristics(type, diametre, rho, porosity, " +
"cdc_riv, cdc_cas, apd, ac, bc, pollutant) " +
f"VALUES ({d[0]}, {d[1]}, {d[2]},{d[3]}, {d[4]}, "
f"{d[5]}, {d[6]}, {d[7]}, {d[8]}, {self.id})"
class Pollutants(SQLSubModel):
_sub_classes = [
PollutantCharacteristics,
# BoundaryConditionAdisTS
]
def __init__(self, id: int = -1, name: str = "",
status=None, owner_scenario=-1):
super(Pollutants, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
if name is None or name == "":
self.name = f"pol{self.id}"
else:
self._name = str(name)
self._enabled = True
self._data = []
self._boundary_conditions_adists = []
@property
def boundary_conditions_adists(self):
return self._boundary_conditions_adists
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
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
)
execute(sql)
]
@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()}
)
""")
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:
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 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)
return True
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "Pollutants"
table_new = "pollutants"
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, name, scenario) " +
"SELECT pamhyr_id, name, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table_new}_tmp RENAME TO {table_new}")
@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, enabled, name, scenario " +
"FROM pollutants " +
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)
enabled = (next(it) == 1)
name = next(it)
owner_scenario = next(it)
new_pollutant = cls(
id=pid, name=name,
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=data
)
new_pollutant._boundary_conditions_adists = (
BoundaryConditionAdisTS._db_load(
execute,
data=data
)
)
loaded.add(pid)
new.append(new_pollutant)
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
ok = True
execute(
"DELETE FROM pollutants " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM pollutants_characteristics " +
f"WHERE pollutant = {self.id} " +
f"AND scenario = {self._status.scenario_id}"
)
execute(
"INSERT INTO " +
"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}" +
")"
)
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,
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)
@property
def enabled(self):
return self._enabled
@@ -181,7 +525,6 @@ class Pollutants(SQLSubModel):
return len(self._data) != 0
def new_from_data(self, data):
try:
new = [int(data[0])]
new += [float(d) for d in data[1:]]
+6 -3
View File
@@ -1,5 +1,5 @@
# PollutantsList.py -- Pamhyr
# Copyright (C) 2024 INRAE
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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
@@ -38,7 +38,10 @@ class PollutantsList(PamhyrModelList):
ok = True
# Delete previous data
execute("DELETE FROM Pollutants")
execute(
"DELETE FROM Pollutants " +
f"WHERE scenario = {self._status.scenario_id}"
)
for sl in self._lst:
ok &= sl._db_save(execute, data)
+78 -29
View File
@@ -1,5 +1,5 @@
# REPLine.py -- Pamhyr
# Copyright (C) 2024 INRAE
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,31 +22,25 @@ from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
class REPLine(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, enabled=True,
name="", line="", solvers=set(),
status=None):
super(REPLine, self).__init__()
if id == -1:
self.id = REPLine._id_cnt
else:
self.id = id
self._status = status
status=None, owner_scenario=-1):
super(REPLine, self).__init__(
id, status=status,
owner_scenario=owner_scenario
)
self._enabled = enabled
self._name = f"Line{self.id}" if name == "" else name
self._name = f"Line #{self._pamhyr_id}" if name == "" else name
self._line = line
self._solvers = solvers
REPLine._id_cnt = max(id, REPLine._id_cnt+1)
def __getitem__(self, key):
value = None
@@ -113,35 +107,75 @@ class REPLine(SQLSubModel):
self._status.modified()
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE rep_lines(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE rep_lines{ext} (
{cls.create_db_add_pamhyr_id()},
enabled BOOLEAN NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
line TEXT NOT NULL,
solvers TEXT NOT NULL
solvers TEXT NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 9:
cls._db_create(execute)
return True
else:
cls._db_update_to_0_0_14(execute, data)
if major == "0" and int(minor) <= 1:
if int(release) < 2:
execute(
"ALTER TABLE rep_lines " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return True
@classmethod
def _db_update_to_0_0_14(cls, execute, data):
table = "rep_lines"
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}_tmp " +
"(pamhyr_id, enabled, name, line, solvers, scenario) " +
"SELECT pamhyr_id, enabled, name, line, solvers, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT id, enabled, name, line, solvers " +
"FROM rep_lines"
"SELECT pamhyr_id, enabled, deleted, name, line, " +
"solvers, scenario " +
"FROM rep_lines " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
@@ -149,32 +183,47 @@ class REPLine(SQLSubModel):
id = next(it)
enabled = (next(it) == 1)
deleted = (next(it) == 1)
name = next(it)
line = next(it)
solvers = set(next(it).split(";;"))
owner_scenario = next(it)
f = cls(
id=id, enabled=enabled, name=name, line=line,
solvers=solvers, status=data['status']
id=id, enabled=enabled,
name=name, line=line, solvers=solvers,
status=data['status'], owner_scenario=owner_scenario
)
if deleted:
f.set_as_deleted()
loaded.add(id)
new.append(f)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
solvers = ";;".join(self._solvers)
sql = (
execute(
"INSERT INTO " +
"rep_lines(id, enabled, name, line, solvers) " +
"rep_lines(pamhyr_id, enabled, deleted, " +
"name, line, solvers, scenario) " +
"VALUES (" +
f"{self.id}, {self._enabled}, " +
f"{self._pamhyr_id}, {self._enabled}, " +
f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._line)}', " +
f"'{self._db_format(solvers)}'" +
f"'{self._db_format(solvers)}', " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
+6 -3
View File
@@ -1,5 +1,5 @@
# REPLineList.py -- Pamhyr
# Copyright (C) 2024 INRAE
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -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.REPLine.REPLine import REPLine
@@ -38,7 +38,10 @@ class REPLineList(PamhyrModelList):
ok = True
# Delete previous data
execute("DELETE FROM rep_lines")
execute(
"DELETE FROM rep_lines " +
f"WHERE scenario = {self._status.scenario_id}"
)
for af in self._lst:
ok &= af._db_save(execute, data)
+343 -87
View File
@@ -1,5 +1,5 @@
# Reservoir.py -- Pamhyr
# Copyright (C) 2023-2024 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,148 +19,384 @@
import logging
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
logger = logging.getLogger()
class Reservoir(SQLSubModel):
class Data(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self, id: int = -1, name: str = "",
status=None):
super(Reservoir, self).__init__()
def __init__(self, id: int = -1,
elevation: float = 0.,
surface: float = 0.,
status=None, owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._data = [elevation, surface]
if id == -1:
self.id = Reservoir._id_cnt
else:
self.id = id
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE reservoir_data{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
elevation REAL NOT NULL,
surface REAL NOT NULL,
reservoir INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reservoir) REFERENCES reservoir(pamhyr_id),
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 == minor == "0":
if int(release) > 5:
cls._db_update_to_0_2_0(execute, data)
if major == "0" and int(minor) <= 1:
if int(release) < 2:
execute(
"ALTER TABLE reservoir_data " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "reservoir_data"
id2pid = data['id2pid']
cls.update_db_add_pamhyr_id(
execute, table,
data=data
)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, ind, elevation, surface, reservoir, scenario) " +
"SELECT pamhyr_id, ind, elevation, surface, reservoir, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
if 'reservoir' in id2pid:
reservoirs = id2pid['reservoir']
cls._db_update_to_0_2_0_set_reservoir_pid(
execute, table, reservoirs
)
@classmethod
def _db_update_to_0_2_0_set_reservoir_pid(cls, execute, table, reservoirs):
bcs = execute(
f"SELECT pamhyr_id, reservoir FROM {table}"
)
for row in bcs:
it = iter(row)
pid = next(it)
reservoir_id = next(it)
execute(
f"UPDATE {table} " +
f"SET reservoir = {reservoirs[reservoir_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
if scenario is None:
return new
loaded = data['loaded_pid']
reseroire = data["reservoir"]
values = execute(
"SELECT pamhyr_id, deleted, ind, " +
"elevation, surface, scenario " +
"FROM reservoir_data " +
f"WHERE reservoir = {reseroire.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) == 1)
ind = next(it)
elevation = next(it)
surface = next(it)
owner_scenario = next(it)
nd = cls(
id=pid, status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
nd.set_as_deleted()
nd._data = [elevation, surface]
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):
if not self.must_be_saved():
return True
pid = self._pamhyr_id
ind = data["ind"]
elevation = self._db_format(str(self[0]))
surface = self._db_format(str(self[1]))
reservoir = data["reservoir"]
execute(
"INSERT INTO reservoir_data " +
"(pamhyr_id, deleted, ind, " +
"elevation, surface, reservoir, scenario) " +
"VALUES (" +
f"{pid}, " +
f"{self._db_format(self.is_deleted())}, " +
f"{ind}, '{elevation}', {surface}, " +
f"{reservoir._pamhyr_id}, " +
f"{self._status.scenario_id}" +
")"
)
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = float(value)
class Reservoir(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1,
name: str = "",
status=None, owner_scenario=-1):
super(Reservoir, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._node = None
self._data = []
Reservoir._id_cnt = max(Reservoir._id_cnt + 1, self.id)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE reservoir(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE reservoir{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
node INTEGER,
FOREIGN KEY(node) REFERENCES river_node(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
execute("""
CREATE TABLE reservoir_data(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
ind INTEGER NOT NULL,
elevation REAL NOT NULL,
surface REAL NOT NULL,
reservoir INTEGER,
FOREIGN KEY(reservoir) REFERENCES reservoir(id)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 5:
cls._db_create(execute)
return True
return True
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE additional_files " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "reservoir"
nodes = data['id2pid']['river_node']
cls.update_db_add_pamhyr_id(execute, table, data=data)
cls._db_update_to_0_2_0_set_node_pid(execute, table, nodes)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
execute(
f"INSERT INTO {table}_tmp " +
"(pamhyr_id, name, node, scenario) " +
"SELECT pamhyr_id, name, node, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT id, name, node " +
"FROM reservoir "
"SELECT pamhyr_id, deleted, name, node, scenario " +
"FROM reservoir " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
id = row[0]
name = row[1]
node_id = row[2]
new_reservoir = cls(id, name, status=data["status"])
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
node_id = next(it)
owner_scenario = next(it)
new_reservoir = cls(
id, name,
status=data["status"],
owner_scenario=owner_scenario
)
if deleted:
new_reservoir.set_as_deleted()
new_reservoir._node = None
if node_id != -1:
if node_id != -1 and node_id is not None:
new_reservoir._node = next(
filter(
lambda n: n.id == node_id, data["nodes"]
)
), None
)
new_data = []
table = execute(
"SELECT elevation, surface " +
"FROM reservoir_data " +
f"WHERE reservoir = {id} " +
"ORDER BY ind ASC"
data["reservoir"] = new_reservoir
new_reservoir._data = Data._db_load(
execute, data=data
)
for t in table:
new_data.append((t[0], t[1]))
new_reservoir._data = new_data
loaded.add(id)
new.append(new_reservoir)
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(f"DELETE FROM reservoir WHERE id = {self.id}")
execute(f"DELETE FROM reservoir_data WHERE reservoir = {self.id}")
execute(
"DELETE FROM reservoir " +
f"WHERE pamhyr_id = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM reservoir_data " +
f"WHERE reservoir = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
node_id = -1
node_id = 'NULL'
if self._node is not None:
node_id = self._node.id
node_id = self._node.pamhyr_id
sql = (
"INSERT INTO " +
"reservoir(id, name, node) " +
"reservoir(pamhyr_id, deleted, name, node, scenario) " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"{node_id}" +
f"{self.pamhyr_id}, " +
f"{self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"{node_id}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
data["reservoir"] = self
ind = 0
for d in self._data:
sql = (
"INSERT INTO " +
"reservoir_data(ind, elevation, surface, reservoir) " +
f"VALUES ({ind}, '{d[0]}', {d[1]}, {self.id})"
)
execute(sql)
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(self._data)
@property
def name(self):
if self._name == "":
return f"R{self.id + 1}"
return f"Reservoir #{self.pamhyr_id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
self.modified()
@property
def node(self):
@@ -169,7 +405,7 @@ class Reservoir(SQLSubModel):
@node.setter
def node(self, node):
self._node = node
self._status.modified()
self.modified()
def has_node(self):
return self._node is not None
@@ -202,42 +438,63 @@ class Reservoir(SQLSubModel):
return (new_0, new_1)
def add(self, index: int):
value = (self._default_elevation, self._default_surface)
self._data.insert(index, value)
self._status.modified()
return value
data = Data(
elevation=self._default_elevation,
surface=self._default_surface,
status=self._status,
)
self._data.insert(index, data)
self.modified()
return data
def insert(self, index: int, value):
self._data.insert(index, value)
self._status.modified()
if type(value) is tuple or type(value) is list:
value = Data(
elevation=value[0],
surface=value[1]
)
def delete_i(self, indexes):
self._data.insert(index, value)
self.modified()
def hard_delete_i(self, indexes):
self._data = list(
map(
lambda e: e[1],
filter(
lambda e: e[0] not in indexes,
enumerate(self.data)
enumerate(self._data)
)
)
)
self._status.modified()
self.modified()
def delete(self, els):
self._data = list(
filter(
lambda e: e not in els,
self.data
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self.lst)
)
)
)
self._status.modified()
self.modified()
def delete(self, els):
for el in els:
el.set_as_deleted()
self.modified()
def sort(self, _reverse=False, key=None):
if key is None:
self._data.sort(reverse=_reverse)
self._data.sort(reverse=_reverse, key=lambda d: d[0])
else:
self._data.sort(reverse=_reverse, key=key)
self._status.modified()
self.modified()
def get_i(self, index):
return self.data[index]
@@ -246,13 +503,12 @@ class Reservoir(SQLSubModel):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index, column, value):
v = list(self._data[index])
v[column] = value
self._data[index] = tuple(v)
self._status.modified()
self._data[index][column] = value
self.modified()
def set_i_elevation(self, index: int, value):
self._set_i_c_v(index, 0, value)
@@ -265,11 +521,11 @@ class Reservoir(SQLSubModel):
next = index - 1
d = self._data
d[index], d[next] = d[next], d[index]
self._status.modified()
self.modified()
def move_down(self, index):
if index >= 0:
prev = index + 1
d = self._data
d[index], d[prev] = d[prev], d[index]
self._status.modified()
self.modified()
+10 -4
View File
@@ -1,5 +1,5 @@
# ReservoirList.py -- Pamhyr
# Copyright (C) 2023-2024 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,7 +19,7 @@
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Reservoir.Reservoir import Reservoir
@@ -39,8 +39,14 @@ class ReservoirList(PamhyrModelList):
return new
def _db_save(self, execute, data=None):
execute("DELETE FROM reservoir")
execute("DELETE FROM reservoir_data")
execute(
"DELETE FROM reservoir " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM reservoir_data " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
+599 -6
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr
# Copyright (C) 2023-2024 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,32 +14,361 @@
# 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 struct
import logging
import itertools
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
logger = logging.getLogger()
class Results(object):
def __init__(self, study=None, solver=None,
repertory="", name="0"):
class AdditionalData(SQLSubModel):
_sub_classes = []
def __init__(self, id=-1, study=None, data=None):
super(AdditionalData, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
)
self._study = study
self._data = data
@property
def data(self):
return self._data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results_add_data{ext} (
{cls.create_db_add_pamhyr_id()},
result INTEGER NOT NULL,
type_x TEXT NOT NULL,
type_y TEXT NOT NULL,
legend TEXT NOT NULL,
unit TEXT NOT NULL,
data_len INTEGER NOT NULL,
x BLOB NOT NULL,
y BLOB NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(result) REFERENCES results(pamhyr_id),
PRIMARY KEY(pamhyr_id, result, scenario)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) == 2 and int(release) <= 1:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
study = data['study']
status = data['status']
scenario = data["scenario"]
table = execute(
"SELECT pamhyr_id, type_x, type_y, " +
"legend, unit, data_len, x, y, " +
"scenario " +
"FROM results_add_data " +
f"WHERE scenario = {scenario.id}"
)
if table is None:
return new
for v in table:
it = iter(v)
pid = next(it)
type_x = next(it)
type_y = next(it)
legend = next(it)
unit = next(it)
data_len = next(it)
bx = next(it)
by = next(it)
owner_scenario = next(it)
data_format = ">" + ''.join(itertools.repeat("d", data_len))
x = struct.unpack(data_format, bx)
y = struct.unpack(data_format, by)
data = {
'type_x': type_x,
'type_y': type_y,
'legend': legend,
'unit': unit,
'x': x, 'y': y
}
new_data = cls(study=study)
new_data._data = data
new.append(new_data)
return new
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
return
pid = self._pamhyr_id
data_len = len(self._data["x"])
data_format = ">" + ''.join(itertools.repeat("d", data_len))
bx = struct.pack(data_format, *self._data["x"])
by = struct.pack(data_format, *self._data["y"])
execute(
"INSERT INTO " +
"results_add_data (pamhyr_id, result, " +
"type_x, type_y, " +
"legend, unit, data_len, x, y, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, data["result"],
self._data["type_x"], self._data["type_y"],
self._data["legend"], self._data["unit"],
data_len, bx, by, self._owner_scenario
)
return True
class TableData(SQLSubModel):
_sub_classes = []
def __init__(self, id=-1, study=None,
legend="", data=None,
owner_scenario=-1):
super(TableData, self).__init__(
id=id, status=study.status,
owner_scenario=owner_scenario
)
self._study = study
self._legend = legend
self._data = data
@property
def data(self):
return self._data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results_table_data{ext} (
{cls.create_db_add_pamhyr_id()},
result INTEGER NOT NULL,
legend TEXT NOT NULL,
unit TEXT NOT NULL,
data_shape_x INTEGER NOT NULL,
data_shape_y INTEGER NOT NULL,
data_shape_z INTEGER DEFAULT -1,
data BLOB NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(result) REFERENCES results(pamhyr_id),
PRIMARY KEY(pamhyr_id, result, legend, scenario)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
create = False
if major == "0" and int(minor) == 2:
if int(release) < 7 and not create:
cls._db_create(execute)
create = True
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
res = {}
study = data['study']
status = data['status']
scenario = data["scenario"]
result = data["result"]
rid = data["result_pid"]
table = execute(
"SELECT pamhyr_id, " +
"legend, unit, " +
"data_shape_x, data_shape_y, data_shape_z, " +
"data, " +
"scenario " +
"FROM results_table_data " +
f"WHERE scenario = {scenario.id} " +
f"AND result = {rid}"
)
for v in table:
it = iter(v)
pid = next(it)
legend = next(it)
unit = next(it)
data_shape_x = next(it)
data_shape_y = next(it)
data_shape_z = next(it)
data = next(it)
owner_scenario = next(it)
dtype = cls._unit_to_dtype(unit)
np_data = np.frombuffer(data, dtype=dtype)
if data_shape_y == -1:
np_data = np.reshape(np_data, data_shape_x)
elif data_shape_z == -1:
np_data = np.reshape(np_data, (data_shape_x,
data_shape_y))
else:
np_data = np.reshape(np_data, (data_shape_x,
data_shape_y,
data_shape_z))
new_table = cls(study=study,
legend=legend, data=np_data,
owner_scenario=owner_scenario)
res[legend] = new_table
return res
def _db_save(self, execute, data=None):
if self._status.scenario.id != self._owner_scenario:
return
shape_x = -1
shape_y = -1
shape_z = -1
shape = self._data.shape
if len(shape) == 2:
shape_x, shape_y = shape
elif len(shape) == 1:
shape_x, = shape
elif len(shape) == 3:
shape_x, shape_y, shape_z = shape
else:
logger.error(
f"Failed to parse numpy shape: {shape}"
)
execute(
"INSERT INTO " +
"results_table_data (pamhyr_id, result, " +
"legend, unit, " +
"data_shape_x, data_shape_y, data_shape_z, " +
"data, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, data["result"],
self._legend, self._dtype_to_unit(self._data.dtype),
shape_x, shape_y, shape_z,
self._data,
self._owner_scenario
)
return True
@classmethod
def _unit_to_dtype(cls, unit):
t = {
"i32": np.int32, "i64": np.int64,
"u32": np.uint32, "u64": np.uint64,
"f32": np.float32, "f64": np.float64,
}
return t[unit]
def _dtype_to_unit(self, dtype):
t = {
np.dtype(np.int32): "i32", np.dtype(np.int64): "i64",
np.dtype(np.uint32): "u32", np.dtype(np.uint64): "u64",
np.dtype(np.float32): "f32", np.dtype(np.float64): "f64",
}
return t[dtype]
def table(self):
return self._data
def __len__(self):
return len(self._data)
def __getitem__(self, key):
if isinstance(key, tuple):
return self._data[key[0], key[1]]
self._data[key]
class Results(SQLSubModel):
_sub_classes = [River, TableData, AdditionalData]
def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0", is_ensemble=False):
super(Results, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
)
self._name = name
self._study = study
self._river = River(self._study, self)
self._solver = solver
self._repertory = repertory
self._name = name
self._river = River(self._study)
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"]
@@ -53,6 +382,7 @@ class Results(object):
def study(self):
return self._study
@timer
def set(self, key, value):
self._meta_data[key] = value
@@ -60,12 +390,275 @@ class Results(object):
def is_valid(self):
return ("timestamps" in self._meta_data)
@property
def solver_name(self):
if self._solver is None:
return self._meta_data["solver_name"]
return self._solver.name
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,
self._repertory,
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)))
return struct.pack(sf, ts)
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results{ext} (
{cls.create_db_add_pamhyr_id()},
solver_name TEXT NOT NULL,
solver_type TEXT NOT NULL,
study_revision INTEGER 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)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
create = False
if major == "0" and int(minor) < 2:
cls._db_create(execute)
create = True
if major == "0" and int(minor) == 2:
if int(release) < 1 and not create:
cls._db_create(execute)
create = True
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
def _db_load(cls, execute, data=None):
new = None
study = data['study']
status = data['status']
scenario = data["scenario"]
table = execute(
"SELECT pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_date, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " +
"scenario " +
"FROM results " +
f"WHERE scenario = {scenario.id}"
)
if table is None:
yield
if len(table) > 1:
logger.warning("Multiple results for this scenario")
for v in table:
it = iter(v)
pid = next(it)
solver_name = next(it)
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)
new_results = cls(study=study)
new_results.set("solver_name", solver_name)
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(
"additional_data",
AdditionalData._db_load(execute, data)
)
# Update global index
i = 0
for reach in new_results._river.reachs:
nb = len(reach.profiles)
reach.set_global_index(range(i, i + nb + 1))
i += nb
if "Z" in table_data:
for ts in data["timestamps"]:
new_results._river.update_water_limits(
ts,
table_data["Z"][new_results.get_timestamp_id(ts), :]
)
yield (solver_type, new_results)
def _db_save_clear(self, execute, solver_type, data=None):
old_pid = execute(
"SELECT pamhyr_id FROM results " +
f"WHERE scenario = {self._owner_scenario} " +
f"AND solver_type = '{solver_type}'"
)
if len(old_pid) != 0:
for pid in old_pid:
pid = pid[0]
execute(
"DELETE FROM results " +
f"WHERE scenario = {self._owner_scenario} " +
f"AND solver_type = '{solver_type}'"
)
execute(
"DELETE FROM results_data " +
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 True
if not self._is_valid():
return False
pid = self._pamhyr_id
if self._solver is None:
solver_name = self.get("solver_name")
solver_type = self.get("solver_type")
else:
solver_name = self._solver._name
solver_type = self._solver._type
self._db_save_clear(execute, solver_type, data=data)
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_date, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, solver_name, solver_type,
self._status.version, self.get("creation_date"),
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
)
+9 -2
View File
@@ -1,5 +1,5 @@
# Results.py -- Pamhyr
# Copyright (C) 2023-2024 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
@@ -50,6 +51,9 @@ class Results(object):
self._phys_var_list = ["C", "G", "M", "D", "L", "N", "R"]
if solver is not None:
self.set("solver_type", solver._type)
@property
def date(self):
date = self._meta_data["creation_date"]
@@ -87,3 +91,6 @@ class Results(object):
self._repertory,
qlog=None,
)
def _db_save(self, execute, data=None):
return False
+353 -16
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr
# Copyright (C) 2023-2024 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,18 +14,34 @@
# 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, timer
from functools import reduce
from datetime import datetime
from Model.Scenario import Scenario
from Model.Tools.PamhyrDB import SQLSubModel
logger = logging.getLogger()
class Profile(object):
def __init__(self, profile, study):
class Profile(SQLSubModel):
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)
@@ -42,41 +58,252 @@ class Profile(object):
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):
return list(
map(lambda ts: self._data[ts][key], self._data)
res = list(
map(lambda ts: self._data[ts][key],
sorted(self._data.keys()))
)
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):
return any(map(lambda ts: "sl" in self._data[ts], self._data))
class Reach(object):
def __init__(self, reach, study):
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE results_data{ext}(
{cls.create_db_add_pamhyr_id()},
result INTEGER NOT NULL,
key TEXT NOT NULL,
reach INTEGER NOT NULL,
section INTEGER NOT NULL,
len_data INTEGER NOT NULL,
data BLOB NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(result) REFERENCES results(pamhyr_id),
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
FOREIGN KEY(section)
REFERENCES geometry_profileXYZ(pamhyr_id),
PRIMARY KEY(pamhyr_id, result, key, scenario)
)
""")
return True
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
status = data['status']
parent = data['parent']
study = data['study']
reach = data['reach']
profile = data['profile']
scenario = data["scenario"]
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 reach = {reach.pamhyr_id} " +
f"AND section = {profile.pamhyr_id}"
)
for v in values:
it = iter(v)
pid = next(it)
result = next(it)
key = next(it)
len_data = next(it)
data = next(it)
owner_scenario = next(it)
if key in ["Z", "Q", "V"]:
sf = ">" + ''.join(itertools.repeat("f", len_data))
len_values = len(values)
values = struct.unpack(sf, data)
elif key in ["sl"]:
sf = ">" + ''.join(itertools.repeat("f", len_data))
values = struct.unpack(sf, data)
values = cls._db_load_data_sl_format(
values, len_data, timestamps
)
for timestamp, value in zip(timestamps, values):
new.set(timestamp, key, value)
if key == "Z":
new.update_water_limits(timestamp, value)
return new
@classmethod
def _db_load_data_sl_format(cls, values, len_data, timestamps):
tuple_size = 3
# HACK: Transforme list of value to list of n-tuple
# sediment results: (h, d50, sigma)
tmp_values = [[]]
for value in values:
t = tmp_values[-1]
if len(t) < tuple_size:
t.append(value)
else:
tmp_values.append([value])
tmp_values = list(map(tuple, tmp_values))
# HACK: Transforme list of n-tuple to list of n-list
# of n-tuple, where n is the number of layer.
nb_layer = len_data / len(timestamps) / tuple_size
values = [[]]
for value in tmp_values:
t = values[-1]
if len(t) < nb_layer:
t.append(value)
else:
values.append([value])
return list(map(lambda x: [x], values))
def update_water_limits(self, timestamp, z):
limits = self.geometry.get_water_limits(z)
self.set(
timestamp, "water_limits", limits
)
def get_keys(self):
return reduce(
lambda acc, ts: acc.union(self._data[ts].keys()),
self._data.keys(), set()
)
def _db_save(self, execute, data=None):
pid = self._pamhyr_id
result = data["result"]
keys = self.get_keys()
logger.debug(f"{keys}...")
for key in keys:
values = self.get_key(key)
if key in ["Z", "Q", "V"]:
values = list(map(float, values))
sf = ">" + ''.join(itertools.repeat("f", len(values)))
len_values = len(values)
elif key == "sl":
# HACK: Some dirty code to transforme list of list of
# tuple to list of values and ensure the values is
# float type...
values = flatten(
flatten(
map(lambda v: list(
map(lambda t: list(
map(float, t)), v[0])),
values)))
len_values = len(values)
sf = ">" + ''.join(itertools.repeat("f", len_values))
else:
continue
data_bytes = struct.pack(sf, *values)
execute(
"INSERT INTO " +
"results_data (pamhyr_id, result, " +
"reach, section, " +
"key, len_data, data, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
pid, result,
data["reach"].pamhyr_id,
self._profile.pamhyr_id,
key, len_values, data_bytes,
self._owner_scenario
)
return True
class Reach(SQLSubModel):
_sub_classes = [Profile]
def __init__(self, reach, study, parent, with_init=True):
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 = list(
self._profiles = []
if with_init:
self._profiles = list(
map(
lambda p: Profile(p, self._study, self._parent),
reach.profiles
)
)
self._profile_mask = list(
map(
lambda p: Profile(p, self._study),
reach.profiles
lambda p: p.name[0:8] != 'interpol', self._profiles
)
)
self._global_index = (-1, -1)
self._buffers = {}
def __len__(self):
return len(self._profiles)
@@ -92,22 +319,94 @@ class Reach(object):
def profiles(self):
return self._profiles.copy()
@property
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=""):
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
reach = data["reach"]
new_reach = cls(
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)]
return new_reach
def _db_save(self, execute, data=None):
logger.debug("Save reach...")
for profile in self._profiles:
data["profile"] = profile.geometry.pamhyr_id
profile._db_save(execute, data)
class River(object):
def __init__(self, study):
class River(SQLSubModel):
_sub_classes = [Reach]
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)
@@ -125,7 +424,7 @@ class River(object):
def add(self, reach_id):
reachs = self._study.river.enable_edges()
new = Reach(reachs[reach_id].reach, self._study)
new = Reach(reachs[reach_id].reach, self._study, self._parent)
self._reachs.append(new)
return new
@@ -137,3 +436,41 @@ class River(object):
self._reachs
)
)
def update_water_limits(self, timestamp, data):
for reach in self._reachs:
ind = reach.global_index
reach.update_water_limits(
timestamp,
data[ind[0]:ind[1]]
)
@classmethod
def _db_create(cls, execute, ext=""):
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
study = data["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)
)
return new_river
def _db_save(self, execute, data=None):
logger.debug("Save river...")
for reach in self._reachs:
data["reach"] = reach.geometry
reach._db_save(execute, data)
+1 -1
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr
# Copyright (C) 2023-2024 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
+447 -81
View File
@@ -1,5 +1,5 @@
# River.py -- Pamhyr river model
# Copyright (C) 2023-2024 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
@@ -16,9 +16,13 @@
# -*- coding: utf-8 -*-
import types
import logging
from tools import flatten, logger_exception
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Network.Node import Node
from Model.Network.Edge import Edge
@@ -55,64 +59,154 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
import LateralContributionsAdisTSList
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results
logger = logging.getLogger()
class RiverNode(Node, SQLSubModel):
class RiverNode(Node):
_sub_classes = []
def __init__(self, id: str, name: str,
x: float, y: float,
status=None):
def __init__(self, id: int = -1, name: str = "",
x: float = 0.0, y: float = 0.0,
status=None, owner_scenario=-1):
super(RiverNode, self).__init__(
id, name,
x, y,
status=status
id=id, name=name, x=x, y=y,
status=status,
owner_scenario=owner_scenario
)
self._locker = None
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE river_node(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE river_node{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
x REAL NOT NULL,
y REAL NOT NULL
y REAL NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
cls._create_submodel(execute)
return True
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
return True
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_update_to_0_2_0(execute, data=data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE river_node " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data=None):
table = "river_node"
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
# Copy table
execute(
f"INSERT INTO {table}_tmp " +
f"(pamhyr_id, name, x, y, scenario) " +
"SELECT pamhyr_id, name, x, y, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_load(cls, execute, data=None):
nodes = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return nodes
table = execute(
"SELECT pamhyr_id, deleted, name, x, y, scenario " +
"FROM river_node " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
table = execute("SELECT id, name, x, y FROM river_node")
for row in table:
# Update id counter
cls._id_cnt = max(cls._id_cnt, row[0])
# Create new node
nodes.append(cls(*row, status=data["status"]))
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
x = next(it)
y = next(it)
owner_scenario = next(it)
node = cls(
id=pid,
name=name, x=x, y=y,
status=data["status"],
owner_scenario=owner_scenario
)
if deleted:
node.set_as_deleted()
loaded.add(pid)
nodes.append(node)
data["scenario"] = scenario.parent
nodes += cls._db_load(execute, data)
data["scenario"] = scenario
return nodes
def _db_save(self, execute, data=None):
sql = (
"INSERT OR REPLACE INTO river_node(id, name, x, y) VALUES (" +
f"{self.id}, '{self._db_format(self.name)}', " +
f"{self.x}, {self.y}" +
if not self.must_be_saved():
return True
execute(
"INSERT OR REPLACE INTO river_node(" +
"pamhyr_id, deleted, name, x, y, scenario" +
") " +
"VALUES (" +
f"{self._pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self.name)}', " +
f"{self.x}, {self.y}, {self._status.scenario_id}" +
")"
)
execute(sql)
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_deleted(self):
return self._deleted
@property
def locker(self):
return self._locker
@@ -122,100 +216,207 @@ class RiverNode(Node, SQLSubModel):
self._locker = locker
class RiverReach(Edge, SQLSubModel):
class RiverReach(Edge):
_sub_classes = [
Reach,
FrictionList,
]
def __init__(self, id: str, name: str,
def __init__(self, id: str = -1, name: str = "",
node1: RiverNode = None,
node2: RiverNode = None,
status=None):
status=None, owner_scenario=-1):
super(RiverReach, self).__init__(
id, name,
node1, node2,
status=status
id=id, name=name,
node1=node1, node2=node2,
status=status,
owner_scenario=owner_scenario
)
self._reach = Reach(status=self._status, parent=self)
self._frictions = FrictionList(status=self._status)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE river_reach(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE river_reach{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
enable BOOLEAN NOT NULL,
enabled BOOLEAN NOT NULL,
node1 INTEGER,
node2 INTEGER,
FOREIGN KEY(node1) REFERENCES river_node(id),
FOREIGN KEY(node2) REFERENCES river_node(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node1) REFERENCES river_node(pamhyr_id),
FOREIGN KEY(node2) REFERENCES river_node(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
cls._create_submodel(execute)
return True
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
return cls._update_submodel(execute, version)
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
cls._db_update_to_0_2_0(execute, data)
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE river_reach " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data=None):
table = "river_reach"
nodes = data['id2pid']['river_node']
cls.update_db_add_pamhyr_id(execute, table, data)
Scenario.update_db_add_scenario(execute, table)
cls._db_create(execute, ext="_tmp")
# Copy table
execute(
f"INSERT INTO {table}_tmp " +
f"(pamhyr_id, name, enabled, node1, node2, scenario) " +
"SELECT pamhyr_id, name, enable, node1, node2, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_node_pid(execute, table, nodes)
@classmethod
def _db_update_to_0_2_0_set_node_pid(cls, execute, table, nodes):
bcs = execute(
f"SELECT pamhyr_id, node1, node2 FROM {table}"
)
for row in bcs:
it = iter(row)
pid = next(it)
node1_id = next(it)
node2_id = next(it)
execute(
f"UPDATE {table} " +
f"SET node1 = {nodes[node1_id]}, " +
f"node2 = {nodes[node2_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
reachs = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if data is None:
data = {}
if scenario is None:
return reachs
table = execute(
"SELECT id, name, enable, node1, node2 FROM river_reach"
"SELECT pamhyr_id, deleted, name, enabled, " +
"node1, node2, scenario " +
"FROM river_reach " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
# Update id counter
cls._id_cnt = max(cls._id_cnt, row[0])
# Create new reach
id = row[0]
name = row[1]
enable = (row[2] == 1)
# Get nodes corresponding to db foreign key id
node1 = next(filter(lambda n: n.id == row[3], data["nodes"]))
node2 = next(filter(lambda n: n.id == row[4], data["nodes"]))
it = iter(row)
new = cls(id, name, node1, node2, status=data["status"])
new.enable(enable=enable)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
enabled = (next(it) == 1)
node1_pid = next(it)
node2_pid = next(it)
owner_scenario = next(it)
# Get nodes corresponding to db foreign key id
node1 = next(
filter(
lambda n: n.pamhyr_id == node1_pid, data["nodes"]
)
)
node2 = next(
filter(
lambda n: n.pamhyr_id == node2_pid, data["nodes"]
)
)
new = cls(
id=pid, name=name, node1=node1, node2=node2,
status=data["status"],
owner_scenario=owner_scenario
)
new.enable(enable=enabled)
if deleted:
new.set_as_deleted()
data["reach"] = new
new._reach = Reach._db_load(execute, data)
new._frictions = FrictionList._db_load(execute, data)
loaded.add(pid)
reachs.append(new)
data["scenario"] = scenario.parent
reachs += cls._db_load(execute, data)
data["scenario"] = scenario
return reachs
def _db_save(self, execute, data=None):
sql = (
if not self.must_be_saved():
return True
execute(
"INSERT OR REPLACE INTO " +
"river_reach(id, name, enable, node1, node2) " +
"river_reach(" +
"pamhyr_id, deleted, name, enabled, " +
"node1, node2, scenario" +
") " +
"VALUES (" +
f"{self.id}, '{self._db_format(self._name)}', " +
f"{self._db_format(self.is_enable())},"
f"{self.node1.id}, {self.node2.id}" +
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.node1.pamhyr_id}, {self.node2.pamhyr_id}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
if data is None:
data = {}
data["reach"] = self
objs = [self._reach, self._frictions]
return self._save_submodel(execute, objs, data)
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
self._reach._data_traversal(predicate, modifier, data)
self._frictions._data_traversal(predicate, modifier, data)
def is_deleted(self):
return self._deleted
@property
def reach(self):
return self._reach
@@ -224,17 +425,46 @@ class RiverReach(Edge, SQLSubModel):
def frictions(self):
return self._frictions
def _split(self, profile, new_node):
name = self.name
node1 = self.node1
node2 = self.node2
class River(Graph, SQLSubModel):
new_reach1 = RiverReach(
id=-1, name=name + "(part 1)",
node1=node1, node2=new_node,
status=self._status,
)
new_reach2 = RiverReach(
id=-1, name=name + "(part 2)",
node1=new_node, node2=node2,
status=self._status,
)
nr1, nr2 = self._reach._split(profile, new_reach1, new_reach2)
new_reach1._reach = nr1
new_reach1._frictions = self._frictions._splited_for(new_reach1)
new_reach2._reach = nr2
new_reach2._frictions = self._frictions._splited_for(new_reach2)
self.disable()
return new_reach1, new_reach2
class River(Graph):
_sub_classes = [
StricklersList,
SedimentLayerList,
RiverNode,
RiverReach,
BoundaryConditionList,
LateralContributionList,
InitialConditionsDict,
StricklersList,
SolverParametersList,
SedimentLayerList,
ReservoirList,
HydraulicStructureList,
AddFileList,
@@ -246,10 +476,15 @@ class River(Graph, SQLSubModel):
LateralContributionsAdisTSList,
D90AdisTSList,
DIFAdisTSList,
GeoTIFFList,
EnsembleList,
Results
]
def __init__(self, status=None):
super(River, self).__init__(status=status)
super(River, self).__init__(
status=status
)
# Replace Node and Edge ctor by custom ctor
self._node_ctor = RiverNode
@@ -280,19 +515,25 @@ class River(Graph, SQLSubModel):
self._D90AdisTS = D90AdisTSList(status=self._status)
self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._geotiff = GeoTIFFList(status=self._status)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {}
@classmethod
def _db_create(cls, execute):
cls._create_submodel(execute)
return True
@classmethod
def _db_update(cls, execute, version):
cls._update_submodel(execute, version)
return True
def _db_update(cls, execute, version, data=None):
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = cls(data["status"])
new = cls(status=data["status"])
# Stricklers (Stricklers is load in first because it's needed
# for reachs)
@@ -300,65 +541,84 @@ class River(Graph, SQLSubModel):
execute, data
)
data["stricklers"] = new._stricklers
data['loaded_pid'] = set()
# Initial conditions
new._sediment_layers = SedimentLayerList._db_load(
execute, data
)
data["sediment_layers_list"] = new._sediment_layers
data['loaded_pid'] = set()
# Network
new._nodes = RiverNode._db_load(
execute, data
)
data["nodes"] = new.nodes()
data["nodes"] = new._nodes
data['loaded_pid'] = set()
new._edges = RiverReach._db_load(
execute, data
)
data["edges"] = new.edges()
data['loaded_pid'] = set()
# Boundary Condition
new._boundary_condition = BoundaryConditionList._db_load(
execute, data
)
data['loaded_pid'] = set()
# Lateral Contribution
new._lateral_contribution = LateralContributionList._db_load(
execute, data
)
data['loaded_pid'] = set()
# Initial conditions
new._initial_conditions = InitialConditionsDict._db_load(
execute, data
)
data['loaded_pid'] = set()
# Reservoir
new._reservoir = ReservoirList._db_load(
execute, data
)
data['loaded_pid'] = set()
# Hydraulic Structures
new._hydraulic_structures = HydraulicStructureList._db_load(
execute, data
)
data['loaded_pid'] = set()
# Parameters
new._parameters = SolverParametersList._db_load(
execute, data
)
data['loaded_pid'] = set()
# Additional Files
new._additional_files = AddFileList._db_load(
execute, data
)
new._rep_lines = REPLineList._db_load(execute, data)
data['loaded_pid'] = set()
new._Pollutants = PollutantsList._db_load(execute, data)
new._Output_rk_adists = OutputRKAdistsList._db_load(
execute, data
)
new._Pollutants = PollutantsList._db_load(execute, data)
new._InitialConditionsAdisTS = InitialConditionsAdisTSList._db_load(
execute, data)
@@ -372,8 +632,18 @@ class River(Graph, SQLSubModel):
new._DIFAdisTS = DIFAdisTSList._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):
results_lst = Results._db_load(execute, data)
for solv_type, results in results_lst:
self._results[solv_type] = results
def _db_save(self, execute, data=None):
self._db_save_delete_artefact(execute, data)
@@ -399,6 +669,13 @@ class River(Graph, SQLSubModel):
objs.append(self._D90AdisTS)
objs.append(self._DIFAdisTS)
objs.append(self._geotiff)
objs.append(self._ensembles)
for solv_type in self.results:
objs.append(self.results[solv_type])
self._save_submodel(execute, objs, data)
return True
@@ -422,15 +699,64 @@ class River(Graph, SQLSubModel):
execute(
f"DELETE FROM {table} " +
"WHERE " +
f"scenario = {self._status.scenario_id} AND (" +
" OR ".join(
map(
lambda n: f"( id <> {n.id} )", els
lambda n: f"( pamhyr_id <> {n.pamhyr_id} )",
els
)
)
) + ")"
)
except Exception as e:
logger_exception(e)
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
"""Traversal data and execute modifier fonction if predicate
true
Args:
predicate: Function predicate, take current obj and data as input
modifier: Function modifier, take current obj and data as input
Returns:
Nothing
"""
if predicate(self, data):
modifier(self, data)
for obj in self._get_objs_list():
obj._data_traversal(predicate, modifier, data)
def _get_objs_list(self):
objs = (self._nodes + self._edges)
objs += [
self._boundary_condition,
self._initial_conditions,
self._lateral_contribution,
self._sediment_layers,
self._stricklers,
self._reservoir,
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._ensembles,
]
for solver in self._parameters:
objs.append(self._parameters[solver])
return objs
def init_default(self):
self.init_default_network()
self.init_default_sediment()
@@ -468,6 +794,9 @@ execution...
Last export at: @date."""
def reachs(self):
return self.edges()
@property
def boundary_condition(self):
return self._boundary_condition
@@ -513,6 +842,10 @@ Last export at: @date."""
def additional_files(self):
return self._additional_files
@property
def geotiff(self):
return self._geotiff
@property
def rep_lines(self):
return self._rep_lines
@@ -589,3 +922,36 @@ Last export at: @date."""
)
return has
@property
def results(self):
results = self._results
if isinstance(results, types.GeneratorType):
self._results = next(results)
return self._results
@results.setter
def results(self, results):
solv_type = results.get("solver_type")
self._results[solv_type] = results
def _split_reach(self, reach, profile):
node1 = reach.node1
node2 = reach.node2
x1, y1 = node1.x, node1.y
x2, y2 = node2.x, node2.y
center_x = (x1 + x2) / 2
center_y = (y1 + y2) / 2
new_node = self.add_node(center_x, center_y)
r1, r2 = reach._split(profile, new_node)
self._add_edge(r1)
self._add_edge(r2)
return r1, r2
-53
View File
@@ -1,53 +0,0 @@
# Saved.py -- Pamhyr model status class
# Copyright (C) 2023-2024 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
logger = logging.getLogger()
class SavedStatus(object):
def __init__(self, version=0):
super(SavedStatus, self).__init__()
self._version = version
self._saved = True
@property
def version(self):
return self._version
@version.setter
def version(self, version):
self._version = version
def is_saved(self):
return self._saved
def save(self):
logger.debug("model status set as saved")
self._saved = True
def modified(self):
if self._saved:
self._version += 1
logger.debug(
"STATUS: Model status set as modified " +
f"at version {self._version}"
)
self._saved = False
+373
View File
@@ -0,0 +1,373 @@
# Scenario.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import os
import logging
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
"output_rk_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",
"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 = 1000.0, y: int = 1000.0,
is_ensemble: bool = False,
revision: int = 0,
parent=None):
super(Scenario, self).__init__()
self._set_id(id)
self._x, self._y = x, y
self._name = name
self._description = description
self._is_ensemble = is_ensemble
self._revision = revision
self._parent = parent
def _set_id(self, id):
if id == -1:
self._id = Scenario._id_cnt
else:
self._id = id
Scenario._id_cnt = max(
self._id + 1, Scenario._id_cnt + 1
)
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE scenario(
id INTEGER PRIMARY KEY,
x REAL NOT NULL DEFAULT 1000,
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)
)
""")
cls._create_submodel(execute)
return True
@classmethod
def _db_add_default(cls, execute):
execute(
"INSERT OR REPLACE INTO " +
"scenario(id, x, y, name, description, revision, parent_id) " +
"VALUES (\n" +
" 0, 1000, 1000, 'default', 'Default scenario',\n" +
" 0, NULL\n" +
")"
)
@classmethod
def create_db_add_scenario(cls):
return "scenario INTEGER NOT NULL DEFAULT 0"
@classmethod
def create_db_add_scenario_fk(cls):
return "FOREIGN KEY(scenario) REFERENCES scenario(id)"
@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)
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
def update_db_add_scenario(cls, execute, table):
execute(
f"ALTER TABLE {table} " +
"ADD COLUMN scenario INTEGER NOT NULL DEFAULT 0"
)
# execute(
# f"ALTER TABLE {table} " +
# "ADD CONSTRAINT fk_scenario FOREIGN KEY (scenario) " +
# "REFERENCES scenario(id)"
# )
@classmethod
def _db_load(cls, execute, data=None):
scenarios = {}
table = execute(
"SELECT id, x, y, name, description, " +
" is_ensemble, revision, parent_id " +
"FROM scenario " +
"ORDER BY id ASC"
)
for row in table:
it = iter(row)
id = next(it)
x, y = next(it), next(it)
name = next(it)
desc = next(it)
is_ens = next(it)
revi = next(it)
parent = next(it)
if parent is not None:
parent = scenarios[parent]
new = cls(
id=id, x=x, y=y,
name=name, description=desc,
is_ensemble=is_ens,
revision=revi,
parent=parent
)
scenarios[id] = new
return scenarios
def _db_save(self, execute, data=None):
if self.is_deleted():
return self.drop_all(execute)
parent = None
if self.parent is not None:
parent = self.parent._id
execute(
"INSERT OR REPLACE INTO " +
"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
def drop_all(self, execute):
execute(f"DELETE FROM scenario WHERE id = {self.id}")
tables = self.related_tables
for table in tables:
execute(
f"DELETE FROM {table} " +
f"WHERE scenario = {self.id}"
)
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
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@property
def name(self):
if self._name == "":
return f"Child of '{self._parent.name}' ({self.id})"
return self._name
@name.setter
def name(self, name):
self._name = name
@property
def description(self):
if self._description == "":
return f"Child of '{self._parent.name}' ({self.id})"
return self._description
@description.setter
def description(self, description):
self._description = description
@property
def revision(self):
return self._revision
@revision.setter
def revision(self, revision):
self._revision = revision
@property
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
def __setitem__(self, key, value):
if key == "name":
self.name = value
elif key == "description":
self.description = value
def __getitem__(self, key):
if key == "id":
return self.id
if key == "name":
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)
)
return srep
+94
View File
@@ -0,0 +1,94 @@
# Scenarios.py -- Pamhyr
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from functools import reduce
from tools import logger_exception
from Model.Tools.PamhyrDict import PamhyrModelDict
from Model.Scenario import Scenario
class Scenarios(PamhyrModelDict):
_sub_classes = [
Scenario,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data["status"])
new._dict = Scenario._db_load(
execute,
data=data
)
return new
def _db_save(self, execute, data=None):
ok = True
if data is None:
data = {}
scenarios = self._dict
for sid in scenarios:
ok &= scenarios[sid]._db_save(execute, data)
return ok
def get(self, key):
if key in self._dict:
return self._dict[key]
return None
def new(self, parent, is_ensemble=False):
new = Scenario(
parent=parent,
is_ensemble=is_ensemble
)
self.set(new._id, new)
return new
def __len__(self):
return len(self.lst)
@property
def lst(self):
return list(
map(
lambda k: self.get(k),
self._dict
)
)
def delete(self, key):
if key not in self._dict:
return
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
)
+285 -97
View File
@@ -1,5 +1,5 @@
# SedimentLayer.py -- Pamhyr
# Copyright (C) 2024 INRAE
# Copyright (C) 2024-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,23 +21,26 @@ from functools import reduce
from tools import trace, timer
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Except import NotImplementedMethodeError
class Layer(SQLSubModel):
_sub_classes = []
_id_cnt = 0
def __init__(self,
id: int = -1, name: str = "",
type="",
height=0.0, d50=0.0, sigma=0.0,
critical_constraint=0.0,
sl=None, status=None):
super(Layer, self).__init__()
self._status = status
sl=None, status=None,
owner_scenario=-1):
super(Layer, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._sl = sl
self._name = name
self._type = type
@@ -46,23 +49,17 @@ class Layer(SQLSubModel):
self._sigma = sigma
self._critical_constraint = critical_constraint
if id == -1:
self.id = Layer._id_cnt
else:
self.id = id
Layer._id_cnt = max(id, Layer._id_cnt+1)
@property
def name(self):
if self._name == "":
return f"Layer{self.id + 1}"
return f"Layer #{self.pamhyr_id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def type(self):
@@ -71,6 +68,7 @@ class Layer(SQLSubModel):
@type.setter
def type(self, type):
self._type = type
self.modified()
@property
def height(self):
@@ -79,6 +77,7 @@ class Layer(SQLSubModel):
@height.setter
def height(self, height):
self._height = float(height)
self.modified()
@property
def d50(self):
@@ -87,6 +86,7 @@ class Layer(SQLSubModel):
@d50.setter
def d50(self, d50):
self._d50 = float(d50)
self.modified()
@property
def sigma(self):
@@ -95,6 +95,7 @@ class Layer(SQLSubModel):
@sigma.setter
def sigma(self, sigma):
self._sigma = float(sigma)
self.modified()
@property
def critical_constraint(self):
@@ -103,12 +104,14 @@ class Layer(SQLSubModel):
@critical_constraint.setter
def critical_constraint(self, critical_constraint):
self._critical_constraint = float(critical_constraint)
self.modified()
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE sedimentary_layer_layer(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE sedimentary_layer_layer{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
ind INTEGER NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
@@ -117,92 +120,177 @@ class Layer(SQLSubModel):
sigma REAL NOT NULL,
critical_constraint REAL NOT NULL,
sl INTEGER,
FOREIGN KEY(sl) REFERENCES sedimentary_layer(id)
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(sl) REFERENCES sedimentary_layer(pamhyr_id),
PRIMARY KEY(pamhyr_id, scenario)
)
""")
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 2:
cls._db_create(execute)
return True
else:
cls._db_update_to_0_2_0(execute, data)
return True
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE sedimentary_layer_layer " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "sedimentary_layer_layer"
sl = data['id2pid']['sedimentary_layer']
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}_tmp " +
"(pamhyr_id, ind, name, type, height, " +
"d50, sigma, critical_constraint, sl, scenario) " +
"SELECT pamhyr_id, ind, name, type, height, " +
"d50, sigma, critical_constraint, sl, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
cls._db_update_to_0_2_0_set_sl_pid(execute, table, sl)
@classmethod
def _db_update_to_0_2_0_set_sl_pid(cls, execute, table, sl):
els = execute(
f"SELECT pamhyr_id, sl FROM {table}"
)
if els is None:
return True
for row in els:
it = iter(row)
pid = next(it)
sl_id = next(it)
execute(
f"UPDATE {table} " +
f"SET sl = {sl[sl_id]} " +
f"WHERE pamhyr_id = {pid}"
)
@classmethod
def _db_load(cls, execute, data=None):
new = []
sl = data["sl"]
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT id, ind, name, type, height, " +
"d50, sigma, critical_constraint " +
"SELECT pamhyr_id, deleted, name, type, height, " +
"d50, sigma, critical_constraint, scenario " +
"FROM sedimentary_layer_layer " +
f"WHERE sl = {sl}"
f"WHERE sl = {sl.pamhyr_id} " +
f"AND scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
"ORDER BY ind ASC"
)
for _ in table:
new.append(None)
for row in table:
ind = row[1]
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
type = next(it)
height = next(it)
d50 = next(it)
sigma = next(it)
critical_constraint = next(it)
owner_scenario = next(it)
layer = cls(
id=row[0], name=row[2],
type=row[3], height=row[4],
d50=row[5], sigma=row[6],
critical_constraint=row[7],
sl=sl, status=data['status']
id=pid, name=name,
type=type, height=height,
d50=d50, sigma=sigma,
critical_constraint=critical_constraint,
sl=sl, status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
layer.set_as_deleted()
new[ind] = layer
loaded.add(pid)
new.append(layer)
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
ind = data["ind"]
sl = data["sl"]
sql = (
execute(
"INSERT INTO " +
"sedimentary_layer_layer(id, ind, name, type, height, " +
"d50, sigma, critical_constraint, sl) " +
"sedimentary_layer_layer(" +
"pamhyr_id, deleted, ind, name, type, height, " +
"d50, sigma, critical_constraint, sl, scenario" +
") " +
"VALUES (" +
f"{self.id}, {ind}, '{self._db_format(self._name)}', " +
f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"{ind}, '{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', {self._height}, " +
f"{self._d50}, {self._sigma}, {self._critical_constraint}, " +
f"{sl.id}" +
f"{sl.pamhyr_id}, " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
return True
def modified(self):
super(Layer, self).modified()
self._sl.modified()
class SedimentLayer(SQLSubModel):
_sub_classes = [Layer]
_id_cnt = 0
def __init__(self, id: int = -1,
name: str = "", comment: str = "",
status=None):
super(SedimentLayer, self).__init__()
status=None, owner_scenario=-1):
super(SedimentLayer, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = name
self._comment = comment
self._layers = []
if id == -1:
self.id = SedimentLayer._id_cnt
else:
self.id = id
SedimentLayer._id_cnt = max(id, SedimentLayer._id_cnt+1)
def __str__(self):
s = f"{self.name} ({len(self)})"
@@ -212,31 +300,48 @@ class SedimentLayer(SQLSubModel):
return s
def __len__(self):
return len(self._layers)
return len(
list(
filter(
lambda el: not el.is_deleted(),
self._layers
)
)
)
@property
def layers(self):
return self._layers.copy()
return list(
filter(
lambda el: not el.is_deleted(),
self._layers
)
)
def height(self):
return list(
map(lambda layer: layer.height, self._layers)
map(lambda layer: layer.height,
self.layers)
)
@property
def name(self):
if self._name == "":
return f"SL{self.id + 1}"
return f"SL #{self.pamhyr_id}"
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
def names(self):
return list(
map(lambda layer: layer.name, self._layers)
map(
lambda layer: layer.name,
self.layers
)
)
@property
@@ -246,62 +351,128 @@ class SedimentLayer(SQLSubModel):
@comment.setter
def comment(self, comment):
self._comment = comment
self.modified()
@classmethod
def _db_create(cls, execute):
execute("""
CREATE TABLE sedimentary_layer(
id INTEGER NOT NULL PRIMARY KEY,
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE sedimentary_layer{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
comment TEXT NOT NULL
comment TEXT 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):
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == minor == "0":
if int(release) < 2:
cls._db_create(execute)
return True
else:
cls._db_update_to_0_2_0(execute, data)
return True
if major == "0" and minor == "1":
if int(release) < 2:
execute(
"ALTER TABLE sedimentary_layer " +
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "sedimentary_layer"
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}_tmp " +
"(pamhyr_id, name, comment, scenario) " +
"SELECT pamhyr_id, name, comment, scenario " +
f"FROM {table}"
)
execute(f"DROP TABLE {table}")
execute(f"ALTER TABLE {table}_tmp RENAME TO {table}")
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
table = execute(
"SELECT id, name, comment " +
"FROM sedimentary_layer "
"SELECT pamhyr_id, deleted, name, comment, scenario " +
"FROM sedimentary_layer " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
)
for row in table:
sl = cls(
id=row[0],
name=row[1],
comment=row[2],
status=data['status']
)
if table is not None:
for row in table:
it = iter(row)
data["sl"] = sl.id
sl._layers = Layer._db_load(execute, data)
pid = next(it)
deleted = (next(it) == 1)
name = next(it)
comment = next(it)
owner_scenario = next(it)
new.append(sl)
sl = cls(
id=pid,
name=name,
comment=comment,
status=data['status'],
owner_scenario=owner_scenario
)
if deleted:
sl.set_as_deleted()
data["sl"] = sl
sl._layers = Layer._db_load(execute, data)
loaded.add(pid)
new.append(sl)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if data is None:
data = {}
if not self.must_be_saved():
return True
sql = (
"INSERT INTO sedimentary_layer (id, name, comment) " +
f"VALUES ({self.id}, '{self._db_format(self._name)}', " +
f"'{self._db_format(self._comment)}')"
execute(
"INSERT INTO sedimentary_layer (" +
"pamhyr_id, deleted, name, comment, scenario" +
") " +
"VALUES (" +
f"{self._pamhyr_id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._comment)}', " +
f"{self._status.scenario_id}" +
")"
)
execute(sql)
data["sl"] = self
@@ -313,39 +484,56 @@ class SedimentLayer(SQLSubModel):
return True
def get(self, index):
return self._layers[index]
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
def set(self, index, new):
self._layers[index] = new
self._status.modified()
for layer in self._layers:
layer._data_traversal(predicate, modifier, data)
def get(self, index):
return self.layers[index]
# def set(self, index, new):
# self._layers[index] = new
# self.modified()
def insert(self, index, new):
self._layers.insert(index, new)
self._status.modified()
self.modified()
def new(self, index):
n = Layer(sl=self, status=self._status)
self.insert(index, n)
self._status.modified()
self.modified()
return n
def undelete(self, els):
for el in els:
el.set_as_not_deleted()
self.modified()
def delete(self, els):
for el in els:
self._layers.remove(el)
self._status.modified()
el.set_as_deleted()
self.modified()
def delete_i(self, indexes):
els = list(
list(
map(
lambda x: x[1],
lambda e: e[1].set_as_deleted(),
filter(
lambda x: x[0] in indexes,
lambda e: e[0] in indexes,
enumerate(self._layers)
)
)
)
self.delete(els)
self.modified()
def move_up(self, index):
if index >= 0:
@@ -354,7 +542,7 @@ class SedimentLayer(SQLSubModel):
lst = self._layers
lst[index], lst[next] = lst[next], lst[index]
self._status.modified()
self.modified()
def move_down(self, index):
if index + 1 < len(self._layers):
@@ -363,7 +551,7 @@ class SedimentLayer(SQLSubModel):
lst = self._layers
lst[index], lst[prev] = lst[prev], lst[index]
self._status.modified()
self.modified()
def compute_height_from_bottom(self, bottom_elevation: list):
sl_height = self.height()
Loaded 100 of 414 files, more files were not shown because too many files have changed in this diff. Show more