Compare commits

...
43 Commits
Author SHA1 Message Date
parouby 58e3da8942 Ensemble: Add base for RKC ensemble plot. 2026-09-04 17:51:52 +02:00
parouby b7c3793a66 Geometry: Point: Fix index creation. 2026-09-04 11:31:23 +02:00
parouby 2498466a51 Ensemble: Results: Fix runs table. 2026-09-04 10:31:04 +02:00
parouby ef36eba039 Ensemble: Results: Add method to open ensemble results window. 2026-09-04 10:11:46 +02:00
parouby c40e28ed0f Ensemble: Results: Add ui file for results window. 2026-09-03 16:21:53 +02:00
parouby b21846a513 Ensemble: Results: Add base of 'ResultsEnsembleWindow' obj. 2026-09-03 16:17:49 +02:00
parouby 3532f75c2b Solver: Mage: Minor change. 2026-09-02 16:21:31 +02:00
parouby 6c59919972 Pep8: Fix. 2026-09-01 11:36:35 +02:00
parouby 77d56b8bec Ensemble: Keep results nim, mean and max for each run. 2026-09-01 11:34:37 +02:00
parouby 7ef7bc13dc Ensemble: Continue to adapt results. 2026-08-28 16:15:59 +02:00
parouby a1d85274b9 Ensemble: Put ensemble results data into results obj. 2026-08-28 11:10:29 +02:00
parouby e7b9bc92a2 Ensemble: Run: Log variation. 2026-08-27 10:10:08 +02:00
parouby 7d8e414fe2 Ensemble: Fix missing requirements 'psutil'. 2026-08-27 09:48:12 +02:00
parouby 476bcc9c55 Ensemble: Made it compatible with other solver than mage8. 2026-08-27 09:44:03 +02:00
parouby ae006f52f4 Ensemble: Table: Fix double assignation for data type and target. 2026-08-26 16:04:58 +02:00
parouby 3583a93ae0 Ensemble: Set ensemble as read only study (except for ensemble). 2026-08-25 17:07:34 +02:00
parouby 212a00a43c Ensemble: Fix missing file. 2026-08-25 14:42:41 +02:00
parouby e5dbba362a Debug: Add memory usage to debug timer window. 2026-08-21 16:55:50 +02:00
parouby bdc09addfa Ensemble: Fix results reading. 2026-08-21 16:55:17 +02:00
parouby 51b3987a4a Mage: Ensemble: Add bin reading method. 2026-08-21 15:28:17 +02:00
parouby e7290fa81b Ensemble: Fix ensemble study meta data save. 2026-08-21 15:27:17 +02:00
parouby b5432498c6 Ensemble: Add sequential runs with RUG variation into mage export. 2026-08-20 16:00:40 +02:00
parouby 807592eab2 Ensemble: Rename 'range' to 'params'. 2026-08-14 16:33:41 +02:00
parouby 394c0779fc Ensemble: Add min max dialog for range. 2026-08-14 16:30:27 +02:00
parouby 5b3f777100 Ensemble: Minor change and add target data to table. 2026-08-14 09:27:39 +02:00
parouby 8b36905842 Ensemble: Fix some view/model interations. 2026-08-14 09:27:36 +02:00
parouby b203e8e062 Ensemble: Fix editing ensemble list. 2026-08-12 16:46:13 +02:00
parouby 4d4df1bde2 Ensemble: Function: Randomize Uniform sample. 2026-08-12 15:19:12 +02:00
parouby 876780a349 Ensembles: Open Ensemble windows from scenarios. 2026-08-12 14:50:58 +02:00
parouby 3b83377f3c Ensemble: Draw ensemble scenario as rect and minor fix. 2026-08-07 16:28:13 +02:00
parouby e64c45b621 Ensemble: Create new ensemble scenario. 2026-08-07 15:18:22 +02:00
parouby 70dae9ab1b Ensemble: Prepare view with add base classes. 2026-08-06 15:33:53 +02:00
parouby 0bb3c5bc65 Ensemble: Minor change to prepare view integration. 2026-07-31 16:19:13 +02:00
parouby 54f5c7c548 Ensemble: Switch data pid to data ref, and minor change. 2026-07-30 11:14:33 +02:00
parouby 56217186ff Ensemble: Fix save/load methode. 2026-07-29 16:31:13 +02:00
parouby 82fb2bcc48 Ensemble: Change function to id and name, minor change and fix. 2026-07-29 15:19:36 +02:00
parouby 6dc42b637a Ensemble: Function: Some minor fix and add it to study. 2026-07-24 11:45:39 +02:00
parouby e6d12713e3 Ensemble: Minor change. 2026-07-24 10:04:15 +02:00
parouby 9456cf32fc Ensemble: Function: Add base classes. 2026-07-24 10:02:25 +02:00
parouby 9b1fb4e7d2 Ensemble: Add ensemble base classes. 2026-07-23 09:51:17 +02:00
parouby c09c62f077 Merge: Results reading optimisation. 2026-07-10 10:18:58 +02:00
parouby 7a2e93a995 Results: Custom plot: Minor change. 2026-07-10 09:16:05 +02:00
Dylan Jeannin cfbc2d96a1 Mage: Fix parsing for .LAT files 2026-07-03 13:07:10 +02:00
38 changed files with 3232 additions and 34 deletions

No files matched your search

