Repository: graphnet-team/graphnet
Issue: Crash in `01_icetray/01_convert_i3_files.py` example script

**Describe the bug**
As part of openjournals/joss-reviews#4971, I’m trying out the example scripts in the Docker image and have discovered a crash in the example script `01_icetray/01_convert_i3_files.py` when running with the arguments `parquet icecube-upgrade`.

Additionally, the output directory `{EXAMPLE_OUTPUT_DIR}/convert_i3_files/ic86/` is used for both values of `detector` (`icecube-86` and `icecube-upgrade`), which seems like it might not be intended?

**To Reproduce**
Steps to reproduce the behavior:
1. Run Docker image as `docker run --rm -it asogaard/graphnet:main`
2. In the container, run example script: `python examples/01_icetray/01_convert_i3_files.py parquet icecube-upgrade`
3. See error

**Expected behavior**
I expect the script to run successfully and produce the expected output files, as it does when running `python examples/01_icetray/01_convert_i3_files.py parquet icecube-86`.

**Full traceback**
```bash
🐳 graphnet@fccc2bd071dd ❯ ~/graphnet $ python examples/01_icetray/01_convert_i3_files.py parquet icecube-upgrade
graphnet: INFO     2023-01-30 13:04:00 - get_logger - Writing log to logs/graphnet_20230130-130400.log
/usr/local/lib/python3.8/dist-packages/pandas/core/computation/expressions.py:20: UserWarning: Pandas requires version '2.7.3' or newer of 'numexpr' (version '2.7.1' currently installed).
  from pandas.core.computation.check import NUMEXPR_INSTALLED
graphnet: INFO     2023-01-30 13:04:03 - ParquetDataConverter.info - Saving results to /root/graphnet/data/examples/outputs/convert_i3_files/ic86
graphnet: INFO     2023-01-30 13:04:03 - ParquetDataConverter.info - Processing 1 I3 file(s) in main thread (not multiprocessing)
  0%|                                                                                                                                                                                                                         | 0/1 [00:12<?, ?file(s)/s]
Traceback (most recent call last):
  File "examples/01_icetray/01_convert_i3_files.py", line 119, in <module>
    main_icecube_upgrade(args.backend)
  File "examples/01_icetray/01_convert_i3_files.py", line 80, in main_icecube_upgrade
    converter(inputs)
  File "/root/graphnet/src/graphnet/data/dataconverter.py", line 215, in __call__
    self.execute(filesets)
  File "/root/graphnet/src/graphnet/data/dataconverter.py", line 286, in execute
    pool = self._iterate_over_individual_files(filesets)
  File "/root/graphnet/src/graphnet/data/dataconverter.py", line 330, in _iterate_over_individual_files
    for _ in map_fn(
  File "/root/graphnet/src/graphnet/data/dataconverter.py", line 83, in wrapper
    output_file = process_method(self, *args)
  File "/root/graphnet/src/graphnet/data/dataconverter.py", line 381, in _process_file
    self.save_data(data, output_file)
  File "/root/graphnet/src/graphnet/data/parquet/parquet_dataconverter.py", line 32, in save_data
    awkward.to_parquet(awkward.from_iter(data), output_file)
  File "/usr/local/lib/python3.8/dist-packages/awkward/operations/convert.py", line 3071, in to_parquet
    writer = pyarrow.parquet.ParquetWriter(**options)
  File "/usr/local/lib/python3.8/dist-packages/pyarrow/parquet/core.py", line 990, in __init__
    self.writer = _parquet.ParquetWriter(
  File "pyarrow/_parquet.pyx", line 1753, in pyarrow._parquet.ParquetWriter.__cinit__
  File "pyarrow/error.pxi", line 144, in pyarrow.lib.pyarrow_internal_check_status
  File "pyarrow/error.pxi", line 100, in pyarrow.lib.check_status
pyarrow.lib.ArrowInvalid: NullType Arrow field must be nullable
```

**Additional context**
n/a


Changed files:
src/graphnet/data/extractors/i3featureextractor.py, src/graphnet/constants.py, examples/01_icetray/01_convert_i3_files.py, src/graphnet/utilities/logging.py, src/graphnet/data/extractors/i3retroextractor.py

diff --git a/examples/01_icetray/01_convert_i3_files.py b/examples/01_icetray/01_convert_i3_files.py
index e9bafaa4..70992497 100644
--- a/examples/01_icetray/01_convert_i3_files.py
+++ b/examples/01_icetray/01_convert_i3_files.py
@@ -56,19 +56,15 @@ def main_icecube_upgrade(backend: str) -> None:
     assert backend in CONVERTER_CLASS
 
     inputs = [f"{TEST_DATA_DIR}/i3/upgrade_genie_step4_140028_000998"]
