diff --git a/cellmlmanip/model.py b/cellmlmanip/model.py index 2dcedb63..1f9dd928 100644 --- a/cellmlmanip/model.py +++ b/cellmlmanip/model.py @@ -1,5 +1,6 @@ """Classes to represent a flattened CellML model and metadata about its variables.""" import logging +from enum import Enum from io import StringIO import networkx as nx @@ -12,11 +13,16 @@ logger = logging.getLogger(__name__) - # Delimiter for variables name in Sympy expressions: SYMPY_SYMBOL_DELIMITER = '$' +class DataDirectionFlow(Enum): + """ Direction of data flow for converting units""" + INPUT = 1 + OUTPUT = 2 + + class Model(object): """ A componentless representation of a CellML model, containing a list of equations, units, and RDF metadata about @@ -34,6 +40,7 @@ class Model(object): :param name: the name of the model e.g. from ````. :param cmeta_id: An optional cmeta id, e.g. from ````. """ + def __init__(self, name, cmeta_id=None): self.name = name @@ -617,6 +624,276 @@ def variables(self): """ Returns an iterator over this model's variable symbols. """ return self._name_to_symbol.values() + def convert_variable(self, original_variable, units, direction): + """ + Add a new linked version of the given variable in the desired units. + + If the variable already has the requested units, no changes are made and the original variable is returned. + Otherwise ``direction`` specifies how information flows between the new variable and the original, and + hence what new equation(s) are added to the model to perform the conversion. + If ``INPUT`` then the original variable takes its value from the newly added variable; + if ``OUTPUT`` then the opposite happens. + + Any ``cmeta:id`` attribute on the original variable is moved to the new one, + so ontology annotations will refer to the new variable. + + Similarly if the direction is ``INPUT`` then any initial value will be moved to the new variable + (and converted appropriately). + + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + + ode(sv1, time) = 1{mV_per_ms}; + + convert_variable(time, second, DataDirectionFlow.INPUT) + + becomes + var time: ms; + var{time} time_converted: s; + var{sv11} sv1: mV {init: 2}; + var sv1_orig_deriv mV_per_ms + + time = 1000 * time_converted; + sv1_orig_deriv = 1{mV_per_ms} + ode(sv1, time_converted) = 1000 * sv1_orig_deriv + + + convert_variable(time, second, DataDirectionFlow.OUTPUT) + + creates model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + var{time} time_converted: s + + ode(sv1, time) = 1{mV_per_ms}; + time_converted = 0.001 * time + + + :param original_variable: the VariableDummy object representing the variable in the model to be converted + :param units: a Pint unit object representing the units to convert variable to (note if variable is already + in these units, model remains unchanged and the original variable is returned + :param direction: either DataDirectionFlow.INPUT; the variable to be changed is an input and all affected + equations will be adjusted + or DataDirectionFlow.OUTPUT; the variable to be changed is an output, equations + are unaffected apart from converting the actual output + :return: new variable with desired units, or original unchanged if conversion was not necessary + :throws: AssertionError if the arguments are of incorrect type + or the variable does not exist in the model + DimensionalityError if the unit conversion is impossible + """ + # assertion errors will be thrown here if arguments are incorrect type + self._check_arguments_for_convert_variables(original_variable, units, direction) + + original_units = original_variable.units + # no conversion necessary + if original_units == units: + return original_variable + + # conversion_factor for old units to new + # throws DimensionalityError if unit conversion is not possible + cf = self.units.get_conversion_factor(from_unit=original_units, to_unit=units) + + state_symbols = self.get_state_symbols() + free_symbol = self.get_free_variable_symbol() + # create new variable and relevant equations + new_variable = self._convert_variable_instance(original_variable, cf, units, direction) + + # if is output do not need to do additional changes for state/free symbols + if direction == DataDirectionFlow.OUTPUT: + return new_variable + + new_derivatives = [] + # if state variable + if original_variable in state_symbols: + new_derivatives.append(self._convert_state_variable_deriv(original_variable, new_variable, cf)) + + # if free variable + if original_variable == free_symbol: + # for each derivative wrt to free variable add necessary variables/equations + current_equations = self.equations.copy() + for equation in current_equations: + if equation.args[0].is_Derivative: + if equation.args[0].args[1].args[0] == original_variable: + new_derivatives.append(self._convert_free_variable_deriv(equation, new_variable, cf)) + + # replace any instances of derivative of rhs of other eqns with new derivative variable + for new_derivative in new_derivatives: + self._replace_derivatives(new_derivative) + + self._invalidate_cache() + + return new_variable + + def _check_arguments_for_convert_variables(self, variable, units, direction): + """ + Checks the arguments of the convert_variable function. + :param variable: variable must be a VariableDummy object present in the model + :param units: units must be a pint Unit object in this model + :param direction: must be part of DataDirectionFlow enum + :throws: AssertionError if the arguments are of incorrect type + or the variable does not exist in the model + """ + # variable should be a VariableDummy + assert isinstance(variable, VariableDummy) + + # variable must be in model + assert variable.name in self._name_to_symbol + + # units should be a pint Unit object in the registry for this model + assert isinstance(units, self.units.ureg.Unit) + + # direction should be part of enum + assert isinstance(direction, DataDirectionFlow) + + def _replace_derivatives(self, new_derivative): + """ + Function to replace an instance of a derivative that occurs on the RHS of any equation + :param new_derivative: new variable representing the derivative + """ + for equation in self.equations: + for argument in equation.rhs.args: + if new_derivative['expression'] == argument: + # add new equation + new_eqn = equation.subs(new_derivative['expression'], new_derivative['variable']) + self.add_equation(new_eqn) + self.remove_equation(equation) + break + + def _create_new_deriv_variable_and_equation(self, eqn, derivative_variable): + """ + Create a new variable and equation for the derivative. + :param eqn: the original derivative eqn + :param derivative_variable: the dependent variable + :return: new variable for the derivative + """ + # 1. create a new variable + deriv_name = self._get_unique_name(derivative_variable.name + '_orig_deriv') + deriv_units = self.units.calculator.traverse(eqn.args[0]) + new_deriv_variable = self.add_variable(name=deriv_name, + units=deriv_units.units) + + # 2. create new equation and remove original + expression = sympy.Eq(new_deriv_variable, eqn.args[1]) + self.add_equation(expression) + self.remove_equation(eqn) + + return new_deriv_variable + + def _convert_free_variable_deriv(self, eqn, new_variable, cf): + """ + Create relevant variables/equations when converting a free variable within a derivative. + :param eqn: the derivative equation containing free variable + :param new_variable: the new variable representing the converted symbol [new_units] + :param cf: conversion factor for unit conversion [new units/old units] + """ + derivative_variable = eqn.args[0].args[0] # units [x] + # 1. create a new variable/equation for original derivative + # will have units [x/old units] + new_deriv_variable = self._create_new_deriv_variable_and_equation(eqn, derivative_variable) + + # 2. create equation for derivative wrt new variable + # dx/dnewvar [x/new units] = new_deriv_var [x/old units] / cf [new units/old units] + expression = sympy.Eq(sympy.Derivative(derivative_variable, new_variable), new_deriv_variable / cf) + self.add_equation(expression) + return {'variable': new_deriv_variable, 'expression': eqn.args[0]} + + def _convert_state_variable_deriv(self, original_variable, new_variable, cf): + """ + Create relevant variables/equations when converting a state variable. + :param original_variable: the variable to be converted [old units] + :param new_variable: the new variable representing the converted symbol [new units] + :param cf: conversion factor for unit conversion [new units/old units] + :return: a dictionary containing the 'variable' and 'expression' for new derivative + """ + # 1. find the derivative equation for this variable + # and get the free variable (wrt_variable) + eqn = None + for equation in self.equations: + if equation.args[0].is_Derivative: + if equation.args[0].args[0] == original_variable: + eqn = equation + break + + # get free variable symbol + # units [x] + wrt_variable = eqn.args[0].args[1] + + # 1. create a new variable/equation for original derivative + # will have units [old units/x] + new_deriv_variable = self._create_new_deriv_variable_and_equation(eqn, original_variable) + + # 2. add a new derivative equation + # dnewvar/dx [new units/x] = new_deriv_var [old units/x] * cf [new units/old units] + expression = sympy.Eq(sympy.Derivative(new_variable, wrt_variable), new_deriv_variable * cf) + self.add_equation(expression) + return {'variable': new_deriv_variable, 'expression': eqn.args[0]} + + def _convert_variable_instance(self, original_variable, cf, units, direction): + """ + Internal function to create new variable and an equation for it. + :param original_variable: VariableDummy object to be converted [old units] + :param cf: conversion factor [new units/old units] + :param units: Unit object for new units + :param direction: enumeration value specifying input or output + :return: the new variable created [new units] + """ + # 1. get unique name for new variable + new_name = self._get_unique_name(original_variable.name + '_converted') + + # 2. if original has initial_value calculate new initial value (only needed for INPUT case) + new_value = None + if direction == DataDirectionFlow.INPUT and original_variable.initial_value: + new_value = original_variable.initial_value * cf + + # 3. copy cmeta_id from original and remove from original + new_variable = self.add_variable(name=new_name, + units=units, + initial_value=new_value, + cmeta_id=original_variable.cmeta_id) + original_variable.cmeta_id = '' + + # 4 add/remove/replace equations + if direction == DataDirectionFlow.INPUT: + # if direction is input; original var will be replaced by equation so do not need to store initial value + original_variable.initial_value = None + + # find the equation for the original variable: orig_var = rhs + # remove equation from model + # add eqn for new variabale in terms of rhs of equation + # new_var [new units] = rhs [old units] * cf [new units/old units] + for equation in self.equations: + if equation.is_Equality and equation.args[0] == original_variable: + expression = sympy.Eq(new_variable, equation.args[1] * cf) + self.add_equation(expression) + self.remove_equation(equation) + break + + # add eqn for original variable in terms of new variable + # orig_var [old units] = new var [new units] / cf [new units/old units] + expression = sympy.Eq(original_variable, new_variable / cf) + self.add_equation(expression) + else: + # if direction is output add eqn for new variable in terms of original variable + # new_var [new units] = orig_var [old units] * cf [new units/old units] + expression = sympy.Eq(new_variable, original_variable * cf) + self.add_equation(expression) + + return new_variable + + def _get_unique_name(self, name): + """ Function to create a unique name within the model. + :param name: String Suggested unique name + :return: String unique name + """ + for var in list(self.variables()): + if name == var.name: + name = self._get_unique_name(name + '_a') + break + return name + class NumberDummy(sympy.Dummy): """ @@ -626,6 +903,7 @@ class NumberDummy(sympy.Dummy): Number dummies should never be created directly, but always via :meth:`Model.add_number()`. """ + # Sympy annoyingly overwrites __new__ def __new__(cls, value, *args, **kwargs): return super().__new__(cls, str(value)) @@ -649,6 +927,7 @@ class VariableDummy(sympy.Dummy): For the constructor arguments, see :meth:`Model.add_variable()`. """ + # Sympy annoyingly overwrites __new__ def __new__(cls, name, *args, **kwargs): return super().__new__(cls, name) @@ -661,7 +940,6 @@ def __init__(self, private_interface=None, order_added=None, cmeta_id=None): - self.name = name self.units = units self.initial_value = None if initial_value is None else float(initial_value) @@ -690,4 +968,3 @@ def __init__(self, def __str__(self): return self.name - diff --git a/tests/cellml_files/literals_for_conversion_tests.cellml b/tests/cellml_files/literals_for_conversion_tests.cellml new file mode 100644 index 00000000..9c3aa30a --- /dev/null +++ b/tests/cellml_files/literals_for_conversion_tests.cellml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + time + + sv1 + + 1 + + + + x + 1 + + + + y + + + 1 + x + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cellml_files/repeated_ode_for_conversion_tests.cellml b/tests/cellml_files/repeated_ode_for_conversion_tests.cellml new file mode 100644 index 00000000..84edcedd --- /dev/null +++ b/tests/cellml_files/repeated_ode_for_conversion_tests.cellml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + time + + sv1 + + 1 + + + + x + + + + + + time + + sv1 + + 3 + + + + + + + + time + + y + + 2 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cellml_files/repeated_ode_freevar_for_conversion_tests.cellml b/tests/cellml_files/repeated_ode_freevar_for_conversion_tests.cellml new file mode 100644 index 00000000..c9fc727d --- /dev/null +++ b/tests/cellml_files/repeated_ode_freevar_for_conversion_tests.cellml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + time + + sv1 + + 1 + + + + + + + time + + y + + 2 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cellml_files/silly_names.cellml b/tests/cellml_files/silly_names.cellml new file mode 100644 index 00000000..b88f3a25 --- /dev/null +++ b/tests/cellml_files/silly_names.cellml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + time + + sv1 + + 1 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_unit_conversion.py b/tests/test_unit_conversion.py index 5a5af30e..126b6581 100644 --- a/tests/test_unit_conversion.py +++ b/tests/test_unit_conversion.py @@ -1,80 +1,779 @@ import pytest +from pint import DimensionalityError +from cellmlmanip import units +from cellmlmanip.model import DataDirectionFlow from . import shared -def test_add_preferred_custom_unit_name(simple_ode_model): - """ Tests Units.add_preferred_custom_unit_name() function. """ - time_var = simple_ode_model.get_symbol_by_ontology_term(shared.OXMETA, "time") - assert str(simple_ode_model.units.summarise_units(time_var)) == "ms" - simple_ode_model.units.add_preferred_custom_unit_name('millisecond', [{'prefix': 'milli', 'units': 'second'}]) - assert str(simple_ode_model.units.summarise_units(time_var)) == "millisecond" - # add_custom_unit does not allow adding already existing units but add_preferred_custom_unit_name does since we - # cannot know in advance if a model will already have the unit named this way. To test this we add the same unit - # again - simple_ode_model.units.add_preferred_custom_unit_name('millisecond', [{'prefix': 'milli', 'units': 'second'}]) - assert str(simple_ode_model.units.summarise_units(time_var)) == "millisecond" - - -def test_conversion_factor_original(simple_units_model): - """ Tests Units.get_conversion_factor() function. """ - symbol_b1 = simple_units_model.get_symbol_by_cmeta_id("b_1") - equation = simple_units_model.get_equations_for([symbol_b1]) - factor = simple_units_model.units.get_conversion_factor( - quantity=1 * simple_units_model.units.summarise_units(equation[0].lhs), - to_unit=simple_units_model.units.ureg('us').units) - assert factor == 1000 - - -def test_conversion_factor_bad_types(simple_units_model): - """ Tests Units.get_conversion_factor() function for - cases when arguments are missing or incorrectly typed.""" - symbol_b1 = simple_units_model.get_symbol_by_cmeta_id("b_1") - equation = simple_units_model.get_equations_for([symbol_b1]) - expression = equation[0].lhs - to_unit = simple_units_model.units.ureg('us').units - from_unit = simple_units_model.units.summarise_units(expression) - quantity = 1 * from_unit - # no source unit - with pytest.raises(AssertionError, match='^No unit given as source.*'): - simple_units_model.units.get_conversion_factor(to_unit=to_unit) - with pytest.raises(AssertionError, match='^No unit given as source.*'): - simple_units_model.units.get_conversion_factor(to_unit) - - # no target unit - with pytest.raises(TypeError): - simple_units_model.units.get_conversion_factor(from_unit=from_unit) - # multiple sources - with pytest.raises(AssertionError, match='^Multiple target.*'): - simple_units_model.units.get_conversion_factor(to_unit, from_unit=from_unit, quantity=quantity) - # incorrect types - with pytest.raises(AssertionError, match='^from_unit must be of type pint:Unit$'): - simple_units_model.units.get_conversion_factor(to_unit, from_unit=quantity) - with pytest.raises(AssertionError, match='^quantity must be of type pint:Quantity$'): - simple_units_model.units.get_conversion_factor(to_unit, quantity=from_unit) - with pytest.raises(AssertionError, match='^expression must be of type Sympy expression$'): - simple_units_model.units.get_conversion_factor(to_unit, expression=quantity) - - # unit to unit - assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, from_unit=from_unit) == 1000 - # quantity to unit - assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, quantity=quantity) == 1000 - # expression to unit - assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, expression=expression) == 1000 - - -def test_conversion_factor_same_units(simple_units_model): - """ Tests Units.get_conversion_factor() function when units are same - and conversion factor should be '1'. """ - symbol_b = simple_units_model.get_symbol_by_cmeta_id("b") - equation = simple_units_model.get_equations_for([symbol_b]) - expression = equation[1].rhs - to_unit = simple_units_model.units.ureg('per_ms').units - from_unit = simple_units_model.units.summarise_units(expression) - quantity = 1 * from_unit - # quantity to unit - assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, quantity=quantity) == 1 - # unit to unit - assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, from_unit=from_unit) == 1 - # expression to unit - assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, expression=expression) == 1 +class TestUnitConversion: + ############################################################### + # fixtures + + @pytest.fixture + def local_model(scope='function'): + """ Fixture to load a local copy of the basic_ode model that may get modified. """ + return shared.load_model('basic_ode') + + @pytest.fixture + def literals_model(scope='function'): + """ Fixture to load a local copy of the basic_ode model that may get modified. """ + return shared.load_model('literals_for_conversion_tests') + + @pytest.fixture + def multiode_model(scope='function'): + """ Fixture to load a local copy of the basic_ode model that may get modified. """ + return shared.load_model('repeated_ode_for_conversion_tests') + + @pytest.fixture + def multiode_freevar_model(scope='function'): + """ Fixture to load a local copy of the basic_ode model that may get modified. """ + return shared.load_model('repeated_ode_freevar_for_conversion_tests') + + @pytest.fixture + def silly_names(scope='function'): + """ Fixture to load a local copy of the basic_ode model that may get modified. """ + return shared.load_model('silly_names') + + def test_conversion_factor_original(self, simple_units_model): + """ Tests Units.get_conversion_factor() function. """ + symbol_b1 = simple_units_model.get_symbol_by_cmeta_id("b_1") + equation = simple_units_model.get_equations_for([symbol_b1]) + factor = simple_units_model.units.get_conversion_factor( + quantity=1 * simple_units_model.units.summarise_units(equation[0].lhs), + to_unit=simple_units_model.units.ureg('us').units) + assert factor == 1000 + + def test_conversion_factor_bad_types(self, simple_units_model): + """ Tests Units.get_conversion_factor() function for + cases when arguments are missing or incorrectly typed.""" + symbol_b1 = simple_units_model.get_symbol_by_cmeta_id("b_1") + equation = simple_units_model.get_equations_for([symbol_b1]) + expression = equation[0].lhs + to_unit = simple_units_model.units.ureg('us').units + from_unit = simple_units_model.units.summarise_units(expression) + quantity = 1 * from_unit + # no source unit + with pytest.raises(AssertionError, match='^No unit given as source.*'): + simple_units_model.units.get_conversion_factor(to_unit=to_unit) + with pytest.raises(AssertionError, match='^No unit given as source.*'): + simple_units_model.units.get_conversion_factor(to_unit) + + # no target unit + with pytest.raises(TypeError): + simple_units_model.units.get_conversion_factor(from_unit=from_unit) + # multiple sources + with pytest.raises(AssertionError, match='^Multiple target.*'): + simple_units_model.units.get_conversion_factor(to_unit, from_unit=from_unit, quantity=quantity) + # incorrect types + with pytest.raises(AssertionError, match='^from_unit must be of type pint:Unit$'): + simple_units_model.units.get_conversion_factor(to_unit, from_unit=quantity) + with pytest.raises(AssertionError, match='^quantity must be of type pint:Quantity$'): + simple_units_model.units.get_conversion_factor(to_unit, quantity=from_unit) + with pytest.raises(AssertionError, match='^expression must be of type Sympy expression$'): + simple_units_model.units.get_conversion_factor(to_unit, expression=quantity) + + # unit to unit + assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, from_unit=from_unit) == 1000 + # quantity to unit + assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, quantity=quantity) == 1000 + # expression to unit + assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, expression=expression) == 1000 + + def test_conversion_factor_same_units(self, simple_units_model): + """ Tests Units.get_conversion_factor() function when units are same + and conversion factor should be '1'. """ + symbol_b = simple_units_model.get_symbol_by_cmeta_id("b") + equation = simple_units_model.get_equations_for([symbol_b]) + expression = equation[1].rhs + to_unit = simple_units_model.units.ureg('per_ms').units + from_unit = simple_units_model.units.summarise_units(expression) + quantity = 1 * from_unit + # quantity to unit + assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, quantity=quantity) == 1 + # unit to unit + assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, from_unit=from_unit) == 1 + # expression to unit + assert simple_units_model.units.get_conversion_factor(to_unit=to_unit, expression=expression) == 1 + + def test_bad_units(self, bad_units_model): + """ Tests units read and calculated from an inconsistent model. """ + symbol_a = bad_units_model.get_symbol_by_cmeta_id("a") + symbol_b = bad_units_model.get_symbol_by_cmeta_id("b") + equation = bad_units_model.get_equations_for([symbol_b], strip_units=False) + assert len(equation) == 2 + assert equation[0].lhs == symbol_a + assert bad_units_model.units.summarise_units(equation[0].lhs) == 'ms' + with pytest.raises(units.UnitError): + # cellml file states a (ms) = 1 (ms) + 1 (second) + bad_units_model.units.summarise_units(equation[0].rhs) + + assert equation[1].lhs == symbol_b + with pytest.raises(units.UnitError): + # cellml file states b (per_ms) = power(a (ms), 1 (second)) + bad_units_model.units.summarise_units(equation[1].rhs) + + # original state for local_model + def _original_state_local_model(self, local_model): + assert len(local_model.variables()) == 3 + symbol_a = local_model.get_symbol_by_cmeta_id('sv11') + symbol_t = local_model.get_symbol_by_cmeta_id('time') + assert local_model.get_initial_value(symbol_a) == 2.0 + assert symbol_a.units == 'mV' + assert symbol_t.units == 'ms' + assert len(local_model.equations) == 1 + assert str(local_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + state_symbols = local_model.get_state_symbols() + assert len(state_symbols) == 1 + assert symbol_a in state_symbols + return True + + # original state for literals_model + def _original_state_literals_model(self, literals_model): + assert len(literals_model.variables()) == 5 + symbol_a = literals_model.get_symbol_by_cmeta_id('sv11') + symbol_t = literals_model.get_symbol_by_cmeta_id('time') + symbol_x = literals_model.get_symbol_by_cmeta_id('current') + symbol_y = literals_model.get_symbol_by_name('env_ode$y') + assert symbol_x.name == 'env_ode$x' + assert literals_model.get_initial_value(symbol_a) == 2.0 + assert not literals_model.get_initial_value(symbol_t) + assert not literals_model.get_initial_value(symbol_x) + assert not literals_model.get_initial_value(symbol_y) + assert symbol_a.units == 'mV' + assert symbol_t.units == 'ms' + assert symbol_x.units == 'pA' + assert symbol_y.units == 'per_pA' + assert len(literals_model.equations) == 3 + assert str(literals_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + assert str(literals_model.equations[1]) == 'Eq(_env_ode$x, _1.0)' + assert str(literals_model.equations[2]) == 'Eq(_env_ode$y, _1.0/_env_ode$x)' + return True + + # original state for multiode_model + def _original_state_multiode_model(self, multiode_model): + assert len(multiode_model.variables()) == 5 + symbol_a = multiode_model.get_symbol_by_cmeta_id('sv11') + symbol_t = multiode_model.get_symbol_by_cmeta_id('time') + assert multiode_model.get_initial_value(symbol_a) == 2.0 + assert symbol_a.units == 'mV' + assert symbol_t.units == 'ms' + assert len(multiode_model.equations) == 3 + assert str(multiode_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + assert str(multiode_model.equations[1]) == 'Eq(_env_ode$x, ' \ + '_3.0*Derivative(_env_ode$sv1, _environment$time))' + assert str(multiode_model.equations[2]) == 'Eq(Derivative(_env_ode$y, _environment$time), _2.0)' + return True + + # original state + def _original_state_multiode_freevar(self, multiode_freevar_model): + assert len(multiode_freevar_model.variables()) == 4 + symbol_a = multiode_freevar_model.get_symbol_by_cmeta_id('sv11') + symbol_t = multiode_freevar_model.get_symbol_by_cmeta_id('time') + symbol_y = multiode_freevar_model.get_symbol_by_name('env_ode$y') + assert multiode_freevar_model.get_initial_value(symbol_a) == 2.0 + assert multiode_freevar_model.get_initial_value(symbol_y) == 3.0 + assert symbol_a.units == 'mV' + assert symbol_t.units == 'ms' + assert symbol_y.units == 'mV' + assert len(multiode_freevar_model.equations) == 2 + assert str(multiode_freevar_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + assert str(multiode_freevar_model.equations[1]) == 'Eq(Derivative(_env_ode$y, _environment$time), _2.0)' + return True + + def test_add_input_state_variable(self, local_model): + """ Tests the Model.convert_variable function that changes units. + This particular test is when a state variable is being converted + + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + + ode(sv1, time) = 1{mV_per_ms}; + + convert_variable(sv1, volt, DataDirectionFlow.INPUT) + + creates model + var{time} time: ms {pub: in}; + var{sv11} sv1_converted: V {init: 0.002}; + var sv1 mV + var sv1_orig_deriv mV_per_ms + + sv1 = 1000 * sv1_converted + sv1_orig_deriv = 1{mV_per_ms} + ode(sv1_converted, time) = 0.001 * sv1_orig_deriv + """ + mV_unit = local_model.get_units('mV') + volt_unit = local_model.get_units('volt') + original_var = local_model.get_symbol_by_name('env_ode$sv1') + + assert self._original_state_local_model(local_model) + # test no change in units + newvar = local_model.convert_variable(original_var, mV_unit, DataDirectionFlow.INPUT) + assert newvar == original_var + assert self._original_state_local_model(local_model) + + # change mV to V + newvar = local_model.convert_variable(original_var, volt_unit, DataDirectionFlow.INPUT) + assert newvar != original_var + assert len(local_model.variables()) == 5 + symbol_a = local_model.get_symbol_by_cmeta_id('sv11') + assert local_model.get_initial_value(symbol_a) == 0.002 + assert symbol_a.units == 'volt' + assert symbol_a.name == 'env_ode$sv1_converted' + symbol_t = local_model.get_symbol_by_cmeta_id('time') + assert symbol_t.units == 'ms' + symbol_orig = local_model.get_symbol_by_name('env_ode$sv1') + assert symbol_orig.units == 'mV' + assert not local_model.get_initial_value(symbol_orig) + symbol_derv = local_model.get_symbol_by_name('env_ode$sv1_orig_deriv') + assert symbol_derv.units == 'mV / ms' + assert not local_model.get_initial_value(symbol_derv) + assert len(local_model.equations) == 3 + assert str(local_model.equations[0]) == 'Eq(_env_ode$sv1, 1000.0*_env_ode$sv1_converted)' + assert str(local_model.equations[1]) == 'Eq(_env_ode$sv1_orig_deriv, _1.0)' + assert str(local_model.equations[2]) == 'Eq(Derivative(_env_ode$sv1_converted, _environment$time), ' \ + '0.001*_env_ode$sv1_orig_deriv)' + + state_symbols = local_model.get_state_symbols() + assert len(state_symbols) == 1 + assert symbol_a in state_symbols + + def test_add_input_free_variable(self, local_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This particular case tests changing a free variable + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + + ode(sv1, time) = 1{mV_per_ms}; + + convert_variable(time, second, DataDirectionFlow.INPUT) + + becomes + var time: ms; + var{time} time_converted: s; + var{sv11} sv1: mV {init: 2}; + var sv1_orig_deriv mV_per_ms + + time = 1000 * time_converted; + sv1_orig_deriv = 1{mV_per_ms} + ode(sv1, time_converted) = 1000 * sv1_orig_deriv + """ + ms_unit = local_model.get_units('ms') + second_unit = local_model.get_units('second') + original_var = local_model.get_symbol_by_name('environment$time') + + assert self._original_state_local_model(local_model) + # test no change in units + local_model.convert_variable(original_var, ms_unit, DataDirectionFlow.INPUT) + assert self._original_state_local_model(local_model) + + # change ms to s + local_model.convert_variable(original_var, second_unit, DataDirectionFlow.INPUT) + assert len(local_model.variables()) == 5 + symbol_a = local_model.get_symbol_by_cmeta_id('sv11') + assert local_model.get_initial_value(symbol_a) == 2.0 + assert symbol_a.units == 'mV' + assert symbol_a.name == 'env_ode$sv1' + symbol_t = local_model.get_symbol_by_cmeta_id('time') + assert symbol_t.units == 'second' + assert symbol_t.name == 'environment$time_converted' + symbol_orig = local_model.get_symbol_by_name('env_ode$sv1') + assert symbol_orig.units == 'mV' + symbol_derv = local_model.get_symbol_by_name('env_ode$sv1_orig_deriv') + assert symbol_derv.units == 'mV / ms' + assert len(local_model.equations) == 3 + assert str(local_model.equations[0]) == 'Eq(_environment$time, 1000.0*_environment$time_converted)' + assert str(local_model.equations[1]) == 'Eq(_env_ode$sv1_orig_deriv, _1.0)' + assert str(local_model.equations[2]) == 'Eq(Derivative(_env_ode$sv1, _environment$time_converted), ' \ + '1000.0*_env_ode$sv1_orig_deriv)' + + def test_add_input_literal_variable(self, literals_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This particular case tests changing a literal variable/constant + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + var{current} x: pA; + var y: per_pA; + + ode(sv1, time) = 1{mV_per_ms}; + x = 1{pA}; + y = 1{dimensionless}/x; + + convert_variable(x, nA, DataDirectionFlow.INPUT) + + becomes + var time: ms; + var{sv11} sv1: mV {init: 2}; + var{current} x_converted: nA; + var x : pA + var y: per_pA; + + ode(sv1, time) = 1{mV_per_ms}; + x_converted = 0.001 * 1{pA} + x = 1000* x_converted; + y = 1{dimensionless}/x; + """ + pA_unit = literals_model.get_units('pA') + nA_unit = literals_model.get_units('nA') + original_var = literals_model.get_symbol_by_name('env_ode$x') + + assert self._original_state_literals_model(literals_model) + # test no change in units + literals_model.convert_variable(original_var, pA_unit, DataDirectionFlow.INPUT) + assert self._original_state_literals_model(literals_model) + + # change pA to nA + literals_model.convert_variable(original_var, nA_unit, DataDirectionFlow.INPUT) + assert len(literals_model.variables()) == 6 + symbol_a = literals_model.get_symbol_by_cmeta_id('sv11') + symbol_t = literals_model.get_symbol_by_cmeta_id('time') + symbol_x = literals_model.get_symbol_by_cmeta_id('current') + assert symbol_x.name == 'env_ode$x_converted' + symbol_y = literals_model.get_symbol_by_name('env_ode$y') + symbol_x_orig = literals_model.get_symbol_by_name('env_ode$x') + assert literals_model.get_initial_value(symbol_a) == 2.0 + assert not literals_model.get_initial_value(symbol_t) + assert not literals_model.get_initial_value(symbol_x) + assert not literals_model.get_initial_value(symbol_y) + assert not literals_model.get_initial_value(symbol_x_orig) + assert symbol_a.units == 'mV' + assert symbol_t.units == 'ms' + assert symbol_x.units == 'nA' + assert symbol_y.units == 'per_pA' + assert symbol_x_orig.units == 'pA' + assert len(literals_model.equations) == 4 + assert str(literals_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + assert str(literals_model.equations[1]) == 'Eq(_env_ode$y, _1.0/_env_ode$x)' + assert str(literals_model.equations[2]) == 'Eq(_env_ode$x_converted, 0.001*_1.0)' + assert str(literals_model.equations[3]) == 'Eq(_env_ode$x, 1000.0*_env_ode$x_converted)' + + def test_add_input_free_variable_multiple(self, multiode_freevar_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This particular case tests changing a free variable where there are multiple ode instances. + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + var y: mV {init: 3}; + + ode(sv1, time) = 1{mV_per_ms}; + ode(y, time) = 2{mV_per_ms} + + convert_variable(time, second, DataDirectionFlow.INPUT) + + becomes + var time: ms; + var{time} time_converted: s; + var{sv11} sv1: mV {init: 2}; + var sv1_orig_deriv mV_per_ms + var y : mV {init: 3} + var y_orig_deriv mV_per_ms + + time = 1000 * time_converted; + sv1_orig_deriv = 1{mV_per_ms} + ode(sv1, time_converted) = 1000 * sv1_orig_deriv + y_orig_deriv = 2{mV_per_ms} + ode(y, time_converted) = 1000 * y_orig_deriv + """ + ms_unit = multiode_freevar_model.get_units('ms') + second_unit = multiode_freevar_model.get_units('second') + original_var = multiode_freevar_model.get_symbol_by_name('environment$time') + + assert self._original_state_multiode_freevar(multiode_freevar_model) + # test no change in units + multiode_freevar_model.convert_variable(original_var, ms_unit, DataDirectionFlow.INPUT) + assert self._original_state_multiode_freevar(multiode_freevar_model) + + # change ms to s + multiode_freevar_model.convert_variable(original_var, second_unit, DataDirectionFlow.INPUT) + assert len(multiode_freevar_model.variables()) == 7 + symbol_a = multiode_freevar_model.get_symbol_by_cmeta_id('sv11') + assert multiode_freevar_model.get_initial_value(symbol_a) == 2.0 + assert symbol_a.units == 'mV' + assert symbol_a.name == 'env_ode$sv1' + symbol_t = multiode_freevar_model.get_symbol_by_cmeta_id('time') + assert symbol_t.units == 'second' + assert symbol_t.name == 'environment$time_converted' + symbol_orig = multiode_freevar_model.get_symbol_by_name('env_ode$sv1') + assert symbol_orig.units == 'mV' + symbol_derv = multiode_freevar_model.get_symbol_by_name('env_ode$sv1_orig_deriv') + assert symbol_derv.units == 'mV / ms' + symbol_orig_y = multiode_freevar_model.get_symbol_by_name('env_ode$y') + assert symbol_orig_y.units == 'mV' + assert symbol_orig_y.initial_value == 3.0 + symbol_derv_y = multiode_freevar_model.get_symbol_by_name('env_ode$y_orig_deriv') + assert symbol_derv_y.units == 'mV / ms' + assert len(multiode_freevar_model.equations) == 5 + assert str(multiode_freevar_model.equations[0]) == 'Eq(_environment$time, 1000.0*_environment$time_converted)' + assert str(multiode_freevar_model.equations[1]) == 'Eq(_env_ode$sv1_orig_deriv, _1.0)' + assert str(multiode_freevar_model.equations[2]) == 'Eq(Derivative(_env_ode$sv1, ' \ + '_environment$time_converted), ' \ + '1000.0*_env_ode$sv1_orig_deriv)' + assert str(multiode_freevar_model.equations[3]) == 'Eq(_env_ode$y_orig_deriv, _2.0)' + assert str(multiode_freevar_model.equations[4]) == 'Eq(Derivative(_env_ode$y, _environment$time_converted), ' \ + '1000.0*_env_ode$y_orig_deriv)' + + def test_multiple_odes(self, multiode_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This tests that any other uses of the derivative on the rhs of other equations + are replaced with the new variable representing the old derivative + For example:: + + Original model + var time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + var x: mV_per_ms; + var y: mV {init: 3}; + + ode(sv1, time) = 1{mV_per_ms}; + x = ode(sv1, time) * 3{mV_per_ms}; + ode(y, time) = 2{mV_per_ms}; + + convert_variable(sv1, volt, DataDirectionFlow.INPUT) + + becomes + var{time} time: ms {pub: in}; + var{sv11} sv1_converted: V {init: 0.002}; + var sv1 mV {init: 2} + var sv1_orig_deriv mV_per_ms + var x: mV_per_ms; + var y: mV {init: 3}; + var y_orig_deriv + + ode(y, time) = 2{mv_per_ms}; + sv1 = 1000 * sv1_converted + sv1_orig_deriv = 1{mV_per_ms} + ode(sv1_converted, time) = 0.001 * sv1_orig_deriv + x = 3 * sv1_orig_deriv + """ + mV_unit = multiode_model.get_units('mV') + volt_unit = multiode_model.get_units('volt') + original_var = multiode_model.get_symbol_by_name('env_ode$sv1') + + assert self._original_state_multiode_model(multiode_model) + # test no change in units + multiode_model.convert_variable(original_var, mV_unit, DataDirectionFlow.INPUT) + assert self._original_state_multiode_model(multiode_model) + + # change mV to V + multiode_model.convert_variable(original_var, volt_unit, DataDirectionFlow.INPUT) + assert len(multiode_model.equations) == 5 + assert str(multiode_model.equations[0]) == 'Eq(Derivative(_env_ode$y, _environment$time), _2.0)' + assert str(multiode_model.equations[1]) == 'Eq(_env_ode$sv1, 1000.0*_env_ode$sv1_converted)' + assert str(multiode_model.equations[2]) == 'Eq(_env_ode$sv1_orig_deriv, _1.0)' + assert str(multiode_model.equations[3]) == 'Eq(Derivative(_env_ode$sv1_converted, _environment$time), ' \ + '0.001*_env_ode$sv1_orig_deriv)' + assert str(multiode_model.equations[4]) == 'Eq(_env_ode$x, _3.0*_env_ode$sv1_orig_deriv)' + + def test_multiple_odes_1(self, multiode_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This tests that any other uses of the derivative on the rhs of other equations + are replaced with the new variable representing the old derivative + For example:: + + Original model + var time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + var x: mV_per_ms; + var y: mV {init: 3}; + + ode(sv1, time) = 1{mV_per_ms}; + x = ode(sv1, time) * 3{mV_per_ms}; + ode(y, time) = 2{mV_per_ms}; + + + convert_variable(time, second, DataDirectionFlow.INPUT) + + becomes + var time: ms; + var{time} time_converted: s; + var{sv11} sv1: mV {init: 2}; + var sv1_orig_deriv mV_per_ms + var x: mV_per_ms; + var y: mV {init: 3}; + var y_orig_deriv: {mv_per_ms} + + time_converted = 1000 * time + y_orig_deriv = 2{mV_per_ms} + ode(y, time_converted) = 1000 * y_orig_deriv; + sv1_orig_deriv = 1{mV_per_ms} + ode(sv1, time_converted) = 1000 * sv1_orig_deriv + x = 3 * sv1_orig_deriv + """ + ms_unit = multiode_model.get_units('ms') + second_unit = multiode_model.get_units('second') + original_var = multiode_model.get_symbol_by_name('environment$time') + + assert self._original_state_multiode_model(multiode_model) + # test no change in units + multiode_model.convert_variable(original_var, ms_unit, DataDirectionFlow.INPUT) + assert self._original_state_multiode_model(multiode_model) + + # change ms to s + multiode_model.convert_variable(original_var, second_unit, DataDirectionFlow.INPUT) + assert len(multiode_model.equations) == 6 + assert str(multiode_model.equations[0]) == 'Eq(_environment$time, 1000.0*_environment$time_converted)' + assert str(multiode_model.equations[1]) == 'Eq(_env_ode$sv1_orig_deriv, _1.0)' + assert str(multiode_model.equations[2]) == 'Eq(Derivative(_env_ode$sv1, _environment$time_converted), ' \ + '1000.0*_env_ode$sv1_orig_deriv)' + assert str(multiode_model.equations[3]) == 'Eq(_env_ode$y_orig_deriv, _2.0)' + assert str(multiode_model.equations[4]) == 'Eq(Derivative(_env_ode$y, _environment$time_converted), ' \ + '1000.0*_env_ode$y_orig_deriv)' + assert str(multiode_model.equations[5]) == 'Eq(_env_ode$x, _3.0*_env_ode$sv1_orig_deriv)' + + def test_add_output(self, literals_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This tests the case when variable to be changed is an OUTPUT + + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + var{current} x: pA; + var y: per_pA; + + ode(sv1, time) = 1{mV_per_ms}; + x = 1{pA}; + y = 1{dimensionless}/x; + + convert_variable(x, nA, DataDirectionFlow.OUTPUT) + + becomes + var time: ms; + var{sv11} sv1: mV {init: 2}; + var{current} x_converted: nA; + var x : pA + var y: per_pA; + + ode(sv1, time) = 1{mV_per_ms}; + x = 1{pA} + y = 1{dimensionless}/x; + x_converted = 0.001 * x + """ + pA_unit = literals_model.get_units('pA') + nA_unit = literals_model.get_units('nA') + original_var = literals_model.get_symbol_by_name('env_ode$x') + + assert self._original_state_literals_model(literals_model) + # test no change in units + literals_model.convert_variable(original_var, pA_unit, DataDirectionFlow.OUTPUT) + assert self._original_state_literals_model(literals_model) + + # change pA to nA + literals_model.convert_variable(original_var, nA_unit, DataDirectionFlow.OUTPUT) + assert len(literals_model.variables()) == 6 + symbol_a = literals_model.get_symbol_by_cmeta_id('sv11') + symbol_t = literals_model.get_symbol_by_cmeta_id('time') + symbol_x = literals_model.get_symbol_by_cmeta_id('current') + assert symbol_x.name == 'env_ode$x_converted' + symbol_y = literals_model.get_symbol_by_name('env_ode$y') + symbol_x_orig = literals_model.get_symbol_by_name('env_ode$x') + assert literals_model.get_initial_value(symbol_a) == 2.0 + assert not literals_model.get_initial_value(symbol_t) + assert not literals_model.get_initial_value(symbol_x) + assert not literals_model.get_initial_value(symbol_y) + assert not literals_model.get_initial_value(symbol_x_orig) + assert symbol_a.units == 'mV' + assert symbol_t.units == 'ms' + assert symbol_x.units == 'nA' + assert symbol_y.units == 'per_pA' + assert symbol_x_orig.units == 'pA' + assert len(literals_model.equations) == 4 + assert str(literals_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + assert str(literals_model.equations[1]) == 'Eq(_env_ode$x, _1.0)' + assert str(literals_model.equations[2]) == 'Eq(_env_ode$y, _1.0/_env_ode$x)' + assert str(literals_model.equations[3]) == 'Eq(_env_ode$x_converted, 0.001*_env_ode$x)' + state_symbols = literals_model.get_state_symbols() + assert len(state_symbols) == 1 + assert symbol_a in state_symbols + + def test_add_output_state_variable(self, local_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This particular test is when a state variable is being converted as an output + + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + + ode(sv1, time) = 1{mV_per_ms}; + + convert sv11 from mV to V + + creates model + var{time} time: ms {pub: in}; + var{sv11} sv1_converted: V; + var sv1 mV {init: 2} + + ode(sv1, time) = 1{mV_per_ms}; + sv1_converted = sv1 / 1000 + """ + mV_unit = local_model.get_units('mV') + volt_unit = local_model.get_units('volt') + original_var = local_model.get_symbol_by_name('env_ode$sv1') + + assert self._original_state_local_model(local_model) + # test no change in units + local_model.convert_variable(original_var, mV_unit, DataDirectionFlow.OUTPUT) + assert self._original_state_local_model(local_model) + + # change mV to V + local_model.convert_variable(original_var, volt_unit, DataDirectionFlow.OUTPUT) + assert len(local_model.variables()) == 4 + symbol_a = local_model.get_symbol_by_cmeta_id('sv11') + assert not local_model.get_initial_value(symbol_a) + assert symbol_a.units == 'volt' + assert symbol_a.name == 'env_ode$sv1_converted' + symbol_t = local_model.get_symbol_by_cmeta_id('time') + assert symbol_t.units == 'ms' + symbol_orig = local_model.get_symbol_by_name('env_ode$sv1') + assert symbol_orig.units == 'mV' + assert local_model.get_initial_value(symbol_orig) == 2.0 + assert len(local_model.equations) == 2 + assert str(local_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + assert str(local_model.equations[1]) == 'Eq(_env_ode$sv1_converted, 0.001*_env_ode$sv1)' + state_symbols = local_model.get_state_symbols() + assert len(state_symbols) == 1 + assert symbol_orig in state_symbols + + def test_add_output_free_variable(self, local_model): + """ Tests the Model.convert_variable function that changes units of given variable. + This particular test is when a free variable is being converted as an output + + For example:: + + Original model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + + ode(sv1, time) = 1{mV_per_ms}; + + convert time from ms to s + + creates model + var{time} time: ms {pub: in}; + var{sv11} sv1: mV {init: 2}; + var{time} time_converted: s + + ode(sv1, time) = 1{mV_per_ms}; + time_converted = 0.001 * time + """ + + ms_unit = local_model.get_units('ms') + second_unit = local_model.get_units('second') + original_var = local_model.get_symbol_by_name('environment$time') + + assert self._original_state_local_model(local_model) + # test no change in units + local_model.convert_variable(original_var, ms_unit, DataDirectionFlow.OUTPUT) + assert self._original_state_local_model(local_model) + + # change ms to s + local_model.convert_variable(original_var, second_unit, DataDirectionFlow.OUTPUT) + assert len(local_model.variables()) == 4 + symbol_a = local_model.get_symbol_by_cmeta_id('sv11') + assert local_model.get_initial_value(symbol_a) == 2.0 + assert symbol_a.units == 'mV' + assert symbol_a.name == 'env_ode$sv1' + symbol_t = local_model.get_symbol_by_cmeta_id('time') + assert symbol_t.units == 'second' + assert symbol_t.name == 'environment$time_converted' + symbol_orig = local_model.get_symbol_by_name('environment$time') + assert symbol_orig.units == 'ms' + assert len(local_model.equations) == 2 + assert str(local_model.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + assert str(local_model.equations[1]) == 'Eq(_environment$time_converted, 0.001*_environment$time)' + state_symbols = local_model.get_state_symbols() + assert len(state_symbols) == 1 + assert symbol_a in state_symbols + assert local_model.get_free_variable_symbol() == symbol_orig + + def test_convert_variable_invalid_arguments(self, local_model): + """ Tests the Model.convert_variable() function when involid arguments are passed. + """ + unit = local_model.get_units('second') + variable = local_model.get_free_variable_symbol() + direction = DataDirectionFlow.INPUT + bad_unit = local_model.get_units('mV') + + # arguments wrong types + with pytest.raises(AssertionError): + local_model.convert_variable('x', unit, direction) + + with pytest.raises(AssertionError): + local_model.convert_variable(variable, 'x', direction) + + with pytest.raises(AssertionError): + local_model.convert_variable(variable, unit, 'x') + + # variable not present in model + model = shared.load_model('literals_for_conversion_tests') + other_var = model.get_symbol_by_name('env_ode$x') + with pytest.raises(AssertionError): + local_model.convert_variable(other_var, unit, direction) + + # ontology term not present in model + with pytest.raises(AssertionError): + local_model.convert_variable('current', unit, direction) + + # unit conversion is impossible + with pytest.raises(DimensionalityError): + local_model.convert_variable(variable, bad_unit, direction) + + def test_unique_names(self, silly_names): + # original state + def test_original_state(silly_names): + assert len(silly_names.variables()) == 5 + symbol_a = silly_names.get_symbol_by_cmeta_id('sv11') + symbol_t = silly_names.get_symbol_by_cmeta_id('time') + assert silly_names.get_initial_value(symbol_a) == 2.0 + assert symbol_a.units == 'mV' + assert symbol_t.units == 'ms' + assert silly_names.get_symbol_by_name('env_ode$sv1_converted') + assert silly_names.get_symbol_by_name('env_ode$sv1_orig_deriv') + assert len(silly_names.equations) == 1 + assert str(silly_names.equations[0]) == 'Eq(Derivative(_env_ode$sv1, _environment$time), _1.0)' + state_symbols = silly_names.get_state_symbols() + assert len(state_symbols) == 1 + assert symbol_a in state_symbols + assert silly_names.get_free_variable_symbol() == symbol_t + return True + + volt_unit = silly_names.get_units('volt') + original_var = silly_names.get_symbol_by_name('env_ode$sv1') + + assert test_original_state(silly_names) + # change mV to V + silly_names.convert_variable(original_var, volt_unit, DataDirectionFlow.INPUT) + assert len(silly_names.variables()) == 7 + symbol_a = silly_names.get_symbol_by_cmeta_id('sv11') + assert silly_names.get_initial_value(symbol_a) == 0.002 + assert symbol_a.units == 'volt' + assert symbol_a.name == 'env_ode$sv1_converted_a' + symbol_t = silly_names.get_symbol_by_cmeta_id('time') + assert symbol_t.units == 'ms' + assert symbol_t.name == 'environment$time' + symbol_orig = silly_names.get_symbol_by_name('env_ode$sv1') + assert symbol_orig.units == 'mV' + assert silly_names.get_symbol_by_name('env_ode$sv1_converted') + assert silly_names.get_symbol_by_name('env_ode$sv1_orig_deriv') + symbol_derv = silly_names.get_symbol_by_name('env_ode$sv1_orig_deriv_a') + assert symbol_derv.units == 'mV / ms' + assert not silly_names.get_initial_value(symbol_derv) + assert len(silly_names.equations) == 3 + assert str(silly_names.equations[0]) == 'Eq(_env_ode$sv1, 1000.0*_env_ode$sv1_converted_a)' + assert str(silly_names.equations[1]) == 'Eq(_env_ode$sv1_orig_deriv_a, _1.0)' + assert str(silly_names.equations[2]) == 'Eq(Derivative(_env_ode$sv1_converted_a, _environment$time), ' \ + '0.001*_env_ode$sv1_orig_deriv_a)' diff --git a/tests/test_units.py b/tests/test_units.py index 76bcd4b5..9a9deaca 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -165,6 +165,18 @@ def test_make_pint_unit_definition(self, quantity_store): assert(quantity_store._make_pint_unit_definition('kg_mm_per_ms', unit_attributes) == 'kg_mm_per_ms=(meter * 1e-3)*(((second * 0.001))**-1)*(2 * kilogram)') + def test_add_preferred_custom_unit_name(self, simple_ode_model): + """ Tests Units.add_preferred_custom_unit_name() function. """ + time_var = simple_ode_model.get_symbol_by_ontology_term(shared.OXMETA, "time") + assert str(simple_ode_model.units.summarise_units(time_var)) == "ms" + simple_ode_model.units.add_preferred_custom_unit_name('millisecond', [{'prefix': 'milli', 'units': 'second'}]) + assert str(simple_ode_model.units.summarise_units(time_var)) == "millisecond" + # add_custom_unit does not allow adding already existing units but add_preferred_custom_unit_name does since we + # cannot know in advance if a model will already have the unit named this way. To test this we add the same unit + # again + simple_ode_model.units.add_preferred_custom_unit_name('millisecond', [{'prefix': 'milli', 'units': 'second'}]) + assert str(simple_ode_model.units.summarise_units(time_var)) == "millisecond" + # Test UnitCalculator class def test_unit_calculator(self, quantity_store):