Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test the format of states, parameters, stratified_parameters, dimensions defined by user in model class #103

Merged
merged 5 commits into from
Oct 9, 2024
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
18 changes: 9 additions & 9 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ pySODM contains a class to build ordinary differential equation models (`ODE`) a
* 2 dimensions: states are 2-D (np.ndarray)
* etc.

* **stratified_parameters** (list) - optional - Names of the *stratified* parameters. Stratified parameters must be of type list/1D np.array and their length must be equal to the number of coordinates of the dimension axis it corresponds to. Their use is optional and mainly serves as a way for the user to structure his code.
* 0-D model: not possible to have *stratified* parameters
* 1-D model: list containing strings - `['stratpar_1', 'stratpar_2']`
* 2-D+ dimensions: List contains *n* sublists, where *n* is the number of model dimensions. Each sublist contains names of stratified parameters associated with the dimension in the corresponding position in `dimensions` - example for a 3-D model: `[['stratpar_1', 'stratpar_2'],[],['stratpar_3']]`, first dimension in `dimensions` has two stratified parameters `stratpar_1` and `stratpar_2`, second dimension has no stratified parameters, third dimensions has one stratified parameter `stratpar_3`.
* **stratified_parameters** (list) - optional - Names of the *stratified* parameters. Their use is optional and mainly serves as a way for the user to structure his code.
* 0 dimensions: not possible to have *stratified* parameters
* 1 dimension: list containing strings - `['stratpar_1', 'stratpar_2']`
* 2+ dimensions: List contains *n* sublists, where *n* is the number of model dimensions. Each sublist contains names of stratified parameters associated with the dimension in the corresponding position in `dimensions` - example for 3 dimensions model: `[['stratpar_1', 'stratpar_2'],[],['stratpar_3']]`, first dimension in `dimensions` has two stratified parameters `stratpar_1` and `stratpar_2`, second dimension has no stratified parameters, third dimensions has one stratified parameter `stratpar_3`.

* **dimensions_per_state** (list) - optional - Specify the dimensions of each model states. Allows users to define models with states of different sizes. If `dimensions_per_state` is not provided, all model states will have the same number of dimensions, equal to the number of model dimensions specified using `dimensions`. If specified, `dimensions_per_state` must contain *n* sublists, where *n* is the number of model states (`n = len(states)`). If a model state has no dimensions (i.e. it is a float), specify an empty list.

Expand Down Expand Up @@ -120,11 +120,11 @@ The parameters of the initial condition function become a part of the model's pa
* 2 dimensions: states are 2-D (np.ndarray)
* etc.

* **stratified_parameters** (list) - optional - Names of the *stratified* parameters. Stratified parameters must be of type list/1D np.array and their length must be equal to the number of coordinates of the dimension axis it corresponds to. Their use is optional and mainly serves as a way for the user to structure his code.
* 0-D model: not possible to have *stratified* parameters
* 1-D model: list containing strings - `['stratpar_1', 'stratpar_2']`
* 2-D+ dimensions: List contains *n* sublists, where *n* is the number of model dimensions. Each sublist contains names of stratified parameters associated with the dimension in the corresponding position in `dimensions` - example for a 3-D model: `[['stratpar_1', 'stratpar_2'],[],['stratpar_3']]`, first dimension in `dimensions` has two stratified parameters `stratpar_1` and `stratpar_2`, second dimension has no stratified parameters, third dimensions has one stratified parameter `stratpar_3`.

* **stratified_parameters** (list) - optional - Names of the *stratified* parameters. Their use is optional and mainly serves as a way for the user to structure his code.
* 0 dimensions: not possible to have *stratified* parameters
* 1 dimension: list containing strings - `['stratpar_1', 'stratpar_2']`
* 2+ dimensions: List contains *n* sublists, where *n* is the number of model dimensions. Each sublist contains names of stratified parameters associated with the dimension in the corresponding position in `dimensions` - example for 3 dimensions model: `[['stratpar_1', 'stratpar_2'],[],['stratpar_3']]`, first dimension in `dimensions` has two stratified parameters `stratpar_1` and `stratpar_2`, second dimension has no stratified parameters, third dimensions has one stratified parameter `stratpar_3`.
* **dimensions_per_state** (list) - optional - Specify the dimensions of each model states. Allows users to define models with states of different sizes. If `dimensions_per_state` is not provided, all model states will have the same number of dimensions, equal to the number of model dimensions specified using `dimensions`. If specified, `dimensions_per_state` must contain *n* sublists, where *n* is the number of model states (`n = len(states)`). If a model state has no dimensions (i.e. it is a float), specify an empty list.

