Skip to content

Commit ed609f1

Browse files
authored
Merge pull request #34 from wx4stg/nldn
Add NLDN/ENTLN plotting
2 parents e4fcc0f + 7b3ccc3 commit ed609f1

10 files changed

Lines changed: 289 additions & 6 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
12/24/23 00:57:01.123456789 33.582 -101.881 +12.0 kA 0 0.4 0.2 1.8 75 0.8 4 C
2+
12/24/23 00:57:31.987654321 33.590 -102.032 -9.0 kA 0 0.2 0.2 1.0 3 1.0 5 G
3+
12/24/23 00:57:58.135792468 33.585 -101.872 -15.0 kA 0 2.2 1.5 1.5 26 2.1 6 C
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type,timestamp,latitude,longitude,peakcurrent,icheight,numbersensors,majoraxis,minoraxis,bearing
2+
1, 2023-12-24T00:57:04.123456789, 33.581914, -101.880986, 12345, 3014, 8, 371.00, 58.00, 104.00
3+
0, 2023-12-24T00:57:26.987654321, 33.590077, -102.032033, 9876, 6028, 7, 693.00, 108.00, 99.00
4+
1, 2023-12-24T00:57:47.246813579, 33.584480, -101.871498, 15790, 13591, 11, 178.00, 62.00, 102.00

pyxlma/lmalib/io/read.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,116 @@ def to_dataset(lma_file, event_id_start=0):
220220
return ds
221221

222222

