Skip to content

use zero for land-values throughout Parcels#2762

Merged
erikvansebille merged 5 commits into
Parcels-code:mainfrom
erikvansebille:land_filling
Jul 24, 2026
Merged

use zero for land-values throughout Parcels#2762
erikvansebille merged 5 commits into
Parcels-code:mainfrom
erikvansebille:land_filling

Conversation

@erikvansebille

Copy link
Copy Markdown
Member

Description

This PR implements the definition that land values are zero in Parcels. This choice is intuitive for velocities (since ocean velocities are zero on land) and is needed for slip boundary conditions. For tracer fields, using zero for land also works nicer because NaNs currently throw errors. The XLinearInvdistLandTracer interpolator also assumes that land is 0.0

This PR adds a _assert_no_nan_in_field_data() function that throws a ValueError when there are NaNs in the Sgrid-dataset, and adds fillna(0) calls to some of the convert functions.

This PR supersedes #2451

Checklist

AI Disclosure

None

Comment thread src/parcels/_core/model.py Outdated
Comment on lines +514 to +515
arr = field_data.isel(time=0) if "time" in field_data.dims else field_data
if xr.ufuncs.isnan(arr).any():

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since isnan is not a lazy function, we need to be careful not to check the entire DataArray. Here only checking the first time step, under the assumption that if there is no land in that first time step it will also not be there in other time steps

Admittedly, this is a bit of a hacky fix, since we have to compute the entire first time slice of each Field

If you know of a cleaner way to do this, I'd be very open to that

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.

ah, didn't see this haha. see my other comment for suggestions

Comment thread src/parcels/_core/model.py Outdated
Comment on lines +513 to +518
def _assert_no_nan_in_field_data(field_data: xr.DataArray) -> None:
arr = field_data.isel(time=0) if "time" in field_data.dims else field_data
if xr.ufuncs.isnan(arr).any():
raise ValueError(
f"Field data {field_data.name!r} contains NaN values. Please fill or remove NaN values before using this field in a Parcels simulation."
)

@VeckoTheGecko VeckoTheGecko Jul 24, 2026

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.

This would be a computationally expensive check. Basically it would mean that we look at every value in every field for the first timeslice.

Since assert_valid_field_data is called in assert_valid_model_data, which in turn is called in StructuredModelData.__init__ means this check happens at FieldSet instantiation. Having slow class instantiation can also result in bad user experience, and I think with Copernicusmarine this would be the case.

Do you think you can quickly test the perf of this with copernicusmarine @erikvansebille ?

The options I see are:

  1. doing this eager check
  • cons: as above
  1. doing a ds.fillna(0) internally on the models
  • cons: its a potentially redundant operation
  1. using dataset metadata (if available)/trusting users to have the right data
  • cons: trusting user data

Perhaps (2) just makes the most sense since at least that's done lazily at runtime?

@VeckoTheGecko VeckoTheGecko Jul 24, 2026

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.

The nice thing about (2) is that we can ensure correctness, and for the performance hungry users we can offer the escape hatch of FieldSet.from_...(skip_field_data_validation=True) giving us (3)

@erikvansebille

Copy link
Copy Markdown
Member Author

I agree that your option (2) above is the best - and it seems to work well for Dask-arrays. But not for our Zarr backend.. Because if I make the change below

diff --git a/src/parcels/_core/model.py b/src/parcels/_core/model.py
index af208b3f..8c0e21d1 100644
--- a/src/parcels/_core/model.py
+++ b/src/parcels/_core/model.py
@@ -133,11 +133,13 @@ def preprocess_sgrid_model_data(ds: xr.Dataset) -> xr.Dataset:
 
 
 class StructuredModelData(ModelData):
-    def __init__(self, data: xr.Dataset, mesh: Mesh, vector_field_components: ptyping.VectorFields):
+    def __init__(self, data: xr.Dataset, mesh: Mesh, vector_field_components: ptyping.VectorFields, fill_nan_to_zero: bool = True):
         if not isinstance(data, xr.Dataset):
             raise ValueError(f"Expected `data` to be an xarray.Dataset . Got {type(data)}")
 
         data = preprocess_sgrid_model_data(data)
+        if fill_nan_to_zero:
+            data = data.fillna(0)
         grid = XGrid(data, mesh)

And then run pytest tests/test_fieldset.py::test_fieldset_describe_backends
I get

>       assert actual == expected
E       assert "| Name   | T...00000000'))\n" == "| Name   | T...00000000'))\n"
E         
E         Skipping 231 identical leading characters in diff, use -v to show
E         Skipping 309 identical trailing characters in diff, use -v to show
E         -         | Zarr              |
E         ?           ^^^^^
E         +         | NumPy             |
E         ?           ^^^^^...
E         
E         ...Full output truncated (8 lines hidden), use '-vv' to show

So the Zarr array seems to get converted to Numpy. Is that a problem?

erikvansebille added a commit to erikvansebille/Parcels that referenced this pull request Jul 24, 2026
Comment thread src/parcels/_core/fieldset.py Outdated
Comment on lines +312 to +315
fill_nan_to_zero : bool, optional
If True (default), fill NaN values in the dataset with zeros, which avoids Interpolation errors.
assert_valid_fields : bool, optional
If True (default), asserts that the Fields are valid, including checks for dimensions names. In some cases, it can be useful to be able to override this check, but thread with caution, as invalid Fields can lead to unexpected behavior during execution.

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.

I think we should just have one option and call it either fill_nan_to_zero or skip_field_data_validation. The emphasis here is to bypass expensive checks on the data itself.

Other validation (of coordinates, dimension ordering, dimension naming) is cheap and should always be done - hence I don't think we should have assert_valid_fields as an option here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah sorry, then I mistook your comment. I thought you also proposed a way to work around the validation checks in general.

In that case, I prefer skip_field_data_validation, and highlight in the docstring that means no NaN filling. I'll change

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Implemented in 2bb3d10

@erikvansebille
erikvansebille enabled auto-merge (squash) July 24, 2026 13:26
@erikvansebille
erikvansebille merged commit f38e0d1 into Parcels-code:main Jul 24, 2026
15 of 16 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Parcels development Jul 24, 2026
erikvansebille added a commit that referenced this pull request Jul 24, 2026
* Add support for copernicusmarine without depth

And convert NaNs to zeroes (for interpolators)

* Removing filling NaNs in convert

As will be done in model, per #2762
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Land as np.nan or 0 in v4?

2 participants