Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

LaTeX representation for Brian objects / ipython notebook integration #28

Merged
merged 18 commits into from

2 participants

@mstimberg
Owner

Add a LaTeX representation to core brian objects (in particular units and equations) that works nicely with the ipython notebook.

  • LaTeX for equations
  • LaTeX for units (e.g. $\omega$ for ohm)
  • LaTeX for scales (e.g. $\mu$ for u)
  • LaTeX for explicit state updater descriptions
  • LaTeX for NeuronGroup/Synapses
  • anything else?
@mstimberg mstimberg was assigned
@mstimberg
Owner

I think this is ready to merge for now, it's certainly not perfect but it has support for units, equations and NeuronGroup in the ipython notebook (it also adds simple repr methods to most objects). Since these changes are also useful for testing, I'd say it's better to merge it now and not keep it in a parallel branch for a long time.

@thesamovar
Owner

It seems now to be failing on Python 3.2 and 3.3 (whereas before it was just 3.3 I think). Is it the same magic issue? If so, I'll go ahead and merge and we can deal with it later.

@mstimberg
Owner

It seems now to be failing on Python 3.2 and 3.3 (whereas before it was just 3.3 I think). Is it the same magic issue?

Ah, sorry. No it's not the same issue, but it's the same issue I had in the sphinx_improvements branch where I therefore removed the 3.2 testing: The problem is that sphinx 1.2 no longer supports Python 3.2 whereas the previous version not yet supports Python 3.3 -- for testing we would therefore have to change the sphinx version depending on the Python version or just stop testing for 3.2 (for now at least).

@thesamovar
Owner

OK, but it's unrelated to the code so I'll go ahead and merge.

@thesamovar thesamovar merged commit 8478045 into master

1 check failed

Details default The Travis CI build failed
@thesamovar thesamovar deleted the notebook_integration branch
@mstimberg
Owner

Ok. I disabled Python 3.2 testing in master for now -- I don't want to regularly adapt version information for special cases like this, so let's only track the most recent sphinx/Python3 version for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 29, 2013
  1. @mstimberg
  2. @mstimberg
  3. @mstimberg
  4. @mstimberg
Commits on Apr 2, 2013
  1. @mstimberg
  2. @mstimberg
Commits on Apr 3, 2013
  1. @mstimberg
  2. @mstimberg
  3. @mstimberg

    Treat static equations and parameters correctly for latex output, also

    mstimberg authored
    rename SingleEquation.eq_type to SingleEquation.type
Commits on Apr 23, 2013
  1. @mstimberg

    Merge branch 'master' into notebook_integration

    mstimberg authored
    Conflicts:
    	brian2/groups/neurongroup.py
Commits on May 6, 2013
  1. @mstimberg

    Merge remote-tracking branch 'origin/master' into notebook_integration

    mstimberg authored
    Conflicts:
    	brian2/equations/equations.py
    	brian2/groups/neurongroup.py
Commits on May 16, 2013
  1. @mstimberg
  2. @mstimberg
Commits on May 21, 2013
  1. @mstimberg
  2. @mstimberg
Commits on May 28, 2013
  1. @mstimberg
  2. @mstimberg
  3. @mstimberg

    Merge remote-tracking branch 'origin/master' into notebook_integration

    mstimberg authored
    Conflicts:
    	brian2/stateupdaters/explicit.py
