-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vector Laplacian on C-grid #34
Vector Laplacian on C-grid #34
Conversation
…s into vec-Laplacian-Cgrid
Codecov Report
@@ Coverage Diff @@
## master #34 +/- ##
==========================================
+ Coverage 98.19% 98.47% +0.27%
==========================================
Files 7 7
Lines 499 719 +220
==========================================
+ Hits 490 708 +218
- Misses 9 11 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
The figures above compare diffusion-filtered vs. viscosity-filtered velocity for NeverWorld2 data. The fields are filtered from 1/16 degree to 1 degree. As expected, the differences between the two filters become more visible as you move to higher latitudes. Close to the equator, the filters produce the same result, because here the NeverWorld2 grid is essentially Cartesian. |
I discussed this with @NoraLoose yesterday and she suggested that the numerical stability could be related to the fact that the lat/lon Neverworld2 grid is extremely anisotropic near the poleward boundaries. I support merging this PR and then continuing to work on numerical stability in the future. It would be nice to see if the stability problems persist on a more realistic (e.g. Mercator) grid, with cells that are closer to unit aspect ratio. |
On the other hand, the diffusion-based filter also knows about the extreme anisotropy of the NeverWorld2 grid. Nonetheless, the diffusion-based filter works okay. Another possibility is the handling of boundaries: this is where the diffusion-based and viscosity-based filter differ quite a bit. You can even see that in the figures above (right-most panels). There seems to be some noise building up, particularly at the southern and northern boundaries. One could potentially look into whether there is a better way to handle the wet masks than done here: |
Sorry for taking so long to respond to this. I was on vacation last week and have still not managed to catch up! 🙃 I have just created the group @ocean-eddy-cpt/gcm-filters-dev which now has admin rights on this repo. It includes myself, @NoraLoose and @iangrooms, but we can add anyone else interested in maintaining. At this point, I don't want to be a bottleneck on progress here. As long as someone from @ocean-eddy-cpt/gcm-filters-dev reviews and approves the PR, they should feel empowered to merge. I can try to review early next week if desired. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some superficial comments.
gcm_filters/filter.py
Outdated
@@ -83,6 +83,7 @@ def _compute_filter_spec( | |||
else: # ndim==2 | |||
n_steps = np.ceil(6.4 * filter_scale / dx_min).astype(int) | |||
|
|||
print("n_steps = %i" % n_steps) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No print statements in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching this! The print statement is now removed.
tests/test_filter.py
Outdated
# check conservation under solid body rotation: u = cos(lat), v=0; | ||
# this test fails due to numerical instabilities | ||
# data_u = np.cos(geolat_u/360*2*np.pi) | ||
# data_v = np.zeros_like(data_u) | ||
# da_u = xr.DataArray(data_u, dims=["y", "x"]) | ||
# da_v = xr.DataArray(data_v, dims=["y", "x"]) | ||
# filtered_u, filtered_v = filter.apply_to_vector(da_u, da_v, dims=["y", "x"]) | ||
# xr.testing.assert_allclose(filtered_u, 0.0, atol=1e-12) | ||
# xr.testing.assert_allclose(filtered_v, 0.0, atol=1e-12) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why commented-out code? Probably best to avoid this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed the test and uncommented it.
(I had wrongly assumed that numerical instability had caused the test to fail, but it was actually a small bug in the test - which is now fixed.)
- correct typo in test that checks invariance of viscosity-based filter under solid body rotation - test was previously commented out, but passes now
- change field_u_bar to ufield_bar and similar
- If the user provides area_u or area_v that contain zeros (as is the case for e.g., MOM6 output over land) then the previous version did a division by zero; - This issue is now fixed via a conditional operation.
I made a small modification to the code. The previous version did a division by zero if the user provides
The code still raises a The following version would avoid the warning on CPUs, but does not work on GPUs:
Can anyone think of a practical solution here? Maybe @jbusecke? I also added a new test |
Good catch! Ready for another review here? I don't know what's up with the RTD build 🤔 |
I'm also not sure why the Read the Docs build suddenly fails. In my last three commits I haven't changed anything that should affect the docs. Any ideas? |
The most likely reason is the one of the packages that the docs depend on has released a new version with an incompatibility. The quick solution is to figure out which one it is and then pin a specific version number here: gcm-filters/docs/environment.yml Lines 2 to 22 in 64b3b8e
Unfortunately the error does not immediately make it obvious which one. Myst-nb had a minor release (0.12.3) about a month ago, so that could be it: https://github.com/executablebooks/MyST-NB/releases/ |
Sure, thanks! 👍 |
* Goal: Get the RTD build to work again
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome Nora. It will be so extremely useful in many different scenarios.
My main concern is that we are overloading the BaseLaplacian
class a little too heavily. See detailed comments below.
gcm_filters/kernels.py
Outdated
@@ -290,6 +299,115 @@ def __call__(self, field: ArrayType): | |||
ALL_KERNELS[GridType.TRIPOLAR_POP_WITH_LAND] = POPTripolarLaplacianTpoint | |||
|
|||
|
|||
@dataclass | |||
class VectorLaplacian(BaseLaplacian): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class VectorLaplacian(BaseLaplacian): | |
class CgridVectorLaplacian(BaseLaplacian): |
Maybe this name would be a bit more specific?
I also wonder whether we should use different base classes, e.g. BaseScalarLaplacian
, BaseVectorLaplacian
. This would clarify a few ambiguities in the code.
self.recip_area_u = np.where(self.area_u > 0, 1 / self.area_u, 0) | ||
self.recip_area_v = np.where(self.area_v > 0, 1 / self.area_v, 0) | ||
|
||
def __call__(self, ufield: ArrayType, vfield: ArrayType): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern here is the the signature of this Laplacian does not match the signature of BaseLaplacian
, which is
def __call__(self, field):
I'm surprised that mypy did not complain about that. In any case, it supports the idea suggested above that we should have a separate base class for vector laplacians.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Definitely a good idea to have two separate base classes.
tests/test_filter.py
Outdated
@@ -67,7 +67,11 @@ def test_filter_spec(filter_args, expected_filter_spec): | |||
# TODO: check other properties of filter_spec? | |||
|
|||
|
|||
@pytest.fixture(scope="module", params=list(GridType)) | |||
vector_grids = [gt for gt in GridType if gt.name.startswith("VECTOR")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parsing the name string does not seem like a robust way to detect vector laplacians. Instead we should somehow refactor our base data structures to store this information. In the meantime, simply hard-coding the grid types here would not be a bad way to go. There are not so many.
# with regular arguments | ||
assert len(args) == len(Laplacian.required_grid_args()) | ||
grid_vars = {k: v for k, v in zip(Laplacian.required_grid_args(), args)} | ||
laplacian = Laplacian(**grid_vars) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the user passes a non-vector Laplacian here? It should not be allowed.
def apply_to_vector(self, ufield, vfield, dims): | ||
"""Filter a vector field across the dimensions specified by dims.""" | ||
|
||
filter_func_vec = _create_filter_func_vec(self.filter_spec, self.Laplacian) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be able to raise an informative error if a user tries to do a vector filter with a scalar laplacian I think we can resolve this by implementing the suggestion to have a separate BaseScalarLaplacian
and BaseVectorLaplacian
and then checking the type in the Filter.__post_init__
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is an attempt to raise an error based on the two base classes:
https://github.com/NoraLoose/gcm-filters/blob/59bb142a8c01224138d00026000a1732873271de/gcm_filters/filter.py#L330-L334
Does this look good to you?
- one for scalar Laplacians, one for vector Laplacians
- and vice versa
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic. Really excited to try this.
gcm_filters/filter.py
Outdated
if not issubclass(self.Laplacian, BaseScalarLaplacian): | ||
raise ValueError( | ||
f"Provided Laplacian {self.Laplacian} is a vector Laplacian. " | ||
f"The .apply method is only suitable for scalar Laplacians." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f"The .apply method is only suitable for scalar Laplacians." | |
f"The ``.apply`` method is only suitable for scalar Laplacians." |
gcm_filters/filter.py
Outdated
if not issubclass(self.Laplacian, BaseVectorLaplacian): | ||
raise ValueError( | ||
f"Provided Laplacian {self.Laplacian} is a scalar Laplacian. " | ||
f"The .apply_to_vector method is only suitable for vector Laplacians." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f"The .apply_to_vector method is only suitable for vector Laplacians." | |
f"The ``.apply_to_vector`` method is only suitable for vector Laplacians." |
vector_grid_type_field_and_extra_kwargs, | ||
): | ||
"""This test checks that vector Laplacians are invariant under solid body rotations: | ||
a corollary of conserving angular momentum.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, I love this test! 🎉
I manually resolved the merge conflict and it made pre-commit mad. But I'm just going to merge this so we can move forward. |
Summary
This PR adds a vector Laplacian on a C-grid mimicking the MOM6 implementation of viscosity, based on the friction operator suggested by Griffies & Hallberg, 2000.
Tests
While our scalar Laplacians are tested to conserve the area integral, the new vector Laplacian is designed to preserve angular momentum. A corollary of angular momentum conservation is that the vector Laplacian is invariant under solid body rotations. The latter is the test that I implemented for the vector Laplacian. Shoutout to @adcroft, who helped with designing this vector Laplacian test!
Numerical instability
While the viscosity-based filter (i.e., the filter using the vector Laplacian) seems to produce reasonable filtered fields (see also figure below), it unfortunately seems way more sensitive to numerical instability than the diffusion-based filter (the filter using the scalar Laplacian). Tests with NeverWorld2 data suggest that the old stage order (before #32) is more stable for the vector Laplacian. I don't know why... I am happy to revert this PR to "draft" until we have figured this out.
A few other (minor) notes:
filter.py
has two separate functions:_create_filter_func
and_create_filter_func_vec
; as well as methods:apply
andapply_to_vector
, to treat diffusion- vs. viscosity-based filters. There is probably a more elegant way to do this. Suggestions are welcome!n=(n_1, n_2)
that is input by the user) a la Smith and McWilliams, 2003 - see also MOM6 code and this issue on how to correctly implement this.