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,734 @@
|
|
|
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
|
+
"""Tests for functions in `_main`."""
|
|
9
|
+
|
|
10
|
+
from io import StringIO
|
|
11
|
+
import logging
|
|
12
|
+
import datetime as dt
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pds
|
|
15
|
+
import unittest
|
|
16
|
+
import xarray as xr
|
|
17
|
+
|
|
18
|
+
import pyIntensityFeatures
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def clean_func(inst_data, clean_var="clean_flag", bad_val=1):
|
|
22
|
+
"""Clean data as a test for the `clean_func` attribute.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
inst_data : dict, list, array, pds.DataFrame, or xr.Dataset
|
|
27
|
+
Instrument data
|
|
28
|
+
clean_var : str or int
|
|
29
|
+
Data variable with the clean flag (default='clean_flag')
|
|
30
|
+
bad_val : float or int
|
|
31
|
+
Values to flag as bad (default=1)
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
clean_mask : 2D array or NoneType
|
|
36
|
+
Cleaning mask output
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(inst_data, xr.Dataset):
|
|
40
|
+
clean_mask = inst_data[clean_var].values != bad_val
|
|
41
|
+
else:
|
|
42
|
+
clean_mask = np.array(inst_data[clean_var]) != bad_val
|
|
43
|
+
|
|
44
|
+
return clean_mask
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestAuroralBounds(unittest.TestCase):
|
|
48
|
+
"""Tests for the AuroralBounds class."""
|
|
49
|
+
|
|
50
|
+
def setUp(self):
|
|
51
|
+
"""Set up the test runs."""
|
|
52
|
+
# Intialize the AuroralBounds attributes
|
|
53
|
+
self.inst_data = {}
|
|
54
|
+
self.time_var = 'time'
|
|
55
|
+
self.glon_var = 'lon'
|
|
56
|
+
self.glat_var = 'lat'
|
|
57
|
+
self.intensity_var = 'intensity'
|
|
58
|
+
self.alt = 110.0
|
|
59
|
+
self.alb_kwargs = {
|
|
60
|
+
'hemisphere': 1, 'transpose': False, 'opt_coords': None,
|
|
61
|
+
'stime': None, 'etime': None, 'slice_kwargs': None,
|
|
62
|
+
'clean_func': None, 'clean_kwargs': None,
|
|
63
|
+
'slice_func':
|
|
64
|
+
pyIntensityFeatures.instruments.satellites.get_auroral_slice}
|
|
65
|
+
self.new_vals = {'inst_data': [], 'time_var': 0, 'glon_var': 1,
|
|
66
|
+
'glat_var': 2, 'intensity_var': 3, 'alt': 400.0,
|
|
67
|
+
'transpose': True, 'opt_coords': {'hi': 'test'},
|
|
68
|
+
'hemisphere': -1, 'stime': dt.datetime(1999, 2, 11),
|
|
69
|
+
'etime': dt.datetime(1999, 2, 11), 'slice_func': None,
|
|
70
|
+
'clean_func': clean_func}
|
|
71
|
+
self.alb = None
|
|
72
|
+
|
|
73
|
+
# Intialize the logging attributes
|
|
74
|
+
self.msg = ""
|
|
75
|
+
self.out = ""
|
|
76
|
+
self.log_capture = StringIO()
|
|
77
|
+
pyIntensityFeatures.logger.addHandler(logging.StreamHandler(
|
|
78
|
+
self.log_capture))
|
|
79
|
+
pyIntensityFeatures.logger.setLevel(logging.INFO)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
def tearDown(self):
|
|
83
|
+
"""Tear down the test environment."""
|
|
84
|
+
del self.inst_data, self.time_var, self.glon_var, self.glat_var
|
|
85
|
+
del self.intensity_var, self.alt, self.alb_kwargs, self.msg, self.out
|
|
86
|
+
del self.log_capture, self.new_vals
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def set_inst_data(self, class_name='dict'):
|
|
90
|
+
"""Set the `inst_data` attribute and vars using the desired class.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
class_name : str
|
|
95
|
+
String specifying one of 'dict', 'list', 'array', 'pandas', or
|
|
96
|
+
'xarray' (default='dict')
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
# Start by assuming a dict
|
|
100
|
+
time_val = [dt.datetime(1999, 2, 11) + dt.timedelta(seconds=i)
|
|
101
|
+
for i in range(400)]
|
|
102
|
+
shape2d = (len(time_val), 40)
|
|
103
|
+
self.inst_data = {self.time_var: time_val,
|
|
104
|
+
self.glat_var: np.ones(shape=shape2d),
|
|
105
|
+
self.glon_var: np.ones(shape=shape2d),
|
|
106
|
+
self.intensity_var: np.full(
|
|
107
|
+
shape=shape2d, fill_value=500 * np.sin(
|
|
108
|
+
np.linspace(0, np.pi, shape2d[1]))),
|
|
109
|
+
'clean_flag': np.ones(shape=shape2d)}
|
|
110
|
+
|
|
111
|
+
self.inst_data[self.glat_var][0] = np.linspace(-6.0, 6.0, shape2d[1])
|
|
112
|
+
self.inst_data[self.glon_var][0] = np.linspace(100.0, 300.0, shape2d[1])
|
|
113
|
+
|
|
114
|
+
for i, lat in enumerate(self.inst_data[self.glat_var][0]):
|
|
115
|
+
self.inst_data[self.glat_var][:, i] = lat + (
|
|
116
|
+
90.0 - abs(lat)) * np.sin(np.linspace(0, 2.0 * np.pi,
|
|
117
|
+
shape2d[0]))
|
|
118
|
+
self.inst_data[self.glon_var][:, i] = self.inst_data[
|
|
119
|
+
self.glon_var][0, i] + (360.0 - self.inst_data[self.glon_var][
|
|
120
|
+
0, i]) * np.sin(np.linspace(0, np.pi, shape2d[0]))
|
|
121
|
+
self.inst_data[self.intensity_var][:, i] *= np.sin(np.linspace(
|
|
122
|
+
0, np.pi, shape2d[0]))
|
|
123
|
+
|
|
124
|
+
if self.alb_kwargs['transpose']:
|
|
125
|
+
for var in [self.glat_var, self.glon_var, self.intensity_var,
|
|
126
|
+
'clean_flag']:
|
|
127
|
+
self.inst_data[var] = self.inst_data[var].transpose()
|
|
128
|
+
|
|
129
|
+
# Update the non-dict class types
|
|
130
|
+
if class_name.lower() == 'xarray':
|
|
131
|
+
if self.alb_kwargs['transpose']:
|
|
132
|
+
dat_dims = ["sweep_loc", self.time_var]
|
|
133
|
+
else:
|
|
134
|
+
dat_dims = [self.time_var, "sweep_loc"]
|
|
135
|
+
|
|
136
|
+
# Cast an xarray Dataset from a reshaped dict
|
|
137
|
+
self.inst_data = xr.Dataset.from_dict({
|
|
138
|
+
key: {"dims": [self.time_var], "data": self.inst_data[key]}
|
|
139
|
+
if key == self.time_var else
|
|
140
|
+
{"dims": dat_dims,
|
|
141
|
+
"data": self.inst_data[key]} for key in self.inst_data})
|
|
142
|
+
elif class_name.lower() in ['array', 'list']:
|
|
143
|
+
# List and array require the same initial changes
|
|
144
|
+
self.inst_data = [self.inst_data[self.time_var],
|
|
145
|
+
self.inst_data[self.glon_var],
|
|
146
|
+
self.inst_data[self.glat_var],
|
|
147
|
+
self.inst_data[self.intensity_var],
|
|
148
|
+
self.inst_data['clean_flag']]
|
|
149
|
+
self.time_var = 0
|
|
150
|
+
self.glon_var = 1
|
|
151
|
+
self.glat_var = 2
|
|
152
|
+
self.intensity_var = 3
|
|
153
|
+
|
|
154
|
+
if class_name.lower() == 'array':
|
|
155
|
+
# Data must be shaped the same
|
|
156
|
+
self.inst_data[self.time_var] = np.full(
|
|
157
|
+
shape=(shape2d[1], shape2d[0]), fill_value=time_val)
|
|
158
|
+
|
|
159
|
+
if not self.alb_kwargs['transpose']:
|
|
160
|
+
self.inst_data[self.time_var] = self.inst_data[
|
|
161
|
+
self.time_var].transpose()
|
|
162
|
+
|
|
163
|
+
self.inst_data = np.array(self.inst_data)
|
|
164
|
+
|
|
165
|
+
elif class_name.lower() == 'pandas':
|
|
166
|
+
# Data must be shaped the same and be 1D
|
|
167
|
+
self.inst_data[self.time_var] = np.full(
|
|
168
|
+
shape=(shape2d[1], shape2d[0]), fill_value=time_val).transpose()
|
|
169
|
+
|
|
170
|
+
for key in self.inst_data.keys():
|
|
171
|
+
self.inst_data[key] = self.inst_data[key].flatten()
|
|
172
|
+
|
|
173
|
+
# Cast a pandas DataFrame from the dict
|
|
174
|
+
self.inst_data = pds.DataFrame(self.inst_data,
|
|
175
|
+
index=self.inst_data[self.time_var])
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
def set_alb(self, set_attr=True):
|
|
179
|
+
"""Set the ALB test attribute for the tests.
|
|
180
|
+
|
|
181
|
+
set_attr : bool
|
|
182
|
+
If True, set the `alb` attribute, if False return the class object
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
if set_attr:
|
|
186
|
+
self.alb = pyIntensityFeatures.AuroralBounds(
|
|
187
|
+
self.inst_data, self.time_var, self.glon_var, self.glat_var,
|
|
188
|
+
self.intensity_var, self.alt, **self.alb_kwargs)
|
|
189
|
+
return
|
|
190
|
+
else:
|
|
191
|
+
return pyIntensityFeatures.AuroralBounds(
|
|
192
|
+
self.inst_data, self.time_var, self.glon_var, self.glat_var,
|
|
193
|
+
self.intensity_var, self.alt, **self.alb_kwargs)
|
|
194
|
+
|
|
195
|
+
def eval_times(self, class_name="dict"):
|
|
196
|
+
"""Evaluate times based on the input.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
class_name : str
|
|
201
|
+
String specifying one of 'dict', 'list', 'array', 'pandas', or
|
|
202
|
+
'xarray' (default='dict')
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
# Set the comparison time
|
|
206
|
+
if len(self.inst_data) == 0:
|
|
207
|
+
start = self.alb_kwargs['stime']
|
|
208
|
+
end = self.alb_kwargs['etime']
|
|
209
|
+
elif class_name == 'array':
|
|
210
|
+
start = self.inst_data[self.time_var][0, 0]
|
|
211
|
+
end = self.inst_data[self.time_var][-1, -1]
|
|
212
|
+
elif class_name == 'xarray':
|
|
213
|
+
start = pyIntensityFeatures.utils.coords.as_datetime(
|
|
214
|
+
self.inst_data[self.time_var].values[0])
|
|
215
|
+
end = pyIntensityFeatures.utils.coords.as_datetime(
|
|
216
|
+
self.inst_data[self.time_var].values[-1])
|
|
217
|
+
else:
|
|
218
|
+
start = self.inst_data[self.time_var][0]
|
|
219
|
+
end = self.inst_data[self.time_var][-1]
|
|
220
|
+
|
|
221
|
+
# Evaluate the times
|
|
222
|
+
self.assertTrue(self.alb.stime == start,
|
|
223
|
+
msg="{:} != {:}".format(self.alb.stime, start))
|
|
224
|
+
self.assertTrue(self.alb.etime == end,
|
|
225
|
+
msg="{:} != {:}".format(self.alb.etime, end))
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
def eval_boundaries(self, min_mlat_base=59.0, mlat_inc=1.0,
|
|
229
|
+
mag_method="ALLOWTRACE", mlt_inc=0.5, un_threshold=1.25,
|
|
230
|
+
strict_fit=0, lt_out_bin=5.0, max_iqr=1.5):
|
|
231
|
+
"""Evaluate successful setting of boundaries.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
min_mlat_base : float
|
|
236
|
+
Base minimum co-latitude for intensity profiles. (default=59.0)
|
|
237
|
+
method : str
|
|
238
|
+
Method for converting between geographic and magnetic coordinates.
|
|
239
|
+
(default='ALLOWTRACE')
|
|
240
|
+
mlat_inc : float
|
|
241
|
+
Magnetic latitude increment for gridding intensity. (default=1.0)
|
|
242
|
+
mlt_inc : float
|
|
243
|
+
Magnetic local time increment for gridding intensity. (default=0.5)
|
|
244
|
+
un_threshold : float
|
|
245
|
+
Maximum acceptable uncertainty value in degrees (default=1.25)
|
|
246
|
+
strict_fit : int
|
|
247
|
+
Enforce positive values for the x-offsets in quadratic-Gaussian fits
|
|
248
|
+
using integer version of boolean (default=0)
|
|
249
|
+
lt_out_bin : float
|
|
250
|
+
Size of local time bin in hours over which outliers in the data
|
|
251
|
+
will be identified (default=5.0)
|
|
252
|
+
max_iqr : float
|
|
253
|
+
Maximum multiplier for the interquartile range (IQR) used to
|
|
254
|
+
identify outliers above or below the upper or lower quartile
|
|
255
|
+
(default=1.5)
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
# Ensure un-run value is not present
|
|
260
|
+
self.assertIsNotNone(self.alb.boundaries, msg="boundaries were not set")
|
|
261
|
+
|
|
262
|
+
# Ensure no-boundary value is not present
|
|
263
|
+
self.assertGreater(len(self.alb.boundaries), 0,
|
|
264
|
+
msg="no boundaries were found")
|
|
265
|
+
|
|
266
|
+
# Evalute contents of the boundary object
|
|
267
|
+
self.assertDictEqual({'min_mlat_base': min_mlat_base,
|
|
268
|
+
'mlat_inc': mlat_inc, 'mag_method': mag_method,
|
|
269
|
+
'mlt_inc': mlt_inc, 'un_threshold': un_threshold,
|
|
270
|
+
'strict_fit': strict_fit,
|
|
271
|
+
'lt_out_bin': lt_out_bin, 'max_iqr': max_iqr},
|
|
272
|
+
self.alb.boundaries.attrs,
|
|
273
|
+
msg="unexpected default attributes")
|
|
274
|
+
self.assertDictEqual({'sweep_start': 1, 'mlt': 48, 'coeff': 6,
|
|
275
|
+
'lat': 31, 'sweep_end': 1},
|
|
276
|
+
dict(self.alb.boundaries.dims),
|
|
277
|
+
msg="unexpected default dimensions")
|
|
278
|
+
self.assertListEqual(["sweep_start", "sweep_end", "mlt", "hemisphere",
|
|
279
|
+
"lat"],
|
|
280
|
+
[coord for coord
|
|
281
|
+
in self.alb.boundaries.coords.keys()],
|
|
282
|
+
msg="unexpected default coordinate")
|
|
283
|
+
self.assertListEqual(["eq_bounds", "eq_uncert", "po_bounds",
|
|
284
|
+
"po_uncert", "eq_params", "po_params",
|
|
285
|
+
"mean_intensity", "std_intensity",
|
|
286
|
+
"num_intensity"],
|
|
287
|
+
[var for var
|
|
288
|
+
in self.alb.boundaries.data_vars.keys()],
|
|
289
|
+
msg="unexpected data variables")
|
|
290
|
+
self.assertLessEqual(
|
|
291
|
+
min_mlat_base, np.nanmax(self.alb.boundaries['eq_bounds'].values),
|
|
292
|
+
msg="Bad equatorial boundary returned")
|
|
293
|
+
self.assertLessEqual(
|
|
294
|
+
min_mlat_base, np.nanmax(self.alb.boundaries['po_bounds'].values),
|
|
295
|
+
msg="Bad polar boundary returned")
|
|
296
|
+
self.assertLessEqual(
|
|
297
|
+
0.0, np.nanmin(self.alb.boundaries['eq_uncert'].values),
|
|
298
|
+
msg="Bad equatorial uncertainty returned")
|
|
299
|
+
self.assertLessEqual(
|
|
300
|
+
0.0, np.nanmin(self.alb.boundaries['po_uncert'].values),
|
|
301
|
+
msg="Bad polar uncertainty returned")
|
|
302
|
+
self.assertGreaterEqual(
|
|
303
|
+
self.alb.inst_data[self.intensity_var].max(),
|
|
304
|
+
np.nanmax(self.alb.boundaries['mean_intensity'].values),
|
|
305
|
+
msg="Bad mean intensity returned")
|
|
306
|
+
self.assertLessEqual(
|
|
307
|
+
0, np.nanmin(self.alb.boundaries['num_intensity'].values),
|
|
308
|
+
msg="Bad number of intensity points returned")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
def test_init_empty_data(self):
|
|
312
|
+
"""Test initialization of the class with an empty data object."""
|
|
313
|
+
self.msg = "".join(["unable to retrieve 'time' from `inst_data`, ",
|
|
314
|
+
"data may be empty"])
|
|
315
|
+
for ctype, self.inst_data in [("dict", {}), ("array", np.array([])),
|
|
316
|
+
("list", []), ("pandas", pds.DataFrame()),
|
|
317
|
+
("xarray", xr.Dataset())]:
|
|
318
|
+
with self.subTest(inst_data=self.inst_data):
|
|
319
|
+
# Set the class object
|
|
320
|
+
self.set_alb()
|
|
321
|
+
|
|
322
|
+
# Evaluate the logging warnings
|
|
323
|
+
self.out = self.log_capture.getvalue()
|
|
324
|
+
self.assertRegex(self.out, self.msg)
|
|
325
|
+
|
|
326
|
+
# Evaluate the times
|
|
327
|
+
self.eval_times(ctype)
|
|
328
|
+
|
|
329
|
+
# Test the boundaries
|
|
330
|
+
self.assertIsNone(self.alb.boundaries)
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
def test_repr_string(self):
|
|
334
|
+
"""Test __repr__ method string."""
|
|
335
|
+
# Set the class object without a slice function
|
|
336
|
+
self.alb_kwargs['slice_func'] = None
|
|
337
|
+
self.set_alb()
|
|
338
|
+
|
|
339
|
+
# Get the representative output
|
|
340
|
+
self.out = self.alb.__repr__()
|
|
341
|
+
|
|
342
|
+
# Ensure the name and expected number of kwargs are present
|
|
343
|
+
self.assertRegex(self.out, pyIntensityFeatures.AuroralBounds.__name__)
|
|
344
|
+
self.assertEqual(
|
|
345
|
+
len(self.out.split("=")), len(self.alb_kwargs.keys()) - 1,
|
|
346
|
+
msg="unexpected number of kwargs AuroralBounds representation")
|
|
347
|
+
|
|
348
|
+
# Test that a new AuroralBounds object can be created from repr
|
|
349
|
+
# if there are not issues with the data or function reproduction
|
|
350
|
+
self.out = eval(self.out)
|
|
351
|
+
self.assertTrue(self.out == self.alb)
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
def test_print_string(self):
|
|
355
|
+
"""Test __str__ method string."""
|
|
356
|
+
# Set the class object
|
|
357
|
+
self.set_alb()
|
|
358
|
+
|
|
359
|
+
# Get the representative output
|
|
360
|
+
self.out = self.alb.__str__()
|
|
361
|
+
|
|
362
|
+
# Ensure the expected headers are present
|
|
363
|
+
self.msg = ["Auroral Boundary object", "Data Variables",
|
|
364
|
+
"Coordinate Attributes", "Instrument Functions"]
|
|
365
|
+
for comp in self.msg:
|
|
366
|
+
self.assertRegex(self.out, comp)
|
|
367
|
+
|
|
368
|
+
# Remove this header from the output for future evaluations
|
|
369
|
+
self.out = self.out.replace(comp, '')
|
|
370
|
+
|
|
371
|
+
# After removing headers and new lines, ensure all args and kwargs
|
|
372
|
+
# are displayed
|
|
373
|
+
self.out = self.out.replace('=', '').replace('-', '').split('\n')
|
|
374
|
+
|
|
375
|
+
while '' in self.out:
|
|
376
|
+
self.out.pop(self.out.index(''))
|
|
377
|
+
|
|
378
|
+
# There are 6 args and 2 of the input kwargs are not attributes
|
|
379
|
+
self.assertEqual(len(self.out), len(self.alb_kwargs.keys()) + 4)
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
def test_equality(self):
|
|
383
|
+
"""Test class equality with empty data objects."""
|
|
384
|
+
for self.inst_data in [{}, np.array([]), [], pds.DataFrame(),
|
|
385
|
+
xr.Dataset()]:
|
|
386
|
+
with self.subTest(inst_data=self.inst_data):
|
|
387
|
+
# Set the class object
|
|
388
|
+
self.set_alb()
|
|
389
|
+
|
|
390
|
+
# Set a comparison object
|
|
391
|
+
self.out = self.set_alb(False)
|
|
392
|
+
|
|
393
|
+
# Evaluate the equality
|
|
394
|
+
self.assertEqual(self.out, self.alb)
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
def test_inequality_wrong_class(self):
|
|
398
|
+
"""Test class equality with different classes."""
|
|
399
|
+
# Set the class object
|
|
400
|
+
self.set_alb()
|
|
401
|
+
|
|
402
|
+
# Evaluate the inequality
|
|
403
|
+
self.assertFalse(self.alb == np.array([]))
|
|
404
|
+
|
|
405
|
+
# Evalute the logging output
|
|
406
|
+
self.msg = "wrong class"
|
|
407
|
+
self.out = self.log_capture.getvalue()
|
|
408
|
+
self.assertRegex(self.out, self.msg)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
def test_inequality_extra_attributes(self):
|
|
412
|
+
"""Test class equality with extra attributes."""
|
|
413
|
+
# Set the class object and comparison object
|
|
414
|
+
self.set_alb()
|
|
415
|
+
self.out = self.set_alb(False)
|
|
416
|
+
setattr(self.out, "test_attr", "hi")
|
|
417
|
+
|
|
418
|
+
# Evaluate the inequality
|
|
419
|
+
self.assertFalse(self.alb == self.out)
|
|
420
|
+
|
|
421
|
+
# Evalute the logging output
|
|
422
|
+
self.msg = "object contains extra attribute: test_attr"
|
|
423
|
+
self.out = self.log_capture.getvalue()
|
|
424
|
+
self.assertRegex(self.out, self.msg)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
def test_inequality_missing_attributes(self):
|
|
428
|
+
"""Test class equality with missing attributes."""
|
|
429
|
+
# Set the class object and comparison object
|
|
430
|
+
self.set_alb()
|
|
431
|
+
self.out = self.set_alb(False)
|
|
432
|
+
setattr(self.alb, "test_attr", "hi")
|
|
433
|
+
|
|
434
|
+
# Evaluate the inequality
|
|
435
|
+
self.assertFalse(self.alb == self.out)
|
|
436
|
+
|
|
437
|
+
# Evalute the logging output
|
|
438
|
+
self.msg = "object is missing attribute: test_attr"
|
|
439
|
+
self.out = self.log_capture.getvalue()
|
|
440
|
+
self.assertRegex(self.out, self.msg)
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
def test_inequality_attributes(self):
|
|
444
|
+
"""Test class equality with unequal standard attributes."""
|
|
445
|
+
# Set the class object
|
|
446
|
+
self.set_alb()
|
|
447
|
+
|
|
448
|
+
# Cycle through the available attributes
|
|
449
|
+
for attr in self.new_vals.keys():
|
|
450
|
+
with self.subTest(attr=attr):
|
|
451
|
+
# Set and update the comparison object
|
|
452
|
+
if attr in self.alb_kwargs.keys():
|
|
453
|
+
orig_val = self.alb_kwargs[attr]
|
|
454
|
+
self.alb_kwargs[attr] = self.new_vals[attr]
|
|
455
|
+
else:
|
|
456
|
+
orig_val = getattr(self, attr)
|
|
457
|
+
setattr(self, attr, self.new_vals[attr])
|
|
458
|
+
|
|
459
|
+
self.out = self.set_alb(False)
|
|
460
|
+
|
|
461
|
+
# Reset the original values
|
|
462
|
+
if attr in self.alb_kwargs.keys():
|
|
463
|
+
self.alb_kwargs[attr] = orig_val
|
|
464
|
+
else:
|
|
465
|
+
setattr(self, attr, orig_val)
|
|
466
|
+
# Evaluate the inequality
|
|
467
|
+
self.assertFalse(self.alb == self.out)
|
|
468
|
+
|
|
469
|
+
# Evalute the logging output
|
|
470
|
+
self.msg = "{:} differs".format(attr)
|
|
471
|
+
self.out = self.log_capture.getvalue()
|
|
472
|
+
self.assertRegex(self.out, self.msg)
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
def test_update_properties(self):
|
|
476
|
+
"""Test properties can be updated to different values."""
|
|
477
|
+
self.set_alb()
|
|
478
|
+
|
|
479
|
+
# Cycle through the properties
|
|
480
|
+
for prop in ['alt', 'hemisphere', 'stime', 'etime']:
|
|
481
|
+
with self.subTest(prop=prop):
|
|
482
|
+
# Ensure the values do not equal the new values
|
|
483
|
+
self.assertFalse(getattr(self.alb, prop) == self.new_vals[prop])
|
|
484
|
+
|
|
485
|
+
# Update the property
|
|
486
|
+
setattr(self.alb, prop, self.new_vals[prop])
|
|
487
|
+
|
|
488
|
+
# Evaluate the new value
|
|
489
|
+
self.assertTrue(getattr(self.alb, prop) == self.new_vals[prop])
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
def test_init_with_data(self):
|
|
493
|
+
"""Test times are set based on data when data is available."""
|
|
494
|
+
# Class type order must end with 'list' and 'array'
|
|
495
|
+
for ctype in ['dict', 'pandas', 'xarray', 'list', 'array']:
|
|
496
|
+
with self.subTest(ctype=ctype):
|
|
497
|
+
# Update the data
|
|
498
|
+
self.set_inst_data(ctype)
|
|
499
|
+
|
|
500
|
+
# Initialize the AuroralBounds object
|
|
501
|
+
self.set_alb()
|
|
502
|
+
|
|
503
|
+
# Test the times
|
|
504
|
+
self.eval_times(ctype)
|
|
505
|
+
|
|
506
|
+
# Test the boundaries
|
|
507
|
+
self.assertIsNone(self.alb.boundaries)
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
def test_init_with_transposed_data(self):
|
|
511
|
+
"""Test times are set based on data when data is available."""
|
|
512
|
+
self.alb_kwargs['transpose'] = True
|
|
513
|
+
|
|
514
|
+
# Class type order must end with 'list' and 'array'
|
|
515
|
+
for ctype in ['dict', 'pandas', 'xarray', 'list', 'array']:
|
|
516
|
+
with self.subTest(ctype=ctype):
|
|
517
|
+
# Update the data
|
|
518
|
+
self.set_inst_data(ctype)
|
|
519
|
+
|
|
520
|
+
# Initialize the AuroralBounds object
|
|
521
|
+
self.set_alb()
|
|
522
|
+
|
|
523
|
+
# Test the times
|
|
524
|
+
self.eval_times(ctype)
|
|
525
|
+
|
|
526
|
+
# Test that data is returned transposed internally
|
|
527
|
+
for var in self.alb.transpose.keys():
|
|
528
|
+
self.out = self.alb._get_variable(var)
|
|
529
|
+
self.assertTupleEqual(self.out.transpose().shape,
|
|
530
|
+
self.alb.inst_data[var].shape)
|
|
531
|
+
|
|
532
|
+
# Test the boundaries
|
|
533
|
+
self.assertIsNone(self.alb.boundaries)
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
def test_update_times_with_data(self):
|
|
537
|
+
"""Test times are set based on data when data is available."""
|
|
538
|
+
# Class type order must end with 'list' and 'array'
|
|
539
|
+
for ctype in ['dict', 'pandas', 'xarray', 'list', 'array']:
|
|
540
|
+
with self.subTest(ctype=ctype):
|
|
541
|
+
# Set the AuroralBounds object with data
|
|
542
|
+
self.set_inst_data(ctype)
|
|
543
|
+
self.set_alb()
|
|
544
|
+
|
|
545
|
+
# Adjust the data
|
|
546
|
+
if ctype in ['dict', 'list']:
|
|
547
|
+
self.inst_data[self.time_var] = self.inst_data[
|
|
548
|
+
self.time_var][:-10]
|
|
549
|
+
elif ctype in 'array':
|
|
550
|
+
self.inst_data = self.inst_data[:, :-10]
|
|
551
|
+
elif ctype == 'pandas':
|
|
552
|
+
self.inst_data = self.inst_data[:-10]
|
|
553
|
+
else:
|
|
554
|
+
self.inst_data = xr.Dataset({
|
|
555
|
+
var: (self.inst_data[var].dims,
|
|
556
|
+
self.inst_data[var].values[:-10])
|
|
557
|
+
for var in [self.time_var, self.glat_var, self.glon_var,
|
|
558
|
+
self.intensity_var, 'clean_flag']})
|
|
559
|
+
self.alb.inst_data = self.inst_data
|
|
560
|
+
|
|
561
|
+
# Update the times
|
|
562
|
+
self.alb.update_times()
|
|
563
|
+
|
|
564
|
+
# Test the times
|
|
565
|
+
self.eval_times(ctype)
|
|
566
|
+
|
|
567
|
+
# Test the boundaries
|
|
568
|
+
self.assertIsNone(self.alb.boundaries)
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
def test_set_boundaries_empty_data(self):
|
|
572
|
+
"""Test initialization of the class with an empty data object."""
|
|
573
|
+
self.msg = " data, cannot set boundaries"
|
|
574
|
+
for ctype, self.inst_data in [("dict", {}), ("array", np.array([])),
|
|
575
|
+
("list", []), ("pandas", pds.DataFrame()),
|
|
576
|
+
("xarray", xr.Dataset())]:
|
|
577
|
+
with self.subTest(inst_data=self.inst_data):
|
|
578
|
+
# Set the class object
|
|
579
|
+
self.set_alb()
|
|
580
|
+
|
|
581
|
+
# Evaluate the times
|
|
582
|
+
self.eval_times(ctype)
|
|
583
|
+
|
|
584
|
+
# Set the boundaries
|
|
585
|
+
self.alb.set_boundaries()
|
|
586
|
+
|
|
587
|
+
# Test the logging message
|
|
588
|
+
self.out = self.log_capture.getvalue()
|
|
589
|
+
self.assertRegex(self.out, self.msg)
|
|
590
|
+
|
|
591
|
+
# Test the boundaries
|
|
592
|
+
self.assertIsNone(self.alb.boundaries)
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
def test_set_boundaries_with_data(self):
|
|
596
|
+
"""Test setting boundaries from data."""
|
|
597
|
+
# Set the messages that should be raised for the test data
|
|
598
|
+
self.msg = ["Gaussian peak is outside of the intensity profile",
|
|
599
|
+
"Auroral slice at ", "with data", "without data",
|
|
600
|
+
"Removing boundary outliers from",
|
|
601
|
+
"The polar/equatorward boundary locations are mixed up"]
|
|
602
|
+
|
|
603
|
+
# Set the uncertainty threshold to be large for testing
|
|
604
|
+
uncert = 12.0
|
|
605
|
+
|
|
606
|
+
# Class type order must end with 'list'. Not testing array as an
|
|
607
|
+
# appropriate slicing function is not available.
|
|
608
|
+
for ctype in ['dict', 'pandas', 'xarray', 'list']:
|
|
609
|
+
with self.subTest(ctype=ctype):
|
|
610
|
+
# Update the data
|
|
611
|
+
self.set_inst_data(ctype)
|
|
612
|
+
|
|
613
|
+
# Initialize the AuroralBounds object
|
|
614
|
+
self.set_alb()
|
|
615
|
+
|
|
616
|
+
# Test the times
|
|
617
|
+
self.eval_times(ctype)
|
|
618
|
+
|
|
619
|
+
# Set the boundaries
|
|
620
|
+
self.alb.set_boundaries(un_threshold=uncert)
|
|
621
|
+
|
|
622
|
+
# Evaluate the logging messages
|
|
623
|
+
self.out = self.log_capture.getvalue()
|
|
624
|
+
for mes in self.msg:
|
|
625
|
+
self.assertRegex(self.out, mes)
|
|
626
|
+
|
|
627
|
+
# Evaluate the boundaries
|
|
628
|
+
self.eval_boundaries(un_threshold=uncert)
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
def test_set_boundaries_with_long_end_time(self):
|
|
632
|
+
"""Test setting boundaries with an end time beyond the data."""
|
|
633
|
+
# Set the messages that should be raised for the test data
|
|
634
|
+
self.msg = ["Gaussian peak is outside of the intensity profile",
|
|
635
|
+
"Auroral slice at ", "with data", "without data",
|
|
636
|
+
"Removing boundary outliers from",
|
|
637
|
+
"The polar/equatorward boundary locations are mixed up"]
|
|
638
|
+
|
|
639
|
+
# Set the uncertainty threshold to be large for testing
|
|
640
|
+
uncert = 12.0
|
|
641
|
+
|
|
642
|
+
# Class type order must end with 'list'. Not testing array as an
|
|
643
|
+
# appropriate slicing function is not available.
|
|
644
|
+
for ctype in ['dict', 'pandas', 'xarray', 'list']:
|
|
645
|
+
with self.subTest(ctype=ctype):
|
|
646
|
+
# Update the data
|
|
647
|
+
self.set_inst_data(ctype)
|
|
648
|
+
|
|
649
|
+
# Initialize the AuroralBounds object
|
|
650
|
+
self.set_alb()
|
|
651
|
+
|
|
652
|
+
# Test the times
|
|
653
|
+
self.eval_times(ctype)
|
|
654
|
+
|
|
655
|
+
# Update the end time
|
|
656
|
+
self.alb.etime += dt.timedelta(days=1)
|
|
657
|
+
|
|
658
|
+
# Set the boundaries
|
|
659
|
+
self.alb.set_boundaries(un_threshold=uncert)
|
|
660
|
+
|
|
661
|
+
# Evaluate the logging messages
|
|
662
|
+
self.out = self.log_capture.getvalue()
|
|
663
|
+
for mes in self.msg:
|
|
664
|
+
self.assertRegex(self.out, mes)
|
|
665
|
+
|
|
666
|
+
# Evaluate the boundaries
|
|
667
|
+
self.eval_boundaries(un_threshold=uncert)
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
def test_set_boundaries_with_data_no_slice(self):
|
|
671
|
+
"""Test setting boundaries from data without slicing."""
|
|
672
|
+
# Set the messages that should be raised for the test data
|
|
673
|
+
self.msg = ["Gaussian peak is outside of the intensity profile",
|
|
674
|
+
"Auroral slice at ", "with data",
|
|
675
|
+
"Removing boundary outliers from",
|
|
676
|
+
"The polar/equatorward boundary locations are mixed up"]
|
|
677
|
+
self.alb_kwargs['slice_func'] = self.new_vals['slice_func']
|
|
678
|
+
uncert = 12.0
|
|
679
|
+
|
|
680
|
+
# Class type order must end with 'list'. Not testing array as an
|
|
681
|
+
# appropriate slicing function is not available.
|
|
682
|
+
for ctype in ['dict', 'pandas', 'xarray', 'list']:
|
|
683
|
+
with self.subTest(ctype=ctype):
|
|
684
|
+
# Update the data
|
|
685
|
+
self.set_inst_data(ctype)
|
|
686
|
+
|
|
687
|
+
# Initialize the AuroralBounds object
|
|
688
|
+
self.set_alb()
|
|
689
|
+
|
|
690
|
+
# Test the times
|
|
691
|
+
self.eval_times(ctype)
|
|
692
|
+
|
|
693
|
+
# Set the boundaries
|
|
694
|
+
self.alb.set_boundaries(un_threshold=uncert)
|
|
695
|
+
|
|
696
|
+
# Evaluate the logging messages
|
|
697
|
+
self.out = self.log_capture.getvalue()
|
|
698
|
+
for mes in self.msg:
|
|
699
|
+
self.assertRegex(self.out, mes)
|
|
700
|
+
|
|
701
|
+
# Evaluate the boundaries
|
|
702
|
+
self.eval_boundaries(un_threshold=uncert)
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
def test_set_boundaries_with_mask(self):
|
|
706
|
+
"""Test setting boundaries from data while masking all data."""
|
|
707
|
+
# Set the messages that should be raised for the test data
|
|
708
|
+
self.msg = ["Gaussian peak is outside of the intensity profile",
|
|
709
|
+
"Auroral slice at ", "with data", "without data",
|
|
710
|
+
"Removing boundary outliers from",
|
|
711
|
+
"The polar/equatorward boundary locations are mixed up"]
|
|
712
|
+
self.alb_kwargs['clean_func'] = self.new_vals['clean_func']
|
|
713
|
+
|
|
714
|
+
# Not testing 'list' or 'array' as the clean function is not set up
|
|
715
|
+
# to handle integer inputs
|
|
716
|
+
for ctype in ['dict', 'pandas', 'xarray']:
|
|
717
|
+
with self.subTest(ctype=ctype):
|
|
718
|
+
# Update the data
|
|
719
|
+
self.set_inst_data(ctype)
|
|
720
|
+
|
|
721
|
+
# Initialize the AuroralBounds object
|
|
722
|
+
self.set_alb()
|
|
723
|
+
|
|
724
|
+
# Test the times
|
|
725
|
+
self.eval_times(ctype)
|
|
726
|
+
|
|
727
|
+
# Set the boundaries
|
|
728
|
+
self.alb.set_boundaries()
|
|
729
|
+
|
|
730
|
+
# Test the boundaries
|
|
731
|
+
self.assertIsNotNone(self.alb.boundaries)
|
|
732
|
+
self.assertEqual(0, len(self.alb.boundaries),
|
|
733
|
+
msg="found boundaries without data")
|
|
734
|
+
return
|