From ff284631932cad2ed8b77e70143b8ff7aac3d650 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Tue, 13 Oct 2020 15:49:49 +0200 Subject: [PATCH 01/19] Dummy implementation for discussion --- brian2/equations/__init__.py | 7 +-- brian2/equations/codestrings.py | 11 ++-- brian2/equations/equations.py | 91 ++++++++++++++++++++++++++++++--- brian2/only.py | 3 +- brian2/utils/stringtools.py | 3 +- 5 files changed, 99 insertions(+), 16 deletions(-) diff --git a/brian2/equations/__init__.py b/brian2/equations/__init__.py index 1ddf45ddf..8c5434728 100644 --- a/brian2/equations/__init__.py +++ b/brian2/equations/__init__.py @@ -2,7 +2,8 @@ Module handling equations and "code strings", expressions or statements, used for example for the reset and threshold definition of a neuron. ''' -from .codestrings import Expression, Statements -from .equations import Equations +from .codestrings import Expression, ExpressionTemplate, Statements +from .equations import Equations, EquationTemplate -__all__ = ['Equations', 'Expression', 'Statements'] +__all__ = ['Equations', 'EquationTemplate', 'Expression', + 'ExpressionTemplate', 'Statements'] diff --git a/brian2/equations/codestrings.py b/brian2/equations/codestrings.py index ce8510ff2..bf89f9716 100644 --- a/brian2/equations/codestrings.py +++ b/brian2/equations/codestrings.py @@ -15,7 +15,7 @@ from brian2.utils.stringtools import get_identifiers from brian2.parsing.sympytools import str_to_sympy, sympy_to_str -__all__ = ['Expression', 'Statements'] +__all__ = ['Expression', 'ExpressionTemplate', 'Statements'] logger = get_logger(__name__) @@ -103,10 +103,6 @@ def __init__(self, code=None, sympy_expression=None): if code is None: code = sympy_to_str(sympy_expression) - else: - # Just try to convert it to a sympy expression to get syntax errors - # for incorrect expressions - str_to_sympy(code) super(Expression, self).__init__(code=code) stochastic_variables = property(lambda self: {variable for variable in self.identifiers @@ -197,6 +193,11 @@ def __hash__(self): return hash(self.code) +class ExpressionTemplate(Expression): + def __call__(self, **replacements): + return Expression(code=self.code.format(**replacements)) + + def is_constant_over_dt(expression, variables, dt_value): ''' Check whether an expression can be considered as constant over a time step. diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index d4b77c533..a6bc6f535 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -35,7 +35,7 @@ from .unitcheck import check_dimensions -__all__ = ['Equations'] +__all__ = ['Equations', 'EquationTemplate'] logger = get_logger(__name__) @@ -64,8 +64,8 @@ # combination of letters, numbers and underscores # Note that the check_identifiers function later performs more checks, e.g. # names starting with underscore should only be used internally -IDENTIFIER = Word(string.ascii_letters + '_', - string.ascii_letters + string.digits + '_').setResultsName('identifier') +IDENTIFIER = Word(string.ascii_letters + '_' + '{', + string.ascii_letters + string.digits + '_' + '{' + '}').setResultsName('identifier') # very broad definition here, expression will be analysed by sympy anyway # allows for multi-line expressions, where each line can have comments @@ -324,7 +324,7 @@ def dimensions_and_type_from_string(unit_string): @cached -def parse_string_equations(eqns): +def parse_string_equations(eqns, template=False): """ parse_string_equations(eqns) @@ -335,7 +335,9 @@ def parse_string_equations(eqns): eqns : str The (possibly multi-line) string defining the equations. See the documentation of the `Equations` class for details. - + template : bool + Whether the equations should be parsed as a template + Returns ------- equations : dict @@ -363,7 +365,7 @@ def parse_string_equations(eqns): 'variable "%s": %s' % (identifier, ex)) expression = eq_content.get('expression', None) - if not expression is None: + if expression is not None: # Replace multiple whitespaces (arising from joining multiline # strings) with single space p = re.compile(r'\s{2,}') @@ -445,6 +447,8 @@ def __init__(self, type, varname, dimensions, var_type=FLOAT, expr=None, if variable =='xi' or variable.startswith('xi_')}, doc='Stochastic variables in the RHS of this equation') + template = property(lambda self: self.varname.startswith('{') or '{' in self.expr.code) + def __eq__(self, other): if not isinstance(other, SingleEquation): return NotImplemented @@ -1079,6 +1083,81 @@ def _repr_pretty_(self, p, cycle): p.breakable('\n') +class EquationTemplate(Mapping): + def __init__(self, eqns): + if isinstance(eqns, str): + self._equations = parse_string_equations(eqns, template=True) + else: + self._equations = dict(eqns) + + def __iter__(self): + return iter(self._equations) + + def __len__(self): + return len(self._equations) + + def __getitem__(self, key): + return self._equations[key] + + def __call__(self, **replacements): + if len(replacements) == 0: + return self._equations + + new_equations = {} + for eq in self.values(): + if '{' not in eq.varname: + continue + # Replace the name of a model variable (works only for strings) + new_varname = eq.varname.format(**replacements) + # make sure that the replacement is a valid identifier + Equations.check_identifier(new_varname) + if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: + # Replace values in the RHS of the equation + new_code = eq.expr.code + for to_replace, replacement in replacements.items(): + to_replace = '{' + to_replace + '}' + if to_replace in eq.identifiers: + if to_replace.startswith('{'): + boundary = '' + else: + boundary = r'\b' + if isinstance(replacement, Expression): + replacement = str(replacement) + if isinstance(replacement, str): + # replace the name with another name + new_code = re.sub(boundary + to_replace + boundary, + replacement, new_code) + else: + # replace the name with a value + new_code = re.sub(boundary + to_replace + boundary, + '(' + repr(replacement) + ')', + new_code) + try: + Expression(new_code) + except ValueError as ex: + raise ValueError( + ('Replacing "%s" with "%r" failed: %s') % + (to_replace, replacement, ex)) + new_equations[new_varname] = SingleEquation(eq.type, new_varname, + dimensions=eq.dim, + var_type=eq.var_type, + expr=Expression(new_code), + flags=eq.flags) + else: + new_equations[new_varname] = SingleEquation(eq.type, new_varname, + dimensions=eq.dim, + var_type=eq.var_type, + flags=eq.flags) + if any(eq.template for eq in new_equations.values()): + return EquationTemplate(new_equations) + else: + return Equations([eq for eq in new_equations.values()]) + + def __str__(self): + strings = [str(eq) for eq in self.values()] + return '\n'.join(strings) + + def is_stateful(expression, variables): ''' Whether the given expression refers to stateful functions (and is therefore diff --git a/brian2/only.py b/brian2/only.py index 8cde2fc22..8b9aa9a66 100644 --- a/brian2/only.py +++ b/brian2/only.py @@ -81,7 +81,8 @@ def restore_initial_state(): 'DEFAULT_FUNCTIONS', 'Function', 'implementation', 'declare_types', 'PreferenceError', 'BrianPreference', 'prefs', 'brian_prefs', 'Clock', 'defaultclock', - 'Equations', 'Expression', 'Statements', + 'Equations', 'EquationTemplate', 'Expression', 'ExpressionTemplate', + 'Statements', 'BrianObject', 'BrianObjectException', 'Network', 'profiling_summary', 'scheduling_summary', diff --git a/brian2/utils/stringtools.py b/brian2/utils/stringtools.py index c37239280..a98e0dafe 100644 --- a/brian2/utils/stringtools.py +++ b/brian2/utils/stringtools.py @@ -181,13 +181,14 @@ def get_identifiers(expr, include_numbers=False): ['.3e-10', '17', '3', '8', 'A', '_b', 'a', 'c5', 'f', 'tau_2'] ''' identifiers = set(re.findall(r'\b[A-Za-z_][A-Za-z0-9_]*\b', expr)) + template_identifiers = set(re.findall(r'[{][A-Za-z_][A-Za-z0-9_]*[}]', expr)) if include_numbers: # only the number, not a + or - numbers = set(re.findall(r'(?<=[^A-Za-z_])[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?|^[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?', expr)) else: numbers = set() - return (identifiers - KEYWORDS) | numbers + return (identifiers - KEYWORDS) | template_identifiers | numbers def strip_empty_lines(s): From 27c2cb8318dc3ec47f3141ae1fa650890435866a Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Thu, 15 Oct 2020 18:43:02 +0200 Subject: [PATCH 02/19] Example using templates --- examples/IF_curve_LIF.py | 9 +- .../Rothman_Manis_2003_with_templates.py | 161 ++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) create mode 100755 examples/frompapers/Rothman_Manis_2003_with_templates.py diff --git a/examples/IF_curve_LIF.py b/examples/IF_curve_LIF.py index 292579eb4..43a355999 100644 --- a/examples/IF_curve_LIF.py +++ b/examples/IF_curve_LIF.py @@ -7,7 +7,8 @@ The input is set differently for each neuron. ''' from brian2 import * - +import brian2tools +set_device('exporter', debug=True) n = 1000 duration = 1*second tau = 10*ms @@ -23,7 +24,5 @@ monitor = SpikeMonitor(group) run(duration) -plot(group.v0/mV, monitor.count / duration) -xlabel('v0 (mV)') -ylabel('Firing rate (sp/s)') -show() + + diff --git a/examples/frompapers/Rothman_Manis_2003_with_templates.py b/examples/frompapers/Rothman_Manis_2003_with_templates.py new file mode 100755 index 000000000..717f9b058 --- /dev/null +++ b/examples/frompapers/Rothman_Manis_2003_with_templates.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +""" +Cochlear neuron model of Rothman & Manis +---------------------------------------- +Rothman JS, Manis PB (2003) The roles potassium currents play in +regulating the electrical activity of ventral cochlear nucleus neurons. +J Neurophysiol 89:3097-113. + +All model types differ only by the maximal conductances. + +Adapted from their Neuron implementation by Romain Brette +""" +from brian2 import * + +#defaultclock.dt=0.025*ms # for better precision + +''' +Simulation parameters: choose current amplitude and neuron type +(from type1c, type1t, type12, type 21, type2, type2o) +''' +neuron_type = 'type1c' +Ipulse = 250*pA + +C = 12*pF +Eh = -43*mV +EK = -70*mV # -77*mV in mod file +El = -65*mV +ENa = 50*mV +nf = 0.85 # proportion of n vs p kinetics +zss = 0.5 # steady state inactivation of glt +temp = 22. # temperature in degree celcius +q10 = 3. ** ((temp - 22) / 10.) +# hcno current (octopus cell) +frac = 0.0 +qt = 4.5 ** ((temp - 33.) / 10.) + +# Maximal conductances of different cell types in nS +maximal_conductances = dict( +type1c=(1000, 150, 0, 0, 0.5, 0, 2), +type1t=(1000, 80, 0, 65, 0.5, 0, 2), +type12=(1000, 150, 20, 0, 2, 0, 2), +type21=(1000, 150, 35, 0, 3.5, 0, 2), +type2=(1000, 150, 200, 0, 20, 0, 2), +type2o=(1000, 150, 600, 0, 0, 40, 2) # octopus cell +) +gnabar, gkhtbar, gkltbar, gkabar, ghbar, gbarno, gl = [x * nS for x in maximal_conductances[neuron_type]] + +gating_var = EquationTemplate('''d{name}/dt = q10*({name}__inf - {name})/tau_{name} : 1 + {name}__inf = {rate_expression} : 1 + tau_{name} = {tau_scale}/({forward_rate} + + {reverse_rate}) + + {tau_base} : second''') + +pos_sigmoid = ExpressionTemplate('1./(1+exp(-(vu - {midpoint}) / {scale}))') +sqrt_sigmoid = ExpressionTemplate('1./(1+exp(-(vu - {midpoint}) / {scale}))**0.5') +neg_sigmoid = ExpressionTemplate('1./(1+exp((vu - {midpoint}) / {scale}))') +exp_voltage_dep = ExpressionTemplate('{magnitude}*exp((vu-{midpoint})/{scale})') +neg_exp_voltage_dep = ExpressionTemplate('{magnitude}*exp(-(vu-{midpoint})/{scale})') + +# Classical Na channel +m = gating_var(name='m', + rate_expression=pos_sigmoid(midpoint=-38., scale=7.), + forward_rate=exp_voltage_dep(magnitude=5., midpoint=-60, scale=18.), + reverse_rate=neg_exp_voltage_dep(magnitude=36., midpoint=-60, scale=25.), + tau_base=0.04*ms, tau_scale=10*ms) +h = gating_var(name='h', + rate_expression=neg_sigmoid(midpoint=-65., scale=6.), + forward_rate=exp_voltage_dep(magnitude=7., midpoint=-60., scale=11.), + reverse_rate=neg_exp_voltage_dep(magnitude=10., midpoint=-60., scale=25.), + tau_base=0.6*ms, tau_scale=100*ms) + +ina = Equations('ina = gnabar*m**3*h*(ENa-v) : amp') + m + h + +# KHT channel (delayed-rectifier K+) +n = gating_var(name='n', + rate_expression=sqrt_sigmoid(midpoint=-15, scale=5.), + forward_rate=exp_voltage_dep(magnitude=11., midpoint=-60, scale=24.), + reverse_rate=neg_exp_voltage_dep(magnitude=21., midpoint=-60, scale=23.), + tau_base=0.7*ms, tau_scale=100*ms) + +p = gating_var(name='p', + rate_expression=pos_sigmoid(midpoint=-23., scale=6.), + forward_rate=exp_voltage_dep(magnitude=4., midpoint=-60., scale=32.), + reverse_rate=neg_exp_voltage_dep(magnitude=5., midpoint=-60., scale=22.), + tau_base=5*ms, tau_scale=100*ms) + +ikht = Equations('ikht = gkhtbar*(nf*n**2 + (1-nf)*p)*(EK-v) : amp') + n + p + +# Ih channel (subthreshold adaptive, non-inactivating) +eqs_ih = """ +ih = ghbar*r*(Eh-v) : amp +dr/dt=q10*(rinf-r)/rtau : 1 +rinf = 1. / (1+exp((vu + 76.) / 7.)) : 1 +rtau = ((100000. / (237.*exp((vu+60.) / 12.) + 17.*exp(-(vu+60.) / 14.))) + 25.)*ms : second +""" + +# KLT channel (low threshold K+) +eqs_klt = """ +iklt = gkltbar*w**4*z*(EK-v) : amp +dw/dt=q10*(winf-w)/wtau : 1 +dz/dt=q10*(zinf-z)/ztau : 1 +winf = (1. / (1 + exp(-(vu + 48.) / 6.)))**0.25 : 1 +zinf = zss + ((1.-zss) / (1 + exp((vu + 71.) / 10.))) : 1 +wtau = ((100. / (6.*exp((vu+60.) / 6.) + 16.*exp(-(vu+60.) / 45.))) + 1.5)*ms : second +ztau = ((1000. / (exp((vu+60.) / 20.) + exp(-(vu+60.) / 8.))) + 50)*ms : second +""" + +# Ka channel (transient K+) +eqs_ka = """ +ika = gkabar*a**4*b*c*(EK-v): amp +da/dt=q10*(ainf-a)/atau : 1 +db/dt=q10*(binf-b)/btau : 1 +dc/dt=q10*(cinf-c)/ctau : 1 +ainf = (1. / (1 + exp(-(vu + 31) / 6.)))**0.25 : 1 +binf = 1. / (1 + exp((vu + 66) / 7.))**0.5 : 1 +cinf = 1. / (1 + exp((vu + 66) / 7.))**0.5 : 1 +atau = ((100. / (7*exp((vu+60) / 14.) + 29*exp(-(vu+60) / 24.))) + 0.1)*ms : second +btau = ((1000. / (14*exp((vu+60) / 27.) + 29*exp(-(vu+60) / 24.))) + 1)*ms : second +ctau = ((90. / (1 + exp((-66-vu) / 17.))) + 10)*ms : second +""" + +# Leak +eqs_leak = """ +ileak = gl*(El-v) : amp +""" + +# h current for octopus cells +eqs_hcno = """ +ihcno = gbarno*(h1*frac + h2*(1-frac))*(Eh-v) : amp +dh1/dt=(hinfno-h1)/tau1 : 1 +dh2/dt=(hinfno-h2)/tau2 : 1 +hinfno = 1./(1+exp((vu+66.)/7.)) : 1 +tau1 = bet1/(qt*0.008*(1+alp1))*ms : second +tau2 = bet2/(qt*0.0029*(1+alp2))*ms : second +alp1 = exp(1e-3*3*(vu+50)*9.648e4/(8.315*(273.16+temp))) : 1 +bet1 = exp(1e-3*3*0.3*(vu+50)*9.648e4/(8.315*(273.16+temp))) : 1 +alp2 = exp(1e-3*3*(vu+84)*9.648e4/(8.315*(273.16+temp))) : 1 +bet2 = exp(1e-3*3*0.6*(vu+84)*9.648e4/(8.315*(273.16+temp))) : 1 +""" + +eqs =Equations(""" +dv/dt = (ileak + ina + ikht + iklt + ika + ih + ihcno + I)/C : volt +vu = v/mV : 1 # unitless v +I : amp +""") +eqs += Equations(eqs_leak) + Equations(eqs_ka) + ina + Equations(eqs_ih) + Equations(eqs_klt) + ikht + Equations(eqs_hcno) + +neuron = NeuronGroup(1, eqs, method='exponential_euler') +neuron.v = El + +run(50*ms, report='text') # Go to rest + +M = StateMonitor(neuron, 'v', record=0) +neuron.I = Ipulse + +run(100*ms, report='text') + +plot(M.t / ms, M[0].v / mV) +xlabel('t (ms)') +ylabel('v (mV)') +show() From 76c5d42466332083214ad979d87578cff5e34a76 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Thu, 22 Oct 2020 18:59:39 +0200 Subject: [PATCH 03/19] WIP: exploring a few more options for modular template equations --- brian2/equations/codestrings.py | 13 ++-- brian2/equations/equations.py | 66 ++++++++++++++----- .../Rothman_Manis_2003_with_templates.py | 24 ++++--- 3 files changed, 70 insertions(+), 33 deletions(-) diff --git a/brian2/equations/codestrings.py b/brian2/equations/codestrings.py index bf89f9716..8b0dc7a14 100644 --- a/brian2/equations/codestrings.py +++ b/brian2/equations/codestrings.py @@ -3,11 +3,7 @@ information about its namespace. Only serves as a parent class, its subclasses `Expression` and `Statements` are the ones that are actually used. ''' - -try: - from collections.abc import Hashable -except ImportError: - from collections import Hashable +from collections.abc import Hashable import sympy @@ -193,9 +189,14 @@ def __hash__(self): return hash(self.code) +class Default(dict): + def __missing__(self, key): + return f'{{{key}}}' + + class ExpressionTemplate(Expression): def __call__(self, **replacements): - return Expression(code=self.code.format(**replacements)) + return Expression(code=self.code.format_map(Default(replacements))) def is_constant_over_dt(expression, variables, dt_value): diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index a6bc6f535..f08792fc6 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -6,6 +6,7 @@ import keyword import re import string +from typing import Sequence from pyparsing import (Group, ZeroOrMore, OneOrMore, Optional, Word, CharsNotIn, Combine, Suppress, restOfLine, LineEnd, ParseException) @@ -447,7 +448,8 @@ def __init__(self, type, varname, dimensions, var_type=FLOAT, expr=None, if variable =='xi' or variable.startswith('xi_')}, doc='Stochastic variables in the RHS of this equation') - template = property(lambda self: self.varname.startswith('{') or '{' in self.expr.code) + template = property(lambda self: self.varname.startswith('{') or + (self.expr is not None and '{' in self.expr.code)) def __eq__(self, other): if not isinstance(other, SingleEquation): @@ -653,6 +655,9 @@ def _substitute(self, replacements): return new_equations + def __call__(self, **kwds): + return Equations(list(self._substitute(kwds).values())) + def substitute(self, **kwds): return Equations(list(self._substitute(kwds).values())) @@ -668,6 +673,9 @@ def __getitem__(self, key): def __add__(self, other_eqns): if isinstance(other_eqns, str): other_eqns = parse_string_equations(other_eqns) + elif isinstance(other_eqns, EquationTemplate): + return EquationTemplate(list(self.values()) + + list(other_eqns.values())) elif not isinstance(other_eqns, Equations): return NotImplemented @@ -1087,6 +1095,8 @@ class EquationTemplate(Mapping): def __init__(self, eqns): if isinstance(eqns, str): self._equations = parse_string_equations(eqns, template=True) + elif isinstance(eqns, Sequence): + self._equations = {eq.varname: eq for eq in eqns} else: self._equations = dict(eqns) @@ -1099,18 +1109,29 @@ def __len__(self): def __getitem__(self, key): return self._equations[key] + def __add__(self, other_eqns): + if isinstance(other_eqns, str): + other_eqns = parse_string_equations(other_eqns) + elif not isinstance(other_eqns, (Equations, EquationTemplate)): + return NotImplemented + + return EquationTemplate(list(self.values()) + + list(other_eqns.values())) + def __call__(self, **replacements): if len(replacements) == 0: return self._equations new_equations = {} + additional_equations = [] for eq in self.values(): - if '{' not in eq.varname: - continue - # Replace the name of a model variable (works only for strings) - new_varname = eq.varname.format(**replacements) - # make sure that the replacement is a valid identifier - Equations.check_identifier(new_varname) + if '{' in eq.varname: + # Replace the name of a model variable (works only for strings) + new_varname = eq.varname.format(**replacements) + # make sure that the replacement is a valid identifier + Equations.check_identifier(new_varname) + else: + new_varname = eq.varname if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: # Replace values in the RHS of the equation new_code = eq.expr.code @@ -1121,17 +1142,27 @@ def __call__(self, **replacements): boundary = '' else: boundary = r'\b' - if isinstance(replacement, Expression): - replacement = str(replacement) - if isinstance(replacement, str): - # replace the name with another name - new_code = re.sub(boundary + to_replace + boundary, - replacement, new_code) + if not isinstance(replacement, Sequence) or isinstance(replacement, str): + repls = [replacement] else: - # replace the name with a value - new_code = re.sub(boundary + to_replace + boundary, - '(' + repr(replacement) + ')', - new_code) + repls = replacement + names = [] + for replacement in repls: + if isinstance(replacement, (Equations, EquationTemplate)): + names.append(list(replacement)[0]) # use the first name + additional_equations.extend(replacement._equations.items()) + elif isinstance(replacement, Expression): + names.append('('+str(replacement)+')') + elif isinstance(replacement, str): + names.append(replacement) + else: + names.append(repr(replacement)) + if len(names) > 1: + replacement = '(' + ' + '.join(names) + ')' + else: + replacement = names[0] + new_code = re.sub(boundary + to_replace + boundary, + replacement, new_code) try: Expression(new_code) except ValueError as ex: @@ -1148,6 +1179,7 @@ def __call__(self, **replacements): dimensions=eq.dim, var_type=eq.var_type, flags=eq.flags) + new_equations.update(dict(additional_equations)) if any(eq.template for eq in new_equations.values()): return EquationTemplate(new_equations) else: diff --git a/examples/frompapers/Rothman_Manis_2003_with_templates.py b/examples/frompapers/Rothman_Manis_2003_with_templates.py index 717f9b058..0fe877fdc 100755 --- a/examples/frompapers/Rothman_Manis_2003_with_templates.py +++ b/examples/frompapers/Rothman_Manis_2003_with_templates.py @@ -51,11 +51,11 @@ {reverse_rate}) + {tau_base} : second''') -pos_sigmoid = ExpressionTemplate('1./(1+exp(-(vu - {midpoint}) / {scale}))') -sqrt_sigmoid = ExpressionTemplate('1./(1+exp(-(vu - {midpoint}) / {scale}))**0.5') -neg_sigmoid = ExpressionTemplate('1./(1+exp((vu - {midpoint}) / {scale}))') -exp_voltage_dep = ExpressionTemplate('{magnitude}*exp((vu-{midpoint})/{scale})') -neg_exp_voltage_dep = ExpressionTemplate('{magnitude}*exp(-(vu-{midpoint})/{scale})') +pos_sigmoid = ExpressionTemplate('1./(1+exp(-({voltage} - {midpoint}) / {scale}))') +sqrt_sigmoid = ExpressionTemplate('1./(1+exp(-({voltage} - {midpoint}) / {scale}))**0.5') +neg_sigmoid = ExpressionTemplate('1./(1+exp(({voltage} - {midpoint}) / {scale}))') +exp_voltage_dep = ExpressionTemplate('{magnitude}*exp(({voltage}-{midpoint})/{scale})') +neg_exp_voltage_dep = ExpressionTemplate('{magnitude}*exp(-({voltage}-{midpoint})/{scale})') # Classical Na channel m = gating_var(name='m', @@ -63,13 +63,15 @@ forward_rate=exp_voltage_dep(magnitude=5., midpoint=-60, scale=18.), reverse_rate=neg_exp_voltage_dep(magnitude=36., midpoint=-60, scale=25.), tau_base=0.04*ms, tau_scale=10*ms) + h = gating_var(name='h', rate_expression=neg_sigmoid(midpoint=-65., scale=6.), forward_rate=exp_voltage_dep(magnitude=7., midpoint=-60., scale=11.), reverse_rate=neg_exp_voltage_dep(magnitude=10., midpoint=-60., scale=25.), tau_base=0.6*ms, tau_scale=100*ms) -ina = Equations('ina = gnabar*m**3*h*(ENa-v) : amp') + m + h +ina = EquationTemplate('ina = gnabar*{m}**3*{h}*(ENa-v) : amp') +ina = ina(m=m, h=h) # KHT channel (delayed-rectifier K+) n = gating_var(name='n', @@ -138,12 +140,14 @@ bet2 = exp(1e-3*3*0.6*(vu+84)*9.648e4/(8.315*(273.16+temp))) : 1 """ -eqs =Equations(""" -dv/dt = (ileak + ina + ikht + iklt + ika + ih + ihcno + I)/C : volt +eqs =EquationTemplate(""" +dv/dt = (ileak + {currents} + iklt + ika + ih + ihcno + I)/C : volt vu = v/mV : 1 # unitless v I : amp -""") -eqs += Equations(eqs_leak) + Equations(eqs_ka) + ina + Equations(eqs_ih) + Equations(eqs_klt) + ikht + Equations(eqs_hcno) +""")(currents=[ina, ikht])(voltage='vu') +print(eqs) +eqs += Equations(eqs_leak) + Equations(eqs_ka) + Equations(eqs_ih) + Equations(eqs_klt) + Equations(eqs_hcno) +print(eqs) neuron = NeuronGroup(1, eqs, method='exponential_euler') neuron.v = El From 3f2d253b6631ff2cc16c9e31f9138e59b062a935 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 23 Oct 2020 16:55:12 +0200 Subject: [PATCH 04/19] Better support nested equations/expressions --- brian2/equations/equations.py | 132 +++++++++++++----- .../Rothman_Manis_2003_with_templates.py | 8 +- 2 files changed, 101 insertions(+), 39 deletions(-) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index f08792fc6..9dd88e1f8 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -32,7 +32,7 @@ from brian2.utils.logger import get_logger from brian2.utils.topsort import topsort -from .codestrings import Expression +from .codestrings import Expression, ExpressionTemplate from .unitcheck import check_dimensions @@ -1123,13 +1123,95 @@ def __call__(self, **replacements): return self._equations new_equations = {} - additional_equations = [] + # First, do replacements for equations/expressions to allow other values to + # replace values in them + additional_equations = {} for eq in self.values(): + if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: + new_code = eq.expr.code + for to_replace, replacement in replacements.items(): + if not isinstance(replacement, (Expression, ExpressionTemplate, Equations, EquationTemplate)): + continue + to_replace = '{' + to_replace + '}' + if to_replace in eq.varname: + raise TypeError(f'Cannot replace equation \'{eq.varname}\' by another equation or expression.') + if to_replace in eq.identifiers: + if isinstance(replacement, (Equations, EquationTemplate)): + name = list(replacement)[0] # use the first name + additional_equations.update(replacement._equations.items()) + else: # Expression or ExpressionTemplate + name = '('+str(replacement.code)+')' + new_code = re.sub(to_replace, name, new_code) + try: + Expression(new_code) + except ValueError as ex: + raise ValueError( + ('Replacing "%s" with "%r" failed: %s') % + (to_replace, replacement, ex)) + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + expr=Expression(new_code), + flags=eq.flags) + else: + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + flags=eq.flags) + # Now, do replacements for lists of names/expressions + for eq in new_equations.values(): + if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: + new_code = eq.expr.code + for to_replace, replacement in replacements.items(): + to_replace = '{' + to_replace + '}' + if not isinstance(replacement, Sequence) or isinstance(replacement, str): + continue + if to_replace in eq.varname: + raise TypeError(f'Cannot replace equation \'{eq.varname}\' by a list of names/expressions.') + if to_replace in eq.identifiers: + names = [] + for single_replacement in replacement: + if isinstance(single_replacement, (Equations, EquationTemplate)): + name = list(single_replacement)[0] # use the first name + additional_equations.update(single_replacement._equations.items()) + elif isinstance(single_replacement, (Expression, ExpressionTemplate)): + name = '('+str(single_replacement.code)+')' + elif isinstance(single_replacement, str): + name = single_replacement + else: + name = repr(single_replacement) + names.append(name) + new_code = re.sub(to_replace, '(' + (' + '.join(names)) + ')', new_code) + try: + Expression(new_code) + except ValueError as ex: + raise ValueError( + ('Replacing "%s" with "%r" failed: %s') % + (to_replace, replacement, ex)) + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + expr=Expression(new_code), + flags=eq.flags) + else: + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + flags=eq.flags) + new_equations.update(dict(additional_equations)) + # Finally, do replacements with concrete values + final_equations = {} + for eq in new_equations.values(): if '{' in eq.varname: # Replace the name of a model variable (works only for strings) - new_varname = eq.varname.format(**replacements) - # make sure that the replacement is a valid identifier - Equations.check_identifier(new_varname) + new_varname = eq.varname + for to_replace, replacement in replacements.items(): + to_replace = '{' + to_replace + '}' + if to_replace in eq.varname: + new_varname = new_varname.replace(to_replace, replacement) + if '{' not in new_varname: + # make sure that the replacement is a valid identifier + Equations.check_identifier(new_varname) else: new_varname = eq.varname if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: @@ -1138,52 +1220,32 @@ def __call__(self, **replacements): for to_replace, replacement in replacements.items(): to_replace = '{' + to_replace + '}' if to_replace in eq.identifiers: - if to_replace.startswith('{'): - boundary = '' - else: - boundary = r'\b' - if not isinstance(replacement, Sequence) or isinstance(replacement, str): - repls = [replacement] - else: - repls = replacement - names = [] - for replacement in repls: - if isinstance(replacement, (Equations, EquationTemplate)): - names.append(list(replacement)[0]) # use the first name - additional_equations.extend(replacement._equations.items()) - elif isinstance(replacement, Expression): - names.append('('+str(replacement)+')') - elif isinstance(replacement, str): - names.append(replacement) - else: - names.append(repr(replacement)) - if len(names) > 1: - replacement = '(' + ' + '.join(names) + ')' + if isinstance(replacement, str): + name = replacement else: - replacement = names[0] - new_code = re.sub(boundary + to_replace + boundary, - replacement, new_code) + name = repr(replacement) + new_code = re.sub(to_replace, name, new_code) try: Expression(new_code) except ValueError as ex: raise ValueError( ('Replacing "%s" with "%r" failed: %s') % (to_replace, replacement, ex)) - new_equations[new_varname] = SingleEquation(eq.type, new_varname, + final_equations[new_varname] = SingleEquation(eq.type, new_varname, dimensions=eq.dim, var_type=eq.var_type, expr=Expression(new_code), flags=eq.flags) else: - new_equations[new_varname] = SingleEquation(eq.type, new_varname, + final_equations[new_varname] = SingleEquation(eq.type, new_varname, dimensions=eq.dim, var_type=eq.var_type, flags=eq.flags) - new_equations.update(dict(additional_equations)) - if any(eq.template for eq in new_equations.values()): - return EquationTemplate(new_equations) + + if any(eq.template for eq in final_equations.values()): + return EquationTemplate(final_equations) else: - return Equations([eq for eq in new_equations.values()]) + return Equations([eq for eq in final_equations.values()]) def __str__(self): strings = [str(eq) for eq in self.values()] diff --git a/examples/frompapers/Rothman_Manis_2003_with_templates.py b/examples/frompapers/Rothman_Manis_2003_with_templates.py index 0fe877fdc..f28d67c1e 100755 --- a/examples/frompapers/Rothman_Manis_2003_with_templates.py +++ b/examples/frompapers/Rothman_Manis_2003_with_templates.py @@ -86,7 +86,7 @@ reverse_rate=neg_exp_voltage_dep(magnitude=5., midpoint=-60., scale=22.), tau_base=5*ms, tau_scale=100*ms) -ikht = Equations('ikht = gkhtbar*(nf*n**2 + (1-nf)*p)*(EK-v) : amp') + n + p +ikht = EquationTemplate('ikht = gkhtbar*(nf*{n}**2 + (1-nf)*{p})*(EK-v) : amp')(n=n, p=p) # Ih channel (subthreshold adaptive, non-inactivating) eqs_ih = """ @@ -144,10 +144,10 @@ dv/dt = (ileak + {currents} + iklt + ika + ih + ihcno + I)/C : volt vu = v/mV : 1 # unitless v I : amp -""")(currents=[ina, ikht])(voltage='vu') -print(eqs) +""") +eqs = eqs(currents=[ina, ikht], voltage='vu') eqs += Equations(eqs_leak) + Equations(eqs_ka) + Equations(eqs_ih) + Equations(eqs_klt) + Equations(eqs_hcno) -print(eqs) + neuron = NeuronGroup(1, eqs, method='exponential_euler') neuron.v = El From d0945bf72459ef3d9b3f18d86495d8e95f16b50c Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 23 Oct 2020 17:18:56 +0200 Subject: [PATCH 05/19] Merge Equations and EquationTemplate --- brian2/equations/__init__.py | 4 +- brian2/equations/equations.py | 305 ++++++------------ brian2/only.py | 2 +- .../Rothman_Manis_2003_with_templates.py | 18 +- 4 files changed, 116 insertions(+), 213 deletions(-) diff --git a/brian2/equations/__init__.py b/brian2/equations/__init__.py index 8c5434728..2025745c9 100644 --- a/brian2/equations/__init__.py +++ b/brian2/equations/__init__.py @@ -3,7 +3,7 @@ for example for the reset and threshold definition of a neuron. ''' from .codestrings import Expression, ExpressionTemplate, Statements -from .equations import Equations, EquationTemplate +from .equations import Equations -__all__ = ['Equations', 'EquationTemplate', 'Expression', +__all__ = ['Equations', 'Expression', 'ExpressionTemplate', 'Statements'] diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 9dd88e1f8..28f6d54f1 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -36,7 +36,7 @@ from .unitcheck import check_dimensions -__all__ = ['Equations', 'EquationTemplate'] +__all__ = ['Equations'] logger = get_logger(__name__) @@ -561,7 +561,7 @@ class Equations(Hashable, Mapping): def __init__(self, eqns, **kwds): if isinstance(eqns, str): - self._equations = parse_string_equations(eqns) + self._equations = parse_string_equations(eqns, template=True) # Do a basic check for the identifiers self.check_identifiers() else: @@ -604,56 +604,125 @@ def _substitute(self, replacements): return self._equations new_equations = {} + # First, do replacements for equations/expressions to allow other values to + # replace values in them + additional_equations = {} for eq in self.values(): - # Replace the name of a model variable (works only for strings) - if eq.varname in replacements: - new_varname = replacements[eq.varname] - if not isinstance(new_varname, str): - raise ValueError(('Cannot replace model variable "%s" ' - 'with a value') % eq.varname) - if new_varname in self or new_varname in new_equations: - raise EquationError( - ('Cannot replace model variable "%s" ' - 'with "%s", duplicate definition ' - 'of "%s".' % (eq.varname, new_varname, - new_varname))) - # make sure that the replacement is a valid identifier - Equations.check_identifier(new_varname) + if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: + new_code = eq.expr.code + for to_replace, replacement in replacements.items(): + if not isinstance(replacement, (Expression, ExpressionTemplate, Equations)): + continue + if to_replace in eq.varname or '{' + to_replace + '}' in eq.varname: + raise TypeError(f'Cannot replace equation \'{eq.varname}\' by another equation or expression.') + if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: + if isinstance(replacement, Equations): + name = list(replacement)[0] # use the first name + additional_equations.update(replacement._equations.items()) + else: # Expression or ExpressionTemplate + name = '('+str(replacement.code)+')' + new_code = new_code.replace('{' + to_replace + '}', name) + new_code = re.sub('\b' + to_replace + '\b', name, new_code) + try: + Expression(new_code) + except ValueError as ex: + raise ValueError( + ('Replacing "%s" with "%r" failed: %s') % + (to_replace, replacement, ex)) + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + expr=Expression(new_code), + flags=eq.flags) + else: + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + flags=eq.flags) + # Now, do replacements for lists of names/expressions + for eq in new_equations.values(): + if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: + new_code = eq.expr.code + for to_replace, replacement in replacements.items(): + if not isinstance(replacement, Sequence) or isinstance(replacement, str): + continue + if to_replace in eq.varname or '{' + to_replace + '}' in eq.varname: + raise TypeError(f'Cannot replace equation \'{eq.varname}\' by a list of names/expressions.') + if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: + names = [] + for single_replacement in replacement: + if isinstance(single_replacement, Equations): + name = list(single_replacement)[0] # use the first name + additional_equations.update(single_replacement._equations.items()) + elif isinstance(single_replacement, (Expression, ExpressionTemplate)): + name = '('+str(single_replacement.code)+')' + elif isinstance(single_replacement, str): + name = single_replacement + else: + name = repr(single_replacement) + names.append(name) + new_code = new_code.replace('{' + to_replace + '}', '(' + (' + '.join(names)) + ')') + new_code = re.sub('\b' + to_replace + '\b', '(' + (' + '.join(names)) + ')', new_code) + try: + Expression(new_code) + except ValueError as ex: + raise ValueError( + ('Replacing "%s" with "%r" failed: %s') % + (to_replace, replacement, ex)) + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + expr=Expression(new_code), + flags=eq.flags) + else: + new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, + dimensions=eq.dim, + var_type=eq.var_type, + flags=eq.flags) + new_equations.update(dict(additional_equations)) + # Finally, do replacements with concrete values + final_equations = {} + for eq in new_equations.values(): + if '{' in eq.varname: + # Replace the name of a model variable (works only for strings) + new_varname = eq.varname + for to_replace, replacement in replacements.items(): + to_replace = '{' + to_replace + '}' + if to_replace in eq.varname: + new_varname = new_varname.replace(to_replace, replacement) + if '{' not in new_varname: + # make sure that the replacement is a valid identifier + Equations.check_identifier(new_varname) else: new_varname = eq.varname - if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: # Replace values in the RHS of the equation new_code = eq.expr.code for to_replace, replacement in replacements.items(): - if to_replace in eq.identifiers: + if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: if isinstance(replacement, str): - # replace the name with another name - new_code = re.sub('\\b' + to_replace + '\\b', - replacement, new_code) + name = replacement else: - # replace the name with a value - new_code = re.sub('\\b' + to_replace + '\\b', - '(' + repr(replacement) + ')', - new_code) + name = repr(replacement) + new_code = new_code.replace('{' + to_replace + '}', name) + new_code = re.sub('\b' + to_replace + '\b', name, new_code) try: Expression(new_code) except ValueError as ex: raise ValueError( ('Replacing "%s" with "%r" failed: %s') % (to_replace, replacement, ex)) - new_equations[new_varname] = SingleEquation(eq.type, new_varname, - dimensions=eq.dim, - var_type=eq.var_type, - expr=Expression(new_code), - flags=eq.flags) + final_equations[new_varname] = SingleEquation(eq.type, new_varname, + dimensions=eq.dim, + var_type=eq.var_type, + expr=Expression(new_code), + flags=eq.flags) else: - new_equations[new_varname] = SingleEquation(eq.type, new_varname, - dimensions=eq.dim, - var_type=eq.var_type, - flags=eq.flags) - - return new_equations + final_equations[new_varname] = SingleEquation(eq.type, new_varname, + dimensions=eq.dim, + var_type=eq.var_type, + flags=eq.flags) + return Equations([eq for eq in final_equations.values()]) def __call__(self, **kwds): return Equations(list(self._substitute(kwds).values())) @@ -673,9 +742,6 @@ def __getitem__(self, key): def __add__(self, other_eqns): if isinstance(other_eqns, str): other_eqns = parse_string_equations(other_eqns) - elif isinstance(other_eqns, EquationTemplate): - return EquationTemplate(list(self.values()) + - list(other_eqns.values())) elif not isinstance(other_eqns, Equations): return NotImplemented @@ -1091,167 +1157,6 @@ def _repr_pretty_(self, p, cycle): p.breakable('\n') -class EquationTemplate(Mapping): - def __init__(self, eqns): - if isinstance(eqns, str): - self._equations = parse_string_equations(eqns, template=True) - elif isinstance(eqns, Sequence): - self._equations = {eq.varname: eq for eq in eqns} - else: - self._equations = dict(eqns) - - def __iter__(self): - return iter(self._equations) - - def __len__(self): - return len(self._equations) - - def __getitem__(self, key): - return self._equations[key] - - def __add__(self, other_eqns): - if isinstance(other_eqns, str): - other_eqns = parse_string_equations(other_eqns) - elif not isinstance(other_eqns, (Equations, EquationTemplate)): - return NotImplemented - - return EquationTemplate(list(self.values()) + - list(other_eqns.values())) - - def __call__(self, **replacements): - if len(replacements) == 0: - return self._equations - - new_equations = {} - # First, do replacements for equations/expressions to allow other values to - # replace values in them - additional_equations = {} - for eq in self.values(): - if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: - new_code = eq.expr.code - for to_replace, replacement in replacements.items(): - if not isinstance(replacement, (Expression, ExpressionTemplate, Equations, EquationTemplate)): - continue - to_replace = '{' + to_replace + '}' - if to_replace in eq.varname: - raise TypeError(f'Cannot replace equation \'{eq.varname}\' by another equation or expression.') - if to_replace in eq.identifiers: - if isinstance(replacement, (Equations, EquationTemplate)): - name = list(replacement)[0] # use the first name - additional_equations.update(replacement._equations.items()) - else: # Expression or ExpressionTemplate - name = '('+str(replacement.code)+')' - new_code = re.sub(to_replace, name, new_code) - try: - Expression(new_code) - except ValueError as ex: - raise ValueError( - ('Replacing "%s" with "%r" failed: %s') % - (to_replace, replacement, ex)) - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - expr=Expression(new_code), - flags=eq.flags) - else: - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - flags=eq.flags) - # Now, do replacements for lists of names/expressions - for eq in new_equations.values(): - if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: - new_code = eq.expr.code - for to_replace, replacement in replacements.items(): - to_replace = '{' + to_replace + '}' - if not isinstance(replacement, Sequence) or isinstance(replacement, str): - continue - if to_replace in eq.varname: - raise TypeError(f'Cannot replace equation \'{eq.varname}\' by a list of names/expressions.') - if to_replace in eq.identifiers: - names = [] - for single_replacement in replacement: - if isinstance(single_replacement, (Equations, EquationTemplate)): - name = list(single_replacement)[0] # use the first name - additional_equations.update(single_replacement._equations.items()) - elif isinstance(single_replacement, (Expression, ExpressionTemplate)): - name = '('+str(single_replacement.code)+')' - elif isinstance(single_replacement, str): - name = single_replacement - else: - name = repr(single_replacement) - names.append(name) - new_code = re.sub(to_replace, '(' + (' + '.join(names)) + ')', new_code) - try: - Expression(new_code) - except ValueError as ex: - raise ValueError( - ('Replacing "%s" with "%r" failed: %s') % - (to_replace, replacement, ex)) - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - expr=Expression(new_code), - flags=eq.flags) - else: - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - flags=eq.flags) - new_equations.update(dict(additional_equations)) - # Finally, do replacements with concrete values - final_equations = {} - for eq in new_equations.values(): - if '{' in eq.varname: - # Replace the name of a model variable (works only for strings) - new_varname = eq.varname - for to_replace, replacement in replacements.items(): - to_replace = '{' + to_replace + '}' - if to_replace in eq.varname: - new_varname = new_varname.replace(to_replace, replacement) - if '{' not in new_varname: - # make sure that the replacement is a valid identifier - Equations.check_identifier(new_varname) - else: - new_varname = eq.varname - if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: - # Replace values in the RHS of the equation - new_code = eq.expr.code - for to_replace, replacement in replacements.items(): - to_replace = '{' + to_replace + '}' - if to_replace in eq.identifiers: - if isinstance(replacement, str): - name = replacement - else: - name = repr(replacement) - new_code = re.sub(to_replace, name, new_code) - try: - Expression(new_code) - except ValueError as ex: - raise ValueError( - ('Replacing "%s" with "%r" failed: %s') % - (to_replace, replacement, ex)) - final_equations[new_varname] = SingleEquation(eq.type, new_varname, - dimensions=eq.dim, - var_type=eq.var_type, - expr=Expression(new_code), - flags=eq.flags) - else: - final_equations[new_varname] = SingleEquation(eq.type, new_varname, - dimensions=eq.dim, - var_type=eq.var_type, - flags=eq.flags) - - if any(eq.template for eq in final_equations.values()): - return EquationTemplate(final_equations) - else: - return Equations([eq for eq in final_equations.values()]) - - def __str__(self): - strings = [str(eq) for eq in self.values()] - return '\n'.join(strings) - - def is_stateful(expression, variables): ''' Whether the given expression refers to stateful functions (and is therefore diff --git a/brian2/only.py b/brian2/only.py index 8b9aa9a66..d2baacf59 100644 --- a/brian2/only.py +++ b/brian2/only.py @@ -81,7 +81,7 @@ def restore_initial_state(): 'DEFAULT_FUNCTIONS', 'Function', 'implementation', 'declare_types', 'PreferenceError', 'BrianPreference', 'prefs', 'brian_prefs', 'Clock', 'defaultclock', - 'Equations', 'EquationTemplate', 'Expression', 'ExpressionTemplate', + 'Equations', 'Expression', 'ExpressionTemplate', 'Statements', 'BrianObject', 'BrianObjectException', diff --git a/examples/frompapers/Rothman_Manis_2003_with_templates.py b/examples/frompapers/Rothman_Manis_2003_with_templates.py index f28d67c1e..79b18e75f 100755 --- a/examples/frompapers/Rothman_Manis_2003_with_templates.py +++ b/examples/frompapers/Rothman_Manis_2003_with_templates.py @@ -45,11 +45,11 @@ ) gnabar, gkhtbar, gkltbar, gkabar, ghbar, gbarno, gl = [x * nS for x in maximal_conductances[neuron_type]] -gating_var = EquationTemplate('''d{name}/dt = q10*({name}__inf - {name})/tau_{name} : 1 - {name}__inf = {rate_expression} : 1 - tau_{name} = {tau_scale}/({forward_rate} + - {reverse_rate}) - + {tau_base} : second''') +gating_var = Equations('''d{name}/dt = q10*({name}__inf - {name})/tau_{name} : 1 + {name}__inf = {rate_expression} : 1 + tau_{name} = {tau_scale}/({forward_rate} + + {reverse_rate}) + + {tau_base} : second''') pos_sigmoid = ExpressionTemplate('1./(1+exp(-({voltage} - {midpoint}) / {scale}))') sqrt_sigmoid = ExpressionTemplate('1./(1+exp(-({voltage} - {midpoint}) / {scale}))**0.5') @@ -63,15 +63,13 @@ forward_rate=exp_voltage_dep(magnitude=5., midpoint=-60, scale=18.), reverse_rate=neg_exp_voltage_dep(magnitude=36., midpoint=-60, scale=25.), tau_base=0.04*ms, tau_scale=10*ms) - h = gating_var(name='h', rate_expression=neg_sigmoid(midpoint=-65., scale=6.), forward_rate=exp_voltage_dep(magnitude=7., midpoint=-60., scale=11.), reverse_rate=neg_exp_voltage_dep(magnitude=10., midpoint=-60., scale=25.), tau_base=0.6*ms, tau_scale=100*ms) -ina = EquationTemplate('ina = gnabar*{m}**3*{h}*(ENa-v) : amp') -ina = ina(m=m, h=h) +ina = Equations('ina = gnabar*{m}**3*{h}*(ENa-v) : amp', m=m, h=h) # KHT channel (delayed-rectifier K+) n = gating_var(name='n', @@ -86,7 +84,7 @@ reverse_rate=neg_exp_voltage_dep(magnitude=5., midpoint=-60., scale=22.), tau_base=5*ms, tau_scale=100*ms) -ikht = EquationTemplate('ikht = gkhtbar*(nf*{n}**2 + (1-nf)*{p})*(EK-v) : amp')(n=n, p=p) +ikht = Equations('ikht = gkhtbar*(nf*{n}**2 + (1-nf)*{p})*(EK-v) : amp', n=n, p=p) # Ih channel (subthreshold adaptive, non-inactivating) eqs_ih = """ @@ -140,7 +138,7 @@ bet2 = exp(1e-3*3*0.6*(vu+84)*9.648e4/(8.315*(273.16+temp))) : 1 """ -eqs =EquationTemplate(""" +eqs =Equations(""" dv/dt = (ileak + {currents} + iklt + ika + ih + ihcno + I)/C : volt vu = v/mV : 1 # unitless v I : amp From 74341ebf59673cde0772dcc07112694e1c7a200b Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 23 Oct 2020 17:25:55 +0200 Subject: [PATCH 06/19] Merge Expression and ExpressionTemplate --- brian2/equations/__init__.py | 5 +- brian2/equations/codestrings.py | 14 ++--- brian2/equations/equations.py | 8 +-- brian2/only.py | 2 +- .../Rothman_Manis_2003_with_templates.py | 57 +++++++++---------- 5 files changed, 40 insertions(+), 46 deletions(-) diff --git a/brian2/equations/__init__.py b/brian2/equations/__init__.py index 2025745c9..1ddf45ddf 100644 --- a/brian2/equations/__init__.py +++ b/brian2/equations/__init__.py @@ -2,8 +2,7 @@ Module handling equations and "code strings", expressions or statements, used for example for the reset and threshold definition of a neuron. ''' -from .codestrings import Expression, ExpressionTemplate, Statements +from .codestrings import Expression, Statements from .equations import Equations -__all__ = ['Equations', 'Expression', - 'ExpressionTemplate', 'Statements'] +__all__ = ['Equations', 'Expression', 'Statements'] diff --git a/brian2/equations/codestrings.py b/brian2/equations/codestrings.py index 8b0dc7a14..72bdefe76 100644 --- a/brian2/equations/codestrings.py +++ b/brian2/equations/codestrings.py @@ -11,7 +11,7 @@ from brian2.utils.stringtools import get_identifiers from brian2.parsing.sympytools import str_to_sympy, sympy_to_str -__all__ = ['Expression', 'ExpressionTemplate', 'Statements'] +__all__ = ['Expression', 'Statements'] logger = get_logger(__name__) @@ -76,6 +76,11 @@ class Statements(CodeString): pass +class Default(dict): + def __missing__(self, key): + return f'{{{key}}}' + + class Expression(CodeString): ''' Class for representing an expression. @@ -188,13 +193,6 @@ def __ne__(self, other): def __hash__(self): return hash(self.code) - -class Default(dict): - def __missing__(self, key): - return f'{{{key}}}' - - -class ExpressionTemplate(Expression): def __call__(self, **replacements): return Expression(code=self.code.format_map(Default(replacements))) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 28f6d54f1..961694c5e 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -32,7 +32,7 @@ from brian2.utils.logger import get_logger from brian2.utils.topsort import topsort -from .codestrings import Expression, ExpressionTemplate +from .codestrings import Expression from .unitcheck import check_dimensions @@ -611,7 +611,7 @@ def _substitute(self, replacements): if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: new_code = eq.expr.code for to_replace, replacement in replacements.items(): - if not isinstance(replacement, (Expression, ExpressionTemplate, Equations)): + if not isinstance(replacement, (Expression, Equations)): continue if to_replace in eq.varname or '{' + to_replace + '}' in eq.varname: raise TypeError(f'Cannot replace equation \'{eq.varname}\' by another equation or expression.') @@ -619,7 +619,7 @@ def _substitute(self, replacements): if isinstance(replacement, Equations): name = list(replacement)[0] # use the first name additional_equations.update(replacement._equations.items()) - else: # Expression or ExpressionTemplate + else: # Expression name = '('+str(replacement.code)+')' new_code = new_code.replace('{' + to_replace + '}', name) new_code = re.sub('\b' + to_replace + '\b', name, new_code) @@ -654,7 +654,7 @@ def _substitute(self, replacements): if isinstance(single_replacement, Equations): name = list(single_replacement)[0] # use the first name additional_equations.update(single_replacement._equations.items()) - elif isinstance(single_replacement, (Expression, ExpressionTemplate)): + elif isinstance(single_replacement, Expression): name = '('+str(single_replacement.code)+')' elif isinstance(single_replacement, str): name = single_replacement diff --git a/brian2/only.py b/brian2/only.py index d2baacf59..accfa8fcf 100644 --- a/brian2/only.py +++ b/brian2/only.py @@ -81,7 +81,7 @@ def restore_initial_state(): 'DEFAULT_FUNCTIONS', 'Function', 'implementation', 'declare_types', 'PreferenceError', 'BrianPreference', 'prefs', 'brian_prefs', 'Clock', 'defaultclock', - 'Equations', 'Expression', 'ExpressionTemplate', + 'Equations', 'Expression', 'Statements', 'BrianObject', 'BrianObjectException', diff --git a/examples/frompapers/Rothman_Manis_2003_with_templates.py b/examples/frompapers/Rothman_Manis_2003_with_templates.py index 79b18e75f..f27216ec6 100755 --- a/examples/frompapers/Rothman_Manis_2003_with_templates.py +++ b/examples/frompapers/Rothman_Manis_2003_with_templates.py @@ -51,40 +51,37 @@ {reverse_rate}) + {tau_base} : second''') -pos_sigmoid = ExpressionTemplate('1./(1+exp(-({voltage} - {midpoint}) / {scale}))') -sqrt_sigmoid = ExpressionTemplate('1./(1+exp(-({voltage} - {midpoint}) / {scale}))**0.5') -neg_sigmoid = ExpressionTemplate('1./(1+exp(({voltage} - {midpoint}) / {scale}))') -exp_voltage_dep = ExpressionTemplate('{magnitude}*exp(({voltage}-{midpoint})/{scale})') -neg_exp_voltage_dep = ExpressionTemplate('{magnitude}*exp(-({voltage}-{midpoint})/{scale})') +pos_sigmoid = Expression('1./(1+exp(-({voltage} - {midpoint}) / {scale}))') +sqrt_sigmoid = Expression('1./(1+exp(-({voltage} - {midpoint}) / {scale}))**0.5') +neg_sigmoid = Expression('1./(1+exp(({voltage} - {midpoint}) / {scale}))') +exp_voltage_dep = Expression('{magnitude}*exp(({voltage}-{midpoint})/{scale})') +neg_exp_voltage_dep = Expression('{magnitude}*exp(-({voltage}-{midpoint})/{scale})') # Classical Na channel -m = gating_var(name='m', - rate_expression=pos_sigmoid(midpoint=-38., scale=7.), - forward_rate=exp_voltage_dep(magnitude=5., midpoint=-60, scale=18.), - reverse_rate=neg_exp_voltage_dep(magnitude=36., midpoint=-60, scale=25.), - tau_base=0.04*ms, tau_scale=10*ms) -h = gating_var(name='h', - rate_expression=neg_sigmoid(midpoint=-65., scale=6.), - forward_rate=exp_voltage_dep(magnitude=7., midpoint=-60., scale=11.), - reverse_rate=neg_exp_voltage_dep(magnitude=10., midpoint=-60., scale=25.), - tau_base=0.6*ms, tau_scale=100*ms) - -ina = Equations('ina = gnabar*{m}**3*{h}*(ENa-v) : amp', m=m, h=h) +ina = Equations('ina = gnabar*{m}**3*{h}*(ENa-v) : amp', + m=gating_var(name='m', + rate_expression=pos_sigmoid(midpoint=-38., scale=7.), + forward_rate=exp_voltage_dep(magnitude=5., midpoint=-60, scale=18.), + reverse_rate=neg_exp_voltage_dep(magnitude=36., midpoint=-60, scale=25.), + tau_base=0.04*ms, tau_scale=10*ms), + h=gating_var(name='h', + rate_expression=neg_sigmoid(midpoint=-65., scale=6.), + forward_rate=exp_voltage_dep(magnitude=7., midpoint=-60., scale=11.), + reverse_rate=neg_exp_voltage_dep(magnitude=10., midpoint=-60., scale=25.), + tau_base=0.6*ms, tau_scale=100*ms)) # KHT channel (delayed-rectifier K+) -n = gating_var(name='n', - rate_expression=sqrt_sigmoid(midpoint=-15, scale=5.), - forward_rate=exp_voltage_dep(magnitude=11., midpoint=-60, scale=24.), - reverse_rate=neg_exp_voltage_dep(magnitude=21., midpoint=-60, scale=23.), - tau_base=0.7*ms, tau_scale=100*ms) - -p = gating_var(name='p', - rate_expression=pos_sigmoid(midpoint=-23., scale=6.), - forward_rate=exp_voltage_dep(magnitude=4., midpoint=-60., scale=32.), - reverse_rate=neg_exp_voltage_dep(magnitude=5., midpoint=-60., scale=22.), - tau_base=5*ms, tau_scale=100*ms) - -ikht = Equations('ikht = gkhtbar*(nf*{n}**2 + (1-nf)*{p})*(EK-v) : amp', n=n, p=p) +ikht = Equations('ikht = gkhtbar*(nf*{n}**2 + (1-nf)*{p})*(EK-v) : amp', + n=gating_var(name='n', + rate_expression=sqrt_sigmoid(midpoint=-15, scale=5.), + forward_rate=exp_voltage_dep(magnitude=11., midpoint=-60, scale=24.), + reverse_rate=neg_exp_voltage_dep(magnitude=21., midpoint=-60, scale=23.), + tau_base=0.7*ms, tau_scale=100*ms), + p=gating_var(name='p', + rate_expression=pos_sigmoid(midpoint=-23., scale=6.), + forward_rate=exp_voltage_dep(magnitude=4., midpoint=-60., scale=32.), + reverse_rate=neg_exp_voltage_dep(magnitude=5., midpoint=-60., scale=22.), + tau_base=5*ms, tau_scale=100*ms)) # Ih channel (subthreshold adaptive, non-inactivating) eqs_ih = """ From bd5b5e86d5bde766171be4ecefeaef6d05d48924 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 23 Oct 2020 18:37:07 +0200 Subject: [PATCH 07/19] Fix a minor replacement bug --- brian2/equations/equations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 961694c5e..fb8288f16 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -613,7 +613,7 @@ def _substitute(self, replacements): for to_replace, replacement in replacements.items(): if not isinstance(replacement, (Expression, Equations)): continue - if to_replace in eq.varname or '{' + to_replace + '}' in eq.varname: + if eq.varname == to_replace or '{' + to_replace + '}' in eq.varname: raise TypeError(f'Cannot replace equation \'{eq.varname}\' by another equation or expression.') if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: if isinstance(replacement, Equations): From 9b8ed846f0af5461c7cf2c6b2e7b6bfa5b0f63a4 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 23 Oct 2020 18:37:27 +0200 Subject: [PATCH 08/19] Use the template approach for all currents in Rothman&Manis example --- .../Rothman_Manis_2003_with_templates.py | 112 +++++++++--------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/examples/frompapers/Rothman_Manis_2003_with_templates.py b/examples/frompapers/Rothman_Manis_2003_with_templates.py index f27216ec6..aaa253789 100755 --- a/examples/frompapers/Rothman_Manis_2003_with_templates.py +++ b/examples/frompapers/Rothman_Manis_2003_with_templates.py @@ -52,8 +52,10 @@ + {tau_base} : second''') pos_sigmoid = Expression('1./(1+exp(-({voltage} - {midpoint}) / {scale}))') -sqrt_sigmoid = Expression('1./(1+exp(-({voltage} - {midpoint}) / {scale}))**0.5') +power_sigmoid = Expression('1./(1+exp(-({voltage} - {midpoint}) / {scale}))**{power}') neg_sigmoid = Expression('1./(1+exp(({voltage} - {midpoint}) / {scale}))') +neg_power_sigmoid = Expression('1./(1+exp(({voltage} - {midpoint}) / {scale}))**{power}') +shifted_neg_sigmoid = Expression('{shift} + (1.0-{shift})/(1+exp(({voltage} - {midpoint}) / {scale}))') exp_voltage_dep = Expression('{magnitude}*exp(({voltage}-{midpoint})/{scale})') neg_exp_voltage_dep = Expression('{magnitude}*exp(-({voltage}-{midpoint})/{scale})') @@ -73,7 +75,7 @@ # KHT channel (delayed-rectifier K+) ikht = Equations('ikht = gkhtbar*(nf*{n}**2 + (1-nf)*{p})*(EK-v) : amp', n=gating_var(name='n', - rate_expression=sqrt_sigmoid(midpoint=-15, scale=5.), + rate_expression=power_sigmoid(midpoint=-15, scale=5., power=0.5), forward_rate=exp_voltage_dep(magnitude=11., midpoint=-60, scale=24.), reverse_rate=neg_exp_voltage_dep(magnitude=21., midpoint=-60, scale=23.), tau_base=0.7*ms, tau_scale=100*ms), @@ -84,65 +86,61 @@ tau_base=5*ms, tau_scale=100*ms)) # Ih channel (subthreshold adaptive, non-inactivating) -eqs_ih = """ -ih = ghbar*r*(Eh-v) : amp -dr/dt=q10*(rinf-r)/rtau : 1 -rinf = 1. / (1+exp((vu + 76.) / 7.)) : 1 -rtau = ((100000. / (237.*exp((vu+60.) / 12.) + 17.*exp(-(vu+60.) / 14.))) + 25.)*ms : second -""" +ih = Equations('ih = ghbar*{r}*(Eh - v) : amp', + r=gating_var(name='r', + rate_expression=neg_sigmoid(midpoint=-76., scale=7.), + forward_rate=exp_voltage_dep(magnitude=237., midpoint=-60., scale=12.), + reverse_rate=neg_exp_voltage_dep(magnitude=17, midpoint=-60, scale=14.), + tau_scale=100*second, tau_base=25*ms)) # KLT channel (low threshold K+) -eqs_klt = """ -iklt = gkltbar*w**4*z*(EK-v) : amp -dw/dt=q10*(winf-w)/wtau : 1 -dz/dt=q10*(zinf-z)/ztau : 1 -winf = (1. / (1 + exp(-(vu + 48.) / 6.)))**0.25 : 1 -zinf = zss + ((1.-zss) / (1 + exp((vu + 71.) / 10.))) : 1 -wtau = ((100. / (6.*exp((vu+60.) / 6.) + 16.*exp(-(vu+60.) / 45.))) + 1.5)*ms : second -ztau = ((1000. / (exp((vu+60.) / 20.) + exp(-(vu+60.) / 8.))) + 50)*ms : second -""" - +iklt = Equations('ih = gkltbar*{w}**4*{z}*(EK-v) : amp', + w=gating_var(name='w', + rate_expression=power_sigmoid(midpoint=-48., scale=6., power=0.25), + forward_rate=exp_voltage_dep(magnitude=6., midpoint=-60., scale=6.), + reverse_rate=neg_exp_voltage_dep(magnitude=16, midpoint=-60, scale=45.), + tau_scale=100*ms, tau_base=1.5*ms), + z=gating_var(name='z', + rate_expression=shifted_neg_sigmoid(shift=zss, midpoint=-71., scale=10.), + forward_rate=exp_voltage_dep(magnitude=1., midpoint=-60., scale=20.), + reverse_rate=neg_exp_voltage_dep(magnitude=1., midpoint=-60., scale=8.), + tau_scale=1*second, tau_base=50*ms)) # Ka channel (transient K+) -eqs_ka = """ -ika = gkabar*a**4*b*c*(EK-v): amp -da/dt=q10*(ainf-a)/atau : 1 -db/dt=q10*(binf-b)/btau : 1 -dc/dt=q10*(cinf-c)/ctau : 1 -ainf = (1. / (1 + exp(-(vu + 31) / 6.)))**0.25 : 1 -binf = 1. / (1 + exp((vu + 66) / 7.))**0.5 : 1 -cinf = 1. / (1 + exp((vu + 66) / 7.))**0.5 : 1 -atau = ((100. / (7*exp((vu+60) / 14.) + 29*exp(-(vu+60) / 24.))) + 0.1)*ms : second -btau = ((1000. / (14*exp((vu+60) / 27.) + 29*exp(-(vu+60) / 24.))) + 1)*ms : second -ctau = ((90. / (1 + exp((-66-vu) / 17.))) + 10)*ms : second -""" - +ika = Equations('ika = gkabar*{a}**4*{b}*{c}*(EK-v): amp', + a=gating_var(name='a', + rate_expression=power_sigmoid(midpoint=-31., scale=6., power=0.25), + forward_rate=exp_voltage_dep(magnitude=7., midpoint=-60, scale=14.), + reverse_rate=neg_exp_voltage_dep(magnitude=29., midpoint=-60, scale=24.), + tau_scale=100*ms, tau_base=0.1*ms), + b=gating_var(name='b', + rate_expression=neg_power_sigmoid(midpoint=-66., scale=7., power=0.5), + forward_rate=exp_voltage_dep(magnitude=14., midpoint=-60., scale=27.), + reverse_rate=neg_exp_voltage_dep(magnitude=29., midpoint=-60., scale=24.), + tau_scale=1*second, tau_base=1*ms), + c=gating_var(name='c', + rate_expression=neg_power_sigmoid(midpoint=-66., scale=7., power=0.5), + forward_rate='1', + reverse_rate=neg_exp_voltage_dep(magnitude=1., midpoint=-66., scale=17.), + tau_scale=90*ms, tau_base=10*ms)) # Leak -eqs_leak = """ -ileak = gl*(El-v) : amp -""" - -# h current for octopus cells -eqs_hcno = """ -ihcno = gbarno*(h1*frac + h2*(1-frac))*(Eh-v) : amp -dh1/dt=(hinfno-h1)/tau1 : 1 -dh2/dt=(hinfno-h2)/tau2 : 1 -hinfno = 1./(1+exp((vu+66.)/7.)) : 1 -tau1 = bet1/(qt*0.008*(1+alp1))*ms : second -tau2 = bet2/(qt*0.0029*(1+alp2))*ms : second -alp1 = exp(1e-3*3*(vu+50)*9.648e4/(8.315*(273.16+temp))) : 1 -bet1 = exp(1e-3*3*0.3*(vu+50)*9.648e4/(8.315*(273.16+temp))) : 1 -alp2 = exp(1e-3*3*(vu+84)*9.648e4/(8.315*(273.16+temp))) : 1 -bet2 = exp(1e-3*3*0.6*(vu+84)*9.648e4/(8.315*(273.16+temp))) : 1 -""" - -eqs =Equations(""" -dv/dt = (ileak + {currents} + iklt + ika + ih + ihcno + I)/C : volt -vu = v/mV : 1 # unitless v -I : amp -""") -eqs = eqs(currents=[ina, ikht], voltage='vu') -eqs += Equations(eqs_leak) + Equations(eqs_ka) + Equations(eqs_ih) + Equations(eqs_klt) + Equations(eqs_hcno) - +ileak = Equations("ileak = gl*(El-v) : amp") + +# h current for octopus cells (gating variables use different equations +gating_var_octopus = Equations('''d{name}/dt = (hinfno - {name})/tau_{name} : 1 + tau_{name} = beta_{name}/(qt*{tau_scale}*(1+alpha_{name}))*ms : second + alpha_{name} = exp(1e-3*3*({voltage}-{midpoint})*9.648e4/(8.315*(273.16+temp))) : 1 + beta_{name} = exp(1e-3*3*0.3*({voltage}-{midpoint})*9.648e4/(8.315*(273.16+temp))) : 1 ''') +p +ihcno = Equations('''ihcno = gbarno*({h1}*frac + {h2}*(1-frac))*(Eh-v) : amp + hinfno = {inf_rate} : 1''', + inf_rate=neg_sigmoid(midpoint=-66., scale=7.), + h1=gating_var_octopus(name='h1', tau_scale=0.008, midpoint=-50.), + h2=gating_var_octopus(name='h2', tau_scale=0.0029, midpoint=-84.)) + +eqs =Equations("""dv/dt = ({currents} + I)/C : volt + vu = v/mV : 1 # unitless v + I : amp""", + currents=[ileak, ina, ikht, ih, iklt, ika, ihcno], voltage='vu') neuron = NeuronGroup(1, eqs, method='exponential_euler') neuron.v = El From 1d58e0e648286f07373046b70de39eff0c2122f2 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Mon, 26 Oct 2020 17:59:51 +0100 Subject: [PATCH 09/19] fix a few replacement bugs Implementation is still quick&dirty --- brian2/equations/equations.py | 45 ++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index fb8288f16..9273f53d4 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -2,6 +2,7 @@ ''' Differential equations for Brian models. ''' +import numbers from collections.abc import Mapping, Hashable import keyword import re @@ -622,7 +623,7 @@ def _substitute(self, replacements): else: # Expression name = '('+str(replacement.code)+')' new_code = new_code.replace('{' + to_replace + '}', name) - new_code = re.sub('\b' + to_replace + '\b', name, new_code) + new_code = re.sub(r'\b' + to_replace + r'\b', name, new_code) try: Expression(new_code) except ValueError as ex: @@ -662,7 +663,7 @@ def _substitute(self, replacements): name = repr(single_replacement) names.append(name) new_code = new_code.replace('{' + to_replace + '}', '(' + (' + '.join(names)) + ')') - new_code = re.sub('\b' + to_replace + '\b', '(' + (' + '.join(names)) + ')', new_code) + new_code = re.sub(r'\b' + to_replace + r'\b', '(' + (' + '.join(names)) + ')', new_code) try: Expression(new_code) except ValueError as ex: @@ -683,18 +684,25 @@ def _substitute(self, replacements): # Finally, do replacements with concrete values final_equations = {} for eq in new_equations.values(): - if '{' in eq.varname: - # Replace the name of a model variable (works only for strings) - new_varname = eq.varname - for to_replace, replacement in replacements.items(): - to_replace = '{' + to_replace + '}' - if to_replace in eq.varname: - new_varname = new_varname.replace(to_replace, replacement) - if '{' not in new_varname: - # make sure that the replacement is a valid identifier - Equations.check_identifier(new_varname) - else: - new_varname = eq.varname + # Replace the name of a model variable (works only for strings) + new_varname = eq.varname + for to_replace, replacement in replacements.items(): + if '{' + to_replace + '}' in eq.varname: + if not isinstance(replacement, str): + raise ValueError(f'Cannot replace variable name \'{eq.varname}\' with value ' + f'\'{repr(replacement)}\'') + new_varname = new_varname.replace('{' + to_replace + '}', replacement) + elif eq.varname == to_replace: + if not isinstance(replacement, str): + raise ValueError(f'Cannot replace variable name \'{eq.varname}\' with value ' + f'\'{repr(replacement)}\'') + new_varname = replacement + break + if new_varname in final_equations: + raise EquationError(f'Cannot have two definitions for the variable \'{new_varname}\'') + if '{' not in new_varname: + # make sure that the replacement is a valid identifier + Equations.check_identifier(new_varname) if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: # Replace values in the RHS of the equation new_code = eq.expr.code @@ -702,10 +710,15 @@ def _substitute(self, replacements): if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: if isinstance(replacement, str): name = replacement - else: + elif isinstance(replacement, (numbers.Number, Quantity)): + if isinstance(replacement, Quantity): + if not replacement.shape == (): + raise SyntaxError('Can only replace variables with scalar quantities.') name = repr(replacement) + elif not isinstance(replacement, (Equations, Expression)): + raise SyntaxError(f'Cannot replace a name with an object of type \'{type(replacement)}\'.') new_code = new_code.replace('{' + to_replace + '}', name) - new_code = re.sub('\b' + to_replace + '\b', name, new_code) + new_code = re.sub(r'\b' + to_replace + r'\b', name, new_code) try: Expression(new_code) except ValueError as ex: From 068fdb5e41103f4c424d6fd82f4a80b95cd9bb01 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Mon, 26 Oct 2020 18:23:18 +0100 Subject: [PATCH 10/19] Give access to template identifiers in expressions/equations --- brian2/equations/codestrings.py | 1 + brian2/equations/equations.py | 13 +++++++++++++ brian2/utils/stringtools.py | 21 +++++++++++++++------ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/brian2/equations/codestrings.py b/brian2/equations/codestrings.py index 72bdefe76..7a2603669 100644 --- a/brian2/equations/codestrings.py +++ b/brian2/equations/codestrings.py @@ -34,6 +34,7 @@ def __init__(self, code): # : Set of identifiers in the code string self.identifiers = get_identifiers(code) + self.template_identifiers = get_identifiers(code, only_template=True) code = property(lambda self: self._code, doc='The code string') diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 9273f53d4..c874d5f26 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -445,6 +445,14 @@ def __init__(self, type, varname, dimensions, var_type=FLOAT, expr=None, if not self.expr is None else set([]), doc='All identifiers in the RHS of this equation.') + template_identifiers = property(lambda self: (self.expr.template_identifiers + if not self.expr is None else set([])) | + get_identifiers(self.varname, only_template=True), + doc='All template identifiers used in this equation (including as part of the variable ' + 'name.') + + template = property(lambda self: len(self.template_identifiers) > 0) + stochastic_variables = property(lambda self: {variable for variable in self.identifiers if variable =='xi' or variable.startswith('xi_')}, doc='Stochastic variables in the RHS of this equation') @@ -970,6 +978,11 @@ def _get_stochastic_type(self): doc=('Set of all identifiers used in the equations, ' 'excluding the variables defined in the equations')) + template_identifiers = property(lambda self: set().union(*[eq.template_identifiers for + eq in self._equations.values()]), + doc=('Set of all template identifiers (placeholders) used in the' + 'equations, including as part of the variable names.')) + stochastic_variables = property(lambda self: {variable for variable in self.identifiers if variable =='xi' or variable.startswith('xi_')}) diff --git a/brian2/utils/stringtools.py b/brian2/utils/stringtools.py index a98e0dafe..6a17dfc9e 100644 --- a/brian2/utils/stringtools.py +++ b/brian2/utils/stringtools.py @@ -152,7 +152,7 @@ def replace(s, substitutions): KEYWORDS = {'and', 'or', 'not', 'True', 'False'} -def get_identifiers(expr, include_numbers=False): +def get_identifiers(expr, include_numbers=False, only_template=False): ''' Return all the identifiers in a given string ``expr``, that is everything that matches a programming language variable like expression, which is @@ -164,7 +164,9 @@ def get_identifiers(expr, include_numbers=False): The string to analyze include_numbers : bool, optional Whether to include number literals in the output. Defaults to ``False``. - + only_template : bool, optional + Whether to only return template_identifiers, i.e. identifiers enclosed + by curly braces. Defaults to ``False`` Returns ------- identifiers : set @@ -174,21 +176,28 @@ def get_identifiers(expr, include_numbers=False): -------- >>> expr = '3-a*_b+c5+8+f(A - .3e-10, tau_2)*17' >>> ids = get_identifiers(expr) - >>> print(sorted(list(ids))) + >>> print(sorted(ids)) ['A', '_b', 'a', 'c5', 'f', 'tau_2'] >>> ids = get_identifiers(expr, include_numbers=True) - >>> print(sorted(list(ids))) + >>> print(sorted(ids)) ['.3e-10', '17', '3', '8', 'A', '_b', 'a', 'c5', 'f', 'tau_2'] + >>> template_expr = '{name}_{suffix} = a*{name}_{suffix} + b' + >>> template_ids = get_identifiers(template_expr, only_template=True) + >>> print(sorted(template_ids)) + ['name', 'suffix'] ''' identifiers = set(re.findall(r'\b[A-Za-z_][A-Za-z0-9_]*\b', expr)) - template_identifiers = set(re.findall(r'[{][A-Za-z_][A-Za-z0-9_]*[}]', expr)) + template_identifiers = set(re.findall(r'(?:[{])([A-Za-z_][A-Za-z0-9_]*)(?:[}])', expr)) if include_numbers: # only the number, not a + or - numbers = set(re.findall(r'(?<=[^A-Za-z_])[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?|^[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?', expr)) else: numbers = set() - return (identifiers - KEYWORDS) | template_identifiers | numbers + if only_template: + return template_identifiers + else: + return (identifiers - KEYWORDS) | template_identifiers | numbers def strip_empty_lines(s): From ab7d29ae6685c9da008007e9bdf9249040a2daf5 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Mon, 26 Oct 2020 18:48:16 +0100 Subject: [PATCH 11/19] WIP: replace dependencies in order --- brian2/equations/equations.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index c874d5f26..5065e4531 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -612,6 +612,23 @@ def _substitute(self, replacements): if len(replacements) == 0: return self._equations + dependencies = {} + for to_replace, replacement in replacements.items(): + if not isinstance(replacement, Sequence) or isinstance(replacement, str): + replacement = [replacement] + for one_replacement in replacement: + dependencies[to_replace] = set() + if not isinstance(one_replacement, (numbers.Number, Quantity, str, Expression, Equations)): + raise TypeError(f'Cannot use an object of type \'{type(one_replacement)}\'' + f'to replace \'{to_replace}\'') + if isinstance(one_replacement, (Expression, Equations)): + dependencies[to_replace] |= one_replacement.template_identifiers + # We only care about dependencies to values that are replaced at the same time + for dep_key, deps in dependencies.items(): + dependencies[dep_key] = {d for d in deps if d in dependencies} + + replacements_in_order = topsort(dependencies)[::-1] + new_equations = {} # First, do replacements for equations/expressions to allow other values to # replace values in them @@ -695,13 +712,15 @@ def _substitute(self, replacements): # Replace the name of a model variable (works only for strings) new_varname = eq.varname for to_replace, replacement in replacements.items(): + if isinstance(replacement, (Expression, Equations)) or isinstance(replacement, Sequence) and not isinstance(replacement, str): + continue if '{' + to_replace + '}' in eq.varname: if not isinstance(replacement, str): raise ValueError(f'Cannot replace variable name \'{eq.varname}\' with value ' f'\'{repr(replacement)}\'') new_varname = new_varname.replace('{' + to_replace + '}', replacement) elif eq.varname == to_replace: - if not isinstance(replacement, str): + if isinstance(replacement, (str)): raise ValueError(f'Cannot replace variable name \'{eq.varname}\' with value ' f'\'{repr(replacement)}\'') new_varname = replacement From f934614a28dd4e0420dfed74eee5485ee78cf872 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Wed, 28 Oct 2020 15:59:32 +0100 Subject: [PATCH 12/19] Refactor/simplify equation substitution code Improve error checking --- brian2/equations/codestrings.py | 4 + brian2/equations/equations.py | 230 ++++++++---------- brian2/tests/test_equations.py | 4 +- .../Rothman_Manis_2003_with_templates.py | 4 +- 4 files changed, 103 insertions(+), 139 deletions(-) diff --git a/brian2/equations/codestrings.py b/brian2/equations/codestrings.py index 7a2603669..ad4eddf5d 100644 --- a/brian2/equations/codestrings.py +++ b/brian2/equations/codestrings.py @@ -105,6 +105,10 @@ def __init__(self, code=None, sympy_expression=None): if code is None: code = sympy_to_str(sympy_expression) + elif '{' not in code: + # Just try to convert it to a sympy expression to get syntax errors + # for incorrect expressions + str_to_sympy(code) super(Expression, self).__init__(code=code) stochastic_variables = property(lambda self: {variable for variable in self.identifiers diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 5065e4531..79104c388 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -608,10 +608,96 @@ def __init__(self, eqns, **kwds): #: Cache for equations with the subexpressions substituted self._substituted_expressions = None + def _do_substitution(self, equations, to_replace, replacement): + # Replacements can be lists, deal with single replacements + # as single-element lists + new_equations = {} + additional_equations = {} + replaced_something = False + if not isinstance(replacement, Sequence) or isinstance(replacement, str): + replacement = [replacement] + replacement_strs = [] + for one_replacement in replacement: + if isinstance(one_replacement, str): + replacement_strs.append(one_replacement) + elif isinstance(one_replacement, (numbers.Number, Quantity)): + if not getattr(one_replacement, 'shape', ()) == (): + raise TypeError(f'Cannot replace variable \'{to_replace}\' with an ' + f'array of values.') + replacement_strs.append(repr(one_replacement)) + elif isinstance(one_replacement, Expression): + replacement_strs.append(one_replacement.code) + elif isinstance(one_replacement, Equations): + replacement_strs.append(list(one_replacement.keys())[0]) # name of first equation + for additional_eq in one_replacement: + if additional_eq in equations or additional_eq in additional_equations: + raise SyntaxError(f'Adding equations to replace \'{to_replace}\' leads ' + f'to duplicated definition of variable \'{additional_eq}\'') + additional_equations[additional_eq] = one_replacement[additional_eq] + else: + raise TypeError(f'Cannot replace \'{to_replace}\' with an object of type ' + f'\'{type(one_replacement)}\'.') + if len(replacement_strs) == 1: + replacement_str = replacement_strs[0] + # Be careful if the string is more than just a name/number + if any(c not in string.ascii_letters + string.digits + '_' + '.' + for c in replacement_str): + replacement_str = '(' + replacement_str + ')' + else: + replacement_str = '(' + (' + '.join(replacement_strs)) + ')' + + for eq in equations.values(): + # Check whether the variable name itself (or part of it) will be replaced + if eq.varname == to_replace: + if not len(replacement) == 1: + raise TypeError(f'Cannot replace variable name \'{to_replace}\' with ' + f'a list of values.') + if not isinstance(replacement[0], str): + raise TypeError(f'Cannot replace variable name \'{to_replace}\' with ' + f'an object of type \'{type(replacement[0])}\'.') + new_varname = replacement[0] + elif '{' + to_replace + '}' in eq.varname: + if not len(replacement) == 1: + raise TypeError(f'Cannot replace \'{{{to_replace}}}\' as a part of a variable' + f'name with a list of values.') + if not isinstance(replacement[0], str): + raise TypeError(f'Cannot replace \'{{{to_replace}}}\' as a part of a variable' + f'name with an object of type \'{type(replacement[0])}\'.') + new_varname = eq.varname.replace('{' + to_replace + '}', replacement[0]) + else: + new_varname = eq.varname + + # Check whether the new variable name is still valid + if '{' not in new_varname: + Equations.check_identifier(new_varname) + if new_varname != eq.varname: + if new_varname in equations: + raise EquationError(f'Cannot replace \'{eq.varname}\' by \'{new_varname}\', ' \ + 'this name is already used for another variable.') + # Replace occurrences in the RHS of equations + new_expr = eq.expr + if to_replace in eq.identifiers: + code = eq.expr.code + new_expr = Expression(re.sub(r'\b' + to_replace + r'\b', + replacement_str, code)) + if to_replace in eq.template_identifiers: + code = eq.expr.code + new_expr = Expression(code.replace('{'+to_replace+'}', + replacement_str)) + new_equations[new_varname] = SingleEquation(eq.type, + new_varname, + dimensions=eq.dim, + var_type=eq.var_type, + expr=new_expr, + flags=eq.flags) + new_equations.update(additional_equations) + return new_equations + def _substitute(self, replacements): if len(replacements) == 0: return self._equations + # Figure out in which order elements should be substituted dependencies = {} for to_replace, replacement in replacements.items(): if not isinstance(replacement, Sequence) or isinstance(replacement, str): @@ -622,147 +708,21 @@ def _substitute(self, replacements): raise TypeError(f'Cannot use an object of type \'{type(one_replacement)}\'' f'to replace \'{to_replace}\'') if isinstance(one_replacement, (Expression, Equations)): - dependencies[to_replace] |= one_replacement.template_identifiers + dependencies[to_replace] |= one_replacement.identifiers | one_replacement.template_identifiers # We only care about dependencies to values that are replaced at the same time for dep_key, deps in dependencies.items(): dependencies[dep_key] = {d for d in deps if d in dependencies} replacements_in_order = topsort(dependencies)[::-1] - new_equations = {} - # First, do replacements for equations/expressions to allow other values to - # replace values in them - additional_equations = {} - for eq in self.values(): - if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: - new_code = eq.expr.code - for to_replace, replacement in replacements.items(): - if not isinstance(replacement, (Expression, Equations)): - continue - if eq.varname == to_replace or '{' + to_replace + '}' in eq.varname: - raise TypeError(f'Cannot replace equation \'{eq.varname}\' by another equation or expression.') - if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: - if isinstance(replacement, Equations): - name = list(replacement)[0] # use the first name - additional_equations.update(replacement._equations.items()) - else: # Expression - name = '('+str(replacement.code)+')' - new_code = new_code.replace('{' + to_replace + '}', name) - new_code = re.sub(r'\b' + to_replace + r'\b', name, new_code) - try: - Expression(new_code) - except ValueError as ex: - raise ValueError( - ('Replacing "%s" with "%r" failed: %s') % - (to_replace, replacement, ex)) - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - expr=Expression(new_code), - flags=eq.flags) - else: - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - flags=eq.flags) - # Now, do replacements for lists of names/expressions - for eq in new_equations.values(): - if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: - new_code = eq.expr.code - for to_replace, replacement in replacements.items(): - if not isinstance(replacement, Sequence) or isinstance(replacement, str): - continue - if to_replace in eq.varname or '{' + to_replace + '}' in eq.varname: - raise TypeError(f'Cannot replace equation \'{eq.varname}\' by a list of names/expressions.') - if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: - names = [] - for single_replacement in replacement: - if isinstance(single_replacement, Equations): - name = list(single_replacement)[0] # use the first name - additional_equations.update(single_replacement._equations.items()) - elif isinstance(single_replacement, Expression): - name = '('+str(single_replacement.code)+')' - elif isinstance(single_replacement, str): - name = single_replacement - else: - name = repr(single_replacement) - names.append(name) - new_code = new_code.replace('{' + to_replace + '}', '(' + (' + '.join(names)) + ')') - new_code = re.sub(r'\b' + to_replace + r'\b', '(' + (' + '.join(names)) + ')', new_code) - try: - Expression(new_code) - except ValueError as ex: - raise ValueError( - ('Replacing "%s" with "%r" failed: %s') % - (to_replace, replacement, ex)) - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - expr=Expression(new_code), - flags=eq.flags) - else: - new_equations[eq.varname] = SingleEquation(eq.type, eq.varname, - dimensions=eq.dim, - var_type=eq.var_type, - flags=eq.flags) - new_equations.update(dict(additional_equations)) - # Finally, do replacements with concrete values - final_equations = {} - for eq in new_equations.values(): - # Replace the name of a model variable (works only for strings) - new_varname = eq.varname - for to_replace, replacement in replacements.items(): - if isinstance(replacement, (Expression, Equations)) or isinstance(replacement, Sequence) and not isinstance(replacement, str): - continue - if '{' + to_replace + '}' in eq.varname: - if not isinstance(replacement, str): - raise ValueError(f'Cannot replace variable name \'{eq.varname}\' with value ' - f'\'{repr(replacement)}\'') - new_varname = new_varname.replace('{' + to_replace + '}', replacement) - elif eq.varname == to_replace: - if isinstance(replacement, (str)): - raise ValueError(f'Cannot replace variable name \'{eq.varname}\' with value ' - f'\'{repr(replacement)}\'') - new_varname = replacement - break - if new_varname in final_equations: - raise EquationError(f'Cannot have two definitions for the variable \'{new_varname}\'') - if '{' not in new_varname: - # make sure that the replacement is a valid identifier - Equations.check_identifier(new_varname) - if eq.type in [SUBEXPRESSION, DIFFERENTIAL_EQUATION]: - # Replace values in the RHS of the equation - new_code = eq.expr.code - for to_replace, replacement in replacements.items(): - if to_replace in eq.identifiers or '{' + to_replace + '}' in eq.identifiers: - if isinstance(replacement, str): - name = replacement - elif isinstance(replacement, (numbers.Number, Quantity)): - if isinstance(replacement, Quantity): - if not replacement.shape == (): - raise SyntaxError('Can only replace variables with scalar quantities.') - name = repr(replacement) - elif not isinstance(replacement, (Equations, Expression)): - raise SyntaxError(f'Cannot replace a name with an object of type \'{type(replacement)}\'.') - new_code = new_code.replace('{' + to_replace + '}', name) - new_code = re.sub(r'\b' + to_replace + r'\b', name, new_code) - try: - Expression(new_code) - except ValueError as ex: - raise ValueError( - ('Replacing "%s" with "%r" failed: %s') % - (to_replace, replacement, ex)) - final_equations[new_varname] = SingleEquation(eq.type, new_varname, - dimensions=eq.dim, - var_type=eq.var_type, - expr=Expression(new_code), - flags=eq.flags) - else: - final_equations[new_varname] = SingleEquation(eq.type, new_varname, - dimensions=eq.dim, - var_type=eq.var_type, - flags=eq.flags) - return Equations([eq for eq in final_equations.values()]) + new_equations = dict(self) + # The replacement code below generates a full set of equations for each replacement + # which is unnecessary most of the time, but simplify things a lot + for to_replace in replacements_in_order: + replacement = replacements[to_replace] + new_equations = self._do_substitution(new_equations, to_replace, replacement) + + return Equations([eq for eq in new_equations.values()]) def __call__(self, **kwds): return Equations(list(self._substitute(kwds).values())) diff --git a/brian2/tests/test_equations.py b/brian2/tests/test_equations.py index 0c614750c..06e338b2f 100644 --- a/brian2/tests/test_equations.py +++ b/brian2/tests/test_equations.py @@ -221,11 +221,11 @@ def test_wrong_replacements(): ''', v='x') # Replacing a model variable name with a value - with pytest.raises(ValueError): + with pytest.raises(TypeError): Equations('dv/dt = -v / tau : 1', v=3 * mV) # Replacing with an illegal value - with pytest.raises(SyntaxError): + with pytest.raises(TypeError): Equations('dv/dt = -v/tau : 1', tau=np.arange(5)) diff --git a/examples/frompapers/Rothman_Manis_2003_with_templates.py b/examples/frompapers/Rothman_Manis_2003_with_templates.py index aaa253789..d0dc50476 100755 --- a/examples/frompapers/Rothman_Manis_2003_with_templates.py +++ b/examples/frompapers/Rothman_Manis_2003_with_templates.py @@ -94,7 +94,7 @@ tau_scale=100*second, tau_base=25*ms)) # KLT channel (low threshold K+) -iklt = Equations('ih = gkltbar*{w}**4*{z}*(EK-v) : amp', +iklt = Equations('iklt = gkltbar*{w}**4*{z}*(EK-v) : amp', w=gating_var(name='w', rate_expression=power_sigmoid(midpoint=-48., scale=6., power=0.25), forward_rate=exp_voltage_dep(magnitude=6., midpoint=-60., scale=6.), @@ -130,7 +130,7 @@ tau_{name} = beta_{name}/(qt*{tau_scale}*(1+alpha_{name}))*ms : second alpha_{name} = exp(1e-3*3*({voltage}-{midpoint})*9.648e4/(8.315*(273.16+temp))) : 1 beta_{name} = exp(1e-3*3*0.3*({voltage}-{midpoint})*9.648e4/(8.315*(273.16+temp))) : 1 ''') -p + ihcno = Equations('''ihcno = gbarno*({h1}*frac + {h2}*(1-frac))*(Eh-v) : amp hinfno = {inf_rate} : 1''', inf_rate=neg_sigmoid(midpoint=-66., scale=7.), From e20fb64e7b2f0c282e0a9126598e4f90c4736a83 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Wed, 28 Oct 2020 18:22:07 +0100 Subject: [PATCH 13/19] Fix a minor replacement issue --- brian2/equations/equations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 79104c388..d44afb005 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -678,10 +678,10 @@ def _do_substitution(self, equations, to_replace, replacement): new_expr = eq.expr if to_replace in eq.identifiers: code = eq.expr.code - new_expr = Expression(re.sub(r'\b' + to_replace + r'\b', + new_expr = Expression(re.sub(r'(? Date: Thu, 29 Oct 2020 15:34:35 +0100 Subject: [PATCH 14/19] Raise an error for an unused replacement argument --- brian2/equations/equations.py | 8 ++++++++ brian2/tests/test_equations.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index d44afb005..de30473dc 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -656,6 +656,7 @@ def _do_substitution(self, equations, to_replace, replacement): raise TypeError(f'Cannot replace variable name \'{to_replace}\' with ' f'an object of type \'{type(replacement[0])}\'.') new_varname = replacement[0] + replaced_something = True elif '{' + to_replace + '}' in eq.varname: if not len(replacement) == 1: raise TypeError(f'Cannot replace \'{{{to_replace}}}\' as a part of a variable' @@ -664,6 +665,7 @@ def _do_substitution(self, equations, to_replace, replacement): raise TypeError(f'Cannot replace \'{{{to_replace}}}\' as a part of a variable' f'name with an object of type \'{type(replacement[0])}\'.') new_varname = eq.varname.replace('{' + to_replace + '}', replacement[0]) + replaced_something = True else: new_varname = eq.varname @@ -680,16 +682,22 @@ def _do_substitution(self, equations, to_replace, replacement): code = eq.expr.code new_expr = Expression(re.sub(r'(? Date: Thu, 29 Oct 2020 15:39:29 +0100 Subject: [PATCH 15/19] Raise a warning for ambiguous replacements --- brian2/equations/equations.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index de30473dc..99dcf6e85 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -613,7 +613,8 @@ def _do_substitution(self, equations, to_replace, replacement): # as single-element lists new_equations = {} additional_equations = {} - replaced_something = False + replaced_name = False + replaced_placeholder = False if not isinstance(replacement, Sequence) or isinstance(replacement, str): replacement = [replacement] replacement_strs = [] @@ -656,7 +657,7 @@ def _do_substitution(self, equations, to_replace, replacement): raise TypeError(f'Cannot replace variable name \'{to_replace}\' with ' f'an object of type \'{type(replacement[0])}\'.') new_varname = replacement[0] - replaced_something = True + replaced_name = True elif '{' + to_replace + '}' in eq.varname: if not len(replacement) == 1: raise TypeError(f'Cannot replace \'{{{to_replace}}}\' as a part of a variable' @@ -665,7 +666,7 @@ def _do_substitution(self, equations, to_replace, replacement): raise TypeError(f'Cannot replace \'{{{to_replace}}}\' as a part of a variable' f'name with an object of type \'{type(replacement[0])}\'.') new_varname = eq.varname.replace('{' + to_replace + '}', replacement[0]) - replaced_something = True + replaced_placeholder = True else: new_varname = eq.varname @@ -682,12 +683,12 @@ def _do_substitution(self, equations, to_replace, replacement): code = eq.expr.code new_expr = Expression(re.sub(r'(? Date: Thu, 29 Oct 2020 18:23:03 +0100 Subject: [PATCH 16/19] Make Expression substitution consistent with Equations --- brian2/equations/codestrings.py | 96 ++++++++++++++++++- brian2/equations/equations.py | 17 +++- brian2/utils/stringtools.py | 37 +++---- .../Rothman_Manis_2003_with_templates.py | 5 +- 4 files changed, 130 insertions(+), 25 deletions(-) diff --git a/brian2/equations/codestrings.py b/brian2/equations/codestrings.py index ad4eddf5d..7e6f1644e 100644 --- a/brian2/equations/codestrings.py +++ b/brian2/equations/codestrings.py @@ -3,12 +3,18 @@ information about its namespace. Only serves as a parent class, its subclasses `Expression` and `Statements` are the ones that are actually used. ''' +import re +import string from collections.abc import Hashable +from typing import Sequence +import numbers import sympy +import numpy as np from brian2.utils.logger import get_logger from brian2.utils.stringtools import get_identifiers +from brian2.utils.topsort import topsort from brian2.parsing.sympytools import str_to_sympy, sympy_to_str __all__ = ['Expression', 'Statements'] @@ -34,7 +40,7 @@ def __init__(self, code): # : Set of identifiers in the code string self.identifiers = get_identifiers(code) - self.template_identifiers = get_identifiers(code, only_template=True) + self.template_identifiers = get_identifiers(code, template=True) code = property(lambda self: self._code, doc='The code string') @@ -198,8 +204,94 @@ def __ne__(self, other): def __hash__(self): return hash(self.code) + def _do_substitution(self, to_replace, replacement): + # Replacements can be lists, deal with single replacements + # as single-element lists + replaced_name = False + replaced_placeholder = False + if not isinstance(replacement, Sequence) or isinstance(replacement, str): + replacement = [replacement] + replacement_strs = [] + for one_replacement in replacement: + if isinstance(one_replacement, str): + if any(c not in string.ascii_letters + '_{}' + for c in one_replacement): + # Check whether the replacement can be interpreted as an expression + try: + expr = Expression(one_replacement) + replacement_strs.append(expr.code) + except SyntaxError: + raise SyntaxError(f'Replacement \'{one_replacement}\' for' + f'\'{to_replace}\' is neither a name nor a ' + f'valid expression.') + else: + replacement_strs.append(one_replacement) + elif isinstance(one_replacement, (numbers.Number, np.ndarray)): + if not getattr(one_replacement, 'shape', ()) == (): + raise TypeError(f'Cannot replace variable \'{to_replace}\' with an ' + f'array of values.') + replacement_strs.append(repr(one_replacement)) + elif isinstance(one_replacement, Expression): + replacement_strs.append(one_replacement.code) + else: + raise TypeError(f'Cannot replace \'{to_replace}\' with an object of type ' + f'\'{type(one_replacement)}\'.') + + if len(replacement_strs) == 1: + replacement_str = replacement_strs[0] + # Be careful if the string is more than just a name/number + if any(c not in string.ascii_letters + string.digits + '_.{}' + for c in replacement_str): + replacement_str = '(' + replacement_str + ')' + else: + replacement_str = '(' + (' + '.join(replacement_strs)) + ')' + + new_expr = self + if to_replace in new_expr.identifiers: + code = new_expr.code + new_expr = Expression(re.sub(r'(?>> print(sorted(ids)) ['.3e-10', '17', '3', '8', 'A', '_b', 'a', 'c5', 'f', 'tau_2'] >>> template_expr = '{name}_{suffix} = a*{name}_{suffix} + b' - >>> template_ids = get_identifiers(template_expr, only_template=True) + >>> template_ids = get_identifiers(template_expr, template=True) >>> print(sorted(template_ids)) ['name', 'suffix'] ''' - identifiers = set(re.findall(r'\b[A-Za-z_][A-Za-z0-9_]*\b', expr)) - template_identifiers = set(re.findall(r'(?:[{])([A-Za-z_][A-Za-z0-9_]*)(?:[}])', expr)) - if include_numbers: - # only the number, not a + or - - numbers = set(re.findall(r'(?<=[^A-Za-z_])[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?|^[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?', - expr)) - else: - numbers = set() - if only_template: - return template_identifiers + if include_numbers and template: + raise ValueError('Cannot combine the \'template\' and \'include_numbers\' arguments.') + + if template: + return set(re.findall(r'(?:[{])([A-Za-z_][A-Za-z0-9_]*)(?:[}])', expr)) else: - return (identifiers - KEYWORDS) | template_identifiers | numbers + if include_numbers: + # only the number, not a + or - + number_regexp = r'(?<=[^A-Za-z_])[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?|^[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?' + numbers = set(re.findall(number_regexp, expr)) + else: + numbers = set() + identifiers = set(re.findall(r'(? Date: Thu, 29 Oct 2020 18:30:30 +0100 Subject: [PATCH 17/19] Raise an error if templates are used in NeuronGroup/Synapses --- brian2/groups/neurongroup.py | 7 +++++++ brian2/synapses/synapses.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/brian2/groups/neurongroup.py b/brian2/groups/neurongroup.py index 5058e1bcc..6ebc451e1 100644 --- a/brian2/groups/neurongroup.py +++ b/brian2/groups/neurongroup.py @@ -505,6 +505,13 @@ def __init__(self, N, model, if not isinstance(model, Equations): raise TypeError(('model has to be a string or an Equations ' 'object, is "%s" instead.') % type(model)) + if len(model.template_identifiers) > 0: + identifiers = [f'\'{identifier}\'' + for identifier in model.template_identifiers] + identifier_list = ', '.join(sorted(identifiers)) + raise TypeError('The model equations contain placeholders, substitute ' + 'names/values for the ' f'following before passing ' + f'them to {self.__class__.__name__}: {identifier_list}.') # Check flags model.check_flags({DIFFERENTIAL_EQUATION: ('unless refractory',), diff --git a/brian2/synapses/synapses.py b/brian2/synapses/synapses.py index 5d5ef523d..da077954c 100644 --- a/brian2/synapses/synapses.py +++ b/brian2/synapses/synapses.py @@ -742,6 +742,14 @@ def __init__(self, source, target=None, model=None, on_pre=None, raise TypeError(('model has to be a string or an Equations ' 'object, is "%s" instead.') % type(model)) + if len(model.template_identifiers) > 0: + identifiers = [f'\'{identifier}\'' + for identifier in model.template_identifiers] + identifier_list = ', '.join(sorted(identifiers)) + raise TypeError('The model equations contain placeholders, substitute ' + 'names/values for the ' f'following before passing ' + f'them to {self.__class__.__name__}: {identifier_list}.') + # Check flags model.check_flags({DIFFERENTIAL_EQUATION: ['event-driven', 'clock-driven'], SUBEXPRESSION: ['summed', 'shared', From a0d0f249b7b25e80d493dbd07e4bb6dc5698cc70 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Tue, 3 Nov 2020 19:51:51 +0100 Subject: [PATCH 18/19] Fix replacements with equations without expressions (i.e. parameters) --- brian2/equations/equations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 2d59299d4..2bba46273 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -690,12 +690,12 @@ def _do_substitution(self, equations, to_replace, replacement): 'this name is already used for another variable.') # Replace occurrences in the RHS of equations new_expr = eq.expr - if to_replace in eq.identifiers: + if to_replace in eq.identifiers and eq.expr is not None: code = eq.expr.code new_expr = Expression(re.sub(r'(? Date: Thu, 31 Dec 2020 17:11:05 +0100 Subject: [PATCH 19/19] Store original equations before substitution Can be helpful for error messages, etc. --- brian2/equations/equations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brian2/equations/equations.py b/brian2/equations/equations.py index 2bba46273..98eb5c3ca 100644 --- a/brian2/equations/equations.py +++ b/brian2/equations/equations.py @@ -584,6 +584,7 @@ def __init__(self, eqns, **kwds): eq.varname) self._equations[eq.varname] = eq + self._orig_equations = self._equations self._equations = self._substitute(kwds) # Check for special symbol xi (stochastic term)