-    outdir = f"{EXAMPLE_OUTPUT_DIR}/convert_i3_files/ic86"
+    outdir = f"{EXAMPLE_OUTPUT_DIR}/convert_i3_files/upgrade"
     workers = 1
 
     converter: DataConverter = CONVERTER_CLASS[backend](
         [
             I3TruthExtractor(),
             I3RetroExtractor(),
-            I3FeatureExtractorIceCubeUpgrade(
-                "I3RecoPulseSeriesMapRFCleaned_mDOM"
-            ),
-            I3FeatureExtractorIceCubeUpgrade(
-                "I3RecoPulseSeriesMapRFCleaned_DEgg"
-            ),
+            I3FeatureExtractorIceCubeUpgrade("I3RecoPulseSeriesMap_mDOM"),
+            I3FeatureExtractorIceCubeUpgrade("I3RecoPulseSeriesMap_DEgg"),
         ],
         outdir,
         workers=workers,
diff --git a/src/graphnet/constants.py b/src/graphnet/constants.py
index ba991977..9e360bef 100644
--- a/src/graphnet/constants.py
+++ b/src/graphnet/constants.py
@@ -27,7 +27,7 @@ TEST_PARQUET_DATA = os.path.join(
 
 # Example data
 EXAMPLE_DATA_DIR = os.path.join(DATA_DIR, "examples")
-EXAMPLE_OUTPUT_DIR = os.path.join(EXAMPLE_DATA_DIR, "outputs")
+EXAMPLE_OUTPUT_DIR = os.path.join(EXAMPLE_DATA_DIR, "output")
 
 # Configuration files
 CONFIG_DIR = os.path.join(GRAPHNET_ROOT_DIR, "configs")
diff --git a/src/graphnet/data/extractors/i3featureextractor.py b/src/graphnet/data/extractors/i3featureextractor.py
index c3465a76..6e489fb7 100644
--- a/src/graphnet/data/extractors/i3featureextractor.py
+++ b/src/graphnet/data/extractors/i3featureextractor.py
@@ -6,6 +6,7 @@ from graphnet.data.extractors.utilities.frames import (
     get_om_keys_and_pulseseries,
 )
 from graphnet.utilities.imports import has_icecube_package
+from graphnet.utilities.logging import warn_once
 
 if has_icecube_package() or TYPE_CHECKING:
     from icecube import (
@@ -70,6 +71,7 @@ class I3FeatureExtractorIceCube86(I3FeatureExtractor):
                 self._calibration,
             )
         else:
+            warn_once(self, f"Pulsemap {self._pulsemap} not found in frame.")
             return output
 
         # Added these :
@@ -202,6 +204,7 @@ class I3FeatureExtractorIceCubeUpgrade(I3FeatureExtractorIceCube86):
                 self._calibration,
             )
         else:
+            warn_once(self, f"Pulsemap {self._pulsemap} not found in frame.")
             return output
 
         for om_key in om_keys:
@@ -258,6 +261,7 @@ class I3PulseNoiseTruthFlagIceCubeUpgrade(I3FeatureExtractorIceCube86):
                 self._calibration,
             )
         else:
+            warn_once(self, f"Pulsemap {self._pulsemap} not found in frame.")
             return output
 
         for om_key in om_keys:
diff --git a/src/graphnet/data/extractors/i3retroextractor.py b/src/graphnet/data/extractors/i3retroextractor.py
index da6e1f8b..b63b0d08 100644
--- a/src/graphnet/data/extractors/i3retroextractor.py
+++ b/src/graphnet/data/extractors/i3retroextractor.py
@@ -7,6 +7,7 @@ from graphnet.data.extractors.utilities.frames import (
     frame_is_montecarlo,
     frame_is_noise,
 )
+from graphnet.utilities.logging import warn_once
 
 if TYPE_CHECKING:
     from icecube import icetray  # pyright: reportMissingImports=false
@@ -74,6 +75,8 @@ class I3RetroExtractor(I3Extractor):
                     ].value,
                 }
             )
+        else:
+            warn_once(self, "RETRO variables not found in frame.")
 
         if self._frame_contains_classifiers(frame):
             classifiers = [
diff --git a/src/graphnet/utilities/logging.py b/src/graphnet/utilities/logging.py
index 64385edc..7ef1f814 100644
--- a/src/graphnet/utilities/logging.py
+++ b/src/graphnet/utilities/logging.py
@@ -57,10 +57,10 @@ def get_formatters() -> Tuple[logging.Formatter, colorlog.ColoredFormatter]:
 @lru_cache(1)
 def warn_once(logger: logging.Logger, message: str) -> None:
     """Print `message` as warning exactly once."""
-    logger.warn(message)
+    logger.warning(message)
 
 
-class RepeatFilter(object):
+class RepeatFilter(logging.Filter):
     """Filter out repeat messages."""
 
     def __init__(self) -> None:
@@ -148,12 +148,12 @@ def get_logger(
 class LoggerMixin(object):
     """Class for enabling logging directly from inheriting classes."""
 
-    def _get_logger(self) -> logging.Logger:
+    def _get_logger(self) -> logging.LoggerAdapter:
         """Construct Logger instance if not already done."""
         if not hasattr(self, "_logger"):
-            logger = colorlog.getLogger(LOGGER_NAME)
+            logger_base = colorlog.getLogger(LOGGER_NAME)
             logger = logging.LoggerAdapter(
-                logger, extra={"className": self.__class__.__name__ + "."}
+                logger_base, extra={"className": self.__class__.__name__ + "."}
             )
             self._logger = logger
         return self._logger