Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 69 additions & 29 deletions spatialpy/core/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
129 changes: 90 additions & 39 deletions test/unit_tests/test_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,89 +15,140 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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):
'''
################################################################################################
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')}")