# 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 = [] @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 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] 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] 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] 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] 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 = [""] 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