def test_repeated_construction(): eqs1 = Equations('dx/dt = x : 1') eqs2 = Equations('dx/dt = x : 1', x='y') assert len(eqs1) == 1 assert 'x' in eqs1 assert eqs1['x'].expr == Expression('x') assert len(eqs2) == 1 assert 'y' in eqs2 assert eqs2['y'].expr == Expression('y')
def test_substitute(): # Check that Equations.substitute returns an independent copy eqs = Equations('dx/dt = x : 1') eqs2 = eqs.substitute(x='y') # First equation should be unaffected assert len(eqs) == 1 and 'x' in eqs assert eqs['x'].expr == Expression('x') # Second equation should have x substituted by y assert len(eqs2) == 1 and 'y' in eqs2 assert eqs2['y'].expr == Expression('y')
def test_correct_replacements(): ''' Test replacing variables via keyword arguments ''' # replace a variable name with a new name eqs = Equations('dv/dt = -v / tau : 1', v='V') # Correct left hand side assert ('V' in eqs) and not ('v' in eqs) # Correct right hand side assert ('V' in eqs['V'].identifiers) and not ('v' in eqs['V'].identifiers) # replace a variable name with a value eqs = Equations('dv/dt = -v / tau : 1', tau=10 * ms) assert not 'tau' in eqs['v'].identifiers
def test_identifier_checks(): legal_identifiers = ['v', 'Vm', 'V', 'x', 'ge', 'g_i', 'a2', 'gaba_123'] illegal_identifiers = ['_v', '1v', 'ü', 'ge!', 'v.x', 'for', 'else', 'if'] for identifier in legal_identifiers: try: check_identifier_basic(identifier) check_identifier_reserved(identifier) except ValueError as ex: raise AssertionError('check complained about ' 'identifier "%s": %s' % (identifier, ex)) for identifier in illegal_identifiers: with pytest.raises(SyntaxError): check_identifier_basic(identifier) for identifier in ('t', 'dt', 'xi', 'i', 'N'): with pytest.raises(SyntaxError): check_identifier_reserved(identifier) for identifier in ('not_refractory', 'refractory', 'refractory_until'): with pytest.raises(SyntaxError): check_identifier_refractory(identifier) for identifier in ('exp', 'sin', 'sqrt'): with pytest.raises(SyntaxError): check_identifier_functions(identifier) for identifier in ('e', 'pi', 'inf'): with pytest.raises(SyntaxError): check_identifier_constants(identifier) for identifier in ('volt', 'second', 'mV', 'nA'): with pytest.raises(SyntaxError): check_identifier_units(identifier) # Check identifier registry assert check_identifier_basic in Equations.identifier_checks assert check_identifier_reserved in Equations.identifier_checks assert check_identifier_refractory in Equations.identifier_checks assert check_identifier_functions in Equations.identifier_checks assert check_identifier_constants in Equations.identifier_checks assert check_identifier_units in Equations.identifier_checks # Set up a dummy identifier check that disallows the variable name # gaba_123 (that is otherwise valid) def disallow_gaba_123(identifier): if identifier == 'gaba_123': raise SyntaxError('I do not like this name') Equations.check_identifier('gaba_123') old_checks = set(Equations.identifier_checks) Equations.register_identifier_check(disallow_gaba_123) with pytest.raises(SyntaxError): Equations.check_identifier('gaba_123') Equations.identifier_checks = old_checks # registering a non-function should not work with pytest.raises(ValueError): Equations.register_identifier_check('no function')
def test_str_repr(): ''' Test the string representation (only that it does not throw errors). ''' tau = 10 * ms eqs = Equations('''dv/dt = -(v + I)/ tau : volt (unless refractory) I = sin(2 * 22/7. * f * t)* volt : volt f : Hz''') assert len(str(eqs)) > 0 assert len(repr(eqs)) > 0 # Test str and repr of SingleEquations explicitly (might already have been # called by Equations for eq in eqs.values(): assert (len(str(eq))) > 0 assert (len(repr(eq))) > 0
def test_extract_subexpressions(): eqs = Equations('''dv/dt = -v / (10*ms) : 1 s1 = 2*v : 1 s2 = -v : 1 (constant over dt) ''') variable, constant = extract_constant_subexpressions(eqs) assert [var in variable for var in ['v', 's1', 's2']] assert variable['s1'].type == SUBEXPRESSION assert variable['s2'].type == PARAMETER assert constant['s2'].type == SUBEXPRESSION
def test_ipython_pprint(): from io import StringIO eqs = Equations('''dv/dt = -(v + I)/ tau : volt (unless refractory) I = sin(2 * 22/7. * f * t)* volt : volt f : Hz''') # Test ipython's pretty printing old_stdout = sys.stdout string_output = StringIO() sys.stdout = string_output pprint(eqs) assert len(string_output.getvalue()) > 0 sys.stdout = old_stdout
def test_concatenation(): eqs1 = Equations('''dv/dt = -(v + I) / tau : volt I = sin(2*pi*freq*t) : volt freq : Hz''') # Concatenate two equation objects eqs2 = (Equations('dv/dt = -(v + I) / tau : volt') + Equations('''I = sin(2*pi*freq*t) : volt freq : Hz''')) # Concatenate using "in-place" addition (which is not actually in-place) eqs3 = Equations('dv/dt = -(v + I) / tau : volt') eqs3 += Equations('''I = sin(2*pi*freq*t) : volt freq : Hz''') # Concatenate with a string (will be parsed first) eqs4 = Equations('dv/dt = -(v + I) / tau : volt') eqs4 += '''I = sin(2*pi*freq*t) : volt freq : Hz''' # Concatenating with something that is not a string should not work with pytest.raises(TypeError): eqs4 + 5 # The string representation is canonical, therefore it should be identical # in all cases assert str(eqs1) == str(eqs2) assert str(eqs2) == str(eqs3) assert str(eqs3) == str(eqs4)
def test_wrong_replacements(): '''Tests for replacements that should not work''' # Replacing a variable name with an illegal new name with pytest.raises(SyntaxError): Equations('dv/dt = -v / tau : 1', v='illegal name') with pytest.raises(SyntaxError): Equations('dv/dt = -v / tau : 1', v='_reserved') with pytest.raises(SyntaxError): Equations('dv/dt = -v / tau : 1', v='t') # Replacing a variable name with a value that already exists with pytest.raises(EquationError): Equations('''dv/dt = -v / tau : 1 dx/dt = -x / tau : 1 ''', v='x') # Replacing a model variable name with a value with pytest.raises(ValueError): Equations('dv/dt = -v / tau : 1', v=3 * mV) # Replacing with an illegal value with pytest.raises(SyntaxError): Equations('dv/dt = -v/tau : 1', tau=np.arange(5))
def test_properties(): ''' Test accessing the various properties of equation objects ''' tau = 10 * ms eqs = Equations('''dv/dt = -(v + I)/ tau : volt I = sin(2 * 22/7. * f * t)* volt : volt f = freq * Hz: Hz freq : 1''') assert (len(eqs.diff_eq_expressions) == 1 and eqs.diff_eq_expressions[0][0] == 'v' and isinstance(eqs.diff_eq_expressions[0][1], Expression)) assert eqs.diff_eq_names == {'v'} assert (len(eqs.eq_expressions) == 3 and {name for name, _ in eqs.eq_expressions} == {'v', 'I', 'f'} and all( (isinstance(expr, Expression) for _, expr in eqs.eq_expressions))) assert len(eqs.eq_names) == 3 and eqs.eq_names == {'v', 'I', 'f'} assert set(eqs.keys()) == {'v', 'I', 'f', 'freq'} # test that the equations object is iterable itself assert all((isinstance(eq, SingleEquation) for eq in eqs.values())) assert all((isinstance(eq, str) for eq in eqs)) assert (len(eqs.ordered) == 4 and all( (isinstance(eq, SingleEquation) for eq in eqs.ordered)) and [eq.varname for eq in eqs.ordered] == ['f', 'I', 'v', 'freq']) assert [eq.unit for eq in eqs.ordered] == [Hz, volt, volt, 1] assert eqs.names == {'v', 'I', 'f', 'freq'} assert eqs.parameter_names == {'freq'} assert eqs.subexpr_names == {'I', 'f'} dimensions = eqs.dimensions assert set(dimensions.keys()) == {'v', 'I', 'f', 'freq'} assert dimensions['v'] is volt.dim assert dimensions['I'] is volt.dim assert dimensions['f'] is Hz.dim assert dimensions['freq'] is DIMENSIONLESS assert eqs.names == set(eqs.dimensions.keys()) assert eqs.identifiers == {'tau', 'volt', 'Hz', 'sin', 't'} # stochastic equations assert len(eqs.stochastic_variables) == 0 assert eqs.stochastic_type is None eqs = Equations('''dv/dt = -v / tau + 0.1*second**-.5*xi : 1''') assert eqs.stochastic_variables == {'xi'} assert eqs.stochastic_type == 'additive' eqs = Equations( '''dv/dt = -v / tau + 0.1*second**-.5*xi_1 + 0.1*second**-.5*xi_2: 1''' ) assert eqs.stochastic_variables == {'xi_1', 'xi_2'} assert eqs.stochastic_type == 'additive' eqs = Equations('''dv/dt = -v / tau + 0.1*second**-1.5*xi*t : 1''') assert eqs.stochastic_type == 'multiplicative' eqs = Equations('''dv/dt = -v / tau + 0.1*second**-1.5*xi*v : 1''') assert eqs.stochastic_type == 'multiplicative'
def test_unit_checking(): # dummy Variable class class S(object): def __init__(self, dimensions): self.dim = get_dimensions(dimensions) # inconsistent unit for a differential equation eqs = Equations('dv/dt = -v : volt') group = SimpleGroup({'v': S(volt)}) with pytest.raises(DimensionMismatchError): eqs.check_units(group, {}) eqs = Equations('dv/dt = -v / tau: volt') group = SimpleGroup(namespace={'tau': 5 * mV}, variables={'v': S(volt)}) with pytest.raises(DimensionMismatchError): eqs.check_units(group, {}) group = SimpleGroup(namespace={'I': 3 * second}, variables={'v': S(volt)}) eqs = Equations('dv/dt = -(v + I) / (5 * ms): volt') with pytest.raises(DimensionMismatchError): eqs.check_units(group, {}) eqs = Equations('''dv/dt = -(v + I) / (5 * ms): volt I : second''') group = SimpleGroup(variables={'v': S(volt), 'I': S(second)}, namespace={}) with pytest.raises(DimensionMismatchError): eqs.check_units(group, {}) # inconsistent unit for a subexpression eqs = Equations('''dv/dt = -v / (5 * ms) : volt I = 2 * v : amp''') group = SimpleGroup(variables={'v': S(volt), 'I': S(second)}, namespace={}) with pytest.raises(DimensionMismatchError): eqs.check_units(group, {})
def test_construction_errors(): ''' Test that the Equations constructor raises errors correctly ''' # parse error with pytest.raises(EquationError): Equations('dv/dt = -v / tau volt') with pytest.raises(EquationError): Equations('dv/dt = -v / tau : volt second') # incorrect unit definition with pytest.raises(EquationError): Equations('dv/dt = -v / tau : mvolt') with pytest.raises(EquationError): Equations('dv/dt = -v / tau : voltage') with pytest.raises(EquationError): Equations('dv/dt = -v / tau : 1.0*volt') # Only a single string or a list of SingleEquation objects is allowed with pytest.raises(TypeError): Equations(None) with pytest.raises(TypeError): Equations(42) with pytest.raises(TypeError): Equations(['dv/dt = -v / tau : volt']) # duplicate variable names with pytest.raises(EquationError): Equations('''dv/dt = -v / tau : volt v = 2 * t/second * volt : volt''') eqs = [ SingleEquation(DIFFERENTIAL_EQUATION, 'v', volt.dim, expr=Expression('-v / tau')), SingleEquation(SUBEXPRESSION, 'v', volt.dim, expr=Expression('2 * t/second * volt')) ] with pytest.raises(EquationError): Equations(eqs) # illegal variable names with pytest.raises(SyntaxError): Equations('ddt/dt = -dt / tau : volt') with pytest.raises(SyntaxError): Equations('dt/dt = -t / tau : volt') with pytest.raises(SyntaxError): Equations('dxi/dt = -xi / tau : volt') with pytest.raises(SyntaxError): Equations('for : volt') with pytest.raises((EquationError, SyntaxError)): Equations('d1a/dt = -1a / tau : volt') with pytest.raises(SyntaxError): Equations('d_x/dt = -_x / tau : volt') # xi in a subexpression with pytest.raises(EquationError): Equations('''dv/dt = -(v + I) / (5 * ms) : volt I = second**-1*xi**-2*volt : volt''') # more than one xi with pytest.raises(EquationError): Equations('''dv/dt = -v / tau + xi/tau**.5 : volt dx/dt = -x / tau + 2*xi/tau : volt tau : second''') # using not-allowed flags eqs = Equations('dv/dt = -v / (5 * ms) : volt (flag)') eqs.check_flags({DIFFERENTIAL_EQUATION: ['flag']}) # allow this flag with pytest.raises(ValueError): eqs.check_flags({DIFFERENTIAL_EQUATION: []}) with pytest.raises(ValueError): eqs.check_flags({}) with pytest.raises(ValueError): eqs.check_flags({SUBEXPRESSION: ['flag']}) with pytest.raises(ValueError): eqs.check_flags({DIFFERENTIAL_EQUATION: ['otherflag']}) eqs = Equations('dv/dt = -v / (5 * ms) : volt (flag1, flag2)') eqs.check_flags({DIFFERENTIAL_EQUATION: ['flag1', 'flag2']}) # allow both flags # Don't allow the two flags in combination with pytest.raises(ValueError): eqs.check_flags({DIFFERENTIAL_EQUATION: ['flag1', 'flag2']}, incompatible_flags=[('flag1', 'flag2')]) eqs = Equations('''dv/dt = -v / (5 * ms) : volt (flag1) dw/dt = -w / (5 * ms) : volt (flag2)''') # They should be allowed when used independently eqs.check_flags({DIFFERENTIAL_EQUATION: ['flag1', 'flag2']}, incompatible_flags=[('flag1', 'flag2')]) # Circular subexpression with pytest.raises(ValueError): Equations('''dv/dt = -(v + w) / (10 * ms) : 1 w = 2 * x : 1 x = 3 * w : 1''') # Boolean/integer differential equations with pytest.raises(TypeError): Equations('dv/dt = -v / (10*ms) : boolean') with pytest.raises(TypeError): Equations('dv/dt = -v / (10*ms) : integer')