From 9b1fb4e7d24c814ad7be877f4afa837df731a637 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 22 Jul 2026 15:38:52 +0200 Subject: [PATCH] Ensemble: Add ensemble base classes. --- src/Model/Ensembles/Ensemble.py | 176 ++++++++++++++++++++++++++++ src/Model/Ensembles/EnsembleList.py | 57 +++++++++ src/Model/River.py | 11 +- src/Model/Study.py | 2 +- 4 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/Model/Ensembles/Ensemble.py create mode 100644 src/Model/Ensembles/EnsembleList.py diff --git a/src/Model/Ensembles/Ensemble.py b/src/Model/Ensembles/Ensemble.py new file mode 100644 index 00000000..fe851a39 --- /dev/null +++ b/src/Model/Ensembles/Ensemble.py @@ -0,0 +1,176 @@ +# Ensemble.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 . + +# -*- coding: utf-8 -*- + +import struct +import logging +import itertools + +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Scenario import Scenario + +logger = logging.getLogger() + + +class Ensemble(SQLSubModel): + _sub_classes = [] + + def __init__(self, id: int = -1, + name: str = "", + prob_dist="generic", + function="generic", + range=[], + data_pid=-1, + status=None, owner_scenario=-1): + super(Ensemble, self).__init__( + id=id, status=status, + owner_scenario=owner_scenario + ) + + self._name = name + self._type = "generic" + self._prob_dist = prob_dist + self._function = function + self._range = range + self._data_pid = data_pid + + @classmethod + def _db_create(cls, execute, ext=""): + execute(f""" + CREATE TABLE ensemble{ext} ( + {cls.create_db_add_pamhyr_id()}, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + name TEXT NOT NULL, + type VARCHAR(8) NOT NULL, + prob_dist VARCHAR(8) NOT NULL, + function VARCHAR(8) NOT NULL, + range BLOB NOT NULL, + range_len INTEGER NOT NULL, + data_pid INTEGER NOT NULL, + {Scenario.create_db_add_scenario()}, + {Scenario.create_db_add_scenario_fk()}, + PRIMARY KEY(pamhyr_id, scenario) + ) + """) + + if ext == "_tmp": + return True + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version, data=None): + major, minor, release = version.strip().split(".") + if major == "0" and int(minor) < 2: + cls._db_create(execute) + elif major == "0" and int(minor) == 2: + if int(release) < 8: + cls._db_create(execute) + + return cls._update_submodel(execute, version, data) + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + scenario = data["scenario"] + loaded = data['loaded_pid'] + + table = execute( + "SELECT pamhyr_id, deleted, name, type, " + + "prob_dist, function, range, range_len, data_pid, " + + "scenario " + + "FROM ensemble " + + f"WHERE scenario = {scenario.id} " + + f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})" + ) + + for row in table: + it = iter(row) + + id = next(it) + deleted = (next(it) == 1) + name = next(it) + type = next(it) + prob_dist = next(it) + function = next(it) + range = next(it) + range_len = next(it) + data_pid = next(it) + owner_scenario = next(it) + + rdata = cls._decode_range(range, range_len) + + new_ensemble = cls( + id, name=name, + prob_dist=prob_dist, + function=function, + range=rdata, + status=data["status"], + owner_scenario=owner_scenario + ) + if deleted: + new_ensemble.set_as_deleted() + + loaded.add(id) + new.append(new_ensemble) + + return new + + def _db_save(self, execute, data=None): + if not self.must_be_saved(): + return True + + execute( + "DELETE FROM ensemble " + + f"WHERE pamhyr_id = {self.pamhyr_id} " + + f"AND scenario = {self._status.scenario_id}" + ) + + brange, length = self._encode_range() + + execute( + "INSERT INTO " + + "ensemble(pamhyr_id, deleted, name, type, " + + " prob_dist, function, range, range_len, " + + " data_pid, scenario) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + self.pamhyr_id, self.is_deleted(), + self._name, self._type, + self._prob_dist, self._function, brange, length, + self._data_pid, self._status.scenario_id + ) + + return True + + def _data_traversal(self, + predicate=lambda obj, data: True, + modifier=lambda obj, data: None, + data={}): + if predicate(self, data): + modifier(self, data) + + def _encode_range(self): + length = len(self._range) + data_format = ">" + ''.join(itertools.repeat("d", length)) + + return struct.pack(data_format, *self._range), length + + @classmethod + def _decode_range(cls, brange, length): + data_format = ">" + ''.join(itertools.repeat("d", length)) + + return struct.unpack(data_format, brange) diff --git a/src/Model/Ensembles/EnsembleList.py b/src/Model/Ensembles/EnsembleList.py new file mode 100644 index 00000000..8ebbe94c --- /dev/null +++ b/src/Model/Ensembles/EnsembleList.py @@ -0,0 +1,57 @@ +# EnsembleList.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 . + +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +from Model.Tools.PamhyrListExt import PamhyrModelList +from Model.Ensembles.Ensemble import Ensemble + + +class EnsembleList(PamhyrModelList): + _sub_classes = [Ensemble] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + new._lst = Ensemble._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute( + "DELETE FROM ensemble " + + f"WHERE scenario = {self._status.scenario_id}" + ) + + if data is None: + data = {} + + for ens in self._lst: + ens._db_save(execute, data=data) + + return True + + def new(self, index): + r = Ensemble(status=self._status) + self._lst.insert(index, r) + self._status.modified() + return r diff --git a/src/Model/River.py b/src/Model/River.py index ce16b7ea..27a2579a 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -60,6 +60,7 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \ from Model.D90AdisTS.D90AdisTSList import D90AdisTSList from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList from Model.GeoTIFF.GeoTIFFList import GeoTIFFList +from Model.Ensembles.EnsembleList import EnsembleList from Model.Results.Results import Results logger = logging.getLogger() @@ -476,6 +477,7 @@ class River(Graph): D90AdisTSList, DIFAdisTSList, GeoTIFFList, + EnsembleList, Results ] @@ -515,6 +517,9 @@ class River(Graph): self._geotiff = GeoTIFFList(status=self._status) + # Scenario ensemble + self._ensembles = EnsembleList(status=self._status) + self._results = {} @classmethod @@ -629,6 +634,8 @@ class River(Graph): new._geotiff = GeoTIFFList._db_load(execute, data) + new._ensembles = EnsembleList._db_load(execute, data) + return new def _db_load_results(self, execute, data=None): @@ -664,6 +671,8 @@ class River(Graph): objs.append(self._geotiff) + objs.append(self._ensembles) + for solv_type in self.results: objs.append(self.results[solv_type]) @@ -740,7 +749,7 @@ class River(Graph): self._BoundaryConditionsAdisTS, self._LateralContributionsAdisTS, self._D90AdisTS, self._DIFAdisTS, - self._geotiff, + self._geotiff, self._ensembles, ] for solver in self._parameters: diff --git a/src/Model/Study.py b/src/Model/Study.py index ded212ba..8b80efb8 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -46,7 +46,7 @@ logger = logging.getLogger() class Study(SQLModel): - _version = "0.2.7" + _version = "0.2.8" _sub_classes = [ Scenario,