def parse_jacobian_declarations( ode, args, args_doc, params, ): jacobian_declaration = "" if not params.functions.jacobian.generate: return jacobian_declaration # Flatten jacobian params if not params.code.array.flatten: debug("Generating jacobian C-code, forcing jacobian array " "to be flattened.") params.code.array.flatten = True jacobian_declaration = jacobian_template.format( num_states=ode.num_full_states, args=args, args_doc=args_doc, jac_name=params.functions.jacobian.result_name, jacobian_function_name=params.functions.jacobian.function_name, ) return jacobian_declaration
def __setattr__(self, name, value): """ A magic function which will register expressions and simpler state expressions """ from modelparameters.sympytools import symbols_from_expr # If we are registering a protected attribute or an attribute # during construction, just add it to the dict if name[0] == "_" or not self._allow_magic_attributes: self.__dict__[name] = value return # If no expression is registered if (not isinstance(value, scalars + (sp.Number, ))) and not (isinstance( value, sp.Basic) and symbols_from_expr(value)): debug( "Not registering: {0} as attribut. It does not contain " "any symbols or scalars.".format(name), ) # FIXME: Should we raise an error? return # Check for special expressions expr, TYPE = special_expression(name, self.root) if TYPE == INTERMEDIATE: self.add_intermediate(name, value) elif TYPE == DERIVATIVE_EXPRESSION: # Try getting corresponding ODEObjects expr_name, var_name = expr.groups() expr_obj = self.root.present_ode_objects.get(expr_name) var_obj = self.root.present_ode_objects.get(var_name) # If the expr or variable is not declared in this ODE we # register an intermediate if expr_obj is None or var_obj is None: self.add_intermediate(name, value) else: self.add_derivative(expr_obj, var_obj, value) elif TYPE == STATE_SOLUTION_EXPRESSION: self.add_state_solution(expr, value) elif TYPE == ALGEBRAIC_EXPRESSION: # Try getting corresponding ODEObjects (var_name, ) = expr.groups() var_obj = self.root.present_ode_objects.get(var_name) if var_obj is None: self.add_intermediate(name, value) else: self.add_algebraic(var_obj, value)
def exec_ode(ode_str, name, **arguments): """ Execute an ode given by a str Arguments --------- ode_str : str The ode as a str name : str The name of ODE arguments : dict (optional) Optional arguments which can control loading of model """ # Dict to collect declared intermediates intermediate_dispatcher = IntermediateDispatcher() # Create an ODE which will be populated with data when ode file is loaded ode = ODE(name, intermediate_dispatcher) intermediate_dispatcher.ode = ode debug(f"Loading {ode.name}") # Create namespace which the ode file will be executed in _init_namespace(ode, arguments, intermediate_dispatcher) # Write str to file with open("_tmp_gotrand.ode", "w") as f: f.write(ode_str) # Execute file and collect with open("_tmp_gotrand.ode") as f: exec(compile(f.read(), "_tmp_gotrand.ode", "exec"), intermediate_dispatcher) os.unlink("_tmp_gotrand.ode") # Finalize ODE ode.finalize() # Check for completeness if not ode.is_complete: warning(f"ODE mode '{ode.name}' is not complete.") info(f"Loaded ODE model '{ode.name}' with:") for what in ["full_states", "parameters"]: num = getattr(ode, f"num_{what}") info(f"{('Num ' + what.replace('_', ' ')).rjust(20)}: {num}") return ode
def __call__(self, name): """ Return a child component, if the component does not excist, create and add one """ check_arg(name, str) # If the requested component is the root component if self == self.root and name == self.root.name: comp = self.root else: comp = self.children.get(name) if comp is None: comp = self.add_component(name) debug(f"Adding '{name}' component to {self}") else: self.root._present_component = comp.name return comp
def _recount(self, new_count=None, dependent=None): """ Method called when an object need to get a new count """ old_count = self._count if isinstance(new_count, (int, float)): check_arg(new_count, (int, float), ge=0, le=ODEObject.__count) self._count = new_count elif isinstance(dependent, ODEObject): ODEObject.__dependent_counts[dependent._count] += 1 # FIXME: Do not hardcode the fractional increase self._count = ( dependent._count + ODEObject.__dependent_counts[dependent._count] * 0.00001) else: self._count = ODEObject.__count ODEObject.__count += 1 debug(f"Change count of {self.name} from {old_count} to {self._count}")
def register_ode_object(self, obj, comp, dependent=None): """ Register an ODE object in the root ODEComponent """ from modelparameters.sympytools import symbols_from_expr if self._is_finalized_ode and isinstance(obj, StateExpression): error("Cannot register a StateExpression, the ODE is finalized") # Check for existing object in the ODE dup_obj = self.present_ode_objects.get(obj.name) # If object with same name is already registered in the ode we # need to figure out what to do if dup_obj: try: dup_comp = self.object_component[dup_obj] except KeyError: dup_comp = None # If a state is substituted by a state solution if isinstance(dup_obj, State) and isinstance(obj, StateSolution): debug(f"Reduce state '{dup_obj}' to {obj.expr}") # If duplicated object is an ODE Parameter and the added object is # either a State or a Parameter we replace the Parameter. elif (isinstance(dup_obj, Parameter) and dup_comp == self and comp != self and isinstance(obj, (State, Parameter))): timer = Timer("Replace objects") # noqa: F841 # Remove the object self.ode_objects.remove(dup_obj) # FIXME: Do we need to recreate all expression the objects is used in? # Replace the object from the object_used_in dict and update # the correponding expressions subs = {dup_obj.sym: obj.sym} subs = {} # Recursively replace object dependencies self._replace_object(dup_obj, obj, subs) # for expr in self.object_used_in[dup_obj]: # updated_expr = recreate_expression(expr, subs) # self.object_used_in[obj].add(updated_expr) # # # Exchange and update the dependencies # self.expression_dependencies[expr].remove(dup_obj) # self.expression_dependencies[expr].add(obj) # # # FIXME: Do not remove the dependencies # #self.expression_dependencies[updated_expr] = \ # # self.expression_dependencies.pop(expr) # self.expression_dependencies[updated_expr] = \ # self.expression_dependencies[expr] # # # Find the index of old expression and exchange it with updated # old_comp = self.object_component[expr] # ind = old_comp.ode_objects.index(expr) # old_comp.ode_objects[ind] = updated_expr # ## Remove information about the replaced objects # self.object_used_in.pop(dup_obj) # If duplicated object is an ODE Parameter and the added # object is an Intermediate we raise an error. elif (isinstance(dup_obj, Parameter) and dup_comp == self and isinstance(obj, Expression)): error( "Cannot replace an ODE parameter with an Expression, " "only with Parameters and States.", ) # If State, Parameter or DerivativeExpression we always raise an error elif any( isinstance( oo, ( State, Parameter, Time, Dt, DerivativeExpression, AlgebraicExpression, StateSolution, ), ) for oo in [dup_obj, obj]): error( "Cannot register {0}. A {1} with name '{2}' is " "already registered in this ODE.".format( type(obj).__name__, type(dup_obj).__name__, dup_obj.name, ), ) else: # Sanity check that both obj and dup_obj are Expressions assert all( isinstance(oo, (Expression)) for oo in [dup_obj, obj]) # Get list of duplicated objects or an empy list dup_objects = self.duplicated_expressions[obj.name] if len(dup_objects) == 0: dup_objects.append(dup_obj) dup_objects.append(obj) # Update global information about ode object self.present_ode_objects[obj.name] = obj self.object_component[obj] = comp self.ns.update({obj.name: obj.sym}) # If Expression if isinstance(obj, Expression): # Append the name to the list of all ordered components with # expressions. If the ODE is finalized we do not update components if not self._is_finalized_ode: self._handle_expr_component(comp, obj) # Expand and add any derivatives in the expressions expression_added = False replace_dict = {} derivative_expression_list = list(obj.expr.atoms(sp.Derivative)) derivative_expression_list.sort(key=lambda e: e.sort_key()) for der_expr in derivative_expression_list: expression_added |= self._expand_single_derivative( comp, obj, der_expr, replace_dict, dependent, ) # If expressions need to be re-created if replace_dict: obj.replace_expr(replace_dict) # If any expression was added we need to bump the count of the ODEObject if expression_added: obj._recount(dependent=dependent) # Add dependencies between the last registered comment and # expressions so they are carried over in Code components if comp._local_comments: self.object_used_in[comp._local_comments[-1]].add(obj) self.expression_dependencies[obj].add(comp._local_comments[-1]) # Get expression dependencies for sym in symbols_from_expr(obj.expr, include_derivatives=True): dep_obj = self.present_ode_objects[sympycode(sym)] if dep_obj is None: error( "The symbol '{0}' is not declared within the '{1}' " "ODE.".format(sym, self.name), ) # Store object dependencies self.expression_dependencies[obj].add(dep_obj) self.object_used_in[dep_obj].add(obj) # If the expression is a StateSolution the state cannot have # been used previously if isinstance(obj, StateSolution) and self.object_used_in.get( obj.state): used_in = self.object_used_in.get(obj.state) error( "A state solution cannot have been used in " "any previous expressions. {0} is used in: {1}".format( obj.state, used_in, ), )
def __init__( self, ode, function_name="forward_hybrid_generalized_rush_larsen", delta=1e-8, params=None, stiff_state_variables=None, ): """ Create a HybridGeneralizedRushLarsen Solver component Arguments --------- ode : gotran.ODE The parent component of this ODEComponent function_name : str The name of the function which should be generated delta : float Value to safeguard the evaluation of the Rush-Larsen step. params : dict Parameters determining how the code should be generated """ check_arg(ode, ODE) # Call base class using empty result_expressions descr = f"Compute a forward step using the FE / GRL1 scheme to the {ode} ODE" super(HybridGeneralizedRushLarsen, self).__init__( "HybridGeneralizedRushLarsen", ode, function_name, descr, params=params, additional_arguments=["dt"], ) state_names = [s.name for s in self.root.full_states] if stiff_state_variables is None: stiff_state_variables = [] elif type(stiff_state_variables) is str: stiff_state_variables = stiff_state_variables.split(",") for s in stiff_state_variables: if s == "": continue assert s in state_names, f"Unknown state '{s}'" # Recount the expressions if representation of states are "array" as # then the method is not full explcit recount = self._params.states.representation != "array" # Gather state expressions and states state_exprs = self.root.state_expressions states = self.root.full_states result_name = self._params.states.array_name self.shapes[result_name] = (len(states), ) # Get time step and start creating the update algorithm if self._params.states.add_offset: offset_str = f"{result_name}_offset" else: offset_str = "" dt = self.root._dt.sym for i, expr in enumerate(state_exprs): state_is_stiff = state_names[i] in stiff_state_variables expr_diff = expr.expr.diff(expr.state.sym) dependent = expr if recount else None if not state_is_stiff or expr_diff.is_zero: # FE scheme self.add_indexed_expression( result_name, (i, ), expr.state.sym + dt * expr.sym, offset_str, dependent=dependent, enum=expr.state, ) continue linearized_name = expr.name + "_linearized" linearized = self.add_intermediate( linearized_name, expr_diff, dependent=dependent, ) need_zero_div_check = not fraction_numerator_is_nonzero(expr_diff) if not need_zero_div_check: debug( f"{linearized_name} cannot be zero. Skipping zero division check" ) RL_term = expr.sym / linearized * (sp.exp(linearized * dt) - 1) if need_zero_div_check: RL_term = Conditional( abs(linearized) > delta, RL_term, dt * expr.sym, ) # Solve "exact" using exp self.add_indexed_expression( result_name, (i, ), expr.state.sym + RL_term, offset_str, dependent=dependent, enum=expr.state, ) # Call recreate body with the solver expressions as the result # expressions results = {result_name: self.indexed_objects(result_name)} results, body_expressions = self._body_from_results(**results) self.body_expressions = self._recreate_body(body_expressions, **results)
def __init__( self, ode, function_name="forward_rush_larsen", delta=1e-8, params=None, ): """ Create a RushLarsen Solver component Arguments --------- ode : gotran.ODE The parent component of this ODEComponent function_name : str The name of the function which should be generated delta : float Value to safeguard the evaluation of the Rush-Larsen step. params : dict Parameters determining how the code should be generated """ timer = Timer("Create RushLarsen expressions") check_arg(ode, ODE) if ode.is_dae: error("Cannot generate a Rush-Larsen forward step for a DAE.") # Call base class using empty result_expressions descr = f"Compute a forward step using the Rush-Larsen scheme to the {ode} ODE" super(RushLarsen, self).__init__( "RushLarsen", ode, function_name, descr, params=params, additional_arguments=["dt"], ) # Recount the expressions if representation of states are "array" as # then the method is not full explcit recount = self._params.states.representation != "array" # Gather state expressions and states state_exprs = self.root.state_expressions states = self.root.full_states result_name = self._params.states.array_name self.shapes[result_name] = (len(states), ) # Get time step and start creating the update algorithm if self._params.states.add_offset: offset_str = f"{result_name}_offset" else: offset_str = "" might_take_time = len(states) >= 10 if might_take_time: info( f"Calculating derivatives of {ode.name}. Might take some time..." ) sys.stdout.flush() dt = self.root._dt.sym for i, expr in enumerate(state_exprs): dependent = expr if recount else None # Diagonal jacobian value time_diff = Timer("Differentiate state_expressions for RushLarsen") expr_diff = expr.expr.diff(expr.state.sym) del time_diff # print expr.state.sym, expr_diff, expr_diff.args if expr_diff and expr.state.sym not in expr_diff.args: linearized_name = expr.name + "_linearized" linearized = self.add_intermediate( linearized_name, expr_diff, dependent=dependent, ) need_zero_div_check = not fraction_numerator_is_nonzero( expr_diff) if not need_zero_div_check: debug( f"{linearized_name} cannot be zero. Skipping zero division check", ) RL_term = expr.sym / linearized * (sp.exp(linearized * dt) - 1) if need_zero_div_check: RL_term = Conditional( abs(linearized) > delta, RL_term, dt * expr.sym, ) # Solve "exact" using exp self.add_indexed_expression( result_name, (i, ), expr.state.sym + RL_term, offset_str, dependent=dependent, enum=expr.state, ) else: # Explicit Euler step self.add_indexed_expression( result_name, (i, ), expr.state.sym + dt * expr.sym, offset_str, dependent=dependent, enum=expr.state, ) if might_take_time: info(" done") # Call recreate body with the solver expressions as the result # expressions del timer results = {result_name: self.indexed_objects(result_name)} results, body_expressions = self._body_from_results(**results) self.body_expressions = self._recreate_body(body_expressions, **results)
def __setitem__(self, name, value): """ This is only for expressions """ from modelparameters.sympytools import symbols_from_expr timer = Timer("Namespace dispatcher") # noqa: F841 # Set the attr of the ODE # If a scalar or a sympy number or it is a sympy.Basic consisting of # sp.Symbols if ( isinstance(value, scalars) or isinstance(value, sp.Number) or (isinstance(value, sp.Basic) and symbols_from_expr(value)) ): # Get source which triggers the insertion to the global namespace frame = inspect.currentframe().f_back lines, lnum = inspect.findsource(frame) # Try getting the code try: code = lines[frame.f_lineno - 1].strip() # Concatenate lines with line continuation symbols prev = 2 while ( frame.f_lineno - prev >= 0 and len(lines[frame.f_lineno - prev]) >= 2 and lines[frame.f_lineno - prev][-2:] == "\\\n" ): code = lines[frame.f_lineno - prev][:-2].strip() + code prev += 1 except Exception: code = "" # Check if the line includes a for statement # Here we strip op potiential code comments after the main for # statement. if re.search(_for_template, code.split("#")[0].strip()) or re.search( _no_intermediate_template, code, ): debug(f"Not registering '{name}' as an intermediate.") # If so just add the value to the namespace without # registering the intermediate dict.__setitem__(self, name, value) else: del timer # Add obj to the present component setattr(self.ode.present_component, name, value) else: debug(f"Not registering '{name}' as an intermediate.") # If no ode attr was generated we just add the value to the # namespace dict.__setitem__(self, name, value)
def _load(filename, name, **arguments): """ Load an ODE or Cell from file and return the instance The method looks for a file with .ode extension. Arguments --------- filename : str Name of the ode file to load name : str (optional) Set the name of ODE (defaults to filename) arguments : dict (optional) Optional arguments which can control loading of model """ timer = Timer("Load ODE") # noqa: F841 filename = Path(filename).with_suffix(".ode").absolute() # Extract name from filename name = filename.stem # Execute the file if not filename.is_file(): error(f"Could not find '{filename}'") # Copy file temporary to current directory basename = Path(filename.name).absolute() copyfile = False if basename != filename: shutil.copy(filename, basename) filename = basename name = filename.stem copyfile = True # If a Param is provided turn it into its value for key, value in list(arguments.items()): if isinstance(value, Param): arguments[key] = value.getvalue() # Dict to collect namespace intermediate_dispatcher = IntermediateDispatcher() # Create an ODE which will be populated with data when ode file is loaded ode = ODE(name, intermediate_dispatcher) intermediate_dispatcher.ode = ode debug(f"Loading {ode.name}") # Create namespace which the ode file will be executed in _init_namespace(ode, arguments, intermediate_dispatcher) # Execute file and collect with open(filename, "r") as f: exec(compile(f.read(), filename, "exec"), intermediate_dispatcher) # Finalize ODE ode.finalize() # Check for completeness if not ode.is_complete: warning(f"ODE model '{ode.name}' is not complete.") info(f"Loaded ODE model '{ode.name}' with:") for what in ["full_states", "parameters"]: num = getattr(ode, f"num_{what}") info(f"{('Num ' + what.replace('_', ' ')).rjust(20)}: {num}") if copyfile: os.unlink(filename) return ode
def _body_from_cse(self, **results): timer = Timer(f"Compute common sub expressions for {self.name}") # noqa: F841 ( orig_result_expressions, result_names, expanded_result_exprs, ) = self._expanded_result_expressions(**results) state_offset = self._params["states"]["add_offset"] # Collect results and body_expressions body_expressions = [] new_results = defaultdict(list) might_take_time = len(orig_result_expressions) >= 40 if might_take_time: info( "Computing common sub expressions for {0}. Might take " "some time...".format(self.name), ) sys.stdout.flush() # Call sympy common sub expression reduction cse_exprs, cse_result_exprs = cse( expanded_result_exprs, symbols=sp.numbered_symbols("cse_"), optimizations=[], ) # Map the cse_expr to an OrderedDict cse_exprs = OrderedDict(cse_expr for cse_expr in cse_exprs) # Extract the symbols into a set for fast comparison cse_syms = set((sym for sym in cse_exprs)) # Create maps between cse_expr and result expressions trying # to optimized the code by weaving in the result expressions # in between the cse_expr # A map between result expr and name and indices so we can # construct IndexedExpressions result_expr_map = defaultdict(list) # A map between last cse_expr used in a particular result expr # so that we can put the result expression right after the # last cse_expr it uses. last_cse_expr_used_in_result_expr = defaultdict(list) # Result expressions that does not contain any cse_sym result_expr_without_cse_syms = [] # A map between cse_sym and its substitutes cse_subs = {} for ind, (orig_result_expr, result_expr) in enumerate( zip(orig_result_expressions, cse_result_exprs), ): # Collect information so that we can recreate the result # expression from result_expr_map[result_expr].append( ( result_names[orig_result_expr], orig_result_expr.indices if isinstance(orig_result_expr, IndexedExpression) else ind, ), ) # If result_expr does not contain any cse_sym if not any(cse_sym in cse_syms for cse_sym in result_expr.atoms()): result_expr_without_cse_syms.append(result_expr) else: # Get last cse_sym used in result expression last_cse_sym = sorted( (cse_sym for cse_sym in result_expr.atoms() if cse_sym in cse_syms), key=cmp_to_key(lambda a, b: cmp(int(a.name[4:]), int(b.name[4:]))), )[-1] if result_expr not in last_cse_expr_used_in_result_expr[last_cse_sym]: last_cse_expr_used_in_result_expr[last_cse_sym].append(result_expr) debug( "Found {0} result expressions without any cse_syms.".format( len(result_expr_without_cse_syms), ), ) # print "" # print "LAST cse_syms:", last_cse_expr_used_in_result_expr.keys() cse_cnt = 0 atoms = [state.sym for state in self.root.full_states] atoms.extend(param.sym for param in self.root.parameters) # Collecte what states and parameters has been used used_states = set() used_parameters = set() self.add_comment( "Common sub expressions for the body and the " "result expressions", ) body_expressions.append(self.ode_objects[-1]) # Register the common sub expressions as Intermediates for cse_sym, expr in list(cse_exprs.items()): # print cse_sym, expr # If the expression is just one of the atoms of the ODE we # skip the cse expressions but add a subs for the atom We # also skip Relationals and Piecewise as the type checking # in Piecewise otherwise kicks in and destroys things for # us. if expr in atoms or isinstance( expr, (sp.Piecewise, sp.relational.Relational, sp.relational.Boolean), ): cse_subs[cse_sym] = expr.xreplace(cse_subs) else: # Add body expression as an intermediate expression sym = self.add_intermediate(f"cse_{cse_cnt}", expr.xreplace(cse_subs)) obj = self.ode_objects.get(sympycode(sym)) for dep in self.root.expression_dependencies[obj]: if isinstance(dep, State): used_states.add(dep) elif isinstance(dep, Parameter): used_parameters.add(dep) cse_subs[cse_sym] = sym cse_cnt += 1 body_expressions.append(obj) # Check if we should add a result expressions if last_cse_expr_used_in_result_expr[cse_sym]: # Iterate over all registered result expr for this cse_sym for result_expr in last_cse_expr_used_in_result_expr.pop(cse_sym): for result_name, indices in result_expr_map[result_expr]: # Replace pure state and param expressions # print cse_subs, result_expr exp_expr = result_expr.xreplace(cse_subs) sym = self.add_indexed_expression( result_name, indices, exp_expr, add_offset=state_offset, ) expr = self.ode_objects.get(sympycode(sym)) for dep in self.root.expression_dependencies[expr]: if isinstance(dep, State): used_states.add(dep) elif isinstance(dep, Parameter): used_parameters.add(dep) # Register the new result expression new_results[result_name].append(expr) body_expressions.append(expr) if might_take_time: info(" done") # Sort used state, parameters and expr self.used_states = sorted(used_states) self.used_parameters = sorted(used_parameters) return new_results, body_expressions