From 8464f1d534e2a4627fe70703ad53ba59de8ae689 Mon Sep 17 00:00:00 2001 From: abaucher <achille.baucher@inria.fr> Date: Mon, 21 Mar 2022 12:56:10 +0100 Subject: [PATCH] Useful functions inside System, and politics in an other file --- afaire.md | 4 +- pydynamo/__init__.py | 4 +- pydynamo/core/parse_system.py | 228 ++------------------------------ pydynamo/core/plot_system.py | 166 +++++++++++++++++++---- pydynamo/core/politics.py | 124 +++++++++++++++++ pydynamo/core/psdsystem.py | 2 +- pydynamo/core/system.py | 184 ++++++++++++++++++++------ pydynamo/world2/__init__.py | 2 - pydynamo/world3/world3_class.py | 167 ++--------------------- 9 files changed, 431 insertions(+), 450 deletions(-) create mode 100644 pydynamo/core/politics.py diff --git a/afaire.md b/afaire.md index 2c3af311..1b88ec5a 100644 --- a/afaire.md +++ b/afaire.md @@ -3,4 +3,6 @@ - [ ] Générer la documentation pydynamo - [ ] tabhl se nomme avec le nom de la variable attribuée - [ ] Une seule fonction spéciale DYNAMO admise dans une équation -- [ ] New table politic, c'est la variable qui est rentréer en paramètre. +- [ ] New table politic, c'est la variable qui est rentrée en paramètre. +- [x] Assertions que le system est bien run +- [ ] Deux change politic marche pas diff --git a/pydynamo/__init__.py b/pydynamo/__init__.py index db99ea1a..1133fcad 100644 --- a/pydynamo/__init__.py +++ b/pydynamo/__init__.py @@ -2,9 +2,7 @@ __version__ = "0.1" -from pydynamo.core.parse_system import new_cst_politic, new_table_politic, new_var_politic,new_system, system_from_lines, system_from_fun, system_from_file -# import pydynamo.core.system -from pydynamo.core.plot_system import * +from pydynamo.core.system import System # import pydynamo.core.dynamo_converter from pydynamo.core import psdsystem from .world3 import World3 diff --git a/pydynamo/core/parse_system.py b/pydynamo/core/parse_system.py index 63af1a83..dfe972c8 100644 --- a/pydynamo/core/parse_system.py +++ b/pydynamo/core/parse_system.py @@ -1,22 +1,20 @@ """Functions to parse an entire pydynamo code and generate a System object. Also defines every political changes. """ import ast -import re import inspect from .parse_equations import * from .parse_dynamo_functions import get_dynamo_fun_params from .specials import step, clip -from .system import System -def get_comment(line): +def comment_from_equation(line): """Retrieve comment by removing '#' and additional spaces. """ if '#' in line: return line.split('#', 1)[1].strip() return '' -def get_nodes_eqs_dicts(lines): +def get_system_dicts(lines): """ Parameters @@ -52,7 +50,7 @@ def get_nodes_eqs_dicts(lines): raise Exception("Error while parsing line:\n"+l) # Add comment - com = get_comment(l) + com = comment_from_equation(l) if com != '' or node not in comments: comments[node] = com @@ -81,87 +79,27 @@ def get_nodes_eqs_dicts(lines): if root.body and not type_is_identified: raise(SyntaxError(f"Invalid equation:\n {l}")) - return all_nodes, all_eqs, comments -def system_from_nodes_eqs(nodes, eqs, comments, s=None, prepare=True): - """Get a system from nodes, equations and comments dictionnaries. If a system is given, just add this equations, nodes and comments to it. - - Parameters - ---------- - nodes : dict - Node (variables, constants or functions) names. - eqs : dict - Equations (constant, initialisation or updates). - comments : dict(str: str) - Comments of each node. - s : System - System to which we add changes. - prepare : bool - If True, set all constants, and functions (constants, update and init), but not special functions. - """ - # In case a System is given, eventually change variables to constants and vice-versa - if s: - for n, args in eqs['cst'].items(): - if n in s.eqs['update']: - change_var_to_cst(s, n) - for n, args in eqs['update'].items(): - if n in s.eqs['cst']: - change_cst_to_var(s, n) - if not s: - s = System() - - s.add_all_nodes(nodes) - s.add_all_eqs(eqs) - s.add_comments(comments) - - if prepare: - s.prepare() - - return s - -def system_from_lines(lines, s=None, prepare=True): - """Get a system from a list of pydynamo equations. If a system is given, just add this equations to it. - - Parameters - ---------- - lines : iterable(str) - List of every pydynamo equations. - s : System - System to which we add changes. - prepare : bool - If True, set all constants, and functions (constants, update and init), but not special functions. - """ - nodes, eqs, comments = get_nodes_eqs_dicts(lines) - return system_from_nodes_eqs(nodes, eqs, comments, s, prepare) - -def system_from_file(filename, s=None, prepare=True): - """Get a system from a file with pydynamo equations. If a system is given, just add this equations to it. +def list_from_file(filename, s=None, prepare=True): + """Get a list of equatinos from a file with pydynamo equations. Parameters ---------- filename : str Files in which every pydynamo equations are written. - s : System - System to which we add changes. - prepare : bool - If True, set all constants, and functions (constants, update and init), but not special functions. """ check_file(filename) with open(filename, 'r') as f: - return system_from_lines(f.readlines(), s, prepare) + return f.readlines() -def system_from_fun(fun, s=None, prepare=True): - """Get a system from a function which lists pydynamo equations. If a system is given, just add this equations to it. +def list_from_function(fun, s=None, prepare=True): + """Get a list of equations from a function which lists pydynamo equations. Parameters ---------- fun : function Function in which every pydynamo equations are written. - s : System - System to which we add changes. - prepare : bool - If True, set all constants, and functions (constants, update and init), but not special functions. Examples -------- @@ -169,11 +107,9 @@ def system_from_fun(fun, s=None, prepare=True): >>> def custom_equations(): >>> pop.i = 100 >>> pop.k = pop.j /2 # Population - >>> custom_system = system_from_fun(custom_equations) + >>> list_of_equations = list_from_function(custom_equations) """ - lines =[l.strip() - for l in inspect.getsource(fun).split('\n')[1:]] - return system_from_lines(lines, s, prepare) + return [l.strip() for l in inspect.getsource(fun).split('\n')[1:]] def check_file(filename): """Check if every line in the file can be parsed by the ast module. If not, raise an error. @@ -190,147 +126,3 @@ def check_file(filename): e.filename = filename e.lineno = i + 1 raise - -def new_system(raw, s=None): - """Create a new system from either a file, a list, or a function, including pydynamo equations. - - Parameters - ---------- - raw : str, iterable(str) or function - If str, open the file named `raw` and read pydynamo equations inside (see parse_system.system_from_file`). If iterable(str), each element is a pydynamo equations (see `parse_system.system_from_lines`). If function, read pydynamo equations written inside (see `parse_system.system_from_fun`). - - Returns - ------- - System - System object. - """ - if callable(raw): - return system_from_fun(raw, s=s) - elif isinstance(raw, list): - return system_from_lines(raw, s=s) - elif isinstance(raw, str): - return system_from_file(raw, s=s) - -def new_cst_politic(s, cst, date, new_value): - """Add a new equations to `s` for the constant `cst`, which becomes a variable changing from its former value to `new_value` after the `date`. - - Parameters - ---------- - s : System - The system to implement the new politic. - cst : str - Constant name. - date : float - Date of politic application. - new_value : float - The new value that `cst` will take after the date `date`. - """ - assert cst in s.eqs['cst'], f"{cst} is not a constant" - initval= s.eqs['cst'][cst]['line'] - del s.eqs['cst'][cst] - - # New equations - eq_clip = f"{cst}.k = clip({cst}2, {cst}1, time.k, {cst}_date) # {s.get_comment(cst)} " - eq_date = f"{cst}_date = {date} # Date of {cst} change" - eq_cst1 = f"{cst}1 = {initval} # {s.get_comment({cst})} before {cst}_date" - eq_cst2 = f"{cst}2 = {new_value} # {s.get_comment({cst})} after {cst}_date" - - # Change calls from cst to var - change_cst_to_var(s, cst) - - # Insert new equations - system_from_lines([eq_clip, eq_date, eq_cst1, eq_cst2], s=s, prepare=True) - - -def new_table_politic(s, var, date, new_value): - """Add a new equations to `s` for the table used by the variable `var`. The table changes from its former value to `new_value` after the `date`. - - Parameters - ---------- - s : System - The system to implement the new politic. - var : str - Variable name. - date : float - Date of politic application. - new_value : list(float) - The new value that the table will take after the date `date`. - """ - assert f'tabhl' in s.eqs['update'][var]['args']['fun'], f"{var} hasn't tabhl function" - table_name = s.eqs['update'][var]['args']['fun']['tabhl']['table'] - table_init_val = s.eqs['cst'][table_name]['line'] - var_line = s.eqs['update'][var]['raw_line'] - - # Define new equations - eq_table_1 = f"{table_name}1 = {table_init_val} # {s.get_comment(table_name)} before {date}" - eq_table_2 = f"{table_name}2 = {list(new_value)} # {s.get_comment(table_name)} after {date}" - eq_var_1 = var_line.replace(f'{var}.k', f'{var}1.k').replace(table_name, table_name + '1') - eq_var_2 = var_line.replace(f'{var}.k', f'{var}2.k').replace(table_name, table_name + '2') - eq_var = f"{var}.k = clip({var}2.k, {var}1.k, time.k, {date}) # {s.get_comment(var)}" - - # Insert new equations - system_from_lines([eq_table_1, eq_table_2, eq_var_1, eq_var_2, eq_var], s=s) - -def new_var_politic(s, var, date, eq2): - """Add a new equations to `s` for the variable `cst`, which changes from its former value to `new_value` after the `date`. - - Parameters - ---------- - s : System - The system to implement the new politic. - var : str - Variable name. - date : float - Date of politic application. - new_value : str - The new value, written with the pydynamo syntax, that the variable `var` will take after the date `date`. - """ - assert var in s.eqs['update'], f"{var} is not a variable" - first_line = s.eqs['update'][var]['raw_line'].split('=')[1] - line_init = None - if var in s.eqs['init']: - line_init = s.eqs['init'][var]['raw_line'].split('=')[1] - del s.eqs['update'][var] - - # Define new equations - eq_clip = f"{var}.k = clip({var}2.k, {var}1.k, time.k, {var}_date) # {s.get_comment(var)}" - eq_date = f"{var}_date = {date} # Date of {var} change" - eq_var1 = f"{var}1.k = {first_line} # {s.get_comment(var)} before {var}_date" - eq_var2 = f"{var}2.k = {eq2} # {s.get_comment(var)} after {var}_date" - eq_init1 = '' - eq_init2 = '' - if line_init: - eq_init1 = f"{var}1.i = {line_init}" - eq_init2 = f"{var}2.i = 0" - - # Insert new equations - system_from_lines([eq_clip, eq_date, eq_var1, eq_var2, eq_init1, eq_init2], s=s, prepare=True) - - -def change_cst_to_var(s, cst): - # TODO: FAIRE AVEC INIT - """Change a constant for a variable. TODO: Write documentation.""" - s.nodes['cst'].remove(cst) - for v, args in s.eqs['update'].items(): - if cst in args['args']['cst']: - args['args']['cst'].remove(cst) - args['args']['var'].add((cst, 'k')) - new_line = re.sub(f'(?<!\w){cst}(?!\w)', f'{cst}_k', args['line']) - new_raw_line = re.sub(f'(?<!\w){cst}(?!\w)', f'{cst}.k', args['raw_line']) - args['raw_line'] = new_raw_line - args['line'] = new_line - - -def change_var_to_cst(s, var): - """Change a variable for a constant. TODO: Write documentation.""" - pass - - # s.nodes['var'].remove(var) - # for v, args in s.eqs['cst'].items(): - # if var in args['args']['var']: - # args['args']['var'].remove(var) - # args['args']['cst'].add((var, 'k')) - # new_line = re.sub(f'(?<!\w){var}(?!\w)', f'{var}_k', args['line']) - # new_raw_line = re.sub(f'(?<!\w){var}(?!\w)', f'{var}.k', args['raw_line']) - # args['raw_line'] = new_raw_line - # args['line'] = new_line diff --git a/pydynamo/core/plot_system.py b/pydynamo/core/plot_system.py index a98d81ab..ca3ddf35 100644 --- a/pydynamo/core/plot_system.py +++ b/pydynamo/core/plot_system.py @@ -1,21 +1,66 @@ +"""Functions used by a System instance to plot curves. + +""" + import matplotlib.pyplot as plt -from pyvis.network import Network -import numpy as np +from pyvis.network import Network as pyvisNetwork + +def plot(self, v_names=None, rescale=False, show_comments=True, filter_no=None, scales=None, colors=None, title='', linestyle='-', outside_legend_number=2, legend=True): + """Plot the curves of the last simulation for indicated variables. + + Parameters + ---------- + v_names : iterable(str) + Variable to plot names. If None, all variables are plotted. + + rescale : bool + If yes, all curves are normalized between 0 and 1. + + show_comments: bool + If yes, comments are shown in the legend. + + filter_no: iterable(str) + Names of variables that won't appear in the plot. + + scales: dict(str, float) + Scales of variables. Variables are divided by their respective scales on the plot. + + colors: dict(str, str) + Colors of each variable. + + title: str + Title of the plot. + + linestyle: str + Linestyle of the plot. + + outside_legend_number: int + Number of lines from which legend is plotted outside the graph. + + legend: bool + If yes, the legend is drawn. + """ + + assert 'time' in dir(self), "No simulation have been run for the system !" -def plot_system(s, v_names=None, rescale=False, com=True, filter_no=None, scales=None, colors=None, title='', linestyle='-', outside_legend_number=2, legend=True): - assert 'time' in dir(s), "Aucune simulation n'a été lancée pour le système !" if not v_names: - v_names = s.get_all_variable_names() + v_names = self.get_all_variable_names() + if isinstance(v_names, str): v_names = [v_names] + if filter_no: v_names = [n for n in v_names if n not in filter_no] + for name in v_names: - v = getattr(s, name) + v = getattr(self, name) + + # Case it's a constant try: v[0] except: - v = v+s.get_time()*0 + v = v+self.get_time()*0 + if scales and name in scales: try: v = v/scales[name] @@ -23,67 +68,132 @@ def plot_system(s, v_names=None, rescale=False, com=True, filter_no=None, scales v = 1 + 0 * v elif rescale: v = v/max(abs(v)) - label = name - if com: - label = label + ' (' + s.get_comment(name).split('\n')[0].capitalize() + ')' + + label = name + if show_comments: + label = label + ' (' + self.definition(name).split('\n')[0].capitalize() + ')' try: color = colors[name] except: color = None - plt.plot(s.get_time(), v, label=label, color=color, linestyle=linestyle) + + plt.plot(self.get_time(), v, label=label, color=color, linestyle=linestyle) + if rescale: plt.ylabel('Rescaled values') + plt.xlabel('Time') + if legend: if len(v_names) > outside_legend_number: plt.legend(loc='center left', bbox_to_anchor=[1, 0.8]) else: plt.legend(loc='center left') + plt.title(title) -def show_pyvis(s, init_val=True, notebook=False, options=None, highlight=None, colors=None): - G = s.get_influence_graph() - Gq = Network(notebook=notebook, directed=True) +def show_influence_graph(self, show_init_val=True, in_notebook=False, options=None, colors=None): + """Show variables influence newtork with the Pyvis library. + + Parameters + ---------- + show_init_val : bool + If True, show the initial value for a variable. + + in_notebook : bool + If True, network appears as a Widget in the notebook. + + options : dict + Pyvis options. + + colors : dict + Colors of each variable and constant. + """ + + G = self.get_influence_graph() + Gq = pyvisNetwork(notebook=in_notebook, directed=True) + for a in G.nodes: - com = s.get_comment(a).split('\n')[0] + com = self.definition(a).split('\n')[0] node = a - title = f"{com}<br>= {s.get_eq(a)}" + title = f"{com}<br>{self.raw_equation(a)}" col = colors[a] if colors and a in colors else None - # if init_val == True and a in dir(s): - # title = f'{title}<br>Init: {getattr(s, a)[0]:.2f}' + if show_init_val == True: + if a in self.nodes['var']: + title = f'{title}<br>Init: {getattr(self, a)[0]:.2f}' + Gq.add_node(node, title=title, color=col) + for a, b in G.edges: nodea, nodeb = (a, b) Gq.add_edge(nodea, nodeb) + if options: Gq.show_buttons(options) + return Gq -def plot_non_linearity(s, name): - assert 'time' in dir(s), "No simulation yet! Please run the system." - x, y, ylabel, xlabel, title = s.get_tabhl_args(name) +def plot_non_linearity(self, var): + """Plot the non linear functions with which the variable is computed. + + Parameters + ---------- + name : str + Variable name. + """ + + assert 'time' in dir(self), "No simulation yet! Please run the system." + + x, y, ylabel, xlabel, title = self.get_tabhl_args(var) + if x is not None and y is not None: plt.plot(x, y) plt.ylabel(ylabel) plt.xlabel(xlabel) plt.title(title) -def compare_systems(s1, s2, v_names, scales=None, rescale=False, *args, **kwargs): - assert 'time' in dir(s1) and 'time' in dir(s2), "No simulation yet! Please run the system." +def plot_compare(self, s2, v_names, scales=None, rescale=False, *args, **kwargs): + """Show the variables of 2 different systems. + + Parameters + ---------- + s2: System + Other system to compare whith. + + v_names: iterable(str) + Names of variables or constant to plot. + + scales: dict(str, float) + Scales of variables. Variables are divided by their respective scales on the plot. + + rescale: bool + If yes, If yes, variables are normalized between 0 and 1. + + *args + Argument list for the pydynamo.core.plot_system.plot function. + + **kwargs + Argument dictionnary for the pydynamo.core.plot_system.plot function. + """ + + assert 'time' in dir(self) and 'time' in dir(s2), "No simulation yet! Please run the system before." + # remove tables - v_names = [v for v in v_names if not isinstance(getattr(s1,v), list)] + v_names = [v for v in v_names if not isinstance(getattr(self,v), list)] + if not scales and rescale: scales= {} for v in v_names: try: - scales[v] = max(max(getattr(s1, v)), max(getattr(s2, v))) + scales[v] = max(max(getattr(self, v)), max(getattr(s2, v))) except TypeError: - scales[v] = max(getattr(s1, v), getattr(s2, v)) - plot_system(s1, v_names, *args, **kwargs, linestyle='-', scales=scales) + scales[v] = max(getattr(self, v), getattr(s2, v)) + + plot(self, v_names, *args, **kwargs, linestyle='-', scales=scales) plt.gca().set_prop_cycle(None) - plot_system(s2, v_names, *args, **kwargs, linestyle='--', scales=scales, legend=False) + plot(s2, v_names, *args, **kwargs, linestyle='--', scales=scales, legend=False) diff --git a/pydynamo/core/politics.py b/pydynamo/core/politics.py new file mode 100644 index 00000000..a4a6c4ef --- /dev/null +++ b/pydynamo/core/politics.py @@ -0,0 +1,124 @@ +import re + +def new_politic(self, name, date, new_val): + """Implements a new politic for some constant, table or variable from a certain date. + + Parameters + ---------- + name: str + Name of a constant, table or variable we want to change. + + date: float + date from which the new value will be activated. + + new_val: float, array or string + If name refers to a constant, a float with the new value. If name srefers to a table, an array of the sime size as the older one. If name refers to a variable, a string with the new value. + + """ + if name in self.nodes['cst']: + n = getattr(self, name) + if '__iter__' in dir(n): + self.new_table_politic(name[:-1], date, new_val) + else: + self.new_cst_politic(name, date, new_val) + elif name in self.nodes['var']: + self.new_var_politic(name, date, new_val) + else: + raise NameError(f'No variable or constant named {name} in the System') + +def new_cst_politic(self, cst, date, new_value): + """Add a new equations to `s` for the constant `cst`, which becomes a variable changing from its former value to `new_value` after the `date`. + + Parameters + ---------- + s : System + The system to implement the new politic. + cst : str + Constant name. + date : float + Date of politic application. + new_value : float + The new value that `cst` will take after the date `date`. + """ + assert cst in self.eqs['cst'], f"{cst} is not a constant" + initval= self.eqs['cst'][cst]['line'] + del self.eqs['cst'][cst] + + # New equations + eq_clip = f"{cst}.k = clip({cst}2, {cst}1, time.k, {cst}_date) # {self.definition(cst)} " + eq_date = f"{cst}_date = {date} # Date of {cst} change" + eq_cst1 = f"{cst}1 = {initval} # {self.definition({cst})} before {cst}_date" + eq_cst2 = f"{cst}2 = {new_value} # {self.definition({cst})} after {cst}_date" + + # Change every call of cst to a call of cst.k + for i in range(len(self.code_lines)): + self.code_lines[i] = re.sub(f'(?<!\w){cst}(?!\w)', f'{cst}.k', self.code_lines[i]) + + self.add_equations([eq_clip, eq_date, eq_cst1, eq_cst2]) + self.reset_eqs() + + +def new_table_politic(s, var, date, new_value): + """Add a new equations to `s` for the table used by the variable `var`. The table changes from its former value to `new_value` after the `date`. + + Parameters + ---------- + s : System + The system to implement the new politic. + var : str + Variable name. + date : float + Date of politic application. + new_value : list(float) + The new value that the table will take after the date `date`. + """ + assert f'tabhl' in self.eqs['update'][var]['args']['fun'], f"{var} hasn't tabhl function" + table_name = self.eqs['update'][var]['args']['fun']['tabhl']['table'] + table_init_val = self.eqs['cst'][table_name]['line'] + var_line = self.eqs['update'][var]['raw_line'] + + # Define new equations + eq_table_1 = f"{table_name}1 = {table_init_val} # {self.definition(table_name)} before {date}" + eq_table_2 = f"{table_name}2 = {list(new_value)} # {self.definition(table_name)} after {date}" + eq_var_1 = var_line.replace(f'{var}.k', f'{var}1.k').replace(table_name, table_name + '1') + eq_var_2 = var_line.replace(f'{var}.k', f'{var}2.k').replace(table_name, table_name + '2') + eq_var = f"{var}.k = clip({var}2.k, {var}1.k, time.k, {date}) # {self.definition(var)}" + + self.add_equations([eq_table_1, eq_table_2, eq_var_1, eq_var_2, eq_var]) + self.reset_eqs() + +def new_var_politic(self, var, date, eq2): + """Add a new equations to `s` for the variable `var`, which changes from its former value to `new_value` after the `date`. + + Parameters + ---------- + s : System + The system to implement the new politic. + var : str + Variable name. + date : float + Date of politic application. + new_value : str + The new value, written with the pydynamo syntax, that the variable `var` will take after the date `date`. + """ + assert var in self.eqs['update'], f"{var} is not a variable" + first_line = self.eqs['update'][var]['raw_line'].split('=')[1] + line_init = None + if var in self.eqs['init']: + line_init = self.eqs['init'][var]['raw_line'].split('=')[1] + del self.eqs['update'][var] + + # Define new equations + eq_clip = f"{var}.k = clip({var}2.k, {var}1.k, time.k, {var}_date) # {self.definition(var)}" + eq_date = f"{var}_date = {date} # Date of {var} change" + eq_var1 = f"{var}1.k = {first_line} # {self.definition(var)} before {var}_date" + eq_var2 = f"{var}2.k = {eq2} # {self.definition(var)} after {var}_date" + eq_init1 = '' + eq_init2 = '' + if line_init: + eq_init1 = f"{var}1.i = {line_init}" + eq_init2 = f"{var}2.i = 0" + + self.add_equations([eq_clip, eq_date, eq_var1, eq_var2, eq_init1, eq_init2]) + self.reset_eqs() + diff --git a/pydynamo/core/psdsystem.py b/pydynamo/core/psdsystem.py index 2d97191c..0d8de923 100644 --- a/pydynamo/core/psdsystem.py +++ b/pydynamo/core/psdsystem.py @@ -74,7 +74,7 @@ class PsdSystem(System): def run(self): self.df_run = self.model.run() - def get_eq(self, name): + def equation(self, name): try: return self.caracs[name]['Original Eqn'] except: diff --git a/pydynamo/core/system.py b/pydynamo/core/system.py index 3c68050b..ec18245c 100644 --- a/pydynamo/core/system.py +++ b/pydynamo/core/system.py @@ -7,6 +7,7 @@ import re from .specials import clip, Sample, step, Interpol from .delays import Delay3, Dlinf3, Smooth from .parse_dynamo_functions import instance_fun_names +from .parse_system import get_system_dicts, list_from_function, list_from_file from math import log, exp, sqrt np.seterr(all='raise') @@ -22,22 +23,84 @@ 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 + + def __init__(self, code=None, prepare=True): + """Initialise a System, empty or from pydynamo code. - def __init__(self): - """Initialise an empty System with nodes and equations dictionnaries""" - self.nodes = { - 'cst': set(), - 'var': set(), - 'fun': set(), - } + Parameters + ---------- + code : str, iterable(str) or function + If str, open the file named `code` and read pydynamo equations inside (see `parse_system.nec_from_file`). If iterable(str), each element is a pydynamo equations (see `parse_system.nec_from_lines`). If function, read pydynamo equations written inside the function (see `parse_system.nec_system_from_fun`). + + prepare : bool + If True, prepare the System to run (see `System.prepare`). + + Examples + -------- + With a file: + Suppose there is the following lines inside the file `pydynamo_equations.py`: + ``` + pop.k = pop.j*2 # Population + pop.i = popi + popi = 25 # Initial population + ``` + >>> s = System("pydynamo_equations.py') + + With a list of equations: + >>> equations_list = ['pop.k = pop.j*2', 'pop.i = popi', 'popi = 25'] + >>> s = System(equations_list) + + With a function: + >>> def equations_function(): + >>> pop.k = pop.j*2 # Population + >>> pop.i = popi + >>> popi = 25 # Initial population + >>> + >>> s = System(equations_function) + """ - self.eqs = { - 'cst': dict(), - 'update': dict(), - 'init': dict() - } - self.comments = {} + # If code is given, create a System with equations + if code: + if isinstance(code, list): + self.code_lines = code + elif callable(code): + self.code_lines = list_from_function(code) + elif isinstance(code, str): + self.code_lines = list_from_file(code) + self.reset_eqs(prepare) + + # Otherwise, create an empty System + else: + self.nodes = { + 'cst': set(), + 'var': set(), + 'fun': set(), + } + + self.eqs = { + 'cst': dict(), + 'update': dict(), + 'init': dict() + } + self.comments = {} + self.code_lines = [] + + 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. + """ + self.code_lines = self.code_lines + new_code_lines + + def reset_eqs(self, prepare=True): + """Set all nodes, equations and comments. + """ + 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) @@ -63,9 +126,6 @@ class System: def add_comments(self, comments): for node, comment in comments.items(): self.comments[node] = comment - - def add_units(self, units): - self.units = units def add_function(self, fun, name=None): if not name: @@ -163,10 +223,10 @@ class System: assert 'tabhl_' + name in dir(self), 'Error, no such tabhl function' f = getattr(self, 'tabhl_' + name) x = np.linspace(f.xl, f.xh, 100) - ylabel = name + '\n'+ self.get_comment(name) + ylabel = name + '\n'+ self.definition(name) argop = self.eqs['update'][name]['args']['fun']['tabhl']['val'].strip() argop = re.sub('^\(|\)$', '',re.sub('\.[jk]', '', argop)).strip() - xlabel = argop + ('\n'+ self.get_comment(argname) if argop==argname else '') + 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): @@ -178,29 +238,49 @@ class System: return self.eqs[eq_type] - def get_comment(self, node): + def definition(self, node): + """Get the definition of a node. + """ try: return self.comments[node] except: return '' - def get_unit(self, node): - try: - return self.units[node] - except: - return '' - def get_eq(self, var): + def equation(self, node): + """Returns the reformatted equation of a variable or constant. + + Parameters + ---------- + node : str + Name of the variable or constant to plot the equation. + """ + for t, eqq in self.eqs.items(): - if var in eqq: + if node in eqq: try: - if 'tabhl' in eqq[var]['raw_line']: - return f'{var}.k = ' + re.sub('tabhl', f'NLF_{var}t', re.sub('\_([jk])', '.\\1', eqq[var]['line'])) - return eqq[var]['raw_line'] + if 'tabhl' in eqq[node]['raw_line']: + return f'{node}.k = ' + re.sub('tabhl', f'NLF_{node}t', re.sub('\_([jk])', '.\\1', eqq[node]['line'])) + return self.raw_equation(node) except: - print(var, eqq) + print(node, eqq) return '' + def raw_equation(self, node): + """Returns the pydynamo raw equation of a variable or constant. + + Parameters + ---------- + node : str + Name of the variable or constant to plot the equation. + """ + for t, eqq in self.eqs.items(): + try: + return eqq[node]['raw_line'] + except: + pass + return '' + # Infos def is_initialized(self, var): return var in self.eqs['init'] @@ -262,6 +342,8 @@ class System: return G 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. + """ G = nx.DiGraph() for v in self.eqs['update']: G.add_node(v) @@ -290,6 +372,7 @@ class System: if ac in self.eqs['cst']: G.add_edge(ac, c) return G + # Assertions def assert_cst_defined(self): for c in self.nodes['cst']: @@ -449,11 +532,14 @@ class System: fun = Sample(isam, self.time) setattr(self, p['fun'], fun) - if f_type == 'step': + if f_type == 'step' or type =='clip': pass def step(self, hght, sttm, k): return step(hght, sttm, self.time[k]) + + def clip(self, v2, v1, t, y): + return clip(v2, v1, t, y) def set_all_special_functions(self): """ @@ -539,7 +625,7 @@ class System: line = self.eqs['update'][var]['line'] raise(AttributeError(f"In updating {var}:\n" f"{var} = {line}\n" - f"{e}")) from None + f"{e}")) from e # Assert that there is no variables considered as constants etc. tps = {'cst', 'var', 'fun'} @@ -589,6 +675,8 @@ class System: def prepare(self): + """Assert that all equations are well defined, ant that the updating graph is acyclic. Also set all updating functions and constants. +""" self.assert_cst_defined() self.assert_init_defined() self.assert_update_defined() @@ -685,16 +773,17 @@ class System: self._update_all_fast(k) def copy(self, prepare=True): - """Returns a copy of the model""" - s = System() - s.add_all_nodes(self.nodes) - s.add_all_eqs(self.eqs) - s.add_comments(self.comments) - if prepare: - s.prepare() + """Returns a copy of the model, with + + Parameters + ---------- + prepare : bool + If yes, prepare the system to run. + """ + + s = System(self.code_lines) for cst in self.nodes['cst']: setattr(s, cst, getattr(self, cst)) - return s def get_out_nodes(self, node, with_definitions=False): @@ -711,7 +800,7 @@ class System: 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} + return {a: self.definition(a) for a in out_nodes} else: return out_nodes @@ -729,13 +818,14 @@ class System: 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} + 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: @@ -746,6 +836,14 @@ class System: 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.""" dic_cst = {cst: getattr(self, cst) for cst in self.nodes['cst'] if cst in dir(self)} @@ -760,4 +858,4 @@ class System: return False except: return any(a!=b) return {cst: (v1, v2) for cst, (v1, v2) in both.items() if diff(v1, v2)} - + diff --git a/pydynamo/world2/__init__.py b/pydynamo/world2/__init__.py index f8b14630..95efb10a 100644 --- a/pydynamo/world2/__init__.py +++ b/pydynamo/world2/__init__.py @@ -14,5 +14,3 @@ def get_w2(): w2.add_comments(w2_defs) return w2 -def plot_w2(w2, title=''): - plot_system(w2, scales_w2, scales=scales_w2, title=title) diff --git a/pydynamo/world3/world3_class.py b/pydynamo/world3/world3_class.py index 5d3b37ef..a5df591d 100644 --- a/pydynamo/world3/world3_class.py +++ b/pydynamo/world3/world3_class.py @@ -3,80 +3,33 @@ Define World3 class """ from pydynamo.core.system import System -from pydynamo.core.plot_system import * -from pydynamo.core.parse_system import new_cst_politic, new_table_politic, new_var_politic,new_system, system_from_lines, system_from_fun, system_from_file from .data_world3 import w3_code, w3_defs, var_color -from .plot_utils import plot_world_03, plot_world_with_scales +from .plot_utils import plot_world_03 from .scenarios_limits_to_growth import scenarios -def get_w3(): - w3 = system_from_lines(w3_code) - w3.add_comments(w3_defs) - return w3 - -def get_scenario(number): - w3s = get_w3() - changes = scenarios[number - 1]['changes'] - for c, v in changes.items(): - setattr(w3s, c, v) - return w3s, scenarios[number - 1]['title'] - - class World3(System): """ A World3 object is a System object with more convenient functions and defaults, adapted for the manipulation of the World3 model 2003's version equations. """ - def __init__(self, scenario_number=2, additional_equations=None, sys=None): - """Initialise a World3 object. By default, the scenario number is the second one, because it's the most "realistic" when we compare to the current situation.""" - s, title = get_scenario(scenario_number) - if sys: - s = sys - self.eqs = s.eqs - self.nodes = s.nodes - self.comments = s.comments - self.prepare() - for cst in self.nodes['cst']: - setattr(self, cst, getattr(s, cst)) - if additional_equations: - new_system(additional_equations, s=self) - - def new_politic(self, name, date, new_val): - """Implements a new politic for some constant, table or variable from a certain date. - - Parameters - ---------- - name: str - Name of a constant, table or variable we want to change. - - date: float - date from which the new value will be activated. - - new_val: float, array or string - If name refers to a constant, a float with the new value. If name srefers to a table, an array of the sime size as the older one. If name refers to a variable, a string with the new value. - + def __init__(self, scenario_number=2, sys=None): + """Initialise a World3 object. By default, the scenario number is the second one, because it's the most "realistic" when we compare to the current situation (in 2022). """ - if name in self.nodes['cst']: - n = getattr(self, name) - if '__iter__' in dir(n): - new_table_politic(self, name[:-1], date, new_val) - else: - new_cst_politic(self, name, date, new_val) - elif name in self.nodes['var']: - new_var_politic(self, name, date, new_val) - + self.code_lines = w3_code + changes = scenarios[scenario_number - 1]['changes'] + for cst, eq in changes.items(): + self.code_lines.append(f'{cst} = {eq}') + self.reset_eqs() + self.add_comments(w3_defs) + def copy(self): - """Returns a copy of the system, with same equations. - - Returns - ------- - World3: - Copy of the system. + """Returns a copy of the system, with the same equations and constant values. """ return World3(sys=super().copy()) def run(self, N=400, dt=0.5): - """Run the system with 400 steps of 1/2 year""" + """Run the system with 400 steps of 1/2 year. + """ super().run(N, dt) def plot_world(self, **kwargs): @@ -84,97 +37,3 @@ class World3(System): plot_world_03(self, with_legend=True, **kwargs) - def plot(self, *args, **kwargs): - """Plot the given variables and constants of a system. - - Parameters - ---------- - v_names: iterable(str) - Name of variables - - rescale: bool - If yes, variables are normalized between 0 and 1 - - com: bool - If yes, comments are shown in the legend - - filter_no: iterable(str) - Names of variables that won't appear in the plot - - scales: dict(str, float) - Scales of variables. Variables are divided by their respective scales on the plot. - - colors: dict(str, str) - Colors of each variable - - title: str - Title of the plot - - linestyle: str - Linestyle of the plot - - outside_legend_number: int - Number of lines from which legend is plotted outside the graph - - legend: bool - If yes, the legend is drawn - """ - - plot_system(self, *args, **kwargs) - - def definition(self, name): - """Returns the definition of a variable or constant.""" - return self.get_comment(name) - - def equation(self, name): - """Returns the pydynamo equation of a variable or constant.""" - return self.get_eq(name) - - def plot_non_linearity(self, var_name): - """Plot the non linear function used by the variable, if exists.""" - plot_non_linearity(self, var_name) - - def plot_compare(self, s, *args, **kwargs): - """Show the variables of 2 different systems. - - Parameters - ---------- - s: System - Other system to compare whith. - - v_names: iterable(str) - Names of variables or constant to plot - - scales: dict(str, float) - Scales of variables. Variables are divided by their respective scales on the plot. - - rescale: bool - If yes, If yes, variables are normalized between 0 and 1 - - *args - Argument list for the pydynamo.core.plot_utils.plot_system function - - **kwargs - Argument dictionnary for the pydynamo.core.plot_utils.plot_system function - """ - - compare_systems(self, s, *args, **kwargs) - - def __getitem__(self, arg): - try: - name, year = arg - return self.get_at(name, year) - - except TypeError: - return getattr(self, arg) - - def add_equations(self, equations): - """Add new equations to the system. Each variable uses the newest equation. - - Parameters - ---------- - equations: list(str) - New equations to add. - - """ - new_system(equations, self) -- GitLab