Repository: pybamm-team/PyBaMM

Issue:
[Bug]: PyBaMM fails to import when user has already imported a library that uses `mp.set_start_method`
### PyBaMM Version

develop

### Python Version

3.11

### Describe the bug

PyBaMM fails to import when importing a library that uses multiprocessing and has already set the start method to something other than fork, as a result of #3975. This should be apparent when reading the documentation for `mp.set_start_method`

Pending a long-term solution, I think that #3975 should at least have `mp.set_start_method` wrapped in a try-except -- most people, most of the time, do not really need the multiprocessing functionality, and I don't think pybamm should force those users to modify their multiprocessing configuration. It can throw a warning saying multiprocessing is not available or whatever.

Other solutions are welcome, of course. 
 

### Steps to Reproduce

module `thing`:
```
import multiprocessing as mp

mp.set_start_method("spawn")

def hello():
    print("hello, world")
```

script:
```
import thing
import pybamm
``` 

### Relevant log output

```shell
❯ python main.py 
Traceback (most recent call last):
  File "/Users/abills/.../main.py", line 2, in <module>
    import pybamm
  File "/Users/abills/.../python3.11/site-packages/pybamm/__init__.py", line 204, in <module>
    from .solvers.base_solver import BaseSolver
  File "/Users/abills/.../python3.11/site-packages/pybamm/solvers/base_solver.py", line 18, in <module>
    mp.set_start_method("fork")
  File "/Users/abills/.../python3.11/multiprocessing/context.py", line 247, in set_start_method
    raise RuntimeError('context has already been set')
RuntimeError: context has already been set
```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pybamm-team/PyBaMM/pull/4001?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team) Report
Attention: Patch coverage is `75.00000%` with `1 lines` in your changes are missing coverage. Please review.
> Project coverage is 99.58%. Comparing base [(`4cf09f3`)](https://app.codecov.io/gh/pybamm-team/PyBaMM/commit/4cf09f36bb4afb42c661e8765e14b4527581eff7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team) to head [(`58f93d4`)](https://app.codecov.io/gh/pybamm-team/PyBaMM/pull/4001?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team).

| [Files](https://app.codecov.io/gh/pybamm-team/PyBaMM/pull/4001?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team) | Patch % | Lines |
|---|---|---|
| [pybamm/solvers/base\_solver.py](https://app.codecov.io/gh/pybamm-team/PyBaMM/pull/4001?src=pr&el=tree&filepath=pybamm%2Fsolvers%2Fbase_solver.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team#diff-cHliYW1tL3NvbHZlcnMvYmFzZV9zb2x2ZXIucHk=) | 75.00% | [1 Missing :warning: ](https://app.codecov.io/gh/pybamm-team/PyBaMM/pull/4001?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #4001      +/-   ##
===========================================
- Coverage    99.58%   99.58%   -0.01%     
===========================================
  Files          257      257              
  Lines        21254    21255       +1     
===========================================
  Hits         21166    21166              
- Misses          88       89       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pybamm-team/PyBaMM/pull/4001?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pybamm-team).

BradyPlanden: This PR is ready for review. It's a small change to fix #3997. Coverage decreases since we only run the CI on Linux machines, open to suggestions on how to capture the missing line, otherwise it's a small decrease. Not sure a CHANGELOG entry is needed, but happy to add one if so.
agriyakhetarpal: It's not the best solution as such, but as a stop-gap, you can mark the conditions with a `# pragma: no cover` comment. It is also possible to exclude coverage for just a specific platform, like `# pragma: win32 no cover` – not sure about macOS or Linux, though. For them, I think
```python
# pragma: no cover windows # pragma: no cover macos
```

should work, based on GitHub code search results and [this StackOverflow answer](https://stackoverflow.com/q/41778073/14001839) which works based on specifying these lines in `[tool.coverage]` in `pyproject.toml` (and possibly adding another dependency or two in `[dev]` besides `coverage`).
valentinsulzer: Wouldn't the `return "spawn"` line still not be covered in the suggested function?

I wonder if we have been over-using "no cover" in the chase for 100% coverage. 99.5 % coverage is fine
kratman: > Wouldn't the `return "spawn"` line still not be covered in the suggested function?
> 
> I wonder if we have been over-using "no cover" in the chase for 100% coverage. 99.5 % coverage is fine

No because you can write a test for the function I put there. My function is basic input output so the test is just a couple lines:

```python
def test_mp_spawn():
    assert get_mp_context("fake_system") == "spawn"
```
kratman: > I wonder if we have been over-using "no cover" in the chase for 100% coverage. 99.5 % coverage is fine

In general, I feel 100% coverage usually requires faking coverage in some way:
- Tests on large entry points that don't really "test" anything meaningful
- Pragmas to ignore coverage
- etc

However, there are often simple functions that can be made to make trivial tests. Generally places that need long comments or switches to turn off tests/coverage are code smells that show the code needs to be broken up or refactored

Patch:
diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py
index 28dd4bdd9..0e60aca18 100644
--- a/pybamm/solvers/base_solver.py
+++ b/pybamm/solvers/base_solver.py
@@ -13,10 +13,6 @@ import numpy as np
 import pybamm
 from pybamm.expression_tree.binary_operators import _Heaviside
 
-# Set context for parallel processing depending on the platform
-if platform.system() == "Darwin" or platform.system() == "Linux":
-    mp.set_start_method("fork")
-
 
 class BaseSolver:
     """Solve a discretised model.
@@ -70,6 +66,7 @@ class BaseSolver:
         self.algebraic_solver = False
         self._on_extrapolation = "warn"
         self.computed_var_fcns = {}
+        self._mp_context = self.get_platform_context(platform.system())
 
     @property
     def root_method(self):
@@ -917,7 +914,7 @@ class BaseSolver:
                         model_inputs_list,
                     )
                 else:
-                    with mp.Pool(processes=nproc) as p:
+                    with mp.get_context(self._mp_context).Pool(processes=nproc) as p:
                         new_solutions = p.starmap(
                             self._integrate,
                             zip(
@@ -1395,6 +1392,12 @@ class BaseSolver:
                         "outside these bounds."
                     )
 
+    def get_platform_context(self, system_type: str):
+        # Set context for parallel processing depending on the platform
+        if system_type.lower() in ["linux", "darwin"]:
+            return "fork"
+        return "spawn"
+
     @staticmethod
     def _set_up_model_inputs(model, inputs):
         """Set up input parameters"""


Test patch:
diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py
index dcd4b5b85..9a6dec0ea 100644
--- a/tests/unit/test_solvers/test_base_solver.py
+++ b/tests/unit/test_solvers/test_base_solver.py
@@ -348,6 +348,12 @@ class TestBaseSolver(TestCase):
         with self.assertRaisesRegex(RuntimeError, "already been initialised"):
             solver.solve(model2, t_eval=[0, 1])
 
+    def test_multiprocess_context(self):
+        solver = pybamm.BaseSolver()
+        assert solver.get_platform_context("Win") == "spawn"
+        assert solver.get_platform_context("Linux") == "fork"
+        assert solver.get_platform_context("Darwin") == "fork"
+
     @unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed")
     def test_sensitivities(self):
         def exact_diff_a(y, a, b):