# 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