Compare commits

..
83 Commits
Author SHA1 Message Date
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
56 changed files with 4867 additions and 546 deletions

No files matched your search

+6
View File
@@ -1069,6 +1069,12 @@ gitlab-ci.
sudo python3 -m pip install pyinstaller sudo python3 -m pip install pyinstaller
#+end_src #+end_src
In addition, you must install Glab[fn:glab] to allow the runner to
proceed with the publication.
[fn:glab] The Gitlab cli releases page:
https://gitlab.com/gitlab-org/cli/-/releases
*** Windows (Wine) {{{windows}}} *** Windows (Wine) {{{windows}}}
The ci-runner environment for Wine need at least wine version 8, let The ci-runner environment for Wine need at least wine version 8, let
+1 -1
View File
@@ -20,4 +20,4 @@
"python-pyqtgraph" "python-qscintilla" "python-pyqtgraph" "python-qscintilla"
"python-numpy@1" "python-shapely" "python-pyshp" "python-numpy@1" "python-shapely" "python-pyshp"
"python-matplotlib" "python-colorama" "python-matplotlib" "python-colorama"
"python-rasterio")) "python-rasterio" "python-psutil"))
+2 -2
View File
@@ -4,7 +4,7 @@ PyQt5-sip==12.18.0
#PyQtWebEngine==5.15.6 #PyQtWebEngine==5.15.6
QScintilla>=2.14.1 QScintilla>=2.14.1
pyqtgraph>=0.12.1 pyqtgraph>=0.12.1
matplotlib==3.8.3 matplotlib==3.10.8
numpy>=1.24.2 numpy>=1.24.2
colorama>=0.4.3 colorama>=0.4.3
pyinstaller>=5.11.0 pyinstaller>=5.11.0
@@ -14,4 +14,4 @@ platformdirs>=4.2.0
pyshp>=2.3.1 pyshp>=2.3.1
rasterio==1.3.11 rasterio==1.3.11
#fortranformat==2.0.3 #fortranformat==2.0.3
psutil>=7.2.2
-13
View File
@@ -57,13 +57,6 @@ class InternalMeshing(AMeshingTool):
# we never modify original profiles, we work on the copies # we never modify original profiles, we work on the copies
m_profiles = self.st_to_m(profiles, guide_list) m_profiles = self.st_to_m(profiles, guide_list)
# Adapt the spacing to the kilometre-point direction. The value from
# the dialog is a distance, so only its magnitude is significant.
step = self.signed_step(
step, m_profiles[0].rk, m_profiles[-1].rk
)
new_profiles = self.interpolate_transversal_step(m_profiles, step) new_profiles = self.interpolate_transversal_step(m_profiles, step)
for new_profiles2 in new_profiles: for new_profiles2 in new_profiles:
@@ -72,12 +65,6 @@ class InternalMeshing(AMeshingTool):
return new_profiles return new_profiles
@staticmethod
def signed_step(step, begin_rk, end_rk):
if end_rk < begin_rk:
return -abs(step)
return abs(step)
def st_to_m(self, profiles, guide_list): def st_to_m(self, profiles, guide_list):
guide_list = ["un"] + guide_list + ["np"] guide_list = ["un"] + guide_list + ["np"]
max_values = [0] * (len(guide_list) - 1) max_values = [0] * (len(guide_list) - 1)
+241
View File
@@ -0,0 +1,241 @@
# 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 = []
@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 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]
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]
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.normal(*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 = ["lam"]
self._default_values = [5.0]
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 = ""
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
+60
View File
@@ -0,0 +1,60 @@
# 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
new._lst = [
Uniform(status=data['status']),
Normal(status=data['status']),
Poisson(status=data['status']),
]
# 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
+270
View File
@@ -0,0 +1,270 @@
# 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):
return {
"name": self.name,
"data_type": self.data_type,
"target_data": self.target_data,
"distribution": self.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"]
+53
View File
@@ -0,0 +1,53 @@
# 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
+5 -6
View File
@@ -95,12 +95,11 @@ class PointXYZ(Point):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
) )
if major == "0" and minor == "2": # Try to create index at each update
if int(release) < 5: execute(
execute( "CREATE INDEX IF NOT EXISTS idx_point " +
"CREATE INDEX idx_point " + "ON geometry_pointXYZ(profile, scenario);"
"ON geometry_pointXYZ(profile, scenario);" )
)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
+7
View File
@@ -826,6 +826,13 @@ class ProfileXYZ(Profile, SQLSubModel):
return n_zones return n_zones
def froude(self, z, v):
return v / np.sqrt(
9.81 * (
self.wet_area(z) / self.wet_width(z)
)
)
@timer @timer
def get_all_water_limits_ac(self, z): def get_all_water_limits_ac(self, z):
""" """
+48 -9
View File
@@ -277,9 +277,9 @@ class TableData(SQLSubModel):
if len(shape) == 2: if len(shape) == 2:
shape_x, shape_y = shape shape_x, shape_y = shape
elif len(shape == 1): elif len(shape) == 1:
shape_x, = shape shape_x, = shape
elif len(shape == 3): elif len(shape) == 3:
shape_x, shape_y, shape_z = shape shape_x, shape_y, shape_z = shape
else: else:
logger.error( logger.error(
@@ -331,14 +331,14 @@ class TableData(SQLSubModel):
if isinstance(key, tuple): if isinstance(key, tuple):
return self._data[key[0], key[1]] return self._data[key[0], key[1]]
self._date[key] self._data[key]
class Results(SQLSubModel): class Results(SQLSubModel):
_sub_classes = [River, TableData, AdditionalData] _sub_classes = [River, TableData, AdditionalData]
def __init__(self, id=-1, study=None, solver=None, def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0"): repertory="", name="0", is_ensemble=False):
super(Results, self).__init__( super(Results, self).__init__(
id=id, status=study.status, id=id, status=study.status,
owner_scenario=study.status.scenario.id owner_scenario=study.status.scenario.id
@@ -351,6 +351,8 @@ class Results(SQLSubModel):
self._solver = solver self._solver = solver
self._repertory = repertory self._repertory = repertory
self._is_ensemble = is_ensemble
self._meta_data = { self._meta_data = {
# Keep results creation date # Keep results creation date
"creation_date": datetime.now(), "creation_date": datetime.now(),
@@ -364,6 +366,9 @@ class Results(SQLSubModel):
if solver is not None: if solver is not None:
self.set("solver_type", solver._type) self.set("solver_type", solver._type)
def is_ensemble(self):
return self._is_ensemble
@property @property
def date(self): def date(self):
date = self._meta_data["creation_date"] date = self._meta_data["creation_date"]
@@ -431,9 +436,11 @@ class Results(SQLSubModel):
solver_name TEXT NOT NULL, solver_name TEXT NOT NULL,
solver_type TEXT NOT NULL, solver_type TEXT NOT NULL,
study_revision INTEGER NOT NULL, study_revision INTEGER NOT NULL,
creation_data DATE NOT NULL, creation_date DATE NOT NULL,
nb_timestamps INTEGER NOT NULL, nb_timestamps INTEGER NOT NULL,
timestamps BLOB NOT NULL, timestamps BLOB NOT NULL,
seed INTEGER,
number_of_run INTEGER,
{Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()}, {Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario) PRIMARY KEY(pamhyr_id, scenario)
@@ -459,6 +466,21 @@ class Results(SQLSubModel):
cls._db_create(execute) cls._db_create(execute)
create = True create = True
if major == "0" and int(minor) <= 2:
if int(release) < 9 and not create:
execute(
"ALTER TABLE results ADD COLUMN " +
"seed INTEGER"
)
execute(
"ALTER TABLE results ADD COLUMN " +
"number_of_run INTEGER"
)
execute(
"ALTER TABLE results RENAME COLUMN " +
"creation_data TO creation_date"
)
return cls._update_submodel(execute, version, data) return cls._update_submodel(execute, version, data)
@classmethod @classmethod
@@ -471,7 +493,9 @@ class Results(SQLSubModel):
table = execute( table = execute(
"SELECT pamhyr_id, solver_name, solver_type, " + "SELECT pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_data, nb_timestamps, timestamps, " + "study_revision, creation_date, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " +
"scenario " + "scenario " +
"FROM results " + "FROM results " +
f"WHERE scenario = {scenario.id}" f"WHERE scenario = {scenario.id}"
@@ -491,6 +515,8 @@ class Results(SQLSubModel):
solver_type = next(it) solver_type = next(it)
revision = next(it) revision = next(it)
creation_date = next(it) creation_date = next(it)
seed = next(it)
number_of_run = next(it)
nb_timestamps = next(it) nb_timestamps = next(it)
timestamps_bytes = next(it) timestamps_bytes = next(it)
owner_scenario = next(it) owner_scenario = next(it)
@@ -501,6 +527,9 @@ class Results(SQLSubModel):
new_results.set("solver_type", solver_type) new_results.set("solver_type", solver_type)
new_results.set("study_revision", revision) new_results.set("study_revision", revision)
new_results.set("creation_date", creation_date) new_results.set("creation_date", creation_date)
if seed is not None:
new_results.set("seed", seed)
new_results.set("number_of_run", number_of_run)
data["result_pid"] = pid data["result_pid"] = pid
data["result"] = new_results data["result"] = new_results
@@ -593,15 +622,25 @@ class Results(SQLSubModel):
ts = sorted(self.get("timestamps")) ts = sorted(self.get("timestamps"))
sf = ">" + ''.join(itertools.repeat("d", len(ts))) sf = ">" + ''.join(itertools.repeat("d", len(ts)))
if "seed" in self._meta_data:
seed = int(self.get("seed"))
number_of_run = int(self.get("number_of_run"))
else:
seed = None
number_of_run = None
execute( execute(
"INSERT INTO " + "INSERT INTO " +
"results (pamhyr_id, solver_name, solver_type, " + "results (pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_data, " + "study_revision, creation_date, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " + "nb_timestamps, timestamps, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, solver_name, solver_type, self._pamhyr_id, solver_name, solver_type,
self._status.version, self.get("creation_date"), self._status.version, self.get("creation_date"),
len(ts), struct.pack(sf, *ts), self._owner_scenario seed, number_of_run,
len(ts), struct.pack(sf, *ts),
self._owner_scenario
) )
data["result"] = self._pamhyr_id data["result"] = self._pamhyr_id
+5 -2
View File
@@ -393,7 +393,7 @@ class Reach(SQLSubModel):
class River(SQLSubModel): class River(SQLSubModel):
_sub_classes = [Reach] _sub_classes = [Reach]
def __init__(self, study, parent): def __init__(self, study, parent, init=True):
super(River, self).__init__( super(River, self).__init__(
id=-1, status=study.status, id=-1, status=study.status,
owner_scenario=study.status.scenario.id owner_scenario=study.status.scenario.id
@@ -404,6 +404,9 @@ class River(SQLSubModel):
# Dict with timestamps as key # Dict with timestamps as key
self._reachs = [] self._reachs = []
if init:
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
def __len__(self): def __len__(self):
return len(self._reachs) return len(self._reachs)
@@ -456,7 +459,7 @@ class River(SQLSubModel):
study = data["study"] study = data["study"]
parent = data["parent"] parent = data["parent"]
new_river = cls(study, parent) new_river = cls(study, parent, init=False)
for reach in study.river.reachs(): for reach in study.river.reachs():
data["reach"] = reach.reach data["reach"] = reach.reach
+10 -1
View File
@@ -60,6 +60,7 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results from Model.Results.Results import Results
logger = logging.getLogger() logger = logging.getLogger()
@@ -476,6 +477,7 @@ class River(Graph):
D90AdisTSList, D90AdisTSList,
DIFAdisTSList, DIFAdisTSList,
GeoTIFFList, GeoTIFFList,
EnsembleList,
Results Results
] ]
@@ -515,6 +517,9 @@ class River(Graph):
self._geotiff = GeoTIFFList(status=self._status) self._geotiff = GeoTIFFList(status=self._status)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {} self._results = {}
@classmethod @classmethod
@@ -629,6 +634,8 @@ class River(Graph):
new._geotiff = GeoTIFFList._db_load(execute, data) new._geotiff = GeoTIFFList._db_load(execute, data)
new._ensembles = EnsembleList._db_load(execute, data)
return new return new
def _db_load_results(self, execute, data=None): def _db_load_results(self, execute, data=None):
@@ -664,6 +671,8 @@ class River(Graph):
objs.append(self._geotiff) objs.append(self._geotiff)
objs.append(self._ensembles)
for solv_type in self.results: for solv_type in self.results:
objs.append(self.results[solv_type]) objs.append(self.results[solv_type])
@@ -740,7 +749,7 @@ class River(Graph):
self._BoundaryConditionsAdisTS, self._BoundaryConditionsAdisTS,
self._LateralContributionsAdisTS, self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS, self._D90AdisTS, self._DIFAdisTS,
self._geotiff, self._geotiff, self._ensembles,
] ]
for solver in self._parameters: for solver in self._parameters:
+29 -12
View File
@@ -53,6 +53,7 @@ class Scenario(SQLSubModel):
"geometry_pointXYZ", "geometry_profileXYZ", "geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node", "river_reach", "river_node",
"geotiff", "reservoir", "reservoir_data", "geotiff", "reservoir", "reservoir_data",
"ensemble",
] ]
related_tables = tables_with_deleted_column + [ related_tables = tables_with_deleted_column + [
@@ -67,6 +68,7 @@ class Scenario(SQLSubModel):
name: str = "", name: str = "",
description: str = "", description: str = "",
x: int = 1000.0, y: int = 1000.0, x: int = 1000.0, y: int = 1000.0,
is_ensemble: bool = False,
revision: int = 0, revision: int = 0,
parent=None): parent=None):
super(Scenario, self).__init__() super(Scenario, self).__init__()
@@ -76,6 +78,7 @@ class Scenario(SQLSubModel):
self._x, self._y = x, y self._x, self._y = x, y
self._name = name self._name = name
self._description = description self._description = description
self._is_ensemble = is_ensemble
self._revision = revision self._revision = revision
self._parent = parent self._parent = parent
@@ -98,6 +101,7 @@ class Scenario(SQLSubModel):
y REAL NOT NULL DEFAULT 1000, y REAL NOT NULL DEFAULT 1000,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
is_ensemble BOOL DEFAULT FALSE,
revision INTEGER NOT NULL, revision INTEGER NOT NULL,
parent_id INTEGER REFERENCES scenario(id) parent_id INTEGER REFERENCES scenario(id)
) )
@@ -133,6 +137,13 @@ class Scenario(SQLSubModel):
cls._db_create(execute) cls._db_create(execute)
cls._db_add_default(execute) cls._db_add_default(execute)
if major == "0" and int(minor) == 2:
if int(release) < 8:
execute(
f"ALTER TABLE scenario " +
"ADD COLUMN is_ensemble BOOL DEFAULT FALSE"
)
return True return True
@classmethod @classmethod
@@ -153,7 +164,8 @@ class Scenario(SQLSubModel):
scenarios = {} scenarios = {}
table = execute( table = execute(
"SELECT id, x, y, name, description, revision, parent_id " + "SELECT id, x, y, name, description, " +
" is_ensemble, revision, parent_id " +
"FROM scenario " + "FROM scenario " +
"ORDER BY id ASC" "ORDER BY id ASC"
) )
@@ -165,6 +177,7 @@ class Scenario(SQLSubModel):
x, y = next(it), next(it) x, y = next(it), next(it)
name = next(it) name = next(it)
desc = next(it) desc = next(it)
is_ens = next(it)
revi = next(it) revi = next(it)
parent = next(it) parent = next(it)
@@ -174,7 +187,9 @@ class Scenario(SQLSubModel):
new = cls( new = cls(
id=id, x=x, y=y, id=id, x=x, y=y,
name=name, description=desc, name=name, description=desc,
revision=revi, parent=parent is_ensemble=is_ens,
revision=revi,
parent=parent
) )
scenarios[id] = new scenarios[id] = new
@@ -184,21 +199,18 @@ class Scenario(SQLSubModel):
if self.is_deleted(): if self.is_deleted():
return self.drop_all(execute) return self.drop_all(execute)
parent = 'NULL' parent = None
if self.parent is not None: if self.parent is not None:
parent = self.parent._id parent = self.parent._id
execute( execute(
"INSERT OR REPLACE INTO " + "INSERT OR REPLACE INTO " +
"scenario(id, x, y, name, description, revision, parent_id) " + "scenario(id, x, y, name, description, " +
"VALUES (" + " is_ensemble, revision, parent_id) " +
f"{self._id}, " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
f"{self.x}, {self.y}, " + self._id, self.x, self.y,
f"'{self._db_format(self.name)}', " + self.name, self.description, self._is_ensemble,
f"'{self._db_format(self.description)}', " + self._revision, parent
f"{self._revision}, " +
f"{parent}" +
")"
) )
return True return True
@@ -323,6 +335,9 @@ class Scenario(SQLSubModel):
def parent(self): def parent(self):
return self._parent return self._parent
def is_ensemble(self):
return self._is_ensemble
def set_pos(self, x, y): def set_pos(self, x, y):
self._x = x self._x = x
self._y = y self._y = y
@@ -340,6 +355,8 @@ class Scenario(SQLSubModel):
return self.name return self.name
if key == "description": if key == "description":
return self.description return self.description
if key == "is_ensemble":
return self.is_ensemble()
if key == "parent": if key == "parent":
return self.parent return self.parent
+5 -2
View File
@@ -57,8 +57,11 @@ class Scenarios(PamhyrModelDict):
return None return None
def new(self, parent): def new(self, parent, is_ensemble=False):
new = Scenario(parent=parent) new = Scenario(
parent=parent,
is_ensemble=is_ensemble
)
self.set(new._id, new) self.set(new._id, new)
return new return new
+56 -4
View File
@@ -33,6 +33,7 @@ from Model.Status import StudyStatus
from Model.Except import NotImplementedMethodeError from Model.Except import NotImplementedMethodeError
from Model.River import River from Model.River import River
from Model.Geometry.Reach import Reach from Model.Geometry.Reach import Reach
from Model.Ensembles.DistributionList import DistributionList
from Model.HydraulicStructures.HydraulicStructures import ( from Model.HydraulicStructures.HydraulicStructures import (
HydraulicStructure HydraulicStructure
) )
@@ -46,10 +47,11 @@ logger = logging.getLogger()
class Study(SQLModel): class Study(SQLModel):
_version = "0.2.7" _version = "0.2.9"
_sub_classes = [ _sub_classes = [
Scenario, Scenario,
DistributionList,
River, River,
] ]
@@ -84,6 +86,9 @@ class Study(SQLModel):
self.scenarios = Scenarios(status=self.status) self.scenarios = Scenarios(status=self.status)
self.scenarios[0] = s0 self.scenarios[0] = s0
self.status.scenario = s0 self.status.scenario = s0
self._ens_distributions = DistributionList(status=self.status)
self._river = River(status=self.status) self._river = River(status=self.status)
else: else:
self._init_db_file(filename, is_new=False) self._init_db_file(filename, is_new=False)
@@ -388,12 +393,19 @@ class Study(SQLModel):
lambda a, s: a or (s.parent is scenario), lambda a, s: a or (s.parent is scenario),
new.scenarios.lst, new.scenarios.lst,
False False
): ) or scenario.is_ensemble():
new.status.set_as_read_only() new.status.set_as_read_only()
data["scenario"] = scenario data["scenario"] = scenario
data["loaded_pid"] = set() data["loaded_pid"] = set()
# Get ensemble distribution
new._ens_distributions = DistributionList._db_load(
sql_exec, data=data
)
data["ens_distributions"] = new._ens_distributions
# Load river data # Load river data
new._river = River._db_load( new._river = River._db_load(
sql_exec, data=data sql_exec, data=data
@@ -454,8 +466,27 @@ class Study(SQLModel):
) )
progress() progress()
if self.status.scenario.is_ensemble():
lst = [
self.scenarios, self._ens_distributions,
self._river._ensembles,
]
for solv in self._river._results:
lst.append(self._river._results[solv])
self._save_submodel(
lst, data=progress
)
progress()
self.commit()
return
self._save_submodel( self._save_submodel(
[self.scenarios, self._river], [
self.scenarios, self._ens_distributions,
self._river
],
data=progress data=progress
) )
@@ -573,7 +604,8 @@ class Study(SQLModel):
data = { data = {
"status": self.status, "status": self.status,
"loaded_pid": set(), "loaded_pid": set(),
"scenario": scenario "scenario": scenario,
"ens_distributions": self._ens_distributions,
} }
self.status.start_loading() self.status.start_loading()
@@ -647,6 +679,7 @@ class Study(SQLModel):
self.status.scenario.y + 100) self.status.scenario.y + 100)
new.name = self.status.scenario.name + " (copy)" new.name = self.status.scenario.name + " (copy)"
new.is_ensemble = self.status.scenario.is_ensemble
self.status.scenario = new self.status.scenario = new
self.river._data_traversal( self.river._data_traversal(
@@ -658,6 +691,25 @@ class Study(SQLModel):
self.status.set_as_editable() self.status.set_as_editable()
return new return new
def create_ensemble_scenario(self, pos=None, switch=True):
new = self.scenarios.new(
self.status.scenario,
is_ensemble=True
)
new.name = self.status.scenario.name + " (ensemble)"
if pos is not None:
new.set_pos(pos.x(), pos.y())
else:
new.set_pos(self.status.scenario.x + 100,
self.status.scenario.y)
if switch:
self.status.scenario = new
self.status.set_as_editable()
return new
@property @property
def results(self): def results(self):
return self._river.results return self._river.results
+7
View File
@@ -185,6 +185,13 @@ class SQLSubModel(PamhyrID):
else: else:
self._owner_scenario = owner_scenario self._owner_scenario = owner_scenario
def __str__(self):
t = type(self)
return (
f"'{t.__module__}.{t.__name__}" +
f"@{self._pamhyr_id}.{self._owner_scenario}'"
)
def must_be_saved(self): def must_be_saved(self):
"""Return True if this object MUST be save in the save file. """Return True if this object MUST be save in the save file.
+235 -1
View File
@@ -1027,6 +1027,75 @@ class Mage8(Mage):
logger_exception(e) logger_exception(e)
return False return False
def export_ensemble_variation(self, study, repertory,
qlog=None, name="0",
samples=None):
self._study = study
name = study.name.replace(" ", "_")
var_samples = self._export_ensemble_RUG(
study, repertory,
qlog, name=name,
samples=samples,
)
return var_samples
def _export_ensemble_RUG(self, study, repertory,
qlog, name="0",
samples={}):
files = []
var_samples = []
for ens in samples:
if ens.target_data is None:
continue
x = next(samples[ens])
var_samples.append(x)
qlog.put(f"*** Variation " +
f"'{ens.target_data}' - {ens.data_type} x {x}")
if ens.data_type == "strickler_minor":
ens.target_data.minor = x
elif ens.data_type == "strickler_medium":
ens.target_data.medium = x
if qlog is not None:
qlog.put("Export variation of RUG file")
# Write header
with mage_file_open(os.path.join(repertory, f"{name}.RUG"), "w+") as f:
files.append(f"{name}.RUG")
edges = study.river.enable_edges()
id = 1
for edge in edges:
frictions = edge.frictions
for friction in frictions.frictions:
if friction.begin_strickler is None:
continue
if friction.is_deleted():
continue
num = f"{id:>3}"
brk = f"{friction.begin_rk:>10.3f}"
erk = f"{friction.end_rk:>10.3f}"
strickler = friction.begin_strickler
coef_1 = f"{strickler.minor:>10.3f}"
coef_2 = f"{strickler.medium:>10.3f}"
f.write(f"K{num} {brk}{erk}{coef_1}{coef_2}\n")
id += 1
return var_samples
########### ###########
# RESULTS # # RESULTS #
########### ###########
@@ -1091,7 +1160,7 @@ class Mage8(Mage):
for i in range(nb_reach): for i in range(nb_reach):
# Add results reach to reach list # Add results reach to reach list
r = results.river.add(i) r = results.river.reach(i)
reachs.append(r) reachs.append(r)
# ID of first and last reach profiles # ID of first and last reach profiles
@@ -1451,6 +1520,171 @@ class Mage8(Mage):
return results return results
@timer
def read_ensemble_bin(self, study, fname, tables, run_number, qlog=None):
logger.info(f"read_ensemble_bin: Start reading '{fname}' ...")
start = time.time()
with mage_file_open(fname, "r") as f:
def newline(): return np.fromfile(f, dtype=np.int32, count=1)
def endline(): return np.fromfile(f, dtype=np.int32, count=1)
def read_int(size): return np.fromfile(
f, dtype=np.int32, count=size)
def read_float(size): return np.fromfile(
f, dtype=np.float32, count=size)
def read_float64(size): return np.fromfile(
f, dtype=np.float64, count=size)
# Meta data (1st line)
newline()
data = read_int(3)
nb_reach = data[0]
nb_profile = data[1]
mage_version = data[2]
if mage_version <= 80:
msg = (
"Read BIN files: " +
f"Possible incompatible mage version '{mage_version}', " +
"please check your solver configuration..."
)
logger.warning(msg)
if qlog is not None:
qlog.put("[WARNING] " + msg)
endline()
# Reach information (2nd line)
newline()
_ = read_int(2*nb_reach)
endline()
# X (3rd line)
newline()
_ = read_float(nb_profile)
endline()
# Z and Y (4th line)
newline()
_ = read_float(3*nb_profile)
endline()
# Data
newline()
ts = set()
tmp_table = {}
end = False
while not end:
n = read_int(1)[0]
timestamp = read_float64(1)[0]
key = bytearray(
np.fromfile(
f, dtype=np.byte, count=1
)
).decode()
data = read_float(n)
if key not in tmp_table:
tmp_table[key] = []
tmp_table[key].append(data)
ts.add(timestamp)
endline()
end = newline().size <= 0
table = {}
for k in tmp_table:
table[k] = np.array(tmp_table[k])
self.read_ensemble_bin_agregate_data(
table, tables, run_number
)
tables["timestamps"] = ts
logger.info(f"reading time: '{time.time() - start}'")
def read_ensemble_bin_agregate_data(self, tables, res, run_number):
z = tables["Z"]
q = tables["Q"]
if "z_mean" not in res:
res["z_mean"] = z
res["q_mean"] = q
res["z_tmp_M2"] = np.zeros(z.shape)
res["q_tmp_M2"] = np.zeros(q.shape)
res["z_std_derivation"] = np.zeros(z.shape)
res["q_std_derivation"] = np.zeros(q.shape)
res["z_min"] = z
res["z_max"] = z
res["q_min"] = q
res["q_max"] = q
else:
# Get mean results for Z and Q
n = run_number
for x, txt in [(z, "z"), (q, "q")]:
t_mean = f"{txt}_mean"
t_M2 = f"{txt}_tmp_M2"
t_std_deriv = f"{txt}_std_derivation"
delta = x - res[t_mean]
res[t_mean] += delta / n
delta_2 = x - res[t_mean]
res[t_M2] += delta * delta_2
res[t_std_deriv] = np.sqrt(res[t_M2] / n)
# Z and Q min max values
res["z_min"] = np.minimum(z, res["z_min"])
res["z_max"] = np.maximum(z, res["z_max"])
res["q_min"] = np.minimum(q, res["q_min"])
res["q_max"] = np.maximum(q, res["q_max"])
@timer
def results_ensemble(self, study, repertory,
tables, run_number,
qlog=None, name=None):
self._study = study
if name is None:
name = study.name.replace(" ", "_")
fname = os.path.join(repertory, f"{name}.BIN".replace(" ", "_"))
if not os.path.isfile(fname):
logger.info(f"Result file {name}.BIN does not exist")
return None
try:
self.read_ensemble_bin(study, fname, tables, run_number, qlog)
except Exception as e:
logger.error(f"Failed to read results")
logger_exception(e)
return None
return True
def get_ensemble_empty_results(self, study, repertory, name=None):
return Results(
study=study,
solver=self,
repertory=repertory,
name=name,
is_ensemble=True,
)
class MageFake7(Mage8): class MageFake7(Mage8):
_type = "mage_fake7" _type = "mage_fake7"
+10 -32
View File
@@ -143,43 +143,21 @@ class TableModel(PamhyrTableModel):
self.update() self.update()
def delete(self, rows, parent=QModelIndex()): def delete(self, rows, parent=QModelIndex()):
if not rows: self.beginRemoveRows(parent, rows[0], rows[-1])
return
rows = sorted(set(rows)) rows = list(map(
rows = [ lambda r: self.get_true_data_row(r),
self.get_true_data_row(row) rows))
for row in rows
]
# A multiple selection may contain non-contiguous rows. A model reset self._undo.push(
# accurately represents this atomic undo command, unlike one DelCommand(
# beginRemoveRows() call spanning rows that are not all removed. self._data, rows
self.beginResetModel()
try:
self._undo.push(
DelCommand(
self._data, rows
)
) )
finally: )
self.endResetModel()
self.endRemoveRows()
self.update() self.update()
def undo(self):
self.beginResetModel()
try:
self._undo.undo()
finally:
self.endResetModel()
def redo(self):
self.beginResetModel()
try:
self._undo.redo()
finally:
self.endResetModel()
def sort(self, _reverse, parent=QModelIndex()): def sort(self, _reverse, parent=QModelIndex()):
self.layoutAboutToBeChanged.emit() self.layoutAboutToBeChanged.emit()
+18 -13
View File
@@ -97,11 +97,12 @@ class DelCommand(QUndoCommand):
QUndoCommand.__init__(self) QUndoCommand.__init__(self)
self._data = data self._data = data
self._rows = sorted(set(rows)) self._rows = rows
self._bc = [
self._data.get_i(row) self._bc = []
for row in self._rows for row in rows:
] self._bc.append(self._data.get_i(row))
self._bc.sort()
def undo(self): def undo(self):
for el in self._bc: for el in self._bc:
@@ -119,17 +120,13 @@ class SortCommand(QUndoCommand):
self._data = data self._data = data
self._reverse = _reverse self._reverse = _reverse
# Keep the order of the complete underlying list, including soft self._old = self._data.data
# deleted items. ``data`` only exposes visible items and therefore self._indexes = None
# cannot reliably restore a sort after a delete/undelete operation.
self._old_positions = {
id(item): index
for index, item in enumerate(self._data._data)
}
def undo(self): def undo(self):
ll = self._data.data
self._data.sort( self._data.sort(
key=lambda item: self._old_positions[id(item)] key=lambda x: self._indexes[ll.index(x)]
) )
def redo(self): def redo(self):
@@ -137,6 +134,14 @@ class SortCommand(QUndoCommand):
_reverse=self._reverse, _reverse=self._reverse,
key=lambda x: x[0] key=lambda x: x[0]
) )
if self._indexes is None:
self._indexes = list(
map(
lambda p: self._old.index(p),
self._data.data
)
)
self._old = None
class MoveCommand(QUndoCommand): class MoveCommand(QUndoCommand):
+15
View File
@@ -16,6 +16,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import psutil
import logging import logging
import traceback import traceback
@@ -120,7 +122,10 @@ class TimerWindow(PamhyrWindow):
parent=parent parent=parent
) )
self._process = psutil.Process(os.getpid())
self.setup_table() self.setup_table()
self.setup_label()
self.setup_alarm() self.setup_alarm()
self.setup_connections() self.setup_connections()
@@ -136,6 +141,15 @@ class TimerWindow(PamhyrWindow):
data=None, data=None,
) )
def setup_label(self):
label = self.find(QLabel, "label")
label.setText(
f" Memory usage:\t" +
"<b>" +
f"{self._process.memory_info().rss / (1000 * 1000):.3f}" +
"</b> MB"
)
def setup_alarm(self): def setup_alarm(self):
self._alarm = QTimer() self._alarm = QTimer()
self._alarm.start(2000) self._alarm.start(2000)
@@ -148,6 +162,7 @@ class TimerWindow(PamhyrWindow):
def update(self): def update(self):
self._table.update_lst() self._table.update_lst()
self.setup_label()
# def reset(self): # def reset(self):
# self._table.reset_timers() # self._table.reset_timers()
+299
View File
@@ -0,0 +1,299 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow, PamhyrDialog
from View.Tools.PamhyrWidget import PamhyrWidget
from View.Tools.PamhyrTree import (
PamhyrModelItem, PamhyrModelListItem,
)
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QSettings,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QGroupBox, QTreeWidget,
)
logger = logging.getLogger()
class StricklersCoefModelItem(PamhyrModelItem):
def get_value(self):
return (self._data, self.parent()._data)
class StricklersModelItem(PamhyrModelItem):
def __init__(self, data=None, trad=None, parent=None):
super(StricklersModelItem, self).__init__(
data=data, trad=trad, parent=parent,
)
def _setup_children(self):
for coef in ["strickler_minor", "strickler_medium"]:
self.addChild(
StricklersCoefModelItem(data=coef, parent=self)
)
def get_value(self):
return self._data
class EnsembleEditDialog(PamhyrDialog):
_pamhyr_ui = "EnsembleEditDialog"
_pamhyr_name = "Parameters"
def __init__(self, study=None, ensemble=None,
trad=None, parent=None):
super(EnsembleEditDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._study = study
self._ensemble = ensemble
self._target_data = None
self._tree = self.find(QTreeWidget, "treeWidget_target_data")
self._tg_group_box = self.find(QGroupBox, "groupBox_target_data")
self._init_tree()
self._init_default_values()
self._init_connection()
def _init_tree(self):
self._tree.setColumnCount(1)
self._tree.setHeaderLabels(["Name"])
stricklers = PamhyrModelListItem(
name="Stricklers",
lst=self._study.river.stricklers,
child_item_ctor=StricklersModelItem,
parent=self._tree
)
self._target_data = (
self._ensemble.data_type,
self._ensemble.target_data
)
self._tg_group_box.setTitle(
self._format_target_data(self._target_data)
)
item = stricklers.find_item_by_value(self._target_data)
if item:
self._tree.setCurrentItem(item)
def _init_default_values(self):
self.set_line_edit_text("lineEdit_name", self._ensemble.name)
self.combobox_add_items(
"comboBox_distribution",
[self._trad["not_defined"]] +
list(
map(
lambda d: d.name,
self._study._ens_distributions.lst
)
)
)
self._params_widget = None
if self._ensemble.distribution is not None:
self.set_combobox_text("comboBox_distribution",
self._ensemble.distribution.name)
self._set_widget(self._ensemble.distribution)
def _init_connection(self):
self.find(QComboBox, "comboBox_distribution")\
.currentTextChanged.connect(self.update_distribution)
self._tree.itemClicked.connect(self._tree_item_select)
def _tree_item_select(self, item):
if type(item) is StricklersCoefModelItem:
self._target_data = item.get_value()
self._tg_group_box.setTitle(
self._format_target_data(self._target_data)
)
def _format_target_data(self, data):
return f" Selection: {str(data[1])}, {data[0]}"
def update_distribution(self, txt):
# txt = self.get_combobox_text("comboBox_distribution")
distribution = next(
filter(
lambda d: d.name == txt,
self._study._ens_distributions.lst
),
None
)
self._set_widget(distribution)
def _set_widget(self, distribution):
box = self.find(QGroupBox, "groupBox_params")
vbox = self.find(QVBoxLayout, "verticalLayout_params")
if self._params_widget is not None:
vbox.removeWidget(self._params_widget)
self._params_widget.setParent(None)
if distribution is None:
return
size = len(distribution._labels)
if size == 1:
self._params_widget = Ensemble1ParamsWidget(
ensemble=self._ensemble,
distribution=distribution,
trad=self._trad, parent=box,
)
vbox.addWidget(self._params_widget)
elif size == 2:
self._params_widget = Ensemble2ParamsWidget(
ensemble=self._ensemble,
distribution=distribution,
trad=self._trad, parent=box,
)
vbox.addWidget(self._params_widget)
else:
self._params_widget = None
@property
def values(self):
return self._values
def accept(self):
txt = self.get_combobox_text("comboBox_distribution")
distribution = next(
filter(
lambda d: d.name == txt,
self._study._ens_distributions.lst
),
None
)
self._values = {
"name": self.get_line_edit_text("lineEdit_name"),
"data_type": self._target_data[0],
"target_data": self._target_data[1],
"distribution": distribution,
"params": self._params_widget.values,
}
super().accept()
def reject(self):
self.close()
class Ensemble1ParamsWidget(PamhyrWidget):
_pamhyr_ui = "Ensemble1ParamsWidget"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
distribution=None,
trad=None, parent=None):
super(Ensemble1ParamsWidget, self).__init__(
parent=parent
)
self._trad = trad
self._ensemble = ensemble
self._distribution = distribution
self._labels = distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) > 0:
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
@property
def values(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
]
return self._values
class Ensemble2ParamsWidget(PamhyrWidget):
_pamhyr_ui = "Ensemble2ParamsWidget"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
distribution=None,
trad=None, parent=None):
super(Ensemble2ParamsWidget, self).__init__(
parent=parent
)
self._trad = trad
self._ensemble = ensemble
self._distribution = distribution
self._labels = distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
self.set_label_text("label_1", self._trad[self._labels[1]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) > 0:
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
self.set_double_spin_box("doubleSpinBox_1", self._values[1])
@property
def values(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
self.get_double_spin_box("doubleSpinBox_1")
]
return self._values
+268
View File
@@ -0,0 +1,268 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
)
from View.Ensembles.UndoCommand import (
SetNameCommand, SetTypeCommand, SetDataCommand,
SetDistributionCommand,
AddCommand, DelCommand,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.Ensembles.Translate import *
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, study=None,
mode="stricklers", trad=None, parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._study = study
self._trad = trad
self._mode = mode
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
if self._mode == "data_type":
self.editor.addItems(
[
self._trad["not_defined"],
self._trad["strickler_minor"],
self._trad["strickler_medium"],
]
)
elif self._mode == "stricklers":
used = list(
map(
lambda e: (e.data_type, e._target_data),
self._data.lst
)
)
current = self._data.get(index.row())
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda s: str(s),
filter(
# Exclude target data already used for
# this data type
lambda s: (
current.target_data == s or
(current.data_type, s) not in used
),
self._study.river.stricklers.stricklers
)
)
)
)
elif self._mode == "distribution":
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda f: f.name,
self._study._ens_distributions.lst
)
)
)
self.editor.setCurrentText(index.data(Qt.DisplayRole))
return self.editor
def setEditorData(self, editor, index):
value = index.data(Qt.DisplayRole)
self.editor.currentTextChanged.connect(self.currentItemChanged)
def setModelData(self, editor, model, index):
text = str(editor.currentText())
if self._mode == "data_type":
value = next(
filter(
lambda x: self._trad[x] == text,
["strickler_minor", "strickler_medium"],
),
""
)
elif self._mode == "stricklers":
value = next(
filter(
lambda s: str(s) == text,
self._study.river.stricklers.stricklers
),
None
)
elif self._mode == "distribution":
value = next(
filter(
lambda f: f.name == text,
self._study._ens_distributions.lst
),
None
)
else:
value = text
model.setData(index, value)
editor.close()
editor.deleteLater()
def updateEditorGeometry(self, editor, option, index):
r = QRect(option.rect)
if self.editor.windowFlags() & Qt.Popup:
if editor.parent() is not None:
r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
editor.setGeometry(r)
@pyqtSlot()
def currentItemChanged(self):
self.commitData.emit(self.sender())
class EnsembleTableModel(PamhyrTableModel):
def _setup_lst(self):
self._lst = self._data
self._study = self._opt_data
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
if self._headers[column] == "name":
return self._lst.get(row).name
elif self._headers[column] == "type":
return self._trad[self._lst.get(row).data_type]
elif self._headers[column] == "target_data":
value = self._lst.get(row).target_data
if value is None:
return self._trad["not_defined"]
return str(value)
elif self._headers[column] == "distribution":
value = self._lst.get(row).distribution
if value is None:
return self._trad["not_defined"]
return value.name
elif self._headers[column] == "parameters":
value = self._lst.get(row).params
return str(value)
return QVariant()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
try:
if self._headers[column] == "name":
self._undo.push(
SetNameCommand(
self._lst, row, value
)
)
elif self._headers[column] == "type":
self._undo.push(
SetTypeCommand(
self._lst, row, value
)
)
elif self._headers[column] == "target_data":
self._undo.push(
SetDataCommand(
self._lst, row, value
)
)
elif self._headers[column] == "distribution":
self._undo.push(
SetDistributionCommand(
self._lst, row, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
self._undo.push(
AddCommand(
self._lst, row,
)
)
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
self._undo.push(
DelCommand(
self._lst, rows
)
)
self.endRemoveRows()
self.layoutChanged.emit()
def undo(self):
self._undo.undo()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self.layoutChanged.emit()
+50
View File
@@ -0,0 +1,50 @@
# Translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class EnsemblesTranslate(MainTranslate):
def __init__(self):
super(EnsemblesTranslate, self).__init__()
self._dict["Ensembles"] = _translate(
"Ensembles", "Ensembles"
)
self._dict["ensemble"] = _translate(
"Ensembles", "Ensembles scenario"
)
self._dict["strickler_minor"] = _translate(
"Ensembles", "Stricklers minor"
)
self._dict["strickler_medium"] = _translate(
"Ensembles", "Stricklers medium"
)
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"type": _translate("Ensembles", "Type"),
"target_data": _translate("Ensembles", "Target data"),
"distribution": _translate("Ensembles", "Distribution"),
"parameters": _translate("Ensembles", "Parameters"),
}
+150
View File
@@ -0,0 +1,150 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.Friction.Friction import Friction
from Model.Friction.FrictionList import FrictionList
class SetNameCommand(QUndoCommand):
def __init__(self, ensembles, index, new_value):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index).name
self._new = str(new_value)
def undo(self):
self._ensembles.get(self._index).name = self._old
def redo(self):
self._ensembles.get(self._index).name = self._new
class SetTypeCommand(QUndoCommand):
def __init__(self, ensembles, index, new_value):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index)\
.data_type
self._new = new_value
def undo(self):
self._ensembles.get(self._index).data_type = self._old
def redo(self):
self._ensembles.get(self._index).data_type = self._new
class SetDataCommand(QUndoCommand):
def __init__(self, ensembles, index, data):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index)\
.target_data
self._new = data
def undo(self):
self._ensembles.get(self._index).target_data = self._old
def redo(self):
self._ensembles.get(self._index).target_data = self._new
class SetDistributionCommand(QUndoCommand):
def __init__(self, ensembles, index, distribution):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index).distribution
self._old_params = self._ensembles.get(self._index).params
self._new = distribution
def undo(self):
self._ensembles.get(self._index).distribution = self._old
self._ensembles.get(self._index).params = self._old_params
def redo(self):
self._ensembles.get(self._index).distribution = self._new
if self._new is not None:
self._ensembles.get(self._index).params = self._new.default_values
class AddCommand(QUndoCommand):
def __init__(self, ensembles, index):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._new = None
def undo(self):
self._ensembles.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._ensembles.new(self._index)
else:
self._ensembles.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, ensembles, rows):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._rows = rows
self._friction = []
for row in rows:
self._friction.append((row, self._ensembles.get(row)))
self._friction.sort()
def undo(self):
for row, el in self._friction:
self._ensembles.insert(row, el)
def redo(self):
self._ensembles.delete_i(self._rows)
class SetAllCommand(QUndoCommand):
def __init__(self, ensemble, data):
QUndoCommand.__init__(self)
self._ensemble = ensemble
self._old = self._ensemble.get_data_dict()
self._new = data
def undo(self):
self._ensemble.set_data_dict(self._old)
def redo(self):
self._ensemble.set_data_dict(self._new)
+290
View File
@@ -0,0 +1,290 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow, PamhyrDialog
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QSettings,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
)
from Modules import Modules
from View.Ensembles.Edit.Window import EnsembleEditDialog
from View.Ensembles.Table import (
EnsembleTableModel, ComboBoxDelegate
)
from View.Ensembles.Translate import EnsemblesTranslate
from View.Ensembles.UndoCommand import SetAllCommand
logger = logging.getLogger()
class EnsemblesWindow(PamhyrWindow):
_pamhyr_ui = "Ensembles"
_pamhyr_name = "Ensembles"
def __init__(self, ensembles=None,
study=None, config=None,
parent=None):
trad = EnsemblesTranslate()
self._ensembles = ensembles
name = (
trad[self._pamhyr_name] + " - " +
study.name
)
super(EnsemblesWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self.setup_table()
self.setup_connections()
def setup_table_delegate(self):
self._delegate_stricklers = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="stricklers",
trad=self._trad,
parent=self
)
self._delegate_data_type = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="data_type",
trad=self._trad,
parent=self
)
self._delegate_distribution = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="distribution",
trad=self._trad,
parent=self
)
return {
"type": self._delegate_data_type,
"target_data": self._delegate_stricklers,
"distribution": self._delegate_distribution,
}
def setup_table(self):
self._table = {}
delegates = self.setup_table_delegate()
editable_headers = [
"name",
]
table = self.find(QTableView, f"tableView")
self._table = EnsembleTableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates=delegates,
data=self._ensembles,
trad=self._trad,
undo=self._undo_stack,
opt_data=self._study
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_connections(self):
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
table = self.find(QTableView, f"tableView")
table.selectionModel()\
.selectionChanged\
.connect(self.update)
self._table.dataChanged\
.connect(self.update)
def index_selected_rows(self):
table = self.find(QTableView, f"tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._ensembles) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def edit(self):
rows = self.index_selected_rows()
for row in rows:
try:
ens = self._ensembles.get(row)
dlg = EnsembleEditDialog(
study=self._study,
ensemble=ens,
trad=self._trad,
parent=self
)
if dlg.exec():
self._undo_stack.push(
SetAllCommand(
ens, dlg.values
)
)
except Exception as e:
logger_exception(e)
return
class Ensemble1ParamsDialog(PamhyrDialog):
_pamhyr_ui = "Ensemble1ParamsDialog"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
trad=None, parent=None):
super(Ensemble1ParamsDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._ensemble = ensemble
self._labels = ensemble.distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) > 0:
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
@property
def values(self):
return self._values
def accept(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
]
super().accept()
def reject(self):
self.close()
class Ensemble2ParamsDialog(PamhyrDialog):
_pamhyr_ui = "Ensemble2ParamsDialog"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
trad=None, parent=None):
super(Ensemble2ParamsDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._ensemble = ensemble
self._labels = ensemble.distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
self.set_label_text("label_1", self._trad[self._labels[1]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) > 0:
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
self.set_double_spin_box("doubleSpinBox_1", self._values[1])
@property
def values(self):
return self._values
def accept(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
self.get_double_spin_box("doubleSpinBox_1")
]
super().accept()
def reject(self):
self.close()
-3
View File
@@ -259,9 +259,6 @@ class GeometryReachTableModel(PamhyrTableModel):
**data **data
) )
self.apply_meshing(new_profiles, data, tableView)
def apply_meshing(self, new_profiles, data, tableView):
if new_profiles is None: if new_profiles is None:
return return
+2 -10
View File
@@ -350,22 +350,14 @@ class GeometryWindow(PamhyrWindow):
tableview = self.tableView tableview = self.tableView
def fn(): def fn():
return mesher.meshing(self._reach, **data) self._table.meshing(mesher, data, tableview)
# self._table.meshing(mesher, data, tableview)
dlg2 = WaitingDialog( dlg2 = WaitingDialog(
payload_fn=fn, payload_fn=fn,
title="waiting_mesh", title="waiting_mesh",
parent=self parent=self
) )
dlg2.exec_() dlg2.exec_()
if dlg2.worker_exception is not None:
raise dlg2.worker_exception
# Models, undo stacks and selections belong to the GUI thread.
self._table.apply_meshing(
dlg2.worker_result, data, tableview
)
self._table.update() self._table.update()
except Exception as e: except Exception as e:
+3 -4
View File
@@ -96,14 +96,13 @@ class ComboBoxDelegate(QItemDelegate):
reach = self._data.hydraulic_structures\ reach = self._data.hydraulic_structures\
.get(index.row())\ .get(index.row())\
.input_reach .input_reach
profiles = list( value = next(
filter( filter(
lambda p: p.display_name() == text, lambda p: p.display_name() == text,
reach.reach.profiles reach.reach.profiles
) ),
None
) )
value = profiles[0] if len(profiles) > 0 else None
else: else:
value = text value = text
+80 -1
View File
@@ -66,6 +66,7 @@ from View.Configure.Window import ConfigureWindow
from View.Study.Window import NewStudyWindow from View.Study.Window import NewStudyWindow
from View.About.Window import AboutWindow from View.About.Window import AboutWindow
from View.Scenarios.Window import ScenariosWindow from View.Scenarios.Window import ScenariosWindow
from View.Ensembles.Window import EnsemblesWindow
from View.Network.Window import NetworkWindow from View.Network.Window import NetworkWindow
from View.Geometry.Window import GeometryWindow from View.Geometry.Window import GeometryWindow
from View.BoundaryCondition.Window import BoundaryConditionWindow from View.BoundaryCondition.Window import BoundaryConditionWindow
@@ -82,11 +83,14 @@ from View.GeoTIFF.Window import GeoTIFFListWindow
from View.REPLines.Window import REPLineListWindow from View.REPLines.Window import REPLineListWindow
from View.SolverParameters.Window import SolverParametersWindow from View.SolverParameters.Window import SolverParametersWindow
from View.RunSolver.Window import ( from View.RunSolver.Window import (
EnsembleRunNumberDialog,
SelectSolverWindow, SelectSolverWindow,
SolverLogWindow, SolverLogWindow,
SolverLogEnsWindow,
) )
from View.Results.Window import ResultsWindow
from View.Results.Window import ResultsWindow, ResultsEnsembleWindow
from View.Results.CompareDialog import ( from View.Results.CompareDialog import (
CompareScenariosWindow CompareScenariosWindow
) )
@@ -1222,6 +1226,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
""" """
if self._study is None: if self._study is None:
return return
if self._study.filename is None or self._study.filename == "": if self._study.filename is None or self._study.filename == "":
self.message_box( self.message_box(
window_title=self._trad["Warning"], window_title=self._trad["Warning"],
@@ -1255,6 +1260,29 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self.scenarios = ScenariosWindow(study=self._study, parent=self) self.scenarios = ScenariosWindow(study=self._study, parent=self)
self.scenarios.show() self.scenarios.show()
def open_ensemble_scenario(self):
"""Open ensemble scenario window
Returns:
Nothing
"""
if self._study is None:
return
if self.sub_window_exists(
EnsemblesWindow,
data=[self._study, self.conf]
):
return
ensemble = EnsemblesWindow(
ensembles=self._study.river._ensembles,
study=self._study,
config=self.conf,
parent=self
)
ensemble.show()
def open_geometry(self): def open_geometry(self):
"""Open geometry window """Open geometry window
@@ -1562,6 +1590,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
sol.show() sol.show()
def solver_log(self, solver): def solver_log(self, solver):
if self._study.status.scenario.is_ensemble():
return self._solver_log_ensemble(solver)
sol = SolverLogWindow( sol = SolverLogWindow(
study=self._study, study=self._study,
config=self.conf, config=self.conf,
@@ -1570,6 +1601,28 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
) )
sol.show() sol.show()
def _solver_log_ensemble(self, solver):
run_number = 1
dlg = EnsembleRunNumberDialog(
trad=self._trad,
parent=self
)
if dlg.exec():
function = dlg.function
number_of_run = dlg.number
seed = dlg.seed
sol = SolverLogEnsWindow(
number_of_run=number_of_run,
seed=seed, function=function,
study=self._study,
config=self.conf,
solver=solver,
parent=self
)
sol.show()
def msg_box_results_continue(self): def msg_box_results_continue(self):
dlg = QMessageBox(self) dlg = QMessageBox(self)
@@ -1646,6 +1699,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if not self.msg_box_results_continue(): if not self.msg_box_results_continue():
return return
if self._study.status.scenario.is_ensemble():
self.open_solver_ens_results_window(solver, results)
else:
self.open_solver_results_window(solver, results)
def open_solver_results_window(self, solver, results):
# Windows already opened # Windows already opened
if self.sub_window_exists( if self.sub_window_exists(
ResultsWindow, ResultsWindow,
@@ -1665,6 +1724,26 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
) )
res.show() res.show()
def open_solver_ens_results_window(self, solver, results):
# Windows already opened
if self.sub_window_exists(
ResultsEnsembleWindow,
data=[
self._study,
None, # No config,
results._solver,
results._repertory,
results._name]
):
return
res = ResultsEnsembleWindow(
study=self._study,
results=[results],
parent=self
)
res.show()
def msg_open_results_no_results(self): def msg_open_results_no_results(self):
self.message_box( self.message_box(
window_title=self._trad["Warning"], window_title=self._trad["Warning"],
+261
View File
@@ -0,0 +1,261 @@
# PlotHEnsemble.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from functools import reduce
from datetime import datetime
from tools import timer, trace, logger_exception
from View.Tools.PamhyrPlot import PamhyrPlot
from PyQt5.QtCore import (
QCoreApplication
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class PlotHEnsemble(PamhyrPlot):
def __init__(self, canvas=None, trad=None, toolbar=None,
results=None, reach_id=0, profile_id=[0], res_id=[0],
parent=None):
super(PlotHEnsemble, self).__init__(
canvas=canvas,
trad=trad,
data=results,
toolbar=toolbar,
parent=parent
)
self._mode = "time"
self._parent = parent
self._current_reach_id = reach_id
self._current_profile_id = profile_id
self._current_res_id = res_id
self._timestamps = parent._timestamps
self._current_timestamp = max(self._timestamps)
self.label_x = self._trad["unit_time_s"]
self.label_y = self._trad["unit_discharge"]
self.label_discharge = _translate("Results", "Cross-section discharge")
self.label_discharge_max = _translate("Results", "Max discharge")
self.label_timestamp = _translate("Results", "Current timestamp")
self._isometric_axis = False
# self._auto_relim_update = False
# self._autoscale_update = False
self._line_max = []
self._line = []
self._add_data_lines = []
@property
def results(self):
return self.data
@results.setter
def results(self, results):
self.data = results
self._current_timestamp = max(self._timestamps)
@timer
def draw(self, highlight=None):
self.canvas.axes.cla()
self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
if self.results is None:
return
if len(self._current_res_id) < 1:
return
for res_id in self._current_res_id:
if self.results[res_id] is None:
continue
results = self.results[res_id]
reach = results.river.reach(self._current_reach_id)
if reach.geometry.number_profiles == 0:
self._init = False
return
self.draw_data(res_id)
self.draw_data_std_derivation(res_id)
self.canvas.axes.set_xlabel(
self._trad["unit_time_s"],
color='black', fontsize=10
)
self.canvas.axes.set_ylabel(
self._trad["unit_discharge"],
color='black', fontsize=10
)
self.set_ticks_time_formater()
self.draw_additional_data(0)
self.enable_legend()
self.canvas.draw_idle()
self.update_idle()
self.draw_current()
self._init = True
@timer
def draw_data(self, res_id):
results = self.results[res_id]
reach = results.river.reach(self._current_reach_id)
colors = [
("q_min", self.color_plot_ensemble_min),
("q_mean", self.color_plot_ensemble_mean),
("q_max", self.color_plot_ensemble_max),
]
for t, c in colors:
q = results.get("table")[t]
profile = reach.profile(self._current_profile_id[0])
x = self._timestamps
y = q[:, profile.global_index]
# y = profile.get_key("Q")
if res_id == 2:
label = f"Δ {self.label_discharge} {t} {profile.name}"
else:
label = f"{self.label_discharge} {t} {profile.name}"
line, = self.canvas.axes.plot(
x, y,
label=label,
color=c,
# linestyle=self.linestyle[i % len(self.linestyle)],
**self.plot_default_kargs
)
self._line.append(line)
@timer
def draw_data_std_derivation(self, res_id):
results = self.results[res_id]
reach = results.river.reach(self._current_reach_id)
q = results.get("table")["q_mean"]
std_deriv = results.get("table")["q_std_derivation"]
profile = reach.profile(self._current_profile_id[0])
x = self._timestamps
y = q[:, profile.global_index]
std = std_deriv[:, profile.global_index]
y_b = y - std
y_t = y + std
plot = self.canvas.axes.fill_between(
x, y_b, y_t,
label=f"Standard derivation {profile.name}",
color=self.color_plot_ensemble_std_deriv,
alpha=0.3,
)
def draw_additional_data(self, res_id):
results = self.results[res_id]
self._add_data_lines = []
for data in results.get("additional_data"):
data = data._data
x, y = data['x'], data['y']
legend = data['legend']
unit = data['unit']
if data['type_x'] == 'time' and data['type_y'] == 'discharge':
line, = self.canvas.axes.plot(
x, y, marker="+",
label=legend + ' ' + unit
)
self._add_data_lines.append(line)
self.enable_legend()
self.canvas.draw_idle()
def draw_current(self):
self._current, = self.canvas.axes.plot(
[self._current_timestamp, self._current_timestamp],
self.canvas.axes.get_ylim(),
# label=self.label_timestamp,
color="grey",
linestyle="dashed",
lw=1.,
)
def set_reach(self, reach_id):
self._current_reach_id = reach_id
self._current_profile_id = [0]
self.draw()
def set_profile(self, profile_id):
self._current_profile_id = profile_id
# self.update()
self.draw()
def set_result(self, res_id):
self._current_res_id = res_id
self.draw()
def set_timestamp(self, timestamp):
self._current_timestamp = timestamp
self.update_current()
self.update_idle()
def update(self):
if not self._init:
self.draw()
self.update_current()
self.update_idle()
def update_additional_data(self):
self.draw_additional_data(0)
self.update_idle()
def update_all(self):
self._current_reach_id = self._parent._get_current_reach()
self._current_profile_id = self._parent._get_current_profiles_list()
self._current_res_id = self._parent._get_current_results()
self._current_timestamp = self._parent._get_current_timestamp()
self._init = False
self.update()
def update_current(self):
y = self._current.get_ydata()
self._current.set_data(
[self._current_timestamp, self._current_timestamp],
y
)
+10 -36
View File
@@ -109,12 +109,8 @@ class PlotRKC(PamhyrPlot):
table = results.get("table")["zfd"] table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp) ts = results.get_timestamp_id(self._current_timestamp)
zfd = list( gi_0, gi_1 = reach.global_index
map( zfd = table[ts, gi_0:gi_1 + 1]
lambda p: table[ts, p.global_index],
reach.profiles
)
)
self.line_bottom, = self.canvas.axes.plot( self.line_bottom, = self.canvas.axes.plot(
rk, zfd, rk, zfd,
@@ -178,14 +174,8 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"] table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp) ts = result.get_timestamp_id(self._current_timestamp)
water_z = list( gi_0, gi_1 = reach.global_index
map( water_z = table[ts, gi_0:gi_1 + 1]
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
self.water = self.canvas.axes.plot( self.water = self.canvas.axes.plot(
rk, water_z, rk, water_z,
@@ -209,16 +199,8 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"] table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp) ts = result.get_timestamp_id(self._current_timestamp)
water_z = list( gi_0, gi_1 = reach.global_index
map( water_z = table[ts, gi_0:gi_1 + 1]
lambda p: np.max(
table[
:, p.global_index
]
),
reach.profiles
)
)
self.canvas.axes.plot( self.canvas.axes.plot(
rk, water_z, lw=1., rk, water_z, lw=1.,
@@ -354,14 +336,8 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"] table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp) ts = result.get_timestamp_id(self._current_timestamp)
water_z = list( gi_0, gi_1 = reach.global_index
map( water_z = table[ts, gi_0:gi_1 + 1]
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
self.water[0].set_data( self.water[0].set_data(
rk, water_z rk, water_z
@@ -402,10 +378,8 @@ class PlotRKC(PamhyrPlot):
table = results.get("table")["zfd"] table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp) ts = results.get_timestamp_id(self._current_timestamp)
zfd = list(map( gi_0, gi_1 = reach.global_index
lambda p: table[ts, p.global_index], zfd = table[ts, gi_0:gi_1 + 1]
reach.profiles
))
self.line_bottom.remove() self.line_bottom.remove()
+355
View File
@@ -0,0 +1,355 @@
# PlotRKCEnsemble.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import numpy as np
from functools import reduce
from tools import timer
from View.Tools.PamhyrPlot import PamhyrPlot
from PyQt5.QtCore import (
QCoreApplication
)
logger = logging.getLogger()
class PlotRKCEnsemble(PamhyrPlot):
def __init__(self, canvas=None, trad=None, toolbar=None,
results=None, reach_id=0, profile_id=0,
parent=None):
super(PlotRKCEnsemble, self).__init__(
canvas=canvas,
trad=trad,
data=results,
toolbar=toolbar,
parent=parent
)
self._parent = parent
self._current_reach_id = reach_id
self._current_profile_id = profile_id
self._timestamps = parent._timestamps
self._current_timestamp = max(self._timestamps)
self.label_x = self._trad["unit_rk"]
self.label_y = self._trad["unit_elevation"]
self.label_bottom = self._trad["label_bottom"]
self.label_water = self._trad["label_water"]
self.label_water_mean = self._trad["label_water_mean"]
self.label_water_min = self._trad["label_water_min"]
self.label_water_max = self._trad["label_water_max"]
self.water = []
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
@property
def results(self):
return self.data
@results.setter
def results(self, results):
self.data = results
self._current_timestamp = max(self._timestamps)
@timer
def draw(self, highlight=None):
self.init_axes()
if self.results is None:
return
if self.results[0] is None:
return
results = self.results[0]
reach = results.river.reach(self._current_reach_id)
self.draw_bottom(reach)
self.draw_std_derivation(reach)
self.draw_water_elevation(reach)
self.draw_current(reach)
self.draw_profiles_hs(reach)
self.enable_legend()
self.idle()
self._init = True
def draw_bottom(self, reach):
self.draw_bottom_geometry(reach)
def draw_profiles_hs(self, reach):
results = self.results[0]
lhs = filter(
lambda hs: hs._input_reach.reach is reach.geometry,
filter(
lambda hs: hs._input_reach is not None,
results.study.river.hydraulic_structures.lst
)
)
for hs in lhs:
x = hs.input_section.rk
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
self.canvas.axes.plot(
[x, x],
[min(z_min), max(z_max)],
linestyle="--",
lw=1.,
color=self.color_plot_previous,
)
self.canvas.axes.annotate(
" > " + hs.name,
(x, max(z_max)),
horizontalalignment='left',
verticalalignment='top',
annotation_clip=True,
fontsize=9, color=self.color_plot_previous,
)
def draw_bottom_geometry(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
self.line_rk_zmin = self.canvas.axes.plot(
rk, z_min,
color=self.color_plot_river_bottom,
lw=1.
)
self._river_bottom = z_min
def draw_water_elevation(self, reach):
if len(reach.geometry.profiles) != 0:
result = self.results[0]
rk = reach.geometry.get_rk()
self.water = []
colors = [
("z_min", self.color_plot_ensemble_min),
("z_mean", self.color_plot_ensemble_mean),
("z_max", self.color_plot_ensemble_max),
]
for t, color in colors:
table = result.get("table")[t]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
plot, = self.canvas.axes.plot(
rk, water_z,
lw=1., color=color,
label=f"Elevation: {t} (m)",
)
self.water.append(plot)
def draw_std_derivation(self, reach):
if len(reach.geometry.profiles) != 0:
result = self.results[0]
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
color = self.color_plot_ensemble_std_deriv
table = result.get("table")["z_mean"]
std_deriv = result.get("table")["z_std_derivation"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
std = std_deriv[ts, gi_0:gi_1 + 1]
water_z_b = water_z - std
water_z_t = water_z + std
plot = self.canvas.axes.fill_between(
rk, water_z_b, water_z_t,
lw=1., color=color, alpha=0.3,
label=f"Standard derivation",
)
self.water_std_deriv = plot
def draw_additional_data(self):
results = self.results[0]
self._add_data_lines = []
for data in results.get("additional_data"):
data = data._data
tx, ty = data['type_x'], data['type_y']
x, y = data['x'], data['y']
legend = data['legend']
unit = data['unit']
if tx == 'water_elevation' and ty == 'time':
line, = self.canvas.axes.plot(
x, y, marker="+",
label=legend + ' ' + unit
)
self._add_data_lines.append(line)
self.canvas.draw_idle()
def draw_current(self, reach):
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
self.profile, = self.canvas.axes.plot(
[
rk[self._current_profile_id],
rk[self._current_profile_id]
],
[
z_max[self._current_profile_id],
z_min[self._current_profile_id]
],
color=self.color_plot,
lw=1.
)
def is_overflow_point(self, profile, point):
left_limit = profile.geometry.point(0)
right_limit = profile.geometry.point(
profile.geometry.number_points - 1
)
return (
point == left_limit
or point == right_limit
)
def set_reach(self, reach_id):
self._current_reach_id = reach_id
self._current_profile_id = 0
self.draw()
def set_profile(self, profile_id):
self._current_profile_id = profile_id
self.update_current()
def set_timestamp(self, timestamp):
self._current_timestamp = timestamp
self.update()
def update(self):
if not self._init:
self.draw()
results = self.results[0]
reach = results.river.reach(self._current_reach_id)
self.update_water_elevation()
self.update_std_derivation()
self.update_idle()
def update_all(self):
self._current_reach_id = self._parent._get_current_reach()
self._current_profile_id = self._parent._get_current_profile()
self._current_timestamp = self._parent._get_current_timestamp()
self._init = False
self.update()
def update_water_elevation(self):
result = self.results[0]
reach = result.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
table = result.get("table")["z_mean"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
colors = [
(0, "z_min", self.color_plot_ensemble_min),
(1, "z_mean", self.color_plot_ensemble_mean),
(2, "z_max", self.color_plot_ensemble_max),
]
for i, t, color in colors:
table = result.get("table")[t]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
self.water[i].set_data(rk, water_z)
def update_std_derivation(self):
result = self.results[0]
reach = result.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
table = result.get("table")["z_mean"]
std_deriv = result.get("table")["z_std_derivation"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
std = std_deriv[ts, gi_0:gi_1 + 1]
water_z_b = water_z - std
water_z_t = water_z + std
self.water_std_deriv.set_data(rk, water_z_b, water_z_t)
def update_additional_data(self):
self.draw_additional_data()
self.update_idle()
def update_current(self):
results = self.results[0]
reach = results.river.reach(self._current_reach_id)
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
z_max = reach.geometry.get_z_max()
cid = self._current_profile_id
self.profile.set_data(
[rk[cid], rk[cid]],
[z_max[cid], z_min[cid]]
)
self.canvas.figure.canvas.draw_idle()
self.update_idle()
+6
View File
@@ -61,6 +61,8 @@ class TableModel(PamhyrTableModel):
# _river.reach(0).profiles)) # _river.reach(0).profiles))
elif self._opt_data == "solver": elif self._opt_data == "solver":
self._lst = self._parent._solvers self._lst = self._parent._solvers
elif self._opt_data == "runs":
self._lst = self._data[0].get("table")["samples"]
def __init__(self, **kwargs): def __init__(self, **kwargs):
self._timestamp = max(kwargs["parent"]._timestamps) self._timestamp = max(kwargs["parent"]._timestamps)
@@ -93,6 +95,10 @@ class TableModel(PamhyrTableModel):
if v is None: if v is None:
v = self._data[0].solver_name v = self._data[0].solver_name
return str(v) return str(v)
elif self._opt_data == "runs":
if self._headers[column] == "runs":
p = self._lst[row, :]
return f"{row}: {p}"
elif self._opt_data == "raw_data": elif self._opt_data == "raw_data":
table = self._data[0].get("table") table = self._data[0].get("table")
ts = self._data[0].get_timestamp_id(self._timestamp) ts = self._data[0].get_timestamp_id(self._timestamp)
+475 -375
View File
@@ -72,6 +72,8 @@ from View.Results.PlotRKC import PlotRKC
from View.Results.PlotH import PlotH from View.Results.PlotH import PlotH
from View.Results.PlotSedReach import PlotSedReach from View.Results.PlotSedReach import PlotSedReach
from View.Results.PlotSedProfile import PlotSedProfile from View.Results.PlotSedProfile import PlotSedProfile
from View.Results.PlotRKCEnsemble import PlotRKCEnsemble
from View.Results.PlotHEnsemble import PlotHEnsemble
from View.Results.CustomPlot.Plot import CustomPlot from View.Results.CustomPlot.Plot import CustomPlot
from View.Results.CustomPlot.CustomPlotValuesSelectionDialog import ( from View.Results.CustomPlot.CustomPlotValuesSelectionDialog import (
@@ -397,21 +399,21 @@ class ResultsWindow(PamhyrWindow):
) )
# Table and Plot # Table and Plot
fun = { funs = [
"reach": self._set_current_reach, ("reach", self._set_current_reach),
"profile": self._set_current_profile, ("profile", self._set_current_profile),
"raw_data": self._set_current_profile_raw_data, ("raw_data", self._set_current_profile_raw_data),
"solver": self._set_current_results, ("solver", self._set_current_results),
} ]
for t in ["reach", "profile", "raw_data", "solver"]: for t, f in funs:
table = self.find(QTableView, f"tableView_{t}") table = self.find(QTableView, f"tableView_{t}")
table.selectionModel()\ table.selectionModel()\
.selectionChanged\ .selectionChanged\
.connect(fun[t]) .connect(f)
self._table[t].dataChanged.connect(fun[t]) self._table[t].dataChanged.connect(f)
self._slider_time.valueChanged.connect(self._set_current_timestamp) self._slider_time.valueChanged.connect(self._set_current_timestamp)
self._button_play.setChecked(False) self._button_play.setChecked(False)
@@ -874,266 +876,174 @@ class ResultsWindow(PamhyrWindow):
dict_x = self._trad.get_dict("values_x") dict_x = self._trad.get_dict("values_x")
dict_y = self._trad.get_dict("values_y") dict_y = self._trad.get_dict("values_y")
if self._current_results == 2:
envelop = False
reach1 = results[0].river.reachs[self._get_current_reach()]
reach2 = results[1].river.reachs[self._get_current_reach()]
if envelop: if envelop:
dict_y.update(self._trad.get_dict("values_y_envelop")) dict_y.update(self._trad.get_dict("values_y_envelop"))
my_dict = {}
my_dict[dict_x["rk"]] = reach.geometry.get_rk()
table = results.get("table") table = results.get("table")
id_ts = results.get_timestamp_id(timestamp) id_ts = results.get_timestamp_id(timestamp)
z = table["Z"] ind = reach.global_index
q = table["Q"]
v = table["V"]
zfd = table["zfd"]
if "bed_elevation" in y: my_dict = {}
if reach.has_bedload(): data = {
zmin = list(map( "z": table["Z"][:, ind[0]:ind[1]+1],
lambda p: zfd[id_ts, p.global_index], "q": table["Q"][:, ind[0]:ind[1]+1],
reach.profiles "v": table["V"][:, ind[0]:ind[1]+1],
)) "zfd": table["zfd"][:, ind[0]:ind[1]+1],
else:
zmin = reach.geometry.get_z_min() "reach": reach,
my_dict[dict_y["bed_elevation"]] = zmin "id_ts": id_ts, "envelop": envelop,
# if envelop and reach.has_sediment(): "my_dict": my_dict,
if "discharge" in y: }
my_dict[dict_y["discharge"]] = list(
map( func = {
lambda p: q[id_ts, p.global_index], "bed_elevation": self._export_rk_compute_elevation,
reach.profiles "discharge": self._export_rk_compute_discharge,
) "water_elevation": self._export_rk_compute_water_elevation,
"velocity": self._export_rk_compute_velocity,
"depth": self._export_rk_compute_depth,
"mean_depth": self._export_rk_compute_mean_depth,
"froude": self._export_rk_compute_froude,
"wet_area": self._export_rk_compute_wet_area,
}
# Compute values
for key in y:
func[key](data)
# Translate dictionary keys
translated_dict = {}
for key in my_dict:
translated_dict[dict_y[key]] = my_dict[key]
translated_dict[dict_x["rk"]] = reach.geometry.get_rk()
return translated_dict
def _export_rk_compute_elevation(self, data):
my_dict = data["my_dict"]
reach = data["reach"]
id_ts = data["id_ts"]
if reach.has_bedload():
zfd = data["zfd"]
zmin = zfd[id_ts, :]
else:
zmin = reach.geometry.get_z_min()
my_dict["bed_elevation"] = zmin
def _export_rk_compute_discharge(self, data):
my_dict = data["my_dict"]
envelop = data["envelop"]
id_ts = data["id_ts"]
q = data["q"]
my_dict["discharge"] = q[id_ts, :]
if envelop:
my_dict["min_discharge"] = np.min(q, axis=0)
my_dict["max_discharge"] = np.max(q, axis=0)
def _export_rk_compute_water_elevation(self, data):
my_dict = data["my_dict"]
envelop = data["envelop"]
id_ts = data["id_ts"]
z = data["z"]
my_dict["water_elevation"] = z[id_ts, :]
if envelop:
my_dict["min_water_elevation"] = np.min(z, axis=0)
my_dict["max_water_elevation"] = np.max(z, axis=0)
def _export_rk_compute_velocity(self, data):
my_dict = data["my_dict"]
envelop = data["envelop"]
id_ts = data["id_ts"]
v = data["v"]
my_dict["velocity"] = v[id_ts, :]
if envelop:
my_dict["min_velocity"] = np.min(v, axis=0)
my_dict["max_velocity"] = np.max(v, axis=0)
def _export_rk_compute_depth(self, data):
my_dict = data["my_dict"]
envelop = data["envelop"]
reach = data["reach"]
id_ts = data["id_ts"]
z = data["z"]
my_dict["depth"] = list(
map(
lambda p: p.geometry.max_water_depth(
z[id_ts, p.global_index]
),
reach.profiles
) )
if envelop: )
my_dict[dict_y["min_discharge"]] = list(
map( if envelop:
lambda p: np.min(q[:, p.global_index]), z_min = reach.geometry.get_z_min()
reach.profiles
) my_dict["min_depth"] = np.min(z, axis=0) - z_min
) my_dict["max_depth"] = np.max(z, axis=0) - z_min
my_dict[dict_y["max_discharge"]] = list(
map( def _export_rk_compute_mean_depth(self, data):
lambda p: np.max(q[:, p.global_index]), my_dict = data["my_dict"]
reach.profiles reach = data["reach"]
) id_ts = data["id_ts"]
)
if "water_elevation" in y: z = data["z"]
my_dict[dict_y["water_elevation"]] = list(
map( my_dict["mean_depth"] = list(
lambda p: z[id_ts, p.global_index], map(
reach.profiles lambda p: p.geometry.mean_water_depth(
) z[id_ts, p.global_index]
),
reach.profiles
) )
if envelop: )
my_dict[dict_y["min_water_elevation"]] = list(
map(
lambda p: np.min(z[:, p.global_index]),
reach.profiles
)
)
my_dict[dict_y["max_water_elevation"]] = list(
map(
lambda p: np.max(z[:, p.global_index]),
reach.profiles
)
)
if "velocity" in y: def _export_rk_compute_froude(self, data):
my_dict[dict_y["velocity"]] = list( my_dict = data["my_dict"]
map( reach = data["reach"]
lambda p: v[id_ts, p.global_index], id_ts = data["id_ts"]
reach.profiles
) z = data["z"]
v = data["v"]
my_dict["froude"] = list(
map(
lambda p: p.geometry.froude(
z[id_ts, p.global_index],
v[id_ts, p.global_index]
),
reach.profiles
) )
if envelop: )
velocities = list(map(
lambda p: v[:, p.global_index],
reach.profiles
))
my_dict[dict_y["min_velocity"]] = [min(v) for v in velocities]
my_dict[dict_y["max_velocity"]] = [max(v) for v in velocities]
if "depth" in y: def _export_rk_compute_wet_area(self, data):
if self._current_results != 2: my_dict = data["my_dict"]
d = my_dict[dict_y["depth"]] = list( reach = data["reach"]
map( id_ts = data["id_ts"]
lambda p: p.geometry.max_water_depth(
z[id_ts, p.global_index]
),
reach.profiles
)
)
else:
d1 = list(
map(
lambda p: p.geometry.max_water_depth(
z[id_ts, p.global_index]
),
reach1.profiles
)
)
d2 = list(
map(
lambda p: p.geometry.max_water_depth(
z[id_ts, p.global_index]
),
reach2.profiles
)
)
d = list(
map(
lambda x, y: x - y,
d1, d2
)
)
my_dict[dict_y["depth"]] = d
if envelop:
my_dict[dict_y["min_depth"]] = list(map(
lambda p1, p2: p1 - p2, map(
lambda p: np.min(z[:, p.global_index]),
reach.profiles
), reach.geometry.get_z_min()
)
)
my_dict[dict_y["max_depth"]] = list(map(
lambda p1, p2: p1 - p2, map(
lambda p: np.max(z[:, p.global_index]),
reach.profiles
), reach.geometry.get_z_min()
)
)
if "mean_depth" in y: z = data["z"]
if self._current_results != 2:
d = list(
map(
lambda p: p.geometry.mean_water_depth(
z[id_ts, p.global_index]
),
reach.profiles
)
)
else:
d1 = list(
map(
lambda p: p.geometry.mean_water_depth(
z[id_ts, p.global_index]
),
reach1.profiles
)
)
d2 = list(
map(
lambda p: p.geometry.mean_water_depth(
z[id_ts, p.global_index]
),
reach2.profiles
)
)
d = list(
map(
lambda x, y: x - y,
d1, d2
)
)
my_dict[dict_y["mean_depth"]] = d
if "froude" in y:
if self._current_results != 2:
fr = my_dict[dict_y["froude"]] = list(
map(
lambda p: (
v[id_ts, p.global_index] /
sqrt(9.81 * (
p.geometry.wet_area(
z[id_ts, p.global_index]
) /
p.geometry.wet_width(
z[id_ts, p.global_index]
)
))
),
reach.profiles
)
)
else:
fr1 = list(
map(
lambda p: (
v[id_ts, p.global_index] /
sqrt(9.81 * (
p.geometry.wet_area(
z[id_ts, p.global_index]
) /
p.geometry.wet_width(
z[id_ts, p.global_index]
)
))
),
reach1.profiles
)
)
fr2 = list(
map(
lambda p: (
v[id_ts, p.global_index] /
sqrt(9.81 * (
p.geometry.wet_area(
z[id_ts, p.global_index]
) /
p.geometry.wet_width(
z[id_ts, p.global_index]
)
))
),
reach2.profiles
)
)
fr = list(map(
lambda x, y: x - y,
fr1, fr2
))
my_dict[dict_y["froude"]] = fr
if "wet_area" in y:
if self._current_results != 2:
wa = list(
map(
lambda p: p.geometry.wet_area(
z[id_ts, p.global_index]
),
reach.profiles
)
)
else:
wa1 = list(
map(
lambda p: p.geometry.wet_area(
z[id_ts, p.global_index]
),
reach1.profiles
)
)
wa2 = list(
map(
lambda p: p.geometry.wet_area(
z[id_ts, p.global_index]
),
reach2.profiles
)
)
wa = list(map(
lambda x, y: x - y,
wa1, wa2
))
my_dict[dict_y["wet_area"]] = wa my_dict["wet_area"] = list(
map(
return my_dict lambda p: p.geometry.wet_area(
z[id_ts, p.global_index]
),
reach.profiles
)
)
def _export_time(self, profile, y, solver_id): def _export_time(self, profile, y, solver_id):
results = self._results[solver_id] results = self._results[solver_id]
@@ -1142,134 +1052,105 @@ class ResultsWindow(PamhyrWindow):
dict_x = self._trad.get_dict("values_x") dict_x = self._trad.get_dict("values_x")
dict_y = self._trad.get_dict("values_y") dict_y = self._trad.get_dict("values_y")
my_dict = {}
my_dict[dict_x["time"]] = self._timestamps
table = results.get("table") table = results.get("table")
ind = profile.global_index
z = table["Z"][:, profile.global_index] my_dict = {}
q = table["Q"][:, profile.global_index] data = {
v = table["V"][:, profile.global_index] "z": table["Z"][:, ind],
zfd = table["zfd"][:, profile.global_index] "q": table["Q"][:, ind],
"v": table["V"][:, ind],
"zfd": table["zfd"][:, ind],
if self._current_results == 2: "reach": reach,
reach1 = self._results[0].river.reach(self._reach) "profile": profile,
reach2 = self._results[1].river.reach(self._reach) "my_dict": my_dict,
reach3 = self._results[2].river.reach(self._reach) }
profile1 = reach1.profile(self._profile)
profile2 = reach2.profile(self._profile)
# profile3 = reach3.profile(self._profile)
z1 = table["Z"][:, profile1.global_index] def set_dict(k, v):
q1 = table["Q"][:, profile1.global_index] my_dict[k] = v
v1 = table["V"][:, profile1.global_index]
z2 = table["Z"][:, profile2.global_index] func = {
q2 = table["Q"][:, profile2.global_index] "bed_elevation": self._export_time_compute_elevation,
v2 = table["V"][:, profile2.global_index] "discharge": lambda d: set_dict("discharge", d["q"]),
"water_elevation": lambda d: set_dict("water_elevation", d["z"]),
"velocity": lambda d: set_dict("velocity", d["v"]),
"depth": self._export_time_compute_depth,
"mean_depth": self._export_time_compute_mean_depth,
"froude": self._export_time_compute_froude,
"wet_area": self._export_time_compute_wet_area,
}
if "bed_elevation" in y: # Compute values
if reach.has_bedload(): for key in y:
z_min = zfd func[key](data)
else:
z_min = [profile.geometry.z_min()] * len(self._timestamps)
my_dict[dict_y["bed_elevation"]] = z_min # Translate dictionary keys
if "discharge" in y: translated_dict = {}
my_dict[dict_y["discharge"]] = q for key in my_dict:
if "water_elevation" in y: translated_dict[dict_y[key]] = my_dict[key]
my_dict[dict_y["water_elevation"]] = z
if "velocity" in y:
my_dict[dict_y["velocity"]] = v
if "depth" in y:
if self._current_results != 2:
d = list(
map(lambda x: profile.geometry.max_water_depth(x), z)
)
else:
d1 = list(
map(lambda x: profile1.geometry.max_water_depth(x), z1)
)
d2 = list(
map(lambda x: profile2.geometry.max_water_depth(x), z2)
)
d = list(
map(
lambda x, y: x - y,
d1, d2
)
)
my_dict[dict_y["depth"]] = d
if "mean_depth" in y:
if self._current_results != 2:
d = list(
map(lambda x: profile.geometry.mean_water_depth(x), z)
)
else:
d1 = list(
map(lambda x: profile1.geometry.mean_water_depth(x), z1)
)
d2 = list(
map(lambda x: profile2.geometry.mean_water_depth(x), z2)
)
d = list(map(
lambda x, y: x - y,
d1, d2
))
my_dict[dict_y["mean_depth"]] = d
if "froude" in y:
if self._current_results != 2:
fr = my_dict[dict_y["froude"]] = list(
map(lambda z, v:
v / sqrt(9.81 * (
profile.geometry.wet_area(z) /
profile.geometry.wet_width(z))
),
z, v)
)
else:
fr1 = list(
map(lambda z, v:
v /
sqrt(9.81 * (
profile1.geometry.wet_area(z) /
profile1.geometry.wet_width(z))
),
z1, v1)
)
fr2 = list(
map(lambda z, v:
v /
sqrt(9.81 * (
profile2.geometry.wet_area(z) /
profile2.geometry.wet_width(z))
),
z2, v2)
)
fr = list(map(
lambda x, y: x - y,
fr1, fr2
))
my_dict[dict_y["froude"]] = fr
if "wet_area" in y:
if self._current_results != 2:
wa = list(
map(lambda x: profile.geometry.wet_area(x), z)
)
else:
wa1 = list(
map(lambda x: profile1.geometry.wet_area(x), z1)
)
wa2 = list(
map(lambda x: profile2.geometry.wet_area(x), z2)
)
wa = list(map(
lambda x, y: x - y,
wa1, wa2
))
my_dict[dict_y["wet_area"]] = wa
return my_dict translated_dict[dict_x["time"]] = self._timestamps
return translated_dict
def _export_time_compute_elevation(self, data):
my_dict = data["my_dict"]
reach = data["reach"]
if reach.has_bedload():
z_min = data["zfd"]
else:
z_min = [profile.geometry.z_min()] * len(self._timestamps)
my_dict["bed_elevation"] = z_min
def _export_time_compute_depth(self, data):
my_dict = data["my_dict"]
profile = data["profile"]
z = data["z"]
my_dict["depth"] = list(
map(lambda x: profile.geometry.max_water_depth(x), z)
)
def _export_time_compute_mean_depth(self, data):
my_dict = data["my_dict"]
profile = data["profile"]
z = data["z"]
my_dict["mean_depth"] = list(
map(lambda x: profile.geometry.mean_water_depth(x), z)
)
def _export_time_compute_froude(self, data):
my_dict = data["my_dict"]
profile = data["profile"]
z = data["z"]
v = data["v"]
my_dict["froude"] = list(
map(
lambda z, v: profile.geometry.froude(z, v),
z, v
)
)
def _export_time_compute_wet_area(self, data):
my_dict = data["my_dict"]
profile = data["profile"]
z = data["z"]
my_dict["wet_area"] = list(
map(
lambda x: profile.geometry.wet_area(x),
z
)
)
def get_timestamps(self): def get_timestamps(self):
self._timestamps = sorted(list(self._results[0].get("timestamps"))) self._timestamps = sorted(list(self._results[0].get("timestamps")))
@@ -1437,3 +1318,222 @@ class ResultsWindow(PamhyrWindow):
for data in results.get("additional_data"): for data in results.get("additional_data"):
for p in self._additional_plot: for p in self._additional_plot:
self._additional_plot[p].add_imported_plot(data) self._additional_plot[p].add_imported_plot(data)
class ResultsEnsembleWindow(ResultsWindow):
_pamhyr_ui = "ResultsEnsemble"
_pamhyr_name = "Results"
def setup_table(self):
self._table = {}
for t in ["reach", "profile", "runs"]:
table = self.find(QTableView, f"tableView_{t}")
self._table[t] = TableModel(
table_view=table,
table_headers=self._trad.get_dict(f"table_headers_{t}"),
data=self._results,
undo=self._undo_stack,
opt_data=t,
parent=self
)
self._table[t]._timestamp = self._timestamps[
self._slider_time.value()
]
def setup_plots(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_widget.setTabsClosable(True)
tab_widget.tabCloseRequested.connect(self.delete_tab)
tab_widget.tabBar().setTabButton(0, QTabBar.RightSide, None)
tab_widget.tabBar().setTabButton(1, QTabBar.RightSide, None)
self.canvas_1 = MplCanvas(width=5, height=4, dpi=100)
self.canvas_1.setObjectName("canvas_1")
self.toolbar_1 = PamhyrPlotToolbar(
self.canvas_1, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_1 = self.find(QVBoxLayout,
"verticalLayout_elevation")
self.plot_layout_1.addWidget(self.toolbar_1)
self.plot_layout_1.addWidget(self.canvas_1)
self.plot_rkc = PlotRKCEnsemble(
canvas=self.canvas_1,
results=self._results,
reach_id=0,
profile_id=0,
trad=self._trad,
toolbar=self.toolbar_1,
parent=self
)
self.plot_rkc.draw()
self.canvas_2 = MplCanvas(width=5, height=4, dpi=100)
self.canvas_2.setObjectName("canvas_2")
self.toolbar_2 = PamhyrPlotToolbar(
self.canvas_2, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_2 = self.find(
QVBoxLayout, "verticalLayout_hydrograph")
self.plot_layout_2.addWidget(self.toolbar_2)
self.plot_layout_2.addWidget(self.canvas_2)
self.plot_h = PlotHEnsemble(
canvas=self.canvas_2,
results=self._results,
reach_id=0,
profile_id=[0],
res_id=self._current_results,
trad=self._trad,
toolbar=self.toolbar_2,
parent=self
)
self.plot_h.draw()
def setup_connections(self):
# Action
actions = {
"action_export": self._export,
# "action_export": self.export_current,
"action_import_data": self.import_data
}
if len(self._results) > 1:
self.find(QAction, "action_reload").setEnabled(False)
for action in actions:
self.find(QAction, action).triggered.connect(
actions[action]
)
# Table and Plot
funs = [
("reach", self._set_current_reach),
("profile", self._set_current_profile),
]
for t, f in funs:
table = self.find(QTableView, f"tableView_{t}")
table.selectionModel()\
.selectionChanged\
.connect(f)
self._table[t].dataChanged.connect(f)
self._slider_time.valueChanged.connect(self._set_current_timestamp)
self._button_play.setChecked(False)
self._button_play.clicked.connect(self._pause)
self._button_back.clicked.connect(self._back)
self._button_next.clicked.connect(self._next)
self._button_first.clicked.connect(self._first)
self._button_last.clicked.connect(self._last)
self._timer.timeout.connect(self._update_slider)
# tabs
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_widget.currentChanged.connect(self.tab_changed)
def _compute_status_label(self):
return (self.text_bief() + " | " +
self.text_profile() + " | " +
self.text_time() + " | " +
self.text_seed())
def text_seed(self):
return f"Seed: {self._results[0].get("seed")}"
def update(self,
reach_id=None,
profile_id=None,
timestamp=None):
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_index = tab_widget.currentIndex()
name = tab_widget.tabText(tab_index)
if reach_id is not None:
if tab_index == 0:
self.plot_rkc.set_reach(reach_id)
elif tab_index == 1:
self.plot_h.set_reach(reach_id)
self.update_table_selection_reach(reach_id)
self.update_table_selection_profile(0)
if profile_id is not None:
if tab_index == 0:
self.plot_rkc.set_profile(profile_id[0])
elif tab_index == 1:
self.plot_h.set_profile(profile_id)
tab_widget = self.find(QTabWidget, f"tabWidget")
if tab_widget.currentIndex() != 2:
self.update_table_selection_profile(profile_id[0])
if timestamp is not None:
self.plot_rkc.set_timestamp(timestamp)
if tab_index == 1:
self.plot_h.set_timestamp(timestamp)
self.update_statusbar()
def tab_changed(self, i):
if i != 2:
if len(self._get_current_profiles_list()) > 1:
# unselect all profiles but the first one
profile_id = self._get_current_profile()
self.update_table_selection_profile(profile_id)
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_index = tab_widget.currentIndex()
name = tab_widget.tabText(tab_index)
if tab_index == 1:
self.plot_rkc.update_all()
elif tab_index == 2:
self.plot_h.update_all()
elif tab_index > 2:
self._additional_plot[name].update_all()
def update_table_selection_reach(self, ind):
table = self.find(QTableView, f"tableView_reach")
selectionModel = table.selectionModel()
index = table.model().index(ind, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
self._table["profile"].update(ind)
def update_table_selection_profile(self, ind):
for t in ["profile"]:
table = self.find(QTableView, f"tableView_{t}")
selectionModel = table.selectionModel()
index = table.model().index(ind, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
def update_table_selection_solver(self, ind):
return
+15
View File
@@ -44,15 +44,26 @@ class ResultsTranslate(MainTranslate):
self._dict['envelop'] = _translate("Results", "Envelop") self._dict['envelop'] = _translate("Results", "Envelop")
self._dict['solver'] = _translate("Results", "Solver") self._dict['solver'] = _translate("Results", "Solver")
self._dict['runs'] = _translate("Results", "Runs")
self._dict['x'] = _translate("Results", "X (m)") self._dict['x'] = _translate("Results", "X (m)")
self._dict['label_bottom'] = _translate("Results", "Bottom") self._dict['label_bottom'] = _translate("Results", "Bottom")
self._dict['label_water'] = _translate("Results", "Water elevation") self._dict['label_water'] = _translate("Results", "Water elevation")
self._dict['label_water_min'] = _translate(
"Results",
"Min water elevation"
)
self._dict['label_water_mean'] = _translate(
"Results",
"Mean water elevation"
)
self._dict['label_water_max'] = _translate( self._dict['label_water_max'] = _translate(
"Results", "Results",
"Max water elevation" "Max water elevation"
) )
self._dict["ImageCoordinates"] = _translate( self._dict["ImageCoordinates"] = _translate(
"Results", "Image coordinates" "Results", "Image coordinates"
) )
@@ -81,6 +92,10 @@ class ResultsTranslate(MainTranslate):
"solver": self._dict['solver'], "solver": self._dict['solver'],
} }
self._sub_dict["table_headers_runs"] = {
"runs": self._dict['runs'],
}
self._sub_dict["table_headers_pollutants"] = { self._sub_dict["table_headers_pollutants"] = {
"name": _translate("Results", "Pollutant name"), "name": _translate("Results", "Pollutant name"),
} }
+290 -1
View File
@@ -20,6 +20,8 @@ import os
import logging import logging
import tempfile import tempfile
import numpy as np
from queue import Queue from queue import Queue
from tools import trace, timer, logger_exception from tools import trace, timer, logger_exception
@@ -40,7 +42,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QTableView, QAbstractItemView, QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate, QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget, QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QTextEdit, QTextEdit, QProgressBar, QCheckBox,
) )
from View.RunSolver.Log.Window import SolverLogFileWindow from View.RunSolver.Log.Window import SolverLogFileWindow
@@ -57,6 +59,63 @@ _translate = QCoreApplication.translate
logger = logging.getLogger() logger = logging.getLogger()
class EnsembleRunNumberDialog(PamhyrDialog):
_pamhyr_ui = "EnsembleRunNumberDialog"
_pamhyr_name = "EnsembleRunNumber"
def __init__(self, trad=None, parent=None):
super(EnsembleRunNumberDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._function = "Random"
self._number = 100
self._seed = -1
self._init_default_values()
self._init_connection()
def _init_default_values(self):
self.combobox_add_items("comboBox", ["Random"])
self.set_combobox_text("comboBox", "Random")
self.set_spin_box("spinBox", 100)
def _init_connection(self):
self._checkbox = self.find(QCheckBox, f"checkBox")
self._checkbox.clicked.connect(self._set_random_seed_spin_box_state)
def _set_random_seed_spin_box_state(self):
self.set_spin_box_enable("spinBox_seed", self._checkbox.isChecked())
@property
def number(self):
return self._number
@property
def seed(self):
return self._seed
@property
def function(self):
return self._function
def accept(self):
self._function = self.get_combobox_text("comboBox")
self._number = self.get_spin_box("spinBox")
if self._checkbox.isChecked():
self._seed = self.get_spin_box("spinBox_seed")
super().accept()
def reject(self):
self.close()
class SelectSolverWindow(PamhyrDialog): class SelectSolverWindow(PamhyrDialog):
_pamhyr_ui = "SelectSolver" _pamhyr_ui = "SelectSolver"
_pamhyr_name = "Select solver" _pamhyr_name = "Select solver"
@@ -425,3 +484,233 @@ class SolverLogWindow(PamhyrWindow):
parent=self, parent=self,
) )
log.show() log.show()
class SolverLogEnsWindow(SolverLogWindow):
_pamhyr_ui = "SolverLogEnsemble"
_pamhyr_name = "Solver Log Ensemble"
def __init__(self, number_of_run=100,
seed=-1, function="Random",
study=None, config=None,
solver=None, parent=None):
self._number_of_run = number_of_run
self._last_run = 0
if seed != -1:
np.random.seed(seed)
self._seed = np.random.get_state()[1][0]
logger.info(
"solver: Set ensemble with " +
f"{number_of_run} {function} and " +
f"{self._seed} as random seed"
)
self._samples = {}
for ens in study.river._ensembles.lst:
if ens.distribution is None:
continue
self._samples[ens] = ens.random(self._number_of_run)
self._results = None
self._run_samples = []
self._results_tables = {}
super(SolverLogEnsWindow, self).__init__(
solver=solver,
study=study,
config=config,
parent=parent,
)
def setup_connections(self):
self.setup_progress_bar()
super(SolverLogEnsWindow, self).setup_connections()
def setup_progress_bar(self):
self._progress_bar = self.find(QProgressBar, "progressBar")
self._progress_bar.setRange(0, self._number_of_run)
self._progress_bar.setValue(0)
def _log_int(self, int_code, color=None):
logger.info(f"solver: Returns {int_code}")
color = "blue" if int_code == 0 else "red"
self.find(QTextEdit, "textEdit")\
.append(
f"<font color=\"{color}\">" +
f" *** Finished with code {int_code}" +
"</font>"
)
self.statusbar.showMessage(
("Done" if int_code == 0 else "Failed")
+ f" #{self._last_run}",
3000
)
self._solver.results_ensemble(
self._study, self._workdir,
self._results_tables,
self._last_run,
qlog=self._output,
)
if self._last_run < self._number_of_run:
self.next()
else:
self._results = self.fill_results_from_tables()
self._parent.set_results(self._solver, self._results)
self._progress_bar.setValue(self._last_run)
def fill_results_from_tables(self):
tables = self._results_tables
results = self._solver.get_ensemble_empty_results(
self._study, self._workdir
)
table = {}
for key in ["z", "q"]:
for func in ["min", "mean", "max", "std_derivation"]:
name = f"{key}_{func}"
table[name] = results.new_table_data(name, tables[name])
table["samples"] = results.new_table_data(
"samples", np.array(self._run_samples)
)
results.set("number_of_run", self._number_of_run)
results.set("seed", self._seed) # Keep random seed
results.set("table", table)
# Compute global index for reaches and profiles
gi = 0
for reach in results.river.reachs:
gi_start = gi
for profile in reach.profiles:
gi += 1
reach.set_global_index(range(gi_start, gi))
# Compute timestamps index table
ts = tables["timestamps"]
ts_index = {}
for i, t in enumerate(sorted(ts)):
ts_index[t] = i
results.set("timestamps", ts)
results.set_timestamp_index(ts_index)
return results
def next(self):
self._process = self.new_process(self._parent)
self._log(f" *** Run #{self._last_run}", color="blue")
if self._solver._type in ["mage8"]:
samples = self._solver.export_ensemble_variation(
self._study, self._workdir, qlog=self._output,
samples=self._samples,
)
else:
samples = self.ensemble_variation()
self._solver.export(
self._study, self._workdir, qlog=self._output
)
# Keep samples generators results
self._run_samples.append(samples)
self._solver.run(
self._study,
process=self._process,
output_queue=self._output
)
self.set_running_button_status()
self._last_run += 1
self._progress_bar.setValue(self._last_run - 1)
def ensemble_variation(self):
samples = []
for ens in self._samples:
if ens.target_data is None:
continue
x = next(self._samples[ens])
samples.append(x)
self._log(f"*** Variation #{self._last_run} " +
f"'{ens.target_data}' - {ens.data_type} x {x}",
color="blue")
if ens.data_type == "strickler_minor":
ens.target_data.minor = x
elif ens.data_type == "strickler_medium":
ens.target_data.medium = x
return samples
def run(self):
self._log(f" *** Run solver {self._solver.name}", color="blue")
self.next()
def start(self):
if self._solver.is_stoped():
self._log(f" *** Export study {self._solver.name}", color="blue")
ok = self._solver.export(
self._study, self._workdir, qlog=self._output
)
if not ok:
self._log(f" *** Failed to export", color="red")
self.update()
return
else:
self.update()
self._process = self.new_process(self._parent)
self._log(" *** Start", color="blue")
self._results = None
self.next()
def set_running_button_status(self):
self.find(QAction, "action_start").setEnabled(False)
if _signal:
self.find(QAction, "action_pause").setEnabled(True)
else:
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(True)
self.find(QAction, "action_log_file").setEnabled(False)
self.find(QAction, "action_results").setEnabled(False)
def stop(self):
self._log(" *** Stop", color="blue")
self._solver.kill()
self.find(QAction, "action_start").setEnabled(True)
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(False)
self.find(QAction, "action_results").setEnabled(True)
if self._solver.log_file() != "":
self.find(QAction, "action_log_file").setEnabled(True)
def results(self):
if self._results is None:
return
self._parent.set_results(self._solver, self._results)
self._parent.open_solver_results(self._solver, self._results)
self._solver.has_results_loaded()
+20 -1
View File
@@ -47,11 +47,19 @@ class AbstractMenu(object):
class DefaultMenu(AbstractMenu): class DefaultMenu(AbstractMenu):
def run(self): def run(self):
current_scenario = self._graph.status.scenario
if current_scenario.is_ensemble():
return
add_node = self._menu.addAction(self._trad["menu_add_scenario"]) add_node = self._menu.addAction(self._trad["menu_add_scenario"])
ensemble = self._menu.addAction(self._trad["menu_ens_scenario"])
action = self._exec() action = self._exec()
if action == add_node: if action == add_node:
self._parent.new_scenario(self._pos) self._parent.new_scenario(self._pos)
if action == ensemble:
self._parent.create_new_ensemble_scenario(self._pos)
class ScenarioMenu(AbstractMenu): class ScenarioMenu(AbstractMenu):
@@ -61,9 +69,13 @@ class ScenarioMenu(AbstractMenu):
current_scenario = item.graph._study.status.scenario.id current_scenario = item.graph._study.status.scenario.id
select = self._menu.addAction(self._trad["menu_select_scenario"]) select = None
duplicate = None duplicate = None
delete = None delete = None
edit_ens = None
if len(scenarios) > 1:
select = self._menu.addAction(self._trad["menu_select_scenario"])
if item.scenario.id != 0: if item.scenario.id != 0:
if scenarios.is_leaf(item.scenario): if scenarios.is_leaf(item.scenario):
@@ -74,6 +86,11 @@ class ScenarioMenu(AbstractMenu):
self._trad["menu_dup_scenario"] self._trad["menu_dup_scenario"]
) )
if item.scenario.is_ensemble():
edit_ens = self._menu.addAction(
self._trad["menu_edit_ens_scenario"]
)
action = self._exec() action = self._exec()
if action is None: if action is None:
return return
@@ -83,3 +100,5 @@ class ScenarioMenu(AbstractMenu):
self._parent.delete_scenario(item) self._parent.delete_scenario(item)
elif action == duplicate: elif action == duplicate:
self._parent.duplicate_scenario(item) self._parent.duplicate_scenario(item)
elif action == edit_ens:
self._parent.edit_ensemble_scenario(item)
+50 -1
View File
@@ -100,7 +100,11 @@ class ScenarioItem(QGraphicsTextItem):
) )
) )
painter.setBrush(QBrush(color)) painter.setBrush(QBrush(color))
painter.drawEllipse(self.boundingRect())
if self.scenario.is_ensemble():
painter.drawRect(self.boundingRect())
else:
painter.drawEllipse(self.boundingRect())
super(ScenarioItem, self).paint(painter, option, widget) super(ScenarioItem, self).paint(painter, option, widget)
@@ -526,6 +530,51 @@ class GraphWidget(QGraphicsView):
self.exec_with_waiting_window(fn, "duplicate_scenario") self.exec_with_waiting_window(fn, "duplicate_scenario")
self.changeScenario.emit(self.sender()) self.changeScenario.emit(self.sender())
def create_ensemble_scenario(self, item):
must_save = self.dialog_save()
if must_save == "Cancel":
return
def fn():
self._close_other_window()
if must_save == "Save":
self._study.save()
self._undo.push(
CreateEnsembleScenarioCommand(
self._study,
)
)
self.exec_with_waiting_window(fn, "create_ensemble_scenario")
self.changeScenario.emit(self.sender())
def create_new_ensemble_scenario(self, pos):
must_save = self.dialog_save()
if must_save == "Cancel":
return
def fn():
self._close_other_window()
if must_save == "Save":
self._study.save()
self._undo.push(
CreateEnsembleScenarioCommand(
self._study, pos=pos,
)
)
self.exec_with_waiting_window(fn, "create_ensemble_scenario")
self.changeScenario.emit(self.sender())
def edit_ensemble_scenario(self, item):
self.parent\
.parent\
.open_ensemble_scenario()
def _close_other_window(self): def _close_other_window(self):
self.parent\ self.parent\
.parent\ .parent\
+20
View File
@@ -98,6 +98,26 @@ class DuplicateScenariosCommand(QUndoCommand):
self._study.reload_from_scenario(self._new) self._study.reload_from_scenario(self._new)
class CreateEnsembleScenarioCommand(QUndoCommand):
def __init__(self, study, pos=None):
QUndoCommand.__init__(self)
self._study = study
self._new = None
self._pos = pos
def undo(self):
self._study.scenarios.delete(self._new.id)
self._study.reload_from_scenario(self._new.parent)
def redo(self):
if self._new is None:
self._new = self._study.create_ensemble_scenario(self._pos)
else:
self._new.set_as_not_deleted()
self._study.reload_from_scenario(self._new)
class SetCommand(QUndoCommand): class SetCommand(QUndoCommand):
def __init__(self, scenario, column, new_value): def __init__(self, scenario, column, new_value):
QUndoCommand.__init__(self) QUndoCommand.__init__(self)
+6
View File
@@ -43,6 +43,12 @@ class ScenariosTranslate(MainTranslate):
self._dict["menu_dup_scenario"] = _translate( self._dict["menu_dup_scenario"] = _translate(
"Scenarios", "Duplicate this scenario" "Scenarios", "Duplicate this scenario"
) )
self._dict["menu_ens_scenario"] = _translate(
"Scenarios", "Create ensemble scenario from current scenario"
)
self._dict["menu_edit_ens_scenario"] = _translate(
"Scenarios", "Edit this ensemble scenario"
)
self._sub_dict["table_headers_scenarios"] = { self._sub_dict["table_headers_scenarios"] = {
# "id": self._dict['id'], # "id": self._dict['id'],
+12
View File
@@ -390,6 +390,18 @@ class ASubWindowFeatures(object):
""" """
return self.find(QSpinBox, name).value() return self.find(QSpinBox, name).value()
def set_spin_box_enable(self, name: str, value: bool):
"""Set status of spinbox component
Args:
name: The spinbox component name
value: The boolean value
Returns:
Nothing
"""
self.find(QSpinBox, name).setEnabled(value)
def set_double_spin_box(self, name: str, value: int): def set_double_spin_box(self, name: str, value: int):
"""Set value of spinbox component """Set value of spinbox component
+6
View File
@@ -42,6 +42,12 @@ class PamhyrPlot(APlot):
color_plot_river_bottom = "grey" color_plot_river_bottom = "grey"
color_plot_river_water = "blue" color_plot_river_water = "blue"
color_plot_river_water_zone = "skyblue" color_plot_river_water_zone = "skyblue"
color_plot_ensemble_min = "green"
color_plot_ensemble_mean = "black"
color_plot_ensemble_max = "red"
color_plot_ensemble_std_deriv = "black"
colors = list(mplcolors.TABLEAU_COLORS) colors = list(mplcolors.TABLEAU_COLORS)
linestyle = ['solid', 'dashed', 'dashdot', 'dotted'] linestyle = ['solid', 'dashed', 'dashdot', 'dotted']
+113
View File
@@ -0,0 +1,113 @@
# PamhyrTree.py -- Pamhyr abstract table model
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from Model.Except import NotImplementedMethodeError
from PyQt5.QtCore import (
Qt, QVariant, QAbstractItemModel,
QCoreApplication, QModelIndex, pyqtSlot,
)
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem,
)
logger = logging.getLogger()
class PamhyrModelItem(QTreeWidgetItem):
def __init__(self, data=None, trad=None, parent=None):
self._data = data
self._name = self._get_name()
super(PamhyrModelItem, self).__init__(
parent, [self._name],
)
self._setup_children()
def _get_name(self):
return str(self._data)
def _setup_children(self):
return
def get_value(self):
return self._data
def find_item_by_value(self, data):
if self.get_value() == data:
return self
for i in range(self.childCount()):
item = self.child(i)
if item.get_value() == data:
return item
found = item.find_item_by_value(data)
if found:
return found
return None
class PamhyrModelListItem(QTreeWidgetItem):
def __init__(self, name=None, lst=None,
child_item_ctor=PamhyrModelItem,
trad=None, parent=None):
super(PamhyrModelListItem, self).__init__(
parent, [name],
)
self._name = name
self._lst = lst
self._trad = trad
self._child_item_ctor = child_item_ctor
self._setup_children()
def _setup_children(self):
for el in self._lst.lst:
self.addChild(
self._child_item_ctor(
data=el, trad=self._trad, parent=self
)
)
def get_value(self):
return None
def find_item_by_value(self, data):
if self.get_value() == data:
return self
for i in range(self.childCount()):
item = self.child(i)
if item.get_value() == data:
return item
found = item.find_item_by_value(data)
if found:
return found
return None
+7
View File
@@ -40,6 +40,13 @@ class CommonWordTranslate(PamhyrTranslate):
self._dict["time"] = _translate("CommonWord", "Time") self._dict["time"] = _translate("CommonWord", "Time")
self._dict["date"] = _translate("CommonWord", "Date") self._dict["date"] = _translate("CommonWord", "Date")
self._dict["low"] = _translate("CommonWord", "Low")
self._dict["high"] = _translate("CommonWord", "High")
self._dict["loc"] = _translate("CommonWord", "Loc")
self._dict["scale"] = _translate("CommonWord", "Scale")
self._dict["lam"] = _translate("CommonWord", "Lam")
self._dict["reach"] = _translate("CommonWord", "Reach") self._dict["reach"] = _translate("CommonWord", "Reach")
self._dict["reaches"] = _translate("CommonWord", "Reaches") self._dict["reaches"] = _translate("CommonWord", "Reaches")
self._dict["Select reach"] = _translate("CommonWord", "Select reach") self._dict["Select reach"] = _translate("CommonWord", "Select reach")
+2 -16
View File
@@ -47,16 +47,10 @@ class Worker(QObject):
super(self.__class__, self).__init__(parent) super(self.__class__, self).__init__(parent)
self._payload_fn = payload_fn self._payload_fn = payload_fn
self.result = None
self.exception = None
def process(self): def process(self):
try: self._payload_fn()
self.result = self._payload_fn() self.signalStatus.emit('end')
except Exception as exception:
self.exception = exception
finally:
self.signalStatus.emit('end')
class WaitingDialog(PamhyrDialog): class WaitingDialog(PamhyrDialog):
@@ -129,14 +123,6 @@ class WaitingDialog(PamhyrDialog):
self._timer = QTimer(self) self._timer = QTimer(self)
self._timer.timeout.connect(self.update_spinner) self._timer.timeout.connect(self.update_spinner)
@property
def worker_result(self):
return self._worker.result
@property
def worker_exception(self):
return self._worker.exception
def update_spinner(self): def update_spinner(self):
self._spinner_step += 1 self._spinner_step += 1
self._spinner_step %= len(self._spin) self._spinner_step %= len(self._spin)
+7
View File
@@ -18,6 +18,13 @@
<item row="0" column="0"> <item row="0" column="0">
<widget class="QTableView" name="tableView"/> <widget class="QTableView" name="tableView"/>
</item> </item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QMenuBar" name="menubar"> <widget class="QMenuBar" name="menubar">
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>416</width>
<height>81</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_0">
<property name="text">
<string>@param0</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_0">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+106
View File
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>416</width>
<height>81</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_0">
<property name="text">
<string>@param0</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_0">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_1">
<property name="text">
<string>@param1</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_1">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+157
View File
@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>875</width>
<height>415</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_name"/>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Distribution</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QComboBox" name="comboBox_distribution"/>
</item>
<item row="0" column="4">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>313</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" rowspan="2" colspan="2">
<widget class="QGroupBox" name="groupBox_target_data">
<property name="title">
<string>Target data</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeWidget" name="treeWidget_target_data">
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="2" colspan="3">
<widget class="QGroupBox" name="groupBox_params">
<property name="title">
<string>Parameters</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_params"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="2" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="2" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>152</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Draw function</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox"/>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Number of run</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>Custom seed</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_seed">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>2147483647</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_del"/>
<addaction name="action_edit"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add ensemble</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset>
<normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete selected ensembles</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit ensembles range</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
+232
View File
@@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>720</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="locale">
<locale language="English" country="Europe"/>
</property>
<widget class="QWidget" name="centralwidget">
<property name="minimumSize">
<size>
<width>874</width>
<height>0</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QSplitter" name="splitter_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QSplitter" name="splitter_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QTableView" name="tableView_reach"/>
<widget class="QWidget" name="layoutWidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTableView" name="tableView_profile"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QTableView" name="tableView_runs"/>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="firstButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-skip-backward.png</normaloff>ressources/media-skip-backward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="backButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-seek-backward.png</normaloff>ressources/media-seek-backward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="playButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="contextMenuPolicy">
<enum>Qt::DefaultContextMenu</enum>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-playback-start.png</normaloff>
<normalon>ressources/media-playback-pause.png</normalon>ressources/media-playback-start.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="nextButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-seek-forward.png</normaloff>ressources/media-seek-forward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="lastButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/media-skip-forward.png</normaloff>ressources/media-skip-forward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="horizontalSlider_time">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Water elevation</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_elevation"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Discharge time series</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_hydrograph"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>24</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_export"/>
<addaction name="action_import_data"/>
</widget>
<action name="action_export">
<property name="icon">
<iconset>
<normaloff>ressources/export.png</normaloff>ressources/export.png</iconset>
</property>
<property name="text">
<string>Export</string>
</property>
<property name="toolTip">
<string>Export data as CSV</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
</action>
<action name="action_import_data">
<property name="icon">
<iconset>
<normaloff>ressources/import.png</normaloff>ressources/import.png</iconset>
</property>
<property name="text">
<string>Import data</string>
</property>
<property name="toolTip">
<string>Import data from SCV file</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1152</width>
<height>648</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTextEdit" name="textEdit">
<property name="font">
<font>
<family>Monospace</family>
<pointsize>10</pointsize>
</font>
</property>
<property name="locale">
<locale language="English" country="Europe"/>
</property>
<property name="documentTitle">
<string notr="true"/>
</property>
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1152</width>
<height>24</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_start"/>
<addaction name="action_pause"/>
<addaction name="action_stop"/>
<addaction name="action_log_file"/>
<addaction name="action_results"/>
</widget>
<action name="action_stop">
<property name="icon">
<iconset>
<normaloff>ressources/close.png</normaloff>ressources/close.png</iconset>
</property>
<property name="text">
<string>Stop</string>
</property>
</action>
<action name="action_start">
<property name="icon">
<iconset>
<normaloff>ressources/run.png</normaloff>ressources/run.png</iconset>
</property>
<property name="text">
<string>Start</string>
</property>
</action>
<action name="action_pause">
<property name="icon">
<iconset>
<normaloff>ressources/player_pause.png</normaloff>ressources/player_pause.png</iconset>
</property>
<property name="text">
<string>Pause</string>
</property>
</action>
<action name="action_log_file">
<property name="icon">
<iconset>
<normaloff>ressources/zoom.png</normaloff>ressources/zoom.png</iconset>
</property>
<property name="text">
<string>LogFile</string>
</property>
</action>
<action name="action_results">
<property name="text">
<string>results</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>482</width>
<height>48</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_0">
<property name="text">
<string>Parameter 1</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_0"/>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>416</width>
<height>80</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_0">
<property name="text">
<string>Parameter 1</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_0"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_1">
<property name="text">
<string>Parameter 2</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_1"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>