Repository: aburrell/ocbpy

Issue:
dual boundary class can have a bad pair
**Describe the bug**
When running the vorticity pairing with a dual boundary class, it failed when a 'pair' was established with a good OCB and an EAB index outside of the data range.

**To Reproduce**
Steps to reproduce the behavior:
```
stime = dt.datetime(2000,1,1)
etime = dt.datetime(2005,1,1)
dual = ocbpy.DualBoundary(stime=stime, etime=etime, hemisphere=1)
vort2ascii_ocb(blah)
```
Fails at 2002-10-31 20:05:16

**Expected behavior**
Create a data file with dual boundary coordinates

**Desktop (please complete the following information):**
 - OS: Linux
 - Python version: 3.12
- Other relevant details of your set up

Hints from issue comments:

Solution patch:
diff --git a/Changelog.rst b/Changelog.rst
index 92250eeb..0f0e843c 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -7,6 +7,9 @@ Summary of all changes made since the first stable release
 ------------------
 * DEP: Removed deprecated functions that depend on ssj_auroral_boundary package
 * DEP: Removed deprecated methods, attributes, and kwargs in VectorData
+* DOC: Added data reference for SuperDARN vorticity data
+* BUG: Fixed `load_vorticity_ascii_data` to correctly cycle through files
+* BUG: Fixed EAB index pairing issue in `DualBoundary` class
 * BUG: Fixed `zenodo-get` import in pyproject.toml
 * TST: Updated GitHub Actions yamls
 
diff --git a/ocbpy/_boundary.py b/ocbpy/_boundary.py
index 0eafee2f..c72db3b8 100644
--- a/ocbpy/_boundary.py
+++ b/ocbpy/_boundary.py
@@ -1281,7 +1281,8 @@ def set_good_ind(self, ocb_min_merit=None, ocb_max_merit=None,
                 max_tol=self.max_delta, **eab_kwargs)
 
             # Save the paired data
-            if iocb is not None and iocb < len(good_ocb):
+            if iocb is not None and iocb < len(
+                    good_ocb) and self.eab.rec_ind < self.eab.records:
                 self.dtime.append(self.ocb.dtime[good_ocb[iocb]])
                 self.ocb_ind.append(good_ocb[iocb])
                 self.eab_ind.append(self.eab.rec_ind)
@@ -1289,7 +1290,8 @@ def set_good_ind(self, ocb_min_merit=None, ocb_max_merit=None,
                 iocb = None
 
             # Cycle to the next good EAB index
-            self.eab.get_next_good_ocb_ind(**eab_kwargs)
+            if self.eab.rec_ind < self.eab.records:
+                self.eab.get_next_good_ocb_ind(**eab_kwargs)
 
         # Re-cast the class attributes as arrays
         self.dtime = np.asarray(self.dtime)
diff --git a/ocbpy/instruments/vort.py b/ocbpy/instruments/vort.py
index 70c392ec..3ce7cf72 100644
--- a/ocbpy/instruments/vort.py
+++ b/ocbpy/instruments/vort.py
@@ -10,7 +10,14 @@
 
 Notes
 -----
-Specialised SuperDARN data product, available from: gchi@bas.ac.uk
+Specialised SuperDARN data product, available from the British Antarctic Survey.
+
+References
+----------
+Chisham, G. (2023). Ionospheric vorticity across the northern hemisphere
+ionosphere determined from particular SuperDARN radar pairs - 2000 to 2005
+inclusive (Version 1.0) [Data set]. NERC EDS UK Polar Data Centre.
+https://doi.org/10.5285/8EEDC594-730B-4AAD-B9CE-827912320C3A
 
 """
 import datetime as dt
@@ -222,6 +229,11 @@ def load_vorticity_ascii_data(vortfile, save_all=False):
     vdata : dict
         Dictionary of numpy arrays
 
+    Raises
+    ------
+    IOError
+        If an unexpected data format is encountered
+
     """
 
     if not ocbpy.instruments.test_file(vortfile):
@@ -252,21 +264,25 @@ def load_vorticity_ascii_data(vortfile, save_all=False):
                   "C2_MLAT", "C2_MLON", "C3_MLAT", "C3_MLON", "C4_MLAT",
                   "C4_MLON"]]
 