Below is a minimal example of a user-defined model class inheriting `JumpProcesses`.
Expand Down
8 changes: 7 additions & 1 deletion src/pySODM/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
validate_time_dependent_parameters, validate_integrate, check_duplicates, build_state_sizes_dimensions, validate_dimensions_per_state, \
validate_initial_states, validate_integrate_or_compute_rates_signature, validate_provided_parameters, validate_parameter_stratified_sizes, \
validate_apply_transitionings_signature, validate_compute_rates, validate_apply_transitionings, validate_solution_methods_ODE, validate_solution_methods_JumpProcess, \
get_initial_states_fuction_parameters, check_overlap, evaluate_initial_condition_function
get_initial_states_fuction_parameters, check_overlap, evaluate_initial_condition_function, check_formatting_names

class JumpProcess:
"""
Expand Down Expand Up @@ -58,6 +58,9 @@ def __init__(self, initial_states, parameters, coordinates=None, time_dependent_
self.coordinates = coordinates
self.time_dependent_parameters = time_dependent_parameters

# Check formatting of state, parameters, dimension names user has defined in his model class
check_formatting_names(self.states_names, self.parameters_names, self.parameters_stratified_names, self.dimensions_names)

# Merge parameter_names and parameter_stratified_names
self.parameters_names_modeldeclaration = merge_parameter_names_parameter_stratified_names(self.parameters_names, self.parameters_stratified_names)

Expand Down Expand Up @@ -535,6 +538,9 @@ def __init__(self, initial_states, parameters, coordinates=None, time_dependent_
self.coordinates = coordinates
self.time_dependent_parameters = time_dependent_parameters

# Check formatting of state, parameters, dimension names user has defined in his model class
check_formatting_names(self.states_names, self.parameters_names, self.parameters_stratified_names, self.dimensions_names)

# Merge parameter_names and parameter_stratified_names
self.parameters_names_modeldeclaration = merge_parameter_names_parameter_stratified_names(self.parameters_names, self.parameters_stratified_names)

Expand Down
45 changes: 45 additions & 0 deletions src/pySODM/models/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,51 @@
from datetime import datetime, timedelta
from collections import OrderedDict

def check_formatting_names(states_names, parameters_names, parameters_stratified_names, dimensions_names):
"""
A function checking the format of the 'states', 'parameters', 'stratified_parameters' and 'dimensions' provided by the user in their model class
"""

# states_names/parameters_names
for val, name in zip([states_names, parameters_names], ['states', 'parameters']):
## list?
assert isinstance(val, list), f"'{name}' must be a list. found '{type(val)}'."
## containing strings
if not all(isinstance(v, str) for v in val):
raise TypeError(f"not all elements in '{name}' are of type str.")

# dimensions
if dimensions_names:
# list?
assert isinstance(dimensions_names, list), f"'dimensions' must be a list. found '{type(dimensions_names)}'."
# containing only strings
if not all(isinstance(v, str) for v in dimensions_names):
raise TypeError(f"not all elements in 'dimensions' are of type str.")
# exract number of dimensions
n_dims = len(dimensions_names)

# parameters_stratified_names
if parameters_stratified_names:
if not dimensions_names:
raise TypeError(f"a model without dimensions cannot have stratified parameters.")
elif n_dims == 1:
# list?
assert isinstance(parameters_stratified_names, list), f"'stratified_parameters' must be a list. found '{type(parameters_stratified_names)}'."
# containing only strings?
if not all(isinstance(v, str) for v in parameters_stratified_names):
raise TypeError(f"not all elements in 'stratified_parameters' are of type str.")
elif n_dims >= 2:
# list?
assert isinstance(parameters_stratified_names, list), f"'stratified_parameters' must be a list. found '{type(parameters_stratified_names)}'."
# containing n_dims sublists?
assert len(parameters_stratified_names) == n_dims, f"'stratified_parameters' must be a list containing {n_dims} sublists."
if not all(isinstance(sublist, list) for sublist in parameters_stratified_names):
raise TypeError(f"'stratified_parameters' must be a list containing {n_dims} sublists.")
# each containing only strings OR being empty?
for sublist in parameters_stratified_names:
if ((not all(isinstance(v, str) for v in sublist)) & (sublist != [])):
raise TypeError(f"'stratified_parameters' must be a list containing {n_dims} sublists. each sublist must either be empty or contain only str.")

def date_to_diff(actual_start_date, end_date):
"""
Convert date string to int (i.e. number of days since day 0 of simulation)
Expand Down
101 changes: 95 additions & 6 deletions src/tests/test_JumpProcess.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,99 @@ def apply_transitionings(t, tau, transitionings, S, I, R, beta, gamma):
R_new = R + transitionings['I'][0]
return S_new, I_new, R_new

