Compare commits

...
8 Commits
10 changed files with 1149 additions and 119 deletions

No files matched your search

@@ -315,6 +315,14 @@ class SolverParametersList(PamhyrModelList):
n._lst = list(map(lambda t: t[1], sorted(lst))) 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 new[solver] = n
return new return new
+6 -1
View File
@@ -25,6 +25,7 @@ from PyQt5.QtCore import QProcess
from Scripts.AScript import AScript from Scripts.AScript import AScript
from Model.Study import Study from Model.Study import Study
from View.RunSolver.Translate import RunSolverTranslate
logger = logging.getLogger() logger = logging.getLogger()
@@ -89,12 +90,16 @@ class ScriptRun(AScript):
# Export and Run # Export and Run
logger.info(f"~Export~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") logger.info(f"~Export~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
solver.export(study, workdir, qlog=self._q) ok = solver.export(study, workdir, qlog=self._q)
while self._q.qsize() != 0: while self._q.qsize() != 0:
s = self._q.get() s = self._q.get()
logger.info(s) logger.info(s)
if not ok:
logger.warning(RunSolverTranslate()["export_failed"])
return
if command == "run": if command == "run":
logger.info(f"~Run~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") logger.info(f"~Run~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
solver.run( solver.run(
+304 -106
View File
@@ -18,11 +18,14 @@
import os import os
import logging import logging
import math
from decimal import Decimal, InvalidOperation, localcontext
import numpy as np import numpy as np
from tools import timer, trace, old_pamhyr_date_to_timestamp, logger_exception from tools import timer, trace, old_pamhyr_date_to_timestamp, logger_exception
from Solver.CommandLine import CommandLineSolver from Solver.CommandLine import CommandLineSolver
from View.RunSolver.Translate import RunSolverTranslate
from Model.Results.Results import Results from Model.Results.Results import Results
from Model.Results.River.River import River, Reach, Profile from Model.Results.River.River import River, Reach, Profile
@@ -44,7 +47,7 @@ class Rubar3(CommandLineSolver):
@classmethod @classmethod
def default_parameters(cls): def default_parameters(cls):
# lst = super(Rubar3, cls).default_parameters() # Keep historical rubarbe_* keys for existing Rubar3 studies.
lst = [ lst = [
("rubarbe_cfl", "0.50000E+00"), ("rubarbe_cfl", "0.50000E+00"),
@@ -52,13 +55,13 @@ class Rubar3(CommandLineSolver):
("rubarbe_condav", "3"), ("rubarbe_condav", "3"),
("rubarbe_regime", "0"), ("rubarbe_regime", "0"),
("rubarbe_iodev", "n"), ("rubarbe_iodev", "n"),
("rubarbe_iodebord", ""), ("rubarbe_iodebord", "o"),
("rubarbe_iostockage", ""), ("rubarbe_iostockage", "o"),
("rubarbe_iopdt", "y"), ("rubarbe_iopdt", "y"),
("rubarbe_iovis", "n"), ("rubarbe_iovis", "n"),
("rubarbe_rep", "n"), ("rubarbe_rep", "n"),
("rubarbe_tinit", "000:00:00:00"), ("rubarbe_tinit", "000:00:00:00"),
("rubarbe_tmax", "000:01:00:00"), # 1 day ("rubarbe_tmax", "001:00:00:00"), # 1 day
("rubarbe_tiopdt", "000:00:00:00"), ("rubarbe_tiopdt", "000:00:00:00"),
("rubarbe_dt", "5.0"), ("rubarbe_dt", "5.0"),
("rubarbe_ts", "999:99:99:00"), ("rubarbe_ts", "999:99:99:00"),
@@ -89,8 +92,9 @@ class Rubar3(CommandLineSolver):
("rubarbe_sediment_mult_3", ""), ("rubarbe_sediment_mult_3", ""),
("rubarbe_sediment_mult_4", ""), ("rubarbe_sediment_mult_4", ""),
("rubarbe_sediment_mult_5", ""), ("rubarbe_sediment_mult_5", ""),
("rubarbe_sediment_visc", "0.047"), ("rubarbe_sediment_tcadim", "0.047"),
("rubarbe_sediment_opts", "6"), ("rubarbe_sediment_visc", "1.3E-06"),
("rubarbe_sediment_opts", "3"),
("rubarbe_sediment_odchar", "0"), ("rubarbe_sediment_odchar", "0"),
("rubarbe_sediment_unisol", "1"), ("rubarbe_sediment_unisol", "1"),
("rubarbe_sediment_typdef", "3"), ("rubarbe_sediment_typdef", "3"),
@@ -140,8 +144,34 @@ class Rubar3(CommandLineSolver):
self._study = study self._study = study
name = study.name.replace(" ", "_") name = study.name.replace(" ", "_")
values = self._parameter_values(study)
if (values.get("rubarbe_regime") == "1" and
values.get("rubarbe_condam") == "0"):
trad = RunSolverTranslate()
message = trad["incompatible_upstream_boundary"]
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
trased = self._parameter_flag(study, "trased") == "O"
if trased:
# Notice §3.6: HYDROS is required at each upstream boundary
missing = self._reaches_without_solid_boundary(study)
if missing:
trad = RunSolverTranslate()
message = trad["missing_solid_boundary"].format(
reaches=", ".join(missing)
)
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
self._export_donnee(study, repertory, qlog, name=name) self._export_donnee(study, repertory, qlog, name=name)
self._export_ts(study, repertory, qlog, name=name) if trased:
if not self._export_ts(study, repertory, qlog, name=name):
return False
self._export_geomac_i(study, repertory, qlog, name=name) self._export_geomac_i(study, repertory, qlog, name=name)
self._export_mail(study, repertory, qlog, name=name) self._export_mail(study, repertory, qlog, name=name)
self._export_devers(study, repertory, qlog, name=name) self._export_devers(study, repertory, qlog, name=name)
@@ -153,108 +183,247 @@ class Rubar3(CommandLineSolver):
return True return True
def _export_donnee(self, study, repertory, qlog, name="0"): # Notice Rubar3/RubarBE, annex 1: records are positional, not keyed.
# Each record starts with an A50 label, including records of integers.
_donnee_fields = (
("cfl", "real"),
("condam", "integer"), ("condav", "integer"),
("regime", "integer"), ("iodev", "flags"),
("iopdt", "flag"), ("iovis", "flag"), ("rep", "flag"),
("tinit", "time"), ("tmax", "time"), ("tiopdt", "time"),
("dt", "real"), ("ts", "time"), ("dtsauv", "time"),
("psave", "time"), ("fdeb1", "real"), ("fdeb2", "real"),
("fdeb3", "real"),
("tf_1", "flag"), ("tf_2", "flag"), ("tf_3", "flag"),
("tf_4", "flag"), ("tf_5", "flag"), ("tf_6", "flag"),
("trased", "flag"),
)
_ts_fields = (
("ros", "real"), ("por", "real"), ("dcharg", "real"),
("halfa", "real"), ("mult_1", "multipliers"),
("visc", "visc"), ("opts", "law"), ("odchar", "integer"),
("unisol", "integer"), ("typdef", "integer"),
("depot", "depot"), ("choixc", "integer"),
("option", "options"), ("capsol", "integer"),
("bmiu", "real"), ("demix", "integer"),
("defond", "integer"), ("varcons", "integer"),
)
# rubar_init.f90 (inits): laws whose critical stress may come from the
# Shields diagram, and laws without critical stress
_shields_laws = {1, 10, 11, 12, 14, 25, 26}
_no_threshold_laws = {2, 3, 8, 13, 18, 19, 22, 23, 24}
_brownlie_law = 16
@classmethod
def tcadim_required(cls, opts, option):
"""True if RubarBE reads the dimensionless critical stress TCADIM
for transport law OPTS and options OPTION (notice §3.10)"""
try:
law = int(opts)
except ValueError:
return False
shields = len(option) == 3 and option[1] != "1"
if shields and law in cls._shields_laws:
return False
if law in cls._no_threshold_laws or law == cls._brownlie_law:
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}
@staticmethod
def _required(values, key):
value = values.get(key, "")
if not value:
raise ValueError(f"Missing Rubar parameter: {key}")
return value
@staticmethod
def _flag(value, key, mixed=False):
result = value.upper()
if result == "Y":
result = "O"
if result not in (("O", "N", "M") if mixed else ("O", "N")):
raise ValueError(f"{key}: expected O/Y or N" +
(" or M" if mixed else ""))
return result
def _parameter_flag(self, study, suffix):
key = "rubarbe_" + suffix
return self._flag(self._required(self._parameter_values(study), key),
key)
@staticmethod
def _integer(value, key, width=1):
if not value.isascii() or not value.isdigit() or len(value) > width:
raise ValueError(f"{key}: expected an integer in {width} columns")
return value.rjust(width)
@staticmethod
def _real(value, key, scientific=False):
try:
number = Decimal(value.replace("d", "E").replace("D", "E"))
except InvalidOperation:
raise ValueError(f"{key}: invalid real value {value!r}") from None
if not number.is_finite():
raise ValueError(f"{key}: expected a finite real value")
if scientific:
# DONNEE: E12.5. Reject overflow instead of shifting columns.
floating = float(number)
if not math.isfinite(floating) or (number and floating == 0):
raise ValueError(f"{key}: real value outside E12.5 range")
result = f"{floating:12.5E}"
width = 12
else:
# TS: F10.0 input accepts an explicit decimal point/exponent.
# Preserve the value exactly; do not round porosity etc. to int.
with localcontext() as context:
context.prec = max(28, len(number.as_tuple().digits))
number = number.normalize()
result = str(number).upper()
if len(result) > 10:
result = format(number, "E")
width = 10
if len(result) > width:
raise ValueError(f"{key}: value {value!r} exceeds {width} columns")
return result.rjust(width)
@staticmethod
def _write_parameters(repertory, filename, lines):
# Validate all records before opening/truncating the destination.
path = os.path.join(repertory, filename)
with open(path, "w", encoding="ascii") as f:
f.writelines(lines)
def _export_donnee(self, study, repertory, qlog=None, name="0"):
values = self._parameter_values(study)
fields = list(self._donnee_fields)
# The diagnostic transport-capacity extension is optional as a block.
if values.get("rubarbe_optfpc", ""):
fields += [("optfpc", "integer"), ("ros", "real"),
("dm", "real"), ("segma", "real")]
elif any(values.get("rubarbe_" + key, "")
for key in ("ros", "dm", "segma")):
raise ValueError("rubarbe_optfpc: required before ROS/DM/SEGMA")
lines = []
for suffix, kind in fields:
key = "rubarbe_" + suffix
value = self._required(values, key)
if kind == "integer":
result = self._integer(value, key)
elif kind in ("flag", "flags"):
result = self._flag(value, key, mixed=suffix == "iopdt")
if kind == "flags":
# Missing optional flags mean O (notice section 3.1).
# Write both explicitly so STOCKAGE cannot shift left.
for extra in ("iodebord", "iostockage"):
extra_key = "rubarbe_" + extra
result += self._flag(values.get(extra_key) or "O",
extra_key)
else:
if kind == "time" and ":" in value:
parts = value.split(":")
if len(parts) != 4 or not all(p.isdigit() for p in parts):
raise ValueError(
f"{key}: expected days:hours:minutes:seconds")
value = str(old_pamhyr_date_to_timestamp(value))
result = self._real(value, key, scientific=True)
lines.append(f"{key:<50}{result}\n")
if qlog is not None: if qlog is not None:
qlog.put("Export DONNEE file") qlog.put("Export DONNEE file")
self._write_parameters(repertory, f"donnee.{name}", lines)
with open( def _export_ts(self, study, repertory, qlog=None, name="0"):
os.path.join( trad = RunSolverTranslate()
repertory, f"donnee.{name}" values = self._parameter_values(study)
), "w+" prefix = "rubarbe_sediment_"
) as f: fields = list(self._ts_fields)
params = filter( demix = self._required(values, prefix + "demix")
lambda p: "rubarbe_sediment_" not in p.name, if demix not in ("0", "1"):
study.river.get_params(self._type).parameters raise ValueError(f"{prefix}demix: expected 0 or 1")
) if demix == "1":
fields += [("dchard", "real"), ("dchars", "real")]
it = iter(params) lines = []
for suffix, kind in fields:
param = next(it, None) key = prefix + suffix
while param is not None: value = self._required(values, key)
name = param.name if kind == "real":
value = param.value result = self._real(value, key)
elif kind == "visc":
if "all_" in name: # Notice annex 1: TCADIM then VISC, read as (A50, A20) then
continue # two F10.0. A single value would be read as TCADIM, with
# VISC = 0 (rubar_init.f90, inits).
if value != "": if Decimal(self._real(value, key)) <= 0:
# Value format raise ValueError(f"{key}: expected a positive viscosity")
if value.count(':') == 3: tcadim_key = prefix + "tcadim"
value = old_pamhyr_date_to_timestamp(value) tcadim = values.get(tcadim_key, "")
value = f"{value:>12.5e}".upper() if tcadim:
result = self._real(tcadim, tcadim_key)
if value.count('.') == 1: elif self.tcadim_required(values.get(prefix + "opts", ""),
value = f"{float(value):>12.5e}".upper() values.get(prefix + "option", "")):
message = trad["missing_tcadim"].format(key=tcadim_key)
if value == "y" or value == "n": logger.warning(message)
value = "O" if value == "y" else "N" if qlog is not None:
qlog.put(trad["warning"].format(message=message))
# Write value return False
f.write(f"{name:<50}{value}") else:
result = " " * 10
# Add values of 'rubarbe_iodebord' and result += self._real(value, key)
# 'rubarbe_iostockage' elif kind == "multipliers":
if name == "rubarbe_iodev": result = self._real(value, key)
v2 = next(it).value extras = [values.get(prefix + f"mult_{i}", "")
v3 = next(it).value for i in range(2, 6)]
# Preserve empty interior F10.0 fields; omit trailing fields
# f.write(f"{v2}{v3}") # to retain the solver's defaults for optional coefficients.
while extras and not extras[-1]:
# New line extras.pop()
f.write(f"\n") for i, extra in enumerate(extras, 2):
result += (self._real(extra, prefix + f"mult_{i}")
param = next(it, None) if extra else " " * 10)
elif kind == "options":
def _export_ts(self, study, repertory, qlog, name="0"): if len(value) != 3 or any(c not in "12" for c in value):
message = trad["invalid_options"].format(key=key)
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
result = value
elif kind == "law":
# Notice section 3.10 / 8.7: 4--7 are not listed.
if value not in {str(i) for i in [1, 2, 3, *range(8, 31)]}:
message = trad["unsupported_transport_law"].format(
key=key, value=value)
logger.warning(message)
if qlog is not None:
qlog.put(trad["warning"].format(message=message))
return False
result = self._integer(value, key, width=2)
else:
result = self._integer(value, key,
width=10 if kind == "depot" else 1)
lines.append(f"{key:<50}{result}\n")
if qlog is not None: if qlog is not None:
qlog.put("Export TS file") qlog.put("Export TS file")
self._write_parameters(repertory, f"ts.{name}", lines)
with open( return True
os.path.join(
repertory, f"ts.{name}"
), "w+"
) as f:
def float_format(string):
if "." in string:
return f"{float(string):>10.0f}"
return ""
params = filter(
lambda p: "rubarbe_sediment_" in p.name,
study.river.get_params(self.type).parameters
)
it = iter(params)
line = 0
while line < 20:
param = next(it)
name = param.name
value = param.value
if value != "":
# Value format
if value.count('.') == 1:
value = f"{float_format(value)}"
else:
value = f"{value:>10}"
# Write value
f.write(f"{name:<50}{value}")
# Add values of 'rubarbe_iodebord' and
# 'rubarbe_iostockage'
if name == "rubarbe_sediment_mult_1":
m2 = f"{float_format(next(it).value)}"
m3 = f"{float_format(next(it).value)}"
m4 = f"{float_format(next(it).value)}"
m5 = f"{float_format(next(it).value)}"
f.write(f"{m2}{m3}{m4}{m5}")
# New line
f.write(f"\n")
line += 1
def _export_geomac_i(self, study, repertory, qlog, name="0"): def _export_geomac_i(self, study, repertory, qlog, name="0"):
if qlog is not None: if qlog is not None:
@@ -616,6 +785,7 @@ class Rubar3(CommandLineSolver):
)).value )).value
if timestamp.count(':') == 3: if timestamp.count(':') == 3:
timestamp = old_pamhyr_date_to_timestamp(timestamp) timestamp = old_pamhyr_date_to_timestamp(timestamp)
timestamp = float(timestamp)
ts.add(timestamp) ts.add(timestamp)
# add initial condition # add initial condition
@@ -636,8 +806,7 @@ class Rubar3(CommandLineSolver):
while True: while True:
line = f.readline() line = f.readline()
if line == "": if line == "":
results.set("timestamps", ts) break
return
timestamp = float(line) timestamp = float(line)
ts.add(timestamp) ts.add(timestamp)
@@ -671,6 +840,24 @@ class Rubar3(CommandLineSolver):
h, s, q, z = read_data_line(f) 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)
# Tables and plots use timestamp rows and global profile columns.
timestamps = sorted(ts)
profiles = []
for reach, _ in reachs:
offset = len(profiles)
profiles.extend(reach.profiles)
reach.set_global_index(range(offset, len(profiles)))
results.set("timestamps", timestamps)
results.compute_timestamp_index()
results.set("table", {
key: np.array([
[profile.get_ts_key(t, key) for profile in profiles]
for t in timestamps
], dtype=float)
for key in ("Z", "Q", "V")
})
@timer @timer
def write_bin(self, study, fname, results, qlog=None, name="0"): def write_bin(self, study, fname, results, qlog=None, name="0"):
logger.info(f"write_bin: Start writing '{fname}' ...") logger.info(f"write_bin: Start writing '{fname}' ...")
@@ -800,3 +987,14 @@ class RubarBE(Rubar3):
self._cmd_input = "" self._cmd_input = ""
self._cmd_solver = "@path @args @input" self._cmd_solver = "@path @args @input"
self._cmd_output = "" self._cmd_output = ""
@classmethod
def default_parameters(cls):
# New BE studies evolve the bed. Keep Rubar3 defaults unchanged.
overrides = {
"rubarbe_trased": "y",
"rubarbe_sediment_opts": "1", # Meyer-Peter and Mueller
"rubarbe_sediment_option": "212", # no Ikeda, given Shields, total
}
return [(key, overrides.get(key, value))
for key, value in super().default_parameters()]
+2 -2
View File
@@ -35,7 +35,7 @@ solver_long_name = {
# "mage_fake7": "Mage fake v7", # "mage_fake7": "Mage fake v7",
"adistswc": "Adis-TS_WC", "adistswc": "Adis-TS_WC",
"adisttwc": "Adis-TT_WC", "adisttwc": "Adis-TT_WC",
# "rubarbe": "RubarBE", "rubarbe": "RubarBE",
"rubar3": "Rubar3", "rubar3": "Rubar3",
} }
@@ -46,6 +46,6 @@ solver_type_list = {
# "mage_fake7": MageFake7, # "mage_fake7": MageFake7,
"adistswc": AdisTSwc, "adistswc": AdisTSwc,
"adisttwc": AdisTTwc, "adisttwc": AdisTTwc,
# "rubarbe": RubarBE, "rubarbe": RubarBE,
"rubar3": Rubar3, "rubar3": Rubar3,
} }
+61
View File
@@ -0,0 +1,61 @@
# 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["missing_solid_boundary"] = _translate(
"Solver",
"Export cancelled: sediment transport (TRASED) is enabled but "
"no solid boundary condition is defined at the upstream node "
"of reach(es): {reaches}. Add a solid boundary condition, or "
"disable sediment transport in the solver parameters."
)
self._dict["export_failed"] = _translate(
"Solver", "Export failed; task cancelled"
)
+355
View File
@@ -0,0 +1,355 @@
"""Rubar parameter descriptions (notice sections 3.1, 3.10 and annex 1).
Keys and values keep their historical database representation.
"""
from dataclasses import dataclass
from decimal import Decimal, DecimalException, InvalidOperation
from Solver.RubarBE import Rubar3
GROUPS = ('Temps et calcul', 'Hydraulique', 'Transport solide',
'Évolution du fond', 'Sauvegardes')
YES_NO = {'y': 'Oui', 'n': 'Non'}
ON_OFF = {'1': 'Oui', '0': 'Non'}
LAWS = {
'1': 'Meyer-Peter et Müller', '2': 'Engelund et Hansen', '3': 'Bagnold',
'8': 'Ackers et White (1973)', '9': 'Smart et Jaeggi', '10': 'van Rijn',
'11': 'Rickenmann', '12': 'Camenen et Larson', '13': 'Schoklitsch',
'14': 'Sato, Hikkawa et Ashida', '15': 'Recking et al. (2008)',
'16': 'Brownlie', '17': 'Laursen', '18': 'Ackers et White (1990)',
'19': 'Karim', '20': 'Karim et Kennedy', '21': 'Yang et Lim (2003)',
'22': 'Yang (1984)', '23': 'Yang (1979)', '24': 'Yang (1973)',
'25': 'van Rijn (1984) — suspension', '26': 'van Rijn (1984) — charriage',
'27': 'Recking et al. (2016)', '28': 'Piton et al. (2017)',
'29': 'Lefort (2007)', '30': 'Lefort (2015)',
}
OPTION_CHOICES = (
('Correction de pente',
{'1': 'Correction d’Ikeda', '2': 'Sans correction'},
'Permet d\'introduire que la répartition de la contrainte critique dans '
'une section en travers est influencée par la pente transversale'),
('Seuil critique', {'1': 'Fourni par l’utilisateur',
'2': 'Calculé par le diagramme de Shields'},
'Permet de choisir pour certaines formules de capacité, entre un seuil '
'critique fourni par l’utilisateur ou '
'un seuil critique calculé par le diagramme de Shields.'),
('Contrainte utilisée', {'1': 'Contrainte efficace',
'2': 'Contrainte non réduite'},
'Permet de choisir pour certaines formules de capacité, entre la '
'contrainte efficace calculée ou la contrainte non réduite.'),
)
@dataclass(frozen=True)
class Field:
key: str
group: int
label: str
help: str
kind: str = 'real'
choices: dict = None
minimum: float = 0
maximum: float = None
positive: bool = False
optional: bool = False
def fields():
"""Describe historical Rubar fields without changing their defaults."""
result = []
def add(key, group, label, help, **kwargs):
result.append(Field('rubarbe_' + key, group, label, help, **kwargs))
def choice(key, group, label, help, choices):
add(key, group, label, help, kind='choice', choices=choices)
add('cfl', 0, 'Nombre de Courant (CFL)',
'Limite supérieure du nombre de Courant, afin d\'assurer la stabilité '
'du schéma numérique ; comprise entre 0 et 1.',
positive=True, maximum=1)
for key, label, help in (
('tinit', 'Début du calcul',
'Doit correspondre aux conditions initiales.'),
('tmax', 'Fin du calcul', 'Doit être postérieure au début du calcul.'),
):
add(key, 0, label + ' (j:h:min:s ou s)', help, kind='time')
choice('iopdt', 0, 'Gestion du pas de temps',
'En mode mixte, le pas reste fixe jusqu’à la date de bascule.',
{'y': 'Automatique', 'n': 'Fixe',
'm': 'Mixte : fixe puis automatique'})
add('dt', 0, 'Pas de temps initial ou fixe (s)',
'Premier pas en mode automatique, pas demandé en mode fixe.',
positive=True)
add('tiopdt', 0, 'Fin de la période à pas fixe' + ' (j:h:min:s ou s)',
'Utilisée uniquement avec un pas de temps mixte.', kind='time')
choice('rep', 0, 'Reprendre un calcul',
'Nécessite les fichiers de reprise du calcul précédent.', YES_NO)
choice('condam', 1, 'Condition à la limite amont',
'Le choix doit correspondre aux données de la limite amont.',
{'0': 'Réflexion', '1': 'Débit imposé Q(t)',
'2': 'Cote imposée Z(t)', '4': 'Confluence'})
choice('condav', 1, 'Condition à la limite aval',
'Le choix doit correspondre aux données de la limite aval.',
{'0': 'Sortie libre', '1': 'Courbe de tarage Q(Z)',
'2': 'Réflexion',
'3': 'Cote imposée Z(t)', '4': 'Confluence / défluence',
'5': 'Régime uniforme', '7': 'Régime critique'})
choice('regime', 1, 'Imposer aussi le tirant d’eau amont',
'Complète le débit imposé ; incompatible avec une limite '
'amont de type "Réflexion".',
ON_OFF)
choice('iodev', 1, 'Déversement latéral',
'Nécessite les données de déversements latéraux.',
YES_NO)
for key, label, help in (
('iodebord', 'Formule Debord',
'Oui: utilise la formule Debord pour les lits composés.\nNon:'
' pas de perte de charge complémentaire entre lit mineur et moyen.'),
('iostockage', 'Stockage dans le lit majeur',
'Traite le lit majeur sous la berge la plus basse comme zone '
'de stockage.'),
):
choice(key, 1, label, help, YES_NO)
choice('optfpc', 1, 'Calcul du frottement',
'Strickler fourni ou frottement calculé à partir des sédiments.',
{'0': 'Coefficients de Strickler fournis', '1': 'Brownlie (1983)',
'2': 'Griffith (1981) - pour un fond mobile', '3': 'Karim (1995)',
'4': 'Wu et Wang (1999)', '5': 'Yu et Lim (2003)',
'6': 'Recking et al. (2008)', '7': 'Garde et Raju (1966)'})
for key, label in (
('ros', 'Masse volumique des sédiments (kg/m³)'),
('dm', 'Diamètre médian des sédiments (m)'),
('segma', 'Étendue granulométrique des sédiments (—)'),
):
add(key, 1, label,
'Utilisé pour le frottement calculé sans transport solide '
'(disponible uniquement si le transport solide est désactivé '
'et que le calcul du frottement se fait avec les sédiments).',
positive=True)
choice('trased', 2, 'Activer le transport solide',
'Fonction de RubarBE. L’évolution du fond se règle séparément.',
YES_NO)
add('sediment_ros', 2, 'Masse volumique des sédiments (kg/m³)',
'Masse volumique des grains.', positive=True)
add('sediment_por', 2, 'Porosité des sédiments (—)',
'Fraction du volume occupée par les vides : de 0 inclus à 1 exclu.',
positive=True, maximum=1)
choice('sediment_opts', 2, 'Loi de capacité solide',
'Formule de capacité solide ; choisir selon le domaine '
'physique.',
LAWS)
choice('sediment_unisol', 2, 'Unité des débits ou concentrations solides',
'Unité des débits ou concentrations solides.',
{'1': 'Débit massique (kg/s)', '2': 'Concentration (kg/m³)'})
choice('sediment_odchar', 2, 'Distance de chargement',
'Option de calcul de la ditance de chargement.',
{'0': 'Distance constante', '1': 'Loi de Han', '2': 'Wu et Wang'})
add('sediment_dcharg', 2, 'Distance de chargement constante (m)',
'Utilisée uniquement si la distance de chargement est constante.')
add('sediment_halfa', 2, 'Paramètre alpha de Han (m)',
'Utilisé uniquement avec la loi de chargement de Han.')
add('sediment_option', 2, 'Calcul de la contrainte critique',
'Trois choix indépendants, enregistrés ensemble dans OPTION.',
kind='options')
add('sediment_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)
for i in range(1, 4):
add(f'fdeb{i}', 4, f'Seuil de débit du front {i} (m³/s)',
'Débit pour lequel à chaque abscisse, est enregistré le temps '
'de premier dépassement (fichier TRAJEC si demandé).',
positive=True)
choice('iovis', 4, 'Sorties complémentaires',
'Largeurs et sorties complémentaires hydrauliques et '
'sédimentaires.',
YES_NO)
return {field.key: field for field in result}
FIELDS = fields()
def canonical(field, value):
value = str(value).strip()
if field.choices and 'y' in field.choices:
aliases = {'o': 'y', 'y': 'y', 'n': 'n', 'm': 'm'}
return aliases.get(value.lower(), value)
return value
def validate(field, value):
"""Return a storage value or raise a user-facing validation error."""
value = canonical(field, value)
if field.kind == 'choice':
if value not in field.choices:
raise ValueError('Choisissez une valeur dans la liste.')
return value
if field.kind == 'options':
if len(value) != 3 or any(c not in '12' for c in value):
raise ValueError('Renseignez les trois choix de contrainte.')
return value
if field.kind == 'text':
return value
if not value and field.optional:
return value
value = value.replace(',', '.').replace('d', 'E').replace('D', 'E')
try:
if field.kind == 'time' and ':' in value:
parts = value.split(':')
valid = all(p.isascii() and p.isdigit() for p in parts)
if len(parts) != 4 or not valid:
raise ValueError(
'Utilisez j:h:min:s ou un nombre de secondes.')
number = sum(Decimal(p) * factor for p, factor in
zip(parts, (86400, 3600, 60, 1)))
else:
number = Decimal(value)
except InvalidOperation:
raise ValueError('Saisissez un nombre fini.') from None
if not number.is_finite():
raise ValueError('Saisissez un nombre fini.')
if number < field.minimum or (field.positive and number == 0):
raise ValueError('La valeur doit être strictement positive.'
if field.positive else
'La valeur doit être positive ou nulle.')
if field.maximum is not None and number > field.maximum:
raise ValueError(f'La valeur ne doit pas dépasser {field.maximum}.')
if field.key == 'rubarbe_sediment_por' and number >= 1:
raise ValueError('La porosité doit être inférieure à 1.')
scientific = not field.key.startswith('rubarbe_sediment_')
try:
Rubar3._real(str(number), field.key, scientific=scientific)
except (ValueError, DecimalException, OverflowError):
raise ValueError(
'Valeur trop longue ou hors plage pour le format du solveur '
'(12 colonnes).' if scientific else
'Valeur trop précise ou hors plage pour le format du solveur '
'(10 colonnes).') from None
return value
def active(field, values):
"""Dependencies only disable editing; stored values are never erased."""
key = field.key.removeprefix('rubarbe_')
transport = values.get('rubarbe_trased', 'n').lower() in ('y', 'o')
if key.startswith('sediment_') and not transport:
return False
if key == 'tiopdt':
return values.get('rubarbe_iopdt', '').lower() == 'm'
if key == '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')
return True
+361
View File
@@ -0,0 +1,361 @@
"""Grouped Rubar parameter editor, backed by the existing parameter list."""
from functools import partial
from PyQt5.QtCore import QCoreApplication, QEvent, Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QComboBox, QFormLayout, QLabel, QLineEdit, QScrollArea,
QSizePolicy, QTabWidget, QVBoxLayout, QWidget,
)
from View.SolverParameters.RubarFields import (
FIELDS, GROUPS, OPTION_CHOICES, Field, active, canonical, validate,
)
from View.SolverParameters.UndoCommand import SetCommand
def tr(text):
return QCoreApplication.translate('RubarParameters', text)
class RubarForm(QWidget):
def __init__(self, parameters, undo, editable=True, parent=None):
super().__init__(parent)
self.parameters = parameters
self.stack = undo
self.editable = editable
self.editors = {}
self.labels = {}
self.errors = {}
self.descriptions = dict(FIELDS)
self.indices = {p.name: i for i, p in enumerate(parameters.parameters)}
self._refreshing = False
self._options_pending = False
layout = QVBoxLayout(self)
intro = QLabel(tr('Les valeurs valides sont appliquées à l’étude. '
'Pensez à enregistrer l’étude pour les conserver.'))
intro.setWordWrap(True)
layout.addWidget(intro)
self.notice = QLabel()
self.notice.setWordWrap(True)
self.notice.setTextFormat(Qt.PlainText)
layout.addWidget(self.notice)
self.tabs = QTabWidget()
layout.addWidget(self.tabs)
forms = []
for group in GROUPS:
page = QWidget()
form = QFormLayout(page)
form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
form.setRowWrapPolicy(QFormLayout.WrapLongRows)
form.setVerticalSpacing(12)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(page)
self.tabs.addTab(scroll, tr(group))
forms.append(form)
# Metadata order is stable even when stored parameter order changes.
keys = [key for key in FIELDS if key in self.indices]
keys += [key for key in self.indices if key not in FIELDS]
for key in keys:
field = self.descriptions.setdefault(key, Field(
key, 0, key, 'Paramètre supplémentaire conservé tel quel.',
kind='text'))
self._add_field(field, forms[field.group])
self.help = QLabel(tr(
'Sélectionnez un réglage pour afficher son aide.'))
self.help.setWordWrap(True)
self.help.setTextFormat(Qt.PlainText)
self.help.setMinimumHeight(60)
layout.addWidget(self.help)
self.stack.indexChanged.connect(self.refresh)
self.refresh()
def _add_field(self, field, form):
key = field.key
container = QWidget()
box = QVBoxLayout(container)
box.setContentsMargins(0, 0, 0, 0)
if field.kind == 'options':
editor = QWidget()
subform = QFormLayout(editor)
subform.setContentsMargins(0, 0, 0, 0)
self.option_editors = []
for title, choices, tooltip in OPTION_CHOICES:
combo = QComboBox()
combo.setSizeAdjustPolicy(
QComboBox.AdjustToMinimumContentsLengthWithIcon)
combo.addItem(tr('À renseigner'), None)
for value, label in choices.items():
combo.addItem(tr(label), value)
combo.currentIndexChanged.connect(self._commit_options)
combo.setProperty('parameter', key)
combo.installEventFilter(self)
combo.setToolTip(tr(tooltip) + '\n' + key)
subform.addRow(tr(title), combo)
self.option_editors.append(combo)
elif field.kind == 'choice':
editor = QComboBox()
editor.setSizeAdjustPolicy(
QComboBox.AdjustToMinimumContentsLengthWithIcon)
for value, label in field.choices.items():
editor.addItem(tr(label), value)
editor.currentIndexChanged.connect(
partial(self._choice_changed, key))
else:
editor = QLineEdit()
editor.setPlaceholderText(tr('Valeur du solveur') if field.optional
else tr('Saisir une valeur'))
editor.textEdited.connect(partial(self._check_draft, key))
editor.editingFinished.connect(partial(self._finish_text, key))
editor.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
editor.setObjectName(key)
editor.setProperty('parameter', key)
editor.installEventFilter(self)
editor.setToolTip(
tr(field.help) + '\n' + key + '\nNotice §3.1 / §3.10')
box.addWidget(editor)
error = QLabel()
error.setWordWrap(True)
error.setTextFormat(Qt.PlainText)
error.setStyleSheet('color: #b42318;')
box.addWidget(error)
label = QLabel(tr(field.label))
label.setWordWrap(True)
label.setFixedWidth(310)
label.setMinimumHeight(label.heightForWidth(310))
label.setBuddy(editor)
label.setToolTip(editor.toolTip())
form.addRow(label, container)
self.editors[key] = editor
self.labels[key] = label
self.errors[key] = error
def eventFilter(self, watched, event):
if event.type() in (QEvent.FocusIn, QEvent.Enter):
key = watched.property('parameter')
if key:
field = self.descriptions[key]
self.help.setText(tr(field.help) + '\n' + key)
# Use the model undo stack even inside a line editor.
if event.type() in (QEvent.ShortcutOverride, QEvent.KeyPress):
if (event.matches(QKeySequence.Undo) or
event.matches(QKeySequence.Redo)):
if event.type() == QEvent.KeyPress:
if event.matches(QKeySequence.Undo):
self.undo()
else:
self.redo()
event.accept()
return True
return super().eventFilter(watched, event)
def values(self):
return {p.name: str(p.value) for p in self.parameters.parameters}
def _error(self, key, message):
self.errors[key].setText(message)
self.errors[key].setVisible(bool(message))
self.editors[key].setStyleSheet(
'QLineEdit { border: 1px solid #b42318; }' if message else '')
def _validated(self, key, value):
try:
value = validate(self.descriptions[key], value)
except ValueError as error:
self._error(key, tr(str(error)) + tr(' Valeur non enregistrée.'))
return None
self._error(key, '')
return value
def _check_draft(self, key, value):
self._validated(key, value)
def _finish_text(self, key):
editor = self.editors[key]
if editor.isModified():
if self.commit(key, editor.text()):
editor.setModified(False)
def _choice_changed(self, key, index):
if not self._refreshing:
value = self.editors[key].itemData(index)
if value is not None:
self.commit(key, value)
def _commit_options(self):
if self._refreshing:
return
values = [combo.currentData() for combo in self.option_editors]
key = 'rubarbe_sediment_option'
if any(value is None for value in values):
self._options_pending = True
self._error(key, tr(
'Renseignez les trois choix. Valeur non enregistrée.'))
else:
self._options_pending = False
self.commit(key, ''.join(values))
def apply_pending(self):
"""Commit focused text before Ctrl+S; leave invalid drafts visible."""
values = self.values()
if (values.get('rubarbe_condam') == '0' and
values.get('rubarbe_regime') == '1'):
self.tabs.setCurrentIndex(1)
self.editors['rubarbe_condam'].setFocus()
return False
for key, editor in self.editors.items():
if (isinstance(editor, QLineEdit) and editor.isModified() and
active(self.descriptions[key], self.values())):
if not self.commit(key, editor.text()):
self.tabs.setCurrentIndex(self.descriptions[key].group)
editor.setFocus()
return False
if (self._options_pending and
active(FIELDS['rubarbe_sediment_option'], self.values())):
self.tabs.setCurrentIndex(2)
self.option_editors[0].setFocus()
return False
return True
def commit(self, key, value):
field = self.descriptions[key]
if not self.editable or not active(field, self.values()):
return False
value = self._validated(key, value)
if value is None:
return False
values = self.values()
values[key] = value
if (key in ('rubarbe_condam', 'rubarbe_regime') and
values.get('rubarbe_condam') == '0' and
values.get('rubarbe_regime') == '1'):
self.refresh()
return False
if isinstance(self.editors[key], QLineEdit):
self.editors[key].setModified(False)
if self.values()[key] != value:
self.stack.push(SetCommand(self.parameters, self.indices[key],
'value', value))
elif isinstance(self.editors[key], QLineEdit):
self.editors[key].setText(value)
return True
def refresh(self, *args):
self._refreshing = True
values = self.values()
for key, editor in self.editors.items():
field = self.descriptions[key]
value = canonical(field, values[key])
enabled = active(field, values)
editor.setEnabled(self.editable and enabled)
self.labels[key].setEnabled(enabled)
if isinstance(editor, QLineEdit) and editor.isModified():
self._validated(key, editor.text())
continue
if field.kind == 'options' and self._options_pending:
self.errors[key].setVisible(enabled)
continue
if field.kind == 'options':
valid = len(value) == 3 and all(c in '12' for c in value)
for i, combo in enumerate(self.option_editors):
index = combo.findData(value[i]) if valid else 0
combo.setCurrentIndex(index)
elif isinstance(editor, QComboBox):
# Display unknown historical values without replacing them.
for i in reversed(range(editor.count())):
if editor.itemData(i) is None:
editor.removeItem(i)
index = editor.findData(value)
if index < 0:
editor.addItem(tr('Valeur non reconnue : ') + value, None)
index = editor.count() - 1
editor.setCurrentIndex(index)
else:
editor.setText(values[key])
try:
validate(field, values[key])
self._error(key, '')
except ValueError as error:
self._error(key, tr('Valeur enregistrée à vérifier : ') +
values[key] + ' — ' + tr(str(error)))
self.errors[key].setVisible(
enabled and bool(self.errors[key].text()))
self._update_context(values)
for key, forbidden, blocked in (
('rubarbe_regime', '1', values.get('rubarbe_condam') == '0'),
('rubarbe_condam', '0', values.get('rubarbe_regime') == '1'),
):
editor = self.editors.get(key)
if editor is None:
continue
index = editor.findData(forbidden)
if index >= 0:
editor.model().item(index).setEnabled(not blocked)
self._refreshing = False
def _update_context(self, values):
warnings = []
if not self.editable:
warnings.append(tr('Étude en lecture seule.'))
transport = values.get('rubarbe_trased', '').lower() in ('y', 'o')
if not transport:
warnings.append(tr('Transport solide désactivé : les réglages '
'sédimentaires sont conservés mais inactifs.'))
elif (values.get('rubarbe_sediment_capsol') in ('2', '3') and
values.get('rubarbe_sediment_choixc') == '2'):
warnings.append(tr(
'La capacité solide choisie nécessite une méthode MPC ; '
'la répartition uniforme est incompatible.'))
if (values.get('rubarbe_condam') == '0' and
values.get('rubarbe_regime') == '1'):
warnings.append(tr('Une limite amont réfléchissante ne permet pas '
'd’imposer aussi le tirant d’eau.'))
try:
from tools import old_pamhyr_date_to_timestamp
def seconds(value):
return (old_pamhyr_date_to_timestamp(value) if ':' in value
else float(value.replace(',', '.')))
start = seconds(values['rubarbe_tinit'])
if seconds(values['rubarbe_tmax']) <= start:
warnings.append(tr('La fin du calcul doit suivre son début.'))
except (KeyError, ValueError):
pass
if transport:
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.UndoCommand import *
from View.SolverParameters.Table import TableModel, ValueDelegate from View.SolverParameters.Table import TableModel, ValueDelegate
from View.SolverParameters.RubarForm import RubarForm
from View.SolverParameters.translate import ParamTranslate from View.SolverParameters.translate import ParamTranslate
from Solver.Solvers import solver_long_name, solver_type_list from Solver.Solvers import solver_long_name, solver_type_list
@@ -62,7 +63,7 @@ class SolverParametersWindow(PamhyrWindow):
title=name, title=name,
study=study, study=study,
config=config, config=config,
options=['copy'], options=['undo', 'copy'],
trad=trad, trad=trad,
parent=parent parent=parent
) )
@@ -88,6 +89,15 @@ class SolverParametersWindow(PamhyrWindow):
editable_headers = [] editable_headers = []
for st in solver_type_list: 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 # Create widgets
widget = QWidget() widget = QWidget()
grid = QGridLayout() grid = QGridLayout()
@@ -135,8 +145,19 @@ class SolverParametersWindow(PamhyrWindow):
tab = self.current_tab() tab = self.current_tab()
self._table[tab].redo() 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): def _copy(self):
logger.info("TODO: copy") editor = self.focusWidget()
if isinstance(editor, QLineEdit):
editor.copy()
def _paste(self): def _paste(self):
logger.info("TODO: paste") editor = self.focusWidget()
if isinstance(editor, QLineEdit) and self._study.is_editable():
editor.paste()
+2 -2
View File
@@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>900</width> <width>1000</width>
<height>480</height> <height>680</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
+26 -5
View File
@@ -37,7 +37,7 @@ logger = logging.getLogger()
class Config(SQL): class Config(SQL):
def __init__(self): def __init__(self):
self._version = '0.0.9' self._version = '0.0.10'
self.filename = Config.filename() self.filename = Config.filename()
self.set_default_value() self.set_default_value()
@@ -220,6 +220,26 @@ class Config(SQL):
"VALUES ('display_manning', 'False')" "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( self.execute(
f"UPDATE info SET value='{self._version}' " + f"UPDATE info SET value='{self._version}' " +
"WHERE key='version'" "WHERE key='version'"
@@ -416,11 +436,12 @@ class Config(SQL):
new._cmd_solver = f""""{path}" @args @input""" new._cmd_solver = f""""{path}" @args @input"""
self._solvers.append(new) self._solvers.append(new)
# Rubar3 # RubarBE (same executable as Rubar3, see notice §10.3)
ctor = solver_type_list["rubar3"] ctor = solver_type_list["rubarbe"]
new = ctor("default-Rubar3") new = ctor("default-RubarBE")
new._description = "Default Pamhyr2 RubarBE version" new._description = "Default Pamhyr2 RubarBE version"
new._cmd_solver = f""""@install_dir/rubar/rubarbe{ext}" @args @input""" path = os.path.join("@install_dir", "rubar", f"rubarbe{ext}")
new._cmd_solver = f""""{path}" @args @input"""
self._solvers.append(new) self._solvers.append(new)
# Mage fake 7 # Mage fake 7