From 3c3a3de3e524e1969a7199193c102f74b3e482fe Mon Sep 17 00:00:00 2001
From: BAUCHER Achille <achille.baucher@inria.fr>
Date: Tue, 15 Mar 2022 10:34:16 +0100
Subject: [PATCH] Completed afaire

---
 afaire.md               | 35 +++++++++++++++-
 pydynamo/core/system.py | 92 ++++++++++++++++++++---------------------
 2 files changed, 80 insertions(+), 47 deletions(-)

diff --git a/afaire.md b/afaire.md
index 965750a8..fb1b6571 100644
--- a/afaire.md
+++ b/afaire.md
@@ -6,4 +6,37 @@
 - [ ] New table politic, c'est la variable qui est rentréer en paramètre.
 - [ ] Ajouter shéma de documentation
 - [ ] Fonctions graphiques et tout de World3 à mettre dans System
-- [ ] add_function ça fonctionne ça ?
\ No newline at end of file
+- [ ] add_function ça fonctionne ça ?
+- [ ] Les new politic dans system directement.
+- [ ] Demander: trop de fonctions dans System ?
+- [ ] Demander: les dictionnaires de forme fixée, mieux avec plusieurs noms ?
+- [ ] Demander: doublons d'informations pour rechercher plus vite, oui ou non ?
+- [ ] Clarifier  dans l'explication de simulation de System les appels et ordre
+- [ ] Demander: ça vaut le coup de faire une documentaiton shématique etc. ?
+
+## La documentation
+### From equations to system.
+Pydynamo equations are parsed to create a System object, through the function `new_system`.
+
+1. We start with a list of string pydynamo equations.
+2. The `parse_system.get_nodes_eqs_dicts` retrieve from this list all informations that a System needs to be created.
+3. It uses functions of `parse_equations`, which analyse an equation to retrieve all relevant information about one equation:
+    - If it's a constant, update or initialisation equation
+    - What parameters are needed in the equation (constants, variables and their indices, special functions)
+4. It also uses functions from `parse_dynamo_functions` to handle the case of special DYNAMO functions (clip, tabhl, etc.)
+5. With all this informations, contained in the *main dictionnnaries*, a System object is created.
+
+### System object
+A system object only contains the informations retrieved by the above . Any user can modify the object:
+- Add new equations or change it
+- Change constant values
+- Add new politic (which changes some equations)
+Then, each time a System object is run:
+1. The *main dictionnnaries* are updated, because maybe some dependencies has been changed.
+2. All variables are initialized as empty arrays.
+3. All constants are set at their values.
+4. All special functions are set.
+5. Thanks to the graph of dependencies, an updating order is defined.
+6. All variables are initialized with the initialisation functions.
+7. For each step, all variables are updated, and their new value set in the arrays.
+8. The results can be plotted with new functions.
diff --git a/pydynamo/core/system.py b/pydynamo/core/system.py
index 5f5ac752..0137735c 100644
--- a/pydynamo/core/system.py
+++ b/pydynamo/core/system.py
@@ -40,13 +40,13 @@ class System:
             'update': dict(),
             'init': dict()
             }
-        
+
         self.comments = {}
 
     # ---------- Modifiers ----------
-    
+
     def add_node(self, node, node_type):
-        """Add a node to the System. 
+        """Add a node to the System.
 
         Parameters
         ----------
@@ -73,7 +73,7 @@ class System:
                self.add_node(node, node_type)
 
     def add_eq(self, node, eq_type, arguments):
-        """Add an equation to the System. 
+        """Add an equation to the System.
 
         Parameters
         ----------
@@ -87,12 +87,12 @@ class System:
              'var': {( variable name, index name)},
              'fun': {dict_of_function_infomations}}
         """
-        
+
         # Check if some special functions (clip, step) are in arguments:
         for fun in set(arguments['args']['fun']):
             if fun not in instance_fun_names and fun in globals():
                 del arguments['args']['fun'][fun]
-                
+
         self.eqs[eq_type][node] = arguments
 
 
@@ -113,13 +113,13 @@ class System:
 
     def add_comments(self, comments):
         """Add comments to the System.
