Compare commits

..
67 changed files with 1556 additions and 5563 deletions

No files matched your search

-6
View File
@@ -1069,12 +1069,6 @@ gitlab-ci.
sudo python3 -m pip install pyinstaller
#+end_src
In addition, you must install Glab[fn:glab] to allow the runner to
proceed with the publication.
[fn:glab] The Gitlab cli releases page:
https://gitlab.com/gitlab-org/cli/-/releases
*** Windows (Wine) {{{windows}}}
The ci-runner environment for Wine need at least wine version 8, let
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

+1 -2
View File
@@ -4,7 +4,7 @@ PyQt5-sip==12.18.0
PyQtWebEngine==5.15.6
QScintilla>=2.14.1
pyqtgraph>=0.12.1
matplotlib==3.10.8
matplotlib==3.8.3
numpy>=1.24.2
colorama>=0.4.3
pyinstaller>=5.11.0
@@ -14,4 +14,3 @@ platformdirs>=4.2.0
pyshp>=2.3.1
rasterio==1.3.11
#fortranformat==2.0.3
psutil>=7.2.2
+1 -1
View File
@@ -20,4 +20,4 @@
"python-pyqtgraph" "python-qscintilla"
"python-numpy@1" "python-shapely" "python-pyshp"
"python-matplotlib" "python-colorama"
"python-rasterio" "python-psutil"))
"python-rasterio"))
+2 -2
View File
@@ -4,7 +4,7 @@ PyQt5-sip==12.18.0
#PyQtWebEngine==5.15.6
QScintilla>=2.14.1
pyqtgraph>=0.12.1
matplotlib==3.10.8
matplotlib==3.8.3
numpy>=1.24.2
colorama>=0.4.3
pyinstaller>=5.11.0
@@ -14,4 +14,4 @@ platformdirs>=4.2.0
pyshp>=2.3.1
rasterio==1.3.11
#fortranformat==2.0.3
psutil>=7.2.2
-307
View File
@@ -1,307 +0,0 @@
# Distribution.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import struct
import logging
import itertools
import numpy as np
from tools import logger_exception
from Model.Except import NotImplementedMethodeError
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
logger = logging.getLogger()
class Distribution(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Distribution, self).__init__(
id=id, status=status,
owner_scenario=0
)
self._name = name
self._type = "generic"
self._script = script
self._labels = []
self._default_values = []
self._params_checkers = []
self._params_tips = []
@property
def name(self):
return self._name
@property
def labels(self):
return self._labels
@property
def default_values(self):
return self._default_values
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble_distribution{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
script TEXT NOT NULL,
PRIMARY KEY(pamhyr_id)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, script " +
"FROM ensemble_distribution " +
f"WHERE pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
script = next(it)
new_distribution = cls(
id, name=name, script=script,
status=data["status"]
)
if deleted:
new_distribution.set_as_deleted()
loaded.add(id)
new.append(new_distribution)
return new
def _db_save(self, execute, data=None):
# Only save custom distribution
if self._type != "custom":
return True
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"ensemble_distribution(pamhyr_id, deleted, " +
" name, script) " +
"VALUES (?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._script
)
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
def is_valid_parameters(self, parameters):
valid = True
tips = []
for i, checker in enumerate(self._params_checkers):
ok = checker(parameters)
if not ok:
tips.append(self._params_tips[i])
valid = False
return (valid, tips)
def check_params(self, params):
return True
def random(self, r):
raise NotImplementedMethodeError(cls, cls._load)
###############################
# DISTRIBUTION IMPLEMENTATION #
###############################
class Uniform(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "uniform", script: str = "",
status=None):
super(Uniform, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["low", "high"]
self._default_values = [20.0, 40.0]
self._params_checkers = [self.check_params, self.check_params]
self._params_tips = ["uniform_low_tips", "uniform_high_tips"]
def check_params(self, params):
return (params[0] < params[1])
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.uniform(*lst)
class Normal(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "normal", script: str = "",
status=None):
super(Normal, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["loc", "scale"]
self._default_values = [30.0, 5.0]
self._params_checkers = [self.check_params, self.check_scale]
self._params_tips = ["normal_loc_tips", "normal_scale_tips"]
def check_scale(self, params):
return 0 <= params[1]
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.normal(*lst)
class Binomial(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "binomial", script: str = "",
status=None):
super(Binomial, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["binomial_n", "binomial_p"]
self._default_values = [100.0, 0.3]
self._params_checkers = [self.check_n, self.check_p]
self._params_tips = ["binomial_n_tips", "binomial_p_tips"]
def check_n(self, params):
n = params[0]
return (0 <= n) and (n == round(n))
def check_p(self, params):
p = params[1]
return 0 <= p <= 1
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.binomial(*lst)
class Poisson(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "poisson", script: str = "",
status=None):
super(Poisson, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["poisson_lam"]
self._default_values = [5.0]
self._params_checkers = [self.check_lam]
self._params_tips = ["poisson_lam_tips"]
def check_lam(self, params):
lam = params[0]
return 0 <= lam
def random(self, params, nb):
lst = params.copy()
lst.append(nb)
return np.random.poisson(*lst)
class Custom(Distribution):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Custom, self).__init__(
id=id, status=status,
name=name, script=script
)
self._labels = ["custom"]
self._type = "custom"
self._default_values = [""]
self._params_checkers = [self.check_params]
self._params_tips = ["custom_params_tips"]
def random(self, params, nb):
try:
# Run script to (re)define the sample distribution
f = eval(self._script)
return f(params, nb)
except Exception as e:
logger_exception(e)
return None # TODO: Raise helpful exception
-62
View File
@@ -1,62 +0,0 @@
# DistributionList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Distribution import *
class DistributionList(PamhyrModelList):
_sub_classes = [Distribution]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
# Default distributions
d = {"status": data['status']}
new._lst = [
Uniform(**d),
Normal(**d),
Binomial(**d),
Poisson(**d),
]
# DB custom distributions
new._lst += Distribution._db_load(
execute, data
)
return new
def _db_save(self, execute, data={}):
execute(
"DELETE FROM ensemble_distribution"
)
for fun in self._lst:
fun._db_save(execute, data)
return True
def new(self, index):
r = Custom(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
-279
View File
@@ -1,279 +0,0 @@
# Ensemble.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import struct
import logging
import itertools
import numpy as np
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Scenario import Scenario
from Model.Ensembles.Distribution import Uniform
logger = logging.getLogger()
class Ensemble(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
distribution=None, params=[0., 100.],
data_type=None, target_data=None,
status=None, owner_scenario=-1):
super(Ensemble, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._name = name
self._data_type = data_type
self._distribution = distribution
self._params = params
self._target_data = target_data
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble{ext} (
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
type VARCHAR(8) NOT NULL,
distribution INTEGER NOT NULL,
distribution_name VARCHAR(32) NOT NULL,
params BLOB NOT NULL,
params_len INTEGER NOT NULL,
data_pid INTEGER NOT NULL,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
)
""")
if ext == "_tmp":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
if major == "0" and int(minor) < 2:
cls._db_create(execute)
elif major == "0" and int(minor) == 2:
if int(release) < 8:
cls._db_create(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_load(cls, execute, data=None):
new = []
scenario = data["scenario"]
distributions = data["ens_distributions"]
stricklers = data["stricklers"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"distribution, distribution_name, params, params_len, data_pid, " +
"scenario " +
"FROM ensemble " +
f"WHERE scenario = {scenario.id} " +
f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})"
)
for row in table:
it = iter(row)
id = next(it)
deleted = (next(it) == 1)
name = next(it)
data_type = next(it)
fid = next(it)
fname = next(it)
params = next(it)
params_len = next(it)
data_pid = next(it)
owner_scenario = next(it)
def fn(f): return f._name == fname
if fid != -1:
def fn(f): return f._pamhyr_id == fid
distribution = next(
filter(fn, distributions.lst),
None
)
target_data = next(
filter(
lambda s: s._pamhyr_id == data_pid,
stricklers.lst
),
None
)
rdata = list(cls._decode_params(params, params_len))
new_ensemble = cls(
id, name=name,
distribution=distribution,
params=rdata,
data_type=data_type,
target_data=target_data,
status=data["status"],
owner_scenario=owner_scenario
)
if deleted:
new_ensemble.set_as_deleted()
loaded.add(id)
new.append(new_ensemble)
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM ensemble " +
f"WHERE pamhyr_id = {self.pamhyr_id} " +
f"AND scenario = {self._status.scenario_id}"
)
bparams, length = self._encode_params()
if self._distribution is None:
fid = -1
fname = "NULL"
else:
fid = self._distribution._pamhyr_id
fname = self._distribution._name
if self._distribution._type == "generic":
fid = -1
data_pid = -1
if self._target_data is not None:
data_pid = self._target_data._pamhyr_id
execute(
"INSERT INTO " +
"ensemble(pamhyr_id, deleted, name, type, " +
" distribution, distribution_name, params, params_len, " +
" data_pid, scenario) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
self.pamhyr_id, self.is_deleted(),
self._name, self._data_type,
fid, fname, bparams, length,
data_pid, self._status.scenario_id
)
return True
def _encode_params(self):
length = len(self._params)
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.pack(data_format, *self._params), length
@classmethod
def _decode_params(cls, bparams, length):
data_format = ">" + ''.join(itertools.repeat("d", length))
return struct.unpack(data_format, bparams)
def random(self, nb):
return np.nditer(
self._distribution.random(
self._params, nb
)
)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
@property
def distribution(self):
return self._distribution
@distribution.setter
def distribution(self, distribution):
self._distribution = distribution
self._status.modified()
@property
def params(self):
return self._params
@params.setter
def params(self, params):
self._params = params
self._status.modified()
@property
def data_type(self):
return self._data_type
@data_type.setter
def data_type(self, data_type):
self._data_type = data_type
self._status.modified()
@property
def target_data(self):
return self._target_data
@target_data.setter
def target_data(self, target_data):
self._target_data = target_data
self._status.modified()
def get_data_dict(self, use_ids=False):
target_data = self.target_data
distribution = self.distribution
if use_ids:
if self.target_data:
target_data = self.target_data.pamhyr_id
if self.distribution:
distribution = self.distribution.name
return {
"name": self.name,
"data_type": self.data_type,
"target_data": target_data,
"distribution": distribution,
"params": self.params,
}
def set_data_dict(self, data):
self.name = data["name"]
self.data_type = data["data_type"]
self.target_data = data["target_data"]
self.distribution = data["distribution"]
self.params = data["params"]
-62
View File
@@ -1,62 +0,0 @@
# EnsembleList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Ensemble import Ensemble
class EnsembleList(PamhyrModelList):
_sub_classes = [Ensemble]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
new._lst = Ensemble._db_load(
execute, data
)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM ensemble " +
f"WHERE scenario = {self._status.scenario_id}"
)
for ens in self._lst:
ens._db_save(execute, data=data)
return True
def new(self, index):
r = Ensemble(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
def new_with_data(self, index, data):
r = Ensemble(status=self._status)
r.set_data_dict(data)
self._lst.insert(index, r)
self._status.modified()
return r
+1 -8
View File
@@ -158,7 +158,7 @@ class ProfileXYZ(Profile, SQLSubModel):
)
if major == "0" and int(minor) <= 2:
if int(release) < 10:
if int(release) < 9:
execute(
"ALTER TABLE geometry_profileXYZ " +
"ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE"
@@ -862,13 +862,6 @@ class ProfileXYZ(Profile, SQLSubModel):
return n_zones
def froude(self, z, v):
return v / np.sqrt(
9.81 * (
self.wet_area(z) / self.wet_width(z)
)
)
@timer
def get_all_water_limits_ac(self, z):
"""
+10 -50
View File
@@ -272,9 +272,9 @@ class TableData(SQLSubModel):
if len(shape) == 2:
shape_x, shape_y = shape
elif len(shape) == 1:
elif len(shape == 1):
shape_x, = shape
elif len(shape) == 3:
elif len(shape == 3):
shape_x, shape_y, shape_z = shape
else:
logger.error(
@@ -326,14 +326,14 @@ class TableData(SQLSubModel):
if isinstance(key, tuple):
return self._data[key[0], key[1]]
self._data[key]
self._date[key]
class Results(SQLSubModel):
_sub_classes = [River, TableData, AdditionalData]
def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0", is_ensemble=False):
repertory="", name="0"):
super(Results, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
@@ -341,14 +341,11 @@ class Results(SQLSubModel):
self._name = name
self._study = study
# Solver readers add their own reaches; ensembles need them upfront.
self._river = River(self._study, self, init=is_ensemble)
self._river = River(self._study, self)
self._solver = solver
self._repertory = repertory
self._is_ensemble = is_ensemble
self._meta_data = {
# Keep results creation date
"creation_date": datetime.now(),
@@ -362,9 +359,6 @@ class Results(SQLSubModel):
if solver is not None:
self.set("solver_type", solver._type)
def is_ensemble(self):
return self._is_ensemble
@property
def date(self):
date = self._meta_data["creation_date"]
@@ -432,11 +426,9 @@ class Results(SQLSubModel):
solver_name TEXT NOT NULL,
solver_type TEXT NOT NULL,
study_revision INTEGER NOT NULL,
creation_date DATE NOT NULL,
creation_data DATE NOT NULL,
nb_timestamps INTEGER NOT NULL,
timestamps BLOB NOT NULL,
seed INTEGER,
number_of_run INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
PRIMARY KEY(pamhyr_id, scenario)
@@ -462,21 +454,6 @@ class Results(SQLSubModel):
cls._db_create(execute)
create = True
if major == "0" and int(minor) <= 2:
if int(release) < 9 and not create:
execute(
"ALTER TABLE results ADD COLUMN " +
"seed INTEGER"
)
execute(
"ALTER TABLE results ADD COLUMN " +
"number_of_run INTEGER"
)
execute(
"ALTER TABLE results RENAME COLUMN " +
"creation_data TO creation_date"
)
return cls._update_submodel(execute, version, data)
@classmethod
@@ -489,9 +466,7 @@ class Results(SQLSubModel):
table = execute(
"SELECT pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_date, " +
"seed, number_of_run, " +
"nb_timestamps, timestamps, " +
"study_revision, creation_data, nb_timestamps, timestamps, " +
"scenario " +
"FROM results " +
f"WHERE scenario = {scenario.id}"
@@ -511,8 +486,6 @@ class Results(SQLSubModel):
solver_type = next(it)
revision = next(it)
creation_date = next(it)
seed = next(it)
number_of_run = next(it)
nb_timestamps = next(it)
timestamps_bytes = next(it)
owner_scenario = next(it)
@@ -523,9 +496,6 @@ class Results(SQLSubModel):
new_results.set("solver_type", solver_type)
new_results.set("study_revision", revision)
new_results.set("creation_date", creation_date)
if seed is not None:
new_results.set("seed", seed)
new_results.set("number_of_run", number_of_run)
data["result_pid"] = pid
data["result"] = new_results
@@ -618,25 +588,15 @@ class Results(SQLSubModel):
ts = sorted(self.get("timestamps"))
sf = ">" + ''.join(itertools.repeat("d", len(ts)))
if "seed" in self._meta_data:
seed = int(self.get("seed"))
number_of_run = int(self.get("number_of_run"))
else:
seed = None
number_of_run = None
execute(
"INSERT INTO " +
"results (pamhyr_id, solver_name, solver_type, " +
"study_revision, creation_date, " +
"seed, number_of_run, " +
"study_revision, creation_data, " +
"nb_timestamps, timestamps, " +
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
self._pamhyr_id, solver_name, solver_type,
self._status.version, self.get("creation_date"),
seed, number_of_run,
len(ts), struct.pack(sf, *ts),
self._owner_scenario
len(ts), struct.pack(sf, *ts), self._owner_scenario
)
data["result"] = self._pamhyr_id
+3 -6
View File
@@ -403,7 +403,7 @@ class Reach(SQLSubModel):
class River(SQLSubModel):
_sub_classes = [Reach]
def __init__(self, study, parent, init=True):
def __init__(self, study, parent):
super(River, self).__init__(
id=-1, status=study.status,
owner_scenario=study.status.scenario.id
@@ -414,9 +414,6 @@ class River(SQLSubModel):
# Dict with timestamps as key
self._reachs = []
if init:
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
def __len__(self):
return len(self._reachs)
@@ -456,7 +453,7 @@ class River(SQLSubModel):
reach.update_water_limits(
timestamp,
data[ind[0]:ind[1] + 1]
data[ind[0]:ind[1]]
)
@classmethod
@@ -472,7 +469,7 @@ class River(SQLSubModel):
study = data["study"]
parent = data["parent"]
new_river = cls(study, parent, init=False)
new_river = cls(study, parent)
for reach in study.river.reachs():
data["reach"] = reach.reach
+1 -10
View File
@@ -69,7 +69,6 @@ from Model.WeatherParameters.WeatherParametersList \
import WeatherParametersList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results
logger = logging.getLogger()
@@ -488,7 +487,6 @@ class River(Graph):
BoundaryConditionsTemperatureList,
WeatherParametersList,
GeoTIFFList,
EnsembleList,
Results
]
@@ -534,9 +532,6 @@ class River(Graph):
self._geotiff = GeoTIFFList(status=self._status)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {}
@classmethod
@@ -655,8 +650,6 @@ class River(Graph):
new._geotiff = GeoTIFFList._db_load(execute, data)
new._ensembles = EnsembleList._db_load(execute, data)
return new
def _db_load_results(self, execute, data=None):
@@ -695,8 +688,6 @@ class River(Graph):
objs.append(self._geotiff)
objs.append(self._ensembles)
for solv_type in self.results:
objs.append(self.results[solv_type])
@@ -775,7 +766,7 @@ class River(Graph):
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
self._WeatherParameters,
self._geotiff, self._ensembles,
self._geotiff,
]
for solver in self._parameters:
+12 -29
View File
@@ -58,7 +58,6 @@ class Scenario(SQLSubModel):
"initial_conditions_temperature",
"initial_conditions_temperature_spec",
"weather_parameters", "weather_parameters_data",
"ensemble",
]
related_tables = tables_with_deleted_column + [
@@ -73,7 +72,6 @@ class Scenario(SQLSubModel):
name: str = "",
description: str = "",
x: int = 1000.0, y: int = 1000.0,
is_ensemble: bool = False,
revision: int = 0,
parent=None):
super(Scenario, self).__init__()
@@ -83,7 +81,6 @@ class Scenario(SQLSubModel):
self._x, self._y = x, y
self._name = name
self._description = description
self._is_ensemble = is_ensemble
self._revision = revision
self._parent = parent
@@ -106,7 +103,6 @@ class Scenario(SQLSubModel):
y REAL NOT NULL DEFAULT 1000,
name TEXT NOT NULL,
description TEXT NOT NULL,
is_ensemble BOOL DEFAULT FALSE,
revision INTEGER NOT NULL,
parent_id INTEGER REFERENCES scenario(id)
)
@@ -142,13 +138,6 @@ class Scenario(SQLSubModel):
cls._db_create(execute)
cls._db_add_default(execute)
if major == "0" and int(minor) == 2:
if int(release) < 8:
execute(
f"ALTER TABLE scenario " +
"ADD COLUMN is_ensemble BOOL DEFAULT FALSE"
)
return True
@classmethod
@@ -169,8 +158,7 @@ class Scenario(SQLSubModel):
scenarios = {}
table = execute(
"SELECT id, x, y, name, description, " +
" is_ensemble, revision, parent_id " +
"SELECT id, x, y, name, description, revision, parent_id " +
"FROM scenario " +
"ORDER BY id ASC"
)
@@ -182,7 +170,6 @@ class Scenario(SQLSubModel):
x, y = next(it), next(it)
name = next(it)
desc = next(it)
is_ens = next(it)
revi = next(it)
parent = next(it)
@@ -192,9 +179,7 @@ class Scenario(SQLSubModel):
new = cls(
id=id, x=x, y=y,
name=name, description=desc,
is_ensemble=is_ens,
revision=revi,
parent=parent
revision=revi, parent=parent
)
scenarios[id] = new
@@ -204,18 +189,21 @@ class Scenario(SQLSubModel):
if self.is_deleted():
return self.drop_all(execute)
parent = None
parent = 'NULL'
if self.parent is not None:
parent = self.parent._id
execute(
"INSERT OR REPLACE INTO " +
"scenario(id, x, y, name, description, " +
" is_ensemble, revision, parent_id) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
self._id, self.x, self.y,
self.name, self.description, self._is_ensemble,
self._revision, parent
"scenario(id, x, y, name, description, revision, parent_id) " +
"VALUES (" +
f"{self._id}, " +
f"{self.x}, {self.y}, " +
f"'{self._db_format(self.name)}', " +
f"'{self._db_format(self.description)}', " +
f"{self._revision}, " +
f"{parent}" +
")"
)
return True
@@ -340,9 +328,6 @@ class Scenario(SQLSubModel):
def parent(self):
return self._parent
def is_ensemble(self):
return self._is_ensemble
def set_pos(self, x, y):
self._x = x
self._y = y
@@ -360,8 +345,6 @@ class Scenario(SQLSubModel):
return self.name
if key == "description":
return self.description
if key == "is_ensemble":
return self.is_ensemble()
if key == "parent":
return self.parent
+2 -5
View File
@@ -57,11 +57,8 @@ class Scenarios(PamhyrModelDict):
return None
def new(self, parent, is_ensemble=False):
new = Scenario(
parent=parent,
is_ensemble=is_ensemble
)
def new(self, parent):
new = Scenario(parent=parent)
self.set(new._id, new)
return new
+3 -55
View File
@@ -34,7 +34,6 @@ from Model.Except import NotImplementedMethodeError
from Model.River import River
from Model.TimeShift import shift_river_times, shift_database_times
from Model.Geometry.Reach import Reach
from Model.Ensembles.DistributionList import DistributionList
from Model.HydraulicStructures.HydraulicStructures import (
HydraulicStructure
)
@@ -52,7 +51,6 @@ class Study(SQLModel):
_sub_classes = [
Scenario,
DistributionList,
River,
]
@@ -88,9 +86,6 @@ class Study(SQLModel):
self.scenarios = Scenarios(status=self.status)
self.scenarios[0] = s0
self.status.scenario = s0
self._ens_distributions = DistributionList(status=self.status)
self._river = River(status=self.status)
else:
self._init_db_file(filename, is_new=False)
@@ -411,19 +406,12 @@ class Study(SQLModel):
lambda a, s: a or (s.parent is scenario),
new.scenarios.lst,
False
) or scenario.is_ensemble():
):
new.status.set_as_read_only()
data["scenario"] = scenario
data["loaded_pid"] = set()
# Get ensemble distribution
new._ens_distributions = DistributionList._db_load(
sql_exec, data=data
)
data["ens_distributions"] = new._ens_distributions
# Load river data
new._river = River._db_load(
sql_exec, data=data
@@ -495,27 +483,8 @@ class Study(SQLModel):
raise
self._pending_time_offset = 0.0
if self.status.scenario.is_ensemble():
lst = [
self.scenarios, self._ens_distributions,
self._river._ensembles,
]
for solv in self._river._results:
lst.append(self._river._results[solv])
self._save_submodel(
lst, data=progress
)
progress()
self.commit()
return
self._save_submodel(
[
self.scenarios, self._ens_distributions,
self._river
],
[self.scenarios, self._river],
data=progress
)
@@ -633,8 +602,7 @@ class Study(SQLModel):
data = {
"status": self.status,
"loaded_pid": set(),
"scenario": scenario,
"ens_distributions": self._ens_distributions,
"scenario": scenario
}
self.status.start_loading()
@@ -711,7 +679,6 @@ class Study(SQLModel):
self.status.scenario.y + 100)
new.name = self.status.scenario.name + " (copy)"
new.is_ensemble = self.status.scenario.is_ensemble
self.status.scenario = new
self.river._data_traversal(
@@ -723,25 +690,6 @@ class Study(SQLModel):
self.status.set_as_editable()
return new
def create_ensemble_scenario(self, pos=None, switch=True):
new = self.scenarios.new(
self.status.scenario,
is_ensemble=True
)
new.name = self.status.scenario.name + " (ensemble)"
if pos is not None:
new.set_pos(pos.x(), pos.y())
else:
new.set_pos(self.status.scenario.x + 100,
self.status.scenario.y)
if switch:
self.status.scenario = new
self.status.set_as_read_only()
return new
@property
def results(self):
return self._river.results
-7
View File
@@ -185,13 +185,6 @@ class SQLSubModel(PamhyrID):
else:
self._owner_scenario = owner_scenario
def __str__(self):
t = type(self)
return (
f"'{t.__module__}.{t.__name__}" +
f"@{self._pamhyr_id}.{self._owner_scenario}'"
)
def must_be_saved(self):
"""Return True if this object MUST be save in the save file.
@@ -341,7 +341,7 @@ class WeatherParameters(SQLSubModel):
created = False
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 10)):
(int(minor) == 2 and int(release) <= 7)):
if not cls.is_table_exists(execute, "air_temperature"):
cls._db_create(execute)
created = True
+6 -1
View File
@@ -25,6 +25,7 @@ from PyQt5.QtCore import QProcess
from Scripts.AScript import AScript
from Model.Study import Study
from View.RunSolver.Translate import RunSolverTranslate
logger = logging.getLogger()
@@ -89,12 +90,16 @@ class ScriptRun(AScript):
# Export and Run
logger.info(f"~Export~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
solver.export(study, workdir, qlog=self._q)
ok = solver.export(study, workdir, qlog=self._q)
while self._q.qsize() != 0:
s = self._q.get()
logger.info(s)
if not ok:
logger.warning(RunSolverTranslate()["export_failed"])
return
if command == "run":
logger.info(f"~Run~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
solver.run(
-234
View File
@@ -1063,75 +1063,6 @@ class Mage8(Mage):
logger_exception(e)
return False
def export_ensemble_variation(self, study, repertory,
qlog=None, name="0",
samples=None):
self._study = study
name = study.name.replace(" ", "_")
var_samples = self._export_ensemble_RUG(
study, repertory,
qlog, name=name,
samples=samples,
)
return var_samples
def _export_ensemble_RUG(self, study, repertory,
qlog, name="0",
samples={}):
files = []
var_samples = []
for ens in samples:
if ens.target_data is None:
continue
x = next(samples[ens])
var_samples.append(x)
qlog.put(f"*** Variation " +
f"'{ens.target_data}' - {ens.data_type} x {x}")
if ens.data_type == "strickler_minor":
ens.target_data.minor = x
elif ens.data_type == "strickler_medium":
ens.target_data.medium = x
if qlog is not None:
qlog.put("Export variation of RUG file")
# Write header
with mage_file_open(os.path.join(repertory, f"{name}.RUG"), "w+") as f:
files.append(f"{name}.RUG")
edges = study.river.enable_edges()
id = 1
for edge in edges:
frictions = edge.frictions
for friction in frictions.frictions:
if friction.begin_strickler is None:
continue
if friction.is_deleted():
continue
num = f"{id:>3}"
brk = f"{friction.begin_rk:>10.3f}"
erk = f"{friction.end_rk:>10.3f}"
strickler = friction.begin_strickler
coef_1 = f"{strickler.minor:>10.3f}"
coef_2 = f"{strickler.medium:>10.3f}"
f.write(f"K{num} {brk}{erk}{coef_1}{coef_2}\n")
id += 1
return var_samples
###########
# RESULTS #
###########
@@ -1562,171 +1493,6 @@ class Mage8(Mage):
return results
@timer
def read_ensemble_bin(self, study, fname, tables, run_number, qlog=None):
logger.info(f"read_ensemble_bin: Start reading '{fname}' ...")
start = time.time()
with mage_file_open(fname, "r") as f:
def newline(): return np.fromfile(f, dtype=np.int32, count=1)
def endline(): return np.fromfile(f, dtype=np.int32, count=1)
def read_int(size): return np.fromfile(
f, dtype=np.int32, count=size)
def read_float(size): return np.fromfile(
f, dtype=np.float32, count=size)
def read_float64(size): return np.fromfile(
f, dtype=np.float64, count=size)
# Meta data (1st line)
newline()
data = read_int(3)
nb_reach = data[0]
nb_profile = data[1]
mage_version = data[2]
if mage_version <= 80:
msg = (
"Read BIN files: " +
f"Possible incompatible mage version '{mage_version}', " +
"please check your solver configuration..."
)
logger.warning(msg)
if qlog is not None:
qlog.put("[WARNING] " + msg)
endline()
# Reach information (2nd line)
newline()
_ = read_int(2*nb_reach)
endline()
# X (3rd line)
newline()
_ = read_float(nb_profile)
endline()
# Z and Y (4th line)
newline()
_ = read_float(3*nb_profile)
endline()
# Data
newline()
ts = set()
tmp_table = {}
end = False
while not end:
n = read_int(1)[0]
timestamp = read_float64(1)[0]
key = bytearray(
np.fromfile(
f, dtype=np.byte, count=1
)
).decode()
data = read_float(n)
if key not in tmp_table:
tmp_table[key] = []
tmp_table[key].append(data)
ts.add(timestamp)
endline()
end = newline().size <= 0
table = {}
for k in tmp_table:
table[k] = np.array(tmp_table[k])
self.read_ensemble_bin_agregate_data(
table, tables, run_number
)
tables["timestamps"] = ts
logger.info(f"reading time: '{time.time() - start}'")
def read_ensemble_bin_agregate_data(self, tables, res, run_number):
z = tables["Z"]
q = tables["Q"]
if "z_mean" not in res:
res["z_mean"] = z
res["q_mean"] = q
res["z_tmp_M2"] = np.zeros(z.shape)
res["q_tmp_M2"] = np.zeros(q.shape)
res["z_std_derivation"] = np.zeros(z.shape)
res["q_std_derivation"] = np.zeros(q.shape)
res["z_min"] = z
res["z_max"] = z
res["q_min"] = q
res["q_max"] = q
else:
# Get mean results for Z and Q
n = run_number
for x, txt in [(z, "z"), (q, "q")]:
t_mean = f"{txt}_mean"
t_M2 = f"{txt}_tmp_M2"
t_std_deriv = f"{txt}_std_derivation"
delta = x - res[t_mean]
res[t_mean] += delta / n
delta_2 = x - res[t_mean]
res[t_M2] += delta * delta_2
res[t_std_deriv] = np.sqrt(res[t_M2] / n)
# Z and Q min max values
res["z_min"] = np.minimum(z, res["z_min"])
res["z_max"] = np.maximum(z, res["z_max"])
res["q_min"] = np.minimum(q, res["q_min"])
res["q_max"] = np.maximum(q, res["q_max"])
@timer
def results_ensemble(self, study, repertory,
tables, run_number,
qlog=None, name=None):
self._study = study
if name is None:
name = study.name.replace(" ", "_")
fname = os.path.join(repertory, f"{name}.BIN".replace(" ", "_"))
if not os.path.isfile(fname):
logger.info(f"Result file {name}.BIN does not exist")
return None
try:
self.read_ensemble_bin(study, fname, tables, run_number, qlog)
except Exception as e:
logger.error(f"Failed to read results")
logger_exception(e)
return None
return True
def get_ensemble_empty_results(self, study, repertory, name=None):
return Results(
study=study,
solver=self,
repertory=repertory,
name=name,
is_ensemble=True,
)
class MageFake7(Mage8):
_type = "mage_fake7"
+261 -106
View File
@@ -18,11 +18,14 @@
import os
import logging
import math
from decimal import Decimal, InvalidOperation, localcontext
import numpy as np
from tools import timer, trace, old_pamhyr_date_to_timestamp, logger_exception
from Solver.CommandLine import CommandLineSolver
from View.RunSolver.Translate import RunSolverTranslate
from Model.Results.Results import Results
from Model.Results.River.River import River, Reach, Profile
@@ -44,7 +47,7 @@ class Rubar3(CommandLineSolver):
@classmethod
def default_parameters(cls):
# lst = super(Rubar3, cls).default_parameters()
# Keep historical rubarbe_* keys for existing Rubar3 studies.
lst = [
("rubarbe_cfl", "0.50000E+00"),
@@ -52,13 +55,13 @@ class Rubar3(CommandLineSolver):
("rubarbe_condav", "3"),
("rubarbe_regime", "0"),
("rubarbe_iodev", "n"),
("rubarbe_iodebord", ""),
("rubarbe_iostockage", ""),
("rubarbe_iodebord", "o"),
("rubarbe_iostockage", "o"),
("rubarbe_iopdt", "y"),
("rubarbe_iovis", "n"),
("rubarbe_rep", "n"),
("rubarbe_tinit", "000:00:00:00"),
("rubarbe_tmax", "000:01:00:00"), # 1 day
("rubarbe_tmax", "001:00:00:00"), # 1 day
("rubarbe_tiopdt", "000:00:00:00"),
("rubarbe_dt", "5.0"),
("rubarbe_ts", "999:99:99:00"),
@@ -89,8 +92,8 @@ class Rubar3(CommandLineSolver):
("rubarbe_sediment_mult_3", ""),
("rubarbe_sediment_mult_4", ""),
("rubarbe_sediment_mult_5", ""),
("rubarbe_sediment_visc", "0.047"),
("rubarbe_sediment_opts", "6"),
("rubarbe_sediment_visc", "1.3E-06"),
("rubarbe_sediment_opts", "3"),
("rubarbe_sediment_odchar", "0"),
("rubarbe_sediment_unisol", "1"),
("rubarbe_sediment_typdef", "3"),
@@ -140,8 +143,34 @@ class Rubar3(CommandLineSolver):
self._study = study
name = study.name.replace(" ", "_")
values = self._parameter_values(study)
if (values.get("rubarbe_regime") == "1" and
values.get("rubarbe_condam") == "0"):
trad = RunSolverTranslate()
message = trad["incompatible_upstream_boundary"]
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
trased = self._parameter_flag(study, "trased") == "O"
if trased:
# Notice §3.6: HYDROS is required at each upstream boundary
missing = self._reaches_without_solid_boundary(study)
if missing:
trad = RunSolverTranslate()
message = trad["missing_solid_boundary"].format(
reaches=", ".join(missing)
)
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
self._export_donnee(study, repertory, qlog, name=name)
self._export_ts(study, repertory, qlog, name=name)
if trased:
if not self._export_ts(study, repertory, qlog, name=name):
return False
self._export_geomac_i(study, repertory, qlog, name=name)
self._export_mail(study, repertory, qlog, name=name)
self._export_devers(study, repertory, qlog, name=name)
@@ -153,108 +182,205 @@ class Rubar3(CommandLineSolver):
return True
def _export_donnee(self, study, repertory, qlog, name="0"):
# Notice Rubar3/RubarBE, annex 1: records are positional, not keyed.
# Each record starts with an A50 label, including records of integers.
_donnee_fields = (
("cfl", "real"),
("condam", "integer"), ("condav", "integer"),
("regime", "integer"), ("iodev", "flags"),
("iopdt", "flag"), ("iovis", "flag"), ("rep", "flag"),
("tinit", "time"), ("tmax", "time"), ("tiopdt", "time"),
("dt", "real"), ("ts", "time"), ("dtsauv", "time"),
("psave", "time"), ("fdeb1", "real"), ("fdeb2", "real"),
("fdeb3", "real"),
("tf_1", "flag"), ("tf_2", "flag"), ("tf_3", "flag"),
("tf_4", "flag"), ("tf_5", "flag"), ("tf_6", "flag"),
("trased", "flag"),
)
_ts_fields = (
("ros", "real"), ("por", "real"), ("dcharg", "real"),
("halfa", "real"), ("mult_1", "multipliers"),
("visc", "real"), ("opts", "law"), ("odchar", "integer"),
("unisol", "integer"), ("typdef", "integer"),
("depot", "depot"), ("choixc", "integer"),
("option", "options"), ("capsol", "integer"),
("bmiu", "real"), ("demix", "integer"),
("defond", "integer"), ("varcons", "integer"),
)
@staticmethod
def _reaches_without_solid_boundary(study):
river = study.river
solid = [
bc for bc in river.boundary_condition.get_tab("solid")
if not bc.is_deleted() and bc.bctype == "SL" and len(bc) > 0
]
return [
edge.name for edge in river.enable_edges()
if river.is_upstream_node(edge.node1)
and not any(bc.node == edge.node1 for bc in solid)
]
def _parameter_values(self, study):
return {p.name: str(p.value).strip()
for p in study.river.get_params(self.type).parameters}
@staticmethod
def _required(values, key):
value = values.get(key, "")
if not value:
raise ValueError(f"Missing Rubar parameter: {key}")
return value
@staticmethod
def _flag(value, key, mixed=False):
result = value.upper()
if result == "Y":
result = "O"
if result not in (("O", "N", "M") if mixed else ("O", "N")):
raise ValueError(f"{key}: expected O/Y or N" +
(" or M" if mixed else ""))
return result
def _parameter_flag(self, study, suffix):
key = "rubarbe_" + suffix
return self._flag(self._required(self._parameter_values(study), key),
key)
@staticmethod
def _integer(value, key, width=1):
if not value.isascii() or not value.isdigit() or len(value) > width:
raise ValueError(f"{key}: expected an integer in {width} columns")
return value.rjust(width)
@staticmethod
def _real(value, key, scientific=False):
try:
number = Decimal(value.replace("d", "E").replace("D", "E"))
except InvalidOperation:
raise ValueError(f"{key}: invalid real value {value!r}") from None
if not number.is_finite():
raise ValueError(f"{key}: expected a finite real value")
if scientific:
# DONNEE: E12.5. Reject overflow instead of shifting columns.
floating = float(number)
if not math.isfinite(floating) or (number and floating == 0):
raise ValueError(f"{key}: real value outside E12.5 range")
result = f"{floating:12.5E}"
width = 12
else:
# TS: F10.0 input accepts an explicit decimal point/exponent.
# Preserve the value exactly; do not round porosity etc. to int.
with localcontext() as context:
context.prec = max(28, len(number.as_tuple().digits))
number = number.normalize()
result = str(number).upper()
if len(result) > 10:
result = format(number, "E")
width = 10
if len(result) > width:
raise ValueError(f"{key}: value {value!r} exceeds {width} columns")
return result.rjust(width)
@staticmethod
def _write_parameters(repertory, filename, lines):
# Validate all records before opening/truncating the destination.
path = os.path.join(repertory, filename)
with open(path, "w", encoding="ascii") as f:
f.writelines(lines)
def _export_donnee(self, study, repertory, qlog=None, name="0"):
values = self._parameter_values(study)
fields = list(self._donnee_fields)
# The diagnostic transport-capacity extension is optional as a block.
if values.get("rubarbe_optfpc", ""):
fields += [("optfpc", "integer"), ("ros", "real"),
("dm", "real"), ("segma", "real")]
elif any(values.get("rubarbe_" + key, "")
for key in ("ros", "dm", "segma")):
raise ValueError("rubarbe_optfpc: required before ROS/DM/SEGMA")
lines = []
for suffix, kind in fields:
key = "rubarbe_" + suffix
value = self._required(values, key)
if kind == "integer":
result = self._integer(value, key)
elif kind in ("flag", "flags"):
result = self._flag(value, key, mixed=suffix == "iopdt")
if kind == "flags":
# Missing optional flags mean O (notice section 3.1).
# Write both explicitly so STOCKAGE cannot shift left.
for extra in ("iodebord", "iostockage"):
extra_key = "rubarbe_" + extra
result += self._flag(values.get(extra_key) or "O",
extra_key)
else:
if kind == "time" and ":" in value:
parts = value.split(":")
if len(parts) != 4 or not all(p.isdigit() for p in parts):
raise ValueError(
f"{key}: expected days:hours:minutes:seconds")
value = str(old_pamhyr_date_to_timestamp(value))
result = self._real(value, key, scientific=True)
lines.append(f"{key:<50}{result}\n")
if qlog is not None:
qlog.put("Export DONNEE file")
self._write_parameters(repertory, f"donnee.{name}", lines)
with open(
os.path.join(
repertory, f"donnee.{name}"
), "w+"
) as f:
params = filter(
lambda p: "rubarbe_sediment_" not in p.name,
study.river.get_params(self._type).parameters
)
it = iter(params)
param = next(it, None)
while param is not None:
name = param.name
value = param.value
if "all_" in name:
continue
if value != "":
# Value format
if value.count(':') == 3:
value = old_pamhyr_date_to_timestamp(value)
value = f"{value:>12.5e}".upper()
if value.count('.') == 1:
value = f"{float(value):>12.5e}".upper()
if value == "y" or value == "n":
value = "O" if value == "y" else "N"
# Write value
f.write(f"{name:<50}{value}")
# Add values of 'rubarbe_iodebord' and
# 'rubarbe_iostockage'
if name == "rubarbe_iodev":
v2 = next(it).value
v3 = next(it).value
# f.write(f"{v2}{v3}")
# New line
f.write(f"\n")
param = next(it, None)
def _export_ts(self, study, repertory, qlog, name="0"):
def _export_ts(self, study, repertory, qlog=None, name="0"):
trad = RunSolverTranslate()
values = self._parameter_values(study)
prefix = "rubarbe_sediment_"
fields = list(self._ts_fields)
demix = self._required(values, prefix + "demix")
if demix not in ("0", "1"):
raise ValueError(f"{prefix}demix: expected 0 or 1")
if demix == "1":
fields += [("dchard", "real"), ("dchars", "real")]
lines = []
for suffix, kind in fields:
key = prefix + suffix
value = self._required(values, key)
if kind == "real":
result = self._real(value, key)
elif kind == "multipliers":
result = self._real(value, key)
extras = [values.get(prefix + f"mult_{i}", "")
for i in range(2, 6)]
# Preserve empty interior F10.0 fields; omit trailing fields
# to retain the solver's defaults for optional coefficients.
while extras and not extras[-1]:
extras.pop()
for i, extra in enumerate(extras, 2):
result += (self._real(extra, prefix + f"mult_{i}")
if extra else " " * 10)
elif kind == "options":
if len(value) != 3 or any(c not in "12" for c in value):
message = trad["invalid_options"].format(key=key)
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
result = value
elif kind == "law":
# Notice section 3.10 / 8.7: 4--7 are not listed.
if value not in {str(i) for i in [1, 2, 3, *range(8, 31)]}:
message = trad["unsupported_transport_law"].format(
key=key, value=value)
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
result = self._integer(value, key, width=2)
else:
result = self._integer(value, key,
width=10 if kind == "depot" else 1)
lines.append(f"{key:<50}{result}\n")
if qlog is not None:
qlog.put("Export TS file")
with open(
os.path.join(
repertory, f"ts.{name}"
), "w+"
) as f:
def float_format(string):
if "." in string:
return f"{float(string):>10.0f}"
return ""
params = filter(
lambda p: "rubarbe_sediment_" in p.name,
study.river.get_params(self.type).parameters
)
it = iter(params)
line = 0
while line < 20:
param = next(it)
name = param.name
value = param.value
if value != "":
# Value format
if value.count('.') == 1:
value = f"{float_format(value)}"
else:
value = f"{value:>10}"
# Write value
f.write(f"{name:<50}{value}")
# Add values of 'rubarbe_iodebord' and
# 'rubarbe_iostockage'
if name == "rubarbe_sediment_mult_1":
m2 = f"{float_format(next(it).value)}"
m3 = f"{float_format(next(it).value)}"
m4 = f"{float_format(next(it).value)}"
m5 = f"{float_format(next(it).value)}"
f.write(f"{m2}{m3}{m4}{m5}")
# New line
f.write(f"\n")
line += 1
self._write_parameters(repertory, f"ts.{name}", lines)
return True
def _export_geomac_i(self, study, repertory, qlog, name="0"):
if qlog is not None:
@@ -616,6 +742,7 @@ class Rubar3(CommandLineSolver):
)).value
if timestamp.count(':') == 3:
timestamp = old_pamhyr_date_to_timestamp(timestamp)
timestamp = float(timestamp)
ts.add(timestamp)
# add initial condition
@@ -636,8 +763,7 @@ class Rubar3(CommandLineSolver):
while True:
line = f.readline()
if line == "":
results.set("timestamps", ts)
return
break
timestamp = float(line)
ts.add(timestamp)
@@ -671,6 +797,24 @@ class Rubar3(CommandLineSolver):
h, s, q, z = read_data_line(f)
set_and_compute_limites(reach, ind, z+h, q, s)
# Tables and plots use timestamp rows and global profile columns.
timestamps = sorted(ts)
profiles = []
for reach, _ in reachs:
offset = len(profiles)
profiles.extend(reach.profiles)
reach.set_global_index(range(offset, len(profiles)))
results.set("timestamps", timestamps)
results.compute_timestamp_index()
results.set("table", {
key: np.array([
[profile.get_ts_key(t, key) for profile in profiles]
for t in timestamps
], dtype=float)
for key in ("Z", "Q", "V")
})
@timer
def write_bin(self, study, fname, results, qlog=None, name="0"):
logger.info(f"write_bin: Start writing '{fname}' ...")
@@ -800,3 +944,14 @@ class RubarBE(Rubar3):
self._cmd_input = ""
self._cmd_solver = "@path @args @input"
self._cmd_output = ""
@classmethod
def default_parameters(cls):
# New BE studies evolve the bed. Keep Rubar3 defaults unchanged.
overrides = {
"rubarbe_trased": "y",
"rubarbe_sediment_opts": "1", # Meyer-Peter and Mueller
"rubarbe_sediment_option": "212", # no Ikeda, given Shields, total
}
return [(key, overrides.get(key, value))
for key, value in super().default_parameters()]
+2 -2
View File
@@ -35,7 +35,7 @@ solver_long_name = {
# "mage_fake7": "Mage fake v7",
"adistswc": "Adis-TS_WC",
"adisttwc": "Adis-TT_WC",
# "rubarbe": "RubarBE",
"rubarbe": "RubarBE",
"rubar3": "Rubar3",
}
@@ -46,6 +46,6 @@ solver_type_list = {
# "mage_fake7": MageFake7,
"adistswc": AdisTSwc,
"adisttwc": AdisTTwc,
# "rubarbe": RubarBE,
"rubarbe": RubarBE,
"rubar3": Rubar3,
}
-15
View File
@@ -16,8 +16,6 @@
# -*- coding: utf-8 -*-
import os
import psutil
import logging
import traceback
@@ -122,10 +120,7 @@ class TimerWindow(PamhyrWindow):
parent=parent
)
self._process = psutil.Process(os.getpid())
self.setup_table()
self.setup_label()
self.setup_alarm()
self.setup_connections()
@@ -141,15 +136,6 @@ class TimerWindow(PamhyrWindow):
data=None,
)
def setup_label(self):
label = self.find(QLabel, "label")
label.setText(
f" Memory usage:\t" +
"<b>" +
f"{self._process.memory_info().rss / (1000 * 1000):.3f}" +
"</b> MB"
)
def setup_alarm(self):
self._alarm = QTimer()
self._alarm.start(2000)
@@ -162,7 +148,6 @@ class TimerWindow(PamhyrWindow):
def update(self):
self._table.update_lst()
self.setup_label()
# def reset(self):
# self._table.reset_timers()
-64
View File
@@ -1,64 +0,0 @@
# Translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Ensembles.Translate import EnsemblesTranslate
_translate = QCoreApplication.translate
class EnsembleEditTranslate(EnsemblesTranslate):
def __init__(self):
super(EnsembleEditTranslate, self).__init__()
self._dict["binomial_n"] = _translate("Ensemble", "n")
self._dict["binomial_p"] = _translate("Ensemble", "p")
self._dict["poisson_lam"] = _translate("Ensemble", "Lam")
self._dict["checker_failed_title"] = _translate(
"Ensemble",
"Invalid parameter"
)
self._dict["checker_failed_text"] = _translate(
"Ensemble",
"This parameter is not valid for selected distribution: "
)
uniform = _translate(
"Ensemble", "(High - Low) must be non-negative."
)
self._dict["uniform_low_tips"] = uniform
self._dict["uniform_high_tips"] = uniform
self._dict["normal_scale_tips"] = _translate(
"Ensemble", "Scale must be non-negative."
)
self._dict["binomial_n_tips"] = _translate(
"Ensemble", "N must be non-negative and must be an integers."
)
self._dict["binomial_p_tips"] = _translate(
"Ensemble", "P must be >= 0 and <=1."
)
self._dict["poisson_lam_tips"] = _translate(
"Ensemble", "Lam must be >= 0."
)
-320
View File
@@ -1,320 +0,0 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow, PamhyrDialog
from View.Tools.PamhyrWidget import PamhyrWidget
from View.Tools.PamhyrTree import (
PamhyrModelItem, PamhyrModelListItem,
)
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QSettings,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QGroupBox, QTreeWidget,
)
from View.Ensembles.Edit.Translate import EnsembleEditTranslate
logger = logging.getLogger()
class StricklersCoefModelItem(PamhyrModelItem):
def get_value(self):
return (self._data, self.parent()._data)
class StricklersModelItem(PamhyrModelItem):
def __init__(self, data=None, trad=None, parent=None):
super(StricklersModelItem, self).__init__(
data=data, trad=trad, parent=parent,
)
def _setup_children(self):
for coef in ["strickler_minor", "strickler_medium"]:
self.addChild(
StricklersCoefModelItem(data=coef, parent=self)
)
def get_value(self):
return self._data
class EnsembleEditDialog(PamhyrDialog):
_pamhyr_ui = "EnsembleEditDialog"
_pamhyr_name = "Parameters"
def __init__(self, study=None, ensemble=None,
parent=None):
trad = EnsembleEditTranslate()
super(EnsembleEditDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._study = study
self._ensemble = ensemble
self._target_data = None
self._tree = self.find(QTreeWidget, "treeWidget_target_data")
self._tg_group_box = self.find(QGroupBox, "groupBox_target_data")
self._init_tree()
self._init_default_values()
self._init_connection()
def _init_tree(self):
self._tree.setColumnCount(1)
self._tree.setHeaderLabels(["Name"])
stricklers = PamhyrModelListItem(
name="Stricklers",
lst=self._study.river.stricklers,
child_item_ctor=StricklersModelItem,
parent=self._tree
)
self._target_data = (
self._ensemble.data_type,
self._ensemble.target_data
)
self._tg_group_box.setTitle(
self._format_target_data(self._target_data)
)
item = stricklers.find_item_by_value(self._target_data)
if item:
self._tree.setCurrentItem(item)
def _init_default_values(self):
self.set_line_edit_text("lineEdit_name", self._ensemble.name)
self.combobox_add_items(
"comboBox_distribution",
[self._trad["not_defined"]] +
list(
map(
lambda d: d.name,
self._study._ens_distributions.lst
)
)
)
self._params_widget = None
if self._ensemble.distribution is not None:
self.set_combobox_text("comboBox_distribution",
self._ensemble.distribution.name)
self._set_widget(self._ensemble.distribution)
def _init_connection(self):
self.find(QComboBox, "comboBox_distribution")\
.currentTextChanged.connect(self.update_distribution)
self._tree.itemClicked.connect(self._tree_item_select)
def _tree_item_select(self, item):
if type(item) is StricklersCoefModelItem:
self._target_data = item.get_value()
self._tg_group_box.setTitle(
self._format_target_data(self._target_data)
)
def _format_target_data(self, data):
return f" Selection: {str(data[1])}, {data[0]}"
def update_distribution(self, txt):
# txt = self.get_combobox_text("comboBox_distribution")
distribution = next(
filter(
lambda d: d.name == txt,
self._study._ens_distributions.lst
),
None
)
self._set_widget(distribution)
def _set_widget(self, distribution):
box = self.find(QGroupBox, "groupBox_params")
vbox = self.find(QVBoxLayout, "verticalLayout_params")
if self._params_widget is not None:
vbox.removeWidget(self._params_widget)
self._params_widget.setParent(None)
if distribution is None:
return
size = len(distribution._labels)
if size == 1:
self._params_widget = Ensemble1ParamsWidget(
ensemble=self._ensemble,
distribution=distribution,
trad=self._trad, parent=box,
)
vbox.addWidget(self._params_widget)
elif size == 2:
self._params_widget = Ensemble2ParamsWidget(
ensemble=self._ensemble,
distribution=distribution,
trad=self._trad, parent=box,
)
vbox.addWidget(self._params_widget)
else:
self._params_widget = None
@property
def values(self):
return self._values
def accept(self):
txt = self.get_combobox_text("comboBox_distribution")
distribution = next(
filter(
lambda d: d.name == txt,
self._study._ens_distributions.lst
),
None
)
parameters = self._params_widget.values
if distribution is not None:
ok, tips = distribution.is_valid_parameters(parameters)
if not ok:
self.message_box(
text=self._trad["checker_failed_text"],
informative_text="\n".join(
map(lambda t: " - " + self._trad[t], tips)
),
window_title=self._trad["checker_failed_title"],
)
return
self._values = {
"name": self.get_line_edit_text("lineEdit_name"),
"data_type": self._target_data[0],
"target_data": self._target_data[1],
"distribution": distribution,
"params": parameters,
}
super().accept()
def reject(self):
self.close()
class Ensemble1ParamsWidget(PamhyrWidget):
_pamhyr_ui = "Ensemble1ParamsWidget"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
distribution=None,
trad=None, parent=None):
super(Ensemble1ParamsWidget, self).__init__(
parent=parent
)
self._trad = trad
self._ensemble = ensemble
self._distribution = distribution
self._labels = distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) != 1:
self._values = self._distribution.default_values.copy()
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
@property
def values(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
]
return self._values
class Ensemble2ParamsWidget(PamhyrWidget):
_pamhyr_ui = "Ensemble2ParamsWidget"
_pamhyr_name = "Parameters"
def __init__(self, ensemble=None,
distribution=None,
trad=None, parent=None):
super(Ensemble2ParamsWidget, self).__init__(
parent=parent
)
self._trad = trad
self._ensemble = ensemble
self._distribution = distribution
self._labels = distribution.labels
self._init_default_labels()
self._init_default_values()
def _init_default_labels(self):
self.set_label_text("label_0", self._trad[self._labels[0]])
self.set_label_text("label_1", self._trad[self._labels[1]])
def _init_default_values(self):
self._values = self._ensemble.params
if len(self._values) != 2:
self._values = self._distribution.default_values.copy()
self.set_double_spin_box("doubleSpinBox_0", self._values[0])
self.set_double_spin_box("doubleSpinBox_1", self._values[1])
@property
def values(self):
self._values = [
self.get_double_spin_box("doubleSpinBox_0"),
self.get_double_spin_box("doubleSpinBox_1")
]
return self._values
-268
View File
@@ -1,268 +0,0 @@
# 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
@@ -1,50 +0,0 @@
# Translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class EnsemblesTranslate(MainTranslate):
def __init__(self):
super(EnsemblesTranslate, self).__init__()
self._dict["Ensembles"] = _translate(
"Ensembles", "Ensembles"
)
self._dict["ensemble"] = _translate(
"Ensembles", "Ensembles scenario"
)
self._dict["strickler_minor"] = _translate(
"Ensembles", "Stricklers minor"
)
self._dict["strickler_medium"] = _translate(
"Ensembles", "Stricklers medium"
)
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"type": _translate("Ensembles", "Type"),
"target_data": _translate("Ensembles", "Target data"),
"distribution": _translate("Ensembles", "Distribution"),
"parameters": _translate("Ensembles", "Parameters"),
}
-178
View File
@@ -1,178 +0,0 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.Friction.Friction import Friction
from Model.Friction.FrictionList import FrictionList
class SetNameCommand(QUndoCommand):
def __init__(self, ensembles, index, new_value):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index).name
self._new = str(new_value)
def undo(self):
self._ensembles.get(self._index).name = self._old
def redo(self):
self._ensembles.get(self._index).name = self._new
class SetTypeCommand(QUndoCommand):
def __init__(self, ensembles, index, new_value):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index)\
.data_type
self._new = new_value
def undo(self):
self._ensembles.get(self._index).data_type = self._old
def redo(self):
self._ensembles.get(self._index).data_type = self._new
class SetDataCommand(QUndoCommand):
def __init__(self, ensembles, index, data):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index)\
.target_data
self._new = data
def undo(self):
self._ensembles.get(self._index).target_data = self._old
def redo(self):
self._ensembles.get(self._index).target_data = self._new
class SetDistributionCommand(QUndoCommand):
def __init__(self, ensembles, index, distribution):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index).distribution
self._old_params = self._ensembles.get(self._index).params
self._new = distribution
def undo(self):
self._ensembles.get(self._index).distribution = self._old
self._ensembles.get(self._index).params = self._old_params
def redo(self):
self._ensembles.get(self._index).distribution = self._new
if self._new is not None:
self._ensembles.get(self._index).params = self._new.default_values
class AddCommand(QUndoCommand):
def __init__(self, ensembles, index):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._new = None
def undo(self):
self._ensembles.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._ensembles.new(self._index)
else:
self._ensembles.insert(self._index, self._new)
class ImportCommand(QUndoCommand):
def __init__(self, ensembles, data, index):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._data = data
self._news = None
def undo(self):
for new in self._news:
new.set_as_deleted()
def redo(self):
if self._news is None:
self._news = list(
map(
lambda d: self._ensembles.new_with_data(
self._index, d
),
self._data,
)
)
else:
for new in self._news:
new.set_as_not_deleted()
class DelCommand(QUndoCommand):
def __init__(self, ensembles, rows):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._rows = rows
self._friction = []
for row in rows:
self._friction.append((row, self._ensembles.get(row)))
self._friction.sort()
def undo(self):
for row, el in self._friction:
self._ensembles.insert(row, el)
def redo(self):
self._ensembles.delete_i(self._rows)
class SetAllCommand(QUndoCommand):
def __init__(self, ensemble, data):
QUndoCommand.__init__(self)
self._ensemble = ensemble
self._old = self._ensemble.get_data_dict()
self._new = data
def undo(self):
self._ensemble.set_data_dict(self._old)
def redo(self):
self._ensemble.set_data_dict(self._new)
-280
View File
@@ -1,280 +0,0 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import json
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow, PamhyrDialog
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QSettings,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
)
from Modules import Modules
from View.Ensembles.Edit.Window import EnsembleEditDialog
from View.Ensembles.Table import (
EnsembleTableModel, ComboBoxDelegate
)
from View.Ensembles.Translate import EnsemblesTranslate
from View.Ensembles.UndoCommand import SetAllCommand, ImportCommand
logger = logging.getLogger()
class EnsemblesWindow(PamhyrWindow):
_pamhyr_ui = "Ensembles"
_pamhyr_name = "Ensembles"
def __init__(self, ensembles=None,
study=None, config=None,
parent=None):
trad = EnsemblesTranslate()
self._ensembles = ensembles
name = (
trad[self._pamhyr_name] + " - " +
study.name
)
super(EnsemblesWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self.setup_table()
self.setup_connections()
def setup_table_delegate(self):
self._delegate_stricklers = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="stricklers",
trad=self._trad,
parent=self
)
self._delegate_data_type = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="data_type",
trad=self._trad,
parent=self
)
self._delegate_distribution = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="distribution",
trad=self._trad,
parent=self
)
return {
"type": self._delegate_data_type,
"target_data": self._delegate_stricklers,
"distribution": self._delegate_distribution,
}
def setup_table(self):
self._table = {}
delegates = self.setup_table_delegate()
editable_headers = [
"name",
]
table = self.find(QTableView, f"tableView")
self._table = EnsembleTableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates=delegates,
data=self._ensembles,
trad=self._trad,
undo=self._undo_stack,
opt_data=self._study
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_connections(self):
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
self.find(QAction, "action_export").triggered.connect(
self.export_to_json
)
self.find(QAction, "action_import").triggered.connect(
self.import_from_json
)
table = self.find(QTableView, f"tableView")
table.selectionModel()\
.selectionChanged\
.connect(self.update)
self._table.dataChanged\
.connect(self.update)
def index_selected_rows(self):
table = self.find(QTableView, f"tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._ensembles) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def edit(self):
rows = self.index_selected_rows()
for row in rows:
try:
ens = self._ensembles.get(row)
dlg = EnsembleEditDialog(
study=self._study,
ensemble=ens,
parent=self
)
if dlg.exec():
self._undo_stack.push(
SetAllCommand(
ens, dlg.values
)
)
except Exception as e:
logger_exception(e)
return
def export_to_json(self):
ens = []
for e in self._ensembles.lst:
ens.append(e.get_data_dict(use_ids=True))
data = {"ensembles": ens}
def aux(filename):
with open(filename[0], 'w') as f:
f.write(json.dumps(data))
self.file_dialog(
select_file=None,
callback=aux,
default_suffix='.json',
file_filter=[self._trad["file_json"]]
)
def import_from_json(self):
def aux(filename):
with open(filename[0], 'r') as f:
return json.load(f)
data = self.file_dialog(
select_file=None,
callback=aux,
default_suffix='.json',
file_filter=[self._trad["file_json"]]
)
dtarget = dict(
map(
lambda s: (s.pamhyr_id, s),
self._study.river.stricklers.stricklers
)
)
ddistr = dict(
map(
lambda d: (d.name, d),
self._study._ens_distributions.lst
)
)
def update(d, values):
d.update(values)
return d
updated = list(
map(
lambda d: update(
d, {"target_data": dtarget[d["target_data"]]}
),
map(
lambda d: update(
d, {"distribution": ddistr[d["distribution"]]}
),
data["ensembles"]
)
)
)
self._undo_stack.push(
ImportCommand(
self._ensembles, updated, 0
)
)
self._table.update()
+1 -80
View File
@@ -67,7 +67,6 @@ from View.Configure.Window import ConfigureWindow
from View.Study.Window import NewStudyWindow
from View.About.Window import AboutWindow
from View.Scenarios.Window import ScenariosWindow
from View.Ensembles.Window import EnsemblesWindow
from View.Network.Window import NetworkWindow
from View.Geometry.Window import GeometryWindow
from View.BoundaryCondition.Window import BoundaryConditionWindow
@@ -84,14 +83,11 @@ from View.GeoTIFF.Window import GeoTIFFListWindow
from View.REPLines.Window import REPLineListWindow
from View.SolverParameters.Window import SolverParametersWindow
from View.RunSolver.Window import (
EnsembleRunNumberDialog,
SelectSolverWindow,
SolverLogWindow,
SolverLogEnsWindow,
)
from View.Results.Window import ResultsWindow, ResultsEnsembleWindow
from View.Results.Window import ResultsWindow
from View.Results.CompareDialog import (
CompareScenariosWindow
)
@@ -1343,7 +1339,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"""
if self._study is None:
return
if self._study.filename is None or self._study.filename == "":
self.message_box(
window_title=self._trad["Warning"],
@@ -1377,29 +1372,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self.scenarios = ScenariosWindow(study=self._study, parent=self)
self.scenarios.show()
def open_ensemble_scenario(self):
"""Open ensemble scenario window
Returns:
Nothing
"""
if self._study is None:
return
if self.sub_window_exists(
EnsemblesWindow,
data=[self._study, self.conf]
):
return
ensemble = EnsemblesWindow(
ensembles=self._study.river._ensembles,
study=self._study,
config=self.conf,
parent=self
)
ensemble.show()
def open_geometry(self):
"""Open geometry window
@@ -1756,9 +1728,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
sol.show()
def solver_log(self, solver):
if self._study.status.scenario.is_ensemble():
return self._solver_log_ensemble(solver)
sol = SolverLogWindow(
study=self._study,
config=self.conf,
@@ -1767,28 +1736,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
sol.show()
def _solver_log_ensemble(self, solver):
run_number = 1
dlg = EnsembleRunNumberDialog(
trad=self._trad,
parent=self
)
if dlg.exec():
function = dlg.function
number_of_run = dlg.number
seed = dlg.seed
sol = SolverLogEnsWindow(
number_of_run=number_of_run,
seed=seed, function=function,
study=self._study,
config=self.conf,
solver=solver,
parent=self
)
sol.show()
def msg_box_results_continue(self):
dlg = QMessageBox(self)
@@ -1865,12 +1812,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if not self.msg_box_results_continue():
return
if self._study.status.scenario.is_ensemble():
self.open_solver_ens_results_window(solver, results)
else:
self.open_solver_results_window(solver, results)
def open_solver_results_window(self, solver, results):
# Windows already opened
if self.sub_window_exists(
ResultsWindow,
@@ -1890,26 +1831,6 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
)
res.show()
def open_solver_ens_results_window(self, solver, results):
# Windows already opened
if self.sub_window_exists(
ResultsEnsembleWindow,
data=[
self._study,
None, # No config,
results._solver,
results._repertory,
results._name]
):
return
res = ResultsEnsembleWindow(
study=self._study,
results=[results],
parent=self
)
res.show()
def msg_open_results_no_results(self):
self.message_box(
window_title=self._trad["Warning"],
+9 -21
View File
@@ -26,13 +26,10 @@ from View.Results.translate import ResultsTranslate
class CustomExportDialog(PamhyrDialog):
_pamhyr_ui = "CustomExportDialog"
_pamhyr_name = "Custom Export"
_pamhyr_ui = "CustomExportAdisDialog"
_pamhyr_name = "Custom Plot Selection"
def __init__(self,
tag_x="values_x",
tag_y="values_y",
parent=None):
def __init__(self, parent=None):
trad = ResultsTranslate()
super(CustomExportDialog, self).__init__(
title=trad[self._pamhyr_name],
@@ -41,8 +38,8 @@ class CustomExportDialog(PamhyrDialog):
parent=parent
)
self._available_values_x = self._trad.get_dict(tag_x)
self._available_values_y = self._trad.get_dict(tag_y)
self._available_values_x = self._trad.get_dict("values_x")
self._available_values_y = self._trad.get_dict("values_y")
self.setup_radio_buttons_x()
self.setup_radio_buttons_res()
@@ -69,8 +66,8 @@ class CustomExportDialog(PamhyrDialog):
def setup_radio_buttons_res(self):
self._radio2 = []
layout = self.find(QVBoxLayout, "verticalLayout_solv")
label = self.find(QLabel, "label_solv")
layout = self.find(QVBoxLayout, "verticalLayout_pol")
label = self.find(QLabel, "label_3")
label.setText(self._trad["solver"])
for solver in self.parent._solvers:
@@ -92,7 +89,7 @@ class CustomExportDialog(PamhyrDialog):
layout.addStretch()
def setup_label(self):
self._label = self.find(QLabel, "label_data")
self._label = self.find(QLabel, "label_4")
self._label.setFrameStyle(QFrame.StyledPanel)
self._label.setStyleSheet('background-color: white')
self.set_label()
@@ -166,15 +163,6 @@ class CustomExportDialog(PamhyrDialog):
res = i
break
self.value = (x, y, self._envelop.isChecked(), res)
self.value = x, y, self._envelop.isChecked(), res
super().accept()
class CustomExportEnsembleDialog(CustomExportDialog):
def __init__(self, parent=None):
super(CustomExportEnsembleDialog, self).__init__(
tag_x="values_x_ens",
tag_y="values_y_ens",
parent=parent
)
-261
View File
@@ -1,261 +0,0 @@
# 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
)
+36 -10
View File
@@ -124,8 +124,12 @@ class PlotRKC(PamhyrPlot):
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
zfd = table[ts, gi_0:gi_1 + 1]
zfd = list(
map(
lambda p: table[ts, p.global_index],
reach.profiles
)
)
self.line_bottom, = self.canvas.axes.plot(
rk, zfd,
@@ -185,8 +189,14 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
water_z = list(
map(
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
self.water = self.canvas.axes.plot(
rk, water_z,
@@ -209,8 +219,16 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
water_z = list(
map(
lambda p: np.max(
table[
:, p.global_index
]
),
reach.profiles
)
)
self.canvas.axes.plot(
rk, water_z, lw=1.,
@@ -343,8 +361,14 @@ class PlotRKC(PamhyrPlot):
table = result.get("table")["Z"]
ts = result.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
water_z = table[ts, gi_0:gi_1 + 1]
water_z = list(
map(
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
self.water[0].set_data(
rk, water_z
@@ -387,8 +411,10 @@ class PlotRKC(PamhyrPlot):
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
gi_0, gi_1 = reach.global_index
zfd = table[ts, gi_0:gi_1 + 1]
zfd = list(map(
lambda p: table[ts, p.global_index],
reach.profiles
))
self.line_bottom.remove()
-355
View File
@@ -1,355 +0,0 @@
# 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,8 +61,6 @@ class TableModel(PamhyrTableModel):
# _river.reach(0).profiles))
elif self._opt_data == "solver":
self._lst = self._parent._solvers
elif self._opt_data == "runs":
self._lst = self._data[0].get("table")["samples"]
def __init__(self, **kwargs):
self._timestamp = max(kwargs["parent"]._timestamps)
@@ -95,10 +93,6 @@ class TableModel(PamhyrTableModel):
if v is None:
v = self._data[0].solver_name
return str(v)
elif self._opt_data == "runs":
if self._headers[column] == "runs":
p = self._lst[row, :]
return f"{row}: {p}"
elif self._opt_data == "raw_data":
table = self._data[0].get("table")
ts = self._data[0].get_timestamp_id(self._timestamp)
+377 -678
View File
File diff suppressed because it is too large. Load diff
+1 -35
View File
@@ -44,7 +44,6 @@ class ResultsTranslate(MainTranslate):
self._dict['envelop'] = _translate("Results", "Envelop")
self._dict['solver'] = _translate("Results", "Solver")
self._dict['runs'] = _translate("Results", "Runs")
self._dict['x'] = _translate("Results", "X (m)")
self._dict["temperature_map"] = _translate(
@@ -56,20 +55,10 @@ class ResultsTranslate(MainTranslate):
self._dict['label_bottom'] = _translate("Results", "Bottom")
self._dict['label_water'] = _translate("Results", "Water elevation")
self._dict['label_water_min'] = _translate(
"Results",
"Min water elevation"
)
self._dict['label_water_mean'] = _translate(
"Results",
"Mean water elevation"
)
self._dict['label_water_max'] = _translate(
"Results",
"Max water elevation"
)
self._dict["ImageCoordinates"] = _translate(
"Results", "Image coordinates"
)
@@ -98,10 +87,6 @@ class ResultsTranslate(MainTranslate):
"solver": self._dict['solver'],
}
self._sub_dict["table_headers_runs"] = {
"runs": self._dict['runs'],
}
self._sub_dict["table_headers_pollutants"] = {
"name": _translate("Results", "Pollutant name"),
}
@@ -136,25 +121,6 @@ class ResultsTranslate(MainTranslate):
"wet_area": self._dict["unit_wet_area"],
}
self._sub_dict["values_x_ens"] = {
"rk": self._dict["unit_rk"],
"time": self._dict["unit_time_s"],
}
self._sub_dict["values_y_ens"] = {
"z_mean": self._dict["unit_mean_water_elevation"],
"q_mean": self._dict["unit_mean_discharge"],
"z_standard_derivation": self._dict["unit_std_deriv_elevation"],
"q_standard_derivation": self._dict["unit_std_deriv_discharge"],
}
self._sub_dict["values_y_envelop_ens"] = {
"z_min": self._dict["unit_min_water_elevation"],
"z_max": self._dict["unit_max_water_elevation"],
"q_min": self._dict["unit_min_discharge"],
"q_max": self._dict["unit_max_discharge"],
}
if self.pollutants is not None:
self._sub_dict["table_headers_raw_data"] = {
"name": _translate("Results", "Profile"),
@@ -185,7 +151,7 @@ class ResultsTranslate(MainTranslate):
"max_velocity": self._dict["unit_max_velocity"],
"min_depth": self._dict["unit_min_depth"],
"max_depth": self._dict["unit_max_depth"],
}
}
self._sub_dict["values_y_pol"] = {
"unit_C": self._dict["unit_concentration"],
+55
View File
@@ -0,0 +1,55 @@
# 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.Tools.PamhyrTranslate import PamhyrTranslate
_translate = QCoreApplication.translate
class RunSolverTranslate(PamhyrTranslate):
def __init__(self):
super(RunSolverTranslate, self).__init__()
self._dict["invalid_options"] = _translate(
"Solver",
"Export cancelled: {key}: expected three digits, each 1 or 2, "
"Select valid options in the parameters (tab Solid Transport)."
)
self._dict["unsupported_transport_law"] = _translate(
"Solver",
"Export cancelled: {key}: unsupported transport law {value!r}. "
"Please choose another transport law from the list."
)
self._dict["warning"] = _translate("Solver", "Warning: {message}")
self._dict["incompatible_upstream_boundary"] = _translate(
"Solver",
"Export cancelled: REGIME=1 is incompatible with CONDAM=0. "
"A reflecting upstream boundary cannot impose the water depth."
)
self._dict["missing_solid_boundary"] = _translate(
"Solver",
"Export cancelled: sediment transport (TRASED) is enabled but "
"no solid boundary condition is defined at the upstream node "
"of reach(es): {reaches}. Add a solid boundary condition, or "
"disable sediment transport in the solver parameters."
)
self._dict["export_failed"] = _translate(
"Solver", "Export failed; task cancelled"
)
+1 -290
View File
@@ -20,8 +20,6 @@ import os
import logging
import tempfile
import numpy as np
from queue import Queue
from tools import trace, timer, logger_exception
@@ -42,7 +40,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QTextEdit, QProgressBar, QCheckBox,
QTextEdit,
)
from View.RunSolver.Log.Window import SolverLogFileWindow
@@ -59,63 +57,6 @@ _translate = QCoreApplication.translate
logger = logging.getLogger()
class EnsembleRunNumberDialog(PamhyrDialog):
_pamhyr_ui = "EnsembleRunNumberDialog"
_pamhyr_name = "EnsembleRunNumber"
def __init__(self, trad=None, parent=None):
super(EnsembleRunNumberDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._function = "Random"
self._number = 100
self._seed = -1
self._init_default_values()
self._init_connection()
def _init_default_values(self):
self.combobox_add_items("comboBox", ["Random"])
self.set_combobox_text("comboBox", "Random")
self.set_spin_box("spinBox", 100)
def _init_connection(self):
self._checkbox = self.find(QCheckBox, f"checkBox")
self._checkbox.clicked.connect(self._set_random_seed_spin_box_state)
def _set_random_seed_spin_box_state(self):
self.set_spin_box_enable("spinBox_seed", self._checkbox.isChecked())
@property
def number(self):
return self._number
@property
def seed(self):
return self._seed
@property
def function(self):
return self._function
def accept(self):
self._function = self.get_combobox_text("comboBox")
self._number = self.get_spin_box("spinBox")
if self._checkbox.isChecked():
self._seed = self.get_spin_box("spinBox_seed")
super().accept()
def reject(self):
self.close()
class SelectSolverWindow(PamhyrDialog):
_pamhyr_ui = "SelectSolver"
_pamhyr_name = "Select solver"
@@ -486,233 +427,3 @@ class SolverLogWindow(PamhyrWindow):
parent=self,
)
log.show()
class SolverLogEnsWindow(SolverLogWindow):
_pamhyr_ui = "SolverLogEnsemble"
_pamhyr_name = "Solver Log Ensemble"
def __init__(self, number_of_run=100,
seed=-1, function="Random",
study=None, config=None,
solver=None, parent=None):
self._number_of_run = number_of_run
self._last_run = 0
if seed != -1:
np.random.seed(seed)
self._seed = np.random.get_state()[1][0]
logger.info(
"solver: Set ensemble with " +
f"{number_of_run} {function} and " +
f"{self._seed} as random seed"
)
self._samples = {}
for ens in study.river._ensembles.lst:
if ens.distribution is None:
continue
self._samples[ens] = ens.random(self._number_of_run)
self._results = None
self._run_samples = []
self._results_tables = {}
super(SolverLogEnsWindow, self).__init__(
solver=solver,
study=study,
config=config,
parent=parent,
)
def setup_connections(self):
self.setup_progress_bar()
super(SolverLogEnsWindow, self).setup_connections()
def setup_progress_bar(self):
self._progress_bar = self.find(QProgressBar, "progressBar")
self._progress_bar.setRange(0, self._number_of_run)
self._progress_bar.setValue(0)
def _log_int(self, int_code, color=None):
logger.info(f"solver: Returns {int_code}")
color = "blue" if int_code == 0 else "red"
self.find(QTextEdit, "textEdit")\
.append(
f"<font color=\"{color}\">" +
f" *** Finished with code {int_code}" +
"</font>"
)
self.statusbar.showMessage(
("Done" if int_code == 0 else "Failed")
+ f" #{self._last_run}",
3000
)
self._solver.results_ensemble(
self._study, self._workdir,
self._results_tables,
self._last_run,
qlog=self._output,
)
if self._last_run < self._number_of_run:
self.next()
else:
self._results = self.fill_results_from_tables()
self._parent.set_results(self._solver, self._results)
self._progress_bar.setValue(self._last_run)
def fill_results_from_tables(self):
tables = self._results_tables
results = self._solver.get_ensemble_empty_results(
self._study, self._workdir
)
table = {}
for key in ["z", "q"]:
for func in ["min", "mean", "max", "std_derivation"]:
name = f"{key}_{func}"
table[name] = results.new_table_data(name, tables[name])
table["samples"] = results.new_table_data(
"samples", np.array(self._run_samples)
)
results.set("number_of_run", self._number_of_run)
results.set("seed", self._seed) # Keep random seed
results.set("table", table)
# Compute global index for reaches and profiles
gi = 0
for reach in results.river.reachs:
gi_start = gi
for profile in reach.profiles:
gi += 1
reach.set_global_index(range(gi_start, gi))
# Compute timestamps index table
ts = tables["timestamps"]
ts_index = {}
for i, t in enumerate(sorted(ts)):
ts_index[t] = i
results.set("timestamps", ts)
results.set_timestamp_index(ts_index)
return results
def next(self):
self._process = self.new_process(self._parent)
self._log(f" *** Run #{self._last_run}", color="blue")
if self._solver._type in ["mage8"]:
samples = self._solver.export_ensemble_variation(
self._study, self._workdir, qlog=self._output,
samples=self._samples,
)
else:
samples = self.ensemble_variation()
self._solver.export(
self._study, self._workdir, qlog=self._output
)
# Keep samples generators results
self._run_samples.append(samples)
self._solver.run(
self._study,
process=self._process,
output_queue=self._output
)
self.set_running_button_status()
self._last_run += 1
self._progress_bar.setValue(self._last_run - 1)
def ensemble_variation(self):
samples = []
for ens in self._samples:
if ens.target_data is None:
continue
x = next(self._samples[ens])
samples.append(x)
self._log(f"*** Variation #{self._last_run} " +
f"'{ens.target_data}' - {ens.data_type} x {x}",
color="blue")
if ens.data_type == "strickler_minor":
ens.target_data.minor = x
elif ens.data_type == "strickler_medium":
ens.target_data.medium = x
return samples
def run(self):
self._log(f" *** Run solver {self._solver.name}", color="blue")
self.next()
def start(self):
if self._solver.is_stoped():
self._log(f" *** Export study {self._solver.name}", color="blue")
ok = self._solver.export(
self._study, self._workdir, qlog=self._output
)
if not ok:
self._log(f" *** Failed to export", color="red")
self.update()
return
else:
self.update()
self._process = self.new_process(self._parent)
self._log(" *** Start", color="blue")
self._results = None
self.next()
def set_running_button_status(self):
self.find(QAction, "action_start").setEnabled(False)
if _signal:
self.find(QAction, "action_pause").setEnabled(True)
else:
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(True)
self.find(QAction, "action_log_file").setEnabled(False)
self.find(QAction, "action_results").setEnabled(False)
def stop(self):
self._log(" *** Stop", color="blue")
self._solver.kill()
self.find(QAction, "action_start").setEnabled(True)
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(False)
self.find(QAction, "action_results").setEnabled(True)
if self._solver.log_file() != "":
self.find(QAction, "action_log_file").setEnabled(True)
def results(self):
if self._results is None:
return
self._parent.set_results(self._solver, self._results)
self._parent.open_solver_results(self._solver, self._results)
self._solver.has_results_loaded()
+1 -20
View File
@@ -47,19 +47,11 @@ class AbstractMenu(object):
class DefaultMenu(AbstractMenu):
def run(self):
current_scenario = self._graph.status.scenario
if current_scenario.is_ensemble():
return
add_node = self._menu.addAction(self._trad["menu_add_scenario"])
ensemble = self._menu.addAction(self._trad["menu_ens_scenario"])
action = self._exec()
if action == add_node:
self._parent.new_scenario(self._pos)
if action == ensemble:
self._parent.create_new_ensemble_scenario(self._pos)
class ScenarioMenu(AbstractMenu):
@@ -69,13 +61,9 @@ class ScenarioMenu(AbstractMenu):
current_scenario = item.graph._study.status.scenario.id
select = None
select = self._menu.addAction(self._trad["menu_select_scenario"])
duplicate = None
delete = None
edit_ens = None
if len(scenarios) > 1:
select = self._menu.addAction(self._trad["menu_select_scenario"])
if item.scenario.id != 0:
if scenarios.is_leaf(item.scenario):
@@ -86,11 +74,6 @@ class ScenarioMenu(AbstractMenu):
self._trad["menu_dup_scenario"]
)
if item.scenario.is_ensemble():
edit_ens = self._menu.addAction(
self._trad["menu_edit_ens_scenario"]
)
action = self._exec()
if action is None:
return
@@ -100,5 +83,3 @@ class ScenarioMenu(AbstractMenu):
self._parent.delete_scenario(item)
elif action == duplicate:
self._parent.duplicate_scenario(item)
elif action == edit_ens:
self._parent.edit_ensemble_scenario(item)
+1 -50
View File
@@ -100,11 +100,7 @@ class ScenarioItem(QGraphicsTextItem):
)
)
painter.setBrush(QBrush(color))
if self.scenario.is_ensemble():
painter.drawRect(self.boundingRect())
else:
painter.drawEllipse(self.boundingRect())
painter.drawEllipse(self.boundingRect())
super(ScenarioItem, self).paint(painter, option, widget)
@@ -530,51 +526,6 @@ class GraphWidget(QGraphicsView):
self.exec_with_waiting_window(fn, "duplicate_scenario")
self.changeScenario.emit(self.sender())
def create_ensemble_scenario(self, item):
must_save = self.dialog_save()
if must_save == "Cancel":
return
def fn():
self._close_other_window()
if must_save == "Save":
self._study.save()
self._undo.push(
CreateEnsembleScenarioCommand(
self._study,
)
)
self.exec_with_waiting_window(fn, "create_ensemble_scenario")
self.changeScenario.emit(self.sender())
def create_new_ensemble_scenario(self, pos):
must_save = self.dialog_save()
if must_save == "Cancel":
return
def fn():
self._close_other_window()
if must_save == "Save":
self._study.save()
self._undo.push(
CreateEnsembleScenarioCommand(
self._study, pos=pos,
)
)
self.exec_with_waiting_window(fn, "create_ensemble_scenario")
self.changeScenario.emit(self.sender())
def edit_ensemble_scenario(self, item):
self.parent\
.parent\
.open_ensemble_scenario()
def _close_other_window(self):
self.parent\
.parent\
-20
View File
@@ -98,26 +98,6 @@ class DuplicateScenariosCommand(QUndoCommand):
self._study.reload_from_scenario(self._new)
class CreateEnsembleScenarioCommand(QUndoCommand):
def __init__(self, study, pos=None):
QUndoCommand.__init__(self)
self._study = study
self._new = None
self._pos = pos
def undo(self):
self._study.scenarios.delete(self._new.id)
self._study.reload_from_scenario(self._new.parent)
def redo(self):
if self._new is None:
self._new = self._study.create_ensemble_scenario(self._pos)
else:
self._new.set_as_not_deleted()
self._study.reload_from_scenario(self._new)
class SetCommand(QUndoCommand):
def __init__(self, scenario, column, new_value):
QUndoCommand.__init__(self)
-6
View File
@@ -43,12 +43,6 @@ class ScenariosTranslate(MainTranslate):
self._dict["menu_dup_scenario"] = _translate(
"Scenarios", "Duplicate this scenario"
)
self._dict["menu_ens_scenario"] = _translate(
"Scenarios", "Create ensemble scenario from current scenario"
)
self._dict["menu_edit_ens_scenario"] = _translate(
"Scenarios", "Edit this ensemble scenario"
)
self._sub_dict["table_headers_scenarios"] = {
# "id": self._dict['id'],
+346
View File
@@ -0,0 +1,346 @@
"""Rubar parameter descriptions (notice sections 3.1, 3.10 and annex 1).
Keys and values keep their historical database representation.
"""
from dataclasses import dataclass
from decimal import Decimal, DecimalException, InvalidOperation
from Solver.RubarBE import Rubar3
GROUPS = ('Temps et calcul', 'Hydraulique', 'Transport solide',
'Évolution du fond', 'Sauvegardes')
YES_NO = {'y': 'Oui', 'n': 'Non'}
ON_OFF = {'1': 'Oui', '0': 'Non'}
LAWS = {
'1': 'Meyer-Peter et Müller', '2': 'Engelund et Hansen', '3': 'Bagnold',
'8': 'Ackers et White (1973)', '9': 'Smart et Jaeggi', '10': 'van Rijn',
'11': 'Rickenmann', '12': 'Camenen et Larson', '13': 'Schoklitsch',
'14': 'Sato, Hikkawa et Ashida', '15': 'Recking et al. (2008)',
'16': 'Brownlie', '17': 'Laursen', '18': 'Ackers et White (1990)',
'19': 'Karim', '20': 'Karim et Kennedy', '21': 'Yang et Lim (2003)',
'22': 'Yang (1984)', '23': 'Yang (1979)', '24': 'Yang (1973)',
'25': 'van Rijn (1984) — suspension', '26': 'van Rijn (1984) — charriage',
'27': 'Recking et al. (2016)', '28': 'Piton et al. (2017)',
'29': 'Lefort (2007)', '30': 'Lefort (2015)',
}
OPTION_CHOICES = (
('Correction de pente',
{'1': 'Correction d’Ikeda', '2': 'Sans correction'},
'Permet d\'introduire que la répartition de la contrainte critique dans '
'une section en travers est influencée par la pente transversale'),
('Seuil critique', {'1': 'Fourni par l’utilisateur',
'2': 'Calculé par le diagramme de Shields'},
'Permet de choisir pour certaines formules de capacité, entre un seuil '
'critique fourni par l’utilisateur ou '
'un seuil critique calculé par le diagramme de Shields.'),
('Contrainte utilisée', {'1': 'Contrainte efficace',
'2': 'Contrainte non réduite'},
'Permet de choisir pour certaines formules de capacité, entre la '
'contrainte efficace calculée ou la contrainte non réduite.'),
)
@dataclass(frozen=True)
class Field:
key: str
group: int
label: str
help: str
kind: str = 'real'
choices: dict = None
minimum: float = 0
maximum: float = None
positive: bool = False
optional: bool = False
def fields():
"""Describe historical Rubar fields without changing their defaults."""
result = []
def add(key, group, label, help, **kwargs):
result.append(Field('rubarbe_' + key, group, label, help, **kwargs))
def choice(key, group, label, help, choices):
add(key, group, label, help, kind='choice', choices=choices)
add('cfl', 0, 'Nombre de Courant (CFL)',
'Limite supérieure du nombre de Courant, afin d\'assurer la stabilité '
'du schéma numérique ; comprise entre 0 et 1.',
positive=True, maximum=1)
for key, label, help in (
('tinit', 'Début du calcul',
'Doit correspondre aux conditions initiales.'),
('tmax', 'Fin du calcul', 'Doit être postérieure au début du calcul.'),
):
add(key, 0, label + ' (j:h:min:s ou s)', help, kind='time')
choice('iopdt', 0, 'Gestion du pas de temps',
'En mode mixte, le pas reste fixe jusqu’à la date de bascule.',
{'y': 'Automatique', 'n': 'Fixe',
'm': 'Mixte : fixe puis automatique'})
add('dt', 0, 'Pas de temps initial ou fixe (s)',
'Premier pas en mode automatique, pas demandé en mode fixe.',
positive=True)
add('tiopdt', 0, 'Fin de la période à pas fixe' + ' (j:h:min:s ou s)',
'Utilisée uniquement avec un pas de temps mixte.', kind='time')
choice('rep', 0, 'Reprendre un calcul',
'Nécessite les fichiers de reprise du calcul précédent.', YES_NO)
choice('condam', 1, 'Condition à la limite amont',
'Le choix doit correspondre aux données de la limite amont.',
{'0': 'Réflexion', '1': 'Débit imposé Q(t)',
'2': 'Cote imposée Z(t)', '4': 'Confluence'})
choice('condav', 1, 'Condition à la limite aval',
'Le choix doit correspondre aux données de la limite aval.',
{'0': 'Sortie libre', '1': 'Courbe de tarage Q(Z)',
'2': 'Réflexion',
'3': 'Cote imposée Z(t)', '4': 'Confluence / défluence',
'5': 'Régime uniforme', '7': 'Régime critique'})
choice('regime', 1, 'Imposer aussi le tirant d’eau amont',
'Complète le débit imposé ; incompatible avec une limite '
'amont de type "Réflexion".',
ON_OFF)
choice('iodev', 1, 'Déversement latéral',
'Nécessite les données de déversements latéraux.',
YES_NO)
for key, label, help in (
('iodebord', 'Formule Debord',
'Oui: utilise la formule Debord pour les lits composés.\nNon:'
' pas de perte de charge complémentaire entre lit mineur et moyen.'),
('iostockage', 'Stockage dans le lit majeur',
'Traite le lit majeur sous la berge la plus basse comme zone '
'de stockage.'),
):
choice(key, 1, label, help, YES_NO)
choice('optfpc', 1, 'Calcul du frottement',
'Strickler fourni ou frottement calculé à partir des sédiments.',
{'0': 'Coefficients de Strickler fournis', '1': 'Brownlie (1983)',
'2': 'Griffith (1981) - pour un fond mobile', '3': 'Karim (1995)',
'4': 'Wu et Wang (1999)', '5': 'Yu et Lim (2003)',
'6': 'Recking et al. (2008)', '7': 'Garde et Raju (1966)'})
for key, label in (
('ros', 'Masse volumique des sédiments (kg/m³)'),
('dm', 'Diamètre médian des sédiments (m)'),
('segma', 'Étendue granulométrique des sédiments (—)'),
):
add(key, 1, label,
'Utilisé pour le frottement calculé sans transport solide '
'(disponible uniquement si le transport solide est désactivé '
'et que le calcul du frottement se fait avec les sédiments).',
positive=True)
choice('trased', 2, 'Activer le transport solide',
'Fonction de RubarBE. L’évolution du fond se règle séparément.',
YES_NO)
add('sediment_ros', 2, 'Masse volumique des sédiments (kg/m³)',
'Masse volumique des grains.', positive=True)
add('sediment_por', 2, 'Porosité des sédiments (—)',
'Fraction du volume occupée par les vides : de 0 inclus à 1 exclu.',
positive=True, maximum=1)
choice('sediment_opts', 2, 'Loi de capacité solide',
'Formule de capacité solide ; choisir selon le domaine '
'physique.',
LAWS)
choice('sediment_unisol', 2, 'Unité des débits ou concentrations solides',
'Unité des débits ou concentrations solides.',
{'1': 'Débit massique (kg/s)', '2': 'Concentration (kg/m³)'})
choice('sediment_odchar', 2, 'Distance de chargement',
'Option de calcul de la ditance de chargement.',
{'0': 'Distance constante', '1': 'Loi de Han', '2': 'Wu et Wang'})
add('sediment_dcharg', 2, 'Distance de chargement constante (m)',
'Utilisée uniquement si la distance de chargement est constante.')
add('sediment_halfa', 2, 'Paramètre alpha de Han (m)',
'Utilisé uniquement avec la loi de chargement de Han.')
add('sediment_option', 2, 'Calcul de la contrainte critique',
'Trois choix indépendants, enregistrés ensemble dans OPTION.',
kind='options')
add('sediment_visc', 2, 'Viscosité ou seuil critique',
'Selon la configuration : seuil critique adimensionnel fourni, ou '
'viscosité cinématique de l’eau. À 15 °C : environ 1,30 × 10⁻⁶ m²/s. '
'Un changement de mode ne convertit pas la valeur.', positive=True)
choice('sediment_choixc', 2, 'Calcul de la contrainte hydrodynamique de '
'cisaillement',
'MPC : méthode des perpendiculaires confondues (notice §8.5).',
{'1': 'MPC', '2': 'Répartition uniforme',
'3': 'MPC — pente moyenne sur 3 segments',
'4': 'MPC — moyenne sur 3 segments',
'5': 'MPC — moyenne sur une distance de périmètre de '
'longueur égale à la hauteur d’eau',
'6': 'MPC — proportionnelle au diamètre de la couche '
'puissance 1/3',
'7': 'MPC — proportionnelle au Strickler puissance −3/2',
'8': 'MPC — proportionnelle à la hauteur d’eau'})
choice('sediment_capsol', 2, 'Calcul de la capacité solide',
'Les méthodes 2 et 3 nécessitent une répartition MPC.',
{'1': 'Contrainte moyenne, largeur active imposée',
'2': 'Contrainte moyenne, largeur active calculée',
'3': 'Répartition locale des contraintes'})
add('sediment_mult_1', 2, 'Multiplicateur de la capacité de chariage (—)',
'1 conserve la capacité calculée par la loi choisie.')
choice('sediment_defond', 3, 'Faire évoluer la géométrie du lit',
'Le transport solide peut être calculé sans déformation du lit.',
ON_OFF)
choice('sediment_demix', 3, 'Activer le démixage',
'Option pour le demixage de deux compartiments sédimentaires.',
ON_OFF)
for key, label in (
('dchard', 'Distance de chargement pour le diamètre de sédiment (m)'),
('dchars', 'Distance de chargement de l’étendue granulométrique des '
'sédiments (m)'),
):
add('sediment_' + key, 3, label,
'Utilisée uniquement lorsque le démixage est activé.')
choice('sediment_typdef', 3, 'Actualisation de la masse active',
'Actualise la masse active après une variation relative de 10⁻ⁿ.',
{'0': 'À chaque pas', '1': 'Variation de 10 %',
'2': 'Variation de 1 %', '3': 'Variation de 0,1 %',
'4': 'Variation de 0,01 %'})
choice('sediment_depot', 3, 'Méthode d\'application du dépôt',
'Méthodes de déformation de la section décrites au §8.5.3.',
{'1': 'Couches horizontales',
'2': 'Uniforme sur le périmètre mouillé',
'3': 'Inverse de la contrainte locale (1/τ)',
'4': 'Selon le seuil critique — tout le lit actif mouillé',
'5': 'Selon le seuil critique — une partie du lit actif mouillé',
'6': 'Méthode 6 — fonction des contraintes (notice §8.5.3)',
'7': 'Méthode 7 — fonction des contraintes (notice §8.5.3)',
'8': 'Méthode 8 — code cité dans l’annexe 1'})
choice('sediment_varcons', 3, 'Grandeurs conservées après déformation',
'Les autres grandeurs hydrauliques sont recalculées.',
{'1': 'Section mouillée et débit', '2': 'Niveau d’eau et débit',
'3': 'Niveau d’eau et vitesse'})
add('sediment_bmiu', 3, 'Tangente de l’angle de frottement interne (—)',
'Saisir tan(angle), et non l’angle en degrés, de la valeur moyenne '
'de la tangente de l\'angle de frottement interne de sédiments.',
positive=True)
for i, label in enumerate((
'Épaisseur de couche active : multiplicateur du d84',
'Épaisseur de fines infiltrantes : multiplicateur du d16',
'Coefficient de mélange des couches superficielles',
'Coefficient de création d’une couche de dépôt',
), 2):
add(f'sediment_mult_{i}', 3, label + ' (—)',
'Facultatif : laisser vide pour utiliser le comportement '
'du solveur.',
optional=True)
for key, label, help in (
('ts', 'Instant de sauvegarde pour reprise',
'Écrit les fichiers de reprise lorsque cet instant est dépassé.'),
('dtsauv', 'Intervalle de sauvegarde des profils',
'0 demande une sauvegarde à chaque pas de calcul.'),
('psave', 'Intervalle de sauvegarde des hydrogrammes',
'0 demande une sauvegarde à chaque pas de calcul.'),
):
add(key, 4, label + ' (j:h:min:s ou s)', help, kind='time')
for i, label in enumerate((
'Fichiers de reprise', 'Hydrogrammes aux abscisses choisies',
'Enveloppes des résultats', 'Profils aux instants choisis',
'Profils à intervalle fixe', 'Temps de dépassement des débits',
), 1):
choice(f'tf_{i}', 4, label,
'Les abscisses ou instants demandés '
'doivent être définis dans les données correspondantes.',
YES_NO)
for i in range(1, 4):
add(f'fdeb{i}', 4, f'Seuil de débit du front {i} (m³/s)',
'Débit pour lequel à chaque abscisse, est enregistré le temps '
'de premier dépassement (fichier TRAJEC si demandé).',
positive=True)
choice('iovis', 4, 'Sorties complémentaires',
'Largeurs et sorties complémentaires hydrauliques et '
'sédimentaires.',
YES_NO)
return {field.key: field for field in result}
FIELDS = fields()
def canonical(field, value):
value = str(value).strip()
if field.choices and 'y' in field.choices:
aliases = {'o': 'y', 'y': 'y', 'n': 'n', 'm': 'm'}
return aliases.get(value.lower(), value)
return value
def validate(field, value):
"""Return a storage value or raise a user-facing validation error."""
value = canonical(field, value)
if field.kind == 'choice':
if value not in field.choices:
raise ValueError('Choisissez une valeur dans la liste.')
return value
if field.kind == 'options':
if len(value) != 3 or any(c not in '12' for c in value):
raise ValueError('Renseignez les trois choix de contrainte.')
return value
if field.kind == 'text':
return value
if not value and field.optional:
return value
value = value.replace(',', '.').replace('d', 'E').replace('D', 'E')
try:
if field.kind == 'time' and ':' in value:
parts = value.split(':')
valid = all(p.isascii() and p.isdigit() for p in parts)
if len(parts) != 4 or not valid:
raise ValueError(
'Utilisez j:h:min:s ou un nombre de secondes.')
number = sum(Decimal(p) * factor for p, factor in
zip(parts, (86400, 3600, 60, 1)))
else:
number = Decimal(value)
except InvalidOperation:
raise ValueError('Saisissez un nombre fini.') from None
if not number.is_finite():
raise ValueError('Saisissez un nombre fini.')
if number < field.minimum or (field.positive and number == 0):
raise ValueError('La valeur doit être strictement positive.'
if field.positive else
'La valeur doit être positive ou nulle.')
if field.maximum is not None and number > field.maximum:
raise ValueError(f'La valeur ne doit pas dépasser {field.maximum}.')
if field.key == 'rubarbe_sediment_por' and number >= 1:
raise ValueError('La porosité doit être inférieure à 1.')
scientific = not field.key.startswith('rubarbe_sediment_')
try:
Rubar3._real(str(number), field.key, scientific=scientific)
except (ValueError, DecimalException, OverflowError):
raise ValueError(
'Valeur trop longue ou hors plage pour le format du solveur '
'(12 colonnes).' if scientific else
'Valeur trop précise ou hors plage pour le format du solveur '
'(10 colonnes).') from None
return value
def active(field, values):
"""Dependencies only disable editing; stored values are never erased."""
key = field.key.removeprefix('rubarbe_')
transport = values.get('rubarbe_trased', 'n').lower() in ('y', 'o')
if key.startswith('sediment_') and not transport:
return False
if key == 'tiopdt':
return values.get('rubarbe_iopdt', '').lower() == 'm'
if key in ('ros', 'dm', 'segma'):
return (not transport and
values.get('rubarbe_optfpc', '0') not in ('', '0'))
dependencies = {
'sediment_dcharg': ('sediment_odchar', ('0',)),
'sediment_halfa': ('sediment_odchar', ('1',)),
'sediment_dchard': ('sediment_demix', ('1',)),
'sediment_dchars': ('sediment_demix', ('1',)),
'sediment_varcons': ('sediment_defond', ('1',)),
'sediment_depot': ('sediment_defond', ('1',)),
}
if key in dependencies:
source, accepted = dependencies[key]
return values.get('rubarbe_' + source) in accepted
if key.startswith('fdeb'):
return values.get('rubarbe_tf_6', 'n').lower() in ('y', 'o')
return True
+365
View File
@@ -0,0 +1,365 @@
"""Grouped Rubar parameter editor, backed by the existing parameter list."""
from functools import partial
from PyQt5.QtCore import QCoreApplication, QEvent, Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QComboBox, QFormLayout, QLabel, QLineEdit, QScrollArea,
QSizePolicy, QTabWidget, QVBoxLayout, QWidget,
)
from View.SolverParameters.RubarFields import (
FIELDS, GROUPS, OPTION_CHOICES, Field, active, canonical, validate,
)
from View.SolverParameters.UndoCommand import SetCommand
def tr(text):
return QCoreApplication.translate('RubarParameters', text)
class RubarForm(QWidget):
def __init__(self, parameters, undo, editable=True, parent=None):
super().__init__(parent)
self.parameters = parameters
self.stack = undo
self.editable = editable
self.editors = {}
self.labels = {}
self.errors = {}
self.descriptions = dict(FIELDS)
self.indices = {p.name: i for i, p in enumerate(parameters.parameters)}
self._refreshing = False
self._options_pending = False
layout = QVBoxLayout(self)
intro = QLabel(tr('Les valeurs valides sont appliquées à l’étude. '
'Pensez à enregistrer l’étude pour les conserver.'))
intro.setWordWrap(True)
layout.addWidget(intro)
self.notice = QLabel()
self.notice.setWordWrap(True)
self.notice.setTextFormat(Qt.PlainText)
layout.addWidget(self.notice)
self.tabs = QTabWidget()
layout.addWidget(self.tabs)
forms = []
for group in GROUPS:
page = QWidget()
form = QFormLayout(page)
form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
form.setRowWrapPolicy(QFormLayout.WrapLongRows)
form.setVerticalSpacing(12)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(page)
self.tabs.addTab(scroll, tr(group))
forms.append(form)
# Metadata order is stable even when stored parameter order changes.
keys = [key for key in FIELDS if key in self.indices]
keys += [key for key in self.indices if key not in FIELDS]
for key in keys:
field = self.descriptions.setdefault(key, Field(
key, 0, key, 'Paramètre supplémentaire conservé tel quel.',
kind='text'))
self._add_field(field, forms[field.group])
self.help = QLabel(tr(
'Sélectionnez un réglage pour afficher son aide.'))
self.help.setWordWrap(True)
self.help.setTextFormat(Qt.PlainText)
self.help.setMinimumHeight(60)
layout.addWidget(self.help)
self.stack.indexChanged.connect(self.refresh)
self.refresh()
def _add_field(self, field, form):
key = field.key
container = QWidget()
box = QVBoxLayout(container)
box.setContentsMargins(0, 0, 0, 0)
if field.kind == 'options':
editor = QWidget()
subform = QFormLayout(editor)
subform.setContentsMargins(0, 0, 0, 0)
self.option_editors = []
for title, choices, tooltip in OPTION_CHOICES:
combo = QComboBox()
combo.setSizeAdjustPolicy(
QComboBox.AdjustToMinimumContentsLengthWithIcon)
combo.addItem(tr('À renseigner'), None)
for value, label in choices.items():
combo.addItem(tr(label), value)
combo.currentIndexChanged.connect(self._commit_options)
combo.setProperty('parameter', key)
combo.installEventFilter(self)
combo.setToolTip(tr(tooltip) + '\n' + key)
subform.addRow(tr(title), combo)
self.option_editors.append(combo)
elif field.kind == 'choice':
editor = QComboBox()
editor.setSizeAdjustPolicy(
QComboBox.AdjustToMinimumContentsLengthWithIcon)
for value, label in field.choices.items():
editor.addItem(tr(label), value)
editor.currentIndexChanged.connect(
partial(self._choice_changed, key))
else:
editor = QLineEdit()
editor.setPlaceholderText(tr('Valeur du solveur') if field.optional
else tr('Saisir une valeur'))
editor.textEdited.connect(partial(self._check_draft, key))
editor.editingFinished.connect(partial(self._finish_text, key))
editor.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
editor.setObjectName(key)
editor.setProperty('parameter', key)
editor.installEventFilter(self)
editor.setToolTip(
tr(field.help) + '\n' + key + '\nNotice §3.1 / §3.10')
box.addWidget(editor)
error = QLabel()
error.setWordWrap(True)
error.setTextFormat(Qt.PlainText)
error.setStyleSheet('color: #b42318;')
box.addWidget(error)
label = QLabel(tr(field.label))
label.setWordWrap(True)
label.setFixedWidth(310)
label.setMinimumHeight(label.heightForWidth(310))
label.setBuddy(editor)
label.setToolTip(editor.toolTip())
form.addRow(label, container)
self.editors[key] = editor
self.labels[key] = label
self.errors[key] = error
def eventFilter(self, watched, event):
if event.type() in (QEvent.FocusIn, QEvent.Enter):
key = watched.property('parameter')
if key:
field = self.descriptions[key]
self.help.setText(tr(field.help) + '\n' + key)
# Use the model undo stack even inside a line editor.
if event.type() in (QEvent.ShortcutOverride, QEvent.KeyPress):
if (event.matches(QKeySequence.Undo) or
event.matches(QKeySequence.Redo)):
if event.type() == QEvent.KeyPress:
if event.matches(QKeySequence.Undo):
self.undo()
else:
self.redo()
event.accept()
return True
return super().eventFilter(watched, event)
def values(self):
return {p.name: str(p.value) for p in self.parameters.parameters}
def _error(self, key, message):
self.errors[key].setText(message)
self.errors[key].setVisible(bool(message))
self.editors[key].setStyleSheet(
'QLineEdit { border: 1px solid #b42318; }' if message else '')
def _validated(self, key, value):
try:
value = validate(self.descriptions[key], value)
except ValueError as error:
self._error(key, tr(str(error)) + tr(' Valeur non enregistrée.'))
return None
self._error(key, '')
return value
def _check_draft(self, key, value):
self._validated(key, value)
def _finish_text(self, key):
editor = self.editors[key]
if editor.isModified():
if self.commit(key, editor.text()):
editor.setModified(False)
def _choice_changed(self, key, index):
if not self._refreshing:
value = self.editors[key].itemData(index)
if value is not None:
self.commit(key, value)
def _commit_options(self):
if self._refreshing:
return
values = [combo.currentData() for combo in self.option_editors]
key = 'rubarbe_sediment_option'
if any(value is None for value in values):
self._options_pending = True
self._error(key, tr(
'Renseignez les trois choix. Valeur non enregistrée.'))
else:
self._options_pending = False
self.commit(key, ''.join(values))
def apply_pending(self):
"""Commit focused text before Ctrl+S; leave invalid drafts visible."""
values = self.values()
if (values.get('rubarbe_condam') == '0' and
values.get('rubarbe_regime') == '1'):
self.tabs.setCurrentIndex(1)
self.editors['rubarbe_condam'].setFocus()
return False
for key, editor in self.editors.items():
if (isinstance(editor, QLineEdit) and editor.isModified() and
active(self.descriptions[key], self.values())):
if not self.commit(key, editor.text()):
self.tabs.setCurrentIndex(self.descriptions[key].group)
editor.setFocus()
return False
if (self._options_pending and
active(FIELDS['rubarbe_sediment_option'], self.values())):
self.tabs.setCurrentIndex(2)
self.option_editors[0].setFocus()
return False
return True
def commit(self, key, value):
field = self.descriptions[key]
if not self.editable or not active(field, self.values()):
return False
value = self._validated(key, value)
if value is None:
return False
values = self.values()
values[key] = value
if (key in ('rubarbe_condam', 'rubarbe_regime') and
values.get('rubarbe_condam') == '0' and
values.get('rubarbe_regime') == '1'):
self.refresh()
return False
if isinstance(self.editors[key], QLineEdit):
self.editors[key].setModified(False)
if self.values()[key] != value:
self.stack.push(SetCommand(self.parameters, self.indices[key],
'value', value))
elif isinstance(self.editors[key], QLineEdit):
self.editors[key].setText(value)
return True
def refresh(self, *args):
self._refreshing = True
values = self.values()
for key, editor in self.editors.items():
field = self.descriptions[key]
value = canonical(field, values[key])
enabled = active(field, values)
editor.setEnabled(self.editable and enabled)
self.labels[key].setEnabled(enabled)
if isinstance(editor, QLineEdit) and editor.isModified():
self._validated(key, editor.text())
continue
if field.kind == 'options' and self._options_pending:
self.errors[key].setVisible(enabled)
continue
if field.kind == 'options':
valid = len(value) == 3 and all(c in '12' for c in value)
for i, combo in enumerate(self.option_editors):
index = combo.findData(value[i]) if valid else 0
combo.setCurrentIndex(index)
elif isinstance(editor, QComboBox):
# Display unknown historical values without replacing them.
for i in reversed(range(editor.count())):
if editor.itemData(i) is None:
editor.removeItem(i)
index = editor.findData(value)
if index < 0:
editor.addItem(tr('Valeur non reconnue : ') + value, None)
index = editor.count() - 1
editor.setCurrentIndex(index)
else:
editor.setText(values[key])
try:
validate(field, values[key])
self._error(key, '')
except ValueError as error:
self._error(key, tr('Valeur enregistrée à vérifier : ') +
values[key] + ' — ' + tr(str(error)))
self.errors[key].setVisible(
enabled and bool(self.errors[key].text()))
self._update_context(values)
for key, forbidden, blocked in (
('rubarbe_regime', '1', values.get('rubarbe_condam') == '0'),
('rubarbe_condam', '0', values.get('rubarbe_regime') == '1'),
):
editor = self.editors.get(key)
if editor is None:
continue
index = editor.findData(forbidden)
if index >= 0:
editor.model().item(index).setEnabled(not blocked)
self._refreshing = False
def _update_context(self, values):
option = values.get('rubarbe_sediment_option', '')
label = self.labels.get('rubarbe_sediment_visc')
if label is not None:
if len(option) == 3 and all(c in '12' for c in option):
title = ('Seuil critique adimensionnel (—)' if option[1] == '1'
else 'Viscosité cinématique de l’eau (m²/s)')
label.setText(tr(title))
else:
label.setText(tr(
'Viscosité / seuil : préciser les trois choix'))
warnings = []
if not self.editable:
warnings.append(tr('Étude en lecture seule.'))
transport = values.get('rubarbe_trased', '').lower() in ('y', 'o')
if not transport:
warnings.append(tr('Transport solide désactivé : les réglages '
'sédimentaires sont conservés mais inactifs.'))
elif (values.get('rubarbe_sediment_capsol') in ('2', '3') and
values.get('rubarbe_sediment_choixc') == '2'):
warnings.append(tr(
'La capacité solide choisie nécessite une méthode MPC ; '
'la répartition uniforme est incompatible.'))
if (values.get('rubarbe_condam') == '0' and
values.get('rubarbe_regime') == '1'):
warnings.append(tr('Une limite amont réfléchissante ne permet pas '
'd’imposer aussi le tirant d’eau.'))
try:
from tools import old_pamhyr_date_to_timestamp
def seconds(value):
return (old_pamhyr_date_to_timestamp(value) if ':' in value
else float(value.replace(',', '.')))
start = seconds(values['rubarbe_tinit'])
if seconds(values['rubarbe_tmax']) <= start:
warnings.append(tr('La fin du calcul doit suivre son début.'))
except (KeyError, ValueError):
pass
if transport and len(option) == 3 and option[1] == '2':
try:
if float(values.get('rubarbe_sediment_visc', '0')) > 0.001:
warnings.append(tr(
'Viscosité élevée pour de l’eau : vérifiez la valeur, '
'qui n’a pas été convertie lors du changement '
'de mode.'))
except ValueError:
pass
self.notice.setText('\n'.join(warnings))
self.notice.setVisible(bool(warnings))
def undo(self):
if self.editable:
self._options_pending = False
for editor in self.editors.values():
if isinstance(editor, QLineEdit):
editor.setModified(False)
self.stack.undo()
self.refresh()
def redo(self):
if self.editable:
self._options_pending = False
for editor in self.editors.values():
if isinstance(editor, QLineEdit):
editor.setModified(False)
self.stack.redo()
self.refresh()
+24 -3
View File
@@ -42,6 +42,7 @@ from PyQt5.QtWidgets import (
from View.SolverParameters.UndoCommand import *
from View.SolverParameters.Table import TableModel, ValueDelegate
from View.SolverParameters.RubarForm import RubarForm
from View.SolverParameters.translate import ParamTranslate
from Solver.Solvers import solver_long_name, solver_type_list
@@ -62,7 +63,7 @@ class SolverParametersWindow(PamhyrWindow):
title=name,
study=study,
config=config,
options=['copy'],
options=['undo', 'copy'],
trad=trad,
parent=parent
)
@@ -88,6 +89,15 @@ class SolverParametersWindow(PamhyrWindow):
editable_headers = []
for st in solver_type_list:
if st in ('rubar3', 'rubarbe'):
widget = RubarForm(
self._study.river.get_params(st), self._undo_stack[st],
editable=self._study.is_editable(),
)
widget.setObjectName(f"tab_{st}")
self._table[st] = widget
self._tab_widget.addTab(widget, solver_long_name[st])
continue
# Create widgets
widget = QWidget()
grid = QGridLayout()
@@ -135,8 +145,19 @@ class SolverParametersWindow(PamhyrWindow):
tab = self.current_tab()
self._table[tab].redo()
def _save(self, source):
for editor in self._table.values():
if isinstance(editor, RubarForm) and not editor.apply_pending():
self._tab_widget.setCurrentWidget(editor)
return
return super()._save(source)
def _copy(self):
logger.info("TODO: copy")
editor = self.focusWidget()
if isinstance(editor, QLineEdit):
editor.copy()
def _paste(self):
logger.info("TODO: paste")
editor = self.focusWidget()
if isinstance(editor, QLineEdit) and self._study.is_editable():
editor.paste()
+1 -13
View File
@@ -107,7 +107,7 @@ class WindowToolKit(object):
dialog = QFileDialog(self, options=options)
if select_file == "ExistingFile":
if select_file == "Existing_file":
mode = QFileDialog.FileMode.ExistingFile
elif select_file == "Directory":
mode = QFileDialog.FileMode.Directory
@@ -390,18 +390,6 @@ class ASubWindowFeatures(object):
"""
return self.find(QSpinBox, name).value()
def set_spin_box_enable(self, name: str, value: bool):
"""Set status of spinbox component
Args:
name: The spinbox component name
value: The boolean value
Returns:
Nothing
"""
self.find(QSpinBox, name).setEnabled(value)
def set_double_spin_box(self, name: str, value: int):
"""Set value of spinbox component
-6
View File
@@ -42,12 +42,6 @@ class PamhyrPlot(APlot):
color_plot_river_bottom = "grey"
color_plot_river_water = "blue"
color_plot_river_water_zone = "skyblue"
color_plot_ensemble_min = "green"
color_plot_ensemble_mean = "black"
color_plot_ensemble_max = "red"
color_plot_ensemble_std_deriv = "black"
colors = list(mplcolors.TABLEAU_COLORS)
linestyle = ['solid', 'dashed', 'dashdot', 'dotted']
-113
View File
@@ -1,113 +0,0 @@
# 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
+2 -25
View File
@@ -40,12 +40,6 @@ class CommonWordTranslate(PamhyrTranslate):
self._dict["time"] = _translate("CommonWord", "Time")
self._dict["date"] = _translate("CommonWord", "Date")
self._dict["low"] = _translate("CommonWord", "Low")
self._dict["high"] = _translate("CommonWord", "High")
self._dict["loc"] = _translate("CommonWord", "Loc")
self._dict["scale"] = _translate("CommonWord", "Scale")
self._dict["reach"] = _translate("CommonWord", "Reach")
self._dict["reaches"] = _translate("CommonWord", "Reaches")
self._dict["Select reach"] = _translate("CommonWord", "Select reach")
@@ -71,9 +65,6 @@ class CommonWordTranslate(PamhyrTranslate):
self._dict["file_csv"] = _translate(
"CommonWord", "CSV file (*.csv)"
)
self._dict["file_json"] = _translate(
"CommonWord", "JSON file (*.json)"
)
class UnitTranslate(CommonWordTranslate):
@@ -112,12 +103,6 @@ class UnitTranslate(CommonWordTranslate):
self._dict["unit_water_elevation"] = _translate(
"Unit", "Water Elevation (m)"
)
self._dict["unit_mean_water_elevation"] = _translate(
"Unit", "Mean water elevation (m)"
)
self._dict["unit_std_deriv_elevation"] = _translate(
"Unit", "Elevation standard derivation (m)"
)
self._dict["unit_water_elevation_envelop"] = _translate(
"Unit", "Water Elevation Envelop (m)"
)
@@ -137,16 +122,8 @@ class UnitTranslate(CommonWordTranslate):
self._dict["unit_min_velocity"] = _translate(
"Unit", "Min Velocity (m/s)"
)
self._dict["unit_discharge"] = (
_translate("Unit", "Discharge") + f" (m³/s)"
)
self._dict["unit_mean_discharge"] = (
_translate("Unit", "Mean discharge") + f" (m³/s)"
)
self._dict["unit_std_deriv_discharge"] = (
_translate("Unit", "Discharge standard derivation")
+ f" (m³/s)"
)
self._dict["unit_discharge"] = _translate(
"Unit", "Discharge") + f" (m³/s)"
self._dict["unit_discharge_envelop"] = _translate(
"Unit", "Discharge Envelop") + " (m³/s)"
self._dict["unit_max_discharge"] = _translate(
-111
View File
@@ -1,111 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>418</width>
<height>220</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_x">
<item>
<widget class="QLabel" name="label_x">
<property name="text">
<string>X axis:</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_y">
<item>
<widget class="QLabel" name="label_y">
<property name="text">
<string>Y axis:</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_3">
<layout class="QVBoxLayout" name="verticalLayout_solv">
<item>
<widget class="QLabel" name="label_solv">
<property name="text">
<string>Solver:</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="5" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_data">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
-7
View File
@@ -18,13 +18,6 @@
<item row="0" column="0">
<widget class="QTableView" name="tableView"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
-85
View File
@@ -1,85 +0,0 @@
<?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
@@ -1,106 +0,0 @@
<?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
@@ -1,157 +0,0 @@
<?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
@@ -1,123 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>152</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Draw function</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox"/>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Number of run</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>Custom seed</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_seed">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>2147483647</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
-112
View File
@@ -1,112 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_del"/>
<addaction name="action_edit"/>
<addaction name="separator"/>
<addaction name="action_export"/>
<addaction name="action_import"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add ensemble</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset>
<normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete selected ensembles</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit ensembles range</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_export">
<property name="icon">
<iconset>
<normaloff>ressources/export.png</normaloff>ressources/export.png</iconset>
</property>
<property name="text">
<string>Export</string>
</property>
<property name="toolTip">
<string>Export all ensemble configuration into json file</string>
</property>
</action>
<action name="action_import">
<property name="icon">
<iconset>
<normaloff>ressources/import.png</normaloff>ressources/import.png</iconset>
</property>
<property name="text">
<string>Import</string>
</property>
<property name="toolTip">
<string>Import ensemble configuration from json file</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
-232
View File
@@ -1,232 +0,0 @@
<?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
@@ -1,123 +0,0 @@
<?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>
+2 -2
View File
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>480</height>
<width>1000</width>
<height>680</height>
</rect>
</property>
<property name="windowTitle">
@@ -1,42 +0,0 @@
<?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">
<property name="minimum">
<double>-10000000000.000000000000000</double>
</property>
<property name="maximum">
<double>10000000000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -1,55 +0,0 @@
<?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">
<property name="minimum">
<double>-10000000000.000000000000000</double>
</property>
<property name="maximum">
<double>10000000000.000000000000000</double>
</property>
</widget>
</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">
<property name="minimum">
<double>-10000000000.000000000000000</double>
</property>
<property name="maximum">
<double>10000000000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+28 -6
View File
@@ -37,7 +37,7 @@ logger = logging.getLogger()
class Config(SQL):
def __init__(self):
self._version = '0.0.9'
self._version = '0.0.10'
self.filename = Config.filename()
self.set_default_value()
@@ -195,7 +195,7 @@ class Config(SQL):
)
if major == "0" and minor <= "2":
if int(release) < 10:
if int(release) < 8:
posix = os.name == 'posix'
ext = "" if posix else ".exe"
path = os.path.join("@install_dir",
@@ -215,11 +215,32 @@ class Config(SQL):
)
""")
if int(release) < 9:
self.execute(
"INSERT OR IGNORE INTO data "
"VALUES ('display_manning', 'False')"
)
if int(release) < 10:
posix = os.name == 'posix'
ext = "" if posix else ".exe"
path = os.path.join("@install_dir",
"rubar", f"rubarbe{ext}")
self.execute(f"""
INSERT OR IGNORE INTO solver VALUES (
'rubarbe',
'default-RubarBE',
'Default Pamhyr2 RubarBE version',
'', '', '',
'',
'"{path}" @args @input',
''
)
""")
self.execute(
f"UPDATE info SET value='{self._version}' " +
"WHERE key='version'"
@@ -416,11 +437,12 @@ class Config(SQL):
new._cmd_solver = f""""{path}" @args @input"""
self._solvers.append(new)
# Rubar3
ctor = solver_type_list["rubar3"]
new = ctor("default-Rubar3")
# RubarBE (same executable as Rubar3, see notice §10.3)
ctor = solver_type_list["rubarbe"]
new = ctor("default-RubarBE")
new._description = "Default Pamhyr2 RubarBE version"
new._cmd_solver = f""""@install_dir/rubar/rubarbe{ext}" @args @input"""
path = os.path.join("@install_dir", "rubar", f"rubarbe{ext}")
new._cmd_solver = f""""{path}" @args @input"""
self._solvers.append(new)
# Mage fake 7
Binary file not shown.