Repository: pybamm-team/PyBaMM (python)

Issue:
Raise error if empty model passed to discretise.process_model
Currently fails on `mass_matrix` step. Should raise an error straight away if model.rhs and model.algebraic are both empty dicts

Pull request description:
Issue 578 process empty model
# Description

Raise obvious error if an empty model is passed down the pipeline.

Fixes #578 

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)

# Key checklist:

- [x] No style issues: `$ flake8`
- [x] All tests pass: `$ python run-tests.py --unit`
- [x] The documentation builds: `$ cd docs` and then `$ make clean; make html`

You can run all three at once, using `$ python run-tests.py --quick`.

## Further checks: 

- [x] Code is commented, particularly in hard-to-understand areas
- [x] Tests added that prove fix is effective or that feature works
- [x] Any dependent changes have been merged and published in downstream modules


Patch:
diff --git a/.gitignore b/.gitignore
index 9d79ca0b4..ff6bf47a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,7 @@ pyproject.toml
 # virtual enviroment
 venv/
 venv3.5/
+PyBaMM-env/
 bin/
 etc/
 lib/
diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py
index bf712ba68..a00e8f6ae 100644
--- a/pybamm/discretisations/discretisation.py
+++ b/pybamm/discretisations/discretisation.py
@@ -87,12 +87,20 @@ class Discretisation(object):
             have also been discretised in place so model == model_disc. If
             ``inplace`` is False, model != model_disc
 
+        Raises
+        ------
+        :class:`pybamm.ModelError`
+            If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`)
+
         """
+        pybamm.logger.info("Start discretising {}".format(model.name))
+
+        # Make sure model isn't empty
+        if len(model.rhs) == 0 and len(model.algebraic) == 0:
+            raise pybamm.ModelError("Cannot discretise empty model")
         # Check well-posedness to avoid obscure errors
         model.check_well_posedness()
 
-        pybamm.logger.info("Start discretising {}".format(model.name))
-
         # Prepare discretisation
         # set variables (we require the full variable not just id)
         variables = list(model.rhs.keys()) + list(model.algebraic.keys())
diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py
index a6fb433aa..d57db2707 100644
--- a/pybamm/parameters/parameter_values.py
+++ b/pybamm/parameters/parameter_values.py
@@ -93,9 +93,17 @@ class ParameterValues(dict):
             * 'update': Calls :meth:`update_scalars()` for use on already-processed \
             model (update the value of any Scalars in the expression tree.)
 
+        Raises
+        ------
+        :class:`pybamm.ModelError`
+            If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`)
+
         """
         pybamm.logger.info("Start setting parameters for {}".format(model.name))
 
+        if len(model.rhs) == 0 and len(model.algebraic) == 0:
+            raise pybamm.ModelError("Cannot process parameters for empty model")
+
         if processing == "process":
             processing_function = self.process_symbol
         elif processing == "update":
diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py
index eb4a93e22..7b742fc25 100644
--- a/pybamm/solvers/base_solver.py
+++ b/pybamm/solvers/base_solver.py
@@ -47,9 +47,18 @@ class BaseSolver(object):
         t_eval : numeric type
             The times at which to compute the solution
 
+        Raises
+        ------
+        :class:`pybamm.ModelError`
+            If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`)
+
         """
         pybamm.logger.info("Start solving {}".format(model.name))
 
+        # Make sure model isn't empty
+        if len(model.rhs) == 0 and len(model.algebraic) == 0:
+            raise pybamm.ModelError("Cannot solve empty model")
+
         # Set up
         timer = pybamm.Timer()
         start_time = timer.time()
@@ -91,12 +100,21 @@ class BaseSolver(object):
             The number of points at which the solution will be returned during
             the step dt. default is 2 (returns the solution at t0 and t0 + dt).
 
+        Raises
+        ------
+        :class:`pybamm.ModelError`
+            If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`)
+
         """
+        # Make sure model isn't empty
+        if len(model.rhs) == 0 and len(model.algebraic) == 0:
+            raise pybamm.ModelError("Cannot step empty model")
+
         # Set timer
         timer = pybamm.Timer()
 
         # Run set up on first step
-        if not hasattr(self, 'y0'):
+        if not hasattr(self, "y0"):
             start_time = timer.time()
             self.set_up(model)
             self.t = 0.0
@@ -130,9 +148,7 @@ class BaseSolver(object):
             )
         else:
             pybamm.logger.info(
-                "Step time: {}".format(
-                    timer.format(solution.solve_time),
-                )
+                "Step time: {}".format(timer.format(solution.solve_time))
             )
         return solution
 


Test patch:
diff --git a/tests/unit/test_discretisations/test_discretisation.py b/tests/unit/test_discretisations/test_discretisation.py
index 820cde672..e1f2b134e 100644
--- a/tests/unit/test_discretisations/test_discretisation.py
+++ b/tests/unit/test_discretisations/test_discretisation.py
@@ -660,6 +660,12 @@ class TestDiscretise(unittest.TestCase):
         )
         discretised_model.check_well_posedness()
 
+    def test_process_empty_model(self):
+        model = pybamm.BaseModel()
+        disc = pybamm.Discretisation()
+        with self.assertRaisesRegex(pybamm.ModelError, "Cannot discretise empty model"):
+            disc.process_model(model)
+
     def test_broadcast(self):
         whole_cell = ["negative electrode", "separator", "positive electrode"]
 
diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py
index 10d09886d..738694f4c 100644
--- a/tests/unit/test_parameters/test_parameter_values.py
+++ b/tests/unit/test_parameters/test_parameter_values.py
@@ -353,6 +353,14 @@ class TestParameterValues(unittest.TestCase):
             isinstance(model.variables["d_var1"].children[1], pybamm.Variable)
         )
 
+    def test_process_empty_model(self):
+        model = pybamm.BaseModel()
+        parameter_values = pybamm.ParameterValues({"a": 1, "b": 2, "c": 3, "d": 42})
+        with self.assertRaisesRegex(
+            pybamm.ModelError, "Cannot process parameters for empty model"
+        ):
+            parameter_values.process_model(model)
+
 
 if __name__ == "__main__":
     print("Add -v for more debug output")
diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py
index ca64a7ad2..000b01c6b 100644
--- a/tests/unit/test_solvers/test_base_solver.py
+++ b/tests/unit/test_solvers/test_base_solver.py
@@ -14,13 +14,19 @@ class TestBaseSolver(unittest.TestCase):
         solver.tol = 1e-5
         self.assertEqual(solver.tol, 1e-5)
 
-        model = pybamm.BaseModel()
         with self.assertRaises(NotImplementedError):
-            solver.solve(model, None)
-            solver.step(model, None)
             solver.compute_solution(None, None)
+        with self.assertRaises(NotImplementedError):
             solver.set_up(None)
 
+    def test_step_or_solve_empty_model(self):
+        model = pybamm.BaseModel()
+        solver = pybamm.BaseSolver()
+        with self.assertRaisesRegex(pybamm.ModelError, "Cannot step empty model"):
+            solver.step(model, None)
+        with self.assertRaisesRegex(pybamm.ModelError, "Cannot solve empty model"):
+            solver.solve(model, None)
+
 
 if __name__ == "__main__":
     print("Add -v for more debug output")