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.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:

View File

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