def test_formatting_user_model_class():

# needed to init model
parameters = {"beta": 0.9, "gamma": 0.2}
initial_states = {"S": 1_000_000 - 10, "I": 10, "R": 0}

# States
# ------

# states is not a list
SIR.states = {'S': 100, 'I': 1, 'R': 0}
with pytest.raises(AssertionError, match="'states' must be a list"):
model = SIR(initial_states, parameters)

# states contains type other than string
SIR.states = ['S', 'I', 5]
with pytest.raises(TypeError, match="not all elements in 'states' are of type str."):
model = SIR(initial_states, parameters)

# reset states
SIR.states = ['S', 'I', 'R']

# Parameters
# ----------

# parameters is not a list
SIR.parameters = {'beta': 0.3, 'gamma': 5}
with pytest.raises(AssertionError, match="'parameters' must be a list"):
model = SIR(initial_states, parameters)

# parameters contains type other than string
SIR.parameters = ['beta', 5]
with pytest.raises(TypeError, match="not all elements in 'parameters' are of type str."):
model = SIR(initial_states, parameters)

# reset parameters
SIR.parameters = ['beta', 'gamma']

# Dimensions
# ----------

# dimensions is not a list
SIR.dimensions = ('age', 'location')
with pytest.raises(AssertionError, match="'dimensions' must be a list. found"):
model = SIR(initial_states, parameters)

# dimensions contains type other than string
SIR.dimensions = ['age', 5]
with pytest.raises(TypeError, match="not all elements in 'dimensions' are of type str."):
model = SIR(initial_states, parameters)

# reset dimensions
SIR.dimensions = None

# Stratified parameters
# ---------------------

# no dimensions
## can't have stratified parameters
SIR.stratified_parameters = ['beta']
with pytest.raises(TypeError, match="a model without dimensions cannot have stratified parameters."):
model = SIR(initial_states, parameters)

# 1 dimension
SIR.dimensions = ['age']
## must be a list
SIR.stratified_parameters = ('beta',)
with pytest.raises(AssertionError, match="'stratified_parameters' must be a list."):
model = SIR(initial_states, parameters)
## containing only str
SIR.stratified_parameters = ['beta', 5]
with pytest.raises(TypeError, match="not all elements in 'stratified_parameters' are of type str."):
model = SIR(initial_states, parameters)

# 2+ dimensions
SIR.dimensions = ['age', 'location']
## must be a list
SIR.stratified_parameters = ('beta',)
with pytest.raises(AssertionError, match="'stratified_parameters' must be a list."):
model = SIR(initial_states, parameters)
## containing len(dimensions) sublists
SIR.stratified_parameters = [['beta'], [], []]
with pytest.raises(AssertionError, match="'stratified_parameters' must be a list containing 2 sublists."):
model = SIR(initial_states, parameters)
## each of which may only contain str
SIR.stratified_parameters = [['beta'], [None, 6, 'blabla', True]]
with pytest.raises(TypeError, match="'stratified_parameters' must be a list containing 2 sublists. each sublist must either be empty or contain only str."):
model = SIR(initial_states, parameters)

# reset stratified parameters and dimensions
SIR.stratified_parameters = None
SIR.dimensions = None

def test_SIR_time():
""" Test the use of int/float/list time indexing
"""
Expand Down Expand Up @@ -349,15 +442,10 @@ def test_model_stratified_init_validation():
with pytest.raises(ValueError, match=msg):
SIRstratified(initial_states, parameters, coordinates=coordinates)

SIRstratified.parameters = ["gamma"]
SIRstratified.stratified_parameters = [["beta", "alpha"]]
with pytest.raises(ValueError, match=msg):
SIRstratified(initial_states, parameters, coordinates=coordinates)

# ensure to set back to correct ones
SIRstratified.states = ["S", "I", "R"]
SIRstratified.parameters = ["gamma"]
SIRstratified.stratified_parameters = [["beta"]]
SIRstratified.stratified_parameters = ["beta",]

############################################################
## A model with different dimensions for different states ##
Expand Down Expand Up @@ -787,6 +875,7 @@ def draw_function(parameters):
## Call all functions ##
########################

