Compare commits

..
22 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
30 changed files with 1548 additions and 165 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"])
+6 -1
View File
@@ -17,6 +17,7 @@
# -*- coding: utf-8 -*-
import queue
import types
import random
import logging
import numpy as np
@@ -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
@@ -327,6 +334,32 @@ class SolverParametersList(PamhyrModelList):
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,
+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)
+709 -90
View File
@@ -17,6 +17,7 @@
# -*- coding: utf-8 -*-
import os
import re
import logging
import math
from decimal import Decimal, InvalidOperation, localcontext
@@ -76,11 +77,14 @@ class Rubar3(CommandLineSolver):
("rubarbe_fdeb2", "10"),
("rubarbe_fdeb3", "100"),
("rubarbe_tf_1", "y"),
("rubarbe_tf_2", "y"),
# TF(2) and TF(4) need the abscissas and times below
("rubarbe_tf_2", "n"),
("rubarbe_tf_3", "y"),
("rubarbe_tf_4", "y"),
("rubarbe_tf_4", "n"),
("rubarbe_tf_5", "y"),
("rubarbe_tf_6", "n"),
("rubarbe_abshyd", ""),
("rubarbe_tnprof", ""),
("rubarbe_trased", "n"),
("rubarbe_optfpc", "0"),
# trased parameters
@@ -119,10 +123,21 @@ class Rubar3(CommandLineSolver):
@classmethod
def checkers(cls):
lst = [
]
from Checker.RubarBE import (
RubarBEReachChecker, RubarBEGeometryChecker,
RubarBEBoundaryChecker, RubarBEFrictionChecker,
RubarBEInitialConditionsChecker, RubarBELateralChecker,
RubarBEStructureChecker, RubarBESavesChecker,
)
return lst
return [
checker(version=cls._type) for checker in (
RubarBEReachChecker, RubarBEGeometryChecker,
RubarBEBoundaryChecker, RubarBEFrictionChecker,
RubarBEInitialConditionsChecker, RubarBELateralChecker,
RubarBEStructureChecker, RubarBESavesChecker,
)
]
##########
# Export #
@@ -150,33 +165,22 @@ class Rubar3(CommandLineSolver):
name = study.name.replace(" ", "_")
values = self._parameter_values(study)
if (values.get("rubarbe_regime") == "1" and
values.get("rubarbe_condam") == "0"):
trad = RunSolverTranslate()
message = trad["incompatible_upstream_boundary"]
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
trased = self._parameter_flag(study, "trased") == "O"
if trased:
# Notice §3.6: HYDROS is required at each upstream boundary
missing = self._reaches_without_solid_boundary(study)
if missing:
trad = RunSolverTranslate()
message = trad["missing_solid_boundary"].format(
reaches=", ".join(missing)
)
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
# Check the hydraulic data before writing any file
try:
self._check_regime(values)
hydraulics = self._hydraulic_data(study, values)
geometry = self._geometry_data(hydraulics["edge"], trased)
solid = self._solid_boundary(
study.river, hydraulics["edge"], values, trased,
hydraulics["warnings"])
saves = self._save_data(values, hydraulics)
laterals = self._lateral_data(
study.river, hydraulics, values, trased,
hydraulics["warnings"])
structures = self._structure_data(
study.river, hydraulics, hydraulics["warnings"])
except RubarExportError as error:
self._cancel(qlog, str(error))
return False
@@ -194,10 +198,19 @@ class Rubar3(CommandLineSolver):
self._export_geomac_i(geometry, repertory, qlog, name=name)
self._export_mail(study, repertory, qlog, name=name)
self._export_devers(study, repertory, qlog, name=name)
if laterals is not None:
self._export_hydevs(laterals, trased, repertory, qlog, name=name)
self._export_ouvrag(structures, repertory, qlog, name=name)
self._export_condin(hydraulics, repertory, qlog, name=name)
self._export_stricklers(hydraulics, repertory, qlog, name=name)
self._export_boundaries(hydraulics, repertory, qlog, name=name)
# self._export_abshyd(study, repertory, qlog, name=name)
if solid is not None:
self._export_hydros(solid, hydraulics["tinit"], repertory, qlog,
name=name)
if trased:
self._export_condins(geometry, hydraulics["tinit"], repertory,
qlog, name=name)
self._export_saves(saves, repertory, qlog, name=name)
return True
@@ -217,15 +230,25 @@ class Rubar3(CommandLineSolver):
"5": (), "7": ()}
_boundary_kinds = {"TD": "hydrograph Q(t)", "TZ": "water level Z(t)",
"ZD": "rating curve Q(Z)",
"PC": "punctual contribution"}
"PC": "punctual contribution",
"SL": "solid discharge Qs(t)"}
# NCLMAX (variables.f90): points of all the solid hydrographs
_max_solid_points = 1000
def _hydraulic_data(self, study, values):
"""Check the single reach data needed by DONNEE, MAIL, CONDIN,
FROT, HYDRO and CONDAV, and return them. Raise
RubarExportError with a user message"""
@staticmethod
def _check_regime(values):
"""REGIME=1 imposes the water depth upstream, which a reflecting
upstream boundary (CONDAM=0) cannot do"""
if (values.get("rubarbe_regime") == "1" and
values.get("rubarbe_condam") == "0"):
raise RubarExportError(
RunSolverTranslate()["incompatible_upstream_boundary"])
@staticmethod
def _single_reach(river):
"""The single enabled reach and its profiles, with at least two
profiles of increasing rk. Raise RubarExportError"""
trad = RunSolverTranslate()
river = study.river
edges = river.enable_edges()
if len(edges) != 1:
raise RubarExportError(trad["multireach_unsupported"].format(
@@ -239,9 +262,17 @@ class Rubar3(CommandLineSolver):
if any(b <= a for a, b in zip(rks, rks[1:])):
raise RubarExportError(trad["rk_not_increasing"].format(
reach=edge.name))
return edge, profiles
def _hydraulic_data(self, study, values):
"""Check the single reach data needed by DONNEE, MAIL, CONDIN,
FROT, HYDRO and CONDAV, and return them. Raise
RubarExportError with a user message"""
river = study.river
edge, profiles = self._single_reach(river)
warnings = []
boundaries = self._check_boundaries(river, edge, values)
boundaries = self._check_boundaries(river, edge, values, warnings)
return {
"edge": edge,
@@ -307,17 +338,11 @@ class Rubar3(CommandLineSolver):
name=names[marker], profile=self._profile_label(profile),
reach=edge.name))
# Defaults of RubarBE: first and last points for G and D, G and D
# for g and d. Erosion only applies strictly between g and d
def index(marker, default):
return markers.index(marker) if marker in markers else default
left = index("G", 0)
right = index("D", len(points) - 1)
left, right, active_left, active_right = self._bed_limits(markers)
limits = [("main_channel", "rg", "rd", left, right)]
if trased:
limits.append(("active_bed", "g", "d",
index("g", left), index("d", right)))
active_left, active_right))
for bed, lname, rname, i, j in limits:
if j - i < 2:
raise RubarExportError(trad["bed_limits_invalid"].format(
@@ -325,6 +350,18 @@ class Rubar3(CommandLineSolver):
profile=self._profile_label(profile), reach=edge.name))
return markers
@staticmethod
def _bed_limits(markers):
"""Indices of the G, D, g and d points of a section. Defaults of
RubarBE: first and last points for G and D, G and D for g and d.
Erosion only applies strictly between g and d"""
def index(marker, default):
return markers.index(marker) if marker in markers else default
left = index("G", 0)
right = index("D", len(markers) - 1)
return left, right, index("g", left), index("d", right)
def _compartments(self, edge, profile, number, point):
"""Sediment layers of a point as RubarBE compartments, from the
top: (top elevation, D, S, critical stress). A non erodible
@@ -372,7 +409,7 @@ class Rubar3(CommandLineSolver):
return float(old_pamhyr_date_to_timestamp(value))
return float(value)
def _check_boundaries(self, river, edge, values):
def _check_boundaries(self, river, edge, values, warnings):
"""Liquid boundary conditions used for CONDAM, REGIME and CONDAV.
Return {'hydro': bc, 'condav': bc} (or None)"""
trad = RunSolverTranslate()
@@ -408,12 +445,12 @@ class Rubar3(CommandLineSolver):
upstream = self._select_boundaries(
at(edge.node1), self._upstream_types[condam], edge.node1,
{"TD": f"CONDAM={condam}"}, f"CONDAM={condam}"
{"TD": f"CONDAM={condam}"}, f"CONDAM={condam}", warnings
)
downstream = self._select_boundaries(
at(edge.node2), self._downstream_types[condav], edge.node2,
{"ZD": f"CONDAV={condav}", "TZ": f"CONDAV={condav}"},
f"CONDAV={condav}"
f"CONDAV={condav}", warnings
)
return {
@@ -421,9 +458,27 @@ class Rubar3(CommandLineSolver):
"condav": downstream.get("ZD", downstream.get("TZ")),
}
def _select_boundaries(self, bcs, expected, node, required_by, setting):
@staticmethod
def _ignore_empty(bcs, node, warnings, meaningful=()):
"""A boundary condition without value carries no data: it is
ignored with a warning, except for the types where an empty
condition has a meaning. Return (kept, ignored)"""
trad = RunSolverTranslate()
kept, ignored = [], []
for bc in bcs:
if len(bc) == 0 and bc.bctype not in meaningful:
warnings.append(trad["boundary_empty_ignored"].format(
name=bc.name, node=node.name))
ignored.append(bc)
else:
kept.append(bc)
return kept, ignored
def _select_boundaries(self, bcs, expected, node, required_by, setting,
warnings, meaningful=()):
trad = RunSolverTranslate()
selected = {}
bcs, ignored = self._ignore_empty(bcs, node, warnings, meaningful)
for bc in bcs:
kind = self._boundary_kinds[bc.bctype]
@@ -434,9 +489,6 @@ class Rubar3(CommandLineSolver):
if bc.bctype in selected:
raise RubarExportError(trad["boundary_ambiguous"].format(
kind=kind, node=node.name))
if len(bc) == 0:
raise RubarExportError(trad["boundary_empty"].format(
name=bc.name, node=node.name))
times = [d[0] for d in bc.data]
if bc.bctype != "ZD" and any(
b < a for a, b in zip(times, times[1:])):
@@ -446,12 +498,422 @@ class Rubar3(CommandLineSolver):
for bctype in expected:
if bctype not in selected:
raise RubarExportError(trad["boundary_missing"].format(
message = trad["boundary_missing"].format(
setting=required_by[bctype],
kind=self._boundary_kinds[bctype], node=node.name))
kind=self._boundary_kinds[bctype], node=node.name)
# The required condition may be the ignored empty one
empty = [bc.name for bc in ignored if bc.bctype == bctype]
if empty:
message += " " + trad["boundary_missing_empty"].format(
names=", ".join(empty))
raise RubarExportError(message)
return selected
def _solid_boundary(self, river, edge, values, trased, warnings):
"""Solid boundary condition written in HYDROS. RubarBE reads it
at the upstream node when TRASED is enabled and CONDAM=1 (CONDAM=2
is refused), and has no downstream solid condition (rubar_init.f90,
inits). Without sediment transport the solid conditions are
ignored. At the upstream node, an empty solid condition means a
supply at equilibrium in Pamhyr2 (Mage 'default'), written as a
negative Qs (_export_hydros). Return the condition or None"""
if not trased:
return None
trad = RunSolverTranslate()
condam = values.get("rubarbe_condam", "")
solid = [
bc for bc in river.boundary_condition.get_tab("solid")
if not bc.is_deleted() and bc.bctype == "SL"
]
downstream, _ = self._ignore_empty(
[bc for bc in solid if bc.node == edge.node2], edge.node2,
warnings)
if downstream:
raise RubarExportError(
trad["solid_boundary_downstream"].format(
name=downstream[0].name, node=edge.node2.name))
expected = ("SL",) if condam == "1" else ()
selected = self._select_boundaries(
[bc for bc in solid if bc.node == edge.node1], expected,
edge.node1, {"SL": f"TRASED=O, CONDAM={condam}"},
f"CONDAM={condam}", warnings, meaningful=("SL",)
)
bc = selected.get("SL")
if bc is None:
return None
if len(bc) > self._max_solid_points:
raise RubarExportError(trad["solid_boundary_too_long"].format(
name=bc.name, node=edge.node1.name, count=len(bc),
maximum=self._max_solid_points))
# RubarBE stops on a zero D or S
if not (bc.d50 > 0 and bc.sigma >= 1):
raise RubarExportError(trad["solid_boundary_invalid"].format(
name=bc.name, node=edge.node1.name))
return bc
############
# Laterals #
############
# Pamhyr2 lateral contribution types; evaporation is a withdrawal
_lateral_signs = {"LC": 1.0, "RA": 1.0, "EV": -1.0}
# QMU, TMU (variables.f90, LMAX*20): RubarBE copies the series of an
# inflow in each cell of its zone, without checking this size
_max_lateral_points = 3000 * 20
def _lateral_data(self, river, hydraulics, values, trased, warnings):
"""Lateral inflows of HYDEVS, read when IODEV=O (rubar_init.f90,
init2). Return [(first cell, last cell, [(t, q), ...]), ...]
sorted from upstream, or None when IODEV=N.
A contribution from section b to section e (1-based) covers the
cells b+1 to e, i.e. from section b to section e (RubarBE divides
the discharge by this length); a contribution at a single section
b covers the cell downstream of it (upstream of it for the last
section). The first and last cells of the reach are never used,
as RubarBE requires. With IODEV=N, the contributions are ignored
with a warning.
RubarBE brings sediment only with a liquid inflow, on the same
cells: a solid contribution (kg/s, UNISOL=1) is attached to the
liquid contribution of the same zone, whose block gets its D50
and sigma; the times of both series are merged (linear
interpolation, constant outside). Several solid contributions of
a zone are summed when they have the same grain size. A solid
contribution without liquid one, or without sediment transport,
is ignored with a warning. A liquid inflow without solid
contribution brings clear water (Qs = 0, D = S = 0)."""
trad = RunSolverTranslate()
edge = hydraulics["edge"]
profiles = hydraulics["profiles"]
iodev = values.get("rubarbe_iodev", "n").lower() in ("y", "o")
lst = river.lateral_contribution
def on_reach(tab):
# A contribution without value carries no data: ignored
kept = []
for lc in lst.get_tab(tab):
if lc.is_deleted() or lc.reach != edge:
continue
if len(lc) == 0:
warnings.append(trad["lateral_empty_ignored"].format(
name=lc.name, reach=edge.name))
else:
kept.append(lc)
return kept
lcs = on_reach("liquid")
solids = on_reach("solid")
# Not read by RubarBE: the user is warned, the computation runs
# without them
if not iodev:
for lc in lcs + solids:
warnings.append(trad["lateral_iodev_disabled"].format(
name=lc.name, reach=edge.name))
return None
if not trased:
for lc in solids:
warnings.append(trad["lateral_solid_no_transport"].format(
name=lc.name, reach=edge.name))
solids = []
if solids and values.get("rubarbe_sediment_unisol", "1") != "1":
raise RubarExportError(trad["lateral_solid_unisol"].format(
name=solids[0].name, reach=edge.name))
index = {id(p): i for i, p in enumerate(profiles, 1)}
count = len(profiles)
def zone_and_data(lc):
if (id(lc.begin_section) not in index or
id(lc.end_section) not in index):
raise RubarExportError(trad["lateral_sections"].format(
name=lc.name, reach=edge.name))
b, e = sorted((index[id(lc.begin_section)],
index[id(lc.end_section)]))
zone = (b + 1, e) if b < e else ((min(b + 1, count),) * 2)
data = [(float(d[0]), float(d[1])) for d in lc.data]
times = [t for t, _ in data]
if any(t1 < t0 for t0, t1 in zip(times, times[1:])):
raise RubarExportError(trad["lateral_not_sorted"].format(
name=lc.name, reach=edge.name))
return zone, data
blocks = []
for lc in lcs:
if lc.lctype not in self._lateral_signs:
raise RubarExportError(trad["lateral_type_unsupported"].format(
name=lc.name, reach=edge.name))
(first, last), data = zone_and_data(lc)
sign = self._lateral_signs[lc.lctype]
blocks.append((first, last, [(t, sign * q) for t, q in data],
lc.name))
blocks.sort(key=lambda block: block[0])
for (_, last, _, name0), (first, _, _, name1) in zip(
blocks, blocks[1:]):
if first <= last:
raise RubarExportError(trad["lateral_overlap"].format(
first=name0, second=name1, reach=edge.name))
# Solid contributions grouped by the zone of their liquid one
zones = {(first, last) for first, last, _, _ in blocks}
sediment = {}
for lc in solids:
zone, data = zone_and_data(lc)
if not (lc.d50 > 0 and lc.sigma >= 1):
raise RubarExportError(trad["lateral_solid_invalid"].format(
name=lc.name, reach=edge.name))
if zone not in zones:
warnings.append(trad["lateral_solid_unmatched"].format(
name=lc.name, reach=edge.name))
continue
sediment.setdefault(zone, []).append((lc, data))
result = []
for first, last, data, name in blocks:
attached = sediment.get((first, last), [])
grains = {(lc.d50, lc.sigma) for lc, _ in attached}
if len(grains) > 1:
raise RubarExportError(trad["lateral_solid_conflict"].format(
names=", ".join(lc.name for lc, _ in attached),
liquid=name, reach=edge.name))
times = sorted({t for t, _ in data} |
{t for _, solid in attached for t, _ in solid})
def at(series):
return np.interp(times, [t for t, _ in series],
[v for _, v in series])
q = at(data)
qs = sum((at(solid) for _, solid in attached),
np.zeros(len(times)))
length = (self._section_rk(profiles[last - 1]) -
self._section_rk(profiles[first - 2]))
result.append((
first, last,
[(float(t), float(v), float(vs))
for t, v, vs in zip(times, q, qs)],
next(iter(grains), (0.0, 0.0)),
length,
))
points = sum((last - first + 1) * len(data)
for first, last, data, _, _ in result)
if points > self._max_lateral_points:
raise RubarExportError(trad["lateral_too_long"].format(
count=points, maximum=self._max_lateral_points))
return result
##############
# Structures #
##############
# Pamhyr2 basic structures (those of Mage) with a RubarBE equivalent
# (notice, annex 5): weir and rectangular orifice as a weir-orifice D,
# circular orifice as O
_structure_types = {"S1": "D", "OR": "D", "OC": "O"}
# Loading elevation of a weir that never loads (default of Pamhyr2)
_no_loading = 9999.999
# nou1Dmax, noe1Dmax (variables.f90)
_max_structures = 50
_max_elementary = 10
def _structure_data(self, river, hydraulics, warnings):
"""Structures of OUVRAG (rubar_init.f90, LOUVR1D). A Pamhyr2
structure sits on a section: RubarBE structure on this single
interface, with reference -2 (the discharge is that of the
structure only), as for Mage. Disabled structures are skipped, as
for Mage. Return [(rk, [(type, L, zd, zm, coefficient), ...]), ...]
sorted from upstream. Raise RubarExportError with a user message"""
trad = RunSolverTranslate()
edge = hydraulics["edge"]
sections = {id(p) for p in hydraulics["profiles"]}
structures = {}
for hs in river.hydraulic_structures.lst:
if (hs.is_deleted() or not hs.enabled
or hs.input_reach is None or hs.input_section is None
or hs.input_reach != edge):
continue
if id(hs.input_section) not in sections:
raise RubarExportError(trad["structure_section"].format(
name=hs.name, reach=edge.name))
# Basic structures without RubarBE equivalent are not taken
# into account: the user is warned
lines = []
for bhs in hs.basic_structures:
if not bhs.enabled or bhs.is_deleted():
continue
if bhs.type not in self._structure_types:
warnings.append(trad["structure_type_ignored"].format(
name=self._basic_label(hs, bhs), type=bhs.type))
continue
lines.append(self._elementary_structure(hs, bhs))
if not lines:
warnings.append(trad["structure_empty_ignored"].format(
name=hs.name))
continue
if len(lines) > self._max_elementary:
raise RubarExportError(trad["structure_too_many"].format(
name=hs.name, count=len(lines),
maximum=self._max_elementary))
rk = self._section_rk(hs.input_section)
if rk in structures:
raise RubarExportError(trad["structure_same_section"].format(
first=structures[rk][1], second=hs.name, rk=f"{rk:g}"))
structures[rk] = (lines, hs.name)
if len(structures) > self._max_structures:
raise RubarExportError(trad["structure_too_many_structures"]
.format(count=len(structures),
maximum=self._max_structures))
return [(rk, structures[rk][0]) for rk in sorted(structures)]
@staticmethod
def _basic_label(hs, bhs):
return f"{bhs.name or bhs.type} ({hs.name})"
def _elementary_structure(self, hs, bhs):
"""RubarBE line of a basic structure of a type of _structure_types:
(type, L, zd, zm, coefficient) for D (zm: loading elevation),
(type, L, zd, D, coefficient) for O (L: pipe length, none in
Pamhyr2: 0, a thin wall orifice)"""
trad = RunSolverTranslate()
label = self._basic_label(hs, bhs)
values = [float(v.value) for v in bhs.parameters]
if bhs.type == "S1":
width, elevation, coefficient = values
line = ("D", width, elevation, self._no_loading, coefficient)
elif bhs.type == "OR":
width, elevation, loading, coefficient, maximal = values
if maximal < self._no_loading:
raise RubarExportError(trad["structure_maximal_loading"]
.format(name=label))
line = ("D", width, elevation, loading, coefficient)
else:
diameter, elevation, siltation, coefficient = values
if siltation != 0:
raise RubarExportError(trad["structure_siltation"].format(
name=label))
line = ("O", 0.0, elevation, diameter, coefficient)
kind, size, zd, zm, coefficient = line
if (size <= 0 if kind == "D" else zm <= 0) or coefficient <= 0:
raise RubarExportError(trad["structure_invalid"].format(
name=label))
if kind == "D" and zm <= zd:
raise RubarExportError(trad["structure_loading_below"].format(
name=label))
# Fixed columns (A1, A9, 4F10.0)
if any(len(f"{v:10.3f}") > 10 for v in line[1:]):
raise RubarExportError(trad["structure_format"].format(
name=label))
return line
#########
# Saves #
#########
# NBHYPR (variables.f90): abscissas of ABSHYD and times of TNPROF.
# RubarBE does not check this size
_max_saves = 100
@staticmethod
def save_list(text, times=False):
"""Numbers of a list parameter (ABSHYD abscissas, TNPROF times),
separated by spaces or semicolons. Times are in seconds or in
days:hours:minutes:seconds. Raise ValueError with the invalid
token"""
values = []
for token in re.split(r"[\s;]+", text.strip()):
if token == "":
continue
try:
if times and ":" in token:
parts = token.split(":")
if len(parts) != 4 or not all(
p.isascii() and p.isdigit() for p in parts):
raise ValueError(token)
value = float(sum(
int(p) * factor for p, factor in
zip(parts, (86400, 3600, 60, 1))))
else:
value = float(token)
except ValueError:
raise ValueError(token) from None
if not math.isfinite(value):
raise ValueError(token)
values.append(value)
return values
def _save_data(self, values, hydraulics):
"""Abscissas of ABSHYD, read when TF(2)=O, and times of TNPROF,
always read: they give the saves of PROFIL and PROFILS when
TF(4)=O and of the complementary outputs when IOVIS=O
(rubar_init.f90, init2; rubarbe.f90, SAVEVISU). Return
{'abshyd': [rk] or None, 'tnprof': [t]}"""
trad = RunSolverTranslate()
lists = {}
for key, times in (("abshyd", False), ("tnprof", True)):
key = f"rubarbe_{key}"
try:
lists[key] = self.save_list(values.get(key, ""), times)
except ValueError as error:
raise RubarExportError(trad["save_list_invalid"].format(
key=key, value=str(error)))
if len(lists[key]) > self._max_saves:
raise RubarExportError(trad["save_list_too_long"].format(
key=key, count=len(lists[key]),
maximum=self._max_saves))
def enabled(flag):
return values.get(f"rubarbe_{flag}", "n").lower() in ("y", "o")
for flag, key in (("tf_2", "rubarbe_abshyd"),
("tf_4", "rubarbe_tnprof")):
if enabled(flag) and not lists[key]:
raise RubarExportError(trad["save_list_required"].format(
flag=f"rubarbe_{flag}", key=key))
# RubarBE looks for each abscissa between two centres of cells,
# from the first to the last section
abshyd = None
if enabled("tf_2"):
edge = hydraulics["edge"]
first = self._section_rk(hydraulics["profiles"][0])
last = self._section_rk(hydraulics["profiles"][-1])
for rk in lists["rubarbe_abshyd"]:
if not first <= round(rk, 3) <= last:
raise RubarExportError(trad["abshyd_outside"].format(
rk=f"{rk:g}", first=f"{first:g}", last=f"{last:g}",
reach=edge.name))
abshyd = lists["rubarbe_abshyd"]
# Times outside the computation do not falsify it: RubarBE saves
# the earlier ones at the first time steps and never reaches the
# later ones
tinit = hydraulics["tinit"]
tmax = self._seconds(values.get("rubarbe_tmax", "0"))
tnprof = sorted(lists["rubarbe_tnprof"])
outside = [t for t in tnprof if not tinit <= t <= tmax]
if outside:
hydraulics["warnings"].append(trad["tnprof_outside"].format(
times=" ".join(f"{t:g}" for t in outside),
tinit=f"{tinit:g}", tmax=f"{tmax:g}"))
return {"abshyd": abshyd, "tnprof": tnprof}
@staticmethod
def _interpolated_frictions(edge):
"""Minor and medium Strickler at each centre of cell. The model
@@ -578,20 +1040,6 @@ class Rubar3(CommandLineSolver):
return False
return True
@staticmethod
def _reaches_without_solid_boundary(study):
river = study.river
solid = [
bc for bc in river.boundary_condition.get_tab("solid")
if not bc.is_deleted() and bc.bctype == "SL" and len(bc) > 0
]
return [
edge.name for edge in river.enable_edges()
if river.is_upstream_node(edge.node1)
and not any(bc.node == edge.node1 for bc in solid)
]
def _parameter_values(self, study):
return {p.name: str(p.value).strip()
for p in study.river.get_params(self.type).parameters}
@@ -825,29 +1273,33 @@ class Rubar3(CommandLineSolver):
self._write_parameters(repertory, f"mail.{name}", lines)
def _export_abshyd(self, study, repertory, qlog, name="0"):
def _export_saves(self, saves, repertory, qlog, name="0"):
"""ABSHYD (number, then I4, 1X, F11.3 per abscissa; single reach,
its number is omitted) when TF(2)=O, and TNPROF (number, then
index and time per line). TNPROF is always read by RubarBE: an
empty list is written as 0, so that a previous TNPROF of the
directory is not used"""
if saves["abshyd"] is not None:
if qlog is not None:
qlog.put("Export ABSHYD file")
lines = [f"{len(saves['abshyd']):>4}\n"] + [
f"{i:>4} {rk:>11.3f}\n"
for i, rk in enumerate(saves["abshyd"], 1)
]
self._write_parameters(repertory, f"abshyd.{name}", lines)
with open(
os.path.join(
repertory, f"abshyd.{name}"
), "w+"
) as f:
reach_ind = 1
for edge in study.river.enable_edges():
reach = edge.reach
lm = len(reach) + 1
f.write(f"{lm:>13}\n")
ind = 1
for mail in reach.inter_profiles_rk():
f.write(f"{ind:>4} {mail:15.3f} {reach_ind:>4}\n")
ind += 1
reach_ind += 1
if qlog is not None:
qlog.put("Export TNPROF file")
lines = [f"{len(saves['tnprof']):>12}\n"] + [
f"{i:>12} {t:>24.3f}\n"
for i, t in enumerate(saves["tnprof"], 1)
]
self._write_parameters(repertory, f"tnprof.{name}", lines)
def _export_devers(self, study, repertory, qlog, name="0"):
"""DEVERS, required by RubarBE when IODEV=O: per cell, weir
level, coefficient and exponent (notice annex 1). Pamhyr2 has no
lateral weir data: a zero coefficient everywhere (no overflow)"""
if qlog is not None:
qlog.put("Export DEVERS file")
@@ -869,6 +1321,44 @@ class Rubar3(CommandLineSolver):
reach_ind += 1
def _export_hydevs(self, laterals, trased, repertory, qlog, name="0"):
"""HYDEVS: per inflow, first and last cells and number of points,
then (Q, t) per point (rubar_init.f90, init2). With sediment
transport, D and S on the first line and (Q, t, Qs, ts) per point:
Pamhyr2 gives no sediment for a liquid inflow, so Qs = 0 (and
D = S = 0, the sediment of the main channel). Written, possibly
empty, so that a previous HYDEVS of the directory is not used"""
if qlog is not None:
qlog.put("Export HYDEVS file")
lines = []
for first, last, data, (d50, sigma), length in laterals:
if trased:
lines.append(
f"{first} {last} {len(data)} {d50!r} {sigma!r}\n")
lines += [f"{q!r} {t!r} {qs / length!r} {t!r}\n"
for t, q, qs in data]
else:
lines.append(f"{first} {last} {len(data)}\n")
lines += [f"{q!r} {t!r}\n" for t, q, _ in data]
self._write_parameters(repertory, f"hydevs.{name}", lines)
def _export_ouvrag(self, structures, repertory, qlog, name="0"):
"""OUVRAG (notice annex 1, rubar_init.f90, LOUVR1D): per
structure, upstream abscissa, reference and reach, downstream
abscissa, number of basic structures and reach, then one line per
basic structure in (A1, A9, 4F10.0). Written, possibly empty, so
that a previous OUVRAG of the directory is not used"""
if qlog is not None:
qlog.put("Export OUVRAG file")
lines = []
for rk, elementary in structures:
lines.append(f"{rk:11.3f} {-2:4d} {1:4d}\n")
lines.append(f"{rk:11.3f} {len(elementary):4d} {1:4d}\n")
for kind, size, zd, zm, coefficient in elementary:
lines.append(f"{kind}{'':9}{size:10.3f}{zd:10.3f}"
f"{zm:10.3f}{coefficient:10.3f}\n")
self._write_parameters(repertory, f"ouvrag.{name}", lines)
def _export_stricklers(self, hydraulics, repertory, qlog, name="0"):
"""FROT (minor bed) and FROT2 (medium bed): LM, then one Strickler
per cell (notice, annex 1)"""
@@ -930,6 +1420,45 @@ class Rubar3(CommandLineSolver):
lines = [header(len(bc))] + records(bc, columns)
self._write_parameters(repertory, f"{filename}.{name}", lines)
def _export_hydros(self, bc, tinit, repertory, qlog, name="0"):
"""HYDROS: number of points, then Qs, t, D, S (notice annex 1).
Qs is a solid discharge (kg/s) or a concentration (kg/m3)
according to UNISOL, D and S are those of the condition. An
empty condition (supply at equilibrium) is written as a single
negative Qs: RubarBE then takes the transport capacity of the
upstream section at each time step (notice §3.6; rubarbe.f90,
'si Hydros.etude contient un Qs<0')"""
if qlog is not None:
qlog.put("Export HYDROS file")
d50, sigma = float(bc.d50), float(bc.sigma)
data = [(t, qs) for t, qs in bc.data] or [(tinit, -1.0)]
lines = [f"{len(data)}\n"] + [
f"{float(qs)!r} {float(t)!r} {d50!r} {sigma!r}\n"
for t, qs in data
]
self._write_parameters(repertory, f"hydros.{name}", lines)
def _export_condins(self, geometry, tinit, repertory, qlog, name="0"):
"""CONDINS: TINIT, then for each section (intermaille) its number,
Qs, D and S of the active layer (notice annex 1, rubar_init.f90,
inits). Pamhyr2 holds no initial solid condition: Qs = 0 (empty
active layer) and D, S are the mean of the top compartments of the
points strictly between g and d, as advised by the notice §3.5"""
if qlog is not None:
qlog.put("Export CONDINS file")
lines = [f"{tinit!r}\n"]
for i, (_, rows) in enumerate(geometry, 1):
markers = [marker for marker, _, _ in rows]
_, _, left, right = self._bed_limits(markers)
tops = [compartments[0] for _, _, compartments
in rows[left + 1:right]]
# Rounded to 10 significant digits, so that a mean of equal
# values is written as these values
d = float(f"{sum(c[1] for c in tops) / len(tops):.10g}")
s = float(f"{sum(c[2] for c in tops) / len(tops):.10g}")
lines.append(f"{i} 0.0 {d!r} {s!r}\n")
self._write_parameters(repertory, f"condins.{name}", lines)
def read_profil(self, study, fname, results, qlog=None, name="0"):
logger.info(f"read: profil.{name}")
with open(fname, "r") as f:
@@ -949,10 +1478,16 @@ class Rubar3(CommandLineSolver):
else:
return (0.0, 0.0, 0.0, 0.0)
def set_and_compute_limites(reach, ind, z, q, s):
# Bed level of each section (mean of the CTDF of the
# neighbouring cells): {(reach, ind): {timestamp: level}}
bed = {}
def set_and_compute_limites(reach, ind, z, q, s, zf=None):
reach.set(ind, timestamp, "Z", z)
reach.set(ind, timestamp, "Q", q)
reach.set(ind, timestamp, "V", s)
if zf is not None:
bed.setdefault((reach, ind), {})[timestamp] = zf
profile = reach.profile(ind)
limits = profile.geometry.get_water_limits(z)
@@ -990,7 +1525,9 @@ class Rubar3(CommandLineSolver):
if line == "":
break
timestamp = float(line)
# Saves at the times of TNPROF are marked 'tnprof'
# (rubarbe.f90, SAVEVISU)
timestamp = float(line.split()[0])
ts.add(timestamp)
logger.info(f"read: profil.{name}: timestamp = {timestamp}")
@@ -998,7 +1535,7 @@ class Rubar3(CommandLineSolver):
for reach, lm in reachs:
# First profile
h, s, q, z = read_data_line(f)
set_and_compute_limites(reach, 0, z+h, q, s)
set_and_compute_limites(reach, 0, z+h, q, s, z)
# For each profile
ind = 1
@@ -1009,7 +1546,8 @@ class Rubar3(CommandLineSolver):
reach, ind,
(pz + z + ph + h) / 2,
(pq + q) / 2,
(ps + s) / 2
(ps + s) / 2,
(pz + z) / 2
)
ph = h
@@ -1020,7 +1558,7 @@ class Rubar3(CommandLineSolver):
# Last profile
h, s, q, z = read_data_line(f)
set_and_compute_limites(reach, ind, z+h, q, s)
set_and_compute_limites(reach, ind, z+h, q, s, z)
# Tables and plots use timestamp rows and global profile columns.
timestamps = sorted(ts)
@@ -1030,6 +1568,21 @@ class Rubar3(CommandLineSolver):
profiles.extend(reach.profiles)
reach.set_global_index(range(offset, len(profiles)))
# Kept only when the bed moves (sediment transport), so that
# the plots otherwise show the study geometry. The initial
# condition of the study has no bed level: the first one read
# in PROFIL is used
keys = ["Z", "Q", "V"]
moves = any(max(levels.values()) != min(levels.values())
for levels in bed.values())
if moves and len(bed) == len(profiles):
for (reach, ind), levels in bed.items():
first = levels[min(levels)]
for t in timestamps:
first = levels.get(t, first)
reach.set(ind, t, "zfd", first)
keys.append("zfd")
results.set("timestamps", timestamps)
results.compute_timestamp_index()
results.set("table", {
@@ -1037,7 +1590,7 @@ class Rubar3(CommandLineSolver):
[profile.get_ts_key(t, key) for profile in profiles]
for t in timestamps
], dtype=float)
for key in ("Z", "Q", "V")
for key in keys
})
@timer
@@ -1155,8 +1708,74 @@ class Rubar3(CommandLineSolver):
logger_exception(e)
return None
# Without them, the hydraulic results stay readable
try:
self.read_bed_sections(results, repertory, name)
except Exception as e:
logger.warning(f"Failed to read the bed sections")
logger_exception(e)
return results
def read_bed_sections(self, results, repertory, name):
"""Bed sections for the cross section plot of the results window:
- 'bed_points' {global index: [bool, ...]}: the points whose level
changes, strictly between g and d (step 4);
- 'bed_sections' {timestamp: {global index: [z, ...]}}: exact
sections saved at the times of TNPROF when IOVIS=O ('geomac-N',
GEOMAC-I format; rubarbe.f90, SAVEVISU).
Between two exact sections, the plot shifts the points of the last
one by the change of the section bed level 'zfd' since then"""
if "zfd" not in results.get("table") or len(results.river) != 1:
return
profiles = results.river.reach(0).profiles
moving = {}
for profile in profiles:
markers = [self._point_markers.get(p.name.strip(), " ")
for p in profile.geometry.points]
_, _, left, right = self._bed_limits(markers)
moving[profile.global_index] = [
left < i < right for i in range(len(markers))
]
results.set("bed_points", moving)
timestamps = results.get("timestamps")
sections = {}
pattern = re.compile(rf"geomac-\d+\.{re.escape(name)}")
for fname in sorted(os.listdir(repertory)):
if not pattern.fullmatch(fname):
continue
t, levels = self._read_geomac(os.path.join(repertory, fname))
if (len(levels) != len(profiles) or
any(len(z) != len(p.geometry.points)
for z, p in zip(levels, profiles))):
logger.warning(f"{fname}: sections differ from the study "
"geometry, ignored")
continue
# Same save as in PROFIL, with fewer digits (F15.3)
timestamp = min(timestamps, key=lambda ts: abs(ts - t))
if abs(timestamp - t) > 0.01:
continue
sections[timestamp] = {
p.global_index: np.array(z) for p, z in zip(profiles, levels)
}
results.set("bed_sections", sections)
@staticmethod
def _read_geomac(path):
"""Time and level of each point of each section of a GEOMAC file
(GEOMAC-I format: top of the first compartment)"""
with open(path) as f:
lines = iter([line for line in f if line.strip()])
count, time = next(lines).split()[:2]
levels = []
for _ in range(int(count)):
points = int(next(lines).split()[2])
levels.append([float(next(lines)[1:].split()[1])
for _ in range(points)])
return float(time), levels
class RubarBE(Rubar3):
_type = "rubarbe"
-2
View File
@@ -36,7 +36,6 @@ solver_long_name = {
"adistswc": "Adis-TS_WC",
"adisttwc": "Adis-TT_WC",
"rubarbe": "RubarBE",
"rubar3": "Rubar3",
}
solver_type_list = {
@@ -47,5 +46,4 @@ solver_type_list = {
"adistswc": AdisTSwc,
"adisttwc": AdisTTwc,
"rubarbe": RubarBE,
"rubar3": Rubar3,
}
+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]
+179 -8
View File
@@ -49,12 +49,50 @@ class RunSolverTranslate(PamhyrTranslate):
"use a dimensionless critical stress given by the user. "
"Fill it in the parameters (tab Solid Transport)."
)
self._dict["missing_solid_boundary"] = _translate(
self._dict["solid_boundary_downstream"] = _translate(
"Solver",
"Export cancelled: sediment transport (TRASED) is enabled but "
"no solid boundary condition is defined at the upstream node "
"of reach(es): {reaches}. Add a solid boundary condition, or "
"disable sediment transport in the solver parameters."
"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",
@@ -91,10 +129,143 @@ class RunSolverTranslate(PamhyrTranslate):
"Export cancelled: several {kind} boundary conditions at node "
"{node}."
)
self._dict["boundary_empty"] = _translate(
self._dict["structure_section"] = _translate(
"Solver",
"Export cancelled: boundary condition {name} at node {node} "
"has no value."
"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",
+31
View File
@@ -262,6 +262,17 @@ def fields():
'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 '
@@ -298,6 +309,18 @@ def validate(field, value):
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')
@@ -364,4 +387,12 @@ def active(field, values):
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
+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"/>
+1 -17
View File
@@ -428,15 +428,7 @@ 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"
path = os.path.join("@install_dir", "rubar", f"rubarbe{ext}")
new._cmd_solver = f""""{path}" @args @input"""
self._solvers.append(new)
# RubarBE (same executable as Rubar3, see notice §10.3)
# RubarBE
ctor = solver_type_list["rubarbe"]
new = ctor("default-RubarBE")
new._description = "Default Pamhyr2 RubarBE version"
@@ -444,14 +436,6 @@ class Config(SQL):
new._cmd_solver = f""""{path}" @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.