Repository: ModellingWebLab/cellmlmanip

Issue:
Rename/move some Model functions
1. Model. add_number()
This function merely creates a NumberDummy object. It is completely independent of the Model and adds nothing to anything. I think the name could be misleading.

I would prefer: create_number_symbol 

2. Model.check_left_right_units_equal(sympy.Equality)
This functions should be in the one of the unit classes (UnitCalculator); although that would involve passing the model/model units as an argument - so I could let it stay; but it does feel wrong. 

3. Model.find_symbols_and_derivatives(expression)
This creates a set of symbols from the list of expressions passed to it. It does not find symbols/derivatives that are in the model. Again possible misleading.  Also not sure this belongs on Model class (same as add_number) they are helper functions really.

Discussion:

Patch:
diff --git a/cellmlmanip/model.py b/cellmlmanip/model.py
index 743e0fe..cdfc23c 100644
--- a/cellmlmanip/model.py
+++ b/cellmlmanip/model.py
@@ -32,7 +32,7 @@ class Model(object):
     graph that stores further meta data about the model.
 
     Equations are stored as :class:`sympy.Eq` objects, but with the caveat that all variables and numbers must be
-    specified using the :class:`sympy.Dummy` objects returned by :meth:`add_variable()` and :meth:`add_number()`.
+    specified using the :class:`sympy.Dummy` objects returned by :meth:`add_variable()` and :meth:`create_quantity()`.
 
     Units are handled using the ``units`` property of a model, which is an instance of
     :class:`cellmlmanip.units.UnitStore`.
@@ -64,10 +64,10 @@ class Model(object):
         else:
             self.units = UnitStore()
 
-        # Maps string variable names to VariableDummy objects
+        # Maps string variable names to Variable objects
         self._name_to_variable = {}
 
-        # Maps cmeta ids to VariableDummy objects
+        # Maps cmeta ids to Variable objects
         self._cmeta_id_to_variable = {}
 
         # Cached nx.DiGraph of this model's equations, with number dummies or with sympy.Number objects
@@ -77,10 +77,10 @@ class Model(object):
         # An RDF graph containing further meta data
         self.rdf = rdflib.Graph()
 
-        # Map from VariableDummy to defining equation, where the variable is defined by a simple equation
+        # Map from Variable to defining equation, where the variable is defined by a simple equation
         self._var_definition_map = {}
 
-        # Map from VariableDummy to defining equation, where the variable is defined by an ODE
+        # Map from Variable to defining equation, where the variable is defined by an ODE
         self._ode_definition_map = {}
 
     ####################################################################################################
@@ -153,7 +153,7 @@ class Model(object):
         variable references.
         """
         defn = self._var_definition_map.get(variable)
-        return defn is not None and len(defn.rhs.atoms(VariableDummy)) == 0
+        return defn is not None and len(defn.rhs.atoms(Variable)) == 0
 
     def get_variable_by_name(self, name):
         """Returns the variable with the given ``name``."""
@@ -166,7 +166,7 @@ class Model(object):
         To get variables from e.g. an oxmeta ontology term, use :meth:`get_variable_by_ontology_term`.
 
         :param cmeta_id: Either a string id or :class:`rdflib.URIRef` instance.
-        :returns: A :class:`VariableDummy` object
+        :returns: A :class:`Variable` object
         """
 
         # Get cmeta_id from URIRef
@@ -260,7 +260,7 @@ class Model(object):
         Returns all ontology terms linked to the given ``variable`` via the
         http://biomodels.net/biology-qualifiers/is predicate.
 
-        :param variable: The variable to search for (as a :class:`VariableDummy` object).
+        :param variable: The variable to search for (as a :class:`Variable` object).
         :param namespace_uri: An optional namespace URI. If given, only terms within the given namespace will be
             returned.
         :returns: A list of term local names.
@@ -298,7 +298,7 @@ class Model(object):
     def get_definition(self, variable):
         """Get the equation (if any) defining the given variable.
 
-        :param variable: The variable to look up (as a :class:`VariableDummy`). If this appears as the LHS of a straight
+        :param variable: The variable to look up (as a :class:`Variable`). If this appears as the LHS of a straight
             assignment, or the state variable in an ODE, the corresponding equation will be returned.
         :returns: A Sympy equation, or ``None`` if the variable is not defined by an equation.
         """
@@ -316,9 +316,9 @@ class Model(object):
 
         Results are sorted first by dependencies, then by variable name.
 
-        :param variables: The variables to get the equations for (as :class:`VariableDummy` objects).
+        :param variables: The variables to get the equations for (as :class:`Variable` objects).
         :param recurse: Indicates whether to recurse the equation graph, or to return only the top level equations.
-        :param strip_units: If ``True``, all :class:`NumberDummy` objects representing number with units will be
+        :param strip_units: If ``True``, all :class:`Quantity` objects representing number with units will be
             replaced with ordinary sympy number objects.
         """
         # Get graph
@@ -386,7 +386,7 @@ class Model(object):
                 # Get the free symbol and update the variable information
                 free_symbol = lhs.variables[0]
                 free_symbol.type = VariableType.FREE
-            elif isinstance(equation.rhs, NumberDummy):
+            elif isinstance(equation.rhs, Quantity):
                 lhs.type = VariableType.PARAMETER
             else:
                 lhs.type = VariableType.COMPUTED
@@ -437,7 +437,7 @@ class Model(object):
     def graph_with_sympy_numbers(self):
         """
         A :class:`networkx.DiGraph` containing the model equations,
