Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add prefix/postfix method to Equations #1051

Closed
wants to merge 6 commits into from
Closed

Conversation

mstimberg
Copy link
Member

Closes #1050

This should implement basically everything discussed in the PR. In addition to the prefix/postfix, the functions have two arguments: variables and constants, where you can give a list of variable or constant names (where constants is everything that is not defined within the equations) that you want to have prefixed. For convenience, you can give True or False instead of a list, meaning "all" or "none" respectively. I think this is quite intuitive and matches what we do for the record argument of monitors. By default, variables=True, constants=False.

If you are happy with the general approach/syntax, then there are only a few minor things still to do:

  1. Is constants the best name, given that for an ion channel that does not define v itself, v would be part of the "constants"?
  2. Currently, all function names would be part of the "constants" as well, so I'll exclude all the default functions (we certainly do not want exp to become exp_postfixed). User-defined functions would still be prefixed/postfixed if you do constants=True, but fixing this is a bit tricky and I think not worth the trouble for a not highly used feature. Maybe a note about this in the documentation would be enough?
  3. Using this feature in an example could be done in a separate PR, but at least I need to explain it in the equations documentation.

Here (from the docstring) how it works right now:

>>> from brian2 import *
>>> eqs = Equations("""
... I = g*m**3*h*(E - v) : amp
... dm/dt=q10*(minf - m) / mtau : 1
... dh/dt=q10*(hinf - h) / htau : 1""")
>>> print(eqs.prefix('Na_'))
Na_I = g*Na_m**3*Na_h*(E - v) : amp
dNa_h/dt = q10*(hinf - Na_h) / htau : 1
dNa_m/dt = q10*(minf - Na_m) / mtau : 1
>>> print(eqs.prefix('Na_', constants=['E']))
Na_I = g*Na_m**3*Na_h*(Na_E - v) : amp
dNa_h/dt = q10*(hinf - Na_h) / htau : 1
dNa_m/dt = q10*(minf - Na_m) / mtau : 1
>>> print(eqs.prefix('Na_', variables=['I'], constants=True))
Na_I = Na_g*m**3*h*(Na_E - Na_v) : amp
dh/dt = Na_q10*(Na_hinf - h) / Na_htau : 1
dm/dt = Na_q10*(Na_minf - m) / Na_mtau : 1

This gives an easy way to make a copy of an equation, while substituting
some of the variable names.
@thesamovar
Copy link
Member

Could be internal/external instead of variables and constants?

Also, maybe an exclude option in case you want to prefix almost everything except one variable?

I'm wondering if we should have an argument names which can be either variables or constants rather than forcing the user to put things either in the constants or variables lists? So with the above suggested change, usage patterns might be:

eqs.prefix('Na_') # all internal no external variables, most commonly used pattern I guess
eqs.prefix('Na_', names=[...]) # specific set of names
eqs.prefix('Na_', internal=True, external=True, exclude=[...]) # everything except ...

Maybe we should make examples of using this feature part of the PR? Reworking existing examples ideally. Good to make docs not separate from code for PRs.

Another approach that would be rather different would be to have a function that combines multiple Equations together rather than a method of Equations. The advantage here is that more contextual information would be available about what is defined elsewhere. So you'd call it as:

combine_equations(prefixed(eqs, 'Na_'), ...)

or something like that. The point is that like this, assuming prefixed just returns an annotated copy of the equations without actually changing anything, combine_equations then knows what is an internally defined variable across the whole set of equations passed to it. Could that be useful?

@mstimberg
Copy link
Member Author

Thanks, internal/external makes sense, I've updated the code accordingly.

I also think that an exclude option can be useful, I added it as well.

I'm wondering if we should have an argument names which can be either variables or constants rather than forcing the user to put things either in the constants or variables lists?

I'm not very happy with this, because I find it confusing if names gets combined with either internal or external. What does it mean if you call eqs.prefix(names=[...], internal=True)? We could raise an error for this, but at the same time we want internal=True to be the default value. So the only solution I'd see would be to have internal=None, external=None as the default values, where None means "default value" which is True for internal, but False for external... Quite confusing, no? I have to say I prefer the "list or boolean" solution, especially given that we use these semantics already elsewhere (record argument).

