Repository: graphnet-team/graphnet
Issue: I3PulseNoiseTruthFlagIceCubeUpgrade doesn't work anymore

**Describe the bug**
`I3PulseNoiseTruthFlagIceCubeUpgrade` fails because of inheritance to `I3FeatureExtractorIceCube86 `. 

In `__call__` the lines

```
output_icecube86 = super().__call__(frame)
output.update(output_icecube86)
```
the `I3PulseNoiseTruthFlagIceCubeUpgrade` extractor asks `I3FeatureExtractorIceCube86 ` to retrieve hardware information (DOM position, rde etc) along with pulse information (charge, time, width). However, pulse noise flags are not pulses, and the extraction fails because `pulse.charge` doesn't exist.

**To Reproduce**
Steps to reproduce the behavior:
1. Download the files: `/home/rorsoe/phd/upgrade/test_data`
2. Run this

```

import logging
import os

from graphnet.utilities.logging import get_logger

from graphnet.data.extractors import (
    I3FeatureExtractorIceCubeUpgrade,
    I3TruthExtractor,
    I3GenericExtractor,
    I3PulseNoiseTruthFlagIceCubeUpgrade,
)

from graphnet.data.sqlite import SQLiteDataConverter

logger = get_logger(level=logging.INFO)

CONVERTER_CLASS = {
    "sqlite": SQLiteDataConverter,
    "parquet": ParquetDataConverter,
}


def main_icecube_upgrade(backend: str):
    """Convert IceCube-Upgrade I3 files to intermediate `backend` format."""
    # Check(s)
    assert backend in CONVERTER_CLASS
    database_name = 'dev_step4_upgrade_0028_numu_nutau_01.11.202_test_debug_v2'
    inputs = ["/home/rorsoe/phd/upgrade/test_data"]
    outdir = f"debug_test/{database_name}"
    workers = 3
    converter = CONVERTER_CLASS[backend](
        [
            I3TruthExtractor(),
          I3FeatureExtractorIceCubeUpgrade(
                "SplitInIcePulses"
            ),
          I3PulseNoiseTruthFlagIceCubeUpgrade(
                "SplitInIcePulses_TruthFlags"
            ),
            I3FeatureExtractorIceCubeUpgrade(
              "SplitIceCubePulsesTWSRT"
            ),
          I3PulseNoiseTruthFlagIceCubeUpgrade(
               "SplitIceCubePulsesTWSRT_TruthFlags"
           ),
          I3FeatureExtractorIceCubeUpgrade(
             "SplitInIcePulses_GraphSage_Pulses"
           ),
          I3PulseNoiseTruthFlagIceCubeUpgrade(
               "SplitInIcePulses_GraphSage_Predictions"
            ),
        ],
        outdir + '/tmp',
        workers=workers,
        icetray_verbose=1,
        gcd_rescue = '/data/sim/IceCubeUpgrade/genie/step4/140028/GeoCalibDetectorStatus_ICUpgrade.v58.mixed.V0.i3.bz2'
    )
    os.makedirs(outdir + '/data/')
    converter(inputs)
    if backend == "sqlite":
        converter.merge_files(outdir + '/data/' + database_name)


if __name__ == "__main__":
    backend = "sqlite"
    main_icecube_upgrade(backend)
```

3. See error

**Expected behavior**
`I3PulseNoiseTruthFlagIceCubeUpgrade` should not ask for pulse information. (should run and complete without issue)




Changed files:
src/graphnet/data/sqlite/sqlite_dataconverter.py, src/graphnet/data/extractors/i3featureextractor.py

diff --git a/src/graphnet/data/extractors/i3featureextractor.py b/src/graphnet/data/extractors/i3featureextractor.py
index 86aeb111..4628f26e 100644
--- a/src/graphnet/data/extractors/i3featureextractor.py
+++ b/src/graphnet/data/extractors/i3featureextractor.py
@@ -15,15 +15,15 @@ class I3FeatureExtractor(I3Extractor):
 
 
 class I3FeatureExtractorIceCube86(I3FeatureExtractor):