223+
def nldn(filenames):
224+
"""
225+
Read Viasala NLDN data
226+
227+
Reads in one or multiple NLDN files and and returns a pandas dataframe with appropriate column names
228+
229+
230+
Parameters
231+
----------
232+
filenames : str or list of str
233+
The file or files to read in
234+
235+
236+
Returns
237+
-------
238+
full_df : `pandas.DataFrame`
239+
A pandas dataframe of entln data, with columns:
240+
'latitude' - the latitude of the event
241+
'longitude' - the longitude of the event
242+
'peak_current_kA' - the peak current in kA
243+
'multiplicity' - the number of strokes in the event
244+
'semimajor' - the semimajor axis length in km of the 50% confidence ellipse
245+
'semiminor' - the semiminor axis length in km of the 50% confidence ellipse
246+
'ellipseangle' - the angle of the 50% confidence ellipse
247+
'chi2' - the reduced chi-squared value of the event
248+
'num_stations' - the number of stations contributing to the event
249+
'type' - 'IC' or 'CG' for intracloud or cloud-to-ground
250+
'datetime' - the time of the event
251+
252+
253+
Notes
254+
-----
255+
This function is designed to read NLDN files in the format that matches the format of the file located
256+
in examples/network_samples/gld360enldnns_20231224_daily_v1_lit.raw (This is not real NLDN data, but provides a correct sample of the format)
257+
258+
Other file formats may exist and may not be read in correctly. If you have a file that is not read in correctly,
259+
please open an issue on the pyxlma GitHub page.
260+
"""
261+
if type(filenames) is str:
262+
filenames = [filenames]
263+
full_df = pd.DataFrame({})
264+
for filename in filenames:
265+
this_file = pd.read_csv(filename, delim_whitespace=True, header=None,
266+
names=[
267+
'date', 'time', 'latitude', 'longitude', 'peak_current_kA', 'curr_unit', 'multiplicity', 'semimajor',
268+
'semiminor', 'majorminorratio', 'ellipseangle', 'chi2', 'num_stations', 'type'
269+
])
270+
if len(this_file['curr_unit'].drop_duplicates()) == 1:
271+
this_file.drop(columns=['curr_unit'], inplace=True)
272+
else:
273+
raise ValueError('Multiple current units in file')
274+
this_file['datetime'] = pd.to_datetime(this_file['date']+' '+this_file['time'], format='%m/%d/%y %H:%M:%S.%f')
275+
this_file.drop(columns=['date','time'], inplace=True)
276+
this_file['type'] = this_file['type'].map({'G':'CG','C':'IC'})
277+
full_df = pd.concat([full_df, this_file])
278+
return full_df
279+
280+
281+
def entln(filenames):
282+
"""
283+
Read Earth Networks Total Lightning Network data
284+
285+
Reads in one or multiple ENTLN files and and returns a pandas dataframe with appropriate column names
286+
287+
288+
Parameters
289+
----------
290+
filenames : str or list of str
291+
The file or files to read in
292+
293+
294+
Returns
295+
-------
296+
full_df : `pandas.DataFrame`
297+
A pandas dataframe of entln data, with columns:
298+
'type' - 'IC' or 'CG' for intracloud or cloud-to-ground
299+
'datetime' - the time of the event
300+
'latitude' - the latitude of the event
301+
'longitude' - the longitude of the event
302+
'peak_current_kA' - the peak current in kA
303+
'icheight' - the height of the IC event in meters
304+
'num_stations' - the number of stations contributing to the event
305+
'ellipseangle' - the angle of the 50% confidence ellipse
306+
'semimajor' - the semimajor axis length in km of the 50% confidence ellipse
307+
'semiminor' - the semiminor axis length in km of the 50% confidence ellipse
308+
309+
Notes
310+
-----
311+
This function is designed to read ENTLN files in the format that matches the format of the file located
312+
in examples/network_samples/lxarchive_pulse20231224.csv (This is not real ENTLN data, but provides a correct sample of the format)
313+
314+
Other file formats may exist and may not be read in correctly. If you have a file that is not read in correctly,
315+
please open an issue on the pyxlma GitHub page.
316+
"""
317+
if type(filenames) is str:
318+
filenames = [filenames]
319+
full_df = pd.DataFrame({})
320+
for filename in filenames:
321+
this_file = pd.read_csv(filename, parse_dates=['timestamp'])
322+
this_file['peakcurrent'] = this_file['peakcurrent']/1000
323+
this_file['type'] = this_file['type'].map({0:'CG',1:'IC'})
324+
this_file['semimajor'] = this_file['majoraxis']/2000
325+
this_file['semiminor'] = this_file['minoraxis']/2000
326+
this_file.drop(columns=['majoraxis','minoraxis'], inplace=True)
327+
rename = {'timestamp' : 'datetime', 'peakcurrent' : 'peak_current_kA', 'numbersensors' : 'num_stations', 'bearing' : 'ellipseangle'}
328+
this_file.rename(columns=rename, inplace=True)
329+
full_df = pd.concat([full_df, this_file])
330+
return full_df
331+
332+
223333
class lmafile(object):
224334
def __init__(self,filename):
225335
"""

pyxlma/plot/xlma_plot_feature.py

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def setup_hist(lon_data, lat_data, alt_data, time_data,
6363

6464
def plot_points(bk_plot, lon_data, lat_data, alt_data, time_data,
6565
plot_cmap=None, plot_s=None, plot_vmin=None, plot_vmax=None, plot_c=None, edge_color='face',
66-
edge_width=0, add_to_histogram=True, **kwargs):
66+
edge_width=0, add_to_histogram=True, marker='o', **kwargs):
6767
"""
6868
Plot scatter points on an existing bk_plot object given x,y,z,t for each
6969
and defined plotting colormaps and ranges
@@ -88,16 +88,16 @@ def plot_points(bk_plot, lon_data, lat_data, alt_data, time_data,
8888

8989
art_plan = bk_plot.ax_plan.scatter(lon_data, lat_data,
9090
c=plot_c,vmin=plot_vmin, vmax=plot_vmax, cmap=plot_cmap,
91-
s=plot_s,marker='o', linewidths=edge_width, edgecolors=edge_color, **kwargs)
91+
s=plot_s, marker=marker, linewidths=edge_width, edgecolors=edge_color, **kwargs)
9292
art_th = bk_plot.ax_th.scatter(time_data, alt_data,
9393
c=plot_c,vmin=plot_vmin, vmax=plot_vmax, cmap=plot_cmap,
94-
s=plot_s,marker='o', linewidths=edge_width, edgecolors=edge_color, **kwargs)
94+
s=plot_s, marker=marker, linewidths=edge_width, edgecolors=edge_color, **kwargs)
9595
art_lon = bk_plot.ax_lon.scatter(lon_data, alt_data,
9696
c=plot_c,vmin=plot_vmin, vmax=plot_vmax, cmap=plot_cmap,
97-
s=plot_s,marker='o', linewidths=edge_width, edgecolors=edge_color, **kwargs)
97+
s=plot_s, marker=marker, linewidths=edge_width, edgecolors=edge_color, **kwargs)
9898
art_lat = bk_plot.ax_lat.scatter(alt_data, lat_data,
9999
c=plot_c,vmin=plot_vmin, vmax=plot_vmax, cmap=plot_cmap,
100-
s=plot_s,marker='o', linewidths=edge_width, edgecolors=edge_color, **kwargs)
100+
s=plot_s, marker=marker, linewidths=edge_width, edgecolors=edge_color, **kwargs)
101101
art_out = [art_plan, art_th, art_lon, art_lat]
102102

