99Authors:
1010 - Ollie Tooth
1111"""
12+ import os
1213from typing import Optional
1314
14- import os
15- import pystac
1615import icechunk
16+ import numpy as np
17+ import pystac
1718import xarray as xr
1819
1920# -- NOC brand CSS -- #
170171</style>
171172"""
172173
174+ # -- Utility Functions -- #
175+ def apply_bbox (ds : xr .Dataset ,
176+ bbox : tuple
177+ ) -> xr .Dataset :
178+ """
179+ Apply a geographical bounding box to subset an xarray Dataset.
180+
181+ Parameters
182+ ----------
183+ ds : xr.Dataset
184+ Input xarray Dataset.
185+ bbox : tuple
186+ Geographical bounding box in the format (min_lon, max_lon, min_lat, max_lat).
187+
188+ Returns
189+ -------
190+ xr.Dataset
191+ Geographically subsetted xarray Dataset.
192+ """
193+ # -- Validate Inputs -- #
194+ if not isinstance (ds , xr .Dataset ):
195+ raise ValueError ("'ds' must be an xarray Dataset." )
196+ if not (isinstance (bbox , tuple ) and len (bbox ) == 4 ):
197+ raise ValueError ("'bbox' must be a tuple of the form (min_lon, max_lon, min_lat, max_lat)." )
198+
199+ # -- Identify geographical coordinate names & dimensions -- #
200+ # Default lat/lon coord names:
201+ lon_name , lat_name = "nav_lon" , "nav_lat"
202+ # Update lat/lon coord names via standard_name attributes:
203+ for coord in ds .coords :
204+ if ds [coord ].attrs .get ('standard_name' , '' ).lower () == 'longitude' :
205+ lon_name = coord
206+ if ds [coord ].attrs .get ('standard_name' , '' ).lower () == 'latitude' :
207+ lat_name = coord
208+
209+ # -- Apply Bounding Box -- #
210+ if (ds [lon_name ].ndim > 1 ) and (ds [lat_name ].ndim > 1 ):
211+ # -- Case 1: 2D lat/lon coordinates -- #
212+ # Identify lat/lon coordinate dimensions:
213+ if ds [lon_name ].dims != ds [lat_name ].dims :
214+ raise ValueError ("Longitude and latitude coordinates must have the same dimensions." )
215+ else :
216+ y_name , x_name = ds [lon_name ].dims
217+
218+ # Define bbox mask:
219+ mask = (
220+ (ds [lon_name ] >= bbox [0 ])
221+ & (ds [lon_name ] <= bbox [2 ])
222+ & (ds [lat_name ] >= bbox [1 ])
223+ & (ds [lat_name ] <= bbox [3 ])
224+ )
225+
226+ # Find rows/columns containing at least one valid grid point:
227+ rows = mask .any (dim = x_name )
228+ cols = mask .any (dim = y_name )
229+ y_idx = np .where (rows .compute ())[0 ]
230+ x_idx = np .where (cols .compute ())[0 ]
231+
232+ if len (y_idx ) == 0 or len (x_idx ) == 0 :
233+ raise ValueError ("No grid points found inside bbox" )
234+
235+ # Subset dataset to bounding box:
236+ ds_subset = (ds
237+ .where (mask , drop = False )
238+ .isel ({y_name : slice (y_idx .min (), y_idx .max () + 1 ),
239+ x_name : slice (x_idx .min (), x_idx .max () + 1 ),
240+ })
241+ )
242+ else :
243+ # -- Case 2: 1D lat/lon coordinates -- #
244+ ds_subset = ds .sel ({lon_name : slice (bbox [0 ], bbox [1 ]),
245+ lat_name : slice (bbox [2 ], bbox [3 ])
246+ })
247+
248+ return ds_subset
249+
250+
251+ def apply_time_bounds (ds : xr .Dataset ,
252+ start_datetime : str | None = None ,
253+ end_datetime : str | None = None
254+ ) -> xr .Dataset :
255+ """
256+ Apply temporal subsetting to an xarray Dataset.
257+
258+ Parameters
259+ ----------
260+ ds : xr.Dataset
261+ Input xarray Dataset.
262+ start_datetime : str, optional
263+ Start datetime in ISO format (e.g., 'YYYY-MM-DDTHH:MM:SS').
264+ end_datetime : str, optional
265+ End datetime in ISO format (e.g., 'YYYY-MM-DDTHH:MM:SS').
266+
267+ Returns
268+ -------
269+ xr.Dataset
270+ Temporally subsetted xarray Dataset.
271+ """
272+ # -- Validate Inputs -- #
273+ if not isinstance (ds , xr .Dataset ):
274+ raise ValueError ("'ds' must be an xarray Dataset." )
275+ if start_datetime is not None :
276+ if not isinstance (start_datetime , str ):
277+ raise ValueError ("'start_datetime' must be a string in ISO format (e.g., 'YYYY-MM-DDTHH:MM:SS')." )
278+ if end_datetime is not None :
279+ if not isinstance (end_datetime , str ):
280+ raise ValueError ("'end_datetime' must be a string in ISO format (e.g., 'YYYY-MM-DDTHH:MM:SS')." )
281+
282+ # -- Identify time dimension -- #
283+ for coord in ds .dims :
284+ if 'time' in coord .lower ():
285+ time_name = coord
286+ break
287+
288+ # -- Apply temporal subsetting -- #
289+ ds_subset = ds .sel ({time_name : slice (start_datetime , end_datetime )})
290+
291+ return ds_subset
292+
293+
173294# -- Define CatalogSummary() class -- #
174295class CatalogSummary :
175296 """
@@ -1007,7 +1128,7 @@ def open_dataset(self,
10071128 variable_names : Optional [list [str ]] = None ,
10081129 start_datetime : Optional [str ] = None ,
10091130 end_datetime : Optional [str ] = None ,
1010- bbox : Optional [tuple [float , float , float , float ]] = None ,
1131+ bbox : Optional [tuple [float | int , float | int , float | int , float | int ]] = None ,
10111132 branch : str = "main" ,
10121133 consolidated : bool = True ,
10131134 asset_key : Optional [str ] = None
@@ -1033,7 +1154,7 @@ def open_dataset(self,
10331154 End datetime used to subset the dataset. Should be a string
10341155 in ISO format (e.g., "2024-12-31T00:00:00Z"). Default is to use
10351156 the Item end_datetime.
1036- bbox : tuple[float, float, float, float], optional
1157+ bbox : tuple[float | int , float | int , float | int , float | int ], optional
10371158 Spatial bounding box used to subset the dataset. Should be a list of four floats
10381159 representing the bounding box in the format: (min_lon, min_lat, max_lon, max_lat).
10391160 Default is to use the Item bbox.
@@ -1073,8 +1194,8 @@ def open_dataset(self,
10731194 raise TypeError ("'end_datetime' must be a string or None." )
10741195 if not isinstance (bbox , (type (None ), tuple )):
10751196 raise TypeError ("'bbox' must be a tuple or None." )
1076- if bbox is not None and (len (bbox ) != 4 or not all (isinstance (coord , float ) for coord in bbox )):
1077- raise TypeError ("'bbox' must be a tuple of floats in the form (lon_min, lon_max, lat_min, lat_max) ." )
1197+ if bbox is not None and (len (bbox ) != 4 or not all (isinstance (coord , ( float , int ) ) for coord in bbox )):
1198+ raise TypeError ("'bbox' must be a tuple of the form (min_lon, min_lat, max_lon, max_lat) with float or int values ." )
10781199 if not isinstance (branch , str ):
10791200 raise TypeError ("'branch' must be a string." )
10801201 if not isinstance (consolidated , bool ):
@@ -1123,12 +1244,9 @@ def open_dataset(self,
11231244
11241245 # Spatio-temporal subsetting:
11251246 if bbox :
1126- lon = ds .nav_lon .load ()
1127- lat = ds .nav_lat .load ()
1128- ds = ds .where ((lon >= bbox [0 ]) & (lon <= bbox [2 ]) &
1129- (lat >= bbox [1 ]) & (lat <= bbox [3 ]), drop = True )
1247+ ds = apply_bbox (ds = ds , bbox = bbox )
11301248
11311249 if start_datetime or end_datetime :
1132- ds = ds . sel ( time_counter = slice ( start_datetime , end_datetime ) )
1250+ ds = apply_time_bounds ( ds = ds , start_datetime = start_datetime , end_datetime = end_datetime )
11331251
11341252 return ds
0 commit comments