-        but with numbers represented as :class:`sympy.Number` objects instead of :class:`NumberDummy`.
+        but with numbers represented as :class:`sympy.Number` objects instead of :class:`Quantity`.
         """
         if self._graph_with_sympy_numbers is not None:
             return self._graph_with_sympy_numbers
@@ -452,7 +452,7 @@ class Model(object):
                 continue
 
             # Get all the dummy numbers on the RHS, and prepare substitution map
-            dummies = equation.rhs.atoms(NumberDummy)
+            dummies = equation.rhs.atoms(Quantity)
             subs_dict = {d: d.evalf(FLOAT_PRECISION) for d in dummies}
 
             # And replace the equation with one with the rhs subbed with sympy.Number objects
@@ -493,7 +493,7 @@ class Model(object):
     def add_variable(self, name, units, initial_value=None, public_interface=None, private_interface=None,
                      cmeta_id=None):
         """
-        Adds a variable to the model and returns a :class:`VariableDummy` to represent it in sympy expressions.
+        Adds a variable to the model and returns a :class:`Variable` to represent it in sympy expressions.
 
         :param name: A string name.
         :param units: A string unit name or a :class:`~cellmlmanip.units.UnitStore.Unit` object.
@@ -502,7 +502,7 @@ class Model(object):
         :param private_interface: An optional private interface specifier (only required when parsing CellML).
         :param cmeta_id: An optional string specifying a cmeta:id
         :raises ValueError: If a variable with that name already exists, or the given cmeta_id is already taken.
-        :return: A :class:`VariableDummy` object.
+        :return: A :class:`Variable` object.
         """
         # Check for clashes
         if name in self._name_to_variable:
@@ -517,7 +517,7 @@ class Model(object):
             units = self.units.get_unit(units)
 
         # Add variable
-        self._name_to_variable[name] = var = VariableDummy(
+        self._name_to_variable[name] = var = Variable(
             name=name,
             units=units,
             model=self,
@@ -543,7 +543,7 @@ class Model(object):
         This will remove the equation either that defines ``variable`` directly, or if it is a state variable the
         corresponding ODE. All annotations about this variable are also removed from the RDF graph.
 
-        :param VariableDummy variable: the variable to remove
+        :param Variable variable: the variable to remove
         """
         # Remove defining equation
         defn = self.get_definition(variable)
@@ -566,11 +566,11 @@ class Model(object):
         """
         Adds an equation to this model.
 
-        The left-hand side (LHS) of the equation must be either a variable (as a :class:`VariableDummy`) or a derivative
+        The left-hand side (LHS) of the equation must be either a variable (as a :class:`Variable`) or a derivative
         (as a :class:`sympy.Derivative`).
 
         All numbers and variables used in the equation must have been obtained from this model, e.g. via
-        :meth:`add_number()`, :meth:`add_variable()`, or :meth:`get_variable_by_ontology_term()`.
+        :meth:`create_quantity()`, :meth:`add_variable()`, or :meth:`get_variable_by_ontology_term()`.
 
         :param equation: A :class:`sympy.Eq` object.
         :param check_duplicates: whether to check that the equation's LHS is not already defined
@@ -586,7 +586,7 @@ class Model(object):
             if check_duplicates:
                 self._check_duplicate_definitions(state_var, equation)
             self._ode_definition_map[state_var] = equation
-        elif isinstance(lhs, VariableDummy):
+        elif isinstance(lhs, Variable):
             if check_duplicates:
                 self._check_duplicate_definitions(lhs, equation)
             self._var_definition_map[lhs] = equation
@@ -615,21 +615,24 @@ class Model(object):
         # Invalidate cached equation graphs
         self._invalidate_cache()
 
-    def add_number(self, value, units):
+    def create_quantity(self, value, units):
         """
-        Creates and returns a :class:`NumberDummy` to represent a number with units in sympy expressions.
+        Creates and returns a :class:`Quantity` to represent a number with units in sympy expressions.
+
+        Use this method rather than creating quantities directly to ensure their units are compatible with those used by
+        the model, so unit conversion etc. works.
 
         :param number: A number (anything convertible to float).
         :param units: A string unit name or a :class:`~cellmlmanip.units.UnitStore.Unit` object.
 
-        :return: A :class:`NumberDummy` object.
+        :return: A :class:`Quantity` object.
         """
 
         # Check units
         if not isinstance(units, self.units.Unit):
             units = self.units.get_unit(units)
 
-        return NumberDummy(value, units)
+        return Quantity(value, units)
 
     def add_cmeta_id(self, variable):
         """
@@ -637,7 +640,7 @@ class Model(object):
 
         If the variable already has a cmeta id no action is performed.
 
-        :param variable: A :class:`VariableDummy`.
+        :param variable: A :class:`Variable`.
         """
         if variable._cmeta_id is not None:
             return
@@ -738,7 +741,7 @@ class Model(object):
             sv1_orig_deriv = 1 :: mV_per_ms
             ode(sv1, time_converted) = 1000 :: ms_per_s * sv1_orig_deriv
 
-        :param original_variable: the :class:`VariableDummy` object representing the variable in the model to be
+        :param original_variable: the :class:`Variable` object representing the variable in the model to be
                                   converted
         :param units: a :class:`~cellmlmanip.units.UnitStore.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
@@ -751,7 +754,7 @@ class Model(object):
         :raises DimensionalityError: if the unit conversion is impossible
         """
         # Sanity checks on inputs
