# Scenario.py -- Pamhyr # Copyright (C) 2024-2025 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 os import logging from tools import logger_exception, timer, flatten from Model.Tools.PamhyrDB import SQLSubModel logger = logging.getLogger() class Scenario(SQLSubModel): _id_cnt = 0 _sub_classes = [] tables_with_deleted_column = [ # Adists "output_rk_adists", "boundary_condition_adists", "boundary_condition_data_adists", "lateral_contribution_adists", "lateral_contribution_data_adists", "initial_conditions_adists", "initial_conditions_adists_spec", "d90_adists", "d90_adists_spec", "dif_adists_spec", "pollutants", "pollutants_characteristics", # Hydraulic "additional_files", "boundary_condition", "boundary_condition_data", "lateral_contribution", "lateral_contribution_data", "friction", "stricklers", "hydraulic_structures", "hydraulic_structures_basic", "initial_conditions", "sedimentary_layer", "sedimentary_layer_layer", "reservoir", "reservoir_data", "rep_lines", "geometry_pointXYZ", "geometry_profileXYZ", "river_reach", "river_node", "geotiff", "reservoir", "reservoir_data", # Temperature "boundary_condition_temperature", "boundary_condition_data_temperature", "initial_conditions_temperature", "initial_conditions_temperature_spec", "weather_parameters", "weather_parameters_data", ] related_tables = tables_with_deleted_column + [ "dif_adists", "solver_parameter", "hydraulic_structures_basic_value", "results", "results_data", "results_add_data", ] def __init__(self, id: int = -1, name: str = "", description: str = "", x: int = 1000.0, y: int = 1000.0, revision: int = 0, parent=None): super(Scenario, self).__init__() self._set_id(id) self._x, self._y = x, y self._name = name self._description = description self._revision = revision self._parent = parent def _set_id(self, id): if id == -1: self._id = Scenario._id_cnt else: self._id = id Scenario._id_cnt = max( self._id + 1, Scenario._id_cnt + 1 ) @classmethod def _db_create(cls, execute): execute(""" CREATE TABLE scenario( id INTEGER PRIMARY KEY, x REAL NOT NULL DEFAULT 1000, y REAL NOT NULL DEFAULT 1000, name TEXT NOT NULL, description TEXT NOT NULL, revision INTEGER NOT NULL, parent_id INTEGER REFERENCES scenario(id) ) """) cls._create_submodel(execute) return True @classmethod def _db_add_default(cls, execute): execute( "INSERT OR REPLACE INTO " + "scenario(id, x, y, name, description, revision, parent_id) " + "VALUES (\n" + " 0, 1000, 1000, 'default', 'Default scenario',\n" + " 0, NULL\n" + ")" ) @classmethod def create_db_add_scenario(cls): return "scenario INTEGER NOT NULL DEFAULT 0" @classmethod def create_db_add_scenario_fk(cls): return "FOREIGN KEY(scenario) REFERENCES scenario(id)" @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) cls._db_add_default(execute) return True @classmethod def update_db_add_scenario(cls, execute, table): execute( f"ALTER TABLE {table} " + "ADD COLUMN scenario INTEGER NOT NULL DEFAULT 0" ) # execute( # f"ALTER TABLE {table} " + # "ADD CONSTRAINT fk_scenario FOREIGN KEY (scenario) " + # "REFERENCES scenario(id)" # ) @classmethod def _db_load(cls, execute, data=None): scenarios = {} table = execute( "SELECT id, x, y, name, description, revision, parent_id " + "FROM scenario " + "ORDER BY id ASC" ) for row in table: it = iter(row) id = next(it) x, y = next(it), next(it) name = next(it) desc = next(it) revi = next(it) parent = next(it) if parent is not None: parent = scenarios[parent] new = cls( id=id, x=x, y=y, name=name, description=desc, revision=revi, parent=parent ) scenarios[id] = new return scenarios def _db_save(self, execute, data=None): if self.is_deleted(): return self.drop_all(execute) parent = 'NULL' if self.parent is not None: parent = self.parent._id execute( "INSERT OR REPLACE INTO " + "scenario(id, x, y, name, description, revision, parent_id) " + "VALUES (" + f"{self._id}, " + f"{self.x}, {self.y}, " + f"'{self._db_format(self.name)}', " + f"'{self._db_format(self.description)}', " + f"{self._revision}, " + f"{parent}" + ")" ) return True def drop_all(self, execute): execute(f"DELETE FROM scenario WHERE id = {self.id}") tables = self.related_tables for table in tables: execute( f"DELETE FROM {table} " + f"WHERE scenario = {self.id}" ) return True def get_parent_branch(self): def aux(scenario, acc): if scenario is None: return acc return aux(scenario.parent, [scenario.id] + acc) return aux(self, []) @timer def clean_deleted_data(self, execute): tables = self.tables_with_deleted_column branch = self.get_parent_branch() all_ids = [] for table in tables: if self.parent is None: # This scenario is the default scenario, so we can # delete all data marked as deleted execute( f"DELETE FROM {table} " + f"WHERE scenario = {self.id} " + "AND deleted = TRUE" ) continue # Select pamhyr_id for each deleted data in this scenario # who do not exists into parents scenarios ids = execute( f""" SELECT pamhyr_id FROM {table} WHERE deleted = TRUE AND scenario = {self.id} AND pamhyr_id NOT IN ( SELECT pamhyr_id FROM {table} WHERE scenario IN ({', '.join(map(str, branch[:-1]))}) ) """, fetch_one=False ) if ids is None or len(ids) == 0: continue ids = flatten(ids) logger.debug( f"({self.name}) Drop deleted data into '{table}' : {ids}" ) execute( f"DELETE FROM {table} " + f"WHERE scenario = {self.id} " + "AND pamhyr_id IN " + f"({', '.join(map(str, ids))})" ) all_ids += ids return all_ids @property def id(self): return self._id @property def x(self): return self._x @property def y(self): return self._y @property def name(self): if self._name == "": return f"Child of '{self._parent.name}' ({self.id})" return self._name @name.setter def name(self, name): self._name = name @property def description(self): if self._description == "": return f"Child of '{self._parent.name}' ({self.id})" return self._description @description.setter def description(self, description): self._description = description @property def revision(self): return self._revision @revision.setter def revision(self, revision): self._revision = revision @property def parent(self): return self._parent def set_pos(self, x, y): self._x = x self._y = y def __setitem__(self, key, value): if key == "name": self.name = value elif key == "description": self.description = value def __getitem__(self, key): if key == "id": return self.id if key == "name": return self.name if key == "description": return self.description if key == "parent": return self.parent return None def workdir(self): srep = "" if self.id != 0: srep = os.path.join( self.parent.workdir(), "scenario_" + str(self.id) ) return srep