-        # Read the lines and assign data.  Recall that blank lines in file are
-        # returned as '\n'
+        # Initalize the file reading
         vline = fvort.readline()
         vsplit = vline.split()
         vinc = 0
 
+        # Cycle through any potential header lines
+        while len(vsplit) > 0 and vsplit[0] == "#":
+            vline = fvort.readline()
+            vsplit = vline.split()
+
+        # Cycle through the data blocks, reading the lines and assigning data.
+        # Recall that blank lines in file are returned as '\n'
         while len(vline) > 0:
             if vinc == 0:
                 # This is a date line
                 if len(vsplit) != 4:
-                    estr = "".join(["unexpected line encountered when date ",
-                                    "line expected [{:s}]".format(vline)])
-                    ocbpy.logger.error(estr)
                     fvort.close()
-                    return None
+                    raise IOError("".join(["unexpected line encountered when ",
+                                           "date line expected [", vline, "]"]))
 
                 # Save the data in the format desired for the output dict
                 yy = int(vsplit[0])
@@ -279,19 +295,25 @@ def load_vorticity_ascii_data(vortfile, save_all=False):
                 dtime = (dt.datetime.strptime(stime, "%Y %m %d")
                          + dt.timedelta(seconds=np.floor(hh * 3600.0)))
                 vinc += 1
+
+                # Move to next line
+                vline = fvort.readline()
+                vsplit = vline.split()
             elif vinc == 1:
                 # This is a number of entries line
                 if len(vsplit) != 1:
-                    estr = "".join(["unexpected line encountered when number",
-                                    " of entries line expected ",
-                                    "[{:s}]".format(vline)])
-                    ocbpy.logger.error(estr)
                     fvort.close()
-                    return None
+                    raise IOError("".join(["unexpected line encountered when ",
+                                           "number of entries line expected ",
+                                           "[{:s}]".format(vline)]))
 
                 # Save the number of entries
                 nentries = int(vsplit[0])
                 vinc += 1
+
+                # Move to next line
+                vline = fvort.readline()
+                vsplit = vline.split()
             else:
                 # This is an entry.  For each entry there are three lines
                 ninc = 0
@@ -307,12 +329,10 @@ def load_vorticity_ascii_data(vortfile, save_all=False):
                         # Test to see that this line has the right number of
                         # columns
                         if len(vsplit) != len(bklist):
-                            estr = "".join(["unexpected line encountered ",
-                                            "for a data block ",
-                                            "[{:s}]".format(vline)])
-                            ocbpy.logger.error(estr)
                             fvort.close()
-                            return None
+                            raise IOError("".join([
+                                "unexpected line encountered for a data block ",
+                                "[{:s}]".format(vline)]))
 
                         # Save all desired keys
                         gkeys = list(vkeys.intersection(bklist))
@@ -332,12 +352,12 @@ def load_vorticity_ascii_data(vortfile, save_all=False):
                 # All entries in block have been processed, reset incriment
                 vinc = 0
 
-            # Move to next line
-            vline = fvort.readline()
-            vsplit = vline.split()
-
-    # Recast lists as numpy arrays
-    for k in vdata.keys():
-        vdata[k] = np.array(vdata[k])
+    # Recast lists as numpy arrays and ensure data is present
+    for vkey in vdata.keys():
+        if len(vdata[vkey]) > 0:
+            vdata[vkey] = np.array(vdata[vkey])
+        else:
+            vdata = None
+            break
 
     return vdata


Test patch:
diff --git a/ocbpy/tests/test_boundary_dual.py b/ocbpy/tests/test_boundary_dual.py
index 9e888087..0bbf6f6f 100644
--- a/ocbpy/tests/test_boundary_dual.py
+++ b/ocbpy/tests/test_boundary_dual.py
@@ -265,6 +265,17 @@ def test_assign_rec_ind_valid(self):
                 self.assertEqual(self.ocb.eab.rec_ind, self.ocb.eab_ind[i])
         return
 
