Repository: simpeg/simpeg
Pull request #1615 (2025-02-27 00:03:32)

Raise errors if dpred in `BaseDataMisfit` has nans
#### Summary

Make `BaseDataMisfit.residual` to raise an error if the simulation returns an array with `nan`s and/or `inf`s after calling the `dpred` method. Add tests to check if the errors are correctly raised. Remove error in case `BaseDataMisfit.data` is `None`: the `data` setter method doesn't allow `BaseDataMisfit.data` to be `None`, therefore the error will never be raised.

#### PR Checklist
* [ ] If this is a work in progress PR, set as a Draft PR
* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html).
* [ ] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code.
* [ ] Added necessary documentation to any new functions/classes following the
      expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html).
* [ ] Marked as ready for review (if this is was a draft PR), and converted
      to a Pull Request
* [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review.

#### Reference issue
<!--Example: write "Closes #NNNN" to automatically close that issue on merge.-->

#### What does this implement/fix?
<!--Please explain your changes.-->

#### Additional information
<!--Any additional information you think is important.-->


<!--
Once all tests pass and the code has been reviewed and approved, it will be merged into main
-->

TODO:

- [x] raise errors in infs and -infs in dpred as well (not only nans)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/simpeg/simpeg/pull/1615?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) Report
Attention: Patch coverage is `93.54839%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 86.52%. Comparing base [(`2e36896`)](https://app.codecov.io/gh/simpeg/simpeg/commit/2e3689625225955cc338dadad13cb3cdb4310e8b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) to head [(`045e18c`)](https://app.codecov.io/gh/simpeg/simpeg/commit/045e18c83ed408bf14d9470838fa88c5eddb92be?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg).

| [Files with missing lines](https://app.codecov.io/gh/simpeg/simpeg/pull/1615?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) | Patch % | Lines |
|---|---|---|
| [simpeg/data\_misfit.py](https://app.codecov.io/gh/simpeg/simpeg/pull/1615?src=pr&el=tree&filepath=simpeg%2Fdata_misfit.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg#diff-c2ltcGVnL2RhdGFfbWlzZml0LnB5) | 71.42% | [2 Missing :warning: ](https://app.codecov.io/gh/simpeg/simpeg/pull/1615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) |

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


```diff
@@            Coverage Diff             @@
##             main    #1615      +/-   ##
==========================================
- Coverage   86.52%   86.52%   -0.01%     
==========================================
  Files         409      409              
  Lines       52692    52721      +29     
  Branches     5000     5001       +1     
==========================================
+ Hits        45594    45618      +24     
- Misses       5678     5682       +4     
- Partials     1420     1421       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simpeg/simpeg/pull/1615?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg).   
: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=simpeg).


Patch:
diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py
index 6b489b425..7c0f1bbb3 100644
--- a/simpeg/data_misfit.py
+++ b/simpeg/data_misfit.py
@@ -225,9 +225,14 @@ class BaseDataMisfit(L2ObjectiveFunction):
         (n_data, ) numpy.ndarray
             The data residual vector.
         """
-        if self.data is None:
-            raise Exception("data must be set before a residual can be calculated.")
-        return self.simulation.residual(m, self.data.dobs, f=f)
+        dpred = self.simulation.dpred(m, f=f)
+        if np.isnan(dpred).any() or np.isinf(dpred).any():
+            msg = (
+                f"The `{type(self.simulation).__name__}.dpred()` method "
+                "returned an array that contains `nan`s and/or `inf`s."
+            )
+            raise ValueError(msg)
+        return dpred - self.data.dobs
 
 
 class L2DataMisfit(BaseDataMisfit):


Test patch:
diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py
index bf1d0cc08..323673b5b 100644
--- a/tests/base/test_data_misfit.py
+++ b/tests/base/test_data_misfit.py
@@ -1,3 +1,5 @@
+import re
+import pytest
 import unittest
 
 import numpy as np
@@ -5,6 +7,7 @@ import discretize
 
 from simpeg import maps
 from simpeg import data_misfit, simulation, survey
+from simpeg import Data
 
 
 class DataMisfitTest(unittest.TestCase):
@@ -69,5 +72,46 @@ class DataMisfitTest(unittest.TestCase):
         self.dmis.test(x=self.model, random_seed=17)
 
 
+class MockSimulation(simulation.BaseSimulation):
+    """
+    Mock simulation class that returns nans or infs in the dpred array.
+    """
+
+    def __init__(self, invalid_value=np.nan):
+        self.invalid_value = invalid_value
+        super().__init__()
+
+    def dpred(self, m=None, f=None):
+        a = np.arange(4, dtype=np.float64)
+        a[1] = self.invalid_value
+        return a
+
+
+class TestNanOrInfInResidual:
+    """Test errors if the simulation return dpred with nans or infs."""
+
+    @pytest.fixture
+    def n_data(self):
+        return 4
+
+    @pytest.fixture
+    def sample_survey(self, n_data):
+        receivers = survey.BaseRx(np.zeros(n_data)[:, np.newaxis])
+        source = survey.BaseSrc([receivers])
+        return survey.BaseSurvey([source])
+
+    @pytest.mark.parametrize("invalid_value", [np.nan, np.inf])
+    def test_error(self, sample_survey, invalid_value):
+        mock_simulation = MockSimulation(invalid_value)
+        data = Data(sample_survey)
+        dmisfit = data_misfit.BaseDataMisfit(data, mock_simulation)
+        msg = re.escape(
+            "The `MockSimulation.dpred()` method returned an array that contains "
+            "`nan`s and/or `inf`s."
+        )
+        with pytest.raises(ValueError, match=msg):
+            dmisfit.residual(m=None)
+
+
 if __name__ == "__main__":
     unittest.main()