diff --git a/src/Model/Ensembles/Function.py b/src/Model/Ensembles/Function.py new file mode 100644 index 00000000..7c828306 --- /dev/null +++ b/src/Model/Ensembles/Function.py @@ -0,0 +1,191 @@ +# Function.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 tools import logger_exception + +from Model.Except import NotImplementedMethodeError +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Scenario import Scenario + +logger = logging.getLogger() + + +class Function(SQLSubModel): + _sub_classes = [] + + def __init__(self, id: int = -1, + name: str = "", script: str = "", + status=None, owner_scenario=-1): + super(Function, self).__init__( + id=id, status=status, + owner_scenario=owner_scenario + ) + + self._name = name + self._type = "generic" + self._script = script + + @classmethod + def _db_create(cls, execute, ext=""): + execute(f""" + CREATE TABLE ensemble_function{ext} ( + {cls.create_db_add_pamhyr_id()}, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + name TEXT NOT NULL, + script TEXT 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, script, scenario " + + "FROM ensemble_function " + + 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) + script = next(it) + owner_scenario = next(it) + + new_function = cls( + id, name=name, script=script, + status=data["status"], + owner_scenario=owner_scenario + ) + if deleted: + new_function.set_as_deleted() + + loaded.add(id) + new.append(new_function) + + return new + + def _db_save(self, execute, data=None): + if self._type != "custom": + return True + + if not self.must_be_saved(): + return True + + execute( + "DELETE FROM ensemble_function " + + f"WHERE pamhyr_id = {self.pamhyr_id} " + + f"AND scenario = {self._status.scenario_id}" + ) + + brange, length = self._encode_range() + + execute( + "INSERT INTO " + + "ensemble_function(pamhyr_id, deleted, " + + " name, script, scenario) " + + "VALUES (?, ?, ?, ?, ?, )", + self.pamhyr_id, self.is_deleted(), + self._name, self._script, + 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 get_sample(self, r): + raise NotImplementedMethodeError(cls, cls._load) + + +########################### +# FUNCTION IMPLEMENTATION # +########################### + +class Uniform(Function): + _sub_classes = [] + + def __init__(self, id: int = -1, + name: str = "uniform", script: str = "", + status=None, owner_scenario=-1): + super(Custom, self).__init__( + id=id, status=status, + name=name, script=script, + owner_scenario=owner_scenario + ) + + def get_sample(self, dist, ens_range): + return range(ens_range[0], ens_range[1]) + + +class Custom(Function): + _sub_classes = [] + + def __init__(self, id: int = -1, + name: str = "", script: str = "", + status=None, owner_scenario=-1): + super(Custom, self).__init__( + id=id, status=status, + name=name, script=script, + owner_scenario=owner_scenario + ) + + self._type = "custom" + + def get_sample(self, dist, ens_range): + try: + # Run script to (re)define the sample function + f = eval(self._script) + return f(dist, ens_range) + except Exception as e: + logger_exception(e) + return None # TODO: Raise helpful exception diff --git a/src/Model/Ensembles/FunctionList.py b/src/Model/Ensembles/FunctionList.py new file mode 100644 index 00000000..7e1a6050 --- /dev/null +++ b/src/Model/Ensembles/FunctionList.py @@ -0,0 +1,57 @@ +# FunctionList.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 tools import trace, timer + +from Model.Tools.PamhyrListExt import PamhyrModelList +from Model.Ensembles.Function import Function + + +class FunctionList(PamhyrModelList): + _sub_classes = [Function] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + new._lst = [Uniform] + new._lst += Function._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute( + "DELETE FROM ensemble_function " + + 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 = Custom(status=self._status) + self._lst.insert(index, r) + self._status.modified() + return r