-        assert isinstance(original_variable, VariableDummy)
+        assert isinstance(original_variable, Variable)
         assert original_variable.name in self._name_to_variable  # Variable must be in model
         assert isinstance(units, self.units.Unit)  # Units must be in the right registry
         assert isinstance(direction, DataDirectionFlow)
@@ -763,7 +766,7 @@ class Model(object):
             # No conversion necessary. The method above will ensure a factor close to 1 is returned as 1.
             return original_variable
         # Make the conversion factor a number symbol with explicit units
-        cf = self.add_number(cf, units / original_variable.units)
+        cf = self.create_quantity(cf, units / original_variable.units)
 
         # Store original state and free symbols (these might change, so need to store references early)
         state_symbols = self.get_state_variables()
@@ -804,16 +807,16 @@ class Model(object):
     def find_variables_and_derivatives(self, expressions):
         """Returns a set containing all variables and derivatives referenced in a list of expressions.
 
-        Note that we can't just use ``.atoms(VariableDummy, sympy.Derivative)`` for this, because it
+        Note that we can't just use ``.atoms(Variable, sympy.Derivative)`` for this, because it
         will return the state and free variables from inside derivatives, which is not what we want.
 
         :param expressions: an iterable of expressions to get variables for.
-        :return: a set of variables and derivatives, as :class:`VariableDummy` and :class:`sympy.Derivative`
+        :return: a set of variables and derivatives, as :class:`Variable` and :class:`sympy.Derivative`
             objects respectively.
         """
         variables = set()
         for expr in expressions:
-            if expr.is_Derivative or isinstance(expr, VariableDummy):
+            if expr.is_Derivative or isinstance(expr, Variable):
                 variables.add(expr)
             else:
                 variables |= self.find_variables_and_derivatives(expr.args)
@@ -828,7 +831,7 @@ class Model(object):
         """
         Assert that a variable doesn't have an existing definition.
 
-        :param var: the :class:`VariableDummy`, either a state var or normal var
+        :param var: the :class:`Variable`, either a state var or normal var
         :param equation: the new definition being added
         """
         if var in self._ode_definition_map:
@@ -907,7 +910,7 @@ class Model(object):
         # Otherwise, this connection requires a conversion
         else:
             # Dummy to represent this factor in equations, having units for conversion
-            factor_dummy = self.add_number(cf, target.units / source.units)
+            factor_dummy = self.create_quantity(cf, target.units / source.units)
 
             # Add an equation making the connection with the required conversion
             self.add_equation(sympy.Eq(target, source.assigned_to * factor_dummy))
@@ -1015,7 +1018,7 @@ class Model(object):
         If the direction is input, also creates an equation assigning ``original_variable`` from the converted one.
         See :meth:`convert_variable` for the context and examples.
 
-        :param original_variable: :class:`VariableDummy` object to be converted [old units]
+        :param original_variable: :class:`Variable` 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
@@ -1064,18 +1067,18 @@ class Model(object):
         return new_variable
 
 
-class NumberDummy(sympy.Dummy):
+class Quantity(sympy.Dummy):
     """
     Used to represent a number with a unit, inside a Sympy expression.
 
     Unlike sympy expressions, this number type will never be removed in simplify operations etc.
 
-    Number dummies should never be created directly, but always via :meth:`Model.add_number()`.
+    Quantities should never be created directly, but always via :meth:`Model.create_quantity()`.
 
     Assumes the value is real.
 
     To get the actual value as a float or string, use ``float(dummy)`` or ``str(dummy)`` respectively.
-    You can also use ``dummy.evalf()`` to get the value as a :class:`sympy.Float`.
+    You can also use ``quantity.evalf()`` to get the value as a :class:`sympy.Float`.
     """
 
     # Sympy annoyingly overwrites __new__
@@ -1083,7 +1086,7 @@ class NumberDummy(sympy.Dummy):
         # Middle argument is the symbol name, so must be a string
         if not isinstance(value, str):
             value = '{:g}'.format(value)
-        return super().__new__(cls, value, real=True)
+        return super().__new__(cls, '_' + value, real=True)
 
     def __init__(self, value, units):
         self._value = value
@@ -1100,11 +1103,11 @@ class NumberDummy(sympy.Dummy):
         return str(self._value)
 
 
