Skip to content

Fix NaN geolocation crash in swath matchup (DSCOVR EPIC / HE5 fill values) - #119

Merged
eeholmes merged 2 commits into
mainfrom
copilot/fix-latitude-longitude-nan
Mar 16, 2026
Merged

Fix NaN geolocation crash in swath matchup (DSCOVR EPIC / HE5 fill values)#119
eeholmes merged 2 commits into
mainfrom
copilot/fix-latitude-longitude-nan

Conversation

Copilot AI commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Swath products like DSCOVR EPIC store a large fill value (~−1.27e30) for pixels outside the valid Earth disk. xarray converts these to NaN on read, and scipy's/xoak's KD-tree raises ValueError when NaN values appear in the coordinate arrays passed to set_xindex().

Changes

  • _drop_nan_geoloc() helper — new function that stacks all spatial dimensions, drops pixels where lat or lon is NaN/Inf, and returns a 1-D filtered dataset safe for set_xindex(). Fast-path when all coordinates are finite.
  • Applied in three extraction paths — called after the optional 1-D→2-D meshgrid broadcast and before set_xindex() in _extract_xoak(), _extract_xoak_batch(), and _extract_ndpoint_batch().
  • Regression tests added to TestXoakSpatialMethod and TestNdpointSpatialMethod: swath with NaN lat/lon in the last row (mimicking EPIC fill pixels), querying an exact valid pixel, asserting the returned value matches the expected SST (not NaN, no exception).
# Before: raises ValueError when lat/lon contain NaN from fill values
res = pc.matchup(plan, variables=["UVAerosolIndex"], open_method=discovr_epic_aer)

# After: NaN pixels are silently excluded from the k-d tree; valid pixels match normally
Original prompt

This section details on the original issue you should resolve

<issue_title>For some swath data, Latitude/Longitude is nan outside of swath</issue_title>
<issue_description>Task: Matchups should not fail if Latitude/Longitude have nan

Background. I am debugging a matchup algorithm for DSCOVR EPIC swath data. The HE5 file uses fill values of about -1.2676506e30 outside the valid Earth disk. These should be treated as missing, but the matchup code appears to pass them through and later produces NaNs. Here is a summary of the file structure and a small extracted subset. Please inspect the matchup logic and identify where fill values should be masked before spatial matching.

This produces the error

discovr_epic_aer = {
    'xarray_open': 'dataset',
    'merge': ['/HDFEOS/SWATHS/Aerosol NearUV Swath/Geolocation Fields', '/HDFEOS/SWATHS/Aerosol NearUV Swath/Data Fields'],
    'open_kwargs': {'phony_dims':'access'}
}
res = pc.matchup(plan, 
                 variables = ["UVAerosolIndex"], 
                 open_method=discovr_epic_aer)

ValueError Traceback (most recent call last)
Cell In[11], line 1
----> 1 get_ipython().run_cell_magic('time', '', 'res = pc.matchup(plan, \n variables = ["UVAerosolIndex"], \n open_method=discovr_epic_aer)\n')

File /srv/conda/envs/notebook/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2572, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
2570 with self.builtin_trap:
2571 args = (magic_arg_s, cell)
-> 2572 result = fn(*args, **kwargs)
2574 # The code below prevents the output from being displayed
2575 # when using magics with decorator @output_can_be_silenced
2576 # when the last Python token in the expression is a ';'.
2577 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /srv/conda/envs/notebook/lib/python3.12/site-packages/IPython/core/magics/execution.py:1447, in ExecutionMagics.time(self, line, cell, local_ns)
1445 if interrupt_occured:
1446 if exit_on_interrupt and captured_exception:
-> 1447 raise captured_exception
1448 return
1449 return out

File /srv/conda/envs/notebook/lib/python3.12/site-packages/IPython/core/magics/execution.py:1411, in ExecutionMagics.time(self, line, cell, local_ns)
1409 st = clock2()
1410 try:
-> 1411 exec(code, glob, local_ns)
1412 out = None
1413 # multi-line %%time case

File :1

File ~/point-collocation/src/point_collocation/core/engine.py:283, in matchup(plan, open_method, variables, spatial_method, open_dataset_kwargs, silent, batch_size, save_dir, granule_range)
280 spec = _normalize_open_method(effective_open_method, open_dataset_kwargs)
282 effective_vars: list[str] = variables if variables is not None else plan.variables
--> 283 return _execute_plan(
284 plan,
285 spec=spec,
286 spatial_method=spatial_method,
287 variables=effective_vars,
288 silent=silent,
289 batch_size=batch_size,
290 save_dir=save_dir,
291 granule_range=granule_range,
292 )

File ~/point-collocation/src/point_collocation/core/engine.py:693, in _execute_plan(plan, spec, spatial_method, variables, silent, batch_size, save_dir, granule_range)
691 _extract_xoak_batch(ds, rows_for_granule, variables, lon_name, lat_name, time_dim)
692 else:
--> 693 _extract_ndpoint_batch(ds, rows_for_granule, variables, lon_name, lat_name, time_dim)
694 output_rows.extend(rows_for_granule)
695 batch_rows.extend(rows_for_granule)

File ~/point-collocation/src/point_collocation/core/engine.py:1263, in _extract_ndpoint_batch(ds, rows, variables, lon_name, lat_name, time_dim)
1258 ds_work[lon_name] = xr.DataArray(lon_2d, dims=lat_dims)
1260 # Build the NDPointIndex once for all query points using the built-in
1261 # scipy adapter (ScipyKDTreeAdapter). No tree_adapter_cls argument is
1262 # passed so xarray's default applies.
-> 1263 indexed_ds ...


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Co-authored-by: eeholmes <2545978+eeholmes@users.noreply.github.com>
Copilot AI changed the title [WIP] [ISSUE-123] Fix matchup logic for NaN latitude/longitude Fix NaN geolocation crash in swath matchup (DSCOVR EPIC / HE5 fill values) Mar 16, 2026
Copilot AI requested a review from eeholmes March 16, 2026 23:38
@eeholmes

Copy link
Copy Markdown
Member

@copilot what are the speed costs of this fix? Should I restrict to cases where there are nan's only

Copilot AI commented Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot what are the speed costs of this fix? Should I restrict to cases where there are nan's only

The fix already restricts the expensive work to NaN cases only. Here's the breakdown on a 2048×2048 swath (4M pixels):

  • No NaN (fast path): ~2 ms — just two np.isfinite scans that short-circuit immediately once the array is confirmed clean, then return ds unchanged. The coordinate arrays are already in memory at that point (.compute() is called just before), so this is effectively free relative to k-d tree build time and disk I/O.
  • NaN present: ~67 ms — does the stack/filter/isel to remove bad pixels before building the index.

The key lines in _drop_nan_geoloc:

if np.all(np.isfinite(lat_vals)) and np.all(np.isfinite(lon_vals)):
    return ds  # Fast path — nothing to do.

No stacking, no indexing, no copy — just the np.isfinite scan. So there's no benefit to adding an additional outer check; the function already skips everything expensive when there are no NaN values.

@eeholmes
eeholmes marked this pull request as ready for review March 16, 2026 23:53
@eeholmes
eeholmes merged commit 4ba0d33 into main Mar 16, 2026
1 check passed
@eeholmes
eeholmes deleted the copilot/fix-latitude-longitude-nan branch March 16, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

For some swath data, Latitude/Longitude is nan outside of swath

2 participants