103103
if add_to_histogram:
@@ -111,6 +111,84 @@ def plot_points(bk_plot, lon_data, lat_data, alt_data, time_data,
111111
art_out.append(art_hist)
112112
return art_out
113113

114+
115+
def plot_2d_network_points(bk_plot, netw_data, actual_height=None, fake_ic_height=18, fake_cg_height=1,
116+
color_by='time', pos_color='red', neg_color='blue', **kwargs):
117+
"""
118+
Plot points from a 2D lightning mapping neworks (ie, NLDN, ENTLN, etc)
119+
120+
Parameters
121+
----------
122+
bk_plot : `pyxlma.plot.xlma_base_plot.BlankPlot`
123+
A BlankPlot object to plot the data on
124+
netw_data : `pandas.DataFrame` or `xarray.Dataset`
125+
data object with columns/variables 'longitude', 'latitude', 'type' (CG/IC), and 'datetime'
126+
actual_height : `numpy.ndarray` or `pandas.Series` or `xarray.DataArray`
127+
the hieghts of the events to be plotted (default None, fake_ic_height and fake_cg_height used)
128+
fake_ic_height : float
129+
the altitude to plot IC points (default 18 km)
130+
fake_cg_height : float
131+
the altitude to plot CG points (default 1 km)
132+
color_by : ['time', 'polarity']
133+
Whether to color the points by time or polarity. Default 'time'. Ignored if **kwargs contains 'c'.
134+
pos_color : str
135+
color for positive points (default 'red') if color_by='polarity'
136+
neg_color : str
137+
color for negative points (default 'blue') if color_by='polarity'
138+
**kwargs
139+
additional keyword arguments to pass to plt.scatter
140+
141+
Returns
142+
-------
143+
art_out : list
144+
nested lists of artists created by plot_points (first list CG, second list IC)
145+
146+
"""
147+
148+
plot_c = kwargs.pop('c', None)
149+
vmin = kwargs.pop('vmin', None)
150+
vmax = kwargs.pop('vmax', None)
151+
marker = kwargs.pop('marker', '^')
152+
if actual_height is not None:
153+
netw_data['height'] = actual_height
154+
155+
if plot_c is not None:
156+
netw_data['plot_c'] = plot_c
157+
elif color_by == 'time':
158+
netw_data['plot_c'] = color_by_time(netw_data.datetime, bk_plot.tlim)[2]
159+
elif color_by == 'polarity':
160+
pass
161+
else:
162+
raise ValueError("color_by must be 'time' or 'polarity'")
163+
164+
cgs = netw_data[netw_data['type']=='CG']
165+
ics = netw_data[netw_data['type']=='IC']
166+
167+
if actual_height is None:
168+
cgs['height'] = np.full_like(cgs.longitude, fake_cg_height)
169+
ics['height'] = np.full_like(ics.longitude, fake_ic_height)
170+
art_out = []
171+
if color_by == 'polarity':
172+
cgpos = cgs[cgs.peak_current_kA>0]
173+
cgneg = cgs[cgs.peak_current_kA<0]
174+
icpos = ics[ics.peak_current_kA>0]
175+
icneg = ics[ics.peak_current_kA<0]
176+
art_out.append(plot_points(bk_plot, cgneg.longitude, cgneg.latitude, cgneg.height,
177+
cgneg.datetime, c=neg_color, marker=marker, add_to_histogram=False, **kwargs))
178+
art_out.append(plot_points(bk_plot, cgpos.longitude, cgpos.latitude, cgpos.height,
179+
cgpos.datetime, c=pos_color, marker=marker, add_to_histogram=False, **kwargs))
180+
art_out.append(plot_points(bk_plot, icneg.longitude, icneg.latitude, icneg.height,
181+
icneg.datetime, c=neg_color, marker=marker, add_to_histogram=False, **kwargs))
182+
art_out.append(plot_points(bk_plot, icpos.longitude, icpos.latitude, icpos.height,
183+
icpos.datetime, c=pos_color, marker=marker, add_to_histogram=False, **kwargs))
184+
else:
185+
art_out.append(plot_points(bk_plot, cgs.longitude, cgs.latitude, cgs.height,
186+
cgs.datetime, c=cgs.plot_c, vmin=vmin, vmax=vmax, marker=marker, add_to_histogram=False, **kwargs))
187+
art_out.append(plot_points(bk_plot, ics.longitude, ics.latitude, ics.height,
188+
ics.datetime, c=ics.plot_c, vmin=vmin, vmax=vmax, marker=marker, add_to_histogram=False, **kwargs))
189+
return art_out
190+
191+
114192
def plot_3d_grid(bk_plot, xedges, yedges, zedges, tedges,
115193
alt_lon, alt_lat, alt_time, lat_lon,
116194
alt_data, plot_cmap=None, **kwargs):

tests/test_base_plot.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pyxlma.plot.xlma_base_plot import *
44
from pyxlma.plot.xlma_plot_feature import *
55
from pyxlma.lmalib.grid import *
6+
from pyxlma.lmalib.io import read as lma_read
67
import datetime as dt
78
import pandas as pd
89
import matplotlib.dates as md
@@ -135,4 +136,53 @@ def test_plot_feature_inset_view():
135136
c=colors, edgecolors='k', linewidths=0.1, add_to_histogram=True)
136137
inset_view(bk_plot, dataset.event_longitude.data, dataset.event_latitude.data,
137138
[-102.75, -102.25], [32, 32.5], .01, .01)
138-
return bk_plot.fig
139+
return bk_plot.fig
140+
141+
142+
@pytest.mark.mpl_image_compare
143+
def test_plot_feature_ndln_time():
144+
start_time = dt.datetime(2023, 12, 24, 0, 57, 0, 0)
145+
end_time = start_time + dt.timedelta(seconds=60)
146+
bk_plot = BlankPlot(start_time, bkgmap=True, xlim=[-103.5, -99.5], ylim=[31.5, 35.5], zlim=[0, 20], tlim=[start_time, end_time], title='XLMA Test Plot')
147+
nldn_data = lma_read.nldn('examples/network_samples/gld360enldnns_20231224_daily_v1_lit.raw')
148+
plot_2d_network_points(bk_plot, nldn_data)
149+
return bk_plot.fig
150+
151+
152+
def test_plot_feature_ndln_bad_arg():
153+
start_time = dt.datetime(2023, 12, 24, 0, 57, 0, 0)
154+
end_time = start_time + dt.timedelta(seconds=60)
155+
bk_plot = BlankPlot(start_time, bkgmap=True, xlim=[-103.5, -99.5], ylim=[31.5, 35.5], zlim=[0, 20], tlim=[start_time, end_time], title='XLMA Test Plot')
156+
nldn_data = lma_read.nldn('examples/network_samples/gld360enldnns_20231224_daily_v1_lit.raw')
157+
with pytest.raises(ValueError, match="color_by must be 'time' or 'polarity'"):
158+
plot_2d_network_points(bk_plot, nldn_data, color_by='bad_arg')
159+
160+
161+
@pytest.mark.mpl_image_compare
162+
def test_plot_feature_ndln_custom_colors():
163+
start_time = dt.datetime(2023, 12, 24, 0, 57, 0, 0)
164+
end_time = start_time + dt.timedelta(seconds=60)
165+
bk_plot = BlankPlot(start_time, bkgmap=True, xlim=[-103.5, -99.5], ylim=[31.5, 35.5], zlim=[0, 20], tlim=[start_time, end_time], title='XLMA Test Plot')
166+
nldn_data = lma_read.nldn('examples/network_samples/gld360enldnns_20231224_daily_v1_lit.raw')
167+
plot_2d_network_points(bk_plot, nldn_data, c=[0, 10, 5], cmap='plasma', vmin=0, vmax=10)
168+
return bk_plot.fig
169+
170+
171+
@pytest.mark.mpl_image_compare
172+
def test_plot_feature_entln_real_height():
173+
start_time = dt.datetime(2023, 12, 24, 0, 57, 0, 0)
174+
end_time = start_time + dt.timedelta(seconds=60)
175+
bk_plot = BlankPlot(start_time, bkgmap=True, xlim=[-103.5, -99.5], ylim=[31.5, 35.5], zlim=[0, 20], tlim=[start_time, end_time], title='XLMA Test Plot')
176+
entln_data = lma_read.entln('examples/network_samples/lxarchive_pulse20231224.csv')
177+
plot_2d_network_points(bk_plot, entln_data, actual_height=entln_data['icheight'])
178+
return bk_plot.fig
179+
180+
181+
@pytest.mark.mpl_image_compare
182+
def test_plot_feature_ndln_polarity():
183+
start_time = dt.datetime(2023, 12, 24, 0, 57, 0, 0)
184+
end_time = start_time + dt.timedelta(seconds=60)
185+
bk_plot = BlankPlot(start_time, bkgmap=True, xlim=[-103.5, -99.5], ylim=[31.5, 35.5], zlim=[0, 20], tlim=[start_time, end_time], title='XLMA Test Plot')
186+
nldn_data = lma_read.nldn('examples/network_samples/gld360enldnns_20231224_daily_v1_lit.raw')
187+
plot_2d_network_points(bk_plot, nldn_data, color_by='polarity')
188+
return bk_plot.fig