-    def __call__(self, frame) -> dict:
+    def __call__(self, frame, padding_value=-1) -> dict:
         """Extract features to be used as inputs to GNN models."""
         output = {
             "charge": [],
             "dom_time": [],
+            "width": [],
             "dom_x": [],
             "dom_y": [],
             "dom_z": [],
-            "width": [],
             "pmt_area": [],
             "rde": [],
             "is_bright_dom": [],
@@ -65,35 +65,41 @@ class I3FeatureExtractorIceCube86(I3FeatureExtractor):
             y = self._gcd_dict[om_key].position.y
             z = self._gcd_dict[om_key].position.z
             area = self._gcd_dict[om_key].area
-            rde = self._get_relative_dom_efficiency(frame, om_key)
+            rde = self._get_relative_dom_efficiency(
+                frame, om_key, padding_value
+            )
 
             # DOM flags
             if bright_doms:
                 is_bright_dom = 1 if om_key in bright_doms else 0
             else:
-                is_bright_dom = -1
+                is_bright_dom = padding_value
 
             if bad_doms:
                 is_bad_dom = 1 if om_key in bad_doms else 0
             else:
-                is_bad_dom = -1
+                is_bad_dom = padding_value
 
             if saturation_windows:
                 is_saturated_dom = 1 if om_key in saturation_windows else 0
             else:
-                is_saturated_dom = -1
+                is_saturated_dom = padding_value
 
             if calibration_errata:
                 is_errata_dom = 1 if om_key in calibration_errata else 0
             else:
-                is_errata_dom = -1
+                is_errata_dom = padding_value
 
             # Loop over pulses for each OM
             pulses = data[om_key]
             for pulse in pulses:
-                output["charge"].append(pulse.charge)
-                output["dom_time"].append(pulse.time)
-                output["width"].append(pulse.width)
+                output["charge"].append(
+                    getattr(pulse, "charge", padding_value)
+                )
+                output["dom_time"].append(
+                    getattr(pulse, "time", padding_value)
+                )
+                output["width"].append(getattr(pulse, "width", padding_value))
                 output["pmt_area"].append(area)
                 output["rde"].append(rde)
                 output["dom_x"].append(x)
@@ -108,7 +114,7 @@ class I3FeatureExtractorIceCube86(I3FeatureExtractor):
 
         return output
 
-    def _get_relative_dom_efficiency(self, frame, om_key):
+    def _get_relative_dom_efficiency(self, frame, om_key, padding_value):
         if (
             "I3Calibration" in frame
         ):  # Not available for e.g. mDOMs in IceCube Upgrade
@@ -117,7 +123,7 @@ class I3FeatureExtractorIceCube86(I3FeatureExtractor):
             try:
                 rde = self._calibration.dom_cal[om_key].relative_dom_eff
             except:  # noqa: E722
-                rde = -1
+                rde = padding_value
         return rde
 
 
@@ -174,24 +180,17 @@ class I3FeatureExtractorIceCubeUpgrade(I3FeatureExtractorIceCube86):
         return output
 
 
-class I3PulseNoiseTruthFlagIceCubeUpgrade(I3FeatureExtractorIceCube86):
+class I3PulseNoiseTruthFlagIceCubeUpgrade(I3FeatureExtractorIceCubeUpgrade):
     def __call__(self, frame) -> dict:
         """Extract features to be used as inputs to GNN models."""
 
         output = {
-            "string": [],
-            "pmt_number": [],
-            "dom_number": [],
-            "pmt_dir_x": [],
-            "pmt_dir_y": [],
-            "pmt_dir_z": [],
-            "dom_type": [],
             "truth_flag": [],
         }
 
-        # Add features from IceCube86
-        output_icecube86 = super().__call__(frame)
-        output.update(output_icecube86)
+        # Add features from IceCubeUpgrade
+        output_icecube_upgrade = super().__call__(frame)
+        output.update(output_icecube_upgrade)
 
         # Get OM data
         om_keys, data = get_om_keys_and_pulseseries(
@@ -201,25 +200,9 @@ class I3PulseNoiseTruthFlagIceCubeUpgrade(I3FeatureExtractorIceCube86):
         )
 
         for om_key in om_keys:
-            # Common values for each OM
-            pmt_dir_x = self._gcd_dict[om_key].orientation.x
-            pmt_dir_y = self._gcd_dict[om_key].orientation.y
-            pmt_dir_z = self._gcd_dict[om_key].orientation.z
-            string = om_key[0]
-            dom_number = om_key[1]
-            pmt_number = om_key[2]
-            dom_type = self._gcd_dict[om_key].omtype
-
             # Loop over pulses for each OM
             pulses = data[om_key]
             for truth_flag in pulses:
-                output["string"].append(string)
-                output["pmt_number"].append(pmt_number)
-                output["dom_number"].append(dom_number)
-                output["pmt_dir_x"].append(pmt_dir_x)
-                output["pmt_dir_y"].append(pmt_dir_y)
-                output["pmt_dir_z"].append(pmt_dir_z)
-                output["dom_type"].append(dom_type)
                 output["truth_flag"].append(truth_flag)
 
         return output
diff --git a/src/graphnet/data/sqlite/sqlite_dataconverter.py b/src/graphnet/data/sqlite/sqlite_dataconverter.py
index 4b42765e..eabf2311 100644
--- a/src/graphnet/data/sqlite/sqlite_dataconverter.py
+++ b/src/graphnet/data/sqlite/sqlite_dataconverter.py
@@ -255,8 +255,8 @@ def construct_dataframe(extraction: Dict[str, Any]) -> pd.DataFrame:
 
 
 def is_pulsemap_check(table_name: str) -> bool:
-    """Check whether `table_name` corresponds to a pulsemap, and not a truth or RETRO table."""
-    if "retro" in table_name.lower() or "truth" in table_name.lower():
-        return False
-    else:  # Could have to include the lower case word 'pulse'?
+    """Check whether `table_name` corresponds to a pulsemap."""
+    if "pulse" in table_name.lower():
         return True
+    else:
+        return False