# Distribution.py -- Pamhyr # Copyright (C) 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 import numpy as np 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 Distribution(SQLSubModel): _sub_classes = [] def __init__(self, id: int = -1, name: str = "", script: str = "", status=None): super(Distribution, self).__init__( id=id, status=status, owner_scenario=0 ) self._name = name self._type = "generic" self._script = script self._labels = [] self._default_values = [] self._params_checkers = [] self._params_tips = [] @property def name(self): return self._name @property def labels(self): return self._labels @property def default_values(self): return self._default_values @classmethod def _db_create(cls, execute, ext=""): execute(f""" CREATE TABLE ensemble_distribution{ext} ( {cls.create_db_add_pamhyr_id()}, deleted BOOLEAN NOT NULL DEFAULT FALSE, name TEXT NOT NULL, script TEXT NOT NULL, PRIMARY KEY(pamhyr_id) ) """) 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 " + "FROM ensemble_distribution " + f"WHERE 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) new_distribution = cls( id, name=name, script=script, status=data["status"] ) if deleted: new_distribution.set_as_deleted() loaded.add(id) new.append(new_distribution) return new def _db_save(self, execute, data=None): # Only save custom distribution if self._type != "custom": return True if not self.must_be_saved(): return True execute( "INSERT INTO " + "ensemble_distribution(pamhyr_id, deleted, " + " name, script) " + "VALUES (?, ?, ?, ?)", self.pamhyr_id, self.is_deleted(), self._name, self._script ) 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 is_valid_parameters(self, parameters): valid = True tips = [] for i, checker in enumerate(self._params_checkers): ok = checker(parameters) if not ok: tips.append(self._params_tips[i]) valid = False return (valid, tips) def check_params(self, params): return True def random(self, r): raise NotImplementedMethodeError(cls, cls._load) ############################### # DISTRIBUTION IMPLEMENTATION # ############################### class Uniform(Distribution): _sub_classes = [] def __init__(self, id: int = -1, name: str = "uniform", script: str = "", status=None): super(Uniform, self).__init__( id=id, status=status, name=name, script=script ) self._labels = ["low", "high"] self._default_values = [20.0, 40.0] self._params_checkers = [self.check_params, self.check_params] self._params_tips = ["uniform_low_tips", "uniform_high_tips"] def check_params(self, params): return (params[0] < params[1]) def random(self, params, nb): lst = params.copy() lst.append(nb) return np.random.uniform(*lst) class Normal(Distribution): _sub_classes = [] def __init__(self, id: int = -1, name: str = "normal", script: str = "", status=None): super(Normal, self).__init__( id=id, status=status, name=name, script=script ) self._labels = ["loc", "scale"] self._default_values = [30.0, 5.0] self._params_checkers = [self.check_params, self.check_scale] self._params_tips = ["normal_loc_tips", "normal_scale_tips"] def check_scale(self, params): return 0 <= params[1] def random(self, params, nb): lst = params.copy() lst.append(nb) return np.random.normal(*lst) class Binomial(Distribution): _sub_classes = [] def __init__(self, id: int = -1, name: str = "binomial", script: str = "", status=None): super(Binomial, self).__init__( id=id, status=status, name=name, script=script ) self._labels = ["binomial_n", "binomial_p"] self._default_values = [100.0, 0.3] self._params_checkers = [self.check_n, self.check_p] self._params_tips = ["binomial_n_tips", "binomial_p_tips"] def check_n(self, params): n = params[0] return (0 <= n) and (n == round(n)) def check_p(self, params): p = params[1] return 0 <= p <= 1 def random(self, params, nb): lst = params.copy() lst.append(nb) return np.random.binomial(*lst) class Poisson(Distribution): _sub_classes = [] def __init__(self, id: int = -1, name: str = "poisson", script: str = "", status=None): super(Poisson, self).__init__( id=id, status=status, name=name, script=script ) self._labels = ["poisson_lam"] self._default_values = [5.0] self._params_checkers = [self.check_lam] self._params_tips = ["poisson_lam_tips"] def check_lam(self, params): lam = params[0] return 0 <= lam def random(self, params, nb): lst = params.copy() lst.append(nb) return np.random.poisson(*lst) class Custom(Distribution): _sub_classes = [] def __init__(self, id: int = -1, name: str = "", script: str = "", status=None): super(Custom, self).__init__( id=id, status=status, name=name, script=script ) self._labels = ["custom"] self._type = "custom" self._default_values = [""] self._params_checkers = [self.check_params] self._params_tips = ["custom_params_tips"] def random(self, params, nb): try: # Run script to (re)define the sample distribution f = eval(self._script) return f(params, nb) except Exception as e: logger_exception(e) return None # TODO: Raise helpful exception