-class VariableDummy(sympy.Dummy):
+class Variable(sympy.Dummy):
     """
     Used to represent a variable (with meta data) in a Sympy expression.
 
-    Variable dummies should never be created directly, but always via :meth:`Model.add_variable()`.
+    Variables should never be created directly, but always via :meth:`Model.add_variable()`.
 
     For the constructor arguments, see :meth:`Model.add_variable()`.
 
diff --git a/cellmlmanip/parser.py b/cellmlmanip/parser.py
index f6df255..ba8e681 100644
--- a/cellmlmanip/parser.py
+++ b/cellmlmanip/parser.py
@@ -331,7 +331,7 @@ class Parser(object):
         # reuse transpiler so dummy symbols are kept across <math> elements
         transpiler = Transpiler(
             symbol_generator=symbol_generator,
-            number_generator=lambda x, y: self.model.add_number(x, self.model.units.get_unit(y)),
+            number_generator=lambda x, y: self.model.create_quantity(x, self.model.units.get_unit(y)),
         )
 
         # for each math element
@@ -501,7 +501,7 @@ class Parser(object):
             if var in self.model.get_state_variables():
                 assert var.initial_value is not None, 'State variable {} has no initial_value set'.format(var)
             elif var.initial_value is not None:
-                value = self.model.add_number(var.initial_value, var.units)
+                value = self.model.create_quantity(var.initial_value, var.units)
                 self.model.add_equation(sympy.Eq(var, value))
                 var.initial_value = None
 
diff --git a/cellmlmanip/units.py b/cellmlmanip/units.py
index 1676cdf..5f88b18 100644
--- a/cellmlmanip/units.py
+++ b/cellmlmanip/units.py
@@ -434,9 +434,9 @@ class UnitCalculator(object):
         if expr.is_Symbol:
 
             # is this symbol is a placeholder for a number
-            if isinstance(expr, model.NumberDummy):
+            if isinstance(expr, model.Quantity):
                 return float(expr) * expr.units
-            elif isinstance(expr, model.VariableDummy):
+            elif isinstance(expr, model.Variable):
                 if expr.initial_value and expr.initial_value != 0.0:
                     #  if this symbol has an initial value (that is not zero)
                     # substitute with the initial value for unit arithmetic
@@ -627,7 +627,7 @@ class UnitCalculator(object):
                     raise UnitConversionError(expr, from_units, to_units) from None
                 if cf != 1:
                     was_converted = True
-                    cf = model.NumberDummy(cf, to_units / from_units)
+                    cf = model.Quantity(cf, to_units / from_units)
                     expr = cf * expr
             return expr, was_converted, to_units
 
@@ -643,14 +643,14 @@ class UnitCalculator(object):
             # See comment in :meth:`traverse()`!
             raise UnexpectedMathUnitsError(str(expr))
         elif expr.is_Symbol:
-            assert isinstance(expr, (model.NumberDummy, model.VariableDummy))
+            assert isinstance(expr, (model.Quantity, model.Variable))
             expr, was_converted, actual_units = maybe_convert_expr(expr, was_converted, expr.units, to_units)
         elif expr.is_Derivative:
             # Just convert the result if needed
-            if (not isinstance(expr.args[0], model.VariableDummy) or
+            if (not isinstance(expr.args[0], model.Variable) or
                     len(expr.args) > 2 or
                     expr.args[1][1] > 1 or
-                    not isinstance(expr.args[1][0], model.VariableDummy)):
+                    not isinstance(expr.args[1][0], model.Variable)):
                 raise UnexpectedMathUnitsError(
                     expr, 'We only support first order derivatives of single variables wrt time')
             _, was_converted, numerator_units = maybe_convert_child(expr.args[0], was_converted, None)


Test patch:
diff --git a/tests/test_model_functions.py b/tests/test_model_functions.py
index 388b6a8..d04acc5 100644
--- a/tests/test_model_functions.py
+++ b/tests/test_model_functions.py
@@ -4,7 +4,7 @@ import pytest
 import sympy as sp
 
 from cellmlmanip import parser, units
-from cellmlmanip.model import FLOAT_PRECISION, Model, VariableDummy
+from cellmlmanip.model import FLOAT_PRECISION, Model, Variable
 
 from . import shared
 
@@ -215,19 +215,19 @@ class TestModelFunctions():
         a1 = m.add_variable('a1', u)
 
         # dy1/dt = 1
-        m.add_equation(sp.Eq(sp.Derivative(y1, t), m.add_number(1, u)))
+        m.add_equation(sp.Eq(sp.Derivative(y1, t), m.create_quantity(1, u)))
         # dy2/dt = v1 --> Doesn't simplify, reference to v1 is maintained
         m.add_equation(sp.Eq(sp.Derivative(y2, t), v1))
         # dy3/dt = v2 * (2 + dy1/dt)
-        m.add_equation(sp.Eq(sp.Derivative(y3, t), sp.Mul(v2, sp.Add(m.add_number(2, u), sp.Derivative(y1, t)))))
+        m.add_equation(sp.Eq(sp.Derivative(y3, t), sp.Mul(v2, sp.Add(m.create_quantity(2, u), sp.Derivative(y1, t)))))
         # v1 = (5 - 5) * v3 --> Simplifies to 0
-        m.add_equation(sp.Eq(v1, sp.Mul(sp.Add(m.add_number(5, u), m.add_number(-5, u)), v3)))
+        m.add_equation(sp.Eq(v1, sp.Mul(sp.Add(m.create_quantity(5, u), m.create_quantity(-5, u)), v3)))
         # v2 = 23 + v4 --> Doesn't simplify, reference to v4 is maintained
-        m.add_equation(sp.Eq(v2, sp.Add(m.add_number(23, u), v4)))
+        m.add_equation(sp.Eq(v2, sp.Add(m.create_quantity(23, u), v4)))
         # v3 = 2 / 3
-        m.add_equation(sp.Eq(v3, sp.Mul(m.add_number(2, u), sp.Pow(m.add_number(3, u), sp.S.NegativeOne))))
+        m.add_equation(sp.Eq(v3, sp.Mul(m.create_quantity(2, u), sp.Pow(m.create_quantity(3, u), sp.S.NegativeOne))))
         # v4 = -23
-        m.add_equation(sp.Eq(v4, m.add_number(-23, u)))
+        m.add_equation(sp.Eq(v4, m.create_quantity(-23, u)))
         # v5 = v3 + v4
         m.add_equation(sp.Eq(v5, sp.Add(v3, v4)))
         # a1 = v5 + v2 + v1 + t
@@ -457,7 +457,7 @@ class TestModelFunctions():
         t = model.get_variable_by_ontology_term((shared.OXMETA, 'time'))
         equation = model.graph.nodes[sp.Derivative(v, t)]['equation']
         model.remove_equation(equation)
-        equation = sp.Eq(v, model.add_number(-80, v.units))
+        equation = sp.Eq(v, model.create_quantity(-80, v.units))
         model.add_equation(equation)
 
         # Check that V is no longer a state
@@ -468,7 +468,7 @@ class TestModelFunctions():
         # Now make V a state again
         dvdt_units = v.units / t.units
         model.remove_equation(equation)
-        equation = sp.Eq(sp.Derivative(v, t), model.add_number(0, dvdt_units))
+        equation = sp.Eq(sp.Derivative(v, t), model.create_quantity(0, dvdt_units))
         model.add_equation(equation)
 
         # Check that V is a state again
@@ -477,14 +477,14 @@ class TestModelFunctions():
         assert v in state_var
 
         # Test removing non-existing equation
-        equation = sp.Eq(sp.Derivative(v, t), model.add_number(5, dvdt_units))
+        equation = sp.Eq(sp.Derivative(v, t), model.create_quantity(5, dvdt_units))
         with pytest.raises(KeyError, match='Equation not found'):
             model.remove_equation(equation)
 
-    def test_add_number(self, local_model):
-        """ Tests the Model.add_number method. """
+    def test_create_quantity(self, local_model):
+        """ Tests the Model.create_quantity method. """
         model = local_model
-        number2 = model.add_number(2.0, 'mV')
+        number2 = model.create_quantity(2.0, 'mV')
         assert number2.is_Dummy
 
     def test_add_variable(self, local_model):
@@ -607,8 +607,8 @@ class TestModelFunctions():
 
     def test_find_variables_and_derivatives(self, basic_model):
         """ Tests Model.find_variables_and_derivatives() on a simple model. """
-        a = VariableDummy('a', 'second')
-        b = VariableDummy('b', 'second')
+        a = Variable('a', 'second')
+        b = Variable('b', 'second')
         ex = sp.Add(a, b)
         syms = basic_model.find_variables_and_derivatives([ex])
         assert len(syms) == 2
diff --git a/tests/test_parser.py b/tests/test_parser.py
index e536288..a215d09 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -188,10 +188,10 @@ class TestParser(object):
         class ExpressionWithUnitPrinter(LambdaPrinter):
             """Sympy expression printer to print expressions with unit information."""
 
-            def _print_NumberDummy(self, expr):
+            def _print_Quantity(self, expr):
                 return '%f[%s]' % (float(expr), str(expr.units))
 
-            def _print_VariableDummy(self, expr):
+            def _print_Variable(self, expr):
                 return '%s[%s]' % (expr.name, str(expr.units))
 
             def _print_Derivative(self, expr):
diff --git a/tests/test_units.py b/tests/test_units.py
index c0e217e..13e8447 100644
--- a/tests/test_units.py
+++ b/tests/test_units.py
@@ -2,7 +2,7 @@ import pint
 import pytest
 import sympy as sp
 
-from cellmlmanip.model import NumberDummy, VariableDummy
+from cellmlmanip.model import Quantity, Variable
 from cellmlmanip.units import (
     BooleanUnitsError,
     InputArgumentMustBeNumberError,
@@ -214,8 +214,8 @@ class TestEvaluateUnits:
         """Tests on numbers."""
 
         store = UnitStore()
-        _1 = NumberDummy(1, store.get_unit('kelvin'))
-        _2 = NumberDummy(2, store.get_unit('dimensionless'))
+        _1 = Quantity(1, store.get_unit('kelvin'))
+        _2 = Quantity(2, store.get_unit('dimensionless'))
 
         assert store.evaluate_units(_1) == store.get_unit('kelvin')
         assert store.evaluate_units(_2) == store.get_unit('dimensionless')
@@ -226,17 +226,17 @@ class TestEvaluateUnits:
         """ Tests the units.UnitCalculator class. """
 
         store = UnitStore()
-        a = VariableDummy('a', store.get_unit('meter'))
-        b = VariableDummy('b', store.get_unit('second'))
-        c = VariableDummy('c', store.get_unit('gram'))
-        d = VariableDummy('d', store.get_unit('meter'))
-        x = VariableDummy('x', store.get_unit('kilogram'))
-        y = VariableDummy('y', store.get_unit('volt'))
-        n = VariableDummy('n', store.get_unit('dimensionless'))
-        av = VariableDummy('av', store.get_unit('meter'), initial_value=2)
-        _1 = NumberDummy(1, store.get_unit('kelvin'))
-        _2 = NumberDummy(2, store.get_unit('dimensionless'))
-        _25 = NumberDummy(2.5, store.get_unit('dimensionless'))
+        a = Variable('a', store.get_unit('meter'))
+        b = Variable('b', store.get_unit('second'))
+        c = Variable('c', store.get_unit('gram'))
+        d = Variable('d', store.get_unit('meter'))
+        x = Variable('x', store.get_unit('kilogram'))
+        y = Variable('y', store.get_unit('volt'))
+        n = Variable('n', store.get_unit('dimensionless'))
+        av = Variable('av', store.get_unit('meter'), initial_value=2)
+        _1 = Quantity(1, store.get_unit('kelvin'))
+        _2 = Quantity(2, store.get_unit('dimensionless'))
+        _25 = Quantity(2.5, store.get_unit('dimensionless'))
 
         # functions were all args should have same unit
         # and this is the unit that will be returned
@@ -449,7 +449,7 @@ class TestConvertingExpressions:
     Test the UnitStore methods that perform within-equation conversion.
     """
     def test_variable_no_conversion(self, store):
