Skip to content

Refactor transfer functions to handle division by zero and NaN values#588

Merged
selipot merged 3 commits intoCloud-Drift:mainfrom
selipot:fix-transfer-warnings
Jan 2, 2026
Merged

Refactor transfer functions to handle division by zero and NaN values#588
selipot merged 3 commits intoCloud-Drift:mainfrom
selipot:fix-transfer-warnings

Conversation

@selipot
Copy link
Copy Markdown
Member

@selipot selipot commented Jan 1, 2026

Fixes #582

  • 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.

- 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.
@selipot selipot requested a review from philippemiron January 1, 2026 16:32
@selipot selipot added bug Something isn't working enhancement New feature or request labels Jan 1, 2026
@selipot selipot self-assigned this Jan 1, 2026
@selipot selipot requested a review from Copilot January 1, 2026 16:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errstate context managers throughout transfer functions to suppress division and invalid operation warnings
  • Modified zo calculation to explicitly return np.inf when mu == 0 to avoid division by zero
  • Updated _xis function 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 TransferFunctionTestMethods class 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.

Comment on lines +900 to +958
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)
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +900 to +958
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)
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +889 to +891
self.bld = [100.0, 200.0, 200.0, np.inf]
self.K0 = [1 / 10, 1 / 20, 1 / 30]
self.K1 = [1, 2, 3]
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 1, 2026

Codecov Report

❌ Patch coverage is 84.13793% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
clouddrift/transfer.py 84.13% 23 Missing ⚠️

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@selipot selipot removed the bug Something isn't working label Jan 1, 2026
"""

zo = np.divide(delta**2, mu)
zo = np.inf if mu == 0 else np.divide(delta**2, mu)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

otherwise, if they are arrays, we can do:

zo = np.divide(delta**2, mu, out=np.full_like(mu, np.inf), where=mu != 0)

@selipot
Copy link
Copy Markdown
Member Author

selipot commented Jan 2, 2026

Thanks @philippemiron, I have now added more consistent references throughout transfer.py.

@selipot selipot merged commit 014daf1 into Cloud-Drift:main Jan 2, 2026
20 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Development

Successfully merging this pull request may close these issues.

🐛 warning in transfer.py module

3 participants