diff --git a/pydynamo/__init__.py b/pydynamo/__init__.py index 1133fcad652edf901e664dfbc3d72c7d08e4ee4c..4889f5beb648ab85a90e2e3cb85d4592f54ba66e 100644 --- a/pydynamo/__init__.py +++ b/pydynamo/__init__.py @@ -3,7 +3,8 @@ __version__ = "0.1" from pydynamo.core.system import System -# import pydynamo.core.dynamo_converter from pydynamo.core import psdsystem +from pydynamo.core.dynamo_converter import convert_dynamo_file + from .world3 import World3 diff --git a/pydynamo/core/delays.py b/pydynamo/core/delays.py index c73ec5d4a2ec236fa0360ecf69c9c8f32c6a1f12..bfc77272466219a0264dab48af309a6b7ad42db2 100644 --- a/pydynamo/core/delays.py +++ b/pydynamo/core/delays.py @@ -1,5 +1,5 @@ -# File delays.py Delay functions of dynamo. -# Linear systems of order 1 or 3 for delay and smoothing +"""Functions implementing a delayed smooth effect in pydynamo. +""" import numpy as np class Smooth: diff --git a/pydynamo/core/dynamo_converter.py b/pydynamo/core/dynamo_converter.py index 411db14c2c6e3f5d5b2eaf114c549347a14cd3e2..e18acc28dffdcdb9ed84dd8e41c6592ea020c445 100644 --- a/pydynamo/core/dynamo_converter.py +++ b/pydynamo/core/dynamo_converter.py @@ -1,12 +1,31 @@ +""" +Functions to convert a file in DYNAMO to pydynamo syntax. +""" + import re -def convert_dynamo_file(filename, new_file): +def convert_dynamo_file(DYNAMO_file, pydynamo_file): + """Create a new file with pydynamo equations from DYNAMO equations file. + + Parameters + ---------- + DYNAMO_file : str + File name of DYNAMO code. + pydynamo_file : File name of the pydynamo code which will be created. + """ with open(new_file, 'w+') as nf: with open(filename, 'r') as f: for l in f.readlines(): - new_l = dy2pydy(l) - nf.write(new_l) + nf.write(dy2pydy(l)) + def dy2pydy(l): + """Convert a DYNAMO equation line to a pydynamo syntax equation. + + Parameters + --------- + l : str + DYNAMO equation. + """ l = re.sub('^[ATRCLS] ', '', l) l = re.sub('^N (\w*)', '\\1.i', l) l = re.sub('(?!<\w)smooth\((\w+?)\.k(?!\w)', 'smooth(\\1.j',l) diff --git a/pydynamo/core/politics.py b/pydynamo/core/politics.py index 0690f1960adf45a6c450af0b5dc1af9eb1133fd9..59d3fbe9a38feb53afd623f50e2622ce98bead42 100644 --- a/pydynamo/core/politics.py +++ b/pydynamo/core/politics.py @@ -1,3 +1,5 @@ +"""Functions to add new politics in a System object. +""" import re def new_politic(self, name, date, new_val): diff --git a/pydynamo/core/psdsystem.py b/pydynamo/core/psdsystem.py index 0d8de92386cabf020ff57daf0bf6049ff9025566..ce1e831f5e5f5d7d254ed2a1fb70c652150395c1 100644 --- a/pydynamo/core/psdsystem.py +++ b/pydynamo/core/psdsystem.py @@ -1,13 +1,21 @@ +"""System class which creates a System from a Pysd model. +""" + import networkx as nx import numpy as np import re from .system import System class PsdSystem(System): + """System which initialises with a Pysd model. + """ def __init__(self, model): + """Intialise from a Pysd model. Comments are retrieved from code. + """ self.model = model self.caracs = {} self.comments = {} + for n in self.model.components._dependencies: try : doc = getattr(self.model.components, n).__doc__ @@ -21,6 +29,7 @@ class PsdSystem(System): coms.append(l) car['Comment'] = re.sub(' *', ' ', ' '.join(coms)) self.caracs[n] = car + try: self.comments[n] = f"{car['Real Name']} [{car['Units']}]: {car['Comment']}" except Exception as e: @@ -37,6 +46,7 @@ class PsdSystem(System): self.df_run = None def get_influence_graph(self): + """Return a networkx Graph in which there is a node from A to B if B needs A to be computed.""" G = nx.Graph() for var, deps in self.model.components._dependencies.items(): for dep, i in deps.items(): @@ -45,7 +55,20 @@ class PsdSystem(System): return G def get_tabhl_args(self, name): - # print(self.caracs) + """ + Get indications about a tabhl function. + + Parameters + ---------- + name: str + Name of the variable using a tabhl function. + + Returns + ------- + np.array, np.array, str, str, str: + x, f(x), x label, y label, title + """ + if self.caracs[name]['Type'] == 'lookup': try: x, y = np.array(eval(self.caracs[name]['Original Eqn'])).T @@ -61,9 +84,6 @@ class PsdSystem(System): def get_time(self): return np.arange(self.model.time.initial_time(), self.model.time.final_time()+self.model.time.time_step()/2, self.model.time.time_step()) - - def get_tabhl_arg(self, name): - pass def get_var(self, name): if self.df_run is None: diff --git a/pydynamo/core/system.py b/pydynamo/core/system.py index ec18245c5bb51fe1e79c0af82f7d9f3f5444a2b9..d26bc2b53e780ef85b739dcd50d8c9ee0e0718d2 100644 --- a/pydynamo/core/system.py +++ b/pydynamo/core/system.py @@ -1,3 +1,5 @@ +"""Define the System class, base class to simulate system dynamics from equations. +""" import inspect import numpy as np import networkx as nx @@ -23,6 +25,7 @@ class System: From this dictionnaries, it generates the updating pattern and run the simulation. """ + from .plot_system import plot, plot_non_linearity, plot_compare, show_influence_graph from .politics import new_cst_politic, new_var_politic, new_table_politic, new_politic @@ -91,6 +94,11 @@ class System: def add_equations(self, new_code_lines): """Add new equations to the older ones. In case there is a conflict for a same variable or constant, the last equation only is remembered. + + Parameters + ---------- + new_code_lines : list(str) + Pydynamo equations to add. """ self.code_lines = self.code_lines + new_code_lines @@ -100,83 +108,27 @@ class System: self.nodes, self.eqs, self.comments = get_system_dicts(self.code_lines) if prepare: self.prepare() - - # Modifiers - def add_node(self, node, node_type): - self.nodes[node_type].add(node) - - def add_all_nodes(self, all_nodes): - for node_type in all_nodes: - for node in all_nodes[node_type]: - self.add_node(node, node_type) - - def add_eq(self, node, eq_type, arguments): - # 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 - def add_all_eqs(self, all_eqs): - for eq_type in all_eqs: - for node in all_eqs[eq_type]: - self.add_eq(node, eq_type, all_eqs[eq_type][node]) def add_comments(self, comments): for node, comment in comments.items(): self.comments[node] = comment - - def add_function(self, fun, name=None): - if not name: - name = fun.__name__ - 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) - for name, fun in kwargs.items(): - self.add_function(fun, name) - - def add_system_function(self, fun, name=None): - if not name: - name = fun.__name__ - if any(name.startswith(s) for s in ('update', 'init', 'set')): - setattr(self, name, fun) - fun.__okdic__ = False - fun.__doc__ = f"User defined function\n{fun.__doc__}" - return - assert False, "Invalid function name. Should starts with 'init', 'update' or 'set'" - - def add_system_functions(self, *args, **kwargs): - for fun in args: - 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()) - - def get_all_nodes(self, node_type=None): - if node_type == None: - return set(self.iter_all_nodes()) - - return self.nodes[node_type] - + # Getters def get_all_variable_names(self): """ + Get the set of all variables. + Returns ------- - list(str): - List of name of all variables + set(str): + Set of names of all variables """ - return self.nodes['var'] + return set(self.nodes['var']) def get_var(self, name): - """ + """Get the variable array. Parameters ---------- @@ -186,13 +138,87 @@ class System: Returns ------- np.array(float): - Array of values of the variable for the last run + 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_out_nodes(self, node, with_definitions=False): + """Returns the list of the nodes using the node to be computed. + + Parameters + ---------- + 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.definition(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. + + Parameters + ---------- + 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.definition(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. + + Parameters + ---------- + var : str + Variable name. + + t : float + Time. + """ + assert var in self.nodes['var'], f"{var} is not a variable" + assert var in dir(self), "Simulation hasn't been run yet !" + assert t >= self.initial_time and t <= self.final_time, "{t} is out of bounds {self.initial_time}, {s.final_time}" + + 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) + return v[idx1]*(1 - dd) + v[idx1 + 1]*dd + + def __getitem__(self, arg): + """Get item at a certain date. See System.get_at. + + Parameters + ---------- + arg : str, float + Variable name and time. + """ + try: + name, year = arg + return self.get_at(name, year) + + except TypeError: + return getattr(self, arg) + def get_time(self): """ @@ -229,17 +255,13 @@ class System: xlabel = argop + ('\n'+ self.definition(argname) if argop==argname else '') return x, f(x), ylabel, xlabel, f"{name} non-linear function" - def iter_all_eqs(self): - return chain(*self.eqs.values()) - - def get_all_eqs(self, eq_type=None): - if eq_type == None: - return set(self.iter_all_eqs()) - - return self.eqs[eq_type] - def definition(self, node): """Get the definition of a node. + + Parameters + ---------- + node : str + Name of the variable or constant to get the definition. """ try: return self.comments[node] @@ -272,7 +294,7 @@ class System: Parameters ---------- node : str - Name of the variable or constant to plot the equation. + Name of the variable or constant to get the raw pydynamo equation. """ for t, eqq in self.eqs.items(): try: @@ -283,13 +305,24 @@ class System: # Infos def is_initialized(self, var): - return var in self.eqs['init'] + """Indicates if a var is initialized or not. - def is_of_type(self, node, node_type): - return node in self.nodes[node_type] + Parameters + ---------- + var : str + Variable name. + """ + return var in self.eqs['init'] # Graph def get_cst_graph(self): + """Get the graph of influences for constants: an arrow from constant A to constant B if B needs A to be computed. + + Returns + ------- + networkx.DiGraph + Graph of constant influences. + """ G = nx.DiGraph() for c in self.eqs['cst']: G.add_node(c) @@ -305,6 +338,13 @@ class System: return G def get_init_graph(self): + """Get the graph of influences for variables at initialisation: an arrow from variable A to variable B if B needs A to be computed. + + Returns + ------- + networkx.DiGraph + Graph of variable initialisation influences. + """ G = nx.DiGraph() for v in self.eqs['init']: G.add_node(v) @@ -322,6 +362,13 @@ class System: return G def get_update_graph(self): + """Get the graph of influences for variables and their indices at updating step: an arrow from variable (A, i) to variable (B, k) if (B, k) needs (A, i) to be computed. + + Returns + ------- + networkx.DiGraph + Graph of variable influences at updating step. + """ G = nx.DiGraph() for v in self.eqs['update']: G.add_node((v, 'k')) @@ -332,6 +379,13 @@ class System: return G def get_update_graph_quotient(self): + """Get the graph of influences for variables at updating step: an arrow from variable A to variable B if B needs A to be computed. + + Returns + ------- + networkx.DiGraph + Graph of variable influences at updating step. + """ G = nx.DiGraph() for v in self.eqs['update']: G.add_node(v) @@ -343,6 +397,11 @@ class System: def get_influence_graph(self): """Get the graph of influences: an arrow from A to B if B needs A (at initialisation or updating step) to be computed. + + Returns + ------- + networkx.DiGraph + Graph of influences. """ G = nx.DiGraph() for v in self.eqs['update']: @@ -375,51 +434,67 @@ class System: # Assertions def assert_cst_defined(self): + """Assert that every constant has an equation to be computed. + """ for c in self.nodes['cst']: - # assert c in self.eqs['cst'], f'Error: Constant {c} is not set' - if not c in self.eqs['cst']: - print(f'Warning: Constant {c} is not set') + assert c in self.eqs['cst'], f'Error: Constant {c} is not defined in any constant equation' def assert_update_defined(self): + """Assert that every variable has an updating equation to be computed. + """ for v in self.nodes['var']: - # assert (v, 'k') in self.eqs['update'], f'Error: Variable {v} is not updated' - if not v in self.eqs['update']: - print(f'Warning: Variable {v} is not updated') + assert v in self.eqs['update'], f'Error: Variable {v} is not updated in any equation' def assert_init_defined(self): + """Assert that every variable has an initialisation or updated equation to be computed. + """ for v in self.nodes['var']: - # assert ((v, 'i') in self.eqs['init'] - # or (v, 'k') in self.eqs['update']) , f'Error: Variable {v} neither updated nor initialized' - if not (v in self.eqs['init'] - or v in self.eqs['update']): - print(f'Warning: Variable {v} neither updated nor initialized') + assert (v in self.eqs['init'] + or v in self.eqs['update']) , f'Error: Variable {v} neither updated nor initialized' def set_fun(self, node, fun_name, args, line): - line = line.replace('\n','') + """Set an updating, initialisation or constant setting function to the System. It evaluates a lambda function with the line equation inside. + + Parameters + ---------- + node : str + Constant or variable name. + + fun_name : str + Name of function + + args : iterable(str) + Arguments of the function. + + line : str + Formula with wich the `node` is computed (set, initialised orupdated). + """ + line = line.strip() args_str = ', '.join(args) fun_str = f'lambda {args_str}: {line}' doc = f'Get the value of {node} depending on {args}' - try: - fun = eval(fun_str) - except: - print(node, fun_name, args) - print(line) - assert False + + # Function settings + fun = eval(fun_str) fun.__name__ = fun_name fun.__doc__ = doc fun.__line__ = line fun.__okdic__ = True fun.__custom_repr__ = f"{fun_name}({args_str})" fun.__str__ = lambda : f'System custom function {fun_name}({args})' - # Doesn(t work + setattr(self, fun_name, fun) def set_all_funs(self): + """For each equation, set the appropriate function to the System. + """ for eq_type in self.eqs: for node in self.eqs[eq_type]: getattr(self, f'set_{eq_type}_fun_from_dict')(node) def set_cst_fun_from_dict(self, node): + """For each constant equation, set the appropriate function to the System. + """ args = self.eqs['cst'][node]['args'] line = self.eqs['cst'][node]['line'] @@ -429,8 +504,11 @@ class System: line) def set_update_fun_from_dict(self, node): + """For each updating equation, set the appropriate function to the System. + """ args = self.eqs['update'][node]['args'] line = self.eqs['update'][node]['line'] + args_vars = ['_'.join(v_i) for v_i in args['var']] args_cst = list(args['cst']) arg_fun = [] @@ -446,7 +524,10 @@ class System: args_vars + args_cst + arg_fun, line) + def set_init_fun_from_dict(self, node): + """For each initialisation equation, set the appropriate function to the System. + """ args = self.eqs['init'][node]['args'] line = self.eqs['init'][node]['line'] args_vars = [v for v, i in args['var']] @@ -465,6 +546,16 @@ class System: # Simulation def get_cst_val(self, cst, args): + """Get the value of a constant according to its equation and arguments. + + Parameters + ---------- + cst : str + Name of the constant. + + args : dict(str, value) + Values of each arguments. + """ fun = getattr(self, 'set_' + cst) try: value = None @@ -484,9 +575,21 @@ class System: f"Returned value: {value}")) from None def set_cst(self, cst, args): + """Set a constant according to its equation and arguments. + + Parameters + ---------- + cst : str + Name of the constant. + + args : dict(str, value) + Values of each arguments. + """ setattr(self, cst, self.get_cst_val(cst, args)) def set_all_csts(self): + """Set every constant constant according to its equation and arguments ONLY IF the constant is not set yet. + """ G = self.get_cst_graph() for cst in nx.topological_sort(G): try: @@ -503,14 +606,36 @@ class System: self.set_cst(cst, args) def generate_var(self, var, N): + """Initialise an empty array for a variable. + + Parameters + ---------- + var : str + Variable name. + + N : int + Size of simulation. + """ setattr(self, var, np.full(N, np.NAN)) def generate_all_vars(self, N): + """Initialise an empty array for every variable. + + Parameters + ---------- + N : int + Size of simulation. + """ for v in self.nodes['var']: self.generate_var(v, N) def set_special_fun(self, p): + """Set the special function from its parameters. + + p : dict + Parameters of the function to be set. + """ f_type = p['type'] for f_name, f_Class in [('delay3', Delay3), ('smooth', Smooth), @@ -536,9 +661,13 @@ class System: pass def step(self, hght, sttm, k): + """Step function. See specials.step. + """ return step(hght, sttm, self.time[k]) def clip(self, v2, v1, t, y): + """Clip function. See specials.clip. + """ return clip(v2, v1, t, y) def set_all_special_functions(self): @@ -553,6 +682,8 @@ class System: self.set_special_fun(fun_args) def _init_all(self): + """Initialise every variable. Iterate over the initialisaition order and set the first value of each variable. + """ for var in nx.topological_sort(self.get_init_graph()): if self.is_initialized(var): update_fun = getattr(self, 'init_' + var) @@ -594,11 +725,11 @@ class System: line)) raise(AssertionError(msg)) - self.update(var, 0, update_fun, {**var_args, **cst_args, **fun_args}) + self._update_variable(var, 0, update_fun, {**var_args, **cst_args, **fun_args}) def set_update_loop(self): """ - Set the update loop list. + Set the update loop list, stored as _update_loop. The list contains all information we need to update each variable, and is ordered in the topological order of the updating graph """ @@ -642,18 +773,39 @@ class System: def _update_all_fast(self, k): + """Update every variable. Iterate over the updating order graph and set the k-th value of each variable. + + Parameters + ---------- + k : int + Current number of step. """ - Use the update loop list to update every equation. - """ + for u in self._update_loop: if 'k' in u['fun_args']: u['fun_args']['k'] = k - self.update(u['var'], k, u['update_fun'], + self._update_variable(u['var'], k, u['update_fun'], {**{v: t[k+i] for v, (t, i) in u['var_args'].items()}, **u['cst_args'], **u['fun_args']}) - def update(self, var, k, fun, args): + def _update_variable(self, var, k, fun, args): + """Update a variable at step `k` from its function and arguments. + + Parameters + ---------- + var : str + Variable name. + + k : int + Step number. + + fun : function + Function which updates variable. + + args : dict(str, value) + Arguments of the function with their values. + """ try: # getattr(self, var)[k] = fun(**args) # For (small) gain in perfs, uncomment above and comment beside @@ -697,7 +849,7 @@ class System: fun = getattr(self, f_name) var = f_name.split('_', 1)[1] if ('__okdic__' not in dir(fun)) or (not fun.__okdic__): - self.add_node(var, 'var') + self.nodes['var'].add(var) args = inspect.getargs(fun.__code__).args if fun.__doc__: self.comments[var] = fun.__doc__ @@ -713,6 +865,8 @@ class System: fun.__okdic__ = True def assert_update_acyclic(self): + """Assert that the updating graph is acyclic, and print the cycle in case there is some. + """ G = self.get_update_graph() b = '\\' assert nx.is_directed_acyclic_graph(G), \ @@ -723,6 +877,8 @@ class System: + "\nPlease design an update scheme that is not cyclic." def assert_init_acyclic(self): + """Assert that the initialisation graph is acyclic, and print the cycle in case there is some. + """ G = self.get_init_graph() if not nx.is_directed_acyclic_graph(G): msg = "Initialisation is not acyclic:\n" @@ -786,63 +942,6 @@ class System: setattr(s, cst, getattr(self, cst)) return s - def get_out_nodes(self, node, with_definitions=False): - """Returns the list of the nodes using the node to be computed. - - Parameters - ---------- - 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.definition(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. - - Parameters - ---------- - 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.definition(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" - assert var in dir(self), "Simulation hasn't been run yet !" - assert t >= self.initial_time and t <= self.final_time, "{t} is out of bounds {self.initial_time}, {s.final_time}" - - 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) - return v[idx1]*(1 - dd) + v[idx1 + 1]*dd - - def __getitem__(self, arg): - try: - name, year = arg - return self.get_at(name, year) - - except TypeError: - return getattr(self, arg) def get_different_csts(self): """Returns all variables which value is different thatn in the equations and their new values."""