-        
+
         Parameters
         ----------
         comments : dict(str: str)
             Each node name and its comment.
         """
-        
+
         for node, comment in comments.items():
             self.comments[node] = comment
 
@@ -130,13 +130,14 @@ class System:
 
     # Following functions are not used yet.
     # def add_function(self, fun, name=None):
-    #     """Add or reset a function to the system. 
+    #     """Add or reset a function to the system.
     #     The function should be any of init, update or set, and take the same arguments as defined in the system equations.
-        
+
     #     Parameters
     #     ----------
     #     fun : function
-    #         Function taking appropriate arguments and return a value. If name is None, the function name should be one of `init_var`, `upadte_var`, `set_cst` with `var` a variable name and `cst` a constant name.
+    #         Function taking arguments with syntax: (cst, var1_k, var2_j, fun) with cst names of constants, var1 or var2 names of variables, and fun names of functions, and return a value.
+    #         If name is None, the function name should be one of `init_var`, `upadte_var`, `set_cst` with `var` a variable name and `cst` a constant name.
     #     name : str
     #         Name of the function in case we want it different than fun.__name__.
     #     """
@@ -145,7 +146,7 @@ class System:
     #     for s in ('_', 'update', 'init', 'set'):
     #         assert not name.startswith(s), "Fun name shouldn't start with '{s}'"
     #     setattr(self, fun.__name__, fun)
-         
+
     # def add_functions(self, *args, **kwargs):
     #     for fun in args:
     #         self.add_function(fun)
@@ -167,9 +168,9 @@ class System:
     #         self.add_system_function(fun)
     #     for fun, name in kwargs:
     #         sefl.add_system_function(fun, name)
-            
+
     # ---------- Getters ----------
-    
+
     def iter_all_nodes(self):
         return chain(*self.nodes.values())
 
@@ -178,7 +179,7 @@ class System:
             return set(self.iter_all_nodes())
 
         return self.nodes[node_type]
-    
+
     def get_all_variable_names(self):
         """
         Returns
@@ -187,7 +188,7 @@ class System:
             List of name of all variables
         """
         return self.nodes['var']
-    
+
     def get_var(self, name):
         """
 
@@ -201,11 +202,11 @@ class System:
         np.array(float):
             Array of values of the variable for the last run
         """
-        
+
         assert name in self.nodes['var'], f"{name} is not a variable"
         return getattr(self, name)
-        
-    
+
+
     def get_time(self):
         """
 
@@ -215,11 +216,11 @@ class System:
             Array of system time.
         """
         return self.time
-    
+
     def get_tabhl_args(self, name):
         """
         Get indications about a tabhl function.
-        
+
         Parameters
         ----------
         name: str
@@ -230,7 +231,7 @@ class System:
         np.array, np.array, str, str, str:
             x, f(x), x label, y label, title
         """
-        
+
         aa = list(self.eqs['update'][name]['args']['var'])
         argname = aa[0][0].split('.')[0]
         assert 'tabhl_' + name in dir(self), 'Error, no such tabhl function'
@@ -462,7 +463,7 @@ class System:
             if '__iter__' in dir(value):
                 value = np.array(value)
             return value
