Compare commits

...
36 Commits
Author SHA1 Message Date
Dylan Jeannin 9e5153d63d Merge branch 'master' into merge_rubarbe 2026-10-27 10:22:35 +01:00
Dylan Jeannin 797de09683 InitialConditions: sort rows after each generation (single undo step), and fix undo of generation and sort (avoid STOP7 error on Hogneau example) 2026-10-26 16:55:01 +01:00
Dylan Jeannin ef10588880 BoundaryCondition: refit the edit plot to the data after each change 2026-10-26 16:06:55 +01:00
Dylan Jeannin c2a8e17bf3 HydraulicStructures: always show the reach plot, highlight the selected structure in blue, full zoom toolbars and fitted views 2026-10-26 15:52:46 +01:00
Dylan Jeannin 03979a8bea Frictions: display reach name in addition to the rk in combobox 2026-10-26 11:50:22 +01:00
Dylan Jeannin a4eee962f7 Geometry: fix error on the first right click in the profile plot (water limits not yet cached) 2026-10-26 10:57:29 +01:00
Dylan Jeannin d8d1b4cd1f HydraulicStructures: fix crash when selecting a rk before the reach 2026-10-23 13:37:03 +02:00
Dylan Jeannin cdeca8a2e8 BoundaryCondition: restore back/forward and view buttons in the edit plot toolbar 2026-10-23 13:23:34 +02:00
Dylan Jeannin 4d9c4cb5d0 RubarBE: add checkers running the export checks, with its errors and warnings (boundary conditions, lateral contributions, structures, saves...) 2026-10-22 17:53:29 +02:00
Dylan Jeannin 618477b384 Rubar3: remove it from the interface, RubarBE takes the parameters of Rubar3 studies when they are opened 2026-10-22 12:48:53 +02:00
Dylan Jeannin 70a26d0036 Mage: warn with a non-blocking checker about solid lateral contributions, and rerun lateral contribution checkers when they are modified 2026-10-21 17:04:30 +02:00
Dylan Jeannin c3afc4dd58 LateralContribution: hide the suspension tab, used by no solver (contributions kept in the model), and stop writing them + solid in the Mage LAT file 2026-10-21 15:41:07 +02:00
Dylan Jeannin ef72e41b14 RubarBE: export solid lateral contributions with their D50 and sigma, attached to the liquid contribution of the same zone (hydevs), and no longer read suspension ones 2026-10-21 14:56:44 +02:00
Dylan Jeannin be7b5b892a LateralContribution: add D50 and sigma to solid lateral contributions (database 0.2.11) 2026-10-21 14:23:47 +02:00
Dylan Jeannin 5f64362f65 RubarBE: export weirs and rectangular and circular orifices (ouvrag), and ignore other structure types with a warning 2026-10-20 17:41:32 +02:00
Dylan Jeannin 357b39758c RubarBE: export liquid lateral contributions (hydevs), and warn when lateral overflows parameter is disabled 2026-10-20 16:23:31 +02:00
Dylan Jeannin a2f215984b RubarBE: export an empty upstream solid boundary condition as a supply at equilibrium (negative Qs in hydros), like Mage 'default' 2026-10-20 15:14:15 +02:00
Dylan Jeannin b1cf45497b RubarBE: ignore empty boundary conditions with a warning instead of cancelling the export 2026-10-20 11:08:38 +02:00
Dylan Jeannin ee416e1059 RubarBE: show bed evolution in the results window (cross section and longitudinal profile), from the bed level in profil and the sections of geomac-N 2026-10-19 17:43:18 +02:00
Dylan Jeannin 3fee3b0bb3 RubarBE: export save abscissas (abshyd) and times (tnprof) from two solver parameters, and read results with tnprof save 2026-10-19 16:55:28 +02:00
Dylan Jeannin a06c2eaa7e RubarBE: export initial solid conditions (condins) from the active bed top layers, with no initial solid discharge 2026-10-19 13:45:36 +02:00
Dylan Jeannin 4e767d86d6 RubarBE: export the upstream solid boundary condition (hydros), and refuse unused, missing or invalid solid conditions 2026-10-19 11:45:20 +02:00
Dylan Jeannin f43d108871 Geometry: fix Tabulation type annotations (np.ndarray), which broke pdoc in the doc CI job 2026-10-16 17:10:20 +02:00
Dylan Jeannin 33a2107876 RubarBE: export sediment layers and bed limits in geomac-i, and refuse invalid layers or markers 2026-10-16 17:00:41 +02:00
Dylan Jeannin 7103fdcb46 RubarBE: refuse and disable upstream water level settings (REGIME=1, CONDAM=2) 2026-10-16 10:44:25 +02:00
Dylan Jeannin ace85ccb40 RubarBE: refuse upstream water level settings (REGIME=1, CONDAM=2) 2026-10-16 10:31:09 +02:00
Dylan Jeannin ec81a998cb RubarBE: fix hydraulic export (mail, condin, frot, boundary conditions), and prevent user of mistakes instead of run incorrect calculations 2026-10-16 09:55:40 +02:00
Dylan Jeannin a18accb404 RubarBE: seal study translated in pamhyr db 2026-10-15 17:26:35 +02:00
Dylan Jeannin 657f54ff91 RubarBE: split critical Shields stress (TCADIM) from water viscosity (VISC) 2026-10-15 03:40:52 +02:00
Dylan Jeannin bf2d3ae7a9 Merge branch 'dev_dylan' into merge_rubarbe 2026-10-14 19:35:37 +02:00
Dylan Jeannin 656b337499 RubarBE: notify when attempting to use RubarBE with solid boundary conditions missing 2026-10-14 19:14:51 +02:00
Dylan Jeannin ed2ed0ac2a RubarBE: Integration of the solver in the list 2026-10-11 20:33:25 +02:00
Dylan Jeannin ab95f06700 Rubar parameters: prohibit simultaneous selection of REGIME=1 and CONDAM=0 2026-10-08 10:12:26 +02:00
Dylan Jeannin 2a699f46ca Solver: revamp Rubar parameter interface and export validation 2026-10-07 17:11:27 +02:00
Dylan Jeannin 33ad91d1a6 Plot widget: Fix plot selection crash on mouse wheel events 2026-10-06 11:15:14 +02:00
Dylan Jeannin 7d2ef35128 Results Rubar3: fix opening results window from the solver log window 2026-10-05 16:19:41 +02:00
34 changed files with 3191 additions and 470 deletions

No files matched your search