This page is out of date. Refresh to see the latest.
View
8 brian2/core/base.py
@@ -150,6 +150,14 @@ def _set_active(self, val):
it for all `contained_objects`.
''')
+ def __repr__(self):
+ description = ('{classname}(when={scheduler}, name={name})')
+ return description.format(classname=self.__class__.__name__,
+ scheduler=repr(Scheduler(when=self.when,
+ clock=self.clock,
+ order=self.order)),
+ name=repr(self.name))
+
# This is a repeat from Nameable.name, but we want to get the documentation
# here again
name = Nameable.name
View
6 brian2/core/clocks.py
@@ -43,7 +43,7 @@ class Clock(Nameable):
basename = 'clock'
name = Nameable.name
- @check_units(dt=second, t=second)
+ @check_units(dt=second)
def __init__(self, dt=None, name=None):
self._dt_spec = dt
self.i = 0 #: The time step of the simulation as an integer.
@@ -58,10 +58,10 @@ def reinit(self):
self.i = 0
def __str__(self):
- return 'Clock: t = ' + str(self.t) + ', dt = ' + str(self.dt)
+ return 'Clock ' + self.name + ': t = ' + str(self.t) + ', dt = ' + str(self.dt)
def __repr__(self):
- return 'Clock(dt=%s)' % (repr(self.dt),)
+ return 'Clock(dt=%r, name=%r)' % (self.dt, self.name)
def tick(self):
'''
View
4 brian2/core/namespace.py
@@ -195,6 +195,10 @@ def __contains__(self, key):
return True
return False
+
+ def __repr__(self):
+ return '<%s containing namespaces: %s>' % (self.__class__.__name__,
+ ', '.join(self.namespaces.iterkeys()))
def get_default_numpy_namespace(N):
'''
View
5 brian2/core/network.py
@@ -331,3 +331,8 @@ def stop(self):
Stops the network from running, this is reset the next time `Network.run` is called.
'''
self._stopped = True
+
+ def __repr__(self):
+ return '<%s at time t=%s, containing objects: %s>' % (self.__class__.__name__,
+ str(self.t),
+ ', '.join((obj.__repr__() for obj in self.objects)))
View
7 brian2/core/scheduler.py
@@ -105,3 +105,10 @@ def __init__(self, *args, **kwds):
#: Whether or not the user explicitly specified an order
self.defined_order = defined_order
+
+ def __repr__(self):
+ description = '{classname}(clock={clock}, when={when}, order={order})'
+ return description.format(classname=self.__class__.__name__,
+ clock=repr(self.clock),
+ when=repr(self.when),
+ order=repr(self.order))
View
67 brian2/core/specifiers.py
@@ -42,6 +42,9 @@ def __init__(self, name):
#: The name of the thing being specified (e.g. the model variable)
self.name = name
+ def __repr__(self):
+ return '%s(name=%r)' % (self.__class__.__name__, self.name)
+
class VariableSpecifier(Specifier):
'''
@@ -76,6 +79,16 @@ def __init__(self, name, unit, scalar=True, constant=False):
#: Whether the value is constant during a run
self.constant = constant
+
+ def __repr__(self):
+ description = ('{classname}(name={name}, unit={unit}, scalar={scalar}, '
+ 'constant={constant})')
+ return description.format(classname=self.__class__.__name__,
+ name=repr(self.name),
+ unit=repr(self.unit),
+ scalar=repr(self.scalar),
+ constant=repr(self.constant))
+
class Value(VariableSpecifier):
'''
An object providing information about model variables that have an
@@ -123,6 +136,16 @@ def set_value(self):
'''
raise NotImplementedError()
+ def __repr__(self):
+ description = ('{classname}(name={name}, unit={unit}, dtype={dtype}, '
+ 'scalar={scalar}, constant={constant})')
+ return description.format(classname=self.__class__.__name__,
+ name=repr(self.name),
+ unit=repr(self.unit),
+ dtype=repr(self.dtype),
+ scalar=repr(self.scalar),
+ constant=repr(self.constant))
+
###############################################################################
# Concrete classes that are used as specifiers in practice.
###############################################################################
@@ -163,6 +186,15 @@ def get_value(self):
def set_value(self):
raise TypeError('The value "%s" is read-only' % self.name)
+ def __repr__(self):
+ description = ('{classname}(name={name}, unit={unit}, dtype={dtype}, '
+ 'value={value}')
+ return description.format(classname=self.__class__.__name__,
+ name=repr(self.name),
+ unit=repr(self.unit),
+ dtype=repr(self.dtype),
+ value=repr(self.value))
+
class StochasticVariable(VariableSpecifier):
'''
@@ -228,6 +260,17 @@ def __init__(self, name, unit, dtype, obj, attribute, constant=False):
def get_value(self):
return getattr(self.obj, self.attribute)
+ def __repr__(self):
+ description = ('{classname}(name={name}, unit={unit}, dtype={dtype}, '
+ 'obj={obj}, attribute={attribute}, constant={constant})')
+ return description.format(classname=self.__class__.__name__,
+ name=repr(self.name),
+ unit=repr(self.unit),
+ dtype=repr(self.dtype),
+ obj=repr(self.obj),
+ attribute=repr(self.attribute),
+ constant=repr(self.constant))
+
class ArrayVariable(Value):
'''
@@ -276,6 +319,16 @@ def get_value(self):
def set_value(self, value):
self.array[:] = value
+ def __repr__(self):
+ description = ('<{classname}(name={name}, unit={unit}, dtype={dtype}, '
+ 'array=<...>, index={index}, constant={constant})>')
+ return description.format(classname=self.__class__.__name__,
+ name=repr(self.name),
+ unit=repr(self.unit),
+ dtype=repr(self.dtype),
+ index=repr(self.index),
+ constant=self.constant)
+
class Subexpression(Value):
'''
@@ -333,6 +386,15 @@ def get_value(self):
def __contains__(self, var):
return var in self.identifiers
+ def __repr__(self):
+ description = ('<{classname}(name={name}, unit={unit}, dtype={dtype}, '
+ 'expr={expr}, specifiers=<...>, namespace=<....>)>')
+ return description.format(classname=self.__class__.__name__,
+ name=repr(self.name),
+ unit=repr(self.unit),
+ dtype=repr(self.dtype),
+ expr=repr(self.expr))
+
class Index(Specifier):
'''
@@ -358,3 +420,8 @@ def __init__(self, name, iterate_all=True):
'is type %s instead' % type(all)))
#: Whether the index varies over the whole of an input vector
self.iterate_all = iterate_all
+
+ def __repr__(self):
+ return '%s(name=%r, iterate_all=%r)' % (self.__class__.__name__,
+ self.name,
+ self.iterate_all)
View
105 brian2/equations/equations.py
@@ -5,13 +5,13 @@
import keyword
import re
import string
-import sys
import sympy
from pyparsing import (Group, ZeroOrMore, OneOrMore, Optional, Word, CharsNotIn,
Combine, Suppress, restOfLine, LineEnd, ParseException)
+import sympy
-from brian2.codegen.parsing import sympy_to_str
+from brian2.codegen.parsing import sympy_to_str, parse_to_sympy
from brian2.units.fundamentalunits import DimensionMismatchError
from brian2.units.allunits import second
from brian2.utils.logger import get_logger
@@ -219,7 +219,7 @@ class SingleEquation(object):
Parameters
----------
- eq_type : {PARAMETER, DIFFERENTIAL_EQUATION, STATIC_EQUATION}
+ type : {PARAMETER, DIFFERENTIAL_EQUATION, STATIC_EQUATION}
The type of the equation.
varname : str
The variable that is defined by this equation.
@@ -233,8 +233,8 @@ class SingleEquation(object):
context.
'''
- def __init__(self, eq_type, varname, unit, expr=None, flags=None):
- self.eq_type = eq_type
+ def __init__(self, type, varname, unit, expr=None, flags=None):
+ self.type = type
self.varname = varname
self.unit = unit
self.expr = expr
@@ -250,10 +250,16 @@ def __init__(self, eq_type, varname, unit, expr=None, flags=None):
identifiers = property(lambda self: self.expr.identifiers
if not self.expr is None else set([]),
doc='All identifiers in the RHS of this equation.')
-
+
+ def _latex(self, *args):
+ varname = sympy.Symbol(self.varname)
+ t = sympy.Symbol('t')
+ sympy_expr = sympy.Eq(sympy.Derivative(varname, t),
+ parse_to_sympy(self.expr))
+ return sympy.latex(sympy_expr)
def __str__(self):
- if self.eq_type == DIFFERENTIAL_EQUATION:
+ if self.type == DIFFERENTIAL_EQUATION:
s = 'd' + self.varname + '/dt'
else:
s = self.varname
@@ -269,7 +275,7 @@ def __str__(self):
return s
def __repr__(self):
- s = '<' + self.eq_type + ' ' + self.varname
+ s = '<' + self.type + ' ' + self.varname
if not self.expr is None:
s += ': ' + self.expr.code
@@ -290,7 +296,7 @@ def _repr_pretty_(self, p, cycle):
# should never happen
raise AssertionError('Cyclical call of SingleEquation._repr_pretty')
- if self.eq_type == DIFFERENTIAL_EQUATION:
+ if self.type == DIFFERENTIAL_EQUATION:
p.text('d' + self.varname + '/dt')
else:
p.text(self.varname)
@@ -305,6 +311,9 @@ def _repr_pretty_(self, p, cycle):
if len(self.flags):
p.text(' (' + ', '.join(self.flags) + ')')
+ def _repr_latex_(self):
+ return '$' + sympy.latex(self) + '$'
+
class Equations(collections.Mapping):
"""
Container that stores equations from which models can be created.
@@ -397,7 +406,7 @@ def __init__(self, eqns, **kwds):
uses_xi = None
for eq in self._equations.itervalues():
if not eq.expr is None and 'xi' in eq.expr.identifiers:
- if not eq.eq_type == DIFFERENTIAL_EQUATION:
+ if not eq.type == DIFFERENTIAL_EQUATION:
raise EquationError(('The equation defining %s contains the '
'symbol "xi" but is not a differential '
'equation.') % eq.varname)
@@ -513,13 +522,13 @@ def _get_substituted_expressions(self):
new_str_expr = sympy_to_str(new_sympy_expr)
expr = Expression(new_str_expr)
- if eq.eq_type == STATIC_EQUATION:
+ if eq.type == STATIC_EQUATION:
substitutions.update({sympy.Symbol(eq.varname, real=True): expr.sympy_expr})
- elif eq.eq_type == DIFFERENTIAL_EQUATION:
+ elif eq.type == DIFFERENTIAL_EQUATION:
# a differential equation that we have to check
subst_exprs.append((eq.varname, expr))
else:
- raise AssertionError('Unknown equation type %s' % eq.eq_type)
+ raise AssertionError('Unknown equation type %s' % eq.type)
return subst_exprs
@@ -571,13 +580,13 @@ def _get_stochastic_type(self):
diff_eq_expressions = property(lambda self: [(varname, eq.expr) for
varname, eq in self.iteritems()
- if eq.eq_type == DIFFERENTIAL_EQUATION],
+ if eq.type == DIFFERENTIAL_EQUATION],
doc='A list of (variable name, expression) '
'tuples of all differential equations.')
eq_expressions = property(lambda self: [(varname, eq.expr) for
varname, eq in self.iteritems()
- if eq.eq_type in (STATIC_EQUATION,
+ if eq.type in (STATIC_EQUATION,
DIFFERENTIAL_EQUATION)],
doc='A list of (variable name, expression) '
'tuples of all equations.')
@@ -590,19 +599,19 @@ def _get_stochastic_type(self):
doc='All variable names defined in the equations.')
diff_eq_names = property(lambda self: set([eq.varname for eq in self.ordered
- if eq.eq_type == DIFFERENTIAL_EQUATION]),
+ if eq.type == DIFFERENTIAL_EQUATION]),
doc='All differential equation names.')
static_eq_names = property(lambda self: set([eq.varname for eq in self.ordered
- if eq.eq_type == STATIC_EQUATION]),
+ if eq.type == STATIC_EQUATION]),
doc='All static equation names.')
eq_names = property(lambda self: set([eq.varname for eq in self.ordered
- if eq.eq_type in (DIFFERENTIAL_EQUATION, STATIC_EQUATION)]),
+ if eq.type in (DIFFERENTIAL_EQUATION, STATIC_EQUATION)]),
doc='All (static and differential) equation names.')
parameter_names = property(lambda self: set([eq.varname for eq in self.ordered
- if eq.eq_type == PARAMETER]),
+ if eq.type == PARAMETER]),
doc='All parameter names.')
units = property(lambda self:dict([(var, eq.unit) for var, eq in
@@ -636,10 +645,10 @@ def _sort_static_equations(self):
# Get a dictionary of all the dependencies on other static equations,
# i.e. ignore dependencies on parameters and differential equations
static_deps = {}
- for eq in self._equations.itervalues():
+ for eq in self._equations.itervalues():
if eq.type == STATIC_EQUATION:
static_deps[eq.varname] = [dep for dep in eq.identifiers if
- dep in self._equations and
+ dep in self._equations and
self._equations[dep].type == STATIC_EQUATION]
# Use the standard algorithm for topological sorting:
@@ -671,9 +680,9 @@ def _sort_static_equations(self):
self._equations[static_variable].update_order = order
# Sort differential equations and parameters after static equations
- for eq in self._equations.itervalues():
+ for eq in self._equations.itervalues():
if eq.type == DIFFERENTIAL_EQUATION:
- eq.update_order = len(sorted_eqs)
+ eq.update_order = len(sorted_eqs)
elif eq.type == PARAMETER:
eq.update_order = len(sorted_eqs) + 1
@@ -708,18 +717,18 @@ def check_units(self, namespace, specifiers, additional_namespace=None):
additional_namespace,
strip_units=False)
- for var, eq in self._equations.iteritems():
+ for var, eq in self._equations.iteritems():
if eq.type == PARAMETER:
# no need to check units for parameters
continue
-
+
if eq.type == DIFFERENTIAL_EQUATION:
check_unit(eq.expr, self.units[var] / second,
- resolved_namespace, specifiers)
+ resolved_namespace, specifiers)
elif eq.type == STATIC_EQUATION:
check_unit(eq.expr, self.units[var],
resolved_namespace, specifiers)
- else:
+ else:
raise AssertionError('Unknown equation type: "%s"' % eq.type)
@@ -745,13 +754,13 @@ def check_flags(self, allowed_flags):
If any flags are used that are not allowed.
'''
for eq in self.itervalues():
- for flag in eq.flags:
- if not eq.eq_type in allowed_flags or len(allowed_flags[eq.eq_type]) == 0:
- raise ValueError('Equations of type "%s" cannot have any flags.' % eq.eq_type)
+ for flag in eq.flags:
+ if not eq.type in allowed_flags or len(allowed_flags[eq.type]) == 0:
+ raise ValueError('Equations of type "%s" cannot have any flags.' % eq.type)
if not flag in allowed_flags[eq.type]:
raise ValueError(('Equations of type "%s" cannot have a '
- 'flag "%s", only the following flags '
- 'are allowed: %s') % (eq.eq_type,
+ 'flag "%s", only the following flags '
+ 'are allowed: %s') % (eq.type,
flag, allowed_flags[eq.type]))
############################################################################
@@ -764,6 +773,38 @@ def __str__(self):
def __repr__(self):
return '<Equations object consisting of %d equations>' % len(self._equations)
+
+ def _latex(self, *args):
+ equations = []
+ t = sympy.Symbol('t')
+ for eq in self._equations.itervalues():
+ # do not use SingleEquations._latex here as we want nice alignment
+ varname = sympy.Symbol(eq.varname)
+ if eq.type == DIFFERENTIAL_EQUATION:
+ lhs = sympy.Derivative(varname, t)
+ else:
+ # Normal equation or parameter
+ lhs = varname
+ if not eq.type == PARAMETER:
+ rhs = parse_to_sympy(eq.expr)
+ if len(eq.flags):
+ flag_str = ', flags: ' + ', '.join(eq.flags)
+ else:
+ flag_str = ''
+ if eq.type == PARAMETER:
+ eq_latex = r'%s &&& \text{(unit: $%s$%s)}' % (sympy.latex(lhs),
+ sympy.latex(eq.unit),
+ flag_str)
+ else:
+ eq_latex = r'%s &= %s && \text{(unit: $%s$%s)}' % (sympy.latex(lhs),
+ sympy.latex(rhs),
+ sympy.latex(eq.unit),
+ flag_str)
+ equations.append(eq_latex)
+ return r'\begin{align}' + r'\\'.join(equations) + r'\end{align}'
+
+ def _repr_latex_(self):
+ return sympy.latex(self)
def _repr_pretty_(self, p, cycle):
''' Pretty printing for ipython '''
View
2  brian2/equations/refractory.py
@@ -55,7 +55,7 @@ def add_refractoriness(eqs):
# replace differential equations having the active flag
for eq in eqs.itervalues():
- if eq.eq_type == DIFFERENTIAL_EQUATION and 'active' in eq.flags:
+ if eq.type == DIFFERENTIAL_EQUATION and 'active' in eq.flags:
# the only case where we have to change anything
new_code = 'is_active*(' + eq.expr.code + ')'
new_equations.append(SingleEquation(DIFFERENTIAL_EQUATION,
View
30 brian2/groups/neurongroup.py
@@ -3,6 +3,7 @@
'''
import numpy as np
from numpy import array
+import sympy
from brian2.equations.equations import (Equations, DIFFERENTIAL_EQUATION,
STATIC_EQUATION, PARAMETER)
@@ -220,6 +221,7 @@ def __init__(self, N, equations, method=None,
if self.threshold is not None:
self.thresholder = Thresholder(self)
+
#: Resets neurons which have spiked (`spikes`)
self.resetter = None
if self.reset is not None:
@@ -256,7 +258,7 @@ def __len__(self):
def _allocate_memory(self, dtype=None):
# Allocate memory (TODO: this should be refactored somewhere at some point)
arrayvarnames = set(eq.varname for eq in self.equations.itervalues() if
- eq.eq_type in (DIFFERENTIAL_EQUATION,
+ eq.type in (DIFFERENTIAL_EQUATION,
PARAMETER))
arrays = {}
for name in arrayvarnames:
@@ -314,7 +316,7 @@ def _create_specifiers(self):
# First add all the differential equations and parameters, because they
# may be referred to by static equations
for eq in self.equations.itervalues():
- if eq.eq_type in (DIFFERENTIAL_EQUATION, PARAMETER):
+ if eq.type in (DIFFERENTIAL_EQUATION, PARAMETER):
array = self.arrays[eq.varname]
constant = ('constant' in eq.flags)
s.update({eq.varname: ArrayVariable(eq.varname,
@@ -322,8 +324,9 @@ def _create_specifiers(self):
array.dtype,
array,
'_neuron_idx',
- constant)})
- elif eq.eq_type == STATIC_EQUATION:
+ constant)})
+
+ elif eq.type == STATIC_EQUATION:
s.update({eq.varname: Subexpression(eq.varname, eq.unit,
brian_prefs['core.default_scalar_dtype'],
str(eq.expr),
@@ -340,7 +343,7 @@ def _create_specifiers(self):
return s
def pre_run(self, namespace):
-
+
# Update the namespace information in the specifiers in case the
# namespace was not specified explicitly defined at creation time
# Note that values in the explicit namespace might still change
@@ -354,3 +357,20 @@ def pre_run(self, namespace):
# Check units
self.equations.check_units(self.namespace, self.specifiers,
namespace)
+
+ def _repr_html_(self):
+ text = [r'NeuronGroup "%s" with %d neurons.<br>' % (self.name, self.N)]
+ text.append(r'<b>Model:</b><nr>')
+ text.append(sympy.latex(self.equations))
+ text.append(r'<b>Integration method:</b><br>')
+ text.append(sympy.latex(self.state_updater.method)+'<br>')
+ if self.threshold is not None:
+ text.append(r'<b>Threshold condition:</b><br>')
+ text.append('<code>%s</code><br>' % str(self.threshold))
+ text.append('')
+ if self.reset is not None:
+ text.append(r'<b>Reset statement:</b><br>')
+ text.append(r'<code>%s</code><br>' % str(self.reset))
+ text.append('')
+
+ return '\n'.join(text)
View
6 brian2/groups/poissongroup.py
@@ -58,6 +58,12 @@ def pre_run(self, namespace):
def update(self):
self.spikes, = (rand(self.N)<self.pthresh).nonzero()
+ def __repr__(self):
+ description = '{classname}({N}, rates={rates})'
+ return description.format(classname=self.__class__.__name__,
+ N=self.N,
+ rates=repr(self.rates))
+
if __name__=='__main__':
from pylab import *
View
8 brian2/groups/subgroup.py
@@ -57,6 +57,14 @@ def update(self):
# TODO: improve efficiency with bisect?
spikes = spikes[logical_and(spikes>=self.start, spikes<self.end)]
self.spikes = spikes-self.start
+
+ def __repr__(self):
+ description = '<{classname} {name} of {source} from {start} to {end}>'
+ return description.format(classname=self.__class__.__name__,
+ name=repr(self.name),
+ source=repr(self.source.name),
+ start=self.start,
+ end=self.end)
if __name__=='__main__':
View
5 brian2/monitors/spikemonitor.py
@@ -114,6 +114,11 @@ def num_spikes(self):
'''
return sum(self.count)
+ def __repr__(self):
+ description = '<{classname}, recording {source}>'
+ return description.format(classname=self.__class__.__name__,
+ source=self.source.name)
+
if __name__=='__main__':
from pylab import *
View
6 brian2/monitors/statemonitor.py
@@ -137,6 +137,12 @@ def t_(self):
'''
return array(self._t)
+ def __repr__(self):
+ description = '<{classname}, recording {variables} from {source}>'
+ return description.format(classname=self.__class__.__name__,
+ variables=repr(self.variables),
+ source=self.source.name)
+
if __name__=='__main__':
from pylab import *
View
3  brian2/stateupdaters/exact.py
@@ -158,6 +158,9 @@ def __call__(self, equations, namespace=None, specifiers=None):
abstract_code.append('{variable} = _{variable}'.format(variable=variable))
return '\n'.join(abstract_code)
+ def __repr__(self):
+ return '%s()' % self.__class__.__name__
+
linear = LinearStateUpdater()
# The linear state updater has the highest priority
View
53 brian2/stateupdaters/explicit.py
@@ -254,13 +254,20 @@ def can_integrate(self, equations, namespace, specifiers):
return True
def __repr__(self):
- representation = '{classname}({description}, stochastic={stochastic})'
- return representation.format(classname=self.__class__.__name__,
- description=repr(self._description),
- stochastic=repr(self.stochastic))
+ # recreate a description string
+ description = '\n'.join(['%s = %s' % (var, expr)
+ for var, expr in self.statements])
+ if len(description):
+ description += '\n'
+ description += 'return ' + str(self.output)
+ r = "{classname}('''{description}''', stochastic={stochastic})"
+ return r.format(classname=self.__class__.__name__,
+ description=description,
+ stochastic=repr(self.stochastic))
+
def __str__(self):
- s = ''
+ s = '%s\n' % self.__class__.__name__
if len(self.statements) > 0:
s += 'Intermediate statements:\n'
@@ -272,6 +279,20 @@ def __str__(self):
s += str(self.output)
return s
+ def _latex(self, *args):
+ from sympy import latex, Symbol
+ s = [r'\begin{equation}']
+ for var, expr in self.statements:
+ expr = expr.subs(Symbol('x'), Symbol('x_t'))
+ s.append(latex(Symbol(var)) + ' = ' + latex(expr) + r'\\')
+ expr = self.output.subs(Symbol('x'), 'x_t')
+ s.append(r'x_{t+1} = ' + latex(expr))
+ s.append(r'\end{equation}')
+ return '\n'.join(s)
+
+ def _repr_latex_(self):
+ return self._latex()
+
def _generate_RHS(self, eqs, var, eq_symbols, temp_vars, expr,
non_stochastic_expr, stochastic_expr):
'''
@@ -434,11 +455,11 @@ def __call__(self, eqs, namespace=None, specifiers=None):
_v = -dt*v/tau + v
v = _v
>>> print(rk4(eqs))
- _k1_v = -dt*v/tau
- _k2_v = -dt*(_k1_v/2 + v)/tau
- _k3_v = -dt*(_k2_v/2 + v)/tau
- _k4_v = -dt*(_k3_v + v)/tau
- _v = _k1_v/6 + _k2_v/3 + _k3_v/3 + _k4_v/6 + v
+ _k_1_v = -dt*v/tau
+ _k_2_v = -dt*(_k_1_v/2 + v)/tau
+ _k_3_v = -dt*(_k_2_v/2 + v)/tau
+ _k_4_v = -dt*(_k_3_v + v)/tau
+ _v = _k_1_v/6 + _k_2_v/3 + _k_3_v/3 + _k_4_v/6 + v
v = _v
'''
@@ -508,11 +529,11 @@ def __call__(self, eqs, namespace=None, specifiers=None):
#: Classical Runge-Kutta method (RK4)
rk4 = ExplicitStateUpdater('''
- k1=dt*f(x,t)
- k2=dt*f(x+k1/2,t+dt/2)
- k3=dt*f(x+k2/2,t+dt/2)
- k4=dt*f(x+k3,t+dt)
- return x+(k1+2*k2+2*k3+k4)/6
+ k_1=dt*f(x,t)
+ k_2=dt*f(x+k_1/2,t+dt/2)
+ k_3=dt*f(x+k_2/2,t+dt/2)
+ k_4=dt*f(x+k_3,t+dt)
+ return x+(k_1+2*k_2+2*k_3+k_4)/6
''')
#: Derivative-free Milstein method
@@ -528,4 +549,4 @@ def __call__(self, eqs, namespace=None, specifiers=None):
StateUpdateMethod.register('euler', euler)
StateUpdateMethod.register('rk2', rk2)
StateUpdateMethod.register('rk4', rk4)
-StateUpdateMethod.register('milstein', milstein)
+StateUpdateMethod.register('milstein', milstein)
View
10 brian2/tests/test_equations.py
@@ -104,7 +104,7 @@ def test_parse_equations():
''' Test the parsing of equation strings '''
# A simple equation
eqs = parse_string_equations('dv/dt = -v / tau : 1')
- assert len(eqs.keys()) == 1 and 'v' in eqs and eqs['v'].eq_type == DIFFERENTIAL_EQUATION
+ assert len(eqs.keys()) == 1 and 'v' in eqs and eqs['v'].type == DIFFERENTIAL_EQUATION
assert get_dimensions(eqs['v'].unit) == DIMENSIONLESS
# A complex one
@@ -117,10 +117,10 @@ def test_parse_equations():
f : Hz (constant)
''')
assert len(eqs.keys()) == 4
- assert 'v' in eqs and eqs['v'].eq_type == DIFFERENTIAL_EQUATION
- assert 'ge' in eqs and eqs['ge'].eq_type == DIFFERENTIAL_EQUATION
- assert 'I' in eqs and eqs['I'].eq_type == STATIC_EQUATION
- assert 'f' in eqs and eqs['f'].eq_type == PARAMETER
+ assert 'v' in eqs and eqs['v'].type == DIFFERENTIAL_EQUATION
+ assert 'ge' in eqs and eqs['ge'].type == DIFFERENTIAL_EQUATION
+ assert 'I' in eqs and eqs['I'].type == STATIC_EQUATION
+ assert 'f' in eqs and eqs['f'].type == PARAMETER
assert get_dimensions(eqs['v'].unit) == volt.dim
assert get_dimensions(eqs['ge'].unit) == volt.dim
assert get_dimensions(eqs['I'].unit) == volt.dim
View
71 brian2/units/allunits.py
@@ -9,7 +9,6 @@
from .fundamentalunits import (Unit, get_or_create_dimension,
standard_unit_register,
additional_unit_register)
-
__all__ = [
"metre",
@@ -2029,27 +2028,27 @@
"Ykatal2",
"Ykatal3",
]
-
-
-Unit.automatically_register_units = False
-
-#### FUNDAMENTAL UNITS
-metre = Unit.create(get_or_create_dimension(m=1), "metre", "m")
-meter = Unit.create(get_or_create_dimension(m=1), "meter", "m")
-kilogram = Unit.create(get_or_create_dimension(kg=1), "kilogram", "kg")
-gram = Unit.create_scaled_unit(kilogram, "m")
-gram.set_name('gram')
-gram.set_display_name('g')
-gramme = Unit.create_scaled_unit(kilogram, "m")
-gramme.set_name('gramme')
-gramme.set_display_name('g')
-second = Unit.create(get_or_create_dimension(s=1), "second", "s")
-amp = Unit.create(get_or_create_dimension(A=1), "amp", "A")
-kelvin = Unit.create(get_or_create_dimension(K=1), "kelvin", "K")
-mole = Unit.create(get_or_create_dimension(mol=1), "mole", "mol")
-candle = Unit.create(get_or_create_dimension(candle=1), "candle", "cd")
-fundamental_units = [metre, meter, gram, second, amp, kelvin, mole, candle]
-
+
+
+Unit.automatically_register_units = False
+
+#### FUNDAMENTAL UNITS
+metre = Unit.create(get_or_create_dimension(m=1), "metre", "m")
+meter = Unit.create(get_or_create_dimension(m=1), "meter", "m")
+kilogram = Unit.create(get_or_create_dimension(kg=1), "kilogram", "kg")
+gram = Unit.create_scaled_unit(kilogram, "m")
+gram.set_name('gram')
+gram.set_display_name('g')
+gramme = Unit.create_scaled_unit(kilogram, "m")
+gramme.set_name('gramme')
+gramme.set_display_name('g')
+second = Unit.create(get_or_create_dimension(s=1), "second", "s")
+amp = Unit.create(get_or_create_dimension(A=1), "amp", "A")
+kelvin = Unit.create(get_or_create_dimension(K=1), "kelvin", "K")
+mole = Unit.create(get_or_create_dimension(mol=1), "mole", "mol")
+candle = Unit.create(get_or_create_dimension(candle=1), "candle", "cd")
+fundamental_units = [metre, meter, gram, second, amp, kelvin, mole, candle]
+
radian = Unit.create(get_or_create_dimension(), "radian", "rad")
steradian = Unit.create(get_or_create_dimension(), "steradian", "sr")
hertz = Unit.create(get_or_create_dimension(s= -1), "hertz", "Hz")
@@ -2060,7 +2059,7 @@
coulomb = Unit.create(get_or_create_dimension(s=1, A=1), "coulomb", "C")
volt = Unit.create(get_or_create_dimension(m=2, kg=1, s=-3, A=-1), "volt", "V")
farad = Unit.create(get_or_create_dimension(m= -2, kg=-1, s=4, A=2), "farad", "F")
-ohm = Unit.create(get_or_create_dimension(m=2, kg=1, s= -3, A=-2), "ohm", "ohm")
+ohm = Unit.create(get_or_create_dimension(m=2, kg=1, s= -3, A=-2), "ohm", "ohm", r"\Omega")
siemens = Unit.create(get_or_create_dimension(m= -2, kg=-1, s=3, A=2), "siemens", "S")
weber = Unit.create(get_or_create_dimension(m=2, kg=1, s=-2, A=-1), "weber", "Wb")
tesla = Unit.create(get_or_create_dimension(kg=1, s=-2, A=-1), "tesla", "T")
@@ -2072,8 +2071,8 @@
gray = Unit.create(get_or_create_dimension(m=2, s=-2), "gray", "Gy")
sievert = Unit.create(get_or_create_dimension(m=2, s=-2), "sievert", "Sv")
katal = Unit.create(get_or_create_dimension(s=-1, mol=1), "katal", "kat")
-
-
+
+
######### SCALED BASE UNITS ###########
ametre = Unit.create_scaled_unit(metre, "a")
cmetre = Unit.create_scaled_unit(metre, "c")
@@ -5404,8 +5403,6 @@
Ykatal2.name = "Ykatal2"
Ykatal3 = Ykatal**3
Ykatal3.name = "Ykatal3"
-
-
base_units = [
metre,
@@ -5441,7 +5438,7 @@
sievert,
katal,
]
-
+
scaled_units = [
ametre,
@@ -5957,7 +5954,7 @@
kkatal,
Ykatal,
]
-
+
powered_units = [
metre2,
@@ -7049,7 +7046,7 @@
Ykatal2,
Ykatal3,
]
-
+
# Current list from http://physics.nist.gov/cuu/Units/units.html, far from complete
additional_units = [
@@ -7058,7 +7055,7 @@
joule / metre ** 3, volt / metre ** 3, coulomb / metre ** 3, coulomb / metre ** 2,
farad / metre, henry / metre, joule / mole, joule / (mole * kelvin),
coulomb / kilogram, gray / second, katal / metre ** 3 ]
-
+
all_units = [
metre,
@@ -9078,9 +9075,9 @@
Ykatal2,
Ykatal3,
]
-
-
-Unit.automatically_register_units = True
-
-map(standard_unit_register.add, base_units + scaled_units + powered_units)
-map(additional_unit_register.add, additional_units)
+
+
+Unit.automatically_register_units = True
+
+map(standard_unit_register.add, base_units + scaled_units + powered_units)
+map(additional_unit_register.add, additional_units)
View
98 brian2/units/fundamentalunits.py
@@ -22,6 +22,7 @@
import itertools
import numpy as np
+from sympy import latex
__all__ = [
'DimensionMismatchError', 'get_or_create_dimension',
@@ -245,6 +246,7 @@ def f(x, *args, **kwds): # pylint: disable=C0111
"P": 1e15, "E": 1e18, "Z": 1e21, "Y": 1e24}
+
class Dimension(object):
'''
Stores the indices of the 7 basic SI unit dimension (length, mass, etc.).
@@ -337,6 +339,22 @@ def _str_representation(self, python_code=False):
return "1"
return s.strip()
+ def _latex(self, *args):
+ parts = []
+ for i in xrange(len(self._dims)):
+ if self._dims[i]:
+ s = _ilabel[i]
+ if self._dims[i] != 1:
+ s += '^{%s}' % str(self._dims[i])
+ parts.append(s)
+ s = "\,".join(parts)
+ if not len(s):
+ return "1"
+ return s.strip()
+
+ def _repr_latex(self):
+ return '$%s$' % latex(self)
+
def __repr__(self):
return self._str_representation(python_code=True)
@@ -1390,6 +1408,24 @@ def __reduce__(self):
def __repr__(self):
return self.in_best_unit(python_code=True)
+ # TODO: Use sympy's _latex method, then latex(unit) should work
+ def _latex(self, expr):
+ from sympy import Matrix
+ best_unit = self._get_best_unit()
+ if isinstance(best_unit, Unit):
+ best_unit_latex = latex(best_unit)
+ else: # A quantity
+ best_unit_latex = latex(best_unit.dimensions)
+ unitless = np.asarray(self / best_unit)
+ if unitless.ndim == 0:
+ sympy_quantity = np.float(unitless)
+ else:
+ sympy_quantity = Matrix(unitless)
+ return latex(sympy_quantity) + '\,' + best_unit_latex
+
+ def _repr_latex_(self):
+ return '$' + latex(self) + '$'
+
def __str__(self):
return self.in_best_unit()
@@ -1582,7 +1618,7 @@ class Unit(Quantity):
set_display_name
'''
- __slots__ = ["dim", "scale", "scalefactor", "dispname", "name",
+ __slots__ = ["dim", "scale", "scalefactor", "dispname", "name", "latexname",
"iscompound"]
__array_priority__ = 100
@@ -1625,11 +1661,14 @@ def __init__(self, value, dim=None, scale=None):
self.name = ""
#: The display name of this unit.
self.dispname = ""
+ #: A LaTeX expression for the name of this unit.
+ self.latexname = ""
#: Whether this unit is a combination of other units.
self.iscompound = False
@staticmethod
- def create(dim, name="", dispname="", scalefactor="", **keywords):
+ def create(dim, name="", dispname="", latexname=None, scalefactor="",
+ **keywords):
"""
Create a new named unit.
@@ -1641,7 +1680,11 @@ def create(dim, name="", dispname="", scalefactor="", **keywords):
The full name of the unit, e.g. ``'volt'``
dispname : `str`, optional
The display name, e.g. ``'V'``
- scalefactor : `str`, optional
+ latexname : str, optional
+ The name as a LaTeX expression (math mode is assumed, do not add
+ $ signs or similar), e.g. ``'\omega'``. If no `latexname` is
+ specified, `dispname` will be used.
+ scalefactor : str, optional
The scaling factor, e.g. ``'m'`` for mvolt
keywords
The scaling for each SI dimension, e.g. ``length="m"``,
@@ -1661,8 +1704,11 @@ def create(dim, name="", dispname="", scalefactor="", **keywords):
v *= _siprefixes[s] ** i
u = Unit(v * _siprefixes[scalefactor], dim=dim, scale=tuple(scale))
u.scalefactor = scalefactor + ""
- u.name = name + ""
- u.dispname = dispname + ""
+ u.name = str(name)
+ u.dispname = str(dispname)
+ if latexname is None:
+ latexname = u.dispname
+ u.latexname = r'\mathrm{' + latexname + '}'
u.iscompound = False
return u
@@ -1689,6 +1735,14 @@ def create_scaled_unit(baseunit, scalefactor):
u.scalefactor = scalefactor
u.name = scalefactor + baseunit.name
u.dispname = scalefactor + baseunit.dispname
+ # As u --> \mu is the only transformation we have, I think it
+ # makes sense to just special-case it here instead of coming
+ # up with a general system for scale factors
+ # TODO: Unfortunately, \mu gives the typographically incorrect symbol,
+ #it should be an upright letter :-/
+ if scalefactor == 'u':
+ scalefactor = r'\mu'
+ u.latexname = r'\mathrm{' + scalefactor + '}' + r'\,' + baseunit.latexname
u.iscompound = False
return u
@@ -1742,12 +1796,40 @@ def __str__(self):
else:
return self.dispname
+ def _latex(self, *args):
+ if self.latexname == "":
+ if len(self.scalefactor):
+ if self.scalefactor == 'u':
+ scalefactor = r'\mu'
+ else:
+ scalefactor = self.scalefactor
+ s = r'\mathrm{' + scalefactor + "} "
+ else:
+ s = ''
+ for i in range(7):
+ if self.dim._dims[i]:
+ s += self.scale[i] + _ilabel[i]
+ if self.dim._dims[i] != 1:
+ s += "^{" + str(self.dim._dims[i]) + '}'
+ s += " "
+ s = s.strip()
+ if not len(s):
+ return "1"
+ else:
+ return s
+ else:
+ return self.latexname
+
+ def _repr_latex_(self):
+ return '$' + latex(self) + '$'
+
#### ARITHMETIC ####
def __mul__(self, other):
if isinstance(other, Unit):
u = Unit(np.asarray(self) * np.asarray(other))
u.name = self.name + " * " + other.name
u.dispname = self.dispname + ' ' + other.dispname
+ u.latexname = self.latexname + r'\,' + other.latexname
u.dim = self.dim * other.dim
u.iscompound = True
return u
@@ -1776,6 +1858,9 @@ def __div__(self, other):
u.name += other.name
u.dim = self.dim / other.dim
u.iscompound = True
+
+ u.latexname = r'\frac{%s}{%s}' % (self.latexname, other.latexname)
+
return u
else:
return super(Unit, self).__div__(other)
@@ -1792,11 +1877,14 @@ def __pow__(self, other):
if self.iscompound:
u.dispname = '(' + self.dispname + ')'
u.name = '(' + self.name + ')'
+ u.latexname = r'\left(%s\right)' % self.latexname
else:
u.dispname = self.dispname
u.name = self.name
+ u.latexname = self.latexname
u.dispname += '^' + str(other)
u.name += ' ** ' + repr(other)
+ u.latexname += '^{%s}' % latex(other)
u.dim = self.dim ** other
return u
else:
View
3  docs_sphinx/conf.py
@@ -260,7 +260,8 @@
intersphinx_mapping = {
'http://docs.python.org/': None,
'http://docs.scipy.org/doc/numpy': None,
- 'http://docs.scipy.org/doc/scipy/reference': None
+ 'http://docs.scipy.org/doc/scipy/reference': None,
+ 'http://docs.sympy.org/dev/': None
}
autodoc_default_flags = ['show-inheritance']
View
3  docs_sphinx/developer/guidelines/index.rst
@@ -16,7 +16,8 @@ The basic principles of developing Brian are:
workflow
style
+ representation
defensive_programming
documentation
logging
- testing
+ testing
View
91 docs_sphinx/developer/guidelines/representation.rst
@@ -0,0 +1,91 @@
+Representing Brian objects
+=============================
+
+``__repr__`` and ``__str__``
+----------------------------
+
+Every class should specify or inherit useful ``__repr__`` and ``__str__`` methods. The ``__repr__``
+method should give the "official" representation of the object; if possible, this should be a valid
+Python expression, ideally allowing for ``eval(repr(x)) == x``. The ``__str__`` method on the other
+hand, gives an "informal" representation of the object. This can be anything that is helpful but
+does not have to be Python code. For example:
+
+.. doctest::
+
+ >>> import numpy as np
+ >>> ar = np.array([1, 2, 3]) * mV
+ >>> print(ar) # uses __str__
+ [ 1. 2. 3.] mV
+ >>> ar # uses __repr__
+ array([ 1., 2., 3.]) * mvolt
+
+If the representation returned by ``__repr__`` is not Python code, it should be enclosed in
+``<...>``, e.g. a `Synapses` representation might be ``<Synapses object with 64 synapses>``.
+
+If you don't want to make the distinction between ``__repr__`` and ``__str__``, simply define only
+a ``__repr__`` function, it will be used instead of ``__str__`` automatically (no need to write
+``__str__ = __repr__``). Finally, if you include the class name in the representation (which you
+should in most cases), use ``self.__class__.__name__`` instead of spelling out the name explicitly
+-- this way it will automatically work correctly for subclasses. It will also prevent you from
+forgetting to update the class name in the representation if you decide to rename the class.
+
+LaTeX representations with sympy
+--------------------------------
+Brian objects dealing with mathematical expressions and equations often internally use sympy.
+Sympy's `~sympy.printing.latex.latex` function does a nice job of converting expressions into
+LaTeX code, using fractions, root symbols, etc. as well as converting greek variable names into
+corresponding symbols and handling sub- and superscripts. For the conversion of variable names
+to work, they should use an underscore for subscripts and two underscores for superscripts::
+
+ >>> from sympy import latex, Symbol
+ >>> tau_1__e = Symbol('tau_1__e')
+ >>> print latex(tau_1__e)
+ \tau^{e}_{1}
+
+Sympy's printer supports formatting arbitrary objects, all they have to do is to implement a
+``_latex`` method (no trailing underscore). For most Brian objects, this is unnecessary as they will
+never be formatted with sympy's LaTeX printer. For some core objects, in particular the units,
+is is useful, however, as it can be reused in LaTeX representations for ipython (see below).
+Note that the ``_latex`` method should not return ``$`` or ``\begin{equation}`` (sympy's method
+includes a ``mode`` argument that wraps the output automatically).
+
+Representations for ipython
+---------------------------------
+
+"Old" ipython console
+~~~~~~~~~~~~~~~~~~~~~
+
+In particular for representations involing arrays or lists, it can be useful to break up the
+representation into chunks, or indent parts of the representation. This is supported by the
+ipython console's "pretty printer". To make this work for a class, add a
+``_repr_pretty_(self, p, cycle)`` (note the *single* underscores) method. You can find more
+information in the `ipython documentation <http://ipython.org/ipython-doc/dev/api/generated/IPython.lib.pretty.html#extending>`__ .
+
+"New" ipython console (qtconsole and notebook)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The new ipython consoles, the qtconsole and the ipython notebook support a much richer set of
+representations for objects. As Brian deals a lot with mathematical objects, in particular the
+LaTeX and to a lesser extent the HTML formatting capabilities of the ipython notebook are
+interesting. To support LaTeX representation, implement a `_repr_latex_` method returning the
+LaTeX code (*including* ``$``, ``\begin{equation}`` or similar). If the object already has a
+``_latex`` method (see `LaTeX representations with sympy`_ above), this can be as simple as::
+
+ def _repr_latex_(self):
+ return sympy.latex(self, mode='inline') # wraps the expression in $ .. $
+
+The LaTeX rendering only supports a single mathematical block. For complex objects, e.g.
+`NeuronGroup` it might be useful to have a richer representation. This can be achieved by returning
+HTML code from ``_repr_html_`` -- this HTML code is processed by MathJax so it can include literal
+LaTeX code that will be transformed before it is rendered as HTML. An object containing two
+equations could therefore be represented with a method like this::
+
+ def _repr_html_(self):
+ return '''
+ <h3> Equation 1 </h3>
+ {eq_1}
+ <h3> Equation 2 </h3>
+ {eq_2}'''.format(eq_1=sympy.latex(self.eq_1, mode='equation'),
+ eq_2=sympy.latex(self.eq_2, mode='equation'))
+
+
Something went wrong with that request. Please try again.