pyIntensityFeatures 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyIntensityFeatures/__init__.py +30 -0
- pyIntensityFeatures/_main.py +500 -0
- pyIntensityFeatures/instruments/__init__.py +9 -0
- pyIntensityFeatures/instruments/satellites.py +137 -0
- pyIntensityFeatures/proc/__init__.py +10 -0
- pyIntensityFeatures/proc/boundaries.py +420 -0
- pyIntensityFeatures/proc/fitting.py +374 -0
- pyIntensityFeatures/proc/intensity.py +251 -0
- pyIntensityFeatures/tests/__init__.py +1 -0
- pyIntensityFeatures/tests/test_instruments_satellites.py +210 -0
- pyIntensityFeatures/tests/test_main.py +734 -0
- pyIntensityFeatures/tests/test_proc_boundaries.py +613 -0
- pyIntensityFeatures/tests/test_proc_fitting.py +218 -0
- pyIntensityFeatures/tests/test_proc_intensity.py +205 -0
- pyIntensityFeatures/tests/test_utils_checks.py +933 -0
- pyIntensityFeatures/tests/test_utils_coords.py +197 -0
- pyIntensityFeatures/tests/test_utils_distributions.py +236 -0
- pyIntensityFeatures/tests/test_utils_grids.py +189 -0
- pyIntensityFeatures/tests/test_utils_output.py +433 -0
- pyIntensityFeatures/utils/__init__.py +13 -0
- pyIntensityFeatures/utils/checks.py +420 -0
- pyIntensityFeatures/utils/coords.py +157 -0
- pyIntensityFeatures/utils/distributions.py +199 -0
- pyIntensityFeatures/utils/grids.py +113 -0
- pyIntensityFeatures/utils/output.py +276 -0
- pyintensityfeatures-0.1.0.dist-info/METADATA +360 -0
- pyintensityfeatures-0.1.0.dist-info/RECORD +30 -0
- pyintensityfeatures-0.1.0.dist-info/WHEEL +5 -0
- pyintensityfeatures-0.1.0.dist-info/licenses/LICENSE +28 -0
- pyintensityfeatures-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Full license can be found in License.md
|
|
4
|
+
#
|
|
5
|
+
# DISTRIBUTION STATEMENT A: Approved for public release. Distribution is
|
|
6
|
+
# unlimited.
|
|
7
|
+
# ----------------------------------------------------------------------------
|
|
8
|
+
"""Package to identify intensity features in imager data."""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from importlib import metadata
|
|
14
|
+
except ImportError:
|
|
15
|
+
import importlib_metadata as metadata
|
|
16
|
+
|
|
17
|
+
# Define a logger object to allow easier log handling
|
|
18
|
+
logging.raiseExceptions = False
|
|
19
|
+
logger = logging.getLogger('pyIntensityFeatures_logger')
|
|
20
|
+
|
|
21
|
+
# Import the package modules and top-level classes
|
|
22
|
+
from pyIntensityFeatures import _main # noqa F401
|
|
23
|
+
from pyIntensityFeatures import instruments # noqa F401
|
|
24
|
+
from pyIntensityFeatures import proc # noqa F401
|
|
25
|
+
from pyIntensityFeatures import utils # noqa F401
|
|
26
|
+
|
|
27
|
+
from pyIntensityFeatures._main import AuroralBounds # noqa F401
|
|
28
|
+
|
|
29
|
+
# Define the global variables
|
|
30
|
+
__version__ = metadata.version('pyIntensityFeatures')
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# DISTRIBUTION STATEMENT A: Approved for public release. Distribution is
|
|
5
|
+
# unlimited.
|
|
6
|
+
# -----------------------------------------------------------------------------
|
|
7
|
+
"""Main classes and functions for the package."""
|
|
8
|
+
|
|
9
|
+
import datetime as dt
|
|
10
|
+
from functools import partial
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pds
|
|
13
|
+
import xarray as xr
|
|
14
|
+
|
|
15
|
+
from pyIntensityFeatures import logger
|
|
16
|
+
from pyIntensityFeatures import proc
|
|
17
|
+
from pyIntensityFeatures.instruments import satellites
|
|
18
|
+
from pyIntensityFeatures import utils
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuroralBounds(object):
|
|
22
|
+
"""Manage intensity data and obtain auroral luminosity boundaries.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
inst_data : object
|
|
27
|
+
Instrument data of any type (e.g., pysat.Instrument, xr.Dataset,
|
|
28
|
+
pds.DataFrame, dict of arrays) with appropriate observations.
|
|
29
|
+
time_var : str or int
|
|
30
|
+
Time key, attribute, variable name, or index
|
|
31
|
+
glon_var : str
|
|
32
|
+
Geographic longitude key, attribute, variable name, or index
|
|
33
|
+
glat_var : str
|
|
34
|
+
Geographic latitude key, attribute, variable name, or index
|
|
35
|
+
intensity_var : str
|
|
36
|
+
Observed intensity key, attribute, variable name, or index
|
|
37
|
+
alt : float or int
|
|
38
|
+
Altitude in km at which the aurora is observed
|
|
39
|
+
transpose : bool or dict
|
|
40
|
+
Flag that indicates whether or not the 2D data accessed by `glon_var`,
|
|
41
|
+
`glat_var`, and `intensity_var` needs to be transposed. If a dict is
|
|
42
|
+
supplied, the keys should correspond the the different variable names
|
|
43
|
+
and allows different flags for each set of data. (default=False)
|
|
44
|
+
opt_coords : dict or NoneType
|
|
45
|
+
Dict of coordinates to include in `boundaries` or None to only have
|
|
46
|
+
the required coordinates (default=None)
|
|
47
|
+
hemipshere : int
|
|
48
|
+
Integer denoting hemisphere, 1 for North and -1 for South (default=1)
|
|
49
|
+
stime : object or NoneType
|
|
50
|
+
Time object or None to use earliest time from `inst_data`
|
|
51
|
+
(default=None)
|
|
52
|
+
etime : object or NoneType
|
|
53
|
+
Time object or None to use latest time from `inst_data` (default=None)
|
|
54
|
+
slice_func : function
|
|
55
|
+
Appropriate function to retrieve an auroral image slice
|
|
56
|
+
(default=pyIntensityFeatures.instruments.satellites.get_auroral_slice)
|
|
57
|
+
slice_kwargs : dict or NoneType
|
|
58
|
+
Kwargs to be used by `slice_func` (default=None)
|
|
59
|
+
clean_func : function or NoneType
|
|
60
|
+
Appropriate function to clean the intensity data (default=None)
|
|
61
|
+
clean_kwargs : dict or NoneType
|
|
62
|
+
Kwargs to be used by `clean_func` (default=None)
|
|
63
|
+
|
|
64
|
+
Attributes
|
|
65
|
+
----------
|
|
66
|
+
inst_data
|
|
67
|
+
time_var
|
|
68
|
+
glon_var
|
|
69
|
+
glat_var
|
|
70
|
+
intensity_var
|
|
71
|
+
alt
|
|
72
|
+
opt_coords
|
|
73
|
+
hemipshere
|
|
74
|
+
stime
|
|
75
|
+
etime
|
|
76
|
+
slice_func
|
|
77
|
+
clean_func
|
|
78
|
+
boundaries : xr.Dataset or NoneType
|
|
79
|
+
Dataset with Auroral Luminosity Boundaries or NoneType if not run
|
|
80
|
+
|
|
81
|
+
Methods
|
|
82
|
+
-------
|
|
83
|
+
set_boundaries
|
|
84
|
+
Set the `boundaries` attribute using the assigned data.
|
|
85
|
+
update_times
|
|
86
|
+
Update `stime` and `etime` attributes to reflect current data in
|
|
87
|
+
`inst_data`, if available.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, inst_data, time_var, glon_var, glat_var, intensity_var,
|
|
92
|
+
alt, transpose=False, opt_coords=None, hemisphere=1,
|
|
93
|
+
stime=None, etime=None,
|
|
94
|
+
slice_func=satellites.get_auroral_slice, slice_kwargs=None,
|
|
95
|
+
clean_func=None, clean_kwargs=None):
|
|
96
|
+
"""Set up the class attributes."""
|
|
97
|
+
|
|
98
|
+
# Set the data object and altitude
|
|
99
|
+
self.inst_data = inst_data
|
|
100
|
+
self.alt = alt
|
|
101
|
+
|
|
102
|
+
# Set the data access variables
|
|
103
|
+
self.time_var = time_var
|
|
104
|
+
self.glon_var = glon_var
|
|
105
|
+
self.glat_var = glat_var
|
|
106
|
+
self.intensity_var = intensity_var
|
|
107
|
+
|
|
108
|
+
# Set the transpose flag for the intensity/location data
|
|
109
|
+
if hasattr(transpose, 'keys'):
|
|
110
|
+
self.transpose = transpose
|
|
111
|
+
trans_default = False # Set the default for unspecified variables
|
|
112
|
+
else:
|
|
113
|
+
# All variables will be set to the same flag value
|
|
114
|
+
self.transpose = {}
|
|
115
|
+
trans_default = transpose
|
|
116
|
+
|
|
117
|
+
for tkey in [self.glon_var, self.glat_var, self.intensity_var]:
|
|
118
|
+
if tkey not in self.transpose.keys():
|
|
119
|
+
# Update the unset variables
|
|
120
|
+
self.transpose[tkey] = trans_default
|
|
121
|
+
|
|
122
|
+
# Set the hemisphere and coordinates
|
|
123
|
+
self.hemisphere = hemisphere
|
|
124
|
+
|
|
125
|
+
if opt_coords is None:
|
|
126
|
+
self.opt_coords = {'hemisphere': self.hemisphere}
|
|
127
|
+
else:
|
|
128
|
+
self.opt_coords = opt_coords
|
|
129
|
+
self.opt_coords['hemisphere'] = self.hemisphere
|
|
130
|
+
|
|
131
|
+
# Set the starting and ending times
|
|
132
|
+
self.stime = stime
|
|
133
|
+
self.etime = etime
|
|
134
|
+
|
|
135
|
+
# Set the instrument-specific functions
|
|
136
|
+
if slice_kwargs is None:
|
|
137
|
+
slice_kwargs = {}
|
|
138
|
+
|
|
139
|
+
if slice_func is None:
|
|
140
|
+
logger.info('slice function not provided, data must be pre-sliced.')
|
|
141
|
+
self.slice_func = None
|
|
142
|
+
else:
|
|
143
|
+
self.slice_func = partial(slice_func, **slice_kwargs)
|
|
144
|
+
|
|
145
|
+
if clean_kwargs is None:
|
|
146
|
+
clean_kwargs = {}
|
|
147
|
+
|
|
148
|
+
if clean_func is None:
|
|
149
|
+
self.clean_func = None
|
|
150
|
+
else:
|
|
151
|
+
self.clean_func = partial(clean_func, **clean_kwargs)
|
|
152
|
+
|
|
153
|
+
# Initalize the boundary object
|
|
154
|
+
self.boundaries = None
|
|
155
|
+
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
def __eq__(self, other):
|
|
159
|
+
"""Perform equality check.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
other : any
|
|
164
|
+
Other object to compare for equality
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
bool
|
|
169
|
+
True if objects are identical, False if they are not.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
# Check if other is the same class
|
|
173
|
+
if not isinstance(other, self.__class__):
|
|
174
|
+
logger.info("wrong class")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Check to see if other has the same and equal attributes
|
|
178
|
+
for attr in self.__dict__.keys():
|
|
179
|
+
attr_flag = False
|
|
180
|
+
if hasattr(other, attr):
|
|
181
|
+
self_val = getattr(self, attr)
|
|
182
|
+
other_val = getattr(other, attr)
|
|
183
|
+
|
|
184
|
+
if attr.find('inst_data') == 0:
|
|
185
|
+
# This is the Instrument data, different comparisons are
|
|
186
|
+
# needed for known supported data types
|
|
187
|
+
if isinstance(self_val, xr.Dataset) or isinstance(
|
|
188
|
+
self_val, pds.DataFrame):
|
|
189
|
+
attr_flag = self_val.equals(other_val)
|
|
190
|
+
else:
|
|
191
|
+
attr_flag = np.all(self_val == other_val)
|
|
192
|
+
elif attr.find('func') < 0:
|
|
193
|
+
# This is an integer, boolean or string
|
|
194
|
+
attr_flag = self_val == other_val
|
|
195
|
+
else:
|
|
196
|
+
# This is a partial function, ensure it has all the
|
|
197
|
+
# necessary attributes
|
|
198
|
+
attr_flag = str(self_val) == str(other_val)
|
|
199
|
+
else:
|
|
200
|
+
logger.info("object is missing attribute: {:}".format(attr))
|
|
201
|
+
return attr_flag
|
|
202
|
+
|
|
203
|
+
if not attr_flag:
|
|
204
|
+
# Exit upon first inequality
|
|
205
|
+
logger.info("attribute {:} differs".format(attr))
|
|
206
|
+
return attr_flag
|
|
207
|
+
|
|
208
|
+
# Confirm that other object doesn't have extra terms
|
|
209
|
+
for attr in other.__dict__.keys():
|
|
210
|
+
if attr not in self.__dict__.keys():
|
|
211
|
+
logger.info("object contains extra attribute: {:}".format(attr))
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
def __repr__(self):
|
|
217
|
+
"""Print basic class properties."""
|
|
218
|
+
out_str = "".join(["pyIntensityFeatures.AuroralBounds(",
|
|
219
|
+
", ".join([repr(self.inst_data),
|
|
220
|
+
repr(self.time_var),
|
|
221
|
+
repr(self.glon_var), repr(self.glat_var),
|
|
222
|
+
repr(self.intensity_var),
|
|
223
|
+
repr(self.alt)]),
|
|
224
|
+
", transpose=", repr(self.transpose),
|
|
225
|
+
", opt_coords=", repr(self.opt_coords),
|
|
226
|
+
", hemisphere=", repr(self.hemisphere), ", stime=",
|
|
227
|
+
repr(self.stime), ", etime=", repr(self.etime),
|
|
228
|
+
", slice_func=", repr(self.slice_func),
|
|
229
|
+
", clean_func=", repr(self.clean_func), ")"])
|
|
230
|
+
return out_str
|
|
231
|
+
|
|
232
|
+
def __str__(self):
|
|
233
|
+
"""Descriptively print the class properties."""
|
|
234
|
+
|
|
235
|
+
out_str = "\n".join(["Auroral Boundary object",
|
|
236
|
+
"=======================",
|
|
237
|
+
"Instrument Data: {:}".format(self.inst_data),
|
|
238
|
+
"\nData Variables",
|
|
239
|
+
"--------------",
|
|
240
|
+
"Time: {:s}".format(self.time_var),
|
|
241
|
+
"Geo Lon: {:s}".format(self.glon_var),
|
|
242
|
+
"Geo Lat: {:s}".format(self.glat_var),
|
|
243
|
+
"Intensity: {:s}".format(self.intensity_var),
|
|
244
|
+
"Transpose: {:}".format(self.transpose),
|
|
245
|
+
"\nCoordinate Attributes",
|
|
246
|
+
"---------------------",
|
|
247
|
+
"Hemisphere: {:d}".format(self.hemisphere),
|
|
248
|
+
"Altitude: {:.2f} km".format(self.alt),
|
|
249
|
+
"Optional coords: {:}".format(self.opt_coords),
|
|
250
|
+
"Start time: {:}".format(self.stime),
|
|
251
|
+
"End time: {:}".format(self.etime),
|
|
252
|
+
"\nInstrument Functions",
|
|
253
|
+
"--------------------",
|
|
254
|
+
"Slicing: {:}".format(self.slice_func),
|
|
255
|
+
"Cleaning: {:}".format(self.clean_func)])
|
|
256
|
+
|
|
257
|
+
return out_str
|
|
258
|
+
|
|
259
|
+
def _get_variable(self, var):
|
|
260
|
+
"""Get the data variable data as an array from the data object.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
var : str
|
|
265
|
+
Data variable name
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
ret_data :array-like
|
|
270
|
+
numpy array of the desired data.
|
|
271
|
+
|
|
272
|
+
Notes
|
|
273
|
+
-----
|
|
274
|
+
Uses the `transpose` attribute to determine whether or not returned
|
|
275
|
+
data will be transposed.
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
if hasattr(self.inst_data, str(var)):
|
|
280
|
+
# Retrieve the time attribute and cast as a numpy array
|
|
281
|
+
ret_data = np.array(getattr(self.inst_data, str(var)))
|
|
282
|
+
else:
|
|
283
|
+
try:
|
|
284
|
+
# Retrieve the data variable as a key
|
|
285
|
+
ret_data = np.array(self.inst_data[var])
|
|
286
|
+
except (TypeError, KeyError, ValueError, IndexError):
|
|
287
|
+
logger.warning("".join(["unable to retrieve ", repr(var), " ",
|
|
288
|
+
"from `inst_data`, data may be empty"]))
|
|
289
|
+
ret_data = None
|
|
290
|
+
|
|
291
|
+
# Transpose the data if desired
|
|
292
|
+
if var in self.transpose.keys() and ret_data is not None:
|
|
293
|
+
if self.transpose[var]:
|
|
294
|
+
ret_data = ret_data.transpose()
|
|
295
|
+
|
|
296
|
+
return ret_data
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def alt(self):
|
|
300
|
+
"""Altitude in km at which the intensity data is located."""
|
|
301
|
+
return self._alt
|
|
302
|
+
|
|
303
|
+
@alt.setter
|
|
304
|
+
def alt(self, new_alt=-1):
|
|
305
|
+
self._alt = new_alt
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def hemisphere(self):
|
|
309
|
+
"""Hemisphere for data processing (1 for North, -1 for South)."""
|
|
310
|
+
return self._hemisphere
|
|
311
|
+
|
|
312
|
+
@hemisphere.setter
|
|
313
|
+
def hemisphere(self, new_hemisphere=1):
|
|
314
|
+
# Also update the `opt_coords` attribute dict, if it exists and
|
|
315
|
+
# contains hemisphere information.
|
|
316
|
+
if hasattr(self, 'opt_coords'):
|
|
317
|
+
opt_dict = getattr(self, 'opt_coords')
|
|
318
|
+
|
|
319
|
+
if 'hemisphere' in opt_dict.keys():
|
|
320
|
+
opt_dict['hemisphere'] = new_hemisphere
|
|
321
|
+
setattr(self, 'opt_coords', opt_dict)
|
|
322
|
+
|
|
323
|
+
self._hemisphere = new_hemisphere
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def stime(self):
|
|
327
|
+
"""Starting time for loaded data."""
|
|
328
|
+
return self._stime
|
|
329
|
+
|
|
330
|
+
@stime.setter
|
|
331
|
+
def stime(self, new_stime=None):
|
|
332
|
+
if new_stime is None:
|
|
333
|
+
self._stime = self._get_variable(self.time_var)
|
|
334
|
+
|
|
335
|
+
if self._stime is not None:
|
|
336
|
+
self._stime = utils.coords.as_datetime(
|
|
337
|
+
self._stime.flatten().min())
|
|
338
|
+
else:
|
|
339
|
+
self._stime = utils.coords.as_datetime(new_stime)
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def etime(self):
|
|
343
|
+
"""Ending time for loaded data."""
|
|
344
|
+
return self._etime
|
|
345
|
+
|
|
346
|
+
@etime.setter
|
|
347
|
+
def etime(self, new_etime=None):
|
|
348
|
+
if new_etime is None:
|
|
349
|
+
self._etime = self._get_variable(self.time_var)
|
|
350
|
+
|
|
351
|
+
if self._etime is not None:
|
|
352
|
+
self._etime = utils.coords.as_datetime(
|
|
353
|
+
self._etime.flatten().max())
|
|
354
|
+
else:
|
|
355
|
+
self._etime = utils.coords.as_datetime(new_etime)
|
|
356
|
+
|
|
357
|
+
def set_boundaries(self, min_mlat_base=59.0, mag_method='ALLOWTRACE',
|
|
358
|
+
mlat_inc=1.0, mlt_inc=0.5, un_threshold=1.25,
|
|
359
|
+
dayglow_threshold=300.0, strict_fit=False,
|
|
360
|
+
lt_out_bin=5.0, max_iqr=1.5):
|
|
361
|
+
"""Set `boundaries` with auroral boundaries from intensity data.
|
|
362
|
+
|
|
363
|
+
Parameters
|
|
364
|
+
----------
|
|
365
|
+
min_mlat_base : float
|
|
366
|
+
Base minimum co-latitude for intensity profiles. (default=59.0)
|
|
367
|
+
method : str
|
|
368
|
+
Method for converting between geographic and magnetic coordinates.
|
|
369
|
+
(default='ALLOWTRACE')
|
|
370
|
+
mlat_inc : float
|
|
371
|
+
Magnetic latitude increment for gridding intensity. (default=1.0)
|
|
372
|
+
mlt_inc : float
|
|
373
|
+
Magnetic local time increment for gridding intensity. (default=0.5)
|
|
374
|
+
un_threshold : float
|
|
375
|
+
Maximum acceptable uncertainty value in degrees (default=1.25)
|
|
376
|
+
dayglow_threshold : float
|
|
377
|
+
Minimum allowable background intensity value in Rayleighs
|
|
378
|
+
(default=300.0)
|
|
379
|
+
strict_fit : bool
|
|
380
|
+
Enforce positive values for the x-offsets in quadratic-Gaussian fits
|
|
381
|
+
(default=False)
|
|
382
|
+
lt_out_bin : float
|
|
383
|
+
Size of local time bin in hours over which outliers in the data
|
|
384
|
+
will be identified (default=5.0)
|
|
385
|
+
max_iqr : float
|
|
386
|
+
Maximum multiplier for the interquartile range (IQR) used to
|
|
387
|
+
identify outliers above or below the upper or lower quartile
|
|
388
|
+
(default=1.5)
|
|
389
|
+
|
|
390
|
+
See Also
|
|
391
|
+
--------
|
|
392
|
+
utils.coords.convert_geo_to_mag
|
|
393
|
+
|
|
394
|
+
Notes
|
|
395
|
+
-----
|
|
396
|
+
Luminosity boundary identification finding and verification based on,
|
|
397
|
+
but not identical to, the method developed by Longden, et al. (2010).
|
|
398
|
+
|
|
399
|
+
References
|
|
400
|
+
----------
|
|
401
|
+
Longden, N. S., et al. (2010) Estimating the location of the open-closed
|
|
402
|
+
magnetic field line boundary from auroral images, 28 (9), p 1659-1678,
|
|
403
|
+
doi:10.5194/angeo-28-1659-2010.
|
|
404
|
+
|
|
405
|
+
"""
|
|
406
|
+
# Get the time, location, and intensity data
|
|
407
|
+
time_data = self._get_variable(self.time_var)
|
|
408
|
+
glat_data = self._get_variable(self.glat_var)
|
|
409
|
+
glon_data = self._get_variable(self.glon_var)
|
|
410
|
+
intensity_data = self._get_variable(self.intensity_var)
|
|
411
|
+
|
|
412
|
+
for name, var in [(self.time_var, time_data),
|
|
413
|
+
(self.glat_var, glat_data),
|
|
414
|
+
(self.glon_var, glon_data),
|
|
415
|
+
(self.intensity_var, intensity_data)]:
|
|
416
|
+
if var is None:
|
|
417
|
+
logger.info("Missing {:} data, cannot set boundaries".format(
|
|
418
|
+
name))
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
# Initalize the dicts for holding coordinate and boundary data
|
|
422
|
+
attr_dict = {'min_mlat_base': min_mlat_base, 'mag_method': mag_method,
|
|
423
|
+
'mlat_inc': mlat_inc, 'mlt_inc': mlt_inc,
|
|
424
|
+
'un_threshold': un_threshold, 'max_iqr': max_iqr,
|
|
425
|
+
'strict_fit': int(strict_fit), 'lt_out_bin': lt_out_bin}
|
|
426
|
+
coord_dict, data_dict = utils.output.init_boundary_dicts(
|
|
427
|
+
opt_coords=self.opt_coords)
|
|
428
|
+
max_coeff = 0
|
|
429
|
+
|
|
430
|
+
# Clean the intensity data
|
|
431
|
+
if self.clean_func is not None:
|
|
432
|
+
clean_mask = self.clean_func(self.inst_data)
|
|
433
|
+
else:
|
|
434
|
+
clean_mask = None
|
|
435
|
+
|
|
436
|
+
# Cycle through the desired date range
|
|
437
|
+
ctime = utils.coords.as_datetime(self.stime)
|
|
438
|
+
while ctime < utils.coords.as_datetime(self.etime):
|
|
439
|
+
# Get the next auroral intensity slice
|
|
440
|
+
if self.slice_func is not None:
|
|
441
|
+
# Use provided function to select desired data
|
|
442
|
+
intensity, glat, glon, sweep_times = self.slice_func(
|
|
443
|
+
time_data, glat_data, glon_data, intensity_data,
|
|
444
|
+
clean_mask=clean_mask, start_time=ctime,
|
|
445
|
+
hemisphere=self.hemisphere)
|
|
446
|
+
else:
|
|
447
|
+
# No slicing function provided, assume data is pre-sliced
|
|
448
|
+
intensity = intensity_data
|
|
449
|
+
glat = glat_data
|
|
450
|
+
glon = glon_data
|
|
451
|
+
sweep_times = [time_data[0], time_data[-1]]
|
|
452
|
+
|
|
453
|
+
# Get the next auroral slice and boundaries
|
|
454
|
+
(sweep_end, sweep_data,
|
|
455
|
+
max_coeff) = proc.intensity.find_intensity_boundaries(
|
|
456
|
+
intensity, glat, glon, sweep_times, self.alt,
|
|
457
|
+
min_mlat_base, max_coeff, method=mag_method,
|
|
458
|
+
mlat_inc=mlat_inc, mlt_inc=mlt_inc, un_threshold=un_threshold,
|
|
459
|
+
dayglow_threshold=dayglow_threshold, strict_fit=strict_fit)
|
|
460
|
+
|
|
461
|
+
logger.info("Auroral slice at {:} {:s} data".format(
|
|
462
|
+
sweep_end, "without" if sweep_data is None else "with"))
|
|
463
|
+
|
|
464
|
+
if sweep_end < ctime:
|
|
465
|
+
# We've reached the end of the data, but it's not triggering
|
|
466
|
+
# the escape condition
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
# Save the output for this sweep
|
|
470
|
+
utils.output.update_boundary_dicts(sweep_data, coord_dict,
|
|
471
|
+
data_dict)
|
|
472
|
+
|
|
473
|
+
# Cycle the time to start looking for the next slice
|
|
474
|
+
ctime = utils.coords.as_datetime(sweep_end) + dt.timedelta(
|
|
475
|
+
seconds=1)
|
|
476
|
+
|
|
477
|
+
# Reshape the data and cast as xarray
|
|
478
|
+
self.boundaries = utils.output.convert_boundary_dict(
|
|
479
|
+
coord_dict, data_dict, max_coeff, attr_dict=attr_dict)
|
|
480
|
+
|
|
481
|
+
# If there is data, update the boundaries, otherwise inform the user
|
|
482
|
+
if len(self.boundaries) > 0:
|
|
483
|
+
# Remove boundary outliers from each slice
|
|
484
|
+
logger.info("Removing boundary outliers from {:} to {:}.".format(
|
|
485
|
+
self.stime, self.etime))
|
|
486
|
+
self.boundaries = utils.checks.evaluate_boundary_in_mlt(
|
|
487
|
+
self.boundaries, "eq_bounds", "po_bounds", "mlt",
|
|
488
|
+
"sweep_start", lt_bin=lt_out_bin, max_iqr=max_iqr)
|
|
489
|
+
else:
|
|
490
|
+
logger.info("No boundary data found from {:} to {:}.".format(
|
|
491
|
+
self.stime, self.etime))
|
|
492
|
+
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
def update_times(self):
|
|
496
|
+
"""Update `stime` and `etime` based on potentially updated data."""
|
|
497
|
+
# Setting to NoneType triggers the "get value from data" feature
|
|
498
|
+
self.stime = None
|
|
499
|
+
self.etime = None
|
|
500
|
+
return
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# DISTRIBUTION STATEMENT A: Approved for public release. Distribution is
|
|
5
|
+
# unlimited.
|
|
6
|
+
# -----------------------------------------------------------------------------
|
|
7
|
+
"""pyIntensityFeatures instrument modules."""
|
|
8
|
+
|
|
9
|
+
from pyIntensityFeatures.instruments import satellites # noqa F401
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# DISTRIBUTION STATEMENT A: Approved for public release. Distribution is
|
|
5
|
+
# unlimited.
|
|
6
|
+
# -----------------------------------------------------------------------------
|
|
7
|
+
"""Satellite instrument support functions."""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_auroral_slice(time_data, glat_data, glon_data, int_data,
|
|
13
|
+
clean_mask=None, start_time=None, hemisphere=1,
|
|
14
|
+
min_colat=45):
|
|
15
|
+
"""Retrieve an auroral image slice that spans the desired lat range.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
time_data : array-like
|
|
20
|
+
1D array of time indexes
|
|
21
|
+
glat_data : array-like
|
|
22
|
+
2D array of geographic latitudes
|
|
23
|
+
glon_data : array-like
|
|
24
|
+
2D array of geographic longitudes
|
|
25
|
+
int_data : array-like
|
|
26
|
+
2D array of intensity values
|
|
27
|
+
clean_mask : array-like or NoneType
|
|
28
|
+
None to create a mask of finite values or 2D mask array specifying
|
|
29
|
+
good values (default=None)
|
|
30
|
+
start_time : dt.datetime or NoneType
|
|
31
|
+
Start time to search from or None to start from beginning of
|
|
32
|
+
available data (default=None)
|
|
33
|
+
hemisphere : int
|
|
34
|
+
Hemisphere to consider, where 1 is North and -1 is South (default=1)
|
|
35
|
+
min_colat : int or float
|
|
36
|
+
Absolute value of the most equatorward latitude to include (default=45)
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
intensity : array-like
|
|
41
|
+
Array with dimensions of time x sweep-locations containing the
|
|
42
|
+
rectified intensity at the auroral daytime pierce point
|
|
43
|
+
glat : array-like
|
|
44
|
+
Array with dimensions of time x sweep-locations containing the
|
|
45
|
+
geodetic latitude
|
|
46
|
+
glon : array-like
|
|
47
|
+
Array with dimensions of time x sweep-locations containing the
|
|
48
|
+
geographic longitude
|
|
49
|
+
sweep_times : array-like
|
|
50
|
+
Array with the starting and ending times of the auroral sweep. Gives
|
|
51
|
+
the start and end times searched if no sweep is found.
|
|
52
|
+
|
|
53
|
+
Raises
|
|
54
|
+
------
|
|
55
|
+
ValueError
|
|
56
|
+
If imager data does not have the same shape
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
# Set the start time
|
|
60
|
+
if start_time is None:
|
|
61
|
+
start_time = time_data[0]
|
|
62
|
+
itime = 0
|
|
63
|
+
else:
|
|
64
|
+
start_time = np.datetime64(start_time)
|
|
65
|
+
time_data = np.asarray(time_data).astype(np.datetime64)
|
|
66
|
+
itime = int(abs(start_time - time_data).argmin())
|
|
67
|
+
|
|
68
|
+
if time_data[itime] < start_time:
|
|
69
|
+
itime += 1
|
|
70
|
+
|
|
71
|
+
# Create the data mask, if desired
|
|
72
|
+
if clean_mask is None:
|
|
73
|
+
clean_mask = np.isfinite(int_data)
|
|
74
|
+
|
|
75
|
+
# Ensure the data arrays are shaped as expected
|
|
76
|
+
if np.asarray(time_data).shape[0] != int_data.shape[0]:
|
|
77
|
+
raise ValueError('first dimension of intensity data differs from time')
|
|
78
|
+
|
|
79
|
+
if int_data.shape != glat_data.shape or int_data.shape != glon_data.shape:
|
|
80
|
+
raise ValueError('intensity and location input shapes differ')
|
|
81
|
+
|
|
82
|
+
if clean_mask.shape != int_data.shape:
|
|
83
|
+
raise ValueError('clean mask shape differs from intensity data')
|
|
84
|
+
|
|
85
|
+
# Find the start of an auroral sweep
|
|
86
|
+
while itime < len(time_data):
|
|
87
|
+
if (glat_data[itime] * hemisphere >= min_colat).any():
|
|
88
|
+
if clean_mask[itime].any():
|
|
89
|
+
# If there is no clean data at this time, keep looking
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
# Cycle to the next time
|
|
93
|
+
itime += 1
|
|
94
|
+
|
|
95
|
+
if itime < len(time_data):
|
|
96
|
+
# Prepare the output
|
|
97
|
+
time_inds = list()
|
|
98
|
+
sweep_times = list()
|
|
99
|
+
|
|
100
|
+
# Found an auroral sweep in the correct hemisphere
|
|
101
|
+
while itime < len(time_data) and np.any(
|
|
102
|
+
glat_data[itime] * hemisphere >= min_colat):
|
|
103
|
+
if clean_mask[itime].any():
|
|
104
|
+
time_inds.append(itime)
|
|
105
|
+
|
|
106
|
+
if len(sweep_times) < 2:
|
|
107
|
+
sweep_times.append(time_data[itime])
|
|
108
|
+
else:
|
|
109
|
+
sweep_times[-1] = time_data[itime]
|
|
110
|
+
|
|
111
|
+
# Cycle the time index
|
|
112
|
+
itime += 1
|
|
113
|
+
|
|
114
|
+
# If the sweep ended without capturing sufficient data, set the stop
|
|
115
|
+
# time
|
|
116
|
+
if len(sweep_times) < 2:
|
|
117
|
+
if itime >= len(time_data):
|
|
118
|
+
itime = -1
|
|
119
|
+
|
|
120
|
+
if len(sweep_times) == 0:
|
|
121
|
+
# If no times were found, insert the start time
|
|
122
|
+
sweep_times.append(start_time)
|
|
123
|
+
|
|
124
|
+
sweep_times.append(time_data[itime])
|
|
125
|
+
|
|
126
|
+
# The sweep has ended, select the desired time indices
|
|
127
|
+
intensity = np.asarray(int_data[time_inds])
|
|
128
|
+
glat = np.asarray(glat_data[time_inds])
|
|
129
|
+
glon = np.asarray(glon_data[time_inds])
|
|
130
|
+
sweep_times = np.asarray(sweep_times)
|
|
131
|
+
else:
|
|
132
|
+
intensity = np.asarray([])
|
|
133
|
+
glat = np.asarray([])
|
|
134
|
+
glon = np.asarray([])
|
|
135
|
+
sweep_times = np.asarray([start_time, time_data[-1]])
|
|
136
|
+
|
|
137
|
+
return intensity, glat, glon, sweep_times
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# DISTRIBUTION STATEMENT A: Approved for public release. Distribution is
|
|
5
|
+
# unlimited.
|
|
6
|
+
# -----------------------------------------------------------------------------
|
|
7
|
+
"""pyIntensityFeatures processing modules."""
|
|
8
|
+
|
|
9
|
+
from pyIntensityFeatures.proc import boundaries # noqa F401
|
|
10
|
+
from pyIntensityFeatures.proc import intensity # noqa F401
|