test_formatting_user_model_class()
test_SIR_time()
test_SIR_date()
test_SSA()
Expand Down
101 changes: 95 additions & 6 deletions src/tests/test_ODE.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,99 @@ def integrate(t, S, I, R, beta, gamma):
dR = gamma*I
return dS, dI, dR

def test_formatting_user_model_class():

# needed to init model
parameters = {"beta": 0.9, "gamma": 0.2}
initial_states = {"S": 1_000_000 - 10, "I": 10, "R": 0}

# States
# ------

# states is not a list
SIR.states = {'S': 100, 'I': 1, 'R': 0}
with pytest.raises(AssertionError, match="'states' must be a list"):
model = SIR(initial_states, parameters)

# states contains type other than string
SIR.states = ['S', 'I', 5]
with pytest.raises(TypeError, match="not all elements in 'states' are of type str."):
model = SIR(initial_states, parameters)

# reset states
SIR.states = ['S', 'I', 'R']

# Parameters
# ----------

# parameters is not a list
SIR.parameters = {'beta': 0.3, 'gamma': 5}
with pytest.raises(AssertionError, match="'parameters' must be a list"):
model = SIR(initial_states, parameters)

# parameters contains type other than string
SIR.parameters = ['beta', 5]
with pytest.raises(TypeError, match="not all elements in 'parameters' are of type str."):
model = SIR(initial_states, parameters)

# reset parameters
SIR.parameters = ['beta', 'gamma']

# Dimensions
# ----------

# dimensions is not a list
SIR.dimensions = ('age', 'location')
with pytest.raises(AssertionError, match="'dimensions' must be a list. found"):
model = SIR(initial_states, parameters)

# dimensions contains type other than string
SIR.dimensions = ['age', 5]
with pytest.raises(TypeError, match="not all elements in 'dimensions' are of type str."):
model = SIR(initial_states, parameters)

# reset dimensions
SIR.dimensions = None

# Stratified parameters
# ---------------------

# no dimensions
## can't have stratified parameters
SIR.stratified_parameters = ['beta']
with pytest.raises(TypeError, match="a model without dimensions cannot have stratified parameters."):
model = SIR(initial_states, parameters)

# 1 dimension
SIR.dimensions = ['age']
## must be a list
SIR.stratified_parameters = ('beta',)
with pytest.raises(AssertionError, match="'stratified_parameters' must be a list."):
model = SIR(initial_states, parameters)
## containing only str
SIR.stratified_parameters = ['beta', 5]
with pytest.raises(TypeError, match="not all elements in 'stratified_parameters' are of type str."):
model = SIR(initial_states, parameters)

# 2+ dimensions
SIR.dimensions = ['age', 'location']
## must be a list
SIR.stratified_parameters = ('beta',)
with pytest.raises(AssertionError, match="'stratified_parameters' must be a list."):
model = SIR(initial_states, parameters)
## containing len(dimensions) sublists
SIR.stratified_parameters = [['beta'], [], []]
with pytest.raises(AssertionError, match="'stratified_parameters' must be a list containing 2 sublists."):
model = SIR(initial_states, parameters)
## each of which may only contain str
SIR.stratified_parameters = [['beta'], [None, 6, 'blabla', True]]
with pytest.raises(TypeError, match="'stratified_parameters' must be a list containing 2 sublists. each sublist must either be empty or contain only str."):
model = SIR(initial_states, parameters)

# reset stratified parameters and dimensions
SIR.stratified_parameters = None
SIR.dimensions = None

def test_SIR_time():
""" Test the use of int/float/list time indexing
"""
Expand Down Expand Up @@ -351,15 +444,10 @@ def test_stratified_SIR_init_validation():
with pytest.raises(ValueError, match=msg):
SIRstratified(initial_states, parameters, coordinates=coordinates)

SIRstratified.parameters = ["gamma"]
SIRstratified.stratified_parameters = [["beta", "alpha"]]
with pytest.raises(ValueError, match=msg):
SIRstratified(initial_states, parameters, coordinates=coordinates)

# ensure to set back to correct ones
SIRstratified.states = ["S", "I", "R"]
SIRstratified.parameters = ["gamma"]
SIRstratified.stratified_parameters = [["beta"]]
SIRstratified.stratified_parameters = ["beta"]

############################################################
## A model with different dimensions for different states ##
Expand Down Expand Up @@ -768,6 +856,7 @@ def draw_function(parameters):
## Call all functions ##
########################

test_formatting_user_model_class()
test_SIR_time()
test_SIR_date()
test_SIR_discrete_stepper()
Expand Down
Loading