Refactor transfer functions to handle division by zero and NaN values#588
Refactor transfer functions to handle division by zero and NaN values#588selipot merged 3 commits intoCloud-Drift:mainfrom
Conversation
- Added np.errstate context managers to suppress warnings and handle divisions by zero in various transfer functions. - Updated the calculation of the `zo` variable to return np.inf when mu is zero to avoid division by zero errors. - Modified the `_xis` function to return NaN for non-finite values. - Enhanced unit tests to ensure proper handling of edge cases and validate output shapes for both no-slip and free-slip boundary conditions. - Renamed test methods for clarity and consistency with the new transfer function implementations.
There was a problem hiding this comment.
Pull request overview
This PR refactors transfer functions in clouddrift/transfer.py to properly handle division by zero and NaN values, addressing warnings identified in issue #582. The changes add comprehensive error state management and improve test coverage for edge cases.
- Added
np.errstatecontext managers throughout transfer functions to suppress division and invalid operation warnings - Modified
zocalculation to explicitly returnnp.infwhenmu == 0to avoid division by zero - Updated
_xisfunction to replace non-finite values with NaN for proper propagation - Enhanced test suite with expanded coverage for both "lilly" and "elipot" methods across no-slip and free-slip boundary conditions
- Uncommented and updated
TransferFunctionTestMethodsclass to validate method equivalence
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| clouddrift/transfer.py | Added np.errstate context managers across multiple transfer functions; modified zo calculations to handle mu == 0 explicitly; updated _xis to convert non-finite values to NaN |
| tests/transfer_test.py | Renamed test methods for clarity; added comprehensive test coverage for both methods and boundary conditions; uncommented TransferFunctionTestMethods class; updated assertions to handle NaN values with equal_nan=True |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for s in [1, -1]: | ||
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | ||
| for j, bld in enumerate(self.bld): | ||
| Ge, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="elipot", | ||
| boundary_condition=self.slipstr1, | ||
| ) | ||
| Gl, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="lilly", | ||
| boundary_condition=self.slipstr1, | ||
| ) | ||
| # bool_idx = Ge != np.nan and Gl != np.nan | ||
| # print(Ge[bool_idx], Gl[bool_idx]) | ||
| print(Ge, Gl) | ||
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | ||
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | ||
| self.assertTrue(bool1) | ||
|
|
||
| def test_method_equivalence_free_slip(self): | ||
| for s in [1, -1]: | ||
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | ||
| for j, bld in enumerate(self.bld): | ||
| Ge, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="elipot", | ||
| boundary_condition=self.slipstr2, | ||
| ) | ||
| Gl, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="lilly", | ||
| boundary_condition=self.slipstr2, | ||
| ) | ||
| # bool_idx = Ge != np.nan and Gl != np.nan | ||
| # print(Ge, Gl) | ||
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | ||
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | ||
| self.assertTrue(bool1) |
There was a problem hiding this comment.
The loop variable 's' is defined but never used inside the loop. If this variable is intended to test with different signs of the Coriolis frequency, it should be passed to the wind_transfer function (e.g., as s * self.cor_freq). Otherwise, this loop iteration is redundant.
| for s in [1, -1]: | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge[bool_idx], Gl[bool_idx]) | |
| print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) | |
| def test_method_equivalence_free_slip(self): | |
| for s in [1, -1]: | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge[bool_idx], Gl[bool_idx]) | |
| print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) | |
| def test_method_equivalence_free_slip(self): | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) |
| for s in [1, -1]: | ||
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | ||
| for j, bld in enumerate(self.bld): | ||
| Ge, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="elipot", | ||
| boundary_condition=self.slipstr1, | ||
| ) | ||
| Gl, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="lilly", | ||
| boundary_condition=self.slipstr1, | ||
| ) | ||
| # bool_idx = Ge != np.nan and Gl != np.nan | ||
| # print(Ge[bool_idx], Gl[bool_idx]) | ||
| print(Ge, Gl) | ||
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | ||
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | ||
| self.assertTrue(bool1) | ||
|
|
||
| def test_method_equivalence_free_slip(self): | ||
| for s in [1, -1]: | ||
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | ||
| for j, bld in enumerate(self.bld): | ||
| Ge, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="elipot", | ||
| boundary_condition=self.slipstr2, | ||
| ) | ||
| Gl, _, _ = wind_transfer( | ||
| self.omega, | ||
| self.z, | ||
| self.cor_freq, | ||
| delta, | ||
| mu, | ||
| bld, | ||
| method="lilly", | ||
| boundary_condition=self.slipstr2, | ||
| ) | ||
| # bool_idx = Ge != np.nan and Gl != np.nan | ||
| # print(Ge, Gl) | ||
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | ||
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | ||
| self.assertTrue(bool1) |
There was a problem hiding this comment.
The loop variable 's' is defined but never used inside the loop. If this variable is intended to test with different signs of the Coriolis frequency, it should be passed to the wind_transfer function (e.g., as s * self.cor_freq). Otherwise, this loop iteration is redundant.
| for s in [1, -1]: | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge[bool_idx], Gl[bool_idx]) | |
| print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) | |
| def test_method_equivalence_free_slip(self): | |
| for s in [1, -1]: | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr1, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge[bool_idx], Gl[bool_idx]) | |
| print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) | |
| def test_method_equivalence_free_slip(self): | |
| for i, (delta, mu) in enumerate(zip(self.delta, self.mu)): | |
| for j, bld in enumerate(self.bld): | |
| Ge, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="elipot", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| Gl, _, _ = wind_transfer( | |
| self.omega, | |
| self.z, | |
| self.cor_freq, | |
| delta, | |
| mu, | |
| bld, | |
| method="lilly", | |
| boundary_condition=self.slipstr2, | |
| ) | |
| # bool_idx = Ge != np.nan and Gl != np.nan | |
| # print(Ge, Gl) | |
| # bool1 = np.allclose(Ge[bool_idx], Gl[bool_idx], atol=1e-8) | |
| bool1 = np.allclose(Ge, Gl, atol=1e-8, equal_nan=True) | |
| self.assertTrue(bool1) |
tests/transfer_test.py
Outdated
| self.bld = [100.0, 200.0, 200.0, np.inf] | ||
| self.K0 = [1 / 10, 1 / 20, 1 / 30] | ||
| self.K1 = [1, 2, 3] |
There was a problem hiding this comment.
The self.bld list has 4 elements [100.0, 200.0, 200.0, np.inf], but self.K0 and self.K1 only have 3 elements each. This could lead to unexpected test behavior in the nested loops where all combinations are tested. Consider ensuring that the lists have compatible lengths or clarifying the intended test combinations.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! 🚀 New features to boost your workflow:
|
clouddrift/transfer.py
Outdated
| """ | ||
|
|
||
| zo = np.divide(delta**2, mu) | ||
| zo = np.inf if mu == 0 else np.divide(delta**2, mu) |
There was a problem hiding this comment.
otherwise, if they are arrays, we can do:
zo = np.divide(delta**2, mu, out=np.full_like(mu, np.inf), where=mu != 0)
|
Thanks @philippemiron, I have now added more consistent references throughout |
Fixes #582
zovariable to return np.inf when mu is zero to avoid division by zero errors._xisfunction to return NaN for non-finite values.