mirror of https://gitlab.com/pamhyr/pamhyr2
662 lines
18 KiB
Org Mode
662 lines
18 KiB
Org Mode
# 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 <https://www.gnu.org/licenses/>.
|
|
|
|
# -*- 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"<MY SELECT> 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"<MY SELECT> 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 ---
|
|
<<scenario>>
|
|
|
|
### --- class B ---
|
|
<<data-b>>
|
|
|
|
### --- class A ---
|
|
<<data-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
|