Ensemble: Add ensemble base classes.

scenario-dev-pa
Pierre-Antoine 2026-07-22 15:38:52 +02:00
parent c09c62f077
commit 9b1fb4e7d2
4 changed files with 244 additions and 2 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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)

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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

View File

@ -60,6 +60,7 @@ from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList \
from Model.D90AdisTS.D90AdisTSList import D90AdisTSList from Model.D90AdisTS.D90AdisTSList import D90AdisTSList
from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Ensembles.EnsembleList import EnsembleList
from Model.Results.Results import Results from Model.Results.Results import Results
logger = logging.getLogger() logger = logging.getLogger()
@ -476,6 +477,7 @@ class River(Graph):
D90AdisTSList, D90AdisTSList,
DIFAdisTSList, DIFAdisTSList,
GeoTIFFList, GeoTIFFList,
EnsembleList,
Results Results
] ]
@ -515,6 +517,9 @@ class River(Graph):
self._geotiff = GeoTIFFList(status=self._status) self._geotiff = GeoTIFFList(status=self._status)
# Scenario ensemble
self._ensembles = EnsembleList(status=self._status)
self._results = {} self._results = {}
@classmethod @classmethod
@ -629,6 +634,8 @@ class River(Graph):
new._geotiff = GeoTIFFList._db_load(execute, data) new._geotiff = GeoTIFFList._db_load(execute, data)
new._ensembles = EnsembleList._db_load(execute, data)
return new return new
def _db_load_results(self, execute, data=None): def _db_load_results(self, execute, data=None):
@ -664,6 +671,8 @@ class River(Graph):
objs.append(self._geotiff) objs.append(self._geotiff)
objs.append(self._ensembles)
for solv_type in self.results: for solv_type in self.results:
objs.append(self.results[solv_type]) objs.append(self.results[solv_type])
@ -740,7 +749,7 @@ class River(Graph):
self._BoundaryConditionsAdisTS, self._BoundaryConditionsAdisTS,
self._LateralContributionsAdisTS, self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS, self._D90AdisTS, self._DIFAdisTS,
self._geotiff, self._geotiff, self._ensembles,
] ]
for solver in self._parameters: for solver in self._parameters:

View File

@ -46,7 +46,7 @@ logger = logging.getLogger()
class Study(SQLModel): class Study(SQLModel):
_version = "0.2.7" _version = "0.2.8"
_sub_classes = [ _sub_classes = [
Scenario, Scenario,