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.
Files changed (30) hide show
  1. pyIntensityFeatures/__init__.py +30 -0
  2. pyIntensityFeatures/_main.py +500 -0
  3. pyIntensityFeatures/instruments/__init__.py +9 -0
  4. pyIntensityFeatures/instruments/satellites.py +137 -0
  5. pyIntensityFeatures/proc/__init__.py +10 -0
  6. pyIntensityFeatures/proc/boundaries.py +420 -0
  7. pyIntensityFeatures/proc/fitting.py +374 -0
  8. pyIntensityFeatures/proc/intensity.py +251 -0
  9. pyIntensityFeatures/tests/__init__.py +1 -0
  10. pyIntensityFeatures/tests/test_instruments_satellites.py +210 -0
  11. pyIntensityFeatures/tests/test_main.py +734 -0
  12. pyIntensityFeatures/tests/test_proc_boundaries.py +613 -0
  13. pyIntensityFeatures/tests/test_proc_fitting.py +218 -0
  14. pyIntensityFeatures/tests/test_proc_intensity.py +205 -0
  15. pyIntensityFeatures/tests/test_utils_checks.py +933 -0
  16. pyIntensityFeatures/tests/test_utils_coords.py +197 -0
  17. pyIntensityFeatures/tests/test_utils_distributions.py +236 -0
  18. pyIntensityFeatures/tests/test_utils_grids.py +189 -0
  19. pyIntensityFeatures/tests/test_utils_output.py +433 -0
  20. pyIntensityFeatures/utils/__init__.py +13 -0
  21. pyIntensityFeatures/utils/checks.py +420 -0
  22. pyIntensityFeatures/utils/coords.py +157 -0
  23. pyIntensityFeatures/utils/distributions.py +199 -0
  24. pyIntensityFeatures/utils/grids.py +113 -0
  25. pyIntensityFeatures/utils/output.py +276 -0
  26. pyintensityfeatures-0.1.0.dist-info/METADATA +360 -0
  27. pyintensityfeatures-0.1.0.dist-info/RECORD +30 -0
  28. pyintensityfeatures-0.1.0.dist-info/WHEEL +5 -0
  29. pyintensityfeatures-0.1.0.dist-info/licenses/LICENSE +28 -0
  30. 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