-        x = VariableDummy('x', store.get_unit('metre'))
+        x = Variable('x', store.get_unit('metre'))
 
         new_x = store.convert_expression_recursively(x, None)
         assert x is new_x
@@ -458,19 +458,19 @@ class TestConvertingExpressions:
         assert x is new_x
 
     def test_variable_conversion(self, store, cm):
-        x = VariableDummy('x', store.get_unit('metre'))
+        x = Variable('x', store.get_unit('metre'))
         new_x = store.convert_expression_recursively(x, cm)
         assert str(new_x) == '_100*_x'
         assert new_x.args[1] is x
-        assert isinstance(new_x.args[0], NumberDummy)
+        assert isinstance(new_x.args[0], Quantity)
         assert new_x.args[0].units == cm / store.get_unit('metre')
 
     def test_number_conversion(self, store, cm):
-        _5 = NumberDummy(5, cm)
+        _5 = Quantity(5, cm)
         new_5 = store.convert_expression_recursively(_5, store.get_unit('metre'))
         assert str(new_5) == '_0.01*_5'
         assert new_5.args[1] is _5
-        assert isinstance(new_5.args[0], NumberDummy)
+        assert isinstance(new_5.args[0], Quantity)
         assert new_5.args[0].units == store.get_unit('metre') / cm
 
     def test_plain_numbers(self, store):