Another approach that would be rather different would be to have a function that combines multiple Equations together rather than a method of Equations.

Not very fond of this one, either :) I think having the "internal" variables being defined for a single set of equations is what we'd normally want. E.g. you have a general equation for a type of channel, and then postfix it with _Na -- in this case, you only want the variables of this equation to be postfixed, not, say, the membrane potential even though it is an internal variable of the full equations. Or maybe I misunderstood what you meant? Maybe we need to have a few real-life use cases to see how it looks in practice?
Here's something very basic that I could think of (of course you should just write down the equations in this case, but you could imagine something like this with multiple types of synapses and different neuron types having multiple combinations):

basic_eq = Equations('dv/dt = (g_L*(E_L - v) + currents)/Cm : volt')
synaptic_current = Equations('''I = g*(E - v) : amp
                                dg/dt = -g/tau : siemens''')
full_eq = (basic_eq +
           Equations('currents = I_ge + I_gi : amp')  + # any way to simplify this as well?
           synaptic_current.postfix('_ge', external=True, exclude=['v']) +
           synaptic_current.postfix('_gi', external=True, exclude=['v']))
print(full_eq)

Output:

I_ge = g_ge*(E_ge - v) : amp
I_gi = g_gi*(E_gi - v) : amp
currents = I_ge + I_gi : amp
dg_ge/dt = -g_ge/tau_ge : siemens
dg_gi/dt = -g_gi/tau_gi : siemens
dv/dt = (g_L*(E_L - v) + currents)/Cm : volt

@mstimberg
Copy link
Member Author

PS: I'm waiting with the documentation until we are sure about the syntax, don't want to write things twice :)

@mstimberg
Copy link
Member Author

[For the record, the test failure is just a build timeout which can be safely ignored.]

@mstimberg
Copy link
Member Author

@thesamovar : what do you think about the changes and my take on the names question?

@thesamovar
Copy link
Member

I think we're still not quite there, but getting closer. Taking a look at this:

basic_eq = Equations('dv/dt = (g_L*(E_L - v) + currents)/Cm : volt')
synaptic_current = Equations('''I = g*(E - v) : amp
                                dg/dt = -g/tau : siemens''')
full_eq = (basic_eq +
           Equations('currents = I_ge + I_gi : amp')  + # any way to simplify this as well?
           synaptic_current.postfix('_ge', external=True, exclude=['v']) +
           synaptic_current.postfix('_gi', external=True, exclude=['v']))
print(full_eq)

It seems more complicated than it needs to be. In particular, I find external=True, exclude=['v'] complicated, especially when repeated twice. Perhaps it makes sense to add the set of variables that can be prefixed to the Equations definition since it will typically always be the same (it's rare you'd want to prefix some variables in one case and other variables in another case, although not impossible). So then we might have:

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = Equations('''
	I = g*(E - v) : amp
    dg/dt = -g/tau : siemens
	''', replace=['I', 'g', 'E']) # by default, would just be ['I', 'g']
full_eq = (basic_eq +
           synaptic_current.postfix('_ge') +
           synaptic_current.postfix('_gi'))
print(full_eq)

That syntax is also not quite perfect, but perhaps that gets closer to the way a user might want to think about it? Like that the definition of synaptic_current is the definition of a template. Another syntax could be:

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = EquationsTemplate('''
	I = g*(E - v) : amp
    dg/dt = -g/tau : siemens
	''', template_variables=['I', 'g', 'E'])
full_eq = (basic_eq +
           synaptic_current.postfix('_ge') +
           synaptic_current.postfix('_gi'))
print(full_eq)

Or, make it more general again (and another idea for syntax):

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = EquationsTemplate('''
	I = g*(E - v) : amp
    dg/dt = -g/tau : siemens
	''', to_replace=['I', 'g', 'E'])
full_eq = (basic_eq +
           synaptic_current.replace('*_ge') +
           synaptic_current.replace('*_gi'))
print(full_eq)

That would allow prefix/postfix to be replace by *_ge or ge_* respectively, or any other scheme the user wanted. (We could also have prefix/postfix just be implemented with this.) Final option (compatible with the above):

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = Equations('''
	I = g*(E - v) : amp
    dg/dt = -g/tau : siemens
	''').template('I', 'g', 'E')
