diff --git a/spatialpy/core/parameter.py b/spatialpy/core/parameter.py index 7dc35a4d..d3e9904f 100644 --- a/spatialpy/core/parameter.py +++ b/spatialpy/core/parameter.py @@ -17,53 +17,93 @@ class Parameter(): """ - Model of a rate paramter. - A parameter can be given as a String expression (function) or directly as a scalar value. - If given a String expression, it should be evaluable in the namespace of a parent Model. + A parameter can be given as an expression (function) or directly + as a value (scalar). If given an expression, it should be + understood as evaluable in the namespace of a parent Model. - :param name: Name of the Parameter. + :param name: The name by which this parameter is called or referenced. :type name: str - :param expression: Mathematical expression of Parameter. + :param expression: String for a function calculating parameter values. Should be + evaluable in namespace of Model. :type expression: str - :param value: Parameter as value rather than expression. - :type value: float + :raises ParameterError: Arg is of invalid type. Required arg set to None. Arg value is outside of accepted bounds. """ def __init__(self, name=None, expression=None): - if name is None: - raise ParameterError("name is required for a Parameter.") - if not isinstance(name, str): - raise ParameterError("Parameter name must be a string.") - - if expression is None: - raise ParameterError("expression is required for a Parameter.") - - self.name = name - self.value = None # We allow expression to be passed in as a non-string type. Invalid strings # will be caught below. It is perfectly fine to give a scalar value as the expression. # This can then be evaluated in an empty namespace to the scalar value. + self.value = None + self.name = name + if isinstance(expression, (int, float)): - self.expression = str(expression) - else: - self.expression = expression + expression = str(expression) + self.expression = expression + + self.validate() def __str__(self): - print_string = f"{self.name}: {str(self.expression)}" - return print_string + return f"{self.name}: {self.expression}" def _evaluate(self, namespace=None): """ - Evaluate the expression and return the (scalar) value. + Evaluate the expression and return the (scalar) value in the given + namespace. - :param namespace: A dictionary containing key,value pairings of expressions and evaluable executions. + :param namespace: The namespace in which to test evaulation of the parameter, + if it involves other parameters, etc. :type namespace: dict + + :raises ParameterError: expression is of invalid type. expression is set to None. \ + expression is not evaluable within the given namespace. """ - if namespace is None: - namespace = {} + if isinstance(self.expression, (int, float)): + self.expression = str(self.expression) + + self.validate(coverage="expression") + try: - self.value = (float(eval(self.expression, namespace))) + if namespace is None: + namespace = {} + self.value = float(eval(self.expression, namespace)) except Exception as err: - message = f"Could not evaluate expression '{self.expression}': {err}." - raise ParameterError(message) from err + raise ParameterError( + f"Could not evaluate expression: '{self.expression}'. Reason given: {err}." + ) from err + + def validate(self, expression=None, coverage="all"): + """ + Validate the parameter. + + :param expression: String for a function calculating parameter values. Should be + evaluable in namespace of Model. + :type expression: str + + :param coverage: The scope of attributes to validate. Set to an attribute name to restrict validation \ + to a specific attribute. + :type coverage: str + + :raises ParameterError: Attribute is of invalid type. Required attribute set to None. \ + Attribute value is outside of accepted bounds. + """ + # Check name + if coverage in ("all", "name"): + if self.name is None: + raise ParameterError("name can't be None type.") + if not isinstance(self.name, str): + raise ParameterError("name must be of type str.") + if self.name == "": + raise ParameterError("name can't be an empty string.") + + # Check expression + if coverage in ("all", "expression"): + if expression is None: + expression = self.expression + + if expression is None: + raise ParameterError("initial_value can't be None type.") + if not isinstance(expression, str): + raise ParameterError("expression must be of type str, float, or int.") + if expression == "": + raise ParameterError("expression can't be an empty string.") diff --git a/test/unit_tests/test_parameter.py b/test/unit_tests/test_parameter.py index 4d240b48..04ebcee6 100644 --- a/test/unit_tests/test_parameter.py +++ b/test/unit_tests/test_parameter.py @@ -15,9 +15,8 @@ # along with this program. If not, see . import unittest -import spatialpy -from spatialpy import Parameter -from spatialpy import ParameterError +from spatialpy.core.parameter import Parameter +from spatialpy.core.spatialpyerror import ParameterError class TestParameter(unittest.TestCase): ''' @@ -25,79 +24,131 @@ class TestParameter(unittest.TestCase): Unit tests for spatialpy.Parameter. ################################################################################################ ''' + def setUp(self): + self.parameter = Parameter(name="test_parameter", expression="0.5") + def test_constructor(self): """ Test the Parameter constructor. """ parameter = Parameter(name="test_parameter", expression="0.5") self.assertEqual(parameter.name, "test_parameter") self.assertEqual(parameter.expression, "0.5") - def test_constructor__no_name(self): """ Test the Parameter constructor without name. """ with self.assertRaises(ParameterError): - parameter = Parameter(expression="0.5") - + Parameter(expression="0.5") - def test_constructor__name_not_str(self): + def test_constructor__invalid_name(self): """ Test the Parameter constructor with non-str name. """ - with self.assertRaises(ParameterError): - parameter = Parameter(name=0, expression="0.5") - + test_names = ["", None, 0, 0.5, [0]] + for test_name in test_names: + with self.subTest(name=test_name): + with self.assertRaises(ParameterError): + Parameter(name=test_name, expression="0.5") def test_constructor__no_expression(self): """ Test the Parameter constructor without expression. """ with self.assertRaises(ParameterError): - parameter = Parameter(name="test_parameter") - + Parameter(name="test_parameter") def test_constructor__int_expression(self): """ Test the Parameter constructor with int expression. """ parameter = Parameter(name="test_parameter", expression=1) self.assertEqual(parameter.expression, "1") - def test_constructor__float_expression(self): """ Test the Parameter constructor with float expression. """ parameter = Parameter(name="test_parameter", expression=0.5) self.assertEqual(parameter.expression, "0.5") + def test_constructor__invaild_expression(self): + """ Test the Parameter constructor with an invalid expression. """ + test_exps = [None, "", [2]] + for test_exp in test_exps: + with self.subTest(expression=test_exp): + with self.assertRaises(ParameterError): + Parameter(name="test_name", expression=test_exp) def test___str__(self): """ Test Parameter.__str__ method. """ - parameter = Parameter(name="test_parameter", expression="0.5") - self.assertIsInstance(str(parameter), str) - + self.assertIsInstance(str(self.parameter), str) def test__evaluate(self): """ Test Parameter._evaluate method. """ - parameter = Parameter(name="test_parameter", expression="0.5") - parameter._evaluate() - self.assertEqual(parameter.value, 0.5) + self.parameter._evaluate() + self.assertEqual(self.parameter.value, 0.5) + def test__evaluate__int_expression(self): + """ Test Parameter._evaluate method with int expression. """ + self.parameter.expression = 5 + self.parameter._evaluate() + self.assertEqual(self.parameter.value, 5) - def test__evaluate__parameter_in_namespace(self): - """ Test Parameter._evaluate method with parameter in namespace. """ - parameter = Parameter(name="test_parameter", expression="k1 + 0.5") - parameter._evaluate(namespace={"k1": 3}) - self.assertEqual(parameter.value, 3.5) - - - def test__evaluate__species_in_namespace(self): - """ Test Parameter._evaluate method with species in namespace. """ - parameter = Parameter(name="test_parameter", expression="S0 + 0.5") - parameter._evaluate(namespace={"S0": 100}) - self.assertEqual(parameter.value, 100.5) - + def test__evaluate__float_expression(self): + """ Test Parameter._evaluate method with float expression. """ + self.parameter.expression = 0.5 + self.parameter._evaluate() + self.assertEqual(self.parameter.value, 0.5) def test__evaluate__improper_expression(self): """ Test Parameter._evaluate method with invalid expression. """ - parameter = Parameter(name="test_parameter", expression="[0.5]") + self.parameter.expression = "[0.5]" with self.assertRaises(ParameterError): - parameter._evaluate() + self.parameter._evaluate() + def test__evaluate__invaild_expression(self): + """ Test Parameter._evaluate with an invalid expression. """ + test_exps = [None, "", []] + for test_exp in test_exps: + with self.subTest(expression=test_exp): + with self.assertRaises(ParameterError): + self.parameter.expression = test_exp + self.parameter._evaluate() - def test__evaluate__param_not_in_namespace(self): - """ Test Parameter._evaluate method with arg missing from namespace. """ - parameter = Parameter(name="test_parameter", expression="k1 + 0.5") - with self.assertRaises(ParameterError): - parameter._evaluate() + def test__evaluate__parameter_in_namespace(self): + """ Test Parameter._evaluate method with parameter in namespace. """ + self.parameter.expression = "k1 + 0.5" + self.parameter._evaluate(namespace={"k1": 3}) + self.assertEqual(self.parameter.value, 3.5) + + def test__evaluate__species_in_namespace(self): + """ Test Parameter._evaluate method with species in namespace. """ + self.parameter.expression = "S0 + 0.5" + self.parameter._evaluate(namespace={"S0": 100}) + self.assertEqual(self.parameter.value, 100.5) + + def test__evaluate__component_not_in_namespace(self): + """ Test Parameter._evaluate method with component missing from namespace. """ + test_comps = ["SO", "k1"] + for test_comp in test_comps: + with self.subTest(component=test_comp): + self.parameter.expression = f"{test_comp} + 0.5" + with self.assertRaises(ParameterError): + self.parameter._evaluate() + + def test_validate__invalid_name(self): + """ Test Parameter.validate with non-str name. """ + test_names = ["", None, 0, 0.5, [0]] + for test_name in test_names: + with self.subTest(name=test_name): + with self.assertRaises(ParameterError): + self.parameter.name = test_name + self.parameter.validate() + + def test_validate__invaild_expression(self): + """ Test Parameter.validate with an invalid expression. """ + test_exps = [None, "", [2]] + for test_exp in test_exps: + with self.subTest(expression=test_exp): + with self.assertRaises(ParameterError): + self.parameter.expression = test_exp + self.parameter.validate() + + def test_comp_time_of_validate(self): + """ Check the computation time of validate. """ + import time + from datetime import datetime + start = time.time() + self.parameter.validate() + tic = datetime.utcfromtimestamp(time.time() - start) + print(f"Total time to run validate: {tic.strftime('%M mins %S secs %f msecs')}")