@@ -489,8 +489,8 @@ class TestConvertingExpressions:
         assert store.convert_expression_recursively(expr, None) is expr
 
     def test_derivative_no_conversion(self, store):
-        x = VariableDummy('x', store.get_unit('metre'))
-        t = VariableDummy('t', store.get_unit('second'))
+        x = Variable('x', store.get_unit('metre'))
+        t = Variable('t', store.get_unit('second'))
         expr = sp.Derivative(x, t)
 
         new_expr = store.convert_expression_recursively(expr, None)
@@ -500,8 +500,8 @@ class TestConvertingExpressions:
         assert expr is new_expr
 
     def test_derivative_conversion(self, store, cm, ms):
-        x = VariableDummy('x', store.get_unit('metre'))
-        t = VariableDummy('t', store.get_unit('second'))
+        x = Variable('x', store.get_unit('metre'))
+        t = Variable('t', store.get_unit('second'))
         expr = sp.Derivative(x, t)
 
         new_expr = store.convert_expression_recursively(expr, cm / t.units)
@@ -513,8 +513,8 @@ class TestConvertingExpressions:
         assert str(new_expr) == '_0.001*Derivative(_x, _t)'
 
     def test_mul_and_pow(self, store, cm, ms):
-        x = VariableDummy('x', cm)
-        y = VariableDummy('y', ms)
+        x = Variable('x', cm)
+        y = Variable('y', ms)
         expr = x / y  # Becomes x * (1/y)
 
         # No conversion
@@ -528,8 +528,8 @@ class TestConvertingExpressions:
         assert new_expr.args[0].args[0] is y
 
         # With conversion only for exponent
-        _4 = NumberDummy('4', store.get_unit('second'))
-        _2 = NumberDummy('2000', ms)
+        _4 = Quantity('4', store.get_unit('second'))
+        _2 = Quantity('2000', ms)
         expr = x ** (_4 / _2)
         new_expr = store.convert_expression_recursively(expr, None)
         assert str(new_expr) == '_x**(_1000*_4/_2000)'
@@ -540,27 +540,27 @@ class TestConvertingExpressions:
         assert str(new_expr) == '(_0.001*_y + _4)**2'
 
     def test_mul_with_converted_arg(self, store, ms):
-        x = VariableDummy('x', store.get_unit('second'))
-        y = VariableDummy('y', ms)
-        z = VariableDummy('z', ms)
+        x = Variable('x', store.get_unit('second'))
+        y = Variable('y', ms)
+        z = Variable('z', ms)
         expr = (x + y) * z
         new_expr = store.convert_expression_recursively(expr, None)
         assert str(new_expr) == '_z*(_0.001*_y + _x)'
 
     def test_square_root(self, store, ms):
-        x = VariableDummy('x', store.get_unit('second') ** 2)
+        x = Variable('x', store.get_unit('second') ** 2)
         expr = x ** (1 / 2)
         new_expr = store.convert_expression_recursively(expr, ms)
         assert str(new_expr) == '_1000*_x**0.5'
         assert new_expr.args[0].args[0] is x
 
     def test_add_and_subtract(self, store, ms, microsecond):
-        x = VariableDummy('x', store.get_unit('second'))
-        y = VariableDummy('y', ms)
-        z = VariableDummy('z', microsecond)
+        x = Variable('x', store.get_unit('second'))
+        y = Variable('y', ms)
+        z = Variable('z', microsecond)
 
         # If no conversion is needed we get the original expression
-        expr = y + NumberDummy('2', ms)
+        expr = y + Quantity('2', ms)
         assert store.convert_expression_recursively(expr, ms) is expr
 
         # If we don't specify units, the first argument (y in canonical form) is chosen
@@ -572,7 +572,7 @@ class TestConvertingExpressions:
         assert str(new_expr) == '-_1000*_y + _1e+06*_x + _z'
 
     def test_abs_ceil_floor(self, store, ms):
-        x = VariableDummy('x', store.get_unit('second'))
+        x = Variable('x', store.get_unit('second'))
 
         expr = sp.Abs(x)
         new_expr = store.convert_expression_recursively(expr, None)