+1 -1
View File
@@ -20,4 +20,4 @@
"python-pyqtgraph" "python-qscintilla"
"python-numpy@1" "python-shapely" "python-pyshp"
"python-matplotlib" "python-colorama"
"python-rasterio"))
"python-rasterio" "python-psutil"))
+1 -1
View File
@@ -14,4 +14,4 @@ platformdirs>=4.2.0
pyshp>=2.3.1
rasterio==1.3.11
#fortranformat==2.0.3
psutil>=7.2.2
+254
View File
@@ -0,0 +1,254 @@
# 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.Function import Uniform
logger = logging.getLogger()
class Ensemble(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1, name: str = "",
function=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._function = function
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,
function INTEGER NOT NULL,
function_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"]
functions = data["ens_functions"]
stricklers = data["stricklers"]
loaded = data['loaded_pid']
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"function, function_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
function = next(
filter(fn, functions.lst),
None
)
target_data = next(
filter(
lambda s: s._pamhyr_id == data_pid,
stricklers.lst
),
None
)
rdata = cls._decode_params(params, params_len)
new_ensemble = cls(
id, name=name,
function=function,
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._function is None:
fid = -1
fname = "NULL"
else:
fid = self._function._pamhyr_id
fname = self._function._name
if self._function._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, " +
" function, function_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 get_sample(self, nb):
return np.nditer(
self._function.get_sample(
self._params, nb
)
)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self._status.modified()
@property
def function(self):
return self._function
@function.setter
def function(self, function):
self._function = function
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()
+53
View File
@@ -0,0 +1,53 @@
# EnsembleList.py -- Pamhyr
# Copyright (C) 2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Ensembles.Ensemble import Ensemble
class EnsembleList(PamhyrModelList):
_sub_classes = [Ensemble]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
new._lst = Ensemble._db_load(
execute, data
)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM ensemble " +
f"WHERE scenario = {self._status.scenario_id}"
)
for ens in self._lst:
ens._db_save(execute, data=data)
return True
def new(self, index):
r = Ensemble(status=self._status)
self._lst.insert(index, r)
self._status.modified()
return r
+189
View File
@@ -0,0 +1,189 @@
# Function.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 Function(SQLSubModel):
_sub_classes = []
def __init__(self, id: int = -1,
name: str = "", script: str = "",
status=None):
super(Function, self).__init__(
id=id, status=status,
owner_scenario=0
)
self._name = name
self._type = "generic"
self._script = script
@property
def name(self):
return self._name
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE ensemble_function{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_function " +
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_function = cls(
id, name=name, script=script,
status=data["status"]
)
if deleted:
new_function.set_as_deleted()
loaded.add(id)
new.append(new_function)
return new
def _db_save(self, execute, data=None):
# Only save custom function
if self._type != "custom":
return True
if not self.must_be_saved():
return True
execute(
"INSERT INTO " +
"ensemble_function(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 get_sample(self, r):
raise NotImplementedMethodeError(cls, cls._load)
###########################
# FUNCTION IMPLEMENTATION #
###########################
class Uniform(Function):
_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
)
def get_sample(self, ens_range, nb):
b = ens_range[0]
t = ens_range[1]
step = (t - b) / nb
r = np.arange(b, t, step)
np.random.shuffle(r)
return r
class Custom(Function):
_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._type = "custom"
def get_sample(self, ens_range, nb):
try:
# Run script to (re)define the sample function
f = eval(self._script)
return f(ens_range, nb)
except Exception as e:
logger_exception(e)
return None # TODO: Raise helpful exception
+58
View File
@@ -0,0 +1,58 @@
# FunctionList.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.Function import *
class FunctionList(PamhyrModelList):
_sub_classes = [Function]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
# Default functions
new._lst = [
Uniform(status=data['status']),
]
# DB custom functions
new._lst += Function._db_load(
execute, data
)
return new
def _db_save(self, execute, data={}):
execute(
"DELETE FROM ensemble_function"
)
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
+2 -3
View File
@@ -95,10 +95,9 @@ class PointXYZ(Point):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and minor == "2":
if int(release) < 5:
# Try to create index at each update
execute(
"CREATE INDEX idx_point " +
"CREATE INDEX IF NOT EXISTS idx_point " +
"ON geometry_pointXYZ(profile, scenario);"
)
+7 -2
View File
@@ -331,14 +331,14 @@ class TableData(SQLSubModel):
if isinstance(key, tuple):
return self._data[key[0], key[1]]
self._date[key]
self._data[key]
class Results(SQLSubModel):
_sub_classes = [River, TableData, AdditionalData]
def __init__(self, id=-1, study=None, solver=None,
repertory="", name="0"):
repertory="", name="0", is_ensemble=False):
super(Results, self).__init__(
id=id, status=study.status,
owner_scenario=study.status.scenario.id
@@ -351,6 +351,8 @@ class Results(SQLSubModel):
self._solver = solver
self._repertory = repertory
self._is_ensemble = is_ensemble
self._meta_data = {
# Keep results creation date
"creation_date": datetime.now(),
@@ -364,6 +366,9 @@ 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"]
+2
View File
@@ -404,6 +404,8 @@ class River(SQLSubModel):
# Dict with timestamps as key
self._reachs = []
for i, _ in enumerate(self._study.river.enable_edges()):
self.add(i)
def __len__(self):
return len(self._reachs)
+10 -1
View File
@@ -60,6 +60,7 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results
logger = logging.getLogger()
@@ -476,6 +477,7 @@ class River(Graph):
D90AdisTSList,
DIFAdisTSList,
GeoTIFFList,
EnsembleList,
Results
]
@@ -515,6 +517,9 @@ class River(Graph):
self._geotiff = GeoTIFFList(status=self._status)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {}
@classmethod
@@ -629,6 +634,8 @@ 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):
@@ -664,6 +671,8 @@ class River(Graph):
objs.append(self._geotiff)
objs.append(self._ensembles)
for solv_type in self.results:
objs.append(self.results[solv_type])
@@ -740,7 +749,7 @@ class River(Graph):
self._BoundaryConditionsAdisTS,
self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS,
self._geotiff,
self._geotiff, self._ensembles,
]
for solver in self._parameters:
+29 -12
View File
@@ -53,6 +53,7 @@ class Scenario(SQLSubModel):
"geometry_pointXYZ", "geometry_profileXYZ",
"river_reach", "river_node",
"geotiff", "reservoir", "reservoir_data",
"ensemble",
]
related_tables = tables_with_deleted_column + [
@@ -67,6 +68,7 @@ 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__()
@@ -76,6 +78,7 @@ class Scenario(SQLSubModel):
self._x, self._y = x, y
self._name = name
self._description = description
self._is_ensemble = is_ensemble
self._revision = revision
self._parent = parent
@@ -98,6 +101,7 @@ class Scenario(SQLSubModel):
y REAL NOT NULL DEFAULT 1000,
name TEXT NOT NULL,
description TEXT NOT NULL,
is_ensemble BOOL DEFAULT FALSE,
revision INTEGER NOT NULL,
parent_id INTEGER REFERENCES scenario(id)
)
@@ -133,6 +137,13 @@ class Scenario(SQLSubModel):
cls._db_create(execute)
cls._db_add_default(execute)
if major == "0" and int(minor) == 2:
if int(release) < 8:
execute(
f"ALTER TABLE scenario " +
"ADD COLUMN is_ensemble BOOL DEFAULT FALSE"
)
return True
@classmethod
@@ -153,7 +164,8 @@ class Scenario(SQLSubModel):
scenarios = {}
table = execute(
"SELECT id, x, y, name, description, revision, parent_id " +
"SELECT id, x, y, name, description, " +
" is_ensemble, revision, parent_id " +
"FROM scenario " +
"ORDER BY id ASC"
)
@@ -165,6 +177,7 @@ class Scenario(SQLSubModel):
x, y = next(it), next(it)
name = next(it)
desc = next(it)
is_ens = next(it)
revi = next(it)
parent = next(it)
@@ -174,7 +187,9 @@ class Scenario(SQLSubModel):
new = cls(
id=id, x=x, y=y,
name=name, description=desc,
revision=revi, parent=parent
is_ensemble=is_ens,
revision=revi,
parent=parent
)
scenarios[id] = new
@@ -184,21 +199,18 @@ class Scenario(SQLSubModel):
if self.is_deleted():
return self.drop_all(execute)
parent = 'NULL'
parent = None
if self.parent is not None:
parent = self.parent._id
execute(
"INSERT OR REPLACE INTO " +
"scenario(id, x, y, name, description, revision, parent_id) " +
"VALUES (" +
f"{self._id}, " +
f"{self.x}, {self.y}, " +
f"'{self._db_format(self.name)}', " +
f"'{self._db_format(self.description)}', " +
f"{self._revision}, " +
f"{parent}" +
")"
"scenario(id, x, y, name, description, " +
" is_ensemble, revision, parent_id) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
self._id, self.x, self.y,
self.name, self.description, self._is_ensemble,
self._revision, parent
)
return True
@@ -323,6 +335,9 @@ class Scenario(SQLSubModel):
def parent(self):
return self._parent
def is_ensemble(self):
return self._is_ensemble
def set_pos(self, x, y):
self._x = x
self._y = y
@@ -340,6 +355,8 @@ 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
+5 -2
View File
@@ -57,8 +57,11 @@ class Scenarios(PamhyrModelDict):
return None
def new(self, parent):
new = Scenario(parent=parent)
def new(self, parent, is_ensemble=False):
new = Scenario(
parent=parent,
is_ensemble=is_ensemble
)
self.set(new._id, new)
return new
+52 -4
View File
@@ -33,6 +33,7 @@ from Model.Status import StudyStatus
from Model.Except import NotImplementedMethodeError
from Model.River import River
from Model.Geometry.Reach import Reach
from Model.Ensembles.FunctionList import FunctionList
from Model.HydraulicStructures.HydraulicStructures import (
HydraulicStructure
)
@@ -46,10 +47,11 @@ logger = logging.getLogger()
class Study(SQLModel):
_version = "0.2.7"
_version = "0.2.8"
_sub_classes = [
Scenario,
FunctionList,
River,
]
@@ -84,6 +86,9 @@ class Study(SQLModel):
self.scenarios = Scenarios(status=self.status)
self.scenarios[0] = s0
self.status.scenario = s0
self._ens_functions = FunctionList(status=self.status)
self._river = River(status=self.status)
else:
self._init_db_file(filename, is_new=False)
@@ -388,12 +393,19 @@ class Study(SQLModel):
lambda a, s: a or (s.parent is scenario),
new.scenarios.lst,
False
):
) or scenario.is_ensemble():
new.status.set_as_read_only()
data["scenario"] = scenario
data["loaded_pid"] = set()
# Get ensemble function
new._ens_functions = FunctionList._db_load(
sql_exec, data=data
)
data["ens_functions"] = new._ens_functions
# Load river data
new._river = River._db_load(
sql_exec, data=data
@@ -454,8 +466,27 @@ class Study(SQLModel):
)
progress()
if self.status.scenario.is_ensemble():
lst = [
self.scenarios, self._ens_functions,
self._river._ensembles,
]
for solv in self._river._results:
lst.append(self._river._results[solv])
self._save_submodel(
[self.scenarios, self._river],
lst, data=progress
)
progress()
self.commit()
return
self._save_submodel(
[
self.scenarios, self._ens_functions,
self._river
],
data=progress
)
@@ -573,7 +604,8 @@ class Study(SQLModel):
data = {
"status": self.status,
"loaded_pid": set(),
"scenario": scenario
"scenario": scenario,
"ens_functions": self._ens_functions,
}
self.status.start_loading()
@@ -658,6 +690,22 @@ class Study(SQLModel):
self.status.set_as_editable()
return new
def create_ensemble_scenario(self, switch=True):
new = self.scenarios.new(
self.status.scenario,
is_ensemble=True
)
new.name = self.status.scenario.name + " (ensemble)"
new.set_pos(self.status.scenario.x + 100,
self.status.scenario.y)
if switch:
self.status.scenario = new
self.status.set_as_editable()
return new
@property
def results(self):
return self._river.results
+239 -1
View File
@@ -1027,6 +1027,75 @@ 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 #
###########
@@ -1091,7 +1160,7 @@ class Mage8(Mage):
for i in range(nb_reach):
# Add results reach to reach list
r = results.river.add(i)
r = results.river.reach(i)
reachs.append(r)
# ID of first and last reach profiles
@@ -1451,6 +1520,175 @@ 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_min"] = z
res["z_max"] = z
res["q_min"] = q
res["q_max"] = q
res["res_z_min"] = []
res["res_z_mean"] = []
res["res_z_max"] = []
res["res_q_min"] = []
res["res_q_mean"] = []
res["res_q_max"] = []
res["traces"] = []
else:
# Get mean results for Z and Q
res["z_mean"] = (res["z_mean"] + z) / 2
res["q_mean"] = (res["q_mean"] + q) / 2
# 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"])
# Keep results min, mean and max
res["res_z_min"].append((run_number, np.min(z, axis=0)))
res["res_z_mean"].append((run_number, np.mean(z, axis=0)))
res["res_z_max"].append((run_number, np.max(z, axis=0)))
res["res_q_min"].append((run_number, np.min(q, axis=0)))
res["res_q_mean"].append((run_number, np.mean(q, axis=0)))
res["res_q_max"].append((run_number, np.max(q, axis=0)))
# FIXME: Keep all results ?
res["traces"].append((run_number, tables))
@timer
def results_ensemble(self, study, repertory,
tables, run_number,
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"
+15
View File
@@ -16,6 +16,8 @@
# -*- coding: utf-8 -*-
import os
import psutil
import logging
import traceback
@@ -120,7 +122,10 @@ class TimerWindow(PamhyrWindow):
parent=parent
)
self._process = psutil.Process(os.getpid())
self.setup_table()
self.setup_label()
self.setup_alarm()
self.setup_connections()
@@ -136,6 +141,15 @@ 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)
@@ -148,6 +162,7 @@ class TimerWindow(PamhyrWindow):
def update(self):
self._table.update_lst()
self.setup_label()
# def reset(self):
# self._table.reset_timers()
+268
View File
@@ -0,0 +1,268 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
)
from View.Ensembles.UndoCommand import (
SetNameCommand, SetTypeCommand, SetDataCommand,
SetFunctionCommand,
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 == "function":
self.editor.addItems(
[self._trad["not_defined"]] +
list(
map(
lambda f: f.name,
self._study._ens_functions.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 == "function":
value = next(
filter(
lambda f: f.name == text,
self._study._ens_functions.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] == "function":
value = self._lst.get(row).function
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] == "function":
self._undo.push(
SetFunctionCommand(
self._lst, row, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
self._undo.push(
AddCommand(
self._lst, row,
)
)
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
self._undo.push(
DelCommand(
self._lst, rows
)
)
self.endRemoveRows()
self.layoutChanged.emit()
def undo(self):
self._undo.undo()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self.layoutChanged.emit()
+50
View File
@@ -0,0 +1,50 @@
# translate.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class EnsemblesTranslate(MainTranslate):
def __init__(self):
super(EnsemblesTranslate, self).__init__()
self._dict["Ensembles"] = _translate(
"Ensembles", "Ensembles"
)
self._dict["ensemble"] = _translate(
"Ensembles", "Ensembles scenario"
)
self._dict["strickler_minor"] = _translate(
"Ensembles", "Stricklers minor"
)
self._dict["strickler_medium"] = _translate(
"Ensembles", "Stricklers medium"
)
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"type": _translate("Ensembles", "Type"),
"target_data": _translate("Ensembles", "Target data"),
"function": _translate("Ensembles", "Function"),
"parameters": _translate("Ensembles", "Parameters"),
}
+131
View File
@@ -0,0 +1,131 @@
# 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 SetFunctionCommand(QUndoCommand):
def __init__(self, ensembles, index, function):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._old = self._ensembles.get(self._index).function
self._new = function
def undo(self):
self._ensembles.get(self._index).function = self._old
def redo(self):
self._ensembles.get(self._index).function = self._new
class AddCommand(QUndoCommand):
def __init__(self, ensembles, index):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._index = index
self._new = None
def undo(self):
self._ensembles.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._ensembles.new(self._index)
else:
self._ensembles.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, ensembles, rows):
QUndoCommand.__init__(self)
self._ensembles = ensembles
self._rows = rows
self._friction = []
for row in rows:
self._friction.append((row, self._ensembles.get(row)))
self._friction.sort()
def undo(self):
for row, el in self._friction:
self._ensembles.insert(row, el)
def redo(self):
self._ensembles.delete_i(self._rows)
+234
View File
@@ -0,0 +1,234 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2026 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow, PamhyrDialog
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QSettings,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
)
from Modules import Modules
from View.Ensembles.Table import (
EnsembleTableModel, ComboBoxDelegate
)
from View.Ensembles.Translate import EnsemblesTranslate
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_function = ComboBoxDelegate(
data=self._ensembles,
study=self._study,
mode="function",
trad=self._trad,
parent=self
)
return {
"type": self._delegate_data_type,
"target_data": self._delegate_stricklers,
"function": self._delegate_function,
}
def setup_table(self):
self._table = {}
delegates = self.setup_table_delegate()
editable_headers = [
"name", "type", "function", "target_data"
]
table = self.find(QTableView, f"tableView")
self._table = EnsembleTableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates=delegates,
data=self._ensembles,
trad=self._trad,
undo=self._undo_stack,
opt_data=self._study
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_connections(self):
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
table = self.find(QTableView, f"tableView")
table.selectionModel()\
.selectionChanged\
.connect(self.update)
self._table.dataChanged\
.connect(self.update)
def index_selected_rows(self):
table = self.find(QTableView, f"tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._ensembles) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def edit(self):
rows = self.index_selected_rows()
for row in rows:
try:
dlg = MinMaxDialog(
ensemble=self._ensembles.get(row),
trad=self._trad,
parent=self
)
if dlg.exec():
self._ensembles.get(row).params = dlg._minmax
except Exception as e:
logger_exception(e)
return
class MinMaxDialog(PamhyrDialog):
_pamhyr_ui = "MinMaxDialog"
_pamhyr_name = "Minmax"
def __init__(self, ensemble=None,
trad=None, parent=None):
super(MinMaxDialog, self).__init__(
title=trad[self._pamhyr_name],
trad=trad,
options=[],
parent=parent
)
self._ensemble = ensemble
self._init_default_values()
def _init_default_values(self):
self._minmax = self._ensemble.params
if len(self._minmax) > 0:
self.set_double_spin_box("doubleSpinBox_min", self._minmax[0])
self.set_double_spin_box("doubleSpinBox_max", self._minmax[1])
@property
def minmax(self):
return self._minmax
def accept(self):
self._minmax = [
self.get_double_spin_box("doubleSpinBox_min"),
self.get_double_spin_box("doubleSpinBox_max")
]
super().accept()
def reject(self):
self.close()
+3 -4
View File
@@ -96,14 +96,13 @@ class ComboBoxDelegate(QItemDelegate):
reach = self._data.hydraulic_structures\
.get(index.row())\
.input_reach
profiles = list(
value = next(
filter(
lambda p: p.display_name() == text,
reach.reach.profiles
),
None
)
)
value = profiles[0] if len(profiles) > 0 else None
else:
value = text
+77 -1
View File
@@ -66,6 +66,7 @@ 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
@@ -82,11 +83,14 @@ 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
from View.Results.Window import ResultsWindow, ResultsEnsembleWindow
from View.Results.CompareDialog import (
CompareScenariosWindow
)
@@ -1222,6 +1226,7 @@ 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"],
@@ -1255,6 +1260,29 @@ 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
@@ -1562,6 +1590,9 @@ 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,
@@ -1570,6 +1601,25 @@ 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():
run_number = dlg._number
sol = SolverLogEnsWindow(
run_number=run_number,
study=self._study,
config=self.conf,
solver=solver,
parent=self
)
sol.show()
def msg_box_results_continue(self):
dlg = QMessageBox(self)
@@ -1646,6 +1696,12 @@ 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,
@@ -1665,6 +1721,26 @@ 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"],
+1 -1
View File
@@ -95,7 +95,7 @@ class CustomPlot(PamhyrPlot):
table = results.get("table")["zfd"]
ts = results.get_timestamp_id(self._current_timestamp)
rk = reach.geometry.get_rk()
# rk = reach.geometry.get_rk()
z = list(
map(
lambda p: table[ts, p.global_index],
+311
View File
@@ -0,0 +1,311 @@
# 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, run_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._current_run_id = run_id[0]
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._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[self._current_run_id] is None:
return
results = self.results[self._current_run_id]
reach = results.river.reach(self._current_reach_id)
self.draw_bottom(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[self._current_run_id]
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[self._current_run_id]
rk = reach.geometry.get_rk()
z_min = reach.geometry.get_z_min()
colors = [
("z_min", self.color_plot_ensemble_min),
("Z", 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)
water_z = list(
map(
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
self.water = self.canvas.axes.plot(
rk, water_z,
lw=1., color=color,
linestyle='dotted',
)
def draw_additional_data(self, run_id):
results = self.results[run_id]
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_result(self, run_id):
self._current_run_id = run_id[0]
self.update()
def set_timestamp(self, timestamp):
self._current_timestamp = timestamp
self.update()
def update(self):
if not self._init:
self.draw()
results = self.results[self._current_run_id]
reach = results.river.reach(self._current_reach_id)
self.update_water_elevation()
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_run_id = self._parent._get_current_results()[0]
self._current_timestamp = self._parent._get_current_timestamp()
self._init = False
self.update()
def update_water_elevation(self):
result = self.results[self._current_run_id]
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"]
ts = result.get_timestamp_id(self._current_timestamp)
water_z = list(
map(
lambda p: table[
ts, p.global_index
],
reach.profiles
)
)
self.water[0].set_data(
rk, water_z
)
self.water_fill.remove()
self.water_fill = self.canvas.axes.fill_between(
rk, self._river_bottom, water_z,
where=[rb <= wz for rb, wz in zip(self._river_bottom, water_z)],
color=self.color_plot_river_water_zone,
alpha=0.7, interpolate=True
)
def update_additional_data(self):
self.draw_additional_data(0)
self.update_idle()
def update_current(self):
results = self.results[self._current_run_id]
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()
+6
View File
@@ -61,6 +61,8 @@ 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)
@@ -93,6 +95,10 @@ 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)
+240
View File
@@ -72,6 +72,7 @@ from View.Results.PlotRKC import PlotRKC
from View.Results.PlotH import PlotH
from View.Results.PlotSedReach import PlotSedReach
from View.Results.PlotSedProfile import PlotSedProfile
from View.Results.PlotRKCEnsemble import PlotRKCEnsemble
from View.Results.CustomPlot.Plot import CustomPlot
from View.Results.CustomPlot.CustomPlotValuesSelectionDialog import (
@@ -1437,3 +1438,242 @@ class ResultsWindow(PamhyrWindow):
for data in results.get("additional_data"):
for p in self._additional_plot:
self._additional_plot[p].add_imported_plot(data)
class ResultsEnsembleWindow(ResultsWindow):
_pamhyr_ui = "ResultsEnsemble"
_pamhyr_name = "Results"
def setup_table(self):
self._table = {}
for t in ["reach", "profile", "runs"]:
table = self.find(QTableView, f"tableView_{t}")
self._table[t] = TableModel(
table_view=table,
table_headers=self._trad.get_dict(f"table_headers_{t}"),
data=self._results,
undo=self._undo_stack,
opt_data=t,
parent=self
)
self._table[t]._timestamp = self._timestamps[
self._slider_time.value()
]
def setup_plots(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_widget.setTabsClosable(True)
tab_widget.tabCloseRequested.connect(self.delete_tab)
tab_widget.tabBar().setTabButton(0, QTabBar.RightSide, None)
tab_widget.tabBar().setTabButton(1, QTabBar.RightSide, None)
self.canvas_1 = MplCanvas(width=5, height=4, dpi=100)
self.canvas_1.setObjectName("canvas_1")
self.toolbar_1 = PamhyrPlotToolbar(
self.canvas_1, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_1 = self.find(QVBoxLayout,
"verticalLayout_elevation")
self.plot_layout_1.addWidget(self.toolbar_1)
self.plot_layout_1.addWidget(self.canvas_1)
self.plot_rkc = PlotRKCEnsemble(
canvas=self.canvas_1,
results=self._results,
reach_id=0,
profile_id=0,
run_id=self._current_results,
trad=self._trad,
toolbar=self.toolbar_1,
parent=self
)
self.plot_rkc.draw()
self.canvas_2 = MplCanvas(width=5, height=4, dpi=100)
self.canvas_2.setObjectName("canvas_2")
self.toolbar_2 = PamhyrPlotToolbar(
self.canvas_2, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.plot_layout_2 = self.find(
QVBoxLayout, "verticalLayout_hydrograph")
self.plot_layout_2.addWidget(self.toolbar_2)
self.plot_layout_2.addWidget(self.canvas_2)
self.plot_h = PlotH(
canvas=self.canvas_2,
results=self._results,
reach_id=0,
profile_id=[0],
res_id=self._current_results,
trad=self._trad,
toolbar=self.toolbar_2,
parent=self
)
self.plot_h.draw()
def setup_connections(self):
# Action
actions = {
"action_add": self._add_custom_plot,
"action_export": self._export,
# "action_export": self.export_current,
"action_import_data": self.import_data
}
if len(self._results) > 1:
self.find(QAction, "action_reload").setEnabled(False)
for action in actions:
self.find(QAction, action).triggered.connect(
actions[action]
)
# Table and Plot
fun = {
"reach": self._set_current_reach,
"profile": self._set_current_profile,
"runs": self._set_current_results,
}
for t in ["reach", "profile", "runs"]:
table = self.find(QTableView, f"tableView_{t}")
table.selectionModel()\
.selectionChanged\
.connect(fun[t])
self._table[t].dataChanged.connect(fun[t])
self._slider_time.valueChanged.connect(self._set_current_timestamp)
self._button_play.setChecked(False)
self._button_play.clicked.connect(self._pause)
self._button_back.clicked.connect(self._back)
self._button_next.clicked.connect(self._next)
self._button_first.clicked.connect(self._first)
self._button_last.clicked.connect(self._last)
self._timer.timeout.connect(self._update_slider)
# tabs
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_widget.currentChanged.connect(self.tab_changed)
def _set_current_results(self):
table = self.find(QTableView, f"tableView_runs")
indexes = table.selectedIndexes()
if len(indexes) == 0:
return
self.update(runs_id=[i.row() for i in indexes])
def update(self,
reach_id=None,
profile_id=None,
runs_id=None,
timestamp=None):
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_index = tab_widget.currentIndex()
name = tab_widget.tabText(tab_index)
if reach_id is not None:
if tab_index == 1:
self.plot_rkc.set_reach(reach_id)
elif tab_index == 2:
self.plot_h.set_reach(reach_id)
elif tab_index > 2:
self._additional_plot[name].set_reach(reach_id)
self.update_table_selection_reach(reach_id)
self.update_table_selection_profile(0)
if profile_id is not None:
if tab_index == 1:
self.plot_rkc.set_profile(profile_id[0])
elif tab_index == 2:
self.plot_h.set_profile(profile_id)
elif tab_widget.currentIndex() > 2:
self._additional_plot[name].set_profile(profile_id[0])
tab_widget = self.find(QTabWidget, f"tabWidget")
if tab_widget.currentIndex() != 2:
self.update_table_selection_profile(profile_id[0])
if runs_id is not None:
if tab_index == 1:
self._current_results = runs_id
self.plot_rkc.set_result(runs_id)
elif tab_index == 2:
self.plot_h.set_result(runs_id)
elif tab_widget.currentIndex() > 2:
self._additional_plot[name].set_result(runs_id)
if timestamp is not None:
self.plot_rkc.set_timestamp(timestamp)
if tab_index == 2:
self.plot_h.set_timestamp(timestamp)
if tab_widget.currentIndex() > 2:
self._additional_plot[name].set_timestamp(timestamp)
self.update_statusbar()
def tab_changed(self, i):
if i != 2:
if len(self._get_current_profiles_list()) > 1:
# unselect all profiles but the first one
profile_id = self._get_current_profile()
self.update_table_selection_profile(profile_id)
tab_widget = self.find(QTabWidget, f"tabWidget")
tab_index = tab_widget.currentIndex()
name = tab_widget.tabText(tab_index)
if tab_index == 1:
self.plot_rkc.update_all()
elif tab_index == 2:
self.plot_h.update_all()
elif tab_index > 2:
self._additional_plot[name].update_all()
def update_table_selection_reach(self, ind):
table = self.find(QTableView, f"tableView_reach")
selectionModel = table.selectionModel()
index = table.model().index(ind, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
self._table["profile"].update(ind)
def update_table_selection_profile(self, ind):
for t in ["profile"]:
table = self.find(QTableView, f"tableView_{t}")
selectionModel = table.selectionModel()
index = table.model().index(ind, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
table.scrollTo(index)
def update_table_selection_solver(self, ind):
return
+15
View File
@@ -44,15 +44,26 @@ class ResultsTranslate(MainTranslate):
self._dict['envelop'] = _translate("Results", "Envelop")
self._dict['solver'] = _translate("Results", "Solver")
self._dict['runs'] = _translate("Results", "Runs")
self._dict['x'] = _translate("Results", "X (m)")
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"
)
@@ -81,6 +92,10 @@ 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"),
}
+250 -1
View File
@@ -20,6 +20,8 @@ import os
import logging
import tempfile
import numpy as np
from queue import Queue
from tools import trace, timer, logger_exception
@@ -40,7 +42,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QTextEdit,
QTextEdit, QProgressBar,
)
from View.RunSolver.Log.Window import SolverLogFileWindow
@@ -57,6 +59,36 @@ _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._init_default_values()
def _init_default_values(self):
self.set_spin_box("spinBox", 100)
@property
def number(self):
return self._number
def accept(self):
self._number = self.get_spin_box("spinBox")
super().accept()
def reject(self):
self.close()
class SelectSolverWindow(PamhyrDialog):
_pamhyr_ui = "SelectSolver"
_pamhyr_name = "Select solver"
@@ -425,3 +457,220 @@ class SolverLogWindow(PamhyrWindow):
parent=self,
)
log.show()
class SolverLogEnsWindow(SolverLogWindow):
_pamhyr_ui = "SolverLogEnsemble"
_pamhyr_name = "Solver Log Ensemble"
def __init__(self, run_number=100,
study=None, config=None,
solver=None, parent=None):
self._run_number = run_number
self._last_run = 0
self._samples = {}
for ens in study.river._ensembles.lst:
if ens.function is None:
continue
self._samples[ens] = ens.get_sample(self._run_number)
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._run_number)
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._run_number:
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 = {}
table["Z"] = results.new_table_data("Z", tables["z_mean"])
table["Q"] = results.new_table_data("Q", tables["q_mean"])
table["z_min"] = results.new_table_data("z_min", tables["z_min"])
table["q_min"] = results.new_table_data("q_min", tables["q_min"])
table["z_max"] = results.new_table_data("z_max", tables["z_max"])
table["q_max"] = results.new_table_data("q_max", tables["q_max"])
for key in ["z", "q"]:
for func in ["min", "mean", "max"]:
name = f"res_{key}_{func}"
table[name] = results.new_table_data(
name,
np.array(
list(map(lambda e: e[1], tables[name]))
)
)
for nb, data in tables["traces"]:
name = f"{nb}_"
table[name + "z"] = results.new_table_data(name + "z", data["Z"])
table[name + "q"] = results.new_table_data(name + "q", data["Q"])
table["samples"] = results.new_table_data(
"samples", np.array(self._run_samples)
)
results.set("table", table)
results.set("timestamps", tables["timestamps"])
return results
def next(self):
self._process = self.new_process(self._parent)
self._log(f" *** Run #{self._last_run}", color="blue")
if self._solver._type in ["mage8"]:
samples = self._solver.export_ensemble_variation(
self._study, self._workdir, qlog=self._output,
samples=self._samples,
)
else:
samples = self.ensemble_variation()
self._solver.export(
self._study, self._workdir, qlog=self._output
)
# Keep samples generators results
self._run_samples.append(samples)
self._solver.run(
self._study,
process=self._process,
output_queue=self._output
)
self.set_running_button_status()
self._last_run += 1
self._progress_bar.setValue(self._last_run - 1)
def ensemble_variation(self):
samples = []
for ens in self._samples:
if ens.target_data is None:
continue
x = next(self._samples[ens])
samples.append(x)
self._log(f"*** Variation #{self._last_run} " +
f"'{ens.target_data}' - {ens.data_type} x {x}",
color="blue")
if ens.data_type == "strickler_minor":
ens.target_data.minor = x
elif ens.data_type == "strickler_medium":
ens.target_data.medium = x
return samples
def run(self):
self._log(f" *** Run solver {self._solver.name}", color="blue")
self.next()
def start(self):
if self._solver.is_stoped():
self._log(f" *** Export study {self._solver.name}", color="blue")
ok = self._solver.export(
self._study, self._workdir, qlog=self._output
)
if not ok:
self._log(f" *** Failed to export", color="red")
self.update()
return
else:
self.update()
self._process = self.new_process(self._parent)
self._log(" *** Start", color="blue")
self._results = None
self.next()
def set_running_button_status(self):
self.find(QAction, "action_start").setEnabled(False)
if _signal:
self.find(QAction, "action_pause").setEnabled(True)
else:
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(True)
self.find(QAction, "action_log_file").setEnabled(False)
self.find(QAction, "action_results").setEnabled(False)
def stop(self):
self._log(" *** Stop", color="blue")
self._solver.kill()
self.find(QAction, "action_start").setEnabled(True)
self.find(QAction, "action_pause").setEnabled(False)
self.find(QAction, "action_stop").setEnabled(False)
self.find(QAction, "action_results").setEnabled(True)
if self._solver.log_file() != "":
self.find(QAction, "action_log_file").setEnabled(True)
def results(self):
if self._results is None:
return
self._parent.set_results(self._solver, self._results)
self._parent.open_solver_results(self._solver, self._results)
self._solver.has_results_loaded()
+20
View File
@@ -59,11 +59,16 @@ class ScenarioMenu(AbstractMenu):
item = self._items[0]
scenarios = item.graph._study.scenarios
if len(scenarios) == 1:
return
current_scenario = item.graph._study.status.scenario.id
select = self._menu.addAction(self._trad["menu_select_scenario"])
duplicate = None
delete = None
ensemble = None
edit_ens = None
if item.scenario.id != 0:
if scenarios.is_leaf(item.scenario):
@@ -74,6 +79,17 @@ class ScenarioMenu(AbstractMenu):
self._trad["menu_dup_scenario"]
)
if not item.scenario.is_ensemble():
if item.scenario.id == current_scenario:
ensemble = self._menu.addAction(
self._trad["menu_ens_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
@@ -83,3 +99,7 @@ class ScenarioMenu(AbstractMenu):
self._parent.delete_scenario(item)
elif action == duplicate:
self._parent.duplicate_scenario(item)
elif action == ensemble:
self._parent.create_ensemble_scenario(item)
elif action == edit_ens:
self._parent.edit_ensemble_scenario(item)
+29
View File
@@ -100,6 +100,10 @@ class ScenarioItem(QGraphicsTextItem):
)
)
painter.setBrush(QBrush(color))
if self.scenario.is_ensemble():
painter.drawRect(self.boundingRect())
else:
painter.drawEllipse(self.boundingRect())
super(ScenarioItem, self).paint(painter, option, widget)
@@ -526,6 +530,31 @@ 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 edit_ensemble_scenario(self, item):
self.parent\
.parent\
.open_ensemble_scenario()
def _close_other_window(self):
self.parent\
.parent\
+19
View File
@@ -98,6 +98,25 @@ class DuplicateScenariosCommand(QUndoCommand):
self._study.reload_from_scenario(self._new)
class CreateEnsembleScenarioCommand(QUndoCommand):
def __init__(self, study):
QUndoCommand.__init__(self)
self._study = study
self._new = None
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()
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,6 +43,12 @@ class ScenariosTranslate(MainTranslate):
self._dict["menu_dup_scenario"] = _translate(
"Scenarios", "Duplicate this scenario"
)
self._dict["menu_ens_scenario"] = _translate(
"Scenarios", "Create ensemble scenario from this scenario"
)
self._dict["menu_edit_ens_scenario"] = _translate(
"Scenarios", "Edit this ensemble scenario"
)
self._sub_dict["table_headers_scenarios"] = {
# "id": self._dict['id'],
+5
View File
@@ -42,6 +42,11 @@ 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"
colors = list(mplcolors.TABLEAU_COLORS)
linestyle = ['solid', 'dashed', 'dashdot', 'dotted']
+7
View File
@@ -18,6 +18,13 @@
<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">
+84
View File
@@ -0,0 +1,84 @@
<?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>79</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Run number</string>
</property>
</widget>
</item>
<item row="0" column="1">
<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>
<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>
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_del"/>
<addaction name="action_edit"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add ensemble</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset>
<normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete selected ensembles</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit ensembles range</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
+106
View File
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>416</width>
<height>81</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Min:</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_min">
<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_2">
<property name="text">
<string>Max:</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox_max">
<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>
+245
View File
@@ -0,0 +1,245 @@
<?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_add"/>
<addaction name="action_export"/>
<addaction name="action_import_data"/>
</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 customized visualization</string>
</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 data as CSV</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
</action>
<action name="action_import_data">
<property name="icon">
<iconset>
<normaloff>ressources/import.png</normaloff>ressources/import.png</iconset>
</property>
<property name="text">
<string>Import data</string>
</property>
<property name="toolTip">
<string>Import data from SCV file</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1152</width>
<height>648</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTextEdit" name="textEdit">
<property name="font">
<font>
<family>Monospace</family>
<pointsize>10</pointsize>
</font>
</property>
<property name="locale">
<locale language="English" country="Europe"/>
</property>
<property name="documentTitle">
<string notr="true"/>
</property>
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1152</width>
<height>24</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_start"/>
<addaction name="action_pause"/>
<addaction name="action_stop"/>
<addaction name="action_log_file"/>
<addaction name="action_results"/>
</widget>
<action name="action_stop">
<property name="icon">
<iconset>
<normaloff>ressources/close.png</normaloff>ressources/close.png</iconset>
</property>
<property name="text">
<string>Stop</string>
</property>
</action>
<action name="action_start">
<property name="icon">
<iconset>
<normaloff>ressources/run.png</normaloff>ressources/run.png</iconset>
</property>
<property name="text">
<string>Start</string>
</property>
</action>
<action name="action_pause">
<property name="icon">
<iconset>
<normaloff>ressources/player_pause.png</normaloff>ressources/player_pause.png</iconset>
</property>
<property name="text">
<string>Pause</string>
</property>
</action>
<action name="action_log_file">
<property name="icon">
<iconset>
<normaloff>ressources/zoom.png</normaloff>ressources/zoom.png</iconset>
</property>
<property name="text">
<string>LogFile</string>
</property>
</action>
<action name="action_results">
<property name="text">
<string>results</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>