+    def test_assign_rec_ind_max_default(self):
+        """Test to see that `rec_ind` is only good values."""
+
+        # Initialize the object
+        self.ocb = self.test_class(**self.set_default)
+
+        # Ensure the sub-class indices are also above the allowed range
+        self.assertLess(self.ocb.ocb_ind[-1], self.ocb.ocb.records)
+        self.assertLess(self.ocb.eab_ind[-1], self.ocb.eab.records)
+        return
+
     def test_assign_rec_ind_max(self):
         """Test to see that setting `rec_ind` above the max is consistent."""
 
@@ -319,9 +330,18 @@ def test_custom_ind_selection(self):
                 if method.find('eq') >= 0:
                     self.assertEqual(self.ocb.ocb.rec_ind, ocb_ind)
                     self.assertEqual(self.ocb.eab.rec_ind, eab_ind)
+                elif method == 'max':
+                    self.assertListEqual(list(self.ocb.eab_ind), [])
+                    self.assertListEqual(list(self.ocb.ocb_ind), [])
                 else:
-                    self.assertGreater(self.ocb.eab.rec_ind, eab_ind)
-                    self.assertGreater(self.ocb.ocb.rec_ind, ocb_ind)
+                    self.assertGreater(
+                        self.ocb.eab.rec_ind, eab_ind,
+                        msg="Unexpected EAB record from list: {:} {:}".format(
+                            self.ocb.eab.phi_cent, self.ocb.ocb_ind))
+                    self.assertGreater(
+                        self.ocb.ocb.rec_ind, ocb_ind,
+                        msg="Unexpected OCB record from list: {:}".format(
+                            self.ocb.ocb_ind))
         return
 
 
diff --git a/ocbpy/tests/test_vort.py b/ocbpy/tests/test_vort.py
index 6fe090d2..5152fc37 100644
--- a/ocbpy/tests/test_vort.py
+++ b/ocbpy/tests/test_vort.py
@@ -57,33 +57,6 @@ def test_vort2ascii_ocb_wrong_hemi(self):
         self.eval_logging_message()
         return
 
-    def test_vort_unexpected_line(self):
-        """Testing vorticity catch for file loading."""
-
-        # Initalize the vorticity run with different test files
-        for val in [(u'unexpected line encountered when number of entries', 1),
-                    (u'unexpected line encountered for a data block', -1)]:
-            with self.subTest(val=val):
-                # Initalize the warning
-                self.lwarn = val[0]
-
-                # Create the bad file
-                with open(self.temp_output, 'w') as fout:
-                    with open(self.test_file, 'r') as fin:
-                        data = fin.readlines()
-                    data.pop(val[1])
-                    fout.write(''.join(data))
-
-                # Load the bad file
-                data = ocb_ivort.load_vorticity_ascii_data(self.temp_output)
-
-                # Test logging error message
-                self.eval_logging_message()
-
-                # Test the data output
-                self.assertIsNone(data)
-        return
-
 
 class TestVort2AsciiMethods(unittest.TestCase):
     """Test the vorticity instrument ASCII functions."""
@@ -120,6 +93,26 @@ def tearDown(self):
         del self.test_empty, self.test_output_dual, self.test_unscaled_north
         return
 
+    def test_vort_unexpected_line(self):
+        """Testing vorticity catch for file loading."""
+
+        # Initalize the vorticity run with different test files
+        for val in [(u'unexpected line encountered when number of entries', 1),
+                    (u'unexpected line encountered for a data block', -1)]:
+            with self.subTest(val=val):
+                # Create the bad file
+                with open(self.temp_output, 'w') as fout:
+                    with open(self.test_file, 'r') as fin:
+                        data = fin.readlines()
+                    data.pop(val[1])
+                    fout.write(''.join(data))
+
+                # Load the bad file
+                with self.assertRaisesRegex(IOError, val[0]):
+                    ocb_ivort.load_vorticity_ascii_data(self.temp_output)
+
+        return
+
     def test_vort2ascii_ocb(self):
         """Test vorticity conversion with different hemispheres and methods."""
         # Initialize the subTest input