@@ -602,9 +602,9 @@ class TestConvertingExpressions:
 
     def test_exp_log_trig(self, store, ms):
         dimensionless = store.get_unit('dimensionless')
-        x = VariableDummy('x', dimensionless)
-        y = VariableDummy('y', store.get_unit('second'))
-        z = VariableDummy('z', ms)
+        x = Variable('x', dimensionless)
+        y = Variable('y', store.get_unit('second'))
+        z = Variable('z', ms)
 
         expr = sp.exp(x)
         assert store.convert_expression_recursively(expr, dimensionless) is expr
@@ -620,9 +620,9 @@ class TestConvertingExpressions:
         assert str(new_expr) == 'sin(_0.001*_z/_y)'
 
     def test_relations(self, store, ms):
-        x = VariableDummy('x', ms)
-        y = VariableDummy('y', store.get_unit('second'))
-        z = VariableDummy('z', ms)
+        x = Variable('x', ms)
+        y = Variable('y', store.get_unit('second'))
+        z = Variable('z', ms)
 
         expr = y < z
         new_expr = store.convert_expression_recursively(expr, None)
@@ -640,10 +640,10 @@ class TestConvertingExpressions:
     def test_piecewise(self, store, ms, microsecond, cm):
         # This also checks more complex nested expressions
         dimensionless = store.get_unit('dimensionless')
-        _2 = NumberDummy(2, dimensionless)
-        x = VariableDummy('x', dimensionless)
-        y = VariableDummy('y', store.get_unit('second'))
-        z = VariableDummy('z', ms)
+        _2 = Quantity(2, dimensionless)
+        x = Variable('x', dimensionless)
+        y = Variable('y', store.get_unit('second'))
+        z = Variable('z', ms)
 
         expr = sp.Piecewise(
             (y, sp.And(y < z, x > _2)),
@@ -662,13 +662,13 @@ class TestConvertingExpressions:
             'Piecewise((_1000*_y, (_2 < _x) & (_y < _0.001*_z)), (_z, _2 > _x), (_2*_z, True))')
 
         # A simpler case with no conversion
-        _1 = NumberDummy('1', ms)
+        _1 = Quantity('1', ms)
         expr = sp.Piecewise((z, x < _2), (_1, True))
         assert store.convert_expression_recursively(expr, ms) is expr
 
     def test_assignment(self, store, ms):
-        x = VariableDummy('x', store.get_unit('second'))
-        _10 = NumberDummy(10, ms)
+        x = Variable('x', store.get_unit('second'))
+        _10 = Quantity(10, ms)
         expr = sp.Eq(x, _10)
         new_expr = store.convert_expression_recursively(expr, None)
         assert str(new_expr) == 'Eq(_x, _0.001*_10)'
@@ -677,15 +677,15 @@ class TestConvertingExpressions:
         s = store.get_unit('second')
 
         # Note that sympy re-orders the expression into a consistent canonical form, in this case based on the value
-        a = NumberDummy(10, ms)
-        b = NumberDummy(0.1, s)
+        a = Quantity(10, ms)
+        b = Quantity(0.1, s)
         expr = a + b
         assert str(expr) == '_0.1 + _10'
         units, new_expr = store.evaluate_units_and_fix(expr)
         assert str(new_expr) == '_0.001*_10 + _0.1'
         assert units is s
 
-        b = NumberDummy(20, s)
+        b = Quantity(20, s)
         expr = a + b
         assert str(expr) == '_10 + _20'
         units, new_expr = store.evaluate_units_and_fix(expr)
@@ -693,8 +693,8 @@ class TestConvertingExpressions:
         assert units is ms
 
     def test_evaluate_units_and_fix_with_variable(self, store, ms):
-        a = NumberDummy(10, ms)
-        b = VariableDummy('b', store.get_unit('second'))
+        a = Quantity(10, ms)
+        b = Variable('b', store.get_unit('second'))
         expr = a + b
         assert str(expr) == '_10 + _b'
         units, new_expr = store.evaluate_units_and_fix(expr)
@@ -703,30 +703,30 @@ class TestConvertingExpressions:
 
     # Methods below check error cases
     def test_symbol_wrong_dimensions(self, store):
-        x = VariableDummy('x', store.get_unit('metre'))
+        x = Variable('x', store.get_unit('metre'))
         with pytest.raises(UnitConversionError, match='from meter to second'):
             store.convert_expression_recursively(x, store.get_unit('second'))
 
     def test_derivative_wrong_dimensions(self, store):
-        x = VariableDummy('x', store.get_unit('metre'))
-        t = VariableDummy('t', store.get_unit('second'))
+        x = Variable('x', store.get_unit('metre'))
+        t = Variable('t', store.get_unit('second'))
         expr = sp.Derivative(x, t)
         with pytest.raises(UnitConversionError, match='Context: trying to convert'):
             store.convert_expression_recursively(expr, store.get_unit('metre'))
 
     def test_complex_derivative(self, store):
-        x = VariableDummy('x', store.get_unit('metre'))
-        y = VariableDummy('y', store.get_unit('metre'))
-        t = VariableDummy('t', store.get_unit('second'))
-        t2 = VariableDummy('t2', store.get_unit('second'))
+        x = Variable('x', store.get_unit('metre'))
+        y = Variable('y', store.get_unit('metre'))
+        t = Variable('t', store.get_unit('second'))
+        t2 = Variable('t2', store.get_unit('second'))
         expr = sp.Derivative(x + y, t, t2)
         with pytest.raises(UnexpectedMathUnitsError,
                            match='only support first order derivatives of single variables'):
             store.convert_expression_recursively(expr, store.get_unit('metre'))
 
     def test_mul_wrong_dimensions(self, store):