full_eq = (basic_eq +
           synaptic_current.replace('*_ge') +
           synaptic_current.replace('*_gi'))
print(full_eq)

@mstimberg
Copy link
Member Author

Ok, this is some good thinking. I like the template approach, it could simplify the syntax a bit.

I'm a bit undecided between the approach using an EquationTemplates class and having an Equations.template method. The advantage of the second approach is that we do not have to add another name to the already large brian2 namespace, but apart from that the only advantage is that it allows you to use an Equations object both directly and as a template. I'm not sure that this is an important use case or even something that we should encourage. So, I think I'm slightly in favour of introducing the EquationsTemplate class (actually, this would also lend itself to be easily extended later, e.g. for introducing something helpful for summing up currents as I mentioned earlier).

I have to say I'm not very fond of the general * syntax. I think in 90% of the cases users would just use prefixes or postfixes, and in the remaining cases, even a little expression with a placeholder will not be enough (say you want to replace a variable ending in _up by a name ending in _down). I think my preferred solution would be to have prefix/postfix for convenience, plus a general replace or substitute keyword that takes a dictionary. This way, you could always do templ.replace({var: 'pre_%s_post' % var for var in templ.template_variables}) if you wanted to do something fancy.

Oh, and a final possibility if we go for EquationsTemplate: using a proper template syntax, e.g.:

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = EquationsTemplate('''
	{I} = {g}*({E} - v) : amp
        dg/dt = -g/tau : siemens
	''')
full_eq = (basic_eq +
           synaptic_current.postfix('_ge') +
           synaptic_current.postfix('_gi'))
print(full_eq)

Oh, actually with this syntax we could even get rid of the EquationsTemplate class if we wanted to – although not sure that does not become confusing:

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = '''
	{I} = {g}*({E} - v) : amp
        dg/dt = -g/tau : siemens
	'''
full_eq = (basic_eq +
           Equations(synaptic_current, postfix='_ge') +
           Equations(synaptic_current, postfix='_gi'))
print(full_eq)

Um, we keep adding options, we should probably try to converge on something...

@thesamovar
Copy link
Member

I think we're almost there for a solution.

How about we have an EquationsTemplate class but normally the user doesn't write one directly but instead writes Equations.template (or Equations.make_template) which returns an EquationsTemplate object?

I have to say I'm not very fond of the general * syntax

Good points - I definitely agree it shouldn't be our only general syntax. I wonder if it might occasionally be useful and less complicated that your dictionary based method though? On the one hand I think about just having multiple replace methods (maybe including a regexp based one), on the other hand perhaps there should just be one clear way of doing it, Zen of Python style?

I don't think {this syntax} should ever be the only way of doing it because it'll be too ugly and not transparent for people who are not expert Python users I think.

I think at this point, the best option to me looks like the Equations.make_template(list, of, vars, ...)->EquationsTemplate and EquationsTemplate.prefix/postfix methods as the primary way of using it. I would suggest that you have to provide an explicit list of all the variables to be replaced, on the grounds that it's better to be explicit and we can't seem to find a good combination of semantics for internal/external/.... The example would then be:

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = Equations('''
	I = g*(E - v) : amp
    dg/dt = -g/tau : siemens
	''').make_template('I', 'g', 'E')
full_eq = (basic_eq +
           synaptic_current.postfix('_ge') +
           synaptic_current.postfix('_gi'))
print(full_eq)

If you're OK with this, we still need to decide:

  1. What should the EquationsTemplate.replace method be? I'm happy for it to be just a dictionary, and open to the possibility of allowing a * or regexp option too (I'm not decided about this).
  2. Do we want an additional syntax for summing currents since it's a very common use case? I think I'm slightly in favour of saying no because already the example above is very clear and has the nice advantage of being fully explicit.

@mstimberg
Copy link
Member Author

I wonder if it might occasionally be useful and less complicated that your dictionary based method though? On the one hand I think about just having multiple replace methods (maybe including a regexp based one), on the other hand perhaps there should just be one clear way of doing it, Zen of Python style?

