# senario.org -- Pamhyr developers documentation # Copyright (C) 2023-2024 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 -*- #+STARTUP: indent show2levels #+INCLUDE: ../tools/macro.org #+INCLUDE: ../tools/latex.org #+TITLE: Scenario implementation in Pamhyr2: \textbf{Preparatory work} #+SUBTITLE: Version: {{{version}}} #+AUTHOR: {{{INRAE}}} #+OPTIONS: toc:t #+LANGUAGE: UKenglish #+latex: \newpage * COMMENT tools #+begin_src emacs-lisp :export none (add-to-list 'org-structure-template-alist '("p" "#+begin_src python :python python3 :results output\n\n#+end_src")) #+end_src #+RESULTS: #+begin_example ((p #+begin_src python :python python3 :results output ,#+end_src) (d . #+name: graph-XXX ,#+header: :results drawer ,#+header: :exports results ,#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-XXX", caption="Graph XXX", float="t") ,#+begin_src dot :file "images/graph-XXX.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; } ,#+end_src) (p . src python :python python3 :results output :noweb yes src) (t . EXPORT latex \begin{table} \centering \resizebox{0.6\linewidth}{!}{ } %\vspace{0.5pt} %\caption{Number of tests by errors types} \end{table} ,#+E) (B . src shell :session *shell* :results output :exports both) (a . export ascii) (c . center) (C . comment) (e . example) (E . export) (h . export html) (l . export latex) (q . quote) (s . src) (v . verse)) #+end_example #+name: dbg-execute #+begin_src python :python python3 :results output :noweb yes def execute(db, query): print(query) return db.execute(query) #+end_src #+RESULTS: dbg-execute #+name: execute #+begin_src python :python python3 :results output :noweb yes def execute(db, query): return db.execute(query) #+end_src #+RESULTS: execute * Principe ** Scenario A scenario is defined by a name, a description and a parent scenario. If a scenario as no parent, parent value is =None=. So, in Pamhyr2 the list of scenario must by a tree with a root defined by a default scenarios with no parent. #+begin_src sql CREATE TABLE scenario( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- Name of the scenario description TEXT NOT NULL, -- Rich text description parent_id INTEGER REFERENCES scenario(id) -- Recursive references -- for parent scenario ) #+end_src ** Data Each data is associated with one and only one scenario. This senario is defined by the =id=. So, each data structure who can saved in DB must add the following lines in table définition: #+begin_src sql scenario_id INTEGER NOT NULL, FOREIGN KEY(scenario_id) REFERENCES scenario(id) #+end_src To avoid, at least, memory usage exploitation, by default, a scenario must saved only the modified data from its parent. This contrains require to wrap any data modification to update the data scenario id to current scenario id in source code. In addition, we need to modify the data loading méthode to take in considération this recursive dependencies. #+CAPTION: Recursive loading method #+begin_src python :python python3 :results output :noweb yes def rec_load(cls, db, current_scenario): # Default case, end of recursion if current_scenario is None: return [] # Default value table = db.execute(f" WHERE scenario_id = {current_scenario}") # If no data for this scenario, recursion if len(table) == 0: parent_id = db.get_parent_id(current_scenario) return cls.rec_load(db, parent_id) # Otherelse, parse data and return... #+end_src With this method, modify a data in parent scenario, modify this data in child scenario too. Unless is value has been previously modified in the child scenario. Deny tree node modification, except for leaf. This method work, but it dont allow to delete data un child scenarios, because if we dont found data for this scenario we search for parent scenario. So, we need a condition to stop recursion in case of data deletion in child scenario. To stop the recurtion, we can use a dummy data flag. By default, this flags is =false=, but if this flags is =true= the data is ignored at loading. So, we can use this flags for each data structure who can saved in DB: #+begin_src sql is_dummy BOOLEAN, -- True if this data as been deleted #+end_src Modify the loading method: #+CAPTION: Recursive loading method with possible dummy data #+begin_src python :python python3 :results output :noweb yes def rec_load_2(cls, db, current_scenario): # Default case, end of recursion if current_scenario is None: return [] # Default value table = db.execute(f" WHERE scenario_id = {current_scenario}") # If no data for this scenario, recursion if len(table) == 0: parent_id = db.get_parent_id(current_scenario) return cls.rec_load(db, parent_id) for data in table: if is_dummy(data): continue # Otherelse, parse data ... # return ... #+end_src #+RESULTS: And add a data reduction method: #+caption: Data reduction method for data subtree #+begin_src python :python python3 :results output :noweb yes def reduce_change(self): # Get the greater scenario scenar = reduce( lambda acc, x: max(acc, x._scenario), self._values, self._scenario ) # Apply change on this subtree self._scenario = scenar if is_dummy(self): # There are no more subtree self.values = [] else: # Update data subtree scenario id for value in self._values: value.set_scenario(self._scenario) # Dummy data is no more necessary in this subtree self._values = list( filter( lambda v: not is_dummy(v), self._values ) ) # Keep data id unique for all subtree self.get_new_db_id() #+end_src #+RESULTS: #+latex: \newpage * Prototype ** Scenario #+name: scenario #+begin_src python :python python3 :results output :noweb yes class Scenario(): def __init__(self, id, name, parent): self._id = id self._name = name self._parent = parent @classmethod def create_db(cls, db): execute( db, """ CREATE TABLE scenario( id INTEGER PRIMARY KEY, name TEXT NOT NULL, parent_id INTEGER REFERENCES scenario(id) ) """ ) @classmethod def load_db(cls, db): cur = execute( db, f"SELECT * FROM scenario" ) table = cur.fetchall() new = [] for values in table: new.append( cls( values[0], values[1], values[2], ) ) return new def save(self, db): sid = 'NULL' if self._parent is None else self._parent execute( db, "INSERT INTO scenario (id, name, parent_id) " + f"VALUES ({self._id}, '{self._name}', {sid})" ) def __repr__(self): return f"Scenario {{ {self._id}, {self._name}, parent {self._parent} }}" def __str__(self): return f"{self._name}({self._id})" #+end_src #+RESULTS: scenario ** Data #+name: data-b #+begin_src python :python python3 :results output :noweb yes class B(): def __init__(self, value, scenario): self._value = value self._scenario = scenario self._dummy = False @classmethod def create_db(cls, db): execute(db, """ CREATE TABLE b( id INTEGER PRIMARY KEY AUTOINCREMENT, dummy_data BOOLEAN, x INTEGER, a_id INTEGER NOT NULL, scenario_id INTEGER NOT NULL, FOREIGN KEY(a_id) REFERENCES a(id), FOREIGN KEY(scenario_id) REFERENCES scenario(id) ) """) def save(self, db, a_id): execute( db, "INSERT INTO b (x, dummy_data, a_id, scenario_id) " + f"VALUES ({self._value}, {self._dummy}, {a_id}, {self._scenario})" ) @classmethod def load_db(cls, db, a_id, senar): if senar is None: return [] cur = execute( db, "SELECT x, scenario_id, dummy_data FROM b " + f"WHERE scenario_id = {senar} AND a_id = {a_id}" ) table = cur.fetchall() if len(table) == 0: parent = execute( db, f"SELECT parent_id FROM scenario WHERE id = {senar}" ).fetchone() return cls.load_db(db, parent[0]) new = [] for values in table: if values[2]: # Is dummy continue new.append(cls(values[0], values[1])) return new def set_value(self, value, scenario): self._value = value self._scenario = scenario def set_dummy(self, scenario): self._scenario = scenario self._dummy = True def __repr__(self): if self._dummy: return "" return f"{self._value}" def __str__(self): if self._dummy: return "" return f"{self._value}" #+end_src #+RESULTS: data-b #+name: data-a #+begin_src python :python python3 :results output :noweb yes class A(): def __init__(self, id, values, scenario): self._id = id self._values = values self._scenario = scenario self._dummy = False @classmethod def create_db(cls, db): execute(db, """ CREATE TABLE a( id INTEGER PRIMARY KEY, dummy_data BOOLEAN, scenario_id INTEGER, FOREIGN KEY(scenario_id) REFERENCES scenario(id) ) """) def save(self, db): self.reduce_change() execute(db, f"DELETE FROM a WHERE scenario_id = {self._scenario}") execute( db, "INSERT INTO a (id, dummy_data, scenario_id) " + f"VALUES ({self._id}, {self._dummy}, {self._scenario})" ) execute( db, "DELETE FROM b " + f"WHERE scenario_id = {self._scenario} " + f"AND a_id = {self._id}" ) for value in self._values: value.save(db, self._id) def reduce_change(self): diff_scenar = reduce( lambda acc, x: max(acc, x._scenario), self._values, self._scenario ) print(f" ~> Reduce: {self._scenario} to {diff_scenar}") self._scenario = diff_scenar # Reduce new scenario value on each value for value in self._values: value._scenario = self._scenario # Delete useless dummy data self._values = list( filter( lambda v: not v._dummy, self._values ) ) # HACK: keep id unique self._id = self._id * 100 + diff_scenar @classmethod def load_db(cls, db, senar): if senar is None: return [] cur = execute( db, f"SELECT id, scenario_id, dummy_data FROM a WHERE scenario_id = {senar}" ) table = cur.fetchall() if len(table) == 0: parent = execute( db, f"SELECT parent_id FROM scenario WHERE id = {senar}" ).fetchone() return cls.load_db(db, parent[0]) new = [] for values in table: if values[2]: continue bb = B.load_db(db, values[0], senar) new.append(cls(values[0], bb, values[1])) return new def set_dummy(self, scenario): self._scenario = scenario self._dummy = True def add(self, b): self._values.append(b) def delete(self, index, scenario): self._values[index].set_dummy(scenario) self._scenario = scenario def __repr__(self): if self._dummy: return "{ }" return f"{{ {self._values}, ({self._scenario}) }}" def __str__(self): if self._dummy: return "{ }" return f"{{ {list(map(str, self._values))}, ({self._scenario}) }}" #+end_src #+RESULTS: data-a ** Full prototype #+begin_src python :python python3 :results output :exports both :noweb yes :cache yes from functools import reduce def execute(db, query): # print(query) return db.execute(query) def get_tree(db): from treelib import Node, Tree tree = Tree() for senar in db.execute("SELECT * FROM scenario").fetchall(): if senar[1] is None: tree.create_node( str(Scenario(senar[0], senar[1], senar[2])) + " : " + str(A.load_db(db, senar[0])[0]), senar[0] ) else: tree.create_node( str(Scenario(senar[0], senar[1], senar[2])) + " : " + str(A.load_db(db, senar[0])[0]), senar[0], parent=senar[2] ) return tree ### --- Scenario --- <> ### --- class B --- <> ### --- class A --- <> ### --- Script --- import sqlite3 import pprint pp = pprint.PrettyPrinter(width=79, compact=True, depth=6) with sqlite3.connect(":memory:") as db: cur = db.cursor() Scenario.create_db(cur) A.create_db(cur) B.create_db(cur) print("--- Default scenario (s0) ---") # One scenario s0 = Scenario(0, "s0", None) a0 = A( 1, [ B(0, s0._id), B(1, s0._id), B(2, s0._id), ], s0._id ) s0.save(cur) a0.save(cur) print("--- New scenario (s1) -- copy without modification ---") # New scenario s1 = Scenario(1, "s1", s0._id) s1.save(cur) print("--- New scenario (s2) -- data modification ---") # New scenario 2 with data s2 = Scenario(2, "s2", s0._id) s2.save(cur) a2 = a0 bb = list(map( lambda b: b.set_value(b._value + 1, s2._id), a2._values )) a2.save(cur) print("--- New scenario (s3) -- delete data ---") # New scenario 3 with data s3 = Scenario(3, "s3", s0._id) s3.save(cur) a3 = A.load_db(cur, s3._id)[0] a3._values[0].set_dummy(s3._id) a3.save(cur) print("--- New scenario (s4) -- new data ---") s4 = Scenario(4, "s4", s3._id) s4.save(cur) a4 = A.load_db(cur, s3._id)[0] a4.add(B(666, s4._id)) a4.save(cur) print("--- Scenario tree ---") tree = get_tree(db) print(tree) print("--- Modify s0 data ---") aa = A.load_db(db, s0._id)[0] bb = list(map( lambda b: b.set_value(b._value * 100 - 1, s0._id), aa._values )) aa.save(db) print("--- Scenario tree ---") tree = get_tree(db) print(tree) print("--- DB ---") ss = db.execute("SELECT * FROM scenario").fetchall() print(f"+ scenario ({len(ss)}):") pp.pprint(ss) aa = db.execute("SELECT * FROM a").fetchall() print(f"+ a ({len(aa)}):") pp.pprint(aa) bb = db.execute("SELECT * FROM b").fetchall() print(f"+ b ({len(bb)}):") pp.pprint(bb) #+end_src #+RESULTS[96c84f23be557425ae2822ef7f174175ef69f9b9]: #+begin_example --- Default scenario (s0) --- ~> Reduce: 0 to 0 --- New scenario (s1) -- copy without modification --- --- New scenario (s2) -- data modification --- ~> Reduce: 0 to 2 --- New scenario (s3) -- delete data --- ~> Reduce: 0 to 3 --- New scenario (s4) -- new data --- ~> Reduce: 3 to 4 --- Scenario tree --- s0(0) : { ['0', '1', '2'], (0) } ├── s1(1) : { ['0', '1', '2'], (0) } ├── s2(2) : { ['1', '2', '3'], (2) } └── s3(3) : { ['1', '2'], (3) } └── s4(4) : { ['1', '2', '666'], (4) } --- Modify s0 data --- ~> Reduce: 0 to 0 --- Scenario tree --- s0(0) : { ['-1', '99', '199'], (0) } ├── s1(1) : { ['-1', '99', '199'], (0) } ├── s2(2) : { ['1', '2', '3'], (2) } └── s3(3) : { ['1', '2'], (3) } └── s4(4) : { ['1', '2', '666'], (4) } --- DB --- + scenario (5): [(0, 's0', None), (1, 's1', 0), (2, 's2', 0), (3, 's3', 0), (4, 's4', 3)] + a (4): [(10000, 0, 0), (10002, 0, 2), (10003, 0, 3), (1000304, 0, 4)] + b (14): [(1, 0, 0, 100, 0), (2, 0, 1, 100, 0), (3, 0, 2, 100, 0), (4, 0, 1, 10002, 2), (5, 0, 2, 10002, 2), (6, 0, 3, 10002, 2), (7, 0, 1, 10003, 3), (8, 0, 2, 10003, 3), (9, 0, 1, 1000304, 4), (10, 0, 2, 1000304, 4), (11, 0, 666, 1000304, 4), (12, 0, -1, 10000, 0), (13, 0, 99, 10000, 0), (14, 0, 199, 10000, 0)] #+end_example