diff --git a/.coveragerc b/.coveragerc index ec497019..f1f8f6be 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,7 +3,5 @@ source = pyEpiabm omit = pyEpiabm/pyEpiabm/tests/* pyEpiabm/pyEpiabm/version_info.py - pyEpiabm/pyEpiabm/py2c/__init__.py - pyEpiabm/pyEpiabm/py2c/py2c_population.py pyEpiabm/run_tests.py pyEpiabm/setup.py diff --git a/pyEpiabm/docs/source/py2c.rst b/pyEpiabm/docs/source/py2c.rst index 65c40301..9cde4e3f 100644 --- a/pyEpiabm/docs/source/py2c.rst +++ b/pyEpiabm/docs/source/py2c.rst @@ -8,4 +8,4 @@ py2c provides a method for converting python populations to c population for spe Overview: -.. autofunction:: py2c_population +.. autofunction:: py2c_convert_population diff --git a/pyEpiabm/pyEpiabm/py2c/__init__.py b/pyEpiabm/pyEpiabm/py2c/__init__.py index ca89f2e3..b1e247df 100644 --- a/pyEpiabm/pyEpiabm/py2c/__init__.py +++ b/pyEpiabm/pyEpiabm/py2c/__init__.py @@ -7,4 +7,4 @@ """ -from .py2c_population import py2c_population +from .py2c_population import py2c_convert_population diff --git a/pyEpiabm/pyEpiabm/py2c/py2c_population.py b/pyEpiabm/pyEpiabm/py2c/py2c_population.py index 623d4fe2..fbde9949 100644 --- a/pyEpiabm/pyEpiabm/py2c/py2c_population.py +++ b/pyEpiabm/pyEpiabm/py2c/py2c_population.py @@ -3,7 +3,8 @@ from pyEpiabm.property import InfectionStatus -def py2c_population(py_population: Population, c_factory, c_status_map): +def py2c_convert_population(py_population: Population, + c_factory, c_status_map): return _py2c_converter(py_population, c_factory, c_status_map).c_population @@ -72,8 +73,8 @@ def _validate_households(self): else: assert household._microcell_index ==\ person.microcell._index, \ - "Household cannot link two people\ - in different microcells." + ("Household cannot link two people" + + " in different microcells.") if household._microcell_index is None: print("Warning: Empty Household exists.") for cell in self.py_population.cells: @@ -164,7 +165,7 @@ def _configure_households(self): for py_person in py_household.persons: c_person = c_cell.get_person(py_person._index[2]) assert c_person.set_household(hh_i), \ - "Person was already in a different household" + "Person was already in a different household." c_household.add_member(c_person.microcell_pos()) # Configure Household Parameters params = c_household.params() diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/__init__.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/__init__.py new file mode 100644 index 00000000..7660ae6c --- /dev/null +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/__init__.py @@ -0,0 +1,3 @@ +# +# Tests for subpackage pyEpiabm.py2c +# diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/test_py2c_population.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/test_py2c_population.py new file mode 100644 index 00000000..155ef9f4 --- /dev/null +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/test_py2c_population.py @@ -0,0 +1,341 @@ +import unittest +from unittest import mock + +import pyEpiabm as pe +from pyEpiabm.property import InfectionStatus +from pyEpiabm.py2c.py2c_population import _py2c_converter +from pyEpiabm.tests.test_unit.parameter_config_tests import TestPyEpiabm + + +class TestPy2C_Convertor(TestPyEpiabm): + """Test the 'py2c_convertor' class. + """ + @classmethod + def setUp(self) -> None: + """Sets up a population we can use throughout the test. + 2 people are located in one microcell. + """ + # Set up population for conversion to cEpiabm + self.pop = pe.Population() + self.pop.add_cells(1) + self.cell = self.pop.cells[0] + self.cell.add_microcells(1) + self.microcell = self.cell.microcells[0] + self.microcell.add_people(2) + self.persons = self.pop.cells[0].microcells[0].persons + + # Add location complexity to population + self.house = pe.Household(self.microcell, [1.0, 1.0]) + self.house.persons = [self.persons[0]] + self.persons[0].household = self.house + + self.microcell.add_place(1, (1, 1), pe.property.PlaceType.Workplace) + self.microcell.places[0].add_person(self.persons[0]) + + # Create mock cEpiabm objects for use in conversion call + self.cpp_fac = mock.Mock() + self.cpp_map = mock.MagicMock() + self.cpp_map.__getitem__.side_effect = lambda x: getattr(self.cpp_map, + str(x)) + + converter_path = 'pyEpiabm.py2c.py2c_population._py2c_converter' + + @mock.patch(converter_path) + def test_converter_func(self, mock_converter): + mock_converter.side_effect = NotImplementedError() + with self.assertRaises(NotImplementedError): + pe.py2c.py2c_convert_population(self.pop, self.cpp_fac, + self.cpp_map) + mock_converter.assert_called_once_with(self.pop, self.cpp_fac, + self.cpp_map) + + @mock.patch(converter_path + '._link_places') + @mock.patch(converter_path + '._configure_households') + @mock.patch(converter_path + '._configure_people') + @mock.patch(converter_path + '._add_people') + @mock.patch(converter_path + '._copy_structure') + @mock.patch('builtins.print') + def test__init__(self, _, *converter_mocks): + converter = _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + self.assertEqual(converter.py_population, self.pop) + self.assertEqual(converter.c_factory, self.cpp_fac) + self.assertEqual(converter.c_population, None) + self.assertEqual(converter.c_status_map, self.cpp_map) + for converter_mock in converter_mocks: + converter_mock.assert_called_once_with() + + @mock.patch(converter_path + '._validate_households') + @mock.patch('builtins.print') + def test_index_population(self, mock_print, mock_validate): + mock_validate.side_effect = NotImplementedError() + with self.assertRaises(NotImplementedError): + # This stops initialisation after index_pop call + converter = _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + self.assertEqual(converter.households, self.house) + self.assertEqual(converter.n_places, 1) + + self.assertIn('_index_population took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch(converter_path + '._copy_structure') + @mock.patch('builtins.print') + def test_validate_households(self, mock_print, mock_copy): + mock_copy.side_effect = NotImplementedError() + with self.assertRaises(NotImplementedError): + converter = _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + self.assertEqual(converter.households, self.house) + + self.house._microcell_index = None + self.house.persons = [] # Artifical empty household + converter = _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + + self.assertIn('_validate_households took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch(converter_path + '._link_places') + @mock.patch(converter_path + '._configure_households') + @mock.patch(converter_path + '._configure_people') + @mock.patch(converter_path + '._add_people') + @mock.patch(converter_path + '._copy_structure') + @mock.patch('builtins.print') + def test_catch_misindexed_households(self, _, *convertor_mocks): + # Run converter to generate correct household list + converter = _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + + # Modify household set to add error + temp_house = converter.households.pop() + temp_house._microcell_index = 2 + converter.households.add(temp_house) + + with self.assertRaises(AssertionError) as context: + converter._validate_households() + + err_msg = "Household cannot link two people in different microcells." + self.assertEqual(context.exception.args[0], err_msg) + + @mock.patch(converter_path + '._link_places') + @mock.patch(converter_path + '._configure_households') + @mock.patch(converter_path + '._configure_people') + @mock.patch(converter_path + '._add_people') + @mock.patch(converter_path + '._copy_structure') + @mock.patch('builtins.print') + def test_catch_empty_households(self, mock_print, *convertor_mocks): + # Run converter to generate correct household list + converter = _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + + # Modify household set to add error + temp_house = converter.households.pop() + temp_house.persons = [] + temp_house._microcell_index = None + converter.households.add(temp_house) + converter._validate_households() + + # Check penultimate print call (before second validation timing) + err_msg = "Warning: Empty Household exists." + self.assertEqual(str(mock_print.call_args_list[-2][0][0]), err_msg) + + def generate_cpp_mocked_pop(self): + cpp_mcell, cpp_cell, cpp_pop = [mock.Mock()] * 3 + cpp_cell.microcells.return_value = [cpp_mcell] + cpp_pop.cells.return_value = [cpp_cell] + + cpp_cell.index.return_value = 0 + cpp_cell.get_microcell.return_value = cpp_mcell + cpp_pop.get_cell.return_value = cpp_cell + return cpp_mcell, cpp_cell, cpp_pop + + @mock.patch(converter_path + '._add_people') + @mock.patch('builtins.print') + def test_copy_structure(self, mock_print, mock_add): + mock_add.side_effect = NotImplementedError() + cpp_mcell, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + self.cpp_fac.make_empty_population.return_value = cpp_pop + + with self.assertRaises(NotImplementedError): + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + self.cpp_fac.add_microcells.assert_called_once_with(cpp_cell, 1) + self.cpp_fac.add_households.assert_called_once_with(cpp_mcell, 1) + + self.assertIn('_copy_structure took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch(converter_path + '._configure_people') + @mock.patch('builtins.print') + def test_add_people(self, mock_print, mock_conf): + mock_conf.side_effect = NotImplementedError() + cpp_mcell, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + self.cpp_fac.make_empty_population.return_value = cpp_pop + + with self.assertRaises(NotImplementedError): + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + cpp_pop.get_cell.assert_called_once_with(0) + cpp_cell.get_microcell.assert_called_once_with(0) + self.cpp_fac.add_persons.assert_called_once_with(cpp_cell, + cpp_mcell, 2) + + self.assertIn('_add_people took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch(converter_path + '._configure_households') + @mock.patch('builtins.print') + def test_configure_people(self, mock_print, mock_conf): + mock_conf.side_effect = NotImplementedError() + _, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + + cpp_person = mock.Mock() + cpp_params = mock.MagicMock() + cpp_person.params.return_value = cpp_params + cpp_cell.persons.return_value = [cpp_person] * 5 + + cpp_inf = mock.PropertyMock(return_value=1) + type(cpp_params).infectiousness = cpp_inf + cpp_next_status = mock.PropertyMock() + type(cpp_params).next_status = cpp_next_status + + self.cpp_fac.make_empty_population.return_value = cpp_pop + self.cpp_map.__getitem__.side_effect = lambda x: getattr(self.cpp_map, + str(x)) + + # Modify python pop to add additional people with all infection stati + self.microcell.add_people(3) + for i, status in enumerate([InfectionStatus.Susceptible, + InfectionStatus.Exposed, + InfectionStatus.Recovered, + InfectionStatus.Dead, + InfectionStatus.InfectMild]): + self.microcell.persons[i].update_status(status) + self.microcell.persons[-1].next_infection_status = \ + InfectionStatus.Recovered + + with self.assertRaises(NotImplementedError): + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + + cpp_pop.get_cell.assert_called_once_with(0) + cpp_cell.index.assert_called_once_with() + + self.assertEqual(cpp_person.params.call_count, + len(cpp_cell.persons.return_value)) + self.assertEqual(cpp_inf.call_count, + len(cpp_cell.persons.return_value)) + cpp_next_status.assert_called_once_with( + self.cpp_map[InfectionStatus.Recovered]) + + cpp_cell.mark_non_infectious.assert_called_once_with(0) + cpp_cell.mark_exposed.assert_called_once_with(0) + cpp_cell.mark_recovered.assert_called_once_with(0) + cpp_cell.mark_dead.assert_called_once_with(0) + cpp_cell.mark_infectious.assert_called_once_with(0) + + self.assertIn('_configure_people took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch(converter_path + '._link_places') + @mock.patch('builtins.print') + def test_configure_households(self, mock_print, mock_link): + mock_link.side_effect = NotImplementedError() + cpp_mcell, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + cpp_cell.persons.return_value = [mock.Mock()] * 2 + self.cpp_fac.make_empty_population.return_value = cpp_pop + + # Create multiple person house + self.house.persons.append(self.persons[1]) + self.persons[1].household = self.house + + cpp_household = mock.Mock() + cpp_params = mock.MagicMock() + cpp_household.params.return_value = cpp_params + cpp_mcell.get_household.return_value = cpp_household + + cpp_inf = mock.PropertyMock() + cpp_location = mock.PropertyMock() + type(cpp_params).infectiousness = cpp_inf + type(cpp_params).location = cpp_location + + with self.assertRaises(NotImplementedError): + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + + cpp_mcell.get_household.assert_called_once_with(0) + cpp_inf.assert_called_once_with(self.house.infectiousness) + cpp_location.assert_called_once_with(self.house.location) + + self.assertIn('_configure_households took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch(converter_path + '._link_places') + @mock.patch('builtins.print') + def test_handle_neighbours(self, mock_print, mock_link): + cpp_mcell, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + cpp_cell.persons.return_value = [mock.Mock()] * 2 + self.cpp_fac.make_empty_population.return_value = cpp_pop + + # Add 'neighbour' to population - i.e. two households in one microcell + self.new_house = pe.Household(self.microcell, [1.0, 1.0]) + self.new_house.persons = [self.persons[1]] + self.persons[1].household = self.new_house + + cpp_household = mock.Mock() + cpp_mcell.get_household.return_value = cpp_household + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + + mock_link.assert_called_once_with() + self.assertIn('_configure_households took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch(converter_path + '._link_places') + @mock.patch('builtins.print') + def test_catch_misconfigured_households(self, _, mock_link): + cpp_mcell, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + cpp_cell.persons.return_value = [mock.Mock()] * 2 + self.cpp_fac.make_empty_population.return_value = cpp_pop + + cpp_household = mock.Mock() + cpp_mcell.get_household.return_value = cpp_household + + # Modify household set to add person to multiple households + second_house = pe.Household(self.microcell, [1.0, 1.0]) + second_house.persons = [self.persons[0]] # Now in two households + + cpp_person = mock.Mock() + cpp_person.set_household.return_value = False + cpp_cell.get_person.return_value = cpp_person + + with self.assertRaises(AssertionError) as context: + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + mock_link.assert_not_called() # As unhandled error thrown before this + + cpp_person.set_household.assert_called_once_with(0) + err_msg = "Person was already in a different household." + self.assertEqual(context.exception.args[0], err_msg) + + @mock.patch('builtins.print') + def test_link_places(self, mock_print): + _, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + cpp_cell.persons.return_value = [mock.Mock()] * 2 + self.cpp_fac.make_empty_population.return_value = cpp_pop + + cpp_place = mock.Mock() + cpp_place.index.return_value = 0 + cpp_pop.get_place.return_value = cpp_place + + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + cpp_pop.get_place.assert_called_once_with(0) + + self.assertIn('_link_places took', + mock_print.call_args_list[-1][0][0]) + + @mock.patch('builtins.print') + def test_mis_indexed_places(self, _): + _, cpp_cell, cpp_pop = self.generate_cpp_mocked_pop() + cpp_cell.persons.return_value = [mock.Mock()] * 2 + self.cpp_fac.make_empty_population.return_value = cpp_pop + + cpp_place = mock.Mock() + cpp_place.index.return_value = 1 + cpp_pop.get_place.return_value = cpp_place + + with self.assertRaises(AssertionError): + _py2c_converter(self.pop, self.cpp_fac, self.cpp_map) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/test_timer.py b/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/test_timer.py new file mode 100644 index 00000000..bbe44a73 --- /dev/null +++ b/pyEpiabm/pyEpiabm/tests/test_unit/test_py2c/test_timer.py @@ -0,0 +1,28 @@ +import unittest +from unittest import mock + +from pyEpiabm.py2c.py2c_population import _Timer +from pyEpiabm.tests.test_unit.parameter_config_tests import TestPyEpiabm + + +class TestTimer(TestPyEpiabm): + """Test the 'Timer' class. + """ + + @mock.patch('builtins.print') + def test__init__(self, _): + named_timer = _Timer('my_name') + self.assertEqual(named_timer.name, 'my_name') + + @mock.patch('builtins.print') + def test__del__(self, mock_print): + timer = _Timer('timer') + del timer + mock_print.assert_called_once() + args, _ = mock_print.call_args_list[0] + re_scientific_notation = r"(?:0|[1-9]\d*)(?:\.\d+)?(?:[e][+\-]?\d+)?" + self.assertRegex(args[0], f"timer took {re_scientific_notation}s") + + +if __name__ == '__main__': + unittest.main() diff --git a/python_examples/cEpiabm_example/python_to_cpp_population_converter.ipynb b/python_examples/cEpiabm_example/python_to_cpp_population_converter.ipynb index b809c974..94912ff5 100644 --- a/python_examples/cEpiabm_example/python_to_cpp_population_converter.ipynb +++ b/python_examples/cEpiabm_example/python_to_cpp_population_converter.ipynb @@ -91,7 +91,7 @@ } ], "source": [ - "c_population = pe.utility.py2c_population(py_population, c_factory, c_status_map)" + "c_population = pe.utility.py2c_convert_population(py_population, c_factory, c_status_map)" ] } ], diff --git a/python_examples/gibraltar_example_cpp/gibraltar_simulation_flow_cpp.py b/python_examples/gibraltar_example_cpp/gibraltar_simulation_flow_cpp.py index 9d6ca995..f20c4de5 100644 --- a/python_examples/gibraltar_example_cpp/gibraltar_simulation_flow_cpp.py +++ b/python_examples/gibraltar_example_cpp/gibraltar_simulation_flow_cpp.py @@ -69,7 +69,8 @@ pe.property.InfectionStatus.Susceptible: ce.InfectionStatus.Susceptible } logging.info("Converting python population to cpp.") -c_population = pe.py2c.py2c_population(population, c_factory, c_status_map) +c_population = pe.py2c.py2c_convert_population(population, c_factory, + c_status_map) logging.info("Configuring cpp simulation") simulation = ce.BasicSimulation(c_population)