Skip to content

Commit

Permalink
Add symbolic assumptions to parameters (#475)
Browse files Browse the repository at this point in the history
Implements the assumption that all symbolic variables in a model only take real 
values. A modeler can further specify "integer" and "non-negative" assumptions
about parameter values. Assumptions constrain the values that parameters may
take as well as enable simplification of mathematical expressions.
  • Loading branch information
FFroehlich committed Feb 4, 2020
1 parent dccf662 commit 555b2f0
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 11 deletions.
40 changes: 31 additions & 9 deletions pysb/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ def rename(obj, new_name):


class Symbol(sympy.Dummy):
def __new__(cls, name):
return super(Symbol, cls).__new__(cls, name)
def __new__(cls, name, real=True, **kwargs):
return super(Symbol, cls).__new__(cls, name, real=real, **kwargs)

def _lambdacode(self, printer, **kwargs):
""" custom printer method that ensures that the dummyid is not
Expand Down Expand Up @@ -1268,20 +1268,32 @@ class Parameter(Component, Symbol):
The numerical value of the parameter. Defaults to 0.0 if not specified.
The provided value is converted to a float before being stored, so any
value that cannot be coerced to a float will trigger an exception.
nonnegative : bool, optional
Sets the assumption whether this parameter is nonnegative (>=0).
Affects simplifications of expressions that involve this parameter.
By default, parameters are assumed to be non-negative.
integer : bool, optional
Sets the assumption whether this parameter takes integer values,
which affects simplifications of expressions that involve this
parameter. By default, parameters are not assumed to take integer values.
Attributes
----------
Identical to Parameters (see above).
value (see Parameters above).
"""

def __new__(cls, name, value=0.0, _export=True):
return super(Parameter, cls).__new__(cls, name)
def __new__(cls, name, value=0.0, nonnegative=True, integer=False,
_export=True):

return super(Parameter, cls).__new__(cls, name, real=True,
nonnegative=nonnegative,
integer=integer)

def __getnewargs__(self):
return (self.name, self.value, False)

def __init__(self, name, value=0.0, _export=True):
def __init__(self, name, value=0.0, _export=True, **kwargs):
self.value = value
Component.__init__(self, name, _export)

Expand All @@ -1291,17 +1303,27 @@ def value(self):

@value.setter
def value(self, new_value):
self.check_value(new_value)
self._value = float(new_value)

def get_value(self):
return self.value

def check_value(self, value):
if self.is_integer:
if not float(value).is_integer():
raise ValueError('Cannot assign an non-integer value to a '
'parameter assumed to be an integer')
if self.is_nonnegative:
if float(value) < 0:
raise ValueError('Cannot assign a negative value to a '
'parameter assumed to be nonnegative')

def __repr__(self):
return '%s(%s, %s)' % (self.__class__.__name__, repr(self.name), repr(self.value))
return '%s(%s, %s)' % (self.__class__.__name__, repr(self.name), repr(self.value))

def __str__(self):
return repr(self)

return repr(self)


class Compartment(Component):
Expand Down
3 changes: 2 additions & 1 deletion pysb/simulator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .base import SimulatorException, SimulationResult
from .base import SimulatorException, SimulationResult, \
InconsistentParameterError
from .scipyode import ScipyOdeSimulator
from .cupsoda import CupSodaSimulator
from .stochkit import StochKitSimulator
Expand Down
30 changes: 30 additions & 0 deletions pysb/simulator/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ class SimulatorException(Exception):
pass


class InconsistentParameterError(SimulatorException, ValueError):
def __init__(self, parameter_name, value, reason):
super(InconsistentParameterError, self).__init__(
f'Value {value} that was passed for parameter {parameter_name} '
f'was inconsistent with that parameters assumption: {reason}'
)


class Simulator(object):
"""An abstract base class for numerical simulation of models.
Expand Down Expand Up @@ -476,6 +484,15 @@ def _process_incoming_params(self, new_params):
if len(val) != n_sims:
raise ValueError("all arrays in params dictionary "
"must be equal length")

for value in val:
try:
self._model.parameters[key].check_value(value)
except ValueError as e:
raise InconsistentParameterError(
key, value, str(e)
)

elif isinstance(new_params, np.ndarray):
# if new_params is a 1D array, convert to a 2D array of length 1
if len(new_params.shape) == 1:
Expand All @@ -485,6 +502,18 @@ def _process_incoming_params(self, new_params):
if new_params.shape[1] != len(self._model.parameters):
raise ValueError("new_params must be the same length as "
"model.parameters")

for isim in range(n_sims):
for param, value in zip(self._model.parameters,
new_params[isim, :]):
try:
param.check_value(value)
except ValueError as e:
raise InconsistentParameterError(
param.name, value, str(e)
)


else:
raise ValueError(
'Implicit conversion of data type "{}" is not '
Expand All @@ -496,6 +525,7 @@ def _process_incoming_params(self, new_params):
raise ValueError(
self.__class__.__name__ +
" does not support multiple parameter values at this time.")

return new_params

def _reset_run_overrides(self):
Expand Down
39 changes: 39 additions & 0 deletions pysb/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,3 +574,42 @@ def test_reverse_rate_non_reversible_rule():
Parameter('kf', 1)
Parameter('kr', 2)
Rule('r1', None >> A(), kf, kr)


@with_model
def test_parameter_assumptions():
Parameter('k1', 0.0)
assert k1.is_real
assert k1.is_nonnegative
assert not k1.is_integer
Parameter('k2', 0.0, nonnegative=False)
assert not k2.is_nonnegative
Parameter('k3', 0.0, integer=True)
assert k3.is_integer


@raises(ValueError)
@with_model
def test_parameter_noninteger_integer_init():
Parameter('k3', 0.3, integer=True)


@raises(ValueError)
@with_model
def test_parameter_noninteger_integer_setter():
Parameter('k3', 1.0, integer=True)
k3.value = 0.4


@raises(ValueError)
@with_model
def test_parameter_negative_nonnegative_init():
Parameter('k3', -0.2, nonnegative=True)


@raises(ValueError)
@with_model
def test_parameter_negative_nonnegative_setter():
Parameter('k3', 0.0, nonnegative=True)
k3.value = -0.2

14 changes: 13 additions & 1 deletion pysb/tests/test_simulator_scipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import copy
import numpy as np
from pysb import Monomer, Parameter, Initial, Observable, Rule, Expression
from pysb.simulator import ScipyOdeSimulator
from pysb.simulator import ScipyOdeSimulator, InconsistentParameterError
from pysb.examples import robertson, earm_1_0, tyson_oscillator
import unittest
import pandas as pd
Expand Down Expand Up @@ -314,6 +314,18 @@ def test_run_params_different_length_to_base(self):
self.sim.param_values = param_values
self.sim.run(param_values=param_values[0])

@raises(InconsistentParameterError)
def test_run_params_inconsistent_parameter_list(self):
param_values = [55, 65, 75, 0, -3]
self.sim.param_values = param_values
self.sim.run(param_values=param_values[0])

@raises(InconsistentParameterError)
def test_run_params_inconsistent_parameter_dict(self):
param_values = {'A_init': [0, -4]}
self.sim.param_values = param_values
self.sim.run(param_values=param_values[0])

def test_param_values_dict(self):
param_values = {'A_init': [0, 100]}
initials = {self.model.monomers['B'](b=None): [250, 350]}
Expand Down

0 comments on commit 555b2f0

Please sign in to comment.