+1
View File
@@ -44,6 +44,7 @@ __old__
mage8/
adists/
rubarbe/
rubar/
linux/
windows/
+43
View File
@@ -254,3 +254,46 @@ for each reach"
return False
return ok
class MageLateralSolidChecker(AbstractModelChecker):
"""Mage has no solid lateral contribution (notice: only upstream
solid discharges, QSO): the solid ones are not exported (LAT holds
liquid discharges only). Non blocking warning"""
def __init__(self, version="mage8"):
super(MageLateralSolidChecker, self).__init__()
self._name = _translate(
"Checker", "Mage solid lateral contributions checker")
self._description = _translate(
"Checker",
"Check if the study has solid lateral contributions, which "
"Mage does not take into account"
)
self._solver = version
self._modules = Modules.LATERAL_CONTRIBUTION
def run(self, study):
if not self.basic_check(study):
return False
edges = study.river.enable_edges()
names = [
lc.name
for lc in study.river.lateral_contribution.get_tab("solid")
if not lc.is_deleted() and lc.reach in edges and len(lc) > 0
]
if len(names) == 0:
self._status = STATUS.OK
self._summary = "ok"
return True
self._status = STATUS.WARNING
self._summary = _translate(
"Checker",
"Solid lateral contributions not taken into account by Mage: "
"{names}"
).format(names=", ".join(names))
return False
+255
View File
@@ -0,0 +1,255 @@
# RubarBE.py -- Pamhyr RubarBE checkers
# 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 -*-
"""RubarBE checkers. Each one runs a step of the checks of the RubarBE
export (Solver/RubarBE.py), without writing any file, so that they
always give the messages of the export: an error where the export would
be cancelled, a warning for the data it ignores (empty boundary
conditions, lateral contributions not read, solid lateral contributions
without liquid one, structures without RubarBE equivalent...)."""
import logging
from PyQt5.QtCore import QCoreApplication
from Modules import Modules
from tools import logger_exception
from Checker.Checker import AbstractModelChecker, STATUS
_translate = QCoreApplication.translate
logger = logging.getLogger()
class RubarBEChecker(AbstractModelChecker):
"""Base class: _check(solver, study, context, warnings) runs the
export step, raising RubarExportError (error) or appending to
warnings (warning). Steps that need the reach are not run when the
study has not a single valid reach: RubarBEReachChecker reports it"""
_needs_reach = True
def __init__(self, name, description, modules, version="rubarbe"):
super(RubarBEChecker, self).__init__()
self._name = name
self._description = description
self._solver = version
self._modules = modules
@staticmethod
def _context(solver, study):
# Read the parameters without creating them (get_params would
# add default ones to the study)
river = study.river
if solver.type in river._parameters:
values = solver._parameter_values(study)
else:
values = {k: str(v).strip()
for k, v in solver.default_parameters()}
return {
"values": values,
"trased": values.get("rubarbe_trased", "n").lower()
in ("y", "o"),
}
def _check(self, solver, study, context, warnings):
raise NotImplementedError
def run(self, study):
from Solver.RubarBE import RubarBE, RubarExportError
if not self.basic_check(study):
return False
solver = RubarBE("checker")
warnings = []
try:
context = self._context(solver, study)
if self._needs_reach:
try:
edge, profiles = solver._single_reach(study.river)
except RubarExportError:
self._status = STATUS.OK
self._summary = "ok"
return True
context["hydraulics"] = {
"edge": edge,
"profiles": profiles,
"tinit": solver._seconds(
context["values"].get("rubarbe_tinit", "0")),
"warnings": warnings,
}
self._check(solver, study, context, warnings)
except (RubarExportError, ValueError) as error:
self._status = STATUS.ERROR
self._summary = str(error)
return False
except Exception as error:
# Never stop the checker thread
logger_exception(error)
self._status = STATUS.UNKNOWN
self._summary = str(error)
return False
if warnings:
self._status = STATUS.WARNING
self._summary = " | ".join(warnings)
return False
self._status = STATUS.OK
self._summary = "ok"
return True
class RubarBEReachChecker(RubarBEChecker):
_needs_reach = False
def __init__(self, version="rubarbe"):
super(RubarBEReachChecker, self).__init__(
_translate("Checker", "RubarBE reach checker"),
_translate(
"Checker",
"Check that the study has a single enabled reach, with at "
"least two profiles of increasing rk"
),
Modules.GEOMETRY, version,
)
def _check(self, solver, study, context, warnings):
solver._single_reach(study.river)
class RubarBEGeometryChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEGeometryChecker, self).__init__(
_translate("Checker", "RubarBE geometry and sediment checker"),
_translate(
"Checker",
"Check the main channel and active bed limits of the "
"profiles and, with sediment transport, their sediment "
"layers"
),
# Rerun on changes of the geometry, which impact the sediment
# layers
Modules.SEDIMENT_LAYER, version,
)
def _check(self, solver, study, context, warnings):
solver._geometry_data(context["hydraulics"]["edge"],
context["trased"])
class RubarBEBoundaryChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEBoundaryChecker, self).__init__(
_translate("Checker", "RubarBE boundary conditions checker"),
_translate(
"Checker",
"Check the liquid and solid boundary conditions used by "
"the boundary settings of the solver parameters"
),
Modules.BOUNDARY_CONDITION, version,
)
def _check(self, solver, study, context, warnings):
values = context["values"]
edge = context["hydraulics"]["edge"]
solver._check_regime(values)
solver._check_boundaries(study.river, edge, values, warnings)
solver._solid_boundary(study.river, edge, values,
context["trased"], warnings)
class RubarBEFrictionChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEFrictionChecker, self).__init__(
_translate("Checker", "RubarBE friction checker"),
_translate("Checker", "Check that the reach has frictions"),
Modules.FRICTION, version,
)
def _check(self, solver, study, context, warnings):
solver._interpolated_frictions(context["hydraulics"]["edge"])
class RubarBEInitialConditionsChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEInitialConditionsChecker, self).__init__(
_translate("Checker", "RubarBE initial conditions checker"),
_translate(
"Checker", "Check the initial conditions of the reach"),
Modules.INITIAL_CONDITION, version,
)
def _check(self, solver, study, context, warnings):
solver._initial_conditions(study.river,
context["hydraulics"]["edge"])
class RubarBELateralChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBELateralChecker, self).__init__(
_translate("Checker", "RubarBE lateral contributions checker"),
_translate(
"Checker",
"Check the lateral contributions: read only with lateral "
"overflows, solid ones attached to the liquid contribution "
"of the same sections"
),
Modules.LATERAL_CONTRIBUTION, version,
)
def _check(self, solver, study, context, warnings):
solver._lateral_data(study.river, context["hydraulics"],
context["values"], context["trased"], warnings)
class RubarBEStructureChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBEStructureChecker, self).__init__(
_translate("Checker", "RubarBE hydraulic structures checker"),
_translate(
"Checker",
"Check the hydraulic structures: weirs and rectangular and "
"circular orifices only"
),
Modules.HYDRAULIC_STRUCTURES, version,
)
def _check(self, solver, study, context, warnings):
solver._structure_data(study.river, context["hydraulics"], warnings)
class RubarBESavesChecker(RubarBEChecker):
def __init__(self, version="rubarbe"):
super(RubarBESavesChecker, self).__init__(
_translate("Checker", "RubarBE saves checker"),
_translate(
"Checker",
"Check the save abscissas (ABSHYD) and times (TNPROF) of "
"the solver parameters"
),
# The solver parameters have no module: rerun on any change
Modules.NONE, version,
)
def _check(self, solver, study, context, warnings):
solver._save_data(context["values"], context["hydraulics"])
+9 -4
View File
@@ -17,6 +17,7 @@
# -*- coding: utf-8 -*-
import queue
import types
import random
import logging
import numpy as np
@@ -39,9 +40,9 @@ logger = logging.getLogger()
@dataclass
class Tabulation:
z: np.array([])
A: np.array([])
L: np.array([])
z: np.ndarray
A: np.ndarray
L: np.ndarray
class ProfileXYZ(Profile, SQLSubModel):
@@ -842,7 +843,11 @@ class ProfileXYZ(Profile, SQLSubModel):
return poly
def wet_points(self, z):
left, right = self.get_water_limits(z)
limits = self.get_water_limits(z)
if isinstance(limits, types.GeneratorType):
limits = next(limits)
left, right = limits
points = list(filter(lambda p: p.z < z, self._points))
points = [left] + points + [right]
@@ -224,6 +224,9 @@ class LateralContribution(SQLSubModel):
self._header = []
self._types = [float, float]
self._d50 = 0.002
self._sigma = 1.0
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
@@ -236,6 +239,8 @@ class LateralContribution(SQLSubModel):
reach INTEGER,
begin_section INTEGER,
end_section INTEGER,
d50 REAL DEFAULT 0.002,
sigma REAL DEFAULT 1,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id),
@@ -286,8 +291,32 @@ class LateralContribution(SQLSubModel):
"ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE"
)
if major == "0" and (int(minor) < 2 or
(minor == "2" and int(release) < 11)):
cls._db_update_to_0_2_11(execute)
return cls._update_submodel(execute, version, data)
@classmethod
def _db_update_to_0_2_11(cls, execute):
# The table may already have been recreated with these columns
# by _db_update_to_0_2_0
columns = [
row[0] for row in execute(
"SELECT name FROM pragma_table_info('lateral_contribution')"
)
]
if "d50" not in columns:
execute(
"ALTER TABLE lateral_contribution " +
"ADD COLUMN d50 REAL DEFAULT 0.002"
)
if "sigma" not in columns:
execute(
"ALTER TABLE lateral_contribution " +
"ADD COLUMN sigma REAL DEFAULT 1"
)
@classmethod
def _db_update_to_0_2_0(cls, execute, data):
table = "lateral_contribution"
@@ -339,7 +368,7 @@ class LateralContribution(SQLSubModel):
table = execute(
"SELECT pamhyr_id, deleted, name, type, " +
"reach, begin_section, end_section, scenario " +
"reach, begin_section, end_section, d50, sigma, scenario " +
"FROM lateral_contribution " +
f"WHERE tab = '{tab}'" +
f"AND scenario = {scenario.id} " +
@@ -356,6 +385,8 @@ class LateralContribution(SQLSubModel):
reach = next(it)
b_section = next(it)
e_section = next(it)
d50 = next(it)
sigma = next(it)
owner_scenario = next(it)
ctor = cls._get_ctor_from_type(t)
@@ -367,6 +398,10 @@ class LateralContribution(SQLSubModel):
lc.reach = None
lc._begin_section = None
lc._end_section = None
if d50 is not None:
lc._d50 = float(d50)
if sigma is not None:
lc._sigma = float(sigma)
if row[3] != -1:
lc.reach = next(
@@ -426,12 +461,13 @@ class LateralContribution(SQLSubModel):
"INSERT INTO " +
"lateral_contribution(" +
"pamhyr_id, deleted, name, type, tab, " +
"reach, begin_section, end_section, scenario) " +
"reach, begin_section, end_section, d50, sigma, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', " +
f"'{self._db_format(self._type)}', '{tab}', {reach}, " +
f"{begin_section}, {end_section}, " +
f"{self._d50}, {self._sigma}, " +
f"{self._status.scenario_id}" +
")"
)
@@ -523,10 +559,30 @@ class LateralContribution(SQLSubModel):
new._reach = reach
new._begin_section = begin_section
new._end_section = end_section
new._d50 = self._d50
new._sigma = self._sigma
new._data = [data.cloned() for data in self.data]
new.modified()
return new
@property
def d50(self):
return self._d50
@d50.setter
def d50(self, value):
self._d50 = float(value)
self.modified()
@property
def sigma(self):
return self._sigma
@sigma.setter
def sigma(self, value):
self._sigma = float(value)
self.modified()
@property
def begin_rk(self):
if self._begin_section is None:
+4 -1
View File
@@ -104,7 +104,10 @@ class Profile(SQLSubModel):
return any(map(lambda ts: "sl" in self._data[ts], self._data))
def has_bedload(self):
return any(map(lambda ts: "sl" in self._data[ts], self._data))
# Sediment layers (Mage) or bed level of the section (RubarBE)
return any(map(lambda ts: ("sl" in self._data[ts] or
"zfd" in self._data[ts]),
self._data))
@classmethod
def _db_create(cls, execute, ext=""):
@@ -19,6 +19,7 @@
from copy import copy
from tools import trace, timer
from Solver.Solvers import solver_type_list
from Solver.RubarBE import Rubar3
from Model.Scenario import Scenario
from Model.Tools.PamhyrDB import SQLSubModel
@@ -296,14 +297,20 @@ class SolverParametersList(PamhyrModelList):
scenario = data["scenario"]
loaded = data['loaded_pid']
solvers = execute("SELECT DISTINCT solver FROM solver_parameter")
solvers = [
row[0] for row in
execute("SELECT DISTINCT solver FROM solver_parameter")
]
new = {}
if scenario is None:
return new
for solver in solvers:
solver = solver[0]
if solver == "rubar3" and "rubarbe" not in solvers:
new["rubarbe"] = cls._db_load_rubar3(execute, data)
continue
if solver not in solver_type_list:
continue
@@ -315,10 +322,44 @@ class SolverParametersList(PamhyrModelList):
n._lst = list(map(lambda t: t[1], sorted(lst)))
# Parameters added to the solver after the study was saved
names = {p.name for p in n._lst}
n._lst += [
Parameter(name=name, value=value, status=status)
for name, value in st.default_parameters()
if name not in names
]
new[solver] = n
return new
@classmethod
def _db_load_rubar3(cls, execute, data):
"""Rubar3 is no longer offered: RubarBE, its generic version,
takes the parameters of a Rubar3 study (same rubarbe_* keys,
RubarBE reads them the same way). Missing parameters take the
Rubar3 defaults (no sediment transport), so that the
computation is unchanged. New parameters (new ids): saved as
RubarBE ones with the study, the Rubar3 rows are then ignored"""
status = data["status"]
n = cls(solver_type=solver_type_list["rubarbe"], status=status)
data['solver'] = "rubar3"
lst = Parameter._db_load(execute, data)
n._lst = [
Parameter(name=p.name, value=p.value, status=status)
for _, p in sorted(lst, key=lambda t: t[0])
]
names = {p.name for p in n._lst}
n._lst += [
Parameter(name=name, value=value, status=status)
for name, value in Rubar3.default_parameters()
if name not in names
]
return n
def _db_save(self, execute, data=None):
t = self._solver._type
execute(
+1 -1
View File
@@ -48,7 +48,7 @@ logger = logging.getLogger()
class Study(SQLModel):
_version = "0.2.10"
_version = "0.2.11"
_sub_classes = [
Scenario,
+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(
+3 -1
View File
@@ -28,6 +28,7 @@ from tools import timer, trace, logger_exception
from Solver.CommandLine import CommandLineSolver
from Checker.Mage import (
MageNetworkGraphChecker, MageGeometryGuideLineChecker,
MageLateralSolidChecker,
)
from Model.Results.Results import Results
@@ -106,6 +107,7 @@ class Mage(CommandLineSolver):
MageNetworkGraphChecker(connectivity=True, version=cls._type),
MageNetworkGraphChecker(connectivity=False, version=cls._type),
MageGeometryGuideLineChecker(version=cls._type),
MageLateralSolidChecker(version=cls._type),
]
return lst
@@ -391,7 +393,7 @@ class Mage(CommandLineSolver):
qlog.put(f"Export LAT file")
files.append(f"{name}.LAT")
for tab in ["liquid", "solid", "suspenssion"]:
for tab in ["liquid"]:
for lateral in lst.get_tab(tab):
self._export_LC(study, lateral, f, qlog)
+1388 -389
View File
File diff suppressed because it is too large. Load diff
+2 -4
View File
@@ -35,8 +35,7 @@ solver_long_name = {
# "mage_fake7": "Mage fake v7",
"adistswc": "Adis-TS_WC",
"adisttwc": "Adis-TT_WC",
# "rubarbe": "RubarBE",
"rubar3": "Rubar3",
"rubarbe": "RubarBE",
}
solver_type_list = {
@@ -46,6 +45,5 @@ solver_type_list = {
# "mage_fake7": MageFake7,
"adistswc": AdisTSwc,
"adisttwc": AdisTTwc,
# "rubarbe": RubarBE,
"rubar3": Rubar3,
"rubarbe": RubarBE,
}
+4 -1
View File
@@ -188,7 +188,10 @@ class EditBoundaryConditionWindow(PamhyrWindow):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self
self.canvas, self, items=[
"home", "move", "zoom", "save",
"iso", "back/forward"
]
)
self.verticalLayout.addWidget(self.toolbar)
self.verticalLayout.addWidget(self.canvas)
+11 -1
View File
@@ -105,8 +105,18 @@ class ComboBoxDelegate(QItemDelegate):
strickler
)
elif self._mode == "rk":
if self._data is None:
self.editor.addItems(
list(map(str, self._data.reach.get_rk()))
["-"]
)
else:
self.editor.addItems(
list(
map(
lambda p: p.display_name(),
self._data.reach.profiles
)
)
)
self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
+1 -1
View File
@@ -42,7 +42,7 @@ class PlotAC(PamhyrPlot):
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
self._autoscale_update = True
@property
def river(self):
+36 -9
View File
@@ -31,6 +31,14 @@ _translate = QCoreApplication.translate
class PlotRKC(PamhyrPlot):
# Class attributes: PlotRKZ (frictions) does not call this __init__
_selected = None # Selected hydraulic structure, in blue
profile = None
line_rk_zmin = None
line_rk_zmin_zmax = None
hs_vlines = None
_hs_drawn = [] # Structures of hs_vlines, in order
def __init__(self, canvas=None, trad=None, toolbar=None,
river=None, reach=None, profile=None,
parent=None):
@@ -134,19 +142,14 @@ class PlotRKC(PamhyrPlot):
vymin = []
vymax = []
self.anotate_lst = []
self._hs_drawn = []
hs_color = []
index = self.parent.tableView.selectedIndexes()
for i, hs in enumerate(lhs):
for hs in lhs:
section = hs.input_section
if section is None:
continue
if i == index[0].row():
color = "blue"
elif hs.enabled:
color = "red"
else:
color = "darkgrey"
color = self._hs_color(hs)
x = section.rk
if x is not None:
@@ -164,6 +167,7 @@ class PlotRKC(PamhyrPlot):
vymin.append(min(z_min))
vymax.append(max(z_max))
hs_color.append(color)
self._hs_drawn.append(hs)
self.hs_vlines = self.canvas.axes.vlines(
x=vx, ymin=vymin, ymax=vymax,
@@ -172,6 +176,28 @@ class PlotRKC(PamhyrPlot):
color=hs_color,
)
def _hs_color(self, hs):
if hs is self._selected:
return "blue"
if hs.enabled:
return "red"
return "darkgrey"
def set_selected(self, hs):
"""Draw the selected structure in blue, without redrawing the
plot (the view is kept)"""
self._selected = hs
if self.hs_vlines is None:
return
colors = [self._hs_color(h) for h in self._hs_drawn]
if colors:
self.hs_vlines.set_color(colors)
for annotation, color in zip(self.anotate_lst, colors):
annotation.set_color(color)
self.canvas.figure.canvas.draw_idle()
def color_highlight(self, highlight):
reach = self._current_reach
@@ -255,7 +281,8 @@ class PlotRKC(PamhyrPlot):
closest = self._closest_profile(event)
index = self.parent.tableView.selectedIndexes()
if self.parent._table is not None:
# Moves the selected structure: nothing to do without selection
if self.parent._table is not None and len(index) > 2:
self.parent._table.setData(index[2], closest)
return
+15 -6
View File
@@ -130,11 +130,13 @@ class HydraulicStructuresWindow(PamhyrWindow):
self._checkbox = self.find(QCheckBox, f"checkBox")
self._set_checkbox_state()
_toolbar_items = ["home", "move", "zoom", "save", "iso", "back/forward"]
def setup_plots(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self
self.canvas, self, items=self._toolbar_items
)
self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
self.plot_layout.addWidget(self.toolbar)
@@ -143,7 +145,7 @@ class HydraulicStructuresWindow(PamhyrWindow):
self.plot_rkc = PlotRKC(
canvas=self.canvas,
river=self._study.river,
reach=None,
reach=self._displayed_reach(None),
profile=None,
trad=self._trad,
toolbar=self.toolbar,
@@ -154,7 +156,7 @@ class HydraulicStructuresWindow(PamhyrWindow):
self.canvas_2 = MplCanvas(width=5, height=4, dpi=100)
self.canvas_2.setObjectName("canvas_2")
self.toolbar_2 = PamhyrPlotToolbar(
self.canvas_2, self
self.canvas_2, self, items=self._toolbar_items
)
self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2")
self.plot_layout_2.addWidget(self.toolbar_2)
@@ -183,7 +185,7 @@ class HydraulicStructuresWindow(PamhyrWindow):
table = self.find(QTableView, "tableView")
table.selectionModel()\
.selectionChanged\
.connect(self.update)
.connect(self.update_selection)
self._table.dataChanged.connect(self.update)
self._table.layoutChanged.connect(self.update)
@@ -283,10 +285,17 @@ class HydraulicStructuresWindow(PamhyrWindow):
)
def update(self):
"""Data changed: plots redrawn, fitted to their data"""
self._set_checkbox_state()
self._update_clear_plot()
self._update_plots(redraw=True)
def _update_clear_plot(self):
def update_selection(self):
"""Selection changed: the selected structure is drawn in blue,
the view is kept (unless its reach is another one)"""
self._set_checkbox_state()
self._update_plots(redraw=False)
def _selected_structure(self):
rows = self.index_selected_rows()
if (len(rows) == 0 or
+10
View File
@@ -384,9 +384,19 @@ class InitialConditionTableModel(PamhyrTableModel):
self.layoutChanged.emit()
def generate(self, generator, param, option):
self.layoutAboutToBeChanged.emit()
self._undo.beginMacro(f"generate {generator}")
self._undo.push(
GenerateCommand(
self._lst, generator, param, option
)
)
self._undo.push(
SortCommand(
self._lst, False
)
)
self._undo.endMacro()
self.layoutChanged.emit()
+17 -5
View File
@@ -100,15 +100,19 @@ class SortCommand(QUndoCommand):
self._indexes = None
def undo(self):
ll = self._ics.data
order = {
id(x): self._indexes[i]
for i, x in enumerate(self._ics.data)
}
self._ics.sort(
key=lambda x: self._indexes[ll.index(x)]
key=lambda x: order.get(id(x), len(order))
)
def redo(self):
self._ics.sort(
reverse=self._reverse,
key=lambda x: x["rk"].rk
key=lambda x: ((x["rk"] is None),
0.0 if x["rk"] is None else x["rk"].rk)
)
if self._indexes is None:
self._indexes = list(
@@ -182,11 +186,19 @@ class GenerateCommand(QUndoCommand):
self._ics = ics
self._param = param
self._option = option
self._copy = self._ics.data
# All the rows, with their deleted flag: the generation marks the
# previous rows as deleted (clear_data)
self._copy = list(self._ics._data)
self._deleted = [d.is_deleted() for d in self._copy]
self._generator = generator
def undo(self):
self._ics.data = self._copy
for data, deleted in zip(self._copy, self._deleted):
if deleted:
data.set_as_deleted()
else:
data.set_as_not_deleted()
self._ics.data = list(self._copy)
def redo(self):
if self._generator == "growing":
@@ -44,6 +44,22 @@ class SetDataCommand(QUndoCommand):
self._data._set_i_c_v(self._index, self._column, self._new)
class SetMetaDataCommand(QUndoCommand):
def __init__(self, data, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._column = column
self._old = getattr(self._data, self._column)
self._new = float(new_value)
def undo(self):
setattr(self._data, self._column, self._old)
def redo(self):
setattr(self._data, self._column, self._new)
class AddCommand(QUndoCommand):
def __init__(self, data, index):
QUndoCommand.__init__(self)
+50 -2
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QHeaderView,
QHeaderView, QVBoxLayout,
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
@@ -44,6 +44,8 @@ from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.LateralContribution.Edit.translate import LCETranslate
from View.LateralContribution.Edit.Table import TableModel
from View.LateralContribution.Edit.Plot import Plot
from View.LateralContribution.Edit.UndoCommand import SetMetaDataCommand
from View.BoundaryCondition.Edit.Window import WD50Sigma
_translate = QCoreApplication.translate
@@ -54,10 +56,15 @@ class EditLateralContributionWindow(PamhyrWindow):
def __init__(self, data=None,
study=None, config=None,
parent=None):
tab="liquid", parent=None):
self._data = data
# Grain size of the sediment brought: solid contributions only
self._is_solid = tab == "solid"
trad = LCETranslate()
if self._is_solid:
trad.get_dict("table_headers")["discharge"] = \
trad["solid_discharge"]
name = trad[self._pamhyr_name]
if self._data is not None:
edge_name = (self._data.reach.name if self._data.reach is not None
@@ -81,6 +88,7 @@ class EditLateralContributionWindow(PamhyrWindow):
self.setup_table()
self.setup_plot()
self.setup_d50sigma()
self.setup_connections()
def setup_table(self):
@@ -139,7 +147,18 @@ class EditLateralContributionWindow(PamhyrWindow):
)
self.plot.draw()
def setup_d50sigma(self):
if not self._is_solid:
return
layout = self.find(QVBoxLayout, "verticalLayout_table")
self._d50sigma = WD50Sigma(parent=self)
layout.addWidget(self._d50sigma)
self.widget_update()
def setup_connections(self):
self._undo_stack.indexChanged.connect(self._propagate_lateral_update)
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
@@ -148,10 +167,37 @@ class EditLateralContributionWindow(PamhyrWindow):
self._table.dataChanged.connect(self.update)
self._table.layoutChanged.connect(self.update)
if self._is_solid:
if self._study.is_editable():
self._d50sigma.d50Changed.connect(self.d50_changed)
self._d50sigma.sigmaChanged.connect(self.sigma_changed)
else:
self._d50sigma.setEnabled(False)
def _propagate_lateral_update(self, index=None):
# Every change of the lateral contributions goes through the undo
# stack (push, undo, redo): the main window then reruns their
# checkers
self._propagate_update(key=Modules.LATERAL_CONTRIBUTION)
def d50_changed(self, value):
self._undo_stack.push(SetMetaDataCommand(self._data, "d50", value))
def sigma_changed(self, value):
self._undo_stack.push(
SetMetaDataCommand(self._data, "sigma", value)
)
def widget_update(self):
if self._is_solid:
self._d50sigma.set_d50(self._data.d50)
self._d50sigma.set_sigma(self._data.sigma)
def _propagated_update(self, key=Modules(0)):
if Modules.STUDY in key:
self._table.layoutChanged.emit()
self.plot.update()
self.widget_update()
def update(self):
self.plot.update()
@@ -235,7 +281,9 @@ class EditLateralContributionWindow(PamhyrWindow):
def _undo(self):
self._table.undo()
self.plot.update()
self.widget_update()
def _redo(self):
self._table.redo()
self.plot.update()
self.widget_update()
@@ -39,3 +39,7 @@ class LCETranslate(LCTranslate):
"discharge": self._dict["unit_discharge"],
"z": self._dict["unit_elevation"],
}
# Discharge column of a solid contribution
self._dict["solid_discharge"] = _translate(
"LateralContribution", "Solid discharge (kg/s)"
)
+13 -2
View File
@@ -20,6 +20,7 @@ import logging
from tools import trace, timer
from Modules import Modules
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5 import QtWidgets
@@ -67,6 +68,7 @@ logger = logging.getLogger()
class LateralContributionWindow(PamhyrWindow):
_pamhyr_ui = "LateralContributions"
_pamhyr_name = "Lateral contribution"
_tabs = ["liquid", "solid"]
def __init__(self, study=None, config=None, parent=None):
trad = LCTranslate()
@@ -96,7 +98,7 @@ class LateralContributionWindow(PamhyrWindow):
else:
editable_headers = []
for t in ["liquid", "solid", "suspenssion"]:
for t in self._tabs:
self._delegate_type = ComboBoxDelegate(
data=self._study.river,
mode="type",
@@ -158,6 +160,8 @@ class LateralContributionWindow(PamhyrWindow):
)
def setup_connections(self):
self._undo_stack.indexChanged.connect(self._propagate_lateral_update)
if self._study.is_editable():
self.find(QAction, "action_import").triggered.connect(
self.import_from_file)
@@ -168,7 +172,7 @@ class LateralContributionWindow(PamhyrWindow):
self.find(QAction, "action_edit").triggered.connect(self.edit)
self.tabs.currentChanged.connect(self._set_current_reach)
for t in ["liquid", "solid", "suspenssion"]:
for t in self._tabs:
table = self.find(QTableView, f"tableView_{t}")
table.selectionModel()\
.selectionChanged\
@@ -177,6 +181,12 @@ class LateralContributionWindow(PamhyrWindow):
self._table[t].dataChanged\
.connect(self._set_current_reach)
def _propagate_lateral_update(self, index=None):
# Every change of the lateral contributions goes through the undo
# stack (push, undo, redo): the main window then reruns their
# checkers
self._propagate_update(key=Modules.LATERAL_CONTRIBUTION)
def current_tab(self):
return self.find(QTabWidget, "tabWidget")\
.currentWidget()\
@@ -303,6 +313,7 @@ class LateralContributionWindow(PamhyrWindow):
win = EditLateralContributionWindow(
data=data,
study=self._study,
tab=tab,
parent=self
)
win.show()
+3 -3
View File
@@ -30,7 +30,7 @@ from platformdirs import user_cache_dir
from Solver.AdisTS import AdisTS
from Solver.Mage import Mage8
from Solver.RubarBE import Rubar3
from Solver.RubarBE import RubarBE
from tools import logger_exception, pamhyr_db_need_update, timer
from PyQt5 import QtGui
@@ -2082,7 +2082,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
dialog.setDefaultSuffix(".BIN")
# dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden)
dialog.setNameFilters(['Mage (*.BIN)', 'Rubar3 (profil.*)'])
dialog.setNameFilters(['Mage (*.BIN)', 'RubarBE (profil.*)'])
if self._study.filename is not None:
if self._last_solver is None:
@@ -2102,7 +2102,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
if ".BIN" in file_name[0]:
solver = Mage8("Mage8")
else:
solver = Rubar3("Rubar3")
solver = RubarBE("RubarBE")
self.open_solver_results(
solver,
+36
View File
@@ -286,6 +286,10 @@ class PlotAC(PamhyrPlot):
initial_z = np.asarray(profile.geometry.z(), dtype=float)
result = self.results[self._current_res_id]
bed = self._get_bed_from_sections(profile, result, initial_z)
if bed is not None:
return bed
try:
sediment_timestamps = sorted(result.get("sediment_timestamps"))
except (KeyError, TypeError):
@@ -324,6 +328,38 @@ class PlotAC(PamhyrPlot):
return initial_z
def _get_bed_from_sections(self, profile, result, initial_z):
"""Bed of the solvers that give the bed level of each section at
each time ('zfd') and exact sections at some times
('bed_sections', RubarBE): the last exact section before the
current time, or the study geometry, with its moving points
('bed_points') shifted by the change of the bed level since then.
None for the other results"""
try:
sections = result.get("bed_sections")
moving = result.get("bed_points")[profile.global_index]
except KeyError:
return None
current = profile.get_ts_key(self._current_timestamp, "zfd")
if current is None:
return None
index = profile.global_index
saved = [ts for ts in sorted(sections)
if ts <= self._current_timestamp and index in sections[ts]]
if saved:
base_timestamp = saved[-1]
base = np.asarray(sections[base_timestamp][index], dtype=float)
else:
base_timestamp = min(result.get("timestamps"))
base = initial_z
level = profile.get_ts_key(base_timestamp, "zfd")
if level is None or len(base) != len(moving):
return None
return base + np.where(moving, current - level, 0.0)
def _get_wet_mask(self, profile, elevation):
result = self.results[self._current_res_id]
+331
View File
@@ -0,0 +1,331 @@
# 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_tcadim"] = _translate(
"Solver",
"Export cancelled: {key}: the selected transport law and options "
"use a dimensionless critical stress given by the user. "
"Fill it in the parameters (tab Solid Transport)."
)
self._dict["solid_boundary_downstream"] = _translate(
"Solver",
"Export cancelled: solid boundary condition {name} at node "
"{node}: RubarBE only reads a solid boundary condition at the "
"upstream node. Remove it."
)
self._dict["solid_boundary_too_long"] = _translate(
"Solver",
"Export cancelled: solid boundary condition {name} at node "
"{node} has {count} points, RubarBE accepts at most {maximum}."
)
self._dict["save_list_invalid"] = _translate(
"Solver",
"Export cancelled: {key}: '{value}' is not a valid value. "
"Separate the values with spaces or semicolons, use a dot as "
"decimal separator, and give the times in seconds or "
"days:hours:minutes:seconds."
)
self._dict["save_list_too_long"] = _translate(
"Solver",
"Export cancelled: {key} has {count} values, RubarBE accepts "
"at most {maximum}."
)
self._dict["save_list_required"] = _translate(
"Solver",
"Export cancelled: {flag} is enabled but {key} is empty. Fill "
"it in the solver parameters (tab Saves), or disable {flag}."
)
self._dict["abshyd_outside"] = _translate(
"Solver",
"Export cancelled: rubarbe_abshyd: abscissa {rk} is outside "
"reach {reach}, whose sections go from {first} to {last}."
)
self._dict["tnprof_outside"] = _translate(
"Solver",
"rubarbe_tnprof: times {times} s are outside the computation, "
"from {tinit} s to {tmax} s. RubarBE saves the earlier ones at "
"the first time steps and never reaches the later ones."
)
self._dict["solid_boundary_invalid"] = _translate(
"Solver",
"Export cancelled: solid boundary condition {name} at node "
"{node} must have D50 > 0 m and sigma >= 1 (1 for a uniform "
"sediment)."
)
self._dict["multireach_unsupported"] = _translate(
"Solver",
"Export cancelled: the Rubar export supports a single reach, "
"{count} enabled reaches found."
)
self._dict["not_enough_profiles"] = _translate(
"Solver",
"Export cancelled: reach {reach} needs at least two profiles."
)
self._dict["rk_not_increasing"] = _translate(
"Solver",
"Export cancelled: the profiles of reach {reach} must have "
"strictly increasing rk. Sort the profiles of the geometry."
)
self._dict["boundary_code_unsupported"] = _translate(
"Solver",
"Export cancelled: {key}={value} is not supported by the Rubar "
"export (single reach)."
)
self._dict["boundary_missing"] = _translate(
"Solver",
"Export cancelled: {setting} requires a {kind} boundary "
"condition at node {node}."
)
self._dict["boundary_unexpected"] = _translate(
"Solver",
"Export cancelled: boundary condition {name} ({kind}) at node "
"{node} is not used with {setting}. Remove it or change the "
"boundary settings in the solver parameters."
)
self._dict["boundary_ambiguous"] = _translate(
"Solver",
"Export cancelled: several {kind} boundary conditions at node "
"{node}."
)
self._dict["structure_section"] = _translate(
"Solver",
"Export cancelled: the section of hydraulic structure {name} is "
"not a profile of reach {reach}."
)
self._dict["structure_empty_ignored"] = _translate(
"Solver",
"Hydraulic structure {name} has no enabled basic structure "
"that RubarBE takes into account: it is ignored."
)
self._dict["structure_too_many"] = _translate(
"Solver",
"Export cancelled: hydraulic structure {name} has {count} "
"enabled basic structures, RubarBE accepts at most {maximum}."
)
self._dict["structure_too_many_structures"] = _translate(
"Solver",
"Export cancelled: {count} hydraulic structures, RubarBE accepts "
"at most {maximum}."
)
self._dict["structure_same_section"] = _translate(
"Solver",
"Export cancelled: hydraulic structures {first} and {second} are "
"on the same section (rk {rk}). Put their basic structures in a "
"single hydraulic structure."
)
self._dict["structure_type_ignored"] = _translate(
"Solver",
"Basic structure {name} of type {type} has no RubarBE "
"equivalent: it is not taken into account. RubarBE accepts "
"discharge weirs (S1), rectangular orifices (OR) and circular "
"orifices (OC)."
)
self._dict["structure_maximal_loading"] = _translate(
"Solver",
"Export cancelled: rectangular orifice {name}: RubarBE has no "
"maximal loading elevation. Leave it at 9999.999."
)
self._dict["structure_siltation"] = _translate(
"Solver",
"Export cancelled: circular orifice {name}: RubarBE has no "
"siltation height. Set it to 0."
)
self._dict["structure_invalid"] = _translate(
"Solver",
"Export cancelled: basic structure {name} must have a width (or "
"diameter) > 0 and a discharge coefficient > 0."
)
self._dict["structure_loading_below"] = _translate(
"Solver",
"Export cancelled: basic structure {name}: the loading elevation "
"must be above the weir elevation."
)
self._dict["structure_format"] = _translate(
"Solver",
"Export cancelled: a value of basic structure {name} does not fit "
"the 10 columns of RubarBE (3 decimals)."
)
self._dict["lateral_empty_ignored"] = _translate(
"Solver",
"Lateral contribution {name} on reach {reach} has no value: it "
"is ignored."
)
self._dict["lateral_solid_no_transport"] = _translate(
"Solver",
"Solid lateral contribution {name} on reach {reach} is not taken "
"into account: sediment transport (rubarbe_trased) is disabled "
"in the solver parameters."
)
self._dict["lateral_solid_unmatched"] = _translate(
"Solver",
"Solid lateral contribution {name} on reach {reach} is not taken "
"into account: RubarBE brings sediment only with water, and no "
"liquid lateral contribution has the same sections."
)
self._dict["lateral_solid_unisol"] = _translate(
"Solver",
"Export cancelled: solid lateral contribution {name} on reach "
"{reach} gives a solid discharge in kg/s, but the solver "
"parameters set the solid unit (rubarbe_sediment_unisol) to a "
"concentration. Set it to a mass discharge (kg/s)."
)
self._dict["lateral_solid_invalid"] = _translate(
"Solver",
"Export cancelled: solid lateral contribution {name} on reach "
"{reach} must have D50 > 0 m and sigma >= 1 (1 for a uniform "
"sediment)."
)
self._dict["lateral_solid_conflict"] = _translate(
"Solver",
"Export cancelled: solid lateral contributions {names} on reach "
"{reach} are attached to the same liquid contribution {liquid} "
"but have different D50 or sigma. RubarBE accepts a single "
"grain size per inflow."
)
self._dict["lateral_iodev_disabled"] = _translate(
"Solver",
"Lateral contribution {name} on reach {reach} is not taken into "
"account: lateral overflows (rubarbe_iodev) are disabled in the "
"solver parameters."
)
self._dict["lateral_type_unsupported"] = _translate(
"Solver",
"Export cancelled: lateral contribution {name} on reach {reach} "
"has no type. Choose a type (lateral sources, rain or "
"evaporation)."
)
self._dict["lateral_sections"] = _translate(
"Solver",
"Export cancelled: the sections of lateral contribution {name} "
"are not profiles of reach {reach}."
)
self._dict["lateral_not_sorted"] = _translate(
"Solver",
"Export cancelled: the times of lateral contribution {name} on "
"reach {reach} must be increasing."
)
self._dict["lateral_overlap"] = _translate(
"Solver",
"Export cancelled: lateral contributions {first} and {second} "
"on reach {reach} overlap, which RubarBE does not accept. Merge "
"them or move one of them."
)
self._dict["lateral_too_long"] = _translate(
"Solver",
"Export cancelled: the lateral contributions give {count} values "
"(number of points times number of cells of their zone), "
"RubarBE accepts at most {maximum}."
)
self._dict["boundary_empty_ignored"] = _translate(
"Solver",
"Boundary condition {name} at node {node} has no value: it is "
"ignored."
)
self._dict["boundary_missing_empty"] = _translate(
"Solver",
"Empty boundary condition(s) ignored: {names}."
)
self._dict["boundary_not_sorted"] = _translate(
"Solver",
"Export cancelled: the times of boundary condition {name} at "
"node {node} must be increasing."
)
self._dict["upstream_level_unsupported"] = _translate(
"Solver",
"Export cancelled: {setting} imposes an upstream water level, "
"which the current RubarBE version reads but does not use in "
"the computation. Choose another upstream boundary setting."
)
self._dict["missing_friction"] = _translate(
"Solver",
"Export cancelled: no friction defined on reach {reach}."
)
self._dict["missing_initial_conditions"] = _translate(
"Solver",
"Export cancelled: no initial condition defined on reach "
"{reach}."
)
self._dict["discharge_out_of_range"] = _translate(
"Solver",
"Export cancelled: initial discharges of reach {reach} must be "
"greater than -500 m3/s."
)
self._dict["duplicate_point_name"] = _translate(
"Solver",
"Export cancelled: several points are named '{name}' in "
"profile {profile} of reach {reach}."
)
self._dict["main_channel"] = _translate("Solver", "main channel")
self._dict["active_bed"] = _translate("Solver", "active bed")
self._dict["bed_limits_invalid"] = _translate(
"Solver",
"Export cancelled: in profile {profile} of reach {reach}, the "
"{bed} limits (points '{left}' and '{right}', or the profile "
"ends) must be ordered from left to right with at least one "
"point between them."
)
self._dict["missing_sediment_layers"] = _translate(
"Solver",
"Export cancelled: point {point} of profile {profile} of reach "
"{reach} has no sediment layer, which the sediment transport "
"requires."
)
self._dict["invalid_sediment_layer"] = _translate(
"Solver",
"Export cancelled: layer '{layer}' of sediment layers '{sl}' "
"(profile {profile} of reach {reach}) must have a thickness "
">= 0, D50 > 0 m, sigma >= 1 (1 for a uniform sediment) and a "
"critical constraint >= 0 Pa."
)
self._dict["too_many_compartments"] = _translate(
"Solver",
"Export cancelled: sediment layers '{sl}' (profile {profile} of "
"reach {reach}) give {count} compartments, RubarBE accepts at "
"most {maximum}. A non erodible compartment is added under an "
"erodible last layer."
)
self._dict["export_failed"] = _translate(
"Solver", "Export failed; task cancelled"
)
+398
View File
@@ -0,0 +1,398 @@
"""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.'),
)
# Choices read by RubarBE 2021 but never used by its computation: the
# upstream level of HAMONT (rubar_init.f90). The export refuses them.
UNSUPPORTED_CHOICES = {
('rubarbe_condam', '2'): 'La version actuelle de RubarBE ne prend pas '
'en compte la cote amont imposée.',
('rubarbe_regime', '1'): 'La version actuelle de RubarBE ne prend pas '
'en compte le tirant d’eau amont imposé.',
}
@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. '
'La cote imposée n’est pas prise en compte par la version '
'actuelle de RubarBE.',
{'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é ; non pris en compte par la version '
'actuelle de RubarBE.',
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_tcadim', 2, 'Seuil critique adimensionnel (—)',
'Contrainte critique adimensionnelle (TCADIM) fournie par '
'l’utilisateur, par exemple 0,047 pour Meyer-Peter et Müller ou 0,15 '
'pour Recking et al. (2008). Utilisée lorsque le seuil n’est pas '
'calculé par le diagramme de Shields et que la loi en a besoin.',
positive=True, optional=True)
add('sediment_visc', 2, 'Viscosité cinématique de l’eau (m²/s)',
'Viscosité cinématique de l’eau, variable avec la température. '
'À 15 °C : environ 1,30 × 10⁻⁶ m²/s.', 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)
add('abshyd', 4, 'Abscisses des hydrogrammes (m)',
'Abscisses (PK) où sont enregistrés hydrogrammes et limnigrammes '
'au pas de sauvegarde des hydrogrammes, séparées par des espaces '
'ou des points-virgules ; 100 au plus. Nécessaires si '
'« Hydrogrammes aux abscisses choisies » vaut Oui.',
kind='rk_list')
add('tnprof', 4, 'Instants de sauvegarde des profils (j:h:min:s ou s)',
'Instants séparés par des espaces ou des points-virgules ; 100 au '
'plus. Nécessaires si « Profils aux instants choisis » vaut Oui ; '
'les sorties complémentaires sont aussi écrites à ces instants.',
kind='time_list')
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 field.kind in ('rk_list', 'time_list'):
try:
numbers = Rubar3.save_list(value, field.kind == 'time_list')
except ValueError as error:
raise ValueError(
f'Valeur non reconnue : {error}. Séparez les valeurs par '
'des espaces ou des points-virgules, avec un point pour '
'les décimales.') from None
if len(numbers) > Rubar3._max_saves:
raise ValueError(f'{Rubar3._max_saves} valeurs au plus '
'(limite de RubarBE).')
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 == 'sediment_tcadim':
return Rubar3.tcadim_required(
values.get('rubarbe_sediment_opts', ''),
values.get('rubarbe_sediment_option', ''))
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')
def enabled(flag):
return values.get('rubarbe_' + flag, 'n').lower() in ('y', 'o')
if key == 'abshyd':
return enabled('tf_2')
if key == 'tnprof':
return enabled('tf_4') or enabled('iovis')
return True
+376
View File
@@ -0,0 +1,376 @@
"""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, UNSUPPORTED_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)
# Unsupported choices stay visible, so a stored value is still
# displayed, but cannot be selected anymore
for (key, value), reason in UNSUPPORTED_CHOICES.items():
editor = self.editors.get(key)
index = editor.findData(value) if editor is not None else -1
if index >= 0:
item = editor.model().item(index)
item.setEnabled(False)
item.setToolTip(tr(reason))
self._refreshing = False
def _update_context(self, values):
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.'))
for (key, value), reason in UNSUPPORTED_CHOICES.items():
if values.get(key) == value:
warnings.append(tr(reason) + tr(
' Le calcul sera refusé : choisissez un autre réglage '
'de la limite amont.'))
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:
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. '
'Le seuil critique adimensionnel se saisit dans son '
'propre champ.'))
except ValueError:
pass
if (FIELDS['rubarbe_sediment_tcadim'].key in self.editors and
active(FIELDS['rubarbe_sediment_tcadim'], values) and
not values.get('rubarbe_sediment_tcadim', '').strip()):
warnings.append(tr(
'La loi et les options choisies utilisent un seuil '
'critique adimensionnel : renseignez-le.'))
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()
+6
View File
@@ -35,7 +35,13 @@
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_table">
<item>
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
-10
View File
@@ -59,16 +59,6 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_suspenssion">
<attribute name="title">
<string>Suspension</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QTableView" name="tableView_suspenssion"/>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout"/>
+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">
+25 -20
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()
@@ -220,6 +220,26 @@ class Config(SQL):
"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'"
@@ -408,29 +428,14 @@ class Config(SQL):
new._cmd_solver = f"\"{path}{ext}\" @args @input"
self._solvers.append(new)
# Rubar3
ctor = solver_type_list["rubar3"]
new = ctor("default-Rubar3")
new._description = "Default Pamhyr2 Rubar 3 version"
# RubarBE
ctor = solver_type_list["rubarbe"]
new = ctor("default-RubarBE")
new._description = "Default Pamhyr2 RubarBE version"
path = os.path.join("@install_dir", "rubar", f"rubarbe{ext}")
new._cmd_solver = f""""{path}" @args @input"""
self._solvers.append(new)
# Rubar3
ctor = solver_type_list["rubar3"]
new = ctor("default-Rubar3")
new._description = "Default Pamhyr2 RubarBE version"
new._cmd_solver = f""""@install_dir/rubar/rubarbe{ext}" @args @input"""
self._solvers.append(new)
# Mage fake 7
# ctor = solver_type_list["mage_fake7"]
# new = ctor("default-mage-fake-7")
# new._description = "Default Pamhyr2 mage fake version 7"
# new._cmd_solver = f"""\
# "@install_dir/mage8/mage_as7{ext}" @args @input"""
# self._solvers.append(new)
# Backup
self.backup_enable = True
# self.backup_path = ""
Binary file not shown.