Repository: cta-observatory/ctapipe (python)

Issue:
TableLoader can not read subarray events including observation info
**Describe the bug**
Trying to load subarray events with TableLoader including the observation info, leads to following error:
```
ValueError                                Traceback (most recent call last)
Cell In[5], line 4
      1 from ctapipe.io import TableLoader
      3 loader = TableLoader("../build/gamma-diffuse_train_tel.dl2.h5", load_observation_info=True)
----> 4 events = loader.read_subarray_events()

File ~/ctapipe/ctapipe/io/tableloader.py:402, in TableLoader.read_subarray_events(self, start, stop, keep_order)
    399     table = self._join_observation_info(table)
    401 if keep_order:
--> 402     self._sort_to_original_order(table)
    403 return table

File ~/ctapipe/ctapipe/io/tableloader.py:319, in TableLoader._sort_to_original_order(table, include_tel_id)
    317     table.sort(("__index__", "tel_id"))
    318 else:
--> 319     table.sort("__index__")
    320 table.remove_column("__index__")

File /scratch/lbeiske/envs/cta/lib/python3.9/site-packages/astropy/table/table.py:3521, in Table.sort(self, keys, kind, reverse)
   3518 if isinstance(keys, str):
   3519     keys = [keys]
-> 3521 indexes = self.argsort(keys, kind=kind, reverse=reverse)
   3523 with self.index_mode("freeze"):
   3524     for name, col in self.columns.items():
   3525         # Make a new sorted column.  This requires that take() also copies
   3526         # relevant info attributes for mixin columns.

File /scratch/lbeiske/envs/cta/lib/python3.9/site-packages/astropy/table/table.py:3433, in Table.argsort(self, keys, kind, reverse)
   3431 # use index sorted order if possible
   3432 if keys is not None:
-> 3433     index = get_index(self, names=keys)
   3434     if index is not None:
   3435         idx = np.asarray(index.sorted_data())

File /scratch/lbeiske/envs/cta/lib/python3.9/site-packages/astropy/table/index.py:649, in get_index(table, table_copy, names)
    646     names = set(table_copy.colnames)
    648 if not names <= set(table.colnames):
--> 649     raise ValueError(f"{names} is not a subset of table columns")
    651 for name in names:
    652     for index in table[name].info.indices:

ValueError: {'__index__'} is not a subset of table columns
```

**To Reproduce**
```python
from ctapipe.io import TableLoader

loader = TableLoader("path/to/file", load_observation_info=True)
events = loader.read_subarray_events()
```

**Expected behavior**
TableLoader should be able to load subarray events, while including the observation info. 

**Supporting information**
Reading telescope events works fine.
```python
TableLoader("path/to/file", load_observation_info=True).read_telescope_events()
```

Pull request:
Only remove sort index from table if input did not contain it
fixes #2274

Patch:
diff --git a/ctapipe/io/tableloader.py b/ctapipe/io/tableloader.py
index 5d5add27..f7077969 100644
--- a/ctapipe/io/tableloader.py
+++ b/ctapipe/io/tableloader.py
@@ -310,19 +310,23 @@ class TableLoader(Component):
         return table
 
     @staticmethod
-    def _sort_to_original_order(table, include_tel_id=False):
+    def _sort_to_original_order(table, include_tel_id=False, keep_index=False):
         if len(table) == 0:
             return
         if include_tel_id:
             table.sort(("__index__", "tel_id"))
         else:
             table.sort("__index__")
-        table.remove_column("__index__")
+
+        if not keep_index:
+            table.remove_column("__index__")
 
     @staticmethod
     def _add_index_if_needed(table):
         if "__index__" not in table.colnames:
             table["__index__"] = np.arange(len(table))
+            return True
+        return False
 
     def read_simulation_configuration(self):
         """
@@ -350,7 +354,7 @@ class TableLoader(Component):
         obs_table["obs_id"] = obs_table["obs_id"].astype(table["obs_id"].dtype)
 
         # to be able to sort to original table order
-        self._add_index_if_needed(table)
+        index_added = self._add_index_if_needed(table)
 
         joint = join_allow_empty(
             table,
@@ -360,8 +364,9 @@ class TableLoader(Component):
         )
 
         # sort back to original order and remove index col
-        self._sort_to_original_order(joint)
-        del table["__index__"]
+        self._sort_to_original_order(joint, keep_index=not index_added)
+        if index_added:
+            table.remove_column("__index__")
         return joint
 
     def read_subarray_events(self, start=None, stop=None, keep_order=True):
diff --git a/docs/changes/2288.bugfix.rst b/docs/changes/2288.bugfix.rst
new file mode 100644
index 00000000..49e02fed
--- /dev/null
+++ b/docs/changes/2288.bugfix.rst
@@ -0,0 +1,2 @@
+Fix ``TableLoader.read_subarray_events`` raising an exception when
+``load_observation_info=True``.


Test patch:
diff --git a/ctapipe/io/tests/test_table_loader.py b/ctapipe/io/tests/test_table_loader.py
index f946c9eb..864a53ba 100644
--- a/ctapipe/io/tests/test_table_loader.py
+++ b/ctapipe/io/tests/test_table_loader.py
@@ -415,13 +415,17 @@ def test_order_merged():
 
     path = get_dataset_path("gamma_diffuse_dl2_train_small.dl2.h5")
 
+    trigger = read_table(path, "/dl1/event/subarray/trigger")
     tel_trigger = read_table(path, "/dl1/event/telescope/trigger")
     with TableLoader(
         path,
-        load_dl1_parameters=False,
+        load_dl1_parameters=True,
         load_dl2=True,
         load_observation_info=True,
     ) as loader:
+        events = loader.read_subarray_events()
+        check_equal_array_event_order(events, trigger)
+
         tables = loader.read_telescope_events_by_id()
 
         for tel_id, table in tables.items():