-        x = VariableDummy('x', store.get_unit('metre'))
-        _1 = NumberDummy(1, store.get_unit('second'))
+        x = Variable('x', store.get_unit('metre'))
+        _1 = Quantity(1, store.get_unit('second'))
         expr = x * _1
         with pytest.raises(UnitConversionError):
             store.convert_expression_recursively(expr, store.get_unit('metre'))
@@ -737,63 +737,63 @@ class TestConvertingExpressions:
             store.convert_expression_recursively(expr, None)
 
     def test_error_exponent_not_dimensionless(self, store):
-        x = VariableDummy('x', store.get_unit('metre'))
-        _1 = NumberDummy(1, store.get_unit('second'))
+        x = Variable('x', store.get_unit('metre'))
+        _1 = Quantity(1, store.get_unit('second'))
         expr = x ** _1
         with pytest.raises(UnitConversionError):
             store.convert_expression_recursively(expr, None)
 
     def test_error_exponent_not_number(self, store):
-        x = VariableDummy('x', store.get_unit('dimensionless'))
-        _1 = NumberDummy(1, store.get_unit('second'))
+        x = Variable('x', store.get_unit('dimensionless'))
+        _1 = Quantity(1, store.get_unit('second'))
         expr = _1 ** x
         with pytest.raises(InputArgumentMustBeNumberError):
             store.convert_expression_recursively(expr, None)
 
     def test_pow_wrong_dimensions(self, store):
-        x = VariableDummy('x', store.get_unit('second'))
-        _2 = NumberDummy(2, store.get_unit('dimensionless'))
+        x = Variable('x', store.get_unit('second'))
+        _2 = Quantity(2, store.get_unit('dimensionless'))
         expr = x ** _2
         with pytest.raises(UnitConversionError):
             store.convert_expression_recursively(expr, store.get_unit('second'))
 
     def test_add_wrong_dimensions(self, store):
-        x = VariableDummy('x', store.get_unit('second'))
-        _2 = NumberDummy(2, store.get_unit('dimensionless'))
+        x = Variable('x', store.get_unit('second'))
+        _2 = Quantity(2, store.get_unit('dimensionless'))
         expr = x + _2
         with pytest.raises(UnitConversionError):
             store.convert_expression_recursively(expr, None)
 
     def test_relational_must_be_dimensionless(self, store):
-        x = VariableDummy('x', store.get_unit('second'))
-        y = VariableDummy('y', store.get_unit('second'))
+        x = Variable('x', store.get_unit('second'))
+        y = Variable('y', store.get_unit('second'))
         expr = x < y
         with pytest.raises(BooleanUnitsError):
             store.convert_expression_recursively(expr, store.get_unit('second'))
 
     def test_relational_dimension_mismatch(self, store):
-        x = VariableDummy('x', store.get_unit('second'))
-        y = VariableDummy('y', store.get_unit('metre'))
+        x = Variable('x', store.get_unit('second'))
+        y = Variable('y', store.get_unit('metre'))
         expr = x <= y
         with pytest.raises(UnitConversionError):
             store.convert_expression_recursively(expr, store.get_unit('dimensionless'))
 
     def test_piecewise_condition_not_dimensionless(self, store):
-        a = VariableDummy('a', store.get_unit('metre'))
-        x = VariableDummy('x', store.get_unit('second'))
+        a = Variable('a', store.get_unit('metre'))
+        x = Variable('x', store.get_unit('second'))
         expr = sp.Piecewise((a, x), (-a, True))
         with pytest.raises(UnitConversionError):
             store.convert_expression_recursively(expr, store.get_unit('metre'))
 
     def test_piecewise_cannot_convert_result(self, store):
-        a = VariableDummy('a', store.get_unit('metre'))
-        x = VariableDummy('x', store.get_unit('dimensionless'))
+        a = Variable('a', store.get_unit('metre'))
+        x = Variable('x', store.get_unit('dimensionless'))
         expr = sp.Piecewise((a, x), (-a, True))
         with pytest.raises(UnitConversionError):
             store.convert_expression_recursively(expr, store.get_unit('second'))
 
     def test_exp_must_be_dimensionless(self, store):
-        x = VariableDummy('x', store.get_unit('dimensionless'))
+        x = Variable('x', store.get_unit('dimensionless'))
         expr = sp.exp(x)
         with pytest.raises(InputArgumentsMustBeDimensionlessError):
             store.convert_expression_recursively(expr, store.get_unit('second'))
@@ -803,14 +803,14 @@ class TestConvertingExpressions:
             store.convert_expression_recursively(sp.E, store.get_unit('second'))
 
     def test_error_unsupported_math(self, store):
-        x = VariableDummy('x', store.get_unit('second'))
+        x = Variable('x', store.get_unit('second'))
         expr = sp.Integral(x**2, x)
         with pytest.raises(UnexpectedMathUnitsError):
             store.convert_expression_recursively(expr, None)
 
     def test_evaluate_units_and_fix_with_unfixable_expr(self, store):
-        _10 = NumberDummy(10, store.get_unit('metre'))
-        _20 = NumberDummy(20, store.get_unit('second'))
+        _10 = Quantity(10, store.get_unit('metre'))
+        _20 = Quantity(20, store.get_unit('second'))
         expr = _10 + _20
         with pytest.raises(UnitConversionError, match='Context: trying to evaluate'):
             store.evaluate_units_and_fix(expr)