tests/test_io.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from os import listdir
44
from datetime import datetime as dt
55
import xarray as xr
6+
import pandas as pd
7+
import numpy as np
68
from pyxlma.lmalib.io import read as lma_read
79
from tests.test_flash import compare_dataarrays
810

@@ -22,3 +24,39 @@ def test_read_onefile_dataset():
2224
truth = xr.open_dataset('tests/truth/lma_netcdf/small_lma.nc')
2325
for var in truth.data_vars:
2426
compare_dataarrays(dataset, truth, var)
27+
28+
29+
def test_read_nldn():
30+
dataset = lma_read.nldn('examples/network_samples/gld360enldnns_20231224_daily_v1_lit.raw')
31+
truth = pd.DataFrame({
32+
'latitude' : [33.582, 33.590, 33.585],
33+
'longitude' : [-101.881, -102.032, -101.872],
34+
'peak_current_kA' : [12.0, -9.0, -15.0],
35+
'multiplicity' : [0, 0, 0],
36+
'semimajor': [0.4, 0.2, 2.2],
37+
'semiminor': [0.2, 0.2, 1.5],
38+
'majorminorratio': [1.8, 1.0, 1.5],
39+
'ellipseangle' : [75, 3, 26],
40+
'chi2' : [0.8, 1.0, 2.1],
41+
'num_stations': [4, 5, 6],
42+
'type': ['IC', 'CG', 'IC'],
43+
'datetime' : [np.datetime64('2023-12-24T00:57:01.123456789'), np.datetime64('2023-12-24T00:57:31.987654321'), np.datetime64('2023-12-24T00:57:58.135792468')]
44+
})
45+
assert dataset.equals(truth)
46+
47+
48+
def test_read_entln():
49+
dataset = lma_read.entln('examples/network_samples/lxarchive_pulse20231224.csv')
50+
truth = pd.DataFrame({
51+
'type': ['IC', 'CG', 'IC'],
52+
'datetime' : [np.datetime64('2023-12-24T00:57:04.123456789'), np.datetime64('2023-12-24T00:57:26.987654321'), np.datetime64('2023-12-24T00:57:47.246813579')],
53+
'latitude' : [33.581914, 33.590077, 33.584480],
54+
'longitude' : [-101.880986, -102.032033, -101.871498],
55+
'peak_current_kA' : [12.345, 9.876, 15.79],
56+
'icheight' : [3014, 6028, 13591],
57+
'num_stations': [8, 7, 11],
58+
'ellipseangle' : [104., 99., 102.],
59+
'semimajor': [0.1855, 0.3465, 0.089],
60+
'semiminor': [0.029, 0.054, 0.031]
61+
})
62+
assert dataset.equals(truth)
75.6 KB
Loading
76.4 KB
Loading
76.2 KB
Loading
76.4 KB
Loading

0 commit comments

Comments
 (0)