From 783702daf0c2174bb59ec50e5c5984c9d2a755df Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Mon, 14 Oct 2024 14:29:36 -0500 Subject: [PATCH 01/27] fix coords raises --- pyxlma/coords.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyxlma/coords.py b/pyxlma/coords.py index cea1b9a..83c5355 100644 --- a/pyxlma/coords.py +++ b/pyxlma/coords.py @@ -43,15 +43,15 @@ class CoordinateSystem(object): def coordinates(): """Return a tuple of standarized coordinate names""" - raise NotImplemented + raise NotImplementedError() def fromECEF(self, x, y, z): """Take ECEF x, y, z values and return x, y, z in the coordinate system defined by the object subclass""" - raise NotImplemented + raise NotImplementedError() def toECEF(self, x, y, z): """Take x, y, z in the coordinate system defined by the object subclass and return ECEF x, y, z""" - raise NotImplemented + raise NotImplementedError() class GeographicSystem(CoordinateSystem): From ad5dc934b19b52b6536d624679b857c89eeecf5e Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Sun, 12 Jan 2025 11:35:01 -0600 Subject: [PATCH 02/27] add coordinates documentation --- docs/api/coords/index.md | 13 + docs/api/coords/transforms.md | 12 + docs/index.md | 18 + mkdocs.yml | 25 ++ pyxlma/coords.py | 647 ++++++++++++++++++++++++++++------ 5 files changed, 602 insertions(+), 113 deletions(-) create mode 100644 docs/api/coords/index.md create mode 100644 docs/api/coords/transforms.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/docs/api/coords/index.md b/docs/api/coords/index.md new file mode 100644 index 0000000..60e5a6e --- /dev/null +++ b/docs/api/coords/index.md @@ -0,0 +1,13 @@ +# Coordinates +--- +The coords module handles conversions between coordinate systems. The `CoordinateSystem` class represents a generic transform. Subclasses of `CoordinateSystem` are specific coordinate systems that have implemented transformations. These subclasses are documented on the "Transforms" page, see the sidebar. + +There are a few useful tools related to coordinate transforms included as well. + +::: pyxlma.coords + options: + members: + - centers_to_edges + - centers_to_edges_2d + - semiaxes_to_invflattening + - CoordinateSystem \ No newline at end of file diff --git a/docs/api/coords/transforms.md b/docs/api/coords/transforms.md new file mode 100644 index 0000000..821f492 --- /dev/null +++ b/docs/api/coords/transforms.md @@ -0,0 +1,12 @@ +# Transforms +--- +Classes representing implemented coordinate transformations. All classes use the Earth-Centered, Earth-Fixed (ECEF) system as a common intermediary system, allowing for conversions between systems. See + + +::: pyxlma.coords + options: + filters: + - "!^centers_to_edges$" + - "!^centers_to_edges_2d$" + - "!^semiaxes_to_invflattening$" + - "!^CoordinateSystem$" \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a2993ec --- /dev/null +++ b/docs/index.md @@ -0,0 +1,18 @@ +# Welcome to MkDocs + +For full documentation visit [mkdocs.org](https://www.mkdocs.org). + +## Commands + +* `mkdocs new [dir-name]` - Create a new project. +* `mkdocs serve` - Start the live-reloading docs server. +* `mkdocs build` - Build the documentation site. +* `mkdocs -h` - Print help message and exit. + +## Project layout + + + mkdocs.yml # The configuration file. + docs/ + index.md # The documentation homepage. + ... # Other markdown pages, images and other files. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f973cb6 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,25 @@ +site_name: XLMA Python + +theme: + name: material + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: numpy + +nav: + - Home: index.md + - API Reference: + - coords: + - api/coords/index.md + - api/coords/transforms.md + # - GeographicSystem: api/coords/GeographicSystem.md + # - MapProjection: api/coords/MapProjection.md + # - PixelGrid: api/coords/PixelGrid.md + # - GeostationaryFixedGridSystem: api/coords/GeostationaryFixedGridSystem.md + # - RadarCoordinateSystem: api/coords/RadarCoordinateSystem.md + # - TangentPlaneCartesianSystem: api/coords/TangentPlaneCartesianSystem.md diff --git a/pyxlma/coords.py b/pyxlma/coords.py index 67e9340..5ce5aea 100644 --- a/pyxlma/coords.py +++ b/pyxlma/coords.py @@ -13,35 +13,40 @@ class CoordinateSystem(object): - """The abstract coordinate system handling provided here works as follows. + """Superclass representing a generic coordinate system. Subclasses represent specific coordinate systems. + + Each subclass coordinate system must be able to convert data to a common coordinate system, + which is chosen to be Earth-Centered, Earth-Fixed (ECEF) cartesian. - Each coordinate system must be able to convert data to a common coordinate system, which is chosen to be ECEF cartesian. - data -> common system - common system -> dislpay coordinates This is implemented by the fromECEF and toECEF methods in each coordinate system object. + User code is responsible for taking data in its native coord system, - transforming it using to/fromECEF using the a coord system appropriate to the data, and then - transforming that data to the final coordinate system using another coord system. - - Subclasses should maintain an attribute ERSxyz that can be used in - transformations to/from an ECEF cartesian system, e.g. - >>> self.ERSxyz = proj4.Proj(proj='geocent', ellps='WGS84', datum='WGS84') - >>> self.ERSlla = proj4.Proj(proj='latlong', ellps='WGS84', datum='WGS84') - >>> projectedData = proj4.Transformer.from_crs(self.ERSlla.crs, self.ERSxyz.crs).transform(lon, lat, alt) - The ECEF system has its origin at the center of the earth, with the +Z toward the north pole, - +X toward (lat=0, lon=0), and +Y right-handed orthogonal to +X, +Z - - Depends on pyproj, http://code.google.com/p/pyproj/ to handle the ugly details of + transforming it using to/fromECEF using the a coord system appropriate to the data, and then + transforming that data to the final coordinate system using another coord system. + + Subclasses should maintain an attribute ERSxyz that can be used in transformations to/from an ECEF cartesian system, e.g. + + self.ERSxyz = proj4.Proj(proj='geocent', ellps='WGS84', datum='WGS84') + self.ERSlla = proj4.Proj(proj='latlong', ellps='WGS84', datum='WGS84') + projectedData = proj4.Transformer.from_crs(self.ERSlla.crs, self.ERSxyz.crs).transform(lon, lat, alt) + + The ECEF system has its origin at the center of the earth, with the +Z toward the north pole, +X toward (lat=0, lon=0), and +Y right-handed orthogonal to +X, +Z + + Depends on pyproj, [http://code.google.com/p/pyproj/](http://code.google.com/p/pyproj/) to handle the ugly details of various map projections, geodetic transforms, etc. - "You can think of a coordinate system as being something like character encodings, - but messier, and without an obvious winner like UTF-8." - Django OSCON tutorial, 2007 + *"You can think of a coordinate system as being something like character encodings, + but messier, and without an obvious winner like UTF-8." - Django OSCON tutorial, 2007* http://toys.jacobian.org/presentations/2007/oscon/tutorial/ + + Notes + ----- + This class is not intended to be instantiated directly. Instead, use one of the subclasses documented on the "Transforms" page. """ # WGS84xyz = proj4.Proj(proj='geocent', ellps='WGS84', datum='WGS84') - def coordinates(): + def coordinates(self): """Return a tuple of standarized coordinate names""" raise NotImplementedError() @@ -55,15 +60,34 @@ def toECEF(self, x, y, z): class GeographicSystem(CoordinateSystem): - """ - Coordinate system defined on the surface of the earth using latitude, - longitude, and altitude, referenced by default to the WGS84 ellipse. - - Alternately, specify the ellipse shape using an ellipse known - to pyproj, or [NOT IMPLEMENTED] specify r_equator and r_pole directly. + """Coordinate system defined using latitude, longitude, and altitude. + An ellipsoid is used to define the shape of the earth. Latitude and longitude represent the + location of a point on the ellipsoid, and altitude is the height above the ellipsoid. """ def __init__(self, ellipse='WGS84', datum='WGS84', r_equator=None, r_pole=None): + """Initialize a GeographicSystem object. + + Parameters + ---------- + + ellipse : str, default='WGS84' + Ellipse name recognized by pyproj. + + *Ignored if r_equator or r_pole are provided.* + datum : str, default='WGS84' + Datum name recognized by pyproj. + + *Ignored if r_equator or r_pole are provided.* + r_equator : float, optional + Semi-major axis of the ellipse in meters. + + *If only one of r_equator or r_pole is provided, the resulting ellipse is assumed to be spherical.* + r_pole : float, optional + Semi-minor axis of the ellipse in meters. + + *If only one of r_equator or r_pole is provided, the resulting ellipse is assumed to be spherical.* + """ if (r_equator is not None) | (r_pole is not None): if r_pole is None: r_pole=r_equator @@ -76,6 +100,26 @@ def __init__(self, ellipse='WGS84', datum='WGS84', self.ERSlla = proj4.Proj(proj='latlong', ellps=ellipse, datum=datum) self.ERSxyz = proj4.Proj(proj='geocent', ellps=ellipse, datum=datum) def toECEF(self, lon, lat, alt): + """Converts longitude, latitude, and altitude to Earth-Centered, Earth-Fixed (ECEF) X, Y, Z coordinates. + + Parameters + ---------- + lon : float or array_like + Longitude in decimal degrees East of the Prime Meridian. + lat : float or array_like + Latitude in decimal degrees North of the equator. + alt: float or array_like + Altitude in meters above the ellipsoid. + + Returns + ------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + """ lat = atleast_1d(lat) # proj doesn't like scalars lon = atleast_1d(lon) alt = atleast_1d(alt) @@ -87,6 +131,26 @@ def toECEF(self, lon, lat, alt): return projectedData[0,:], projectedData[1,:], projectedData[2,:] def fromECEF(self, x, y, z): + """Converts Earth-Centered, Earth-Fixed (ECEF) X, Y, Z to longitude, latitude, and altitude coordinates. + + Parameters + ---------- + x : float or array_like + ECEF X in meters from the center of the Earth. + y : float or array_like + ECEF Y in meters from the center of the Earth. + z : float or array_like + ECEF Z in meters from the center of the Earth. + + Returns + ------- + lon : float or array_like + Longitude in decimal degrees East of the Prime Meridian. + lat : float or array_like + Latitude in decimal degrees North of the equator. + alt: float or array_like + Altitude in meters above the ellipsoid. + """ x = atleast_1d(x) # proj doesn't like scalars y = atleast_1d(y) z = atleast_1d(z) @@ -99,11 +163,28 @@ def fromECEF(self, x, y, z): class MapProjection(CoordinateSystem): - """Map projection coordinate system. Wraps proj4, and uses its projecion names. Defaults to - equidistant cylindrical projection + """Coordinate system defined using meters x, y, z in a specified map projection. + Wraps proj4, and uses its projecion names. Converts location in any map projection to ECEF, and vice versa. """ def __init__(self, projection='eqc', ctrLat=None, ctrLon=None, ellipse='WGS84', datum='WGS84', **kwargs): + """Initialize a MapProjection object. + + Parameters + ---------- + projection : str, default='eqc' + Projection name recognized by pyproj. Defaults to 'eqc' (equidistant cylindrical). + ctrLat : float, optional + Latitude of the center of the map projection in decimal degrees North of the equator, if required for the projection. + ctrLon : float, optional + Longitude of the center of the map projection in decimal degrees East of the Prime Meridian, if required for the projection. + ellipse : str, default='WGS84' + Ellipse name recognized by pyproj. + datum : str, default='WGS84' + Datum name recognized by pyproj. + **kwargs + Additional keyword arguments passed to pyproj.Proj() + """ self.ERSxyz = proj4.Proj(proj='geocent', ellps=ellipse, datum=datum) self.projection = proj4.Proj(proj=projection, ellps=ellipse, datum=datum, **kwargs) self.ctrLat=ctrLat @@ -114,6 +195,17 @@ def __init__(self, projection='eqc', ctrLat=None, ctrLon=None, ellipse='WGS84', self.cx, self.cy, self.cz = self.ctrPosition() def ctrPosition(self): + """Get the map projection's center position as projected in the specified map projection. + + Returns + ------- + cx : float + X coordinate of the map projection's center in meters. + cy : float + Y coordinate of the map projection's center in meters. + cz : float + Z coordinate of the map projection's center in meters. + """ if (self.ctrLat != None) & (self.ctrLon != None): ex, ey, ez = self.geoCS.toECEF(self.ctrLon, self.ctrLat, self.ctrAlt) cx, cy, cz = self.fromECEF(ex, ey, ez) @@ -122,6 +214,26 @@ def ctrPosition(self): return cx, cy, cz def toECEF(self, x, y, z): + """Converts x, y, z meters in the map projection to Earth-Centered, Earth-Fixed (ECEF) X, Y, Z coordinates. + + Parameters + ---------- + x : float or array_like + x position in the map projection in meters. + y : float or array_like + y position in the map projection in meters. + z: float or array_like + z position in the map projection in meters. + + Returns + ------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + """ x += self.cx y += self.cy z += self.cz @@ -133,6 +245,26 @@ def toECEF(self, x, y, z): return px, py, pz def fromECEF(self, x, y, z): + """Converts x, y, z meters in the map projection to Earth-Centered, Earth-Fixed (ECEF) X, Y, Z coordinates. + + Parameters + ---------- + x : float or array_like + x position in the map projection in meters. + y : float or array_like + y position in the map projection in meters. + z: float or array_like + z position in the map projection in meters. + + Returns + ------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + """ projectedData = array(proj4.Transformer.from_crs(self.ERSxyz.crs, self.projection.crs).transform(x, y, z)) if len(projectedData.shape) == 1: px, py, pz = projectedData[0], projectedData[1], projectedData[2] @@ -141,35 +273,43 @@ def fromECEF(self, x, y, z): return px-self.cx, py-self.cy, pz-self.cz class PixelGrid(CoordinateSystem): + """Coordinate system defined using arbitrary pixel coordinates in a 2D pixel array. + """ def __init__(self, lons, lats, lookup, x, y, alts=None, geosys=None): - """ - Coordinate system for arbitrary pixel coordinates in a 2D pixel array. - Arguments: - lons: 2D array of longitudes of pixel centers - lats: 2D array of longitudes of pixel centers - alts: 2D array of longitudes of pixel centers. If None, zeros are assumed. - Each array is of shape (nx, ny) with pixel coordinate (x=0, y=0) - corresponding to grid index [0, 0] - - lookup is an object with a method 'query' that accepts a single argument, - a (N,2) array of lats, lons and returns pixel IDs that can be used to - index lons and lats, as well as the distances between the pixel centers - and the queried locations. X and Y flattened arrays of pixel coordinates - that align with indices of the flattened lon and lat arrays used to - create the lookup table. - >>> test_events = np.vstack([(-101.5, 33.5), (-102.8, 32.5), (-102.81,32.5)]) - >>> distances, idx = lookup.query(test_events) - >>> loni, lati = lons[X[idx], Y[idx]], lats[X[idx], Y[idx]] - An instance of sklearn.neighbors.KDTree is one such lookup. - - If geosys is provided, it should be an instance of GeographicSystem; - otherwise a GeographicSystem instance with default arguments is created. + """Initialize a PixelGrid object. + Parameters + ---------- + lons : array_like + 2D array of longitudes of pixel centers. + lats : array_like + 2D array of latitudes of pixel centers. + lookup : object + Object with instance method `query` that accepts a single argument, a (N,2) array of lats, lons and + returns a tuple of distances to the nearest pixel centers and the pixel ID of the nearest pixel to each + requested lat/lon. A [`sklearn.neighbors.KDTree`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.cKDTree.html) is the intended use of this argument, but any class with a `query` method is accepted. + + Example of a valid lookup object: + + test_events = np.vstack([(-101.5, 33.5), (-102.8, 32.5), (-102.81,32.5)]) + distances, idx = lookup.query(test_events) + loni, lati = lons[X[idx], Y[idx]], lats[X[idx], Y[idx]] + + x : list or array_like + 1D integer array of pixel row IDs + y : list or array_like + 1D integer array of pixel column IDs + alts : array_like, optional + 2D array of altitudes of pixel centers. If None, zeros are assumed. + geosys : GeographicSystem, optional + GeographicSystem object used to convert pixel coordinates to ECEF. If None, a GeographicSystem instance with default arguments is created. + + Notes + ----- When converting toECEF, which accepts pixel coordinates, the z pixel coordinate is ignored, as it has no meaning. When converting fromECEF, zeros in the shape of x are returned as the z coordinate. - """ if geosys is None: self.geosys = GeographicSystem() @@ -184,7 +324,27 @@ def __init__(self, lons, lats, lookup, x, y, alts=None, geosys=None): alts = zeros_like(lons) self.alts = alts - def toECEF(self, x, y, z): + def toECEF(self, x, y, z=None): + """Converts x, y pixel IDs to Earth-Centered, Earth-Fixed (ECEF) X, Y, Z coordinates. + + Parameters + ---------- + x : float or array_like + row ID of the pixel in the pixel grid. + y : float or array_like + column ID of the pixel in the pixel grid. + z : object, optional + unused. If provided, it is ignored. + + Returns + ------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + """ x = x.astype('int64') y = y.astype('int64') lons = self.lons[x, y] @@ -193,6 +353,26 @@ def toECEF(self, x, y, z): return self.geosys.toECEF(lons, lats, alts) def fromECEF(self, x, y, z): + """Converts Earth-Centered, Earth-Fixed (ECEF) X, Y, Z to x, y pixel ID coordinates. + + Parameters + ---------- + x : float or array_like + ECEF X in meters from the center of the Earth. + y : float or array_like + ECEF Y in meters from the center of the Earth. + z : float or array_like + ECEF Z in meters from the center of the Earth. + + Returns + ------- + x : array_like + row ID of the pixel in the pixel grid. + y : array_like + column ID of the pixel in the pixel grid. + z : array_like + Zeros array in the shape of x. + """ lons, lats, alts = self.geosys.fromECEF(x, y, z) locs = vstack((lons.flatten(), lats.flatten())).T if locs.shape[0] > 0: @@ -204,27 +384,28 @@ def fromECEF(self, x, y, z): return x, y, zeros_like(x) class GeostationaryFixedGridSystem(CoordinateSystem): + """Coordinate system defined using scan angles from the perspective of a geostationary satellite. + The pixel locations are a 2D grid of scan angles (in radians) from the perspective of a + geostationary satellite above an arbitrary ellipsoid. + """ def __init__(self, subsat_lon=0.0, subsat_lat=0.0, sweep_axis='y', sat_ecef_height=35785831.0, ellipse='WGS84'): - """ - Coordinate system representing a grid of scan angles from the perspective of a - geostationary satellite above an arbitrary ellipsoid. Fixed grid coordinates are - in radians. + """Initialize a GeostationaryFixedGridSystem object. Parameters ---------- - subsat_lon : float + subsat_lon : float, default=0.0 Longitude of the subsatellite point in degrees. - subsat_lat : float + subsat_lat : float, default=0.0 Latitude of the subsatellite point in degrees. - sweep_axis : str + sweep_axis : str, default='y' Axis along which the satellite sweeps. 'x' or 'y'. Use 'x' for GOES - and 'y' (default) for EUMETSAT. - sat_ecef_height : float - Height of the satellite in meters above the specified ellipsoid. - ellipse : str or list + and 'y' for EUMETSAT. + sat_ecef_height : float, default=35785831.0 + Height of the satellite in meters above the specified ellipsoid. Defaults to the height of the GOES satellite. + ellipse : str or list, default='WGS84' A string representing a known ellipse to pyproj, or a list of [a, b] (semi-major and semi-minor axes) of the ellipse. Default is 'WGS84'. """ @@ -242,10 +423,50 @@ def __init__(self, subsat_lon=0.0, subsat_lat=0.0, sweep_axis='y', self.h=sat_ecef_height def toECEF(self, x, y, z): + """Converts x, y, z satellite scan angles to Earth-Centered, Earth-Fixed (ECEF) X, Y, Z coordinates. + + Parameters + ---------- + x : float or array_like + horizontal scan angle in radians from the perspective of the satellite. + y : float or array_like + vertical scan angle in radians from the perspective of the satellite. + z : float or array_like + altitude above the ellipsoid expressed as a fraction of the satellite's height above the ellipsoid. + + Returns + ------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + """ X, Y, Z = x*self.h, y*self.h, z*self.h return proj4.Transformer.from_crs(self.fixedgrid.crs, self.ECEFxyz.crs).transform(X, Y, Z) def fromECEF(self, x, y, z): + """Converts Earth-Centered, Earth-Fixed (ECEF) X, Y, Z to longitude, latitude, and altitude coordinates. + + Parameters + ---------- + x : float or array_like + ECEF X in meters from the center of the Earth. + y : float or array_like + ECEF Y in meters from the center of the Earth. + z : float or array_like + ECEF Z in meters from the center of the Earth. + + Returns + ------- + x : float or array_like + horizontal scan angle in radians from the perspective of the satellite. + y : float or array_like + vertical scan angle in radians from the perspective of the satellite. + z : float or array_like + altitude above the ellipsoid expressed as a fraction of the satellite's height above the ellipsoid. + """ X, Y, Z = proj4.Transformer.from_crs(self.ECEFxyz.crs, self.fixedgrid.crs).transform(x, y, z) return X/self.h, Y/self.h, Z/self.h @@ -259,13 +480,34 @@ def fromECEF(self, x, y, z): # return px, py, z class RadarCoordinateSystem(CoordinateSystem): - """ - Converts spherical (range, az, el) radar coordinates to lat/lon/alt, and then to ECEF. + """Coordinate system defined using the range, azimuth, and elevation angles from a radar. + + Locations are defined using the latitude, longitude, and altitude of the radar and the azimuth and elevation angles of the radar beam. - An earth's effective radius of 4/3 is assumed to correct for atmospheric refraction. + Warning + ------- + By default, an imaginary earth radius of 4/3 the actual earth radius is assumed to correct for atmospheric refraction. """ - def __init__(self, ctrLat, ctrLon, ctrAlt, datum='WGS84', ellps='WGS84', effectiveRadiusMultiplier=4./3.): + def __init__(self, ctrLat, ctrLon, ctrAlt, datum='WGS84', ellps='WGS84', effectiveRadiusMultiplier=4/3): + """Initialize a RadarCoordinateSystem object. + + Parameters + ---------- + + ctrLat : float + Latitude of the radar in decimal degrees North of the equator. + ctrLon : float + Longitude of the radar in decimal degrees East of the Prime Meridian. + ctrAlt : float + Altitude of the radar in meters above sea level. + datum : str, default='WGS84' + Datum name recognized by pyproj. + ellps : str, default='WGS84' + Ellipse name recognized by pyproj. + effectiveRadiusMultiplier : float, default=4/3 + Multiplier to scale the earth's radius to account for the beam bending due to atmospheric refraction. + """ self.ctrLat = float(ctrLat) self.ctrLon = float(ctrLon) self.ctrAlt = float(ctrAlt) @@ -283,9 +525,24 @@ def __init__(self, ctrLat, ctrLon, ctrAlt, datum='WGS84', ellps='WGS84', effecti self.effectiveRadiusMultiplier = effectiveRadiusMultiplier def getGroundRangeHeight(self, r, elevationAngle): - """Convert slant range (along the beam) and elevation angle into - ground range (great circle distance) and height above the earth's surface - Follows Doviak and Zrnic 1993, eq. 2.28.""" + """Convert slant range (along the beam) and elevation angle into ground range and height. + Ground range given in great circle distance and height above the surface of the ellipsoid. + Follows [Doviak and Zrnic 1993, eq. 2.28.](https://doi.org/10.1016/C2009-0-22358-0) + + Parameters + ---------- + r : float or array_like + slant range in meters. + elevationAngle : float or array_like + elevation angle in degrees above the horizon. + + Returns + ------- + s : float or array_like + Ground range (great circle distance) in meters. + z : float or array_like + Height above the surface of the ellipsoid in meters. + """ #Double precison arithmetic is crucial to proper operation. lat = self.ctrLat * pi / 180.0 @@ -311,9 +568,23 @@ def getGroundRangeHeight(self, r, elevationAngle): return s, h def getSlantRangeElevation(self, groundRange, z): - """Convert ground range (great circle distance) and height above - the earth's surface to slant range (along the beam) and elevation angle. - Follows Doviak and Zrnic 1993, eq. 2.28""" + """Convert ground range (great circle distance) and height above the earth's surface to slant range (along the beam) and elevation angle. + Follows [Doviak and Zrnic 1993, eq. 2.28.](https://doi.org/10.1016/C2009-0-22358-0) + + Parameters + ---------- + groundRange : float or array_like + Ground range (great circle distance) in meters. + z : float or array_like + Height above the surface of the ellipsoid in meters. + + Returns + ------- + r : float or array_like + Slant range in meters. + el : float or array_like + Elevation angle in degrees above the horizon. + """ lat = self.ctrLat * pi / 180.0 @@ -344,8 +615,26 @@ def getSlantRangeElevation(self, groundRange, z): return r, el def toLonLatAlt(self, r, az, el): - """Convert slant range r, azimuth az, and elevation el to ECEF system""" - geoSys = GeographicSystem() + """Convert slant range r, azimuth az, and elevation el to latitude, longitude, altitude coordiantes. + + Parameters + ---------- + r : float or array_like + Slant range in meters. + az : float or array_like + Azimuth angle in degrees clockwise from North. + el : float or array_like + Elevation angle in degrees above the horizon. + + Returns + ------- + lon : float or array_like + Longitude in decimal degrees East of the Prime Meridian. + lat : float or array_like + Latitude in decimal degrees North of the equator. + z : float or array_like + Altitude in meters above the surface of the ellipsoid. + """ geodetic = proj4.Geod(ellps=self.ellps) try: @@ -358,12 +647,51 @@ def toLonLatAlt(self, r, az, el): return lon, lat, z def toECEF(self, r, az, el): + """Converts range, azimuth, and elevation to Earth-Centered, Earth-Fixed (ECEF) X, Y, Z coordinates. + + Parameters + ---------- + r : float or array_like + Distance in meters along the radar beam from the target to the radar. + az : float or array_like + Azimuth angle of the target in degrees clockwise from North. + el: float or array_like + Elevation angle of the target in degrees above the horizon. + + Returns + ------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + """ geoSys = GeographicSystem() lon, lat, z = self.toLonLatAlt(r, az, el) return geoSys.toECEF(lon, lat, z.ravel()) def fromECEF(self, x, y, z): - """Convert ECEF system to slant range r, azimuth az, and elevation el""" + """Converts Earth-Centered, Earth-Fixed (ECEF) X, Y, Z to longitude, latitude, and altitude coordinates. + + Parameters + ---------- + x : float or array_like + ECEF X in meters from the center of the Earth. + y : float or array_like + ECEF Y in meters from the center of the Earth. + z : float or array_like + ECEF Z in meters from the center of the Earth. + + Returns + ------- + r : float or array_like + Distance in meters along the radar beam from the target to the radar. + az : float or array_like + Azimuth angle of the target in degrees clockwise from North. + el: float or array_like + Elevation angle of the target in degrees above the horizon. + """ # x = np.atleast1d(x) geoSys = GeographicSystem() geodetic = proj4.Geod(ellps=self.ellps) @@ -384,12 +712,22 @@ def fromECEF(self, x, y, z): return r, az, el -class TangentPlaneCartesianSystem(object): - """ TODO: This function needs to be updated to inherit from CoordinateSystem - +class TangentPlaneCartesianSystem(CoordinateSystem): + """Coordinate system defined by a meters relative to plane tangent to the earth at a specified location. """ def __init__(self, ctrLat=0.0, ctrLon=0.0, ctrAlt=0.0): + """Initialize a TangentPlaneCartesianSystem object. + + Parameters + ---------- + ctrLat : float, default=0.0 + Latitude of the center of the local tangent plane in decimal degrees North of the equator. + ctrLon : float, default=0.0 + Longitude of the center of the local tangent plane in decimal degrees East of the Prime Meridian. + ctrAlt : float, default=0.0 + Altitude of the center of the local tangent plane in meters above the ellipsoid. + """ self.ctrLat = float(ctrLat) self.ctrLon = float(ctrLon) self.ctrAlt = float(ctrAlt) @@ -456,7 +794,32 @@ def __init__(self, ctrLat=0.0, ctrLon=0.0, ctrAlt=0.0): [z1, z2, z3]]).squeeze() def fromECEF(self, x, y, z): - """ Transforms 1D arrays of ECEF x, y, z to the local tangent plane system""" + """Converts x, y, z meters in the local tangent plane to Earth-Centered, Earth-Fixed (ECEF) X, Y, Z coordinates. + + Parameters + ---------- + x : float or array_like + x position in meters East of the tangent plane center. + y : float or array_like + y position in meters North of the tangent plane center. + z: float or array_like + z position in meters above the tangent plane center. + + Returns + ------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + + Warning + ------- + + The z coordinate input is **NOT** the altitude above the ellipsoid. It is the z position in the local tangent plane. + Due to the curvature of the Earth, the TPCS z position and altitude difference increases with distance from the center of the TPCS. + """ data = vstack((x, y, z)) tpXYZ = self.toLocal(data) if len(tpXYZ.shape) == 1: @@ -466,7 +829,35 @@ def fromECEF(self, x, y, z): return tpX, tpY, tpZ def toECEF(self, x, y, z): - """ Transforms 1D arrays of x, y, z in the local tangent plane system to ECEF""" + """Converts Earth-Centered, Earth-Fixed (ECEF) X, Y, Z to x, y, z meters in the local tangent plane. + + Parameters + ---------- + X : float or array_like + ECEF X in meters from the center of the Earth. + Y : float or array_like + ECEF Y in meters from the center of the Earth. + Z : float or array_like + ECEF Z in meters from the center of the Earth. + + Returns + ------- + x : float or array_like + x position in meters East of the tangent plane center. + y : float or array_like + y position in meters North of the tangent plane center. + z: float or array_like + z position in meters above the tangent plane center. + + Warnings + -------- + The x and z output coordinates are not the great circle distance (the distance along the surface of the Earth). Distances between points should be compared in their ECEF coordinates. + + Similarly, the z coordinate output is **NOT** the altitude above the ellipsoid. It is the z position in the local tangent plane. + Due to the curvature of the Earth, the TPCS z position and altitude difference increases with distance from the center of the TPCS. + + If you want to find the altitude of a point above the ellipsoid, use the `GeographicSystem` class. + """ data = vstack((x, y, z)) ecXYZ = self.fromLocal(data) if len(ecXYZ.shape) == 1: @@ -476,52 +867,80 @@ def toECEF(self, x, y, z): return ecX, ecY, ecZ def toLocal(self, data): - """Transforms 3xN array of data (position vectors) in the ECEF system to - the local tangent plane cartesian system. + """Transforms 3xN array of ECEF X, Y, Z coordinates to the local tangent plane cartesian system. + + Parameters + ---------- + data : array_like + (3, N) array of data (position vectors) in the ECEF system, representing X, Y, Z meters from the center of the Earth. - Returns another 3xN array. + Returns + ------- + local_data : array_like + (3, N) array of data (position vectors) in the local tangent plane cartesian system, representing x, y, z meters from the center of the tangent plane. """ - return array( [ dot(self.TransformToLocal, (v-self.centerECEF)[:,None]) + local_data = array( [ dot(self.TransformToLocal, (v-self.centerECEF)[:,None]) for v in data[0:3,:].transpose()] ).squeeze().transpose() + return local_data def fromLocal(self, data): - """Transforms 3xN array of data (position vectors) in the local tangent - plane cartesian system to the ECEF system. + """Transforms 3xN array of ECEF X, Y, Z coordinates to the local tangent plane cartesian system. + + Parameters + ---------- + data : array_like + (3, N) array of data (position vectors) in the ECEF system, representing X, Y, Z meters from the center of the Earth. - Returns another 3xN array. + Returns + ------- + ecef_data : array_like + (3, N) array of data (position vectors) in the local tangent plane cartesian system, representing x, y, z meters from the center of the tangent plane. """ #Transform from local to ECEF uses transpose of the TransformToLocal matrix - return array( [ (dot(self.TransformToLocal.transpose(), v) + self.centerECEF) + ecef_data = array( [ (dot(self.TransformToLocal.transpose(), v) + self.centerECEF) for v in data[0:3,:].transpose()] ).squeeze().transpose() + return ecef_data def semiaxes_to_invflattening(semimajor, semiminor): - """ Calculate the inverse flattening from the semi-major - and semi-minor axes of an ellipse""" + """ Calculate the inverse flattening from the semi-major and semi-minor axes of an ellipse" + + Parameters + ---------- + semimajor : float + Semi-major axis of the ellipse + semiminor : float + Semi-minor axis of the ellipse + + Returns + ------- + rf : float + Inverse flattening of the ellipse + """ rf = semimajor/(semimajor-semiminor) return rf def centers_to_edges(x): - """ - Create an array of length N+1 edge locations from an - array of lenght N grid center locations. - - In the interior, the edge positions set to the midpoints - of the values in x. For the outermost edges, half the - closest dx is assumed to apply. + """ Create an array of length N+1 edge locations from an array of lenght N grid center locations. Parameters ---------- - x : array, shape (N) - Locations of the centers + x : array_like + (N,) array of locations of the centers Returns ------- - xedge : array, shape (N+1,M+1) - + xedge : array + (N+1,) array of locations of the edges + + Notes + ----- + In the interior, the edge positions set to the midpoints + of the values in x. For the outermost edges, half the + closest dx is assumed to apply. """ xedge=zeros(x.shape[0]+1) xedge[1:-1] = (x[:-1] + x[1:])/2.0 @@ -531,26 +950,28 @@ def centers_to_edges(x): def centers_to_edges_2d(x): - """ - Create a (N+1, M+1) array of edge locations from a - (N, M) array of grid center locations. + """Create a (N+1)x(M+1) array of edge locations from a + NxM array of grid center locations. - In the interior, the edge positions set to the midpoints - of the values in x. For the outermost edges, half the - closest dx is assumed to apply. This matters for polar - meshes, where one edge of the grid becomes a point at the - polar coordinate origin; dx/2 is a half-hearted way of - trying to prevent negative ranges. Parameters ---------- - x : array, shape (N,M) - Locations of the centers + x : array_like + (N,M) array locations of the centers. Returns ------- - xedge : array, shape (N+1,M+1) + xedge : array_like + (N+1,M+1) array of locations of the edges. + Notes + ----- + In the interior, the edge positions set to the midpoints + of the values in x. For the outermost edges, half the + closest dx is assumed to apply. This matters for polar + meshes, where one edge of the grid becomes a point at the + polar coordinate origin; dx/2 is a half-hearted way of + trying to prevent negative ranges. """ xedge = zeros((x.shape[0]+1,x.shape[1]+1)) # interior is a simple average of four adjacent centers From 4a771177b2321f4804c4cad1daec8bc574d8d159 Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Sun, 12 Jan 2025 17:29:12 -0600 Subject: [PATCH 03/27] add xarray util documentation --- docs/api/xarray/index.md | 5 +++ mkdocs.yml | 4 +- pyxlma/coords.py | 6 +-- pyxlma/xarray_util.py | 86 ++++++++++++++++++++++++++-------------- 4 files changed, 68 insertions(+), 33 deletions(-) create mode 100644 docs/api/xarray/index.md diff --git a/docs/api/xarray/index.md b/docs/api/xarray/index.md new file mode 100644 index 0000000..03c99df --- /dev/null +++ b/docs/api/xarray/index.md @@ -0,0 +1,5 @@ +# [xarray](https://docs.xarray.dev/) utilities + +Some useful tools for handling xarray [datasets](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html), the primary data store used by pyxlma to store LMA data. + +::: pyxlma.xarray_util diff --git a/mkdocs.yml b/mkdocs.yml index f973cb6..3723df5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,7 +16,9 @@ nav: - API Reference: - coords: - api/coords/index.md - - api/coords/transforms.md + - api/coords/transforms.md + - xarray: + - api/xarray/index.md # - GeographicSystem: api/coords/GeographicSystem.md # - MapProjection: api/coords/MapProjection.md # - PixelGrid: api/coords/PixelGrid.md diff --git a/pyxlma/coords.py b/pyxlma/coords.py index 5ce5aea..49978a3 100644 --- a/pyxlma/coords.py +++ b/pyxlma/coords.py @@ -833,11 +833,11 @@ def toECEF(self, x, y, z): Parameters ---------- - X : float or array_like + x : float or array_like ECEF X in meters from the center of the Earth. - Y : float or array_like + y : float or array_like ECEF Y in meters from the center of the Earth. - Z : float or array_like + z : float or array_like ECEF Z in meters from the center of the Earth. Returns diff --git a/pyxlma/xarray_util.py b/pyxlma/xarray_util.py index 79b67cc..11f8cbc 100644 --- a/pyxlma/xarray_util.py +++ b/pyxlma/xarray_util.py @@ -2,16 +2,22 @@ import xarray as xr import numpy as np + def get_1d_dims(d): - """ - Find all dimensions in a dataset that are purely 1-dimensional, - i.e., those dimensions that are not part of a 2D or higher-D - variable. + """Find all dimensions in an [`xarray.Dataset`](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html) that are purely 1-dimensional. + + Finds names of dimensions on the provided dataset that are used in 1D variables. + Excludes dimensions that are part of a 2D or higher-D variable. - arguments - d: xarray Dataset - returns - dims1d: a list of dimension names + Parameters + ---------- + d : xarray.Dataset + The dataset to find 1D dimensions in. + + Returns + ------- + dims1d : list + A list of dimension names that are only used for 1D variables. """ # Assume all dims coorespond to 1D vars dims1d = list(d.dims.keys()) @@ -21,16 +27,22 @@ def get_1d_dims(d): if vardim in dims1d: dims1d.remove(str(vardim)) return dims1d - + + def gen_1d_datasets(d): - """ - Generate a sequence of datasets having only those variables + """Generate a sequence of datasets having only those variables along each dimension that is only used for 1-dimensional variables. - arguments - d: xarray Dataset - returns - generator function yielding a sequence of single-dimension datasets + Parameters + ---------- + d : xarray.Dataset + The dataset to generate 1D datasets from. + + Yields + ------ + xarray.Dataset + A dataset containing only variables along a single dimension. + Each yielded dataset corresponds to one of the 1-dimensional dimensions identified in the input dataset `d`. """ dims1d = get_1d_dims(d) # print(dims1d) @@ -39,35 +51,51 @@ def gen_1d_datasets(d): all_dims.remove(dim) yield d.drop_dims(all_dims) -def get_1d_datasets(d): - """ - Generate a list of datasets having only those variables + +def get_1d_datasets(d, ): + """Generate a list of datasets having only those variables along each dimension that is only used for 1-dimensional variables. - arguments - d: xarray Dataset - returns + Parameters + ---------- + d : xarray.Dataset + The dataset to generate 1D datasets from. + + + Returns + ------- + single_dim_ds : list a list of single-dimension datasets """ - return [d1 for d1 in gen_1d_datasets(d, *args, **kwargs)] + single_dim_ds = [d1 for d1 in gen_1d_datasets(d, *args, **kwargs)] + return single_dim_ds + def get_scalar_vars(d): + scalars = [] for varname, var in d.variables.items(): if len(var.dims) == 0: scalars.append(varname) return scalars -def concat_1d_dims(datasets, stack_scalars=None): - """ + +def concat_1d_dims(datasets, stack_scalars=False): + """Concatenate a list of xarray Datasets along 1D dimensions only. + For each xarray Dataset in datasets, concatenate (preserving the order of datasets) all variables along dimensions that are only used for one-dimensional variables. - arguments - d: iterable of xarray Datasets - stack_scalars: create a new dimension named with this value - that aggregates all scalar variables and coordinates - returns + Parameters + ---------- + d : iterable of xarray.Dataset + The datasets to concatenate. + stack_scalars : bool, default=False + if True, create a new dimension named with this value that aggregates all scalar variables and coordinates + + Returns + ------- + unified : xarray.Dataset a new xarray Dataset with only the single-dimension variables """ # dictionary mapping dimension names to a list of all From 612e52bf2d7f77bbe60565128d91dfa6adc2e274 Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Mon, 13 Jan 2025 22:47:01 -0600 Subject: [PATCH 04/27] add flash documentation --- docs/api/lmalib/flash.md | 7 + pyxlma/lmalib/flash/cluster.py | 46 ++++-- pyxlma/lmalib/flash/properties.py | 228 ++++++++++++++++++++++++++---- pyxlma/xarray_util.py | 15 +- 4 files changed, 257 insertions(+), 39 deletions(-) create mode 100644 docs/api/lmalib/flash.md diff --git a/docs/api/lmalib/flash.md b/docs/api/lmalib/flash.md new file mode 100644 index 0000000..89882b1 --- /dev/null +++ b/docs/api/lmalib/flash.md @@ -0,0 +1,7 @@ +# Flash Processing + +::: pyxlma.lmalib.flash.cluster + +::: pyxlma.lmalib.flash.properties + + diff --git a/pyxlma/lmalib/flash/cluster.py b/pyxlma/lmalib/flash/cluster.py index 871183a..3d4fc0d 100644 --- a/pyxlma/lmalib/flash/cluster.py +++ b/pyxlma/lmalib/flash/cluster.py @@ -6,14 +6,27 @@ import pyxlma.lmalib.io.cf_netcdf as cf_netcdf def cluster_dbscan(X, Y, Z, T, min_points=1): - """ Identify clusters in spatiotemporal data X, Y, Z, T. + """Identify clusters in spatiotemporal data X, Y, Z, T. - min_points is used as min_samples in the call to DBSCAN. + Parameters + ---------- + X : array_like + The x coordinate of the data points. + Y : array_like + The y coordinate of the data points. + Z : array_like + The z coordinate of the data points. + T : array_like + The time coordinate of the data points. + min_points : int, default=1 + Used as the min_samples parameter in the call to [DBSCAN](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html). - Returns an unsigned 64 bit integer array of cluster labels. - Noise points identified by DBSCAN are assigned the maximum value - that can be represented by uint64, i.e., np.iinfo(np.uint64).max or - 18446744073709551615. + Returns + ------- + labels : np.ndarray + an unsigned 64 bit integer array of cluster labels. + Noise points identified by DBSCAN are assigned the maximum value that can be represented by uint64, + i.e., np.iinfo(np.uint64).max or 18446744073709551615. """ from sklearn.cluster import DBSCAN coords = np.vstack((X, Y, Z, T)).T @@ -25,12 +38,25 @@ def cluster_dbscan(X, Y, Z, T, min_points=1): return labels def cluster_flashes(events, distance=3000.0, time=0.15): - """ + """Cluster LMA VHF sources into flashes. + + Parameters + ---------- + events : xarray.Dataset + LMA dataset with event position and time and network center position. + distance : float, default=3000.0 + Spatial separation in meters. Used for normalization of space. + time : float, default=0.15 + Temporal separation in seconds. Used for normalization of time. - events: xarray dataset conforming to the pyxlma CF NetCDF format + Returns + ------- + ds : xarray.Dataset + LMA dataset with added flash_id and event_parent_flash_id variables. - distance: spatial separation in meters. Used for normalization of - time: number + Notes + ----- + Additional data variables for flash properties are created, but are filled with NaN. To compute these properties, use the `pyxlma.lmalib.flash.properties.flash_stats` function. """ geoCS = GeographicSystem() diff --git a/pyxlma/lmalib/flash/properties.py b/pyxlma/lmalib/flash/properties.py index eec9384..c088e88 100644 --- a/pyxlma/lmalib/flash/properties.py +++ b/pyxlma/lmalib/flash/properties.py @@ -5,6 +5,32 @@ from pyxlma.lmalib.traversal import OneToManyTraversal def local_cartesian(lon, lat, alt, lonctr, latctr, altctr): + """Converts lat, lon, altitude points to x, y, z distances from a center point. + + Parameters + ---------- + lon : array_like + Longitude of points in degrees. + lat : array_like + Latitude of points in degrees. + alt : array_like + Altitude of points in meters. + lonctr : float + Longitude of the center point in degrees. + latctr : float + Latitude of the center point in degrees. + altctr : float + Altitude of the center point in meters. + + Returns + ------- + x : array_like + x distance in meters from the center point. + y : array_like + y distance in meters from the center point. + z : array_like + z distance in meters from the center point. + """ Re = 6378.137e3 #Earth's radius in m latavg, lonavg, altavg = latctr, lonctr, altctr x = Re * (np.radians(lon) - np.radians(lonavg)) * np.cos(np.radians(latavg)) @@ -13,9 +39,30 @@ def local_cartesian(lon, lat, alt, lonctr, latctr, altctr): return x,y,z def hull_volume(xyz): - """ Calculate the volume of the convex hull of 3D (X,Y,Z) LMA data. - xyz is a (N_points, 3) array of point locations in space. """ - assert xyz.shape[1] == 3 + """Calculate the volume of the convex hull of 3D (X,Y,Z) LMA data. + + Parameters + ---------- + xyz : array_like + A (N_points, 3) array of point locations in space. + + Returns + ------- + volume : float + The volume of the convex hull. + vertices : array_like + The vertices of the convex hull. + simplex_volumes : array_like + The volumes of the simplices that make up the convex hull. + + Raises + ------ + QhullError + If the convex hull cannot be computed. This is usually because too few points with little spacing are provided. See `perturb_vertex`. + + """ + if xyz.shape[1] != 3: + raise ValueError("Input must be an array of shape (N_points, 3).") tri = Delaunay(xyz[:,0:3]) vertices = tri.points[tri.simplices] @@ -37,11 +84,38 @@ def hull_volume(xyz): volume=np.sum(np.abs(simplex_volumes)) return volume, vertices, simplex_volumes + def perturb_vertex(x,y,z,machine_eps=1.0): - """ With only a few points, QHull can error on a degenerate first simplex - + """Add a random, small perturbation to an x, y, z point. + + With only a few points, QHull can error on a degenerate first simplex - all points are coplanar to machine precision. Add a random perturbation no greater than machine_eps to the first point in x, y, and z. Rerun QHull with the returned x,y,z arrays. + + Parameters + ---------- + x : array_like + x coordinates of points. + y : array_like + y coordinates of points. + z : array_like + z coordinates of points. + machine_eps : float, default=1.0 + The maximum absolute value of the perturbation. (Perturbation can be positive or negative.) + + Returns + ------- + x : array_like + x coordinates of points with perturbation added to the first point. + y : array_like + y coordinates of points with perturbation added to the first point. + z : array_like + z coordinates of points with perturbation added to the first point. + + Notes + ----- + This function is provided to work around the QHullError that can be raised by `pyxlma.lmalib.flash.properties.hull_volume` when the input points are coplanar. """ perturb = 2*machine_eps*np.random.random(size=3)-machine_eps x[0] += perturb[0] @@ -50,6 +124,22 @@ def perturb_vertex(x,y,z,machine_eps=1.0): return (x,y,z) def event_hull_area(x,y,z): + """Compute the 2D area of the convex hull of a set of x, y points. + + Parameters + ---------- + x : array_like + (N_points, 1) array of x coordinates of points. + y : array_like + (N_points, 1) array of y coordinates of points. + z : array_like + (N_points, 1) array of z coordinates of points [unused]. + + Returns + ------- + area : float + The area of the convex hull of the points. + """ pointCount = x.shape[0] area = 0.0 if pointCount > 3: @@ -79,6 +169,22 @@ def event_hull_area(x,y,z): return area def event_hull_volume(x,y,z): + """Compute the 3D volume of the convex hull of a set of x, y points. + + Parameters + ---------- + x : array_like + (N_points, 1) array of x coordinates of points. + y : array_like + (N_points, 1) array of y coordinates of points. + z : array_like + (N_points, 1) array of z coordinates of points. + + Returns + ------- + volume : float + The volume of the convex hull of the points. + """ pointCount = x.shape[0] volume = 0.0 if pointCount > 4: @@ -96,12 +202,20 @@ def event_hull_volume(x,y,z): def rrbe(zinit): - """ - Compute the runway breakeven threshold electric fields given - an initiation altitude for a lightning flash assuming a - surface breakdown electric field threshold of 281 kVm^-1 [Marshall et al. 2005]. + """Compute the runway breakeven threshold electric field. - Returns e_init in kVm^1. + Uses a given initiation altitude for a lightning flash assuming a surface breakdown electric field + threshold of 281 kV/m, following [Marshall et al. 2005](https://doi.org/10.1029/2004GL021802). + + Parameters + ---------- + zinit : float + The altitude of the flash initiation point in meters. + + Returns + ------- + e_init : float + The electric field at the initiation point in kV/m. """ #Compute scaled air density with height. rho_air = 1.208 * np.exp(-(zinit/8.4)) @@ -109,17 +223,29 @@ def rrbe(zinit): return(e_init) def event_discharge_energy(z,area): - """ - Estimate the electrical energy discharged by lightning flashes - using a simple capacitor model. - - Note: -) Model assumes plates area defined by convex hull area, and separation - between the 73 and 27th percentiles of each flash's vertical source - distributions. - -) Only the initiation electric field (e_init) between the plates at the height - of flash initiation to find the critical charge density (sigma_crit) on the plates, - sigma_crit = epsilon * e_init -> epsilon = permitivity of air - -) Model considers the ground as a perfect conductor (image charging), + """Estimate the electrical energy discharged by lightning flashes using a simple capacitor model. + + Parameters + ---------- + z : array_like + The altitude of the flash initiation points. + area : array_like + The area of the convex hull of the flash initiation points. + + Returns + ------- + energy : array_like + The energy discharged by the flash in Joules. + + Notes + ----- + - Model assumes plates area defined by convex hull area, and separation + between the 73 and 27th percentiles of each flash's vertical source + distributions. + - Only the initiation electric field (e_init) between the plates at the height + of flash initiation to find the critical charge density (sigma_crit) on the plates, + sigma_crit = epsilon * e_init -> epsilon = permitivity of air + - Model considers the ground as a perfect conductor (image charging), """ #Compute separation between charge plates (d) and critial charge density (sigma_crit): @@ -139,7 +265,39 @@ def event_discharge_energy(z,area): def flash_stats(ds, area_func=None, volume_func=None): - + """Compute flash statistics from LMA data. + + Calculates the following variables for each flash in the dataset: + + - flash_time_start + - flash_time_end + - flash_duration + - flash_init_latitude + - flash_init_longitude + - flash_init_altitude + - flash_area + - flash_volume + - flash_energy + - flash_center_latitude + - flash_center_longitude + - flash_center_altitude + - flash_power + - flash_event_count + + Parameters + ---------- + ds : xarray.Dataset + An LMA dataset that has flash clustering applied (i.e., has a `flash_id` and `event_parent_flash_id` variable). + area_func : callable, optional + A function that computes the area of the convex hull of a set of points. If None, `event_hull_area` is used. + volume_func : callable, optional + A function that computes the volume of the convex hull of a set of points. If None, `event_hull_volume` is used. + + Returns + ------- + ds : xarray.Dataset + The original dataset with the computed flash statistics added as variables. + """ if area_func is None: area_func = lambda df: event_hull_area(df['event_x'].array, df['event_y'].array, @@ -274,14 +432,28 @@ def flash_stats(ds, area_func=None, volume_func=None): return ds -def filter_flashes(ds, **kwargs): - """ each kwarg is a flash variable name, with tuple of minimum and maximum - values for that kwarg. min and max are inclusive (<=, >=). If either - end of the range can be None to skip it. - - Also removes events not associated with any flashes if prune=True (default) +def filter_flashes(ds, prune=True, **kwargs): + """Filter flashes by their properties. + + Allows removing unwanted flashes from an LMA dataset based on their properties. + After the flashes are removed by the criteria, the dataset can be pruned to remove any events that + are not assoicated with any flashes (or events that were associated with now-removed flashes). + + Parameters + ---------- + ds : xarray.Dataset + An LMA dataset that has flash clustering applied (i.e., has a `flash_id` and `event_parent_flash_id` variable). + prune : bool, default=True + If True, remove events not associated with any flashes. + **kwargs + Variable names and ranges to filter by. The name of the keyword argument is used as the variable name, + and ranges are given as a tuple of (min, max) values. Either end of the range can be None to skip it. + + Returns + ------- + ds : xarray.Dataset + The original dataset with the flashes filtered by the given criteria. """ - prune = kwargs.pop('prune', True) # keep all points good = np.ones(ds.flash_id.shape, dtype=bool) # print("Starting flash count: ", good.sum()) diff --git a/pyxlma/xarray_util.py b/pyxlma/xarray_util.py index 11f8cbc..89f4bfc 100644 --- a/pyxlma/xarray_util.py +++ b/pyxlma/xarray_util.py @@ -72,7 +72,20 @@ def get_1d_datasets(d, ): def get_scalar_vars(d): + """Find all variables in an [`xarray.Dataset`](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html) that are scalars. + + All variables in the dataset that have no dimensions are considered scalars. + + Parameters + ---------- + d : xarray.Dataset + The dataset to find scalar variables in. + Returns + ------- + scalars : list + A list of variable names that are scalars. + """ scalars = [] for varname, var in d.variables.items(): if len(var.dims) == 0: @@ -88,7 +101,7 @@ def concat_1d_dims(datasets, stack_scalars=False): Parameters ---------- - d : iterable of xarray.Dataset + datasets : iterable of xarray.Dataset The datasets to concatenate. stack_scalars : bool, default=False if True, create a new dimension named with this value that aggregates all scalar variables and coordinates From 7505e99f9a1573ad93ff53709471b5f3a46babb1 Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Mon, 13 Jan 2025 22:47:10 -0600 Subject: [PATCH 05/27] add dark mode --- mkdocs.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3723df5..0aedd21 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,19 @@ site_name: XLMA Python theme: name: material + palette: + + # Palette toggle for light mode + - scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode plugins: - search @@ -19,9 +32,6 @@ nav: - api/coords/transforms.md - xarray: - api/xarray/index.md - # - GeographicSystem: api/coords/GeographicSystem.md - # - MapProjection: api/coords/MapProjection.md - # - PixelGrid: api/coords/PixelGrid.md - # - GeostationaryFixedGridSystem: api/coords/GeostationaryFixedGridSystem.md - # - RadarCoordinateSystem: api/coords/RadarCoordinateSystem.md - # - TangentPlaneCartesianSystem: api/coords/TangentPlaneCartesianSystem.md + - lmalib: + - api/lmalib/flash.md + From f55f726c481e6db055b6fe70313159b6553b4e53 Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Tue, 14 Jan 2025 00:22:18 -0600 Subject: [PATCH 06/27] some basic stylization --- README.md | 21 ++- docs/index.md | 19 +- docs/xlma_logo_big_big_ltg.svg | 332 +++++++++++++++++++++++++++++++++ mkdocs.yml | 7 + 4 files changed, 352 insertions(+), 27 deletions(-) mode change 100644 => 120000 docs/index.md create mode 100644 docs/xlma_logo_big_big_ltg.svg diff --git a/README.md b/README.md index e03ddf7..265d98a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +logo + # xlma-python A future, Python-based version of xlma? @@ -5,7 +7,7 @@ XLMA is a venerable IDL GUI program that diplays VHF Lightning Mapping Array dat Please use the issues tracker to discuss ideas and pull requests to contribute examples. -# Installation +## Installation Clone this repostiory install with pip. ``` @@ -16,7 +18,7 @@ pip install -e . Then, copy the `XLMA_plots.ipynb` notebook to wherever you'd like and start changing files, dates and times to show data from your case of interest. There also a notebook showing how to do flash sorting and save a new NetCDF file with those data. -# Dependencies +## Dependencies Required: - xarray (I/O requires the netcdf4 backend) @@ -36,6 +38,7 @@ Plotting: - metpy (optionally, for US county lines) GLM Plotting: + - glmtools (https://github.com/deeplycloudy/glmtools) Interactive: @@ -52,23 +55,23 @@ Building: - lmatools (https://github.com/deeplycloudy/lmatools) - ...and all of the above -# Technical architecture +## Technical architecture We envision a two-part package that keeps a clean separation between the core data model, analysis, and display. XLMA utilized a large, global `state` structure that stored all data, as well as the current data selection corresponding to the view in the GUI. Analysis then operated on whatever data was in the current selection. -## Data model and subsetting +### Data model and subsetting `xarray` is the obvious choice for the core data structure, because it supports multidimensional data with metadata for each variable, subsetting of all varaibles sharing a dimension, and fast indexing. Data can be easily saved and read from the NetCDF format, and converted from ASCII to `Dataset` using standard tools. -## Analysis +### Analysis Some core features of LMA data analysis will be built in, TBD after surveying capabilities in XLMA. -## Display +### Display Keeping the core data structure and selection operations separate from dislpay is good programming practice. It is doubly important in Python, where there is not one obvious solution for high performance *and* publication-quality graphics as in IDL. -### Plotting library +#### Plotting library There are many options, so we want a design that: 1. Permits a GUI to provide the bounds of the current view (or a polygon lasso) to the data model, changing the subset @@ -81,7 +84,7 @@ There are many options, so we want a design that: - Datashader might be useful as a method of data reduction prior to visualization even if we don't use Bokeh. - Yt - written by the astronomy community in Python … is it fast enough? -### GUI +#### GUI There is no obvious choice here, either. @@ -90,7 +93,7 @@ There is no obvious choice here, either. - PyQT - [Glue](https://github.com/glue-viz/glue/wiki/SciPy-2019-Tutorial-on-Multi-dimensional-Linked-Data-Exploration-with-Glue) - seems about 60% there out of the box?! -# Prior art +## Prior art - [`lmatools`](https://github.com/deeplycloudy/lmatools/) - Includes readers for LMA and NLDN data (using older methods from 2010) diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index a2993ec..0000000 --- a/docs/index.md +++ /dev/null @@ -1,18 +0,0 @@ -# Welcome to MkDocs - -For full documentation visit [mkdocs.org](https://www.mkdocs.org). - -## Commands - -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs -h` - Print help message and exit. - -## Project layout - - - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - ... # Other markdown pages, images and other files. diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/docs/xlma_logo_big_big_ltg.svg b/docs/xlma_logo_big_big_ltg.svg new file mode 100644 index 0000000..f15d2f2 --- /dev/null +++ b/docs/xlma_logo_big_big_ltg.svg @@ -0,0 +1,332 @@ + + + +xlma diff --git a/mkdocs.yml b/mkdocs.yml index 0aedd21..e57d2f6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,13 @@ site_name: XLMA Python +repo_url: https://github.com/deeplycloudy/xlma-python + theme: name: material + logo: xlma_logo_big_big_ltg.svg + favicon: xlma_logo_big_big_ltg.svg + icon: + repo: fontawesome/brands/github palette: # Palette toggle for light mode @@ -34,4 +40,5 @@ nav: - api/xarray/index.md - lmalib: - api/lmalib/flash.md + - api/lmalib/cf_netcdf.md From 66b70dce8602317d25d0adb119ab139c07589284 Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Tue, 14 Jan 2025 00:23:03 -0600 Subject: [PATCH 07/27] add glm, lmatools, cf_netcdf documentation --- docs/api/lmalib/cf_netcdf.md | 6 ++ docs/api/lmalib/flash.md | 2 +- pyxlma/lmalib/flash/properties.py | 4 ++ pyxlma/lmalib/io/cf_netcdf.py | 105 ++++++++++++++++++------------ pyxlma/lmalib/io/glm.py | 20 +++++- pyxlma/lmalib/io/lmatools.py | 27 ++++++-- pyxlma/plot/xlma_plot_feature.py | 2 +- 7 files changed, 119 insertions(+), 47 deletions(-) create mode 100644 docs/api/lmalib/cf_netcdf.md diff --git a/docs/api/lmalib/cf_netcdf.md b/docs/api/lmalib/cf_netcdf.md new file mode 100644 index 0000000..bf9d07c --- /dev/null +++ b/docs/api/lmalib/cf_netcdf.md @@ -0,0 +1,6 @@ +# Climate/Forecasting NetCDF + +This module is designed to provide a standardized way to name and attribute lightning data in NetCDF4 dataset, +akin to the [Climate and Forecasting](https://cfconventions.org) specification. These tools are primarily used internally for I/O operations, but are provided to the user for convenience. + +::: pyxlma.lmalib.io.cf_netcdf diff --git a/docs/api/lmalib/flash.md b/docs/api/lmalib/flash.md index 89882b1..8fef466 100644 --- a/docs/api/lmalib/flash.md +++ b/docs/api/lmalib/flash.md @@ -1,4 +1,4 @@ -# Flash Processing +# Flash processing ::: pyxlma.lmalib.flash.cluster diff --git a/pyxlma/lmalib/flash/properties.py b/pyxlma/lmalib/flash/properties.py index c088e88..be3cbff 100644 --- a/pyxlma/lmalib/flash/properties.py +++ b/pyxlma/lmalib/flash/properties.py @@ -139,6 +139,10 @@ def event_hull_area(x,y,z): ------- area : float The area of the convex hull of the points. + + Notes + ----- + For more info on convex hull area and flash size, see [Bruning and MacGorman 2013](https://doi.org/10.1175/JAS-D-12-0289.1). """ pointCount = x.shape[0] area = 0.0 diff --git a/pyxlma/lmalib/io/cf_netcdf.py b/pyxlma/lmalib/io/cf_netcdf.py index 6ce8d45..a3017c2 100644 --- a/pyxlma/lmalib/io/cf_netcdf.py +++ b/pyxlma/lmalib/io/cf_netcdf.py @@ -1,20 +1,20 @@ -""" -To automatically compare a file with the specification in this file, +# """ +# To automatically compare a file with the specification in this file, -import xarray as xr -from pyxlma.lmalib.io.cf_netcdf import new_dataset -ds_test = xr.open_dataset('test_LMA_dataset.nc', decode_cf=False) -ds_valid = new_dataset(flashes=ds_orig.dims['number_of_flashes'], events=ds_orig.dims['number_of_events']) -try: - xr.testing.assert_identical(ds_valid, ds_test) -except AssertionError as e: - print("Left dataset is the validation dataset - print("Right Dataset is the test dataset provided") - report=str(e) - print(report) - with open('report.txt', 'w') as f: - f.write(report) -""" +# import xarray as xr +# from pyxlma.lmalib.io.cf_netcdf import new_dataset +# ds_test = xr.open_dataset('test_LMA_dataset.nc', decode_cf=False) +# ds_valid = new_dataset(flashes=ds_orig.dims['number_of_flashes'], events=ds_orig.dims['number_of_events']) +# try: +# xr.testing.assert_identical(ds_valid, ds_test) +# except AssertionError as e: +# print("Left dataset is the validation dataset +# print("Right Dataset is the test dataset provided") +# report=str(e) +# print(report) +# with open('report.txt', 'w') as f: +# f.write(report) +# """ import copy @@ -26,6 +26,13 @@ # http://cfconventions.org, # https://www.unidata.ucar.edu/software/netcdf/conventions.html def new_template_dataset(): + """Create a new, empty xarray dataset for LMA data. + + Returns + ------- + __template_dataset : dict + A dictionary that can be used to create a new xarray dataset for LMA data. + """ __template_dataset = {'coords': {}, 'attrs': {'title': 'Lightning Mapping Array dataset, L1b events and L2 flashes', 'production_date': '1970-01-01 00:00:00 +00:00', @@ -340,9 +347,9 @@ def new_template_dataset(): return __template_dataset def validate_events(ds, dim='number_of_events', check_events=True, check_flashes=True): - """ Take an xarray dataset ds and check to ensure all expected variables - and attributes exist. Print a report of anything that doesn't match. - """ + # """ Take an xarray dataset ds and check to ensure all expected variables + # and attributes exist. Print a report of anything that doesn't match. + # """ # Will need to make dimensions and data equal for this to work. Subset to # zero-length dimensions? I think that drops the dim. So subset to single length and then assing a value of zero to each variable? # http://xarray.pydata.org/en/stable/generated/xarray.testing.assert_identical.html#xarray.testing.assert_identical @@ -356,29 +363,41 @@ def validate_events(ds, dim='number_of_events', check_events=True, check_flashes def new_dataset(events=None, flashes=None, stations=None, **kwargs): """ Create a new, empty xarray dataset for LMA data. - Keyword arguments: - events (int, optional): number of events - flashes (int, optional): number of flashes - stations (int, optional): number of stations - production_date: a time string corresponding to the CF standards, - e.g., '2020-04-26 21:08:42 +00:00' - production_site (string): Information about the production site. Useful + Parameters + ---------- + events : int, optional + number of events + flashes : int, optional + number of flashes + stations : int, optional + number of stations + production_date : str, optional + a time string corresponding to the CF standards, '%Y-%m-%d %H:%M:%S %z' format. e.g., '2020-04-26 21:08:42 +00:00' + production_site : str + Information about the production site. Useful if an institution has more than one physical location. - event_algorithm_name (string): The name of the algorithm used to locate - station-level triggers as events. For LMA data, usually lma_analysis. - May also be the command issued to process the data, with any relevant - information regarding data quality thresholds also reported in + event_algorithm_name : str + The name of the algorithm used to locate station-level triggers as events. For LMA data, usually lma_analysis. + May also be the command issued to process the data, with any relevant information regarding data quality thresholds also reported in data variables reserved for that purpose. - event_algorithm_version (string): The event algorithm version - flash_algorithm_name (string): The name of the algorithm used to cluster - events to flashes. May also be the command issued to process the data, - with any relevant information regarding space-time separation thresholds - also reported in data variables reserved for that purpose. - flash_algorithm_version (string): The flash algorithm version - institution, comment, history, references, source: see + event_algorithm_version : str + The event algorithm version + flash_algorithm_name : str + The name of the algorithm used to cluster events to flashes. May also be the command issued to process the data, + with any relevant information regarding space-time separation thresholds also reported in data variables reserved for that purpose. + flash_algorithm_version : str + The flash algorithm version institution, comment, history, references, source: see http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#description-of-file-contents - Any other keyword arguments are also added as a global attribute. + **kwargs + Additional attributes to add to the dataset. + Returns + ------- + ds : xarray.Dataset + An empty dataset with the appropriate dimensions and attributes. + + Notes + ----- If event, flash, or station information are not known, passing None (default) will drop variables associated with that dimension. """ @@ -458,8 +477,14 @@ def new_dataset(events=None, flashes=None, stations=None, **kwargs): return xr.Dataset.from_dict(ds_dict) def compare_attributes(ds): - """ Compare the attributes of all data variables in ds to the CF spec - and print a report of any differences. + """ Compare the attributes of all data variables on a dataset to the CF spec. + + Prints a report of any differences. + + Parameters + ---------- + ds : xarray.Dataset + The dataset to compare. """ dst = __template_dataset['data_vars'] dsd = ds.to_dict()['data_vars'] diff --git a/pyxlma/lmalib/io/glm.py b/pyxlma/lmalib/io/glm.py index 1c6abe1..4fcc595 100644 --- a/pyxlma/lmalib/io/glm.py +++ b/pyxlma/lmalib/io/glm.py @@ -1,7 +1,25 @@ -from glmtools.io.glm import GLMDataset from pyxlma.xarray_util import concat_1d_dims def combine_glm_l2(filenames): + """Combine multiple GLM L2 files into a single dataset. + + Reads and concatenates multiple GLM L2 files into a single dataset. + + Parameters + ---------- + filenames : list of str + List of filenames to read. + + Returns + ------- + combo : xarray.Dataset + Combined GLM dataset. + + Notes + ----- + This function requires the `glmtools` package to be installed. + """ + from glmtools.io.glm import GLMDataset scalar_dim = 'segment' datasets=[] for fn in filenames: diff --git a/pyxlma/lmalib/io/lmatools.py b/pyxlma/lmalib/io/lmatools.py index aa7066e..4ee25bf 100644 --- a/pyxlma/lmalib/io/lmatools.py +++ b/pyxlma/lmalib/io/lmatools.py @@ -27,18 +27,36 @@ # flash_init_* -from lmatools.io.LMAarrayFile import LMAdataFile import datetime as dt import numpy as np class LMAdata(object): - - def __init__(self, filename, **kwargs): - self.lma = LMAdataFile(filename, mask_length=kwargs['mask_length']) + """Helper class to read LMA data using lmatools LMAdataFile. + + Warning + ------- + This class is provided for backwards compatibility with lmatools. + It is highly encouraged to use the functions in pyxlma.lmalib.io.read in new code. + """ + def __init__(self, filename, mask_length, **kwargs): + """Initialize LMAdata object. + + Parameters + ---------- + filename : str + Path to LMA data file. + mask_length : int + Length of the hexadecimal station mask in the LMA data file. + **kwargs + Filter parameters to use when reading LMA data. Valid keys are 'stn', 'chi2', and 'alt'. + """ + from lmatools.io.LMAarrayFile import LMAdataFile + self.lma = LMAdataFile(filename, mask_length=mask_length) self.get_date() self.limit_data(**kwargs) def get_date(self): + """Get date from LMA data file.""" for line in self.lma.header: if line[0:10] == 'Data start': datestr = line[17:25] @@ -60,6 +78,7 @@ def get_date(self): self.day = dy def limit_data(self, **kwargs): + """Limit LMA data based on filter parameters.""" good1 = (self.lma.stations >= kwargs['stn']) & \ (self.lma.chi2 <= kwargs['chi2']) & (self.lma.alt < kwargs['alt']) good2 = np.logical_and( diff --git a/pyxlma/plot/xlma_plot_feature.py b/pyxlma/plot/xlma_plot_feature.py index 1f01800..0cebdff 100644 --- a/pyxlma/plot/xlma_plot_feature.py +++ b/pyxlma/plot/xlma_plot_feature.py @@ -209,7 +209,7 @@ def plot_glm_events(glm, bk_plot, fake_alt=[0, 1], should_parallax_correct=True, the axes relative coordinates to plot the vertical lines for GLM events in the cross section, default [0, 1], the full height of the axes. should_parallax_correct : bool - whether to correct the GLM event locations for parallax effect. See https://doi.org/10.1029/2019JD030874 for more information. + whether to correct the GLM event locations for parallax effect. See [Bruning et. al 2019, figure 5](https://doi.org/10.1029/2019JD030874). poly_kwargs : dict dictionary of additional keyword arguments to be passed to matplotlib Polygon vlines_kwargs : dict From 44acbfde9943c32f9e394312266e2a7e216888fb Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Tue, 14 Jan 2025 14:35:43 -0600 Subject: [PATCH 08/27] add io.read and lma_intercept_rhi documentation --- docs/api/lmalib/lma_intercept_rhi.md | 3 + docs/api/lmalib/read.md | 7 ++ mkdocs.yml | 4 ++ pyxlma/lmalib/io/read.py | 97 +++++++++++++++++++++------- pyxlma/lmalib/lma_intercept_rhi.py | 97 ++++++++++++++-------------- 5 files changed, 137 insertions(+), 71 deletions(-) create mode 100644 docs/api/lmalib/lma_intercept_rhi.md create mode 100644 docs/api/lmalib/read.md diff --git a/docs/api/lmalib/lma_intercept_rhi.md b/docs/api/lmalib/lma_intercept_rhi.md new file mode 100644 index 0000000..6fc39b4 --- /dev/null +++ b/docs/api/lmalib/lma_intercept_rhi.md @@ -0,0 +1,3 @@ +# LMA with a radar RHI + +::: pyxlma.lmalib.lma_intercept_rhi \ No newline at end of file diff --git a/docs/api/lmalib/read.md b/docs/api/lmalib/read.md new file mode 100644 index 0000000..d9a6df6 --- /dev/null +++ b/docs/api/lmalib/read.md @@ -0,0 +1,7 @@ +# Reading data + +::: pyxlma.lmalib.io.read + options: + filters: + - "!^to_dataset$" + - "!^lmafile$" \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e57d2f6..7998e4c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,6 +29,7 @@ plugins: python: options: docstring_style: numpy + show_source: false nav: - Home: index.md @@ -41,4 +42,7 @@ nav: - lmalib: - api/lmalib/flash.md - api/lmalib/cf_netcdf.md + - api/lmalib/read.md + - api/lmalib/lma_intercept_rhi.md + diff --git a/pyxlma/lmalib/io/read.py b/pyxlma/lmalib/io/read.py index 4cefe2f..ac3cac8 100644 --- a/pyxlma/lmalib/io/read.py +++ b/pyxlma/lmalib/io/read.py @@ -5,6 +5,11 @@ import datetime as dt class open_gzip_or_dat: + """Helper class open a file with gzip if necessary or as binary if already decompressed. + + Use as a context manager in the same way as `with open(filename) as f:`. If the filename ends with + '.gz', the file will be decompressed with gzip. Otherwise, the file will be opened as binary. + """ def __init__(self, filename): self.filename = filename @@ -19,7 +24,22 @@ def __exit__(self, exc_type, exc_value, traceback): self.file.close() def mask_to_int(mask): - """ Convert object array of mask strings to integers""" + """Convert object array of mask strings to integers. + + Parameters + ---------- + mask : array_like + An array of strings representing the station mask. Each string should be a hexidecimal representing a binary station mask. + + Returns + ------- + mask_int : ndarray + An array of integers representing the station mask. Each integer is the binary representation of the station mask. + + Notes + ----- + Information on the station mask can be found in the [LMA Hardware section of lmaworkshop](https://github.com/deeplycloudy/lmaworkshop/blob/master/LEE-2023/LMAsourcefiles.ipynb) + """ if len(mask.shape) == 0: mask_int = np.asarray([], dtype=int) else: @@ -32,9 +52,17 @@ def mask_to_int(mask): return mask_int def combine_datasets(lma_data): - """ lma_data is a list of xarray datasets of the type returned by - pyxlma.lmalib.io.cf_netcdf.new_dataset or - pyxlma.lmalib.io.read.to_dataset + """Combine a list of LMA datasets. + + Parameters + ---------- + lma_data : list of xarray.Dataset + list of LMA datasets of the type returned by pyxlma.lmalib.io.cf_netcdf.new_dataset or pyxlma.lmalib.io.read.to_dataset + + Returns + ------- + ds : xarray.Dataset + A combined dataset of the LMA data """ # Get a list of all the global attributes from each dataset attrs = [d.attrs for d in lma_data] @@ -94,8 +122,25 @@ def combine_datasets(lma_data): return ds def dataset(filenames, sort_time=True): - """ Create an xarray dataset of the type returned by - pyxlma.lmalib.io.cf_netcdf.new_dataset for each filename in filenames + """Read LMA .dat or .dat.gz file(s) into an xarray dataset. + + The dataset returned is the same type as is returned by + pyxlma.lmalib.io.cf_netcdf.new_dataset for each filename in filenames. + + Parameters + ---------- + filenames : str or list of str + The file or files to read in + sort_time : bool, default=True + Whether to sort the VHF events by time after reading in the data. + + Returns + ------- + ds : xarray.Dataset + An xarray dataset of the LMA data. + starttime : datetime + The start of the period of record of the LMA data. + """ if type(filenames) == str: filenames = [filenames] @@ -140,7 +185,7 @@ def dataset(filenames, sort_time=True): return ds, starttime def to_dataset(lma_file, event_id_start=0): - """ lma_file: an instance of an lmafile object + """lma_file: an instance of an lmafile object returns an xarray dataset of the type returned by pyxlma.lmalib.io.cf_netcdf.new_dataset @@ -222,7 +267,7 @@ def to_dataset(lma_file, event_id_start=0): def nldn(filenames): """ - Read Viasala NLDN data + Read [Viasala NLDN](https://www.vaisala.com/en/products/national-lightning-detection-network-nldn) data Reads in one or multiple NLDN files and and returns a pandas dataframe with appropriate column names @@ -280,7 +325,7 @@ def nldn(filenames): def entln(filenames): """ - Read Earth Networks Total Lightning Network data + Read [aem/Earth Networks Total Lightning Network](https://aem.eco/product/earth-networks-total-lightning-network/) data Reads in one or multiple ENTLN files and and returns a pandas dataframe with appropriate column names @@ -331,20 +376,28 @@ def entln(filenames): class lmafile(object): + """Class representing an lmafile object for data being read in. To read in the data, use the `to_dataset` method.""" def __init__(self,filename): - """ - Pull the basic metadata from a '.dat.gz' LMA file - - startday : the date (datetime format) - station_info_start : the line number (int) where the station information starts - station_data_start : the line number (int) where the summarized station data starts - station_data_end : the line number (int) end of the summarized station data - maskorder : the order of stations in the station mask (str) - names : column header names - data_starts : the line number (int) where the VHF source data starts - - overview : summarized station data from file header (DataFrame, assumes fixed-width format) - stations : station information from file header (DataFrame, assumes fixed-width format) + """Pull the basic metadata from a '.dat.gz' LMA file + + Attributes + ---------- + startday : datetime + the date of the start of the period of record. + station_info_start : int + the line number in the decompressed file where the station information starts + station_data_start : int + the line number in the decompressed file where the summarized station data starts + station_data_end : int + the line number in the decompressed file of the end of the summarized station data + maskorder : str + the order of stations in the station mask + names : list + column header names + data_starts : int + the line number in the decompressed file where the VHF source data starts + stations : pd.Dataframe + station information from file header (DataFrame, assumes fixed-width format) """ self.file = filename diff --git a/pyxlma/lmalib/lma_intercept_rhi.py b/pyxlma/lmalib/lma_intercept_rhi.py index e7a810a..89c0fbe 100644 --- a/pyxlma/lmalib/lma_intercept_rhi.py +++ b/pyxlma/lmalib/lma_intercept_rhi.py @@ -5,30 +5,29 @@ def rcs_to_tps(radar_latitude, radar_longitude, radar_altitude, radar_azimuth): - """ - Find the unit vector coordinates (east, north, up) of the plane of a radar RHI scan. + """Find the unit vector coordinates (east, north, up) of the plane of a radar RHI scan. Creates a azimuth, elevation, range and tangent plane cartesian system at the radar's latitude and longitude, and converts the RHI azimuth direction to the tangent plane coordinate system. Parameters ---------- - radar_latitude : `float` + radar_latitude : float Latitude of the radar in degrees. - radar_longitude : `float` + radar_longitude : float Longitude of the radar in degrees. - radar_altitude : `float` + radar_altitude : float Altitude of the radar in meters. - radar_azimuth : `float` + radar_azimuth : float Azimuth of the RHI scan in degrees. Returns ---------- - X : `numpy.ndarray` + X : numpy.ndarray A 1x2 array representing the start and end points eastward component of the RHI scan. - Y : `numpy.ndarray` + Y : numpy.ndarray A 1x2 array representing the start and end points northward component of the RHI scan. - Z : `numpy.ndarray` + Z : numpy.ndarray A 1x2 array representing the start and end points upward component of the RHI scan. """ @@ -68,22 +67,22 @@ def geo_to_tps(event_longitude, event_latitude, event_altitude, tps_latitude, tp Parameters ---------- - event_longitude : `xarray.Dataset` + event_longitude : xarray.Dataset A pyxlma dataset containing latitude, longitude, and altitude of LMA VHF sources. - tps_latitude : `float` + tps_latitude : float Latitude of the tangent plane in degrees. - tps_longitude : `float` + tps_longitude : float Longitude of the tangent plane in degrees. - tps_altitude : `float` + tps_altitude : float Altitude of the tangent plane in meters. Returns ---------- - Xlma : `numpy.ndarray` + Xlma : numpy.ndarray A 1xN array representing the eastward distance (in meters) of the tangent plane center to the LMA VHF sources. - Ylma : `numpy.ndarray` + Ylma : numpy.ndarray A 1xN array representing the northward distance (in meters) of the tangent plane center to the LMA VHF sources. - Zlma : `numpy.ndarray` + Zlma : numpy.ndarray A 1xN array representing the upward distance (in meters) of the tangent plane center to the LMA VHF sources. """ # GeographicSystem GEO - Lat, lon, alt @@ -115,20 +114,20 @@ def ortho_proj_lma(event_longitude, event_latitude, event_altitude, radar_latitu Parameters ---------- - lma_file : `xarray.Dataset` + lma_file : xarray.Dataset A pyxlma dataset containing latitude, longitude, and altitude of N number of LMA VHF sources. - radar_latitude : `float` + radar_latitude : float Latitude of the radar in degrees. - radar_longitude : `float` + radar_longitude : float Longitude of the radar in degrees. - radar_altitude : `float` + radar_altitude : float Altitude of the radar in meters. - radar_azimuth : `float` + radar_azimuth : float Azimuth of the RHI scan in degrees. Returns ---------- - lma_file_loc : `numpy.ndarray` + lma_file_loc : numpy.ndarray A Nx3 array representing the distance along, distance from, and height above the ground (in m) of the LMA VHF sources. """ @@ -203,39 +202,39 @@ def find_points_near_rhi(event_longitude, event_latitude, event_altitude, event_ Parameters ---------- - event_longitude : array-like + event_longitude : array_link An array of the latitudes of events to be transformed. - event_latitude : array-like + event_latitude : array_link An array of the latitudes of events to be transformed. - event_altitude : array-like + event_altitude : array_link An array of the altitudes of events to be transformed. - event_time : array-like + event_time : array_link An array of the times of events to be transformed. - radar_latitude : `float` + radar_latitude : float Latitude of the radar in degrees. - radar_longitude : `float` + radar_longitude : float Longitude of the radar in degrees. - radar_altitude : `float` + radar_altitude : float Altitude of the radar in meters. - radar_azimuth : `float` + radar_azimuth : float Azimuth of the RHI scan in degrees. - radar_scan_time : `datetime.datetime` or `numpy.datetime64` or `pandas.Timestamp` + radar_scan_time : datetime.datetime or numpy.datetime64 or pandas.Timestamp Time of the RHI scan. - distance_threshold : `float` + distance_threshold : float Maximum distance from the radar to the LMA VHF sources in meters. Default is 1000. - time_threshold : `float` + time_threshold : float Number of seconds before and after the RHI scan time to include LMA VHF sources. Default is 30. (total length: 1 minute) Returns ---------- - lma_range : `numpy.ndarray` + lma_range : numpy.ndarray A 1D array representing the distance along the tangent plane in the direction of the RHI scan. - lma_dist : `numpy.ndarray` + lma_dist : numpy.ndarray A 1D array representing the distance from the radar RHI scan plane to each filtered LMA point. - lma_alt : `numpy.ndarray` + lma_alt : numpy.ndarray A 1D array representing the height above the tangent plane centered at radar level of each filtered LMA point. - point_mask : `numpy.ndarray` + point_mask : numpy.ndarray A 1D array of booleans representing the VHF points that were included in the return. """ @@ -274,33 +273,33 @@ def find_lma_points_near_rhi(lma_file, radar_latitude, radar_longitude, radar_al Parameters ---------- - lma_file : `xarray.Dataset` + lma_file : xarray.Dataset A pyxlma dataset containing latitude, longitude, and altitude, and event_id of N number of LMA VHF sources. - radar_latitude : `float` + radar_latitude : float Latitude of the radar in degrees. - radar_longitude : `float` + radar_longitude : float Longitude of the radar in degrees. - radar_altitude : `float` + radar_altitude : float Altitude of the radar in meters. - radar_azimuth : `float` + radar_azimuth : float Azimuth of the RHI scan in degrees. - radar_scan_time : `datetime.datetime` or `numpy.datetime64` or `pandas.Timestamp` + radar_scan_time : datetime.datetime or numpy.datetime64 or pandas.Timestamp Time of the RHI scan. - distance_threshold : `float` + distance_threshold : float Maximum distance from the radar to the LMA VHF sources in meters. Default is 1000. - time_threshold : `float` + time_threshold : float Number of seconds before and after the RHI scan time to include LMA VHF sources. Default is 30. (total length: 1 minute) Returns ---------- - lma_range : `numpy.ndarray` + lma_range : numpy.ndarray A 1D array representing the distance along the tangent plane in the direction of the RHI scan. - lma_dist : `numpy.ndarray` + lma_dist : numpy.ndarray A 1D array representing the distance from the radar RHI scan plane to each filtered LMA point. - lma_alt : `numpy.ndarray` + lma_alt : numpy.ndarray A 1D array representing the height above the tangent plane centered at radar level of each filtered LMA point. - point_mask : `numpy.ndarray` + point_mask : numpy.ndarray A 1D array of booleans representing the VHF points that were included in the return. """ return find_points_near_rhi(lma_file.event_longitude.data, lma_file.event_latitude.data, lma_file.event_altitude.data, From cc06d48d5f007cb9f72494f3ff9875457dcd0399 Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Thu, 16 Jan 2025 10:40:48 -0600 Subject: [PATCH 09/27] add gridding documentation --- docs/api/lmalib/grid.md | 3 + mkdocs.yml | 2 + pyxlma/lmalib/grid.py | 188 +++++++++++++++++++++++++--------------- 3 files changed, 121 insertions(+), 72 deletions(-) create mode 100644 docs/api/lmalib/grid.md diff --git a/docs/api/lmalib/grid.md b/docs/api/lmalib/grid.md new file mode 100644 index 0000000..784113a --- /dev/null +++ b/docs/api/lmalib/grid.md @@ -0,0 +1,3 @@ +# Gridding + +::: pyxlma.lmalib.grid \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 7998e4c..b7c45c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,5 @@ site_name: XLMA Python +repo_name: deeplycloudy/xlma-python repo_url: https://github.com/deeplycloudy/xlma-python @@ -44,5 +45,6 @@ nav: - api/lmalib/cf_netcdf.md - api/lmalib/read.md - api/lmalib/lma_intercept_rhi.md + - api/lmalib/grid.md diff --git a/pyxlma/lmalib/grid.py b/pyxlma/lmalib/grid.py index fef41ad..3a8bfb0 100644 --- a/pyxlma/lmalib/grid.py +++ b/pyxlma/lmalib/grid.py @@ -3,29 +3,31 @@ import xarray as xr def discretize(x, x0, dx, int_type='uint64', bounds_check=True): - """ Calculate a unique location ID given some - discretization interval and allowed range. - - Values less than x0 raise an exception of bounds_check=True, - othewise the assigned index may wrap according when integer - casting is performed. - - Arguments: - x: coordinates, float array - x0: minimum x value - dx: discretization interval - - Keyword arguments: - int_type: numpy dtype of x_id. 64 bit unsigned int by default, since 32 - bit is limited to a 65536 pixel square grid. - - Returns: - x_id = unique pixel ID - - assert (np.array([0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3], dtype='uint64') - == discretize(np.asarray([-2.0, -1.5, -1.0, -0.1, 0.0, 0.1, 0.4, - 0.5, 0.6, 0.9, 1.0, 1.1]), -2.00, 1.0)).all() + """Calculate a unique location ID given some discretization interval and allowed range. + + Values less than x0 raise an exception of bounds_check=True, + othewise the assigned index may wrap according when integer + casting is performed. + + Parameters + ---------- + x : array_like + coordinates of the points + x0 : float + minimum x value + dx : discretization interval + + int_type : str, default='uint64' + numpy dtype of x_id. 64 bit unsigned int by default, since 32 bit is limited to a 65536 pixel square grid. + + Returns + ------- + x_id : array_like + unique pixel ID """ + # assert (np.array([0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3], dtype='uint64') + # == discretize(np.asarray([-2.0, -1.5, -1.0, -0.1, 0.0, 0.1, 0.4, + # 0.5, 0.6, 0.9, 1.0, 1.1]), -2.00, 1.0)).all() if bounds_check: if (x Date: Sat, 18 Jan 2025 14:51:49 -0600 Subject: [PATCH 10/27] add traversal documentation --- docs/api/lmalib/traversal.md | 3 + mkdocs.yml | 1 + pyxlma/lmalib/traversal.py | 142 +++++++++++++++++++++-------------- 3 files changed, 90 insertions(+), 56 deletions(-) create mode 100644 docs/api/lmalib/traversal.md diff --git a/docs/api/lmalib/traversal.md b/docs/api/lmalib/traversal.md new file mode 100644 index 0000000..c16df93 --- /dev/null +++ b/docs/api/lmalib/traversal.md @@ -0,0 +1,3 @@ +# Traversal + +::: pyxlma.lmalib.traversal \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index b7c45c1..0ca1bd3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,5 +46,6 @@ nav: - api/lmalib/read.md - api/lmalib/lma_intercept_rhi.md - api/lmalib/grid.md + - api/lmalib/traversal.md diff --git a/pyxlma/lmalib/traversal.py b/pyxlma/lmalib/traversal.py index 54cdc01..2bead05 100644 --- a/pyxlma/lmalib/traversal.py +++ b/pyxlma/lmalib/traversal.py @@ -1,32 +1,13 @@ import collections, itertools import numpy as np -""" Code from glmtools, where there are also unit tests for this class. - TODO: port to work as an xarray accessor? And move unit tests here. Adapt - to automatically use cf-tree metadata. -""" +# """ Code from glmtools, where there are also unit tests for this class. +# TODO: port to work as an xarray accessor? And move unit tests here. Adapt +# to automatically use cf-tree metadata. +# """ class OneToManyTraversal(object): - def __init__(self, dataset, entity_id_vars, parent_id_vars): - """ - dataset is an xarray.Dataset - - entity_id_vars is a list of variable names giving indices that are unique - along the dimension of that variable. The names should be given in - order from the parent to children, e.g. - ('child_id', 'grandchild_id', 'greatgrandchild_id') - which corresponds to - ('child_dim', 'grandchild_dim', 'greatgrandchild_dim') - - parent_id_vars is a list of variable names giving the child to parent - relationships, and is along the dimension of the child. The names - should be given in order from parent to children, and be one less in - length than entitiy_id_vars, e.g. - ('parent_id_of_grandchild', 'parent_id_of_greatgrandchild') - which corresponds to - ('grandchild_dim', 'greatgrandchild_dim') - - + """A class to allow traversal of a dataset where data variables have a one-to-many relationships. This object creates groupbys stored by dictionaries that make it convenient to look up the groupby given either the p @@ -53,6 +34,19 @@ def __init__(self, dataset, entity_id_vars, parent_id_vars): of virtual dimension that links set membership instead of coordinates. A future NetCDF spec could formalize this relationship structure to encourage the same library-level functionality as this class. + """ + def __init__(self, dataset, entity_id_vars, parent_id_vars): + """Initialize a OneToManyTraversal object. + + Parameters + ---------- + dataset : xarray.Dataset + The dataset to be traversed. + entity_id_vars : list of str + The names of the N variables to be traversed, in order from grandparent -> parent -> child -> grandchild -> ... + Variables must be unique along the dimension of the variable. + parent_id_vars : list of str + The names of the N-1 variables that link the entities in entity_id_vars, in order from (grandparent_id_of_parent) -> (parent_id_of_child) -> (child_id_of_grandchild) -> ... """ n_entities = len(entity_id_vars) @@ -107,7 +101,7 @@ def _ascend(self): yield from zip(self.entity_id_vars[::-1], self.parent_id_vars[::-1]) def count_children(self, entity_id_var, child_entity_id_var=None): - """ Count the children of entity_id_var. + """Count the children of entity_id_var. Optionally, accumulate counts of children down to and including the level of child_entity_id_var. These are the counts from parent @@ -117,13 +111,26 @@ def count_children(self, entity_id_var, child_entity_id_var=None): where the top and bottom are separated by a few levels, it is possible to get an aggregate count (matching the top parent dimension) of the children many generations below by doing: - > grouper = dataset.groupby('bottom_parent_top_id').groups - > count = [len(grouper[eid]) if (eid in grouper) else 0 + ``` + grouper = dataset.groupby('bottom_parent_top_id').groups + count = [len(grouper[eid]) if (eid in grouper) else 0 for eid in d['top_id'].data] - > assert_equal(storm_child_trig_count, count) - - Returns a list of counts of the children of entity_id_var and - (optionally) its children. + assert_equal(storm_child_trig_count, count) + ``` + + Parameters + ---------- + entity_id_var : str + The name of the variable to count children of. + child_entity_id_var : str, optional + The name of the lowest variable in the hierarchy to count children of. + If None, only the immediate children are counted. + + Returns + ------- + all_counts : tuple of np.ndarray + A tuple of arrays, where each array is the count of children at the + corresponding level in the hierarchy. """ count_next = False all_counts = [] @@ -142,30 +149,38 @@ def count_children(self, entity_id_var, child_entity_id_var=None): return all_counts def replicate_parent_ids(self, entity_id_var, parent_id_var): - """ Ascend from the level of parent_id_var to entity_id_var, - and replicate the IDs in entity_id_var, one each for the - number of rows in the parent_id_var dimension. - - entity_id_var is one of the variables originally - given by entity_id_vars upon class initialization. - - parent_id_var is one of the variables originally - given by parent_id_vars upon class initialization. - - This function is not strictly needed for queries up one level - (where parent_id_var can be used directly), but is needed to ascend - the hierarchy by two or more levels. - - Once the parent entity_ids have been replicated, they can be used - with xarray's indexing functions to replicate any other variable - along that parent entity's dimension. - - returns replicated_parent_ids - - TODO: args should really be: - replicate_to_dim - replicate_var -> replicate_from_dim + """Replicate the IDs of the ancestors at the level of a child entity. + + If given a mapping of child->parent, this function can find the grandparents, great-grandparents, etc. + + Parameters + ---------- + entity_id_var : str + The name of the ancestor entity to find. Must be a variable originally specificied to `entity_id_vars` the class initialization. + parent_id_var : str + The name of initial the child->parent relationship to start ascending through the heirarchy with. + Must be a variable originally specified to the `parent_id_vars` and must be lower than the value specified to `entity_id_var` + + Returns + ------- + last_replicated_p_ids : np.ndarray + The replicated IDs of the ancestors (at the level of `entity_id_var`) replicated to the level of the child in the child->parent + relationship specified to `parent_id_var` + + Notes + ----- + This function is not strictly needed for queries up one level + (where parent_id_var can be used directly), but is needed to ascend + the hierarchy by two or more levels. + + Once the parent entity_ids have been replicated, they can be used + with xarray's indexing functions to replicate any other variable + along that parent entity's dimension. """ + # TODO: args should really be: + # replicate_to_dim + # replicate_var -> replicate_from_dim + # Work from bottom up. # First, wait until the parent_id_var is reached # then use the groupby corresponding to the next level up @@ -201,8 +216,23 @@ def replicate_parent_ids(self, entity_id_var, parent_id_var): # last_e_var, last_p_var = e_var, p_var def reduce_to_entities(self, entity_id_var, entity_ids): - """ Reduce the dataset to the children and parents of entity_id_var - given entity_ids that are within entity_id_var + """Reduce the dataset to the ancestors and descendents of the given entity_ids that are within the given entity_id_var. + + Finds all ancestors and descendents of the specified IDs on the specified variable, and returns a filtered dataset containing only the requested + entity IDs and their ancestors and children. + + Parameters + ---------- + entity_id_var : str + The variable to find the ancestors and descendants of. + + entity_ids : array_like + The IDs to filter the dataset by. + + Returns + ------- + dataset : xarray.Dataset + original dataset filtered to the requested entity_ids along the entity_id_var and all ancestors and descendants. """ entity_ids = np.asarray(entity_ids) From 49c6bec24f28720fb76943188b827183ca1a1cfb Mon Sep 17 00:00:00 2001 From: Sam Gardner Date: Tue, 21 Jan 2025 23:21:19 -0600 Subject: [PATCH 11/27] update svg logo --- README.md | 14 ++-- docs/xlma_logo_big_big_ltg.svg | 130 ++++++++++++++++----------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 265d98a..97e9626 100644 --- a/README.md +++ b/README.md @@ -96,11 +96,11 @@ There is no obvious choice here, either. ## Prior art - [`lmatools`](https://github.com/deeplycloudy/lmatools/) - - Includes readers for LMA and NLDN data (using older methods from 2010) - - Flash sorting and gridding - - Has code for all necessary coordinate transforms. + - Includes readers for LMA and NLDN data (using older methods from 2010) + - Flash sorting and gridding + - Has code for all necessary coordinate transforms. - [`brawl4d`](https://github.com/deeplycloudy/brawl4d/) A working version of the basic GUI functionality of xlma. - - Based on matplotlib; plots can be drag-repositioned. Slow for large numbers of data points. - - Includes charge analyis that auto-saves to disk - - At one point, could display radar data underneath LMA data - - Built around a data pipeline, with a pool of data at the start, subsetting, projection, and finally display. + - Based on matplotlib; plots can be drag-repositioned. Slow for large numbers of data points. + - Includes charge analyis that auto-saves to disk + - At one point, could display radar data underneath LMA data + - Built around a data pipeline, with a pool of data at the start, subsetting, projection, and finally display. diff --git a/docs/xlma_logo_big_big_ltg.svg b/docs/xlma_logo_big_big_ltg.svg index f15d2f2..9ab0705 100644 --- a/docs/xlma_logo_big_big_ltg.svg +++ b/docs/xlma_logo_big_big_ltg.svg @@ -2,12 +2,12 @@ xlma