diff --git a/docs/models.md b/docs/models.md index 6d72e56..b8365ef 100644 --- a/docs/models.md +++ b/docs/models.md @@ -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. @@ -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`. diff --git a/src/pySODM/models/base.py b/src/pySODM/models/base.py index aa6eeb2..97f45c3 100644 --- a/src/pySODM/models/base.py +++ b/src/pySODM/models/base.py @@ -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: """ @@ -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) @@ -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) diff --git a/src/pySODM/models/validation.py b/src/pySODM/models/validation.py index ac5a223..fe83b9a 100644 --- a/src/pySODM/models/validation.py +++ b/src/pySODM/models/validation.py @@ -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) diff --git a/src/tests/test_JumpProcess.py b/src/tests/test_JumpProcess.py index 6023bd6..2d65460 100644 --- a/src/tests/test_JumpProcess.py +++ b/src/tests/test_JumpProcess.py @@ -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 """ @@ -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 ## @@ -787,6 +875,7 @@ def draw_function(parameters): ## Call all functions ## ######################## +test_formatting_user_model_class() test_SIR_time() test_SIR_date() test_SSA() diff --git a/src/tests/test_ODE.py b/src/tests/test_ODE.py index 3fd60f7..bc8bec2 100644 --- a/src/tests/test_ODE.py +++ b/src/tests/test_ODE.py @@ -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 """ @@ -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 ## @@ -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()