-            
+
         except Exception as e:
             raise(Exception(f"Error setting {cst}\n"
                             f" wih function {fun.__custom_repr__}\n"
@@ -475,7 +476,7 @@ class System:
 
     def set_cst(self, cst, args):
         setattr(self, cst, self.get_cst_val(cst, args))
-        
+
     def set_all_csts(self):
         G = self.get_cst_graph()
         for cst in nx.topological_sort(G):
@@ -491,7 +492,7 @@ class System:
 
             if not cst in dir(self):
                 self.set_cst(cst, args)
-            
+
     def generate_var(self, var, N):
         setattr(self, var, np.full(N, np.NAN))
 
@@ -516,7 +517,7 @@ class System:
             table = getattr(self, p['table'])
             fun = Interpol(x_low, x_high, x_incr, table)
             setattr(self, p['fun'], fun)
-                
+
         if f_type == 'sample':
             isam = eval(p['isam'])
             fun = Sample(isam, self.time)
@@ -524,10 +525,10 @@ class System:
 
         if f_type == 'step':
             pass
-    
+
     def step(self, hght, sttm, k):
         return step(hght, sttm, self.time[k])
-    
+
     def set_all_special_functions(self):
         """
         Generate every special dynamo functions that are stored.
@@ -559,11 +560,11 @@ class System:
                         fun_args[p['type']] = getattr(self, p['fun'])
                         fun_args['k'] = 0
             cst_args = {c: getattr(self, c) for c in args['cst']}
-    
+
             # Assert that there is no variables considered as constants etc.
             tps = {'cst', 'var', 'fun'}
             dd = locals()
-                
+
             for t, n, tt in ((t, n, tt) for t in tps
                          for tt in tps.difference({t})
                          for n in dd[tt + '_args']):
@@ -580,7 +581,7 @@ class System:
                     f'{var} = ',
                     line))
                     raise(AssertionError(msg))
-        
+
             self.update(var, 0, update_fun, {**var_args, **cst_args, **fun_args})
 
     def set_update_loop(self):
@@ -595,7 +596,7 @@ class System:
                 u = {'var': var}
                 try:
                     args = self.eqs['update'][var]['args']
-                    
+
                     u['cst_args'] = {c: getattr(self, c) for c in args['cst']}
                     u['var_args'] = {v + '_' + ni: (getattr(self, v), 0 if ni == 'k' else -1)
                                      for v, ni in args['var']}
@@ -604,10 +605,10 @@ class System:
                         p = self.eqs['update'][var]['args']['fun'][fun_type]
                         u['fun_args'][p['type']] =  getattr(self, p['fun'])
                         u['fun_args']['k'] = None
-                    
+
                     u['update_fun'] = getattr(self, 'update_' + var)
                     self._update_loop.append(u)
-        
+
                 except AttributeError as e:
                     line = self.eqs['update'][var]['line']
                     raise(AttributeError(f"In updating {var}:\n"
@@ -719,7 +720,7 @@ class System:
                 msg += f"{j}.i = {line} \n"
             msg +="Please design an initialisation scheme that is not cyclic."
             raise AssertionError(msg)
-    
+
     def run(self, N=None, dt=1):
         """
         After preparing and before running,
@@ -735,10 +736,10 @@ class System:
         >>> system.update_v1 = new_update # New fct for updating v1
         >>> s.run(10, 1) # Run with new parameters
         """
-        
+
         self.change_functions_in_dict()
         self.assert_update_acyclic()
-        self.assert_init_acyclic()        
+        self.assert_init_acyclic()
 
         try:
             if not N:
@@ -747,7 +748,7 @@ class System:
                 self.final_time = self.initial_time + N * dt
         except:
             raise Exception("Either pass N or define both final_time and initial_time")
-            
+
         self.time = self.initial_time + np.arange(N)*dt
         self.dt = dt
         self.generate_all_vars(N)
@@ -777,17 +778,17 @@ class System:
         ----------
         node: str
             Name of the node
-        
+
         with_definitions: bool
             If yes, returns a dictionnary with each node definition.
         """
-        
+
         out_nodes = [b for (a, b) in self.get_influence_graph().out_edges(node)]
         if with_definitions:
             return {a: self.get_comment(a) for a in out_nodes}
         else:
             return out_nodes
-        
+
     def get_in_nodes(self, node, with_definitions=False):
         """Returns the list of the nodes that this node needs to be computed.
 
@@ -795,17 +796,17 @@ class System:
         ----------
         node: str
             Name of the node
-        
+
         with_definitions: bool
             If yes, returns a dictionnary with each node definition.
         """
-        
+
         in_nodes =  [a for (a, b) in self.get_influence_graph().in_edges(node)]
         if with_definitions:
             return {a: self.get_comment(a) for a in in_nodes}
         else:
             return in_nodes
-    
+
     def get_at(self, var, t):
         """Returns the value of var at time t, or an interpolation if between rwo timestep values."""
         assert var in self.nodes['var'], f"{var} is not a variable"
@@ -813,7 +814,7 @@ class System:
 
         if t == self.final_time:
             return getattr(self, var)[-1]
-        
+
         idx1 = np.arange(len(self.time))[self.time <= t][-1]
         dd = (self.time[idx1] - t)/self.dt
         v = getattr(self, var)
@@ -833,4 +834,3 @@ class System:
                 return False
             except: return any(a!=b)
         return {cst: (v1, v2) for cst, (v1, v2) in both.items() if diff(v1, v2)}
-        
-- 
GitLab