@@ -280,6 +280,11 @@ def pyproj_crs(self):
280280 """Return the coordinate reference system (CRS) as a pyproj object."""
281281 return self .crs .to_pyproj ()
282282
283+ @property
284+ def pyproj_proj (self ):
285+ """Return the Proj object corresponding to the coordinate reference system (CRS)."""
286+ return Proj (self .pyproj_crs )
287+
283288 def _fixup_coordinate_map (self , coord_map ):
284289 """Ensure sure we have coordinate variables in map, not coordinate names."""
285290 new_coord_map = {}
@@ -430,9 +435,46 @@ def coordinates(self, *args):
430435 access a single coordinate, use the appropriate attribute on the accessor, or use tuple
431436 unpacking.
432437
438+ If latitude and/or longitude are requested here, and yet are not present on the
439+ DataArray, an on-the-fly computation from the CRS and y/x dimension coordinates is
440+ attempted.
441+
433442 """
443+ latitude = None
444+ longitude = None
434445 for arg in args :
435- yield self ._axis (arg )
446+ try :
447+ yield self ._axis (arg )
448+ except AttributeError as exc :
449+ if (
450+ (arg == 'latitude' and latitude is None )
451+ or (arg == 'longitude' and longitude is None )
452+ ):
453+ # Try to compute on the fly
454+ try :
455+ latitude , longitude = _build_latitude_longitude (self ._data_array )
456+ except Exception :
457+ # Attempt failed, re-raise original error
458+ raise exc from None
459+ # Otherwise, warn and yield result
460+ warnings .warn (
461+ 'Latitude and longitude computed on-demand, which may be an '
462+ 'expensive operation. To avoid repeating this computation, assign '
463+ 'these coordinates ahead of time with '
464+ '.metpy.assign_latitude_longitude().'
465+ )
466+ if arg == 'latitude' :
467+ yield latitude
468+ else :
469+ yield longitude
470+ elif arg == 'latitude' and latitude is not None :
471+ # We have this from previous computation
472+ yield latitude
473+ elif arg == 'longitude' and longitude is not None :
474+ # We have this from previous computation
475+ yield longitude
476+ else :
477+ raise exc
436478
437479 @property
438480 def time (self ):
@@ -465,7 +507,7 @@ def longitude(self):
465507 return self ._axis ('longitude' )
466508
467509 def coordinates_identical (self , other ):
468- """Return whether or not the coordinates of other match this DataArray's."""
510+ """Return whether the coordinates of other match this DataArray's."""
469511 return (len (self ._data_array .coords ) == len (other .coords )
470512 and all (coord_name in other .coords and other [coord_name ].identical (coord_var )
471513 for coord_name , coord_var in self ._data_array .coords .items ()))
@@ -476,6 +518,36 @@ def time_deltas(self):
476518 us_diffs = np .diff (self ._data_array .values ).astype ('timedelta64[us]' ).astype ('int64' )
477519 return units .Quantity (us_diffs / 1e6 , 's' )
478520
521+ @property
522+ def grid_deltas (self ):
523+ """Return the horizontal dimensional grid deltas suitable for vector derivatives."""
524+ if (
525+ (hasattr (self , 'crs' )
526+ and self .crs ._attrs ['grid_mapping_name' ] == 'latitude_longitude' )
527+ or (hasattr (self , 'longitude' ) and self .longitude .squeeze ().ndim == 1
528+ and hasattr (self , 'latitude' ) and self .latitude .squeeze ().ndim == 1 )
529+ ):
530+ # Calculate dx and dy on ellipsoid (on equator and 0 deg meridian, respectively)
531+ from .calc .tools import nominal_lat_lon_grid_deltas
532+ crs = getattr (self , 'pyproj_crs' , CRS ('+proj=latlon' ))
533+ dx , dy = nominal_lat_lon_grid_deltas (
534+ self .longitude .metpy .unit_array ,
535+ self .latitude .metpy .unit_array ,
536+ crs .get_geod ()
537+ )
538+ else :
539+ # Calculate dx and dy in projection space
540+ try :
541+ dx = np .diff (self .x .metpy .unit_array )
542+ dy = np .diff (self .y .metpy .unit_array )
543+ except AttributeError :
544+ raise AttributeError (
545+ 'Grid deltas cannot be calculated since horizontal dimension coordinates '
546+ 'cannot be found.'
547+ )
548+
549+ return {'dx' : dx , 'dy' : dy }
550+
479551 def find_axis_name (self , axis ):
480552 """Return the name of the axis corresponding to the given identifier.
481553
@@ -1115,7 +1187,7 @@ def _build_latitude_longitude(da):
11151187 """Build latitude/longitude coordinates from DataArray's y/x coordinates."""
11161188 y , x = da .metpy .coordinates ('y' , 'x' )
11171189 xx , yy = np .meshgrid (x .values , y .values )
1118- lonlats = np .stack (Proj ( da .metpy .pyproj_crs ) (xx , yy , inverse = True , radians = False ), axis = - 1 )
1190+ lonlats = np .stack (da .metpy .pyproj_proj (xx , yy , inverse = True , radians = False ), axis = - 1 )
11191191 longitude = xr .DataArray (lonlats [..., 0 ], dims = (y .name , x .name ),
11201192 coords = {y .name : y , x .name : x },
11211193 attrs = {'units' : 'degrees_east' , 'standard_name' : 'longitude' })
@@ -1136,7 +1208,7 @@ def _build_y_x(da, tolerance):
11361208 'must be 2D' )
11371209
11381210 # Convert to projected y/x
1139- xxyy = np .stack (Proj ( da .metpy .pyproj_crs ) (
1211+ xxyy = np .stack (da .metpy .pyproj_proj (
11401212 longitude .values ,
11411213 latitude .values ,
11421214 inverse = False ,
@@ -1305,7 +1377,8 @@ def _wrap_output_like_not_matching_units(result, match):
13051377 ):
13061378 result = units .Quantity (result )
13071379 return (
1308- xr .DataArray (result , coords = match .coords , dims = match .dims ) if output_xarray
1380+ xr .DataArray (result , coords = match .coords , dims = match .dims )
1381+ if output_xarray and result is not None
13091382 else result
13101383 )
13111384
@@ -1450,98 +1523,6 @@ def dataarray_arguments(bound_args):
14501523 yield value
14511524
14521525
1453- def add_grid_arguments_from_xarray (func ):
1454- """Fill in optional arguments like dx/dy from DataArray arguments."""
1455- @functools .wraps (func )
1456- def wrapper (* args , ** kwargs ):
1457- bound_args = signature (func ).bind (* args , ** kwargs )
1458- bound_args .apply_defaults ()
1459-
1460- # Search for DataArray with valid latitude and longitude coordinates to find grid
1461- # deltas and any other needed parameter
1462- grid_prototype = None
1463- for da in dataarray_arguments (bound_args ):
1464- if hasattr (da .metpy , 'latitude' ) and hasattr (da .metpy , 'longitude' ):
1465- grid_prototype = da
1466- break
1467-
1468- # Fill in x_dim/y_dim
1469- if (
1470- grid_prototype is not None
1471- and 'x_dim' in bound_args .arguments
1472- and 'y_dim' in bound_args .arguments
1473- ):
1474- try :
1475- bound_args .arguments ['x_dim' ] = grid_prototype .metpy .find_axis_number ('x' )
1476- bound_args .arguments ['y_dim' ] = grid_prototype .metpy .find_axis_number ('y' )
1477- except AttributeError :
1478- # If axis number not found, fall back to default but warn.
1479- warnings .warn ('Horizontal dimension numbers not found. Defaulting to '
1480- '(..., Y, X) order.' )
1481-
1482- # Fill in vertical_dim
1483- if (
1484- grid_prototype is not None
1485- and 'vertical_dim' in bound_args .arguments
1486- ):
1487- try :
1488- bound_args .arguments ['vertical_dim' ] = (
1489- grid_prototype .metpy .find_axis_number ('vertical' )
1490- )
1491- except AttributeError :
1492- # If axis number not found, fall back to default but warn.
1493- warnings .warn (
1494- 'Vertical dimension number not found. Defaulting to (..., Z, Y, X) order.'
1495- )
1496-
1497- # Fill in dz
1498- if (
1499- grid_prototype is not None
1500- and 'dz' in bound_args .arguments
1501- and bound_args .arguments ['dz' ] is None
1502- ):
1503- try :
1504- vertical_coord = grid_prototype .metpy .vertical
1505- bound_args .arguments ['dz' ] = np .diff (vertical_coord .metpy .unit_array )
1506- except (AttributeError , ValueError ):
1507- # Skip, since this only comes up in advection, where dz is optional (may not
1508- # need vertical at all)
1509- pass
1510-
1511- # Fill in dx/dy
1512- if (
1513- 'dx' in bound_args .arguments and bound_args .arguments ['dx' ] is None
1514- and 'dy' in bound_args .arguments and bound_args .arguments ['dy' ] is None
1515- ):
1516- if grid_prototype is not None :
1517- bound_args .arguments ['dx' ], bound_args .arguments ['dy' ] = (
1518- grid_deltas_from_dataarray (grid_prototype , kind = 'actual' )
1519- )
1520- elif 'dz' in bound_args .arguments :
1521- # Handle advection case, allowing dx/dy to be None but dz to not be None
1522- if bound_args .arguments ['dz' ] is None :
1523- raise ValueError (
1524- 'Must provide dx, dy, and/or dz arguments or input DataArray with '
1525- 'proper coordinates.'
1526- )
1527- else :
1528- raise ValueError ('Must provide dx/dy arguments or input DataArray with '
1529- 'latitude/longitude coordinates.' )
1530-
1531- # Fill in latitude
1532- if 'latitude' in bound_args .arguments and bound_args .arguments ['latitude' ] is None :
1533- if grid_prototype is not None :
1534- bound_args .arguments ['latitude' ] = (
1535- grid_prototype .metpy .latitude
1536- )
1537- else :
1538- raise ValueError ('Must provide latitude argument or input DataArray with '
1539- 'latitude/longitude coordinates.' )
1540-
1541- return func (* bound_args .args , ** bound_args .kwargs )
1542- return wrapper
1543-
1544-
15451526def add_vertical_dim_from_xarray (func ):
15461527 """Fill in optional vertical_dim from DataArray argument."""
15471528 @functools .wraps (func )
0 commit comments