Maybe we should follow our usual approach and think in terms of use cases. What do you have in mind where the * based method could be useful? Given that all this is "pre-simulation", I don't think we should overengineer this feature, with all the string manipulation that Python offers itself. The main reason for all the syntax here is that it is easier to read than the standard Python equivalent (users have been doing similar things "by hand" previously), I feel that wouldn't be the case for a general syntax with stuff like * (let's not even get into regular expressions ;) ).

I think at this point, the best option to me looks like the Equations.make_template(list, of, vars, ...)->EquationsTemplate and EquationsTemplate.prefix/postfix methods as the primary way of using it.

Sounds good to me.

I would suggest that you have to provide an explicit list of all the variables to be replaced, on the grounds that it's better to be explicit and we can't seem to find a good combination of semantics for internal/external/....

I think we could still have one automatic variant if no variables are specified at all (which wouldn't make any sense for a template). I think there are two possible choices: either all the variables defined in the equations (i.e. what we previously called "internal"), or everything (i.e. "internal" + "external"). I agree that the half-automatic external/internal stuff is not very clear and explicit.

  1. What should the EquationsTemplate.replace method be?

I'd say either a dictionary or keywords based (see my arguments above), keyword-based is maybe a bit nicer/more consistent with our usual style? What I mean is templ.replace(I='J') instead of templ.replace({'I': 'J'}).

  1. Do we want an additional syntax for summing currents since it's a very common use case? I think I'm slightly in favour of saying no because already the example above is very clear and has the nice advantage of being fully explicit.

I'd say we can leave this decision for a later time and focus on what we've discussed so far for now. I still see a potential usefulness in situations like this:

if has_sodium_channels:
   eqs += current.postfix('_Na')
   # add I_Na to list of currents

But then, this is something that is already pretty straightforward with standard Python, e.g. you could do:

currents = 'I_pas'
if has_sodium_channels:
   eqs += current.postfix('_Na')
   currents += '+ I_Na'
# ...
eqs += Equations('currents = {} : amp'.format(currents))

or something like:

currents = ['I_pas']
if has_sodium_channels:
   eqs += current.postfix('_Na')
   currents.append('I_Na')
eqs += Equations('currents = {} : amp'.format('+ '.join(currents))

@thesamovar
Copy link
Member

Maybe we should follow our usual approach and think in terms of use cases. What do you have in mind where the * based method could be useful?

Very good - you're right let's just leave that out entirely. I'm happy with either dictionary or keywords for replace. I quite like keywords actually, and you can always do replace(**d) if you have a dictionary d instead.

Incidentally, replace could equally well be a method of Equations, right? If you explicitly specify all the names you want to replace, the template information isn't even used.

It would also be nice to allow replacing a variable with a value, e.g. you might have x**n and do .replace(n=3)? Obviously that only makes sense for external names, but that specific usage pops up quite often right?

So we do prefix(str)/postfix(str)/replace(**kwds)? You could also partly combine these, e.g. eqs.prefix('_Na').replace(n=3).

I think we could still have one automatic variant if no variables are specified at all (which wouldn't make any sense for a template). I think there are two possible choices: either all the variables defined in the equations (i.e. what we previously called "internal"), or everything (i.e. "internal" + "external"). I agree that the half-automatic external/internal stuff is not very clear and explicit.

I'm tempted to agree, but I wonder if we think of actual use cases that the default will never be useful. If you do only internal variables, then all constants and externally defined variables (like V) would be the same, so it would actually be the same. To make it worth doing, the constants have to be different. However, if you do all variables, then it's going to include V and functions as well, which is definitely not what would be expected and likely to lead to conclusion. So I think that actually fully explicit is the only coherent choice here.

But then, this is something that is already pretty straightforward with standard Python

Yep - I think it's sufficiently simple that we don't need to do anything ourselves here.

OK, so I think the only issue remaining is whether or not to have default variables, and then we have a fully worked out proposal. Whatever we decide for that, I think we've iterated on a nice solution here. I'm pretty happy with it and I think it'll make life easier for a few people.

@mstimberg
Copy link
Member Author

Incidentally, replace could equally well be a method of Equations, right? If you explicitly specify all the names you want to replace, the template information isn't even used.

It would also be nice to allow replacing a variable with a value, e.g. you might have x**n and do .replace(n=3)? Obviously that only makes sense for external names, but that specific usage pops up quite often right?

We do actually have this already, which actually makes me realize that we only need the template approach for prefix/postfix and can forget about the general solution. From the docs:

>>> general_equation = 'dg/dt = -g / tau : siemens'
>>> eqs_exc = Equations(general_equation, g='g_e', tau='tau_e')
>>> eqs_inh = Equations(general_equation, g='g_i', tau='tau_i')
>>> print(eqs_exc)
dg_e/dt = -g_e / tau_e  : S
>>> print(eqs_inh)
dg_i/dt = -g_i / tau_i  : S

>>> eqs = Equations('dv/dt = mu/tau + sigma/tau**.5*xi : volt',
...                  mu=-65*mV, sigma=3*mV, tau=10*ms)
>>> print(eqs)
dv/dt = (-65. * mvolt)/(10. * msecond) + (3. * mvolt)/(10. * msecond)**.5*xi  : V

There's also a substitute function (replace might have been a better name...), so instead of

Equations('dg/dt = -g / tau : siemens', g='g_e', tau=10*ms)

you can also write:

Equations('dg/dt = -g / tau : siemens').substitute(g='g_e', tau=10*ms)

So the whole template thing would only be used for prefixing/postfixing. Which actually makes me wonder whether:

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = Equations('''
	I = g*(E - v) : amp
        dg/dt = -g/tau : siemens
	''').make_template('I', 'g', 'E')
full_eq = (basic_eq +
           synaptic_current.postfix('_ge') +
           synaptic_current.postfix('_gi'))

is actually that much better than my initial implementation (discarding the internal/external stuff):

basic_eq = Equations('''
   dv/dt = (g_L*(E_L - v) + currents)/Cm : volt
   currents = I_ge + I_gi : amp
   '''
   )
synaptic_current = Equations('''
	I = g*(E - v) : amp
        dg/dt = -g/tau : siemens
	''')
full_eq = (basic_eq +
           synaptic_current.postfix('_ge', variables=['I', 'g', 'E']) +
           synaptic_current.postfix('_gi', variables=['I', 'g', 'E']))

This is more redundant due to its duplicated variables definition, on the other hand it avoids the whole make_template call and the introduction of an EquationsTemplate class (+ stuff like specific error messages when you provide an EquationsTemplate object instead of Equations to NeuronGroup, etc.). I also just realized that staying with Equations also enables some nested replacements that could potentially be useful. For example (a bit contrived, but I think you get the idea):

e_synapses = synaptic_current.postfix('_e', variables=['tau'])
i_synapses = synaptic_current.postfix('_i', variables=['tau'])
full_eq = (basic_eq +
           e_synapses.postfix('_ampa', variables['I', 'g', 'E']) + 
           e_synapses.postfix('_nmda', variables['I', 'g', 'E']) +
           i_synapses.postfix('_gaba', variables=['I', 'g', 'E']))

With the EquationsTemplate approach, we'd have to go EquationsEquationsTemplateEquationsEquationsTemplateEquations...

Oooh, that makes me think of another way to get rid of the redundancy without introducing a new class: what if you could give a list of prefixes/postfixes and get a list of Equations back:

e_synapse, i_synapse = synaptic_current.postfix(['_e', '_i'], variables=['I', 'g', 'E'])
full_eq = (basic_eq + e_synapse + i_synapse)

@thesamovar
Copy link
Member

Good to know that (again) past us is doing well. ;)

I still don't like the repeat of the list of variables. Ideally, if we can find something without that would be good.

One last argument in favour of EquationsTemplate: you could make it so that a template cannot be added to standard Equations and therefore reduce the chance of an error. Maybe that's not really worth worrying about though.

OK assuming that we don't have EquationsTemplate but we also want to avoid repetition, it looks like we have two options. Either we go for an argument/method of Equations that marks certain variables to be replaced / as part of the template; OR we go with your list approach (which I agree is pretty neat). An argument against the list would be that it's less clear what it's doing without reading the documentation (not the worst thing in the world, but to avoid if possible). Another is that it promotes long lines of code, which is generally bad practice.

I think my slight preference at this point is some way of marking certain variables to be replaced in Equations, maybe with a keyword template=[...]? That said, I think having a variables keyword to prefix/postfix that lets you override this is a good idea.

@mstimberg
Copy link
Member Author

I think I mostly agree with you, but I'm now again have some doubt about the real-word usefulness of what we discussed so far (sorry for going in circles here...). I had a look at the Rothman-Manis example, and tried to see what it would look like with a prefix/postfix syntax. The first two blocks of channel equations are:

# Classical Na channel
eqs_na = """
ina = gnabar*m**3*h*(ENa-v) : amp
dm/dt=q10*(minf-m)/mtau : 1
dh/dt=q10*(hinf-h)/htau : 1
minf = 1./(1+exp(-(vu + 38.) / 7.)) : 1
hinf = 1./(1+exp((vu + 65.) / 6.)) : 1
mtau = ((10. / (5*exp((vu+60.) / 18.) + 36.*exp(-(vu+60.) / 25.))) + 0.04)*ms : second
htau = ((100. / (7*exp((vu+60.) / 11.) + 10.*exp(-(vu+60.) / 25.))) + 0.6)*ms : second
"""
# KHT channel (delayed-rectifier K+)
eqs_kht = """
ikht = gkhtbar*(nf*n**2 + (1-nf)*p)*(EK-v) : amp
dn/dt=q10*(ninf-n)/ntau : 1
dp/dt=q10*(pinf-p)/ptau : 1
ninf = (1 + exp(-(vu + 15) / 5.))**-0.5 : 1
pinf = 1. / (1 + exp(-(vu + 23) / 6.)) : 1
ntau = ((100. / (11*exp((vu+60) / 24.) + 21*exp(-(vu+60) / 23.))) + 0.7)*ms : second
ptau = ((100. / (4*exp((vu+60) / 32.) + 5*exp(-(vu+60) / 22.))) + 5)*ms : second
"""

I don't actually see anything here where we could use prefixes/postfixes! It would be helpful if instead of the values we had constants, but in this kind of model I think it makes sense to directly put the fixed parameters into the equations instead of having tons of constants. We can use the already existing substitution mechanism, but this does not make things much shorter or clearer, I think:

eqs_gating = Equations('''dgating/dt = q10*(steady - gating)/tau : 1
                          steady = (1+exp((vu + v_s) / v_d))**exponent : 1
                          tau = ((a / (b1*exp((vu + v_s1) / v_d1) + b2*exp(-(vu+v_s1) / v_d2))) + c)*ms : second''')

# Classical Na channel
eqs_na = (Equations('ina = gnabar*m**3*h*(ENa-v) : amp') +
          eqs_gating.substitute(gating='m', steady='m_inf', tau='tau_m',
                                v_s=38, v_d=-7, exponent=-1,
                                a=10, b1=5, v_s1=60, v_d1=18, b2=36, v_d2=25, c=0.04) +
          eqs_gating.substitute(gating='h', steady='h_inf', tau='tau_h',
                                v_s=65, v_d=-6, exponent=-1,
                                a=100, b1=7, v_s1=60, v_d1=11, b2=10, v_d2=25, c=0.6))

# KHT channel (delayed-rectifier K+)
eqs_kht = (Equations('ikhtbar = gkhtbar*(nf*n**2 + (1-nf)*p)*(EK-v) : amp') +
           eqs_gating.substitute(gating='n', steady='n_inf', tau='tau_n',
                                 v_s=15, v_d=-5, exponent=-0.5,
                                 a=100, b1=11, v_s1=60, v_d1=24, b2=21, v_d2=23, c=0.7) +
           eqs_gating.substitute(gating='p', steady='p_inf', tau='tau_p',
                                 v_s=23, v_d=-6, exponent=-1,
                                 a=100, b1=4, v_s1=60, v_d1=32, b2=5, v_d2=22, c=5))

Now there is one place where something like prefix/postfix would make sense, the three names h, h_inf, tau_h. But then, how to add h as a prefix/postfix here? We cannot call a variable just inf, and for the gating variable h itself...? With a special template syntax, we could allow replacing parts of a name, e.g.:

eqs_gating = Equations('''d{gating}/dt = q10*({gating}_inf - {gating})/tau_{gating} : 1
                          {gating}_inf = (1+exp((vu + v_s) / v_d))**exponent : 1
                          tau_{gating} = ((a / (b1*exp((vu + v_s1) / v_d1) + b2*exp(-(vu+v_s1) / v_d2))) + c)*ms : second''')

# Classical Na channel
eqs_na = (Equations('ina = gnabar*m**3*h*(ENa-v) : amp') +
          eqs_gating.substitute(gating='m',
                                v_s=38, v_d=-7, exponent=-1,
                                a=10, b1=5, v_s1=60, v_d1=18, b2=36, v_d2=25, c=0.04) +
          eqs_gating.substitute(gating='h',
                                v_s=65, v_d=-6, exponent=-1,
                                a=100, b1=7, v_s1=60, v_d1=11, b2=10, v_d2=25, c=0.6))

But then, is it really clearer than simply using Python's format method on the raw string? We might also consider simply making this more "canonical" by using it in examples like Rothman-Manis.

@thesamovar
Copy link
Member

Hmm, maybe you're right - and it does allow more flexibility. Your example above might be simpler rewritten with str.replace actually:

eqs_gating ='''dgating/dt = q10*(gating_inf - gating)/tau_gating : 1
               gating_inf = (1+exp((vu + v_s) / v_d))**exponent : 1
               tau_gating = ((a / (b1*exp((vu + v_s1) / v_d1) + b2*exp(-(vu+v_s1) / v_d2))) + c)*ms : second'''

# Classical Na channel
eqs_na = (Equations('ina = gnabar*m**3*h*(ENa-v) : amp') +
          Equations(eqs_gating.replace('gating', 'm')).substitute(
                                v_s=38, v_d=-7, exponent=-1,
                                a=10, b1=5, v_s1=60, v_d1=18, b2=36, v_d2=25, c=0.04) +
          Equations(eqs_gating.replace('gating', 'h')).substitute(
                                v_s=65, v_d=-6, exponent=-1,
                                a=100, b1=7, v_s1=60, v_d1=11, b2=10, v_d2=25, c=0.6))

or we could imagine having some sort of wrapper around string functions to mildly simplify the syntax:

eqs_gating = Equations('''dgating/dt = q10*(gating_inf - gating)/tau_gating : 1
                          gating_inf = (1+exp((vu + v_s) / v_d))**exponent : 1
                          tau_gating = ((a / (b1*exp((vu + v_s1) / v_d1) + b2*exp(-(vu+v_s1) / v_d2))) + c)*ms : second''')

# Classical Na channel
eqs_na = (Equations('ina = gnabar*m**3*h*(ENa-v) : amp') +
          eqs_gating.string_replace('gating', 'm').substitute(
                                v_s=38, v_d=-7, exponent=-1,
                                a=10, b1=5, v_s1=60, v_d1=18, b2=36, v_d2=25, c=0.04) +
          eqs_gating.string_replace('gating', 'h').substitute(
                                v_s=65, v_d=-6, exponent=-1,
                                a=100, b1=7, v_s1=60, v_d1=11, b2=10, v_d2=25, c=0.6))

@thesamovar
Copy link
Member

Actually, looking again at your code with the {braces} and I think I prefer it to my .replace versions. It makes it clear what the template variable is and wouldn't be expected to work without a substitution.

Maybe we need more opinions here?

@thesamovar
Copy link
Member

I mean, from other people.

@mstimberg
Copy link
Member Author

So, that would mean that we actually abandon the whole prefix/postfix thing, right? I think of all the solutions so far, I'd prefer the variant with curly braces. I think having a tiny bit of syntactic sugar (i.e. you do not have to call format on the string, but can use the Equations keyword argument its substitute method) is still nice, even though it does not add that much.

I'll go ahead and implement this solution and convert Rothman&Manis, I think it is then easier to get feedback from others. This example would be another candidate for the approach.

@mstimberg
Copy link
Member Author

I don't think we will build upon the code in this PR any further, let's continue the discussion in #1244.

@mstimberg mstimberg closed this Oct 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Prefix/postfix method for Equations
2 participants