From ef72e41b148e153ec91aff38f448e8bd98281d0a Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Wed, 21 Oct 2026 14:56:44 +0200 Subject: [PATCH] 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 --- .../LateralContribution.py | 4 +- src/Solver/RubarBE.py | 108 ++++++++++++++---- src/View/LateralContribution/Edit/Window.py | 3 + .../LateralContribution/Edit/translate.py | 4 + src/View/RunSolver/Translate.py | 34 +++++- 5 files changed, 124 insertions(+), 29 deletions(-) diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index ff418f7d..ebef76f5 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -223,9 +223,7 @@ class LateralContribution(SQLSubModel): self._data = [] self._header = [] self._types = [float, float] - # Grain size of the sediment brought by a solid contribution: - # median diameter (m) and spread sqrt(d84/d16), as for a solid - # boundary condition + self._d50 = 0.002 self._sigma = 1.0 diff --git a/src/Solver/RubarBE.py b/src/Solver/RubarBE.py index c5326085..d7a3f0f3 100644 --- a/src/Solver/RubarBE.py +++ b/src/Solver/RubarBE.py @@ -174,7 +174,8 @@ class Rubar3(CommandLineSolver): hydraulics["warnings"]) saves = self._save_data(values, hydraulics) laterals = self._lateral_data( - study.river, hydraulics, values, hydraulics["warnings"]) + study.river, hydraulics, values, trased, + hydraulics["warnings"]) structures = self._structure_data( study.river, hydraulics, hydraulics["warnings"]) except RubarExportError as error: @@ -546,7 +547,7 @@ class Rubar3(CommandLineSolver): # inflow in each cell of its zone, without checking this size _max_lateral_points = 3000 * 20 - def _lateral_data(self, river, hydraulics, values, warnings): + 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. @@ -557,9 +558,17 @@ class Rubar3(CommandLineSolver): 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. Otherwise, solid and suspension contributions have - no RubarBE equivalent (their sediment cannot be linked to an - inflow): they are refused""" + 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"] @@ -580,40 +589,50 @@ class Rubar3(CommandLineSolver): return kept lcs = on_reach("liquid") - sediment = on_reach("solid") + on_reach("suspenssion") + solids = on_reach("solid") # Not read by RubarBE: the user is warned, the computation runs # without them if not iodev: - for lc in lcs + sediment: + for lc in lcs + solids: warnings.append(trad["lateral_iodev_disabled"].format( name=lc.name, reach=edge.name)) return None - if sediment: - raise RubarExportError(trad["lateral_solid_unsupported"].format( - name=sediment[0].name, reach=edge.name)) + 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) - 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)) + + 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)])) - first, last = (b + 1, e) if b < e else ((min(b + 1, count),) * 2) + 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)) @@ -625,13 +644,56 @@ class Rubar3(CommandLineSolver): 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 blocks) + 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 [block[:3] for block in blocks] + return result ############## # Structures # @@ -1251,13 +1313,15 @@ class Rubar3(CommandLineSolver): if qlog is not None: qlog.put("Export HYDEVS file") lines = [] - for first, last, data in laterals: + for first, last, data, (d50, sigma), length in laterals: if trased: - lines.append(f"{first} {last} {len(data)} 0.0 0.0\n") - lines += [f"{q!r} {t!r} 0.0 {t!r}\n" for t, q in data] + 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] + 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"): diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 3084733a..772be51b 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -62,6 +62,9 @@ class EditLateralContributionWindow(PamhyrWindow): 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 diff --git a/src/View/LateralContribution/Edit/translate.py b/src/View/LateralContribution/Edit/translate.py index 01f9e739..9f893ad6 100644 --- a/src/View/LateralContribution/Edit/translate.py +++ b/src/View/LateralContribution/Edit/translate.py @@ -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)" + ) diff --git a/src/View/RunSolver/Translate.py b/src/View/RunSolver/Translate.py index 0a275143..fde28784 100644 --- a/src/View/RunSolver/Translate.py +++ b/src/View/RunSolver/Translate.py @@ -192,11 +192,37 @@ class RunSolverTranslate(PamhyrTranslate): "Lateral contribution {name} on reach {reach} has no value: it " "is ignored." ) - self._dict["lateral_solid_unsupported"] = _translate( + self._dict["lateral_solid_no_transport"] = _translate( "Solver", - "Export cancelled: solid or suspension lateral contribution " - "{name} on reach {reach}: RubarBE only reads the sediment of a " - "liquid inflow, which Pamhyr2 cannot link to it. Remove it." + "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",