Repository: cta-observatory/ctapipe
Pull request #2190 (2023-01-06 11:13:05)

Refactor initialization of telescope parameters, move to own module
I came across code I didn't first understand in the initialization of the `TelescopeParameter` traits, trying to improve the code, I ran into a circular import problem (as the straight forward code would need to check `isinstance(trait, TelescopeParameter` and `TelescopeComponent` is in a module imported by `ctapipe.core.traits`).

So the best solution was to move the `TelescopeComponent` / `TelescopeParameter` related things into their own submodule `ctapipe.core.telescope_component`.

I think this is an improvement.

The main change (aside from moving things around) is replacing this code:

```python
# configure all of the TelescopeParameters
for trait in list(self.class_traits()):
    try:
        getattr(self, trait).attach_subarray(subarray)
    except (AttributeError, TypeError):
        pass
```

with

```python
# configure all of the TelescopeParameters
for attr, trait in self.class_traits().items():
    if not isinstance(trait, TelescopeParameter):
        continue

    # trait is the TelescopeParameter descriptor at the class,
    # need to get the value at the instance, which will be a TelescopePatternList
    pattern_list = getattr(self, attr)
    if pattern_list is not None:
        pattern_list.attach_subarray(subarray)
```

which is I think much clearer.

This also has the additional benefit of not silently ignoring errors that could potentially happen in `attach_subarray`.


Patch:
diff --git a/ctapipe/core/__init__.py b/ctapipe/core/__init__.py
index 14c93723..f83471c8 100644
--- a/ctapipe/core/__init__.py
+++ b/ctapipe/core/__init__.py
@@ -3,11 +3,12 @@
 Core functionality of ctapipe
 """
 
-from .component import Component, TelescopeComponent, non_abstract_children
+from .component import Component, non_abstract_children
 from .container import Container, DeprecatedField, Field, FieldValidationError, Map
 from .feature_generator import FeatureGenerator
 from .provenance import Provenance, get_module_version
 from .qualityquery import QualityCriteriaError, QualityQuery
+from .telescope_component import TelescopeComponent
 from .tool import Tool, ToolConfigurationError, run_tool
 
 __all__ = [
diff --git a/ctapipe/core/component.py b/ctapipe/core/component.py
index 3f0275fd..09294c38 100644
--- a/ctapipe/core/component.py
+++ b/ctapipe/core/component.py
@@ -1,17 +1,14 @@
 """ Class to handle configuration for algorithms """
+import warnings
 from abc import ABCMeta
-from inspect import isabstract
+from inspect import cleandoc, isabstract
 from logging import getLogger
-from docutils.core import publish_parts
-from inspect import cleandoc
 
+from docutils.core import publish_parts
 from traitlets import TraitError
 from traitlets.config import Configurable
 
-import warnings
-
-
-__all__ = ["non_abstract_children", "Component", "TelescopeComponent"]
+__all__ = ["non_abstract_children", "Component"]
 
 
 def find_config_in_hierarchy(parent, class_name, trait_name):
@@ -261,55 +258,3 @@ class Component(Configurable, metaclass=AbstractConfigurableMeta):
         lines.append("</table>")
         lines.append("</div>")
         return "\n".join(lines)
-
-
-class TelescopeComponent(Component):
-    """
-    A component that needs a `~ctapipe.instrument.SubarrayDescription` to be constructed,
-    and which contains configurable `~ctapipe.core.traits.TelescopeParameter` fields
-    that must be configured on construction.
-    """
-
-    def __init__(self, subarray, config=None, parent=None, **kwargs):
-        super().__init__(config, parent, **kwargs)
-
-        self.subarray = subarray
-        # configure all of the TelescopeParameters
-        for trait in list(self.class_traits()):
-            try:
-                getattr(self, trait).attach_subarray(subarray)
-            except (AttributeError, TypeError):
-                pass
-
-    @classmethod
-    def from_name(cls, name, subarray, config=None, parent=None, **kwargs):
-        """
-        Obtain an instance of a subclass via its name
-
-        Parameters
-        ----------
-        name : str
-            Name of the subclass to obtain
-        subarray: ctapipe.instrument.SubarrayDescription
-            The current subarray for this TelescopeComponent.
-        config : traitlets.loader.Config
-            Configuration specified by config file or cmdline arguments.
-            Used to set traitlet values.
-            This argument is typically only specified when using this method
-            from within a Tool.
-        parent : ctapipe.core.Tool
-            Tool executable that is calling this component.
-            Passes the correct logger and configuration to the component.
-            This argument is typically only specified when using this method
-            from within a Tool (config need not be passed if parent is used).
-        kwargs
-
-        Returns
-        -------
-        instace
-            Instance of subclass to this class
-        """
-        requested_subclass = cls.non_abstract_subclasses()[name]
-        return requested_subclass(
-            subarray=subarray, config=config, parent=parent, **kwargs
-        )
diff --git a/ctapipe/core/telescope_component.py b/ctapipe/core/telescope_component.py
new file mode 100644
index 00000000..93f09f1d
--- /dev/null
+++ b/ctapipe/core/telescope_component.py
@@ -0,0 +1,383 @@
+"""
+This module defines classes to enable per-telescope configuration of trait values
+"""
+import copy
+import logging
+from collections import UserList
+from fnmatch import fnmatch
+from typing import Optional, Union
+
+import numpy as np
+from traitlets import List, TraitError, TraitType, Undefined
+
+from .component import Component
+
+__all__ = [
+    "TelescopeComponent",
+    "TelescopeParameter",
+    "TelescopeParameterLookup",
+    "TelescopePatternList",
+]
+
+
+logger = logging.getLogger(__name__)
+
+
+class TelescopeComponent(Component):
+    """
+    A component that needs a `~ctapipe.instrument.SubarrayDescription` to be constructed,
+    and which contains configurable `~ctapipe.core.telescope_component.TelescopeParameter`
+    fields that must be configured on construction.
+    """
+
+    def __init__(self, subarray, config=None, parent=None, **kwargs):
+        super().__init__(config, parent, **kwargs)
+        self.subarray = subarray
+
+        # configure all of the TelescopeParameters
+        for attr, trait in self.class_traits().items():
+            if not isinstance(trait, TelescopeParameter):
+                continue
+
+            # trait is the TelescopeParameter descriptor at the class,
+            # need to get the value at the instance, which will be a TelescopePatternList
+            pattern_list = getattr(self, attr)
+            if pattern_list is not None:
+                pattern_list.attach_subarray(subarray)
+
+    @classmethod
+    def from_name(cls, name, subarray, config=None, parent=None, **kwargs):
+        """
+        Obtain an instance of a subclass via its name
+
+        Parameters
+        ----------
+        name : str
+            Name of the subclass to obtain
+        subarray: ctapipe.instrument.SubarrayDescription
+            The current subarray for this TelescopeComponent.
+        config : traitlets.loader.Config
+            Configuration specified by config file or cmdline arguments.
+            Used to set traitlet values.
+            This argument is typically only specified when using this method
+            from within a Tool.
+        parent : ctapipe.core.Tool
+            Tool executable that is calling this component.
+            Passes the correct logger and configuration to the component.
+            This argument is typically only specified when using this method
+            from within a Tool (config need not be passed if parent is used).
+        kwargs
+            Are passed to the subclass
+
+        Returns
+        -------
+        Instance
+            Instance of subclass to this class
+        """
+        requested_subclass = cls.non_abstract_subclasses()[name]
+        return requested_subclass(
+            subarray=subarray, config=config, parent=parent, **kwargs
+        )
+
+
+class TelescopePatternList(UserList):
+    """
+    Representation for a list of telescope pattern tuples. This is a helper class
+    used  by the Trait TelescopeParameter as its value type
+    """
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self._lookup = None
+        self._subarray = None
+
+        for i in range(len(self)):
+            self[i] = self.single_to_pattern(self[i])
+
+    @property
+    def tel(self):
+        """access the value per telescope_id, e.g. ``param.tel[2]``"""
+        if self._lookup:
+            return self._lookup
+        else:
+            raise RuntimeError(
+                "No TelescopeParameterLookup was registered. You must "
+                "call attach_subarray() first"
+            )
+
+    @staticmethod
+    def single_to_pattern(value):
+        # make sure we only change things that are not already a
+        # pattern tuple
+        if (
+            not isinstance(value, tuple)
+            or len(value) != 3
+            or value[0] not in {"type", "id"}
+        ):
+            return ["type", "*", value]
+
+        return value
+
+    def append(self, value):
+        """Validate and then append a new value"""
+        super().append(self.single_to_pattern(value))
+
+    def attach_subarray(self, subarray):
+        """
+        Register a SubarrayDescription so that the user-specified values can be
+        looked up by tel_id. This must be done before using the ``.tel[x]`` property
+        """
+        self._subarray = subarray
+        self._lookup.attach_subarray(subarray)
+
+
+class TelescopeParameterLookup:
+    def __init__(self, telescope_parameter_list):
+        """
+        Handles the lookup of corresponding configuration value from a list of
+        tuples for a tel_id.
+
+        Parameters
+        ----------
+        telescope_parameter_list : list
+            List of tuples in the form `[(command, argument, value), ...]`
+        """
+        self._telescope_parameter_list = copy.deepcopy(telescope_parameter_list)
+        self._value_for_tel_id = None
+        self._value_for_type = None
+        self._subarray = None
+        self._subarray_global_value = Undefined
+        self._type_strs = None
+        for param in telescope_parameter_list:
+            if param[1] == "*":
+                self._subarray_global_value = param[2]
+
+    def attach_subarray(self, subarray):
+        """
+        Prepare the TelescopeParameter by informing it of the
+        subarray description
+
+        Parameters
+        ----------
+        subarray: ctapipe.instrument.SubarrayDescription
+            Description of the subarray
+            (includes mapping of tel_id to tel_type)
+        """
+        self._subarray = subarray
+        self._value_for_tel_id = {}
+        self._value_for_type = {}
+        self._type_strs = {str(tel) for tel in self._subarray.telescope_types}
+        for command, arg, value in self._telescope_parameter_list:
+            if command == "type":
+                matched_tel_types = [
+                    str(t) for t in subarray.telescope_types if fnmatch(str(t), arg)
+                ]
+                logger.debug(f"argument '{arg}' matched: {matched_tel_types}")
+
+                if len(matched_tel_types) == 0:
+                    logger.warning(
+                        "TelescopeParameter type argument '%s' did not match "
+                        "any known telescope types",
+                        arg,
+                    )
+
+                for tel_type in matched_tel_types:
+                    self._value_for_type[tel_type] = value
+                    for tel_id in subarray.get_tel_ids_for_type(tel_type):
+                        self._value_for_tel_id[tel_id] = value
+            elif command == "id":
+                self._value_for_tel_id[int(arg)] = value
+            else:
+                raise ValueError(f"Unrecognized command: {command}")
+
+    def __getitem__(self, tel: Optional[Union[int, str]]):
+        """
+        Returns the resolved parameter for the given telescope id
+        """
+        if tel is None:
+            if self._subarray_global_value is not Undefined:
+                return self._subarray_global_value
+
+            raise KeyError("No subarray global value set for TelescopeParameter")
+
+        if self._value_for_tel_id is None:
+            raise ValueError(
+                "TelescopeParameterLookup: No subarray attached, call "
+                "`attach_subarray` first before trying to access a value by tel_id"
+            )
+
+        if isinstance(tel, (int, np.integer)):
+            try:
+                return self._value_for_tel_id[tel]
+            except KeyError:
+                raise KeyError(
+                    f"TelescopeParameterLookup: no "
+                    f"parameter value was set for telescope with tel_id="
+                    f"{tel}. Please set it explicitly, "
+                    f"or by telescope type or '*'."
+                )
+
+        from ctapipe.instrument import TelescopeDescription
+
+        if isinstance(tel, TelescopeDescription):
+            tel = str(tel)
+
+        if isinstance(tel, str):
+            if tel not in self._type_strs:
+                raise ValueError(
+                    f"Unknown telescope type {tel}, known: {self._type_strs}"
+                )
+            try:
+                return self._value_for_type[tel]
+            except KeyError:
+                raise KeyError(
+                    "TelescopeParameterLookup: no "
+                    "parameter value was set for telescope type"
+                    f" '{tel}'. Please set explicitly or using '*'."
+                )
+
+        raise TypeError(f"Unsupported lookup type: {type(tel)}")
+
+
+class TelescopeParameter(List):
+    """
+    Allow a parameter value to be specified as a simple value (of type *dtype*),
+    or as a list of patterns that match different telescopes.
+
+    The patterns are given as a list of 3-tuples in the
+    form: ``[(command, argument, value), ...]``.
+
+    Command can be one of:
+
+    - ``'type'``: argument is then a telescope type  string (e.g.
+      ``('type', 'SST_ASTRI_CHEC', 4.0)`` to apply to all telescopes of that type,
+      or use a wildcard like "LST*", or "*" to set a pure default value for all
+      telescopes.
+    - ``'id'``:  argument is a specific telescope ID ``['id', 89, 5.0]``)
+
+    These are evaluated in-order, so you can first set a default value, and then set
+    values for specific telescopes or types to override them.
+
+    Examples
+    --------
+
+    .. code-block: python
+
+        tel_param = [
+            ('type', '*', 5.0),                       # default for all
+            ('type', 'LST_*', 5.2),
+            ('type', 'MST_MST_NectarCam', 4.0),
+            ('type', 'MST_MST_FlashCam', 4.5),
+            ('id', 34, 4.0),                   # override telescope 34 specifically
+        ]
+
+    .. code-block: python
+
+        tel_param = 4.0  # sets this value for all telescopes
+
+    """
+
+    klass = TelescopePatternList
+    _valid_defaults = (object,)  # allow everything, we validate the default ourselves
+
+    def __init__(self, trait, default_value=Undefined, **kwargs):
+        """
+        Create a new TelescopeParameter
+        """
+        self._help = ""
+
+        if not isinstance(trait, TraitType):
+            raise TypeError("trait must be a TraitType instance")
+
+        self._trait = trait
+        self.allow_none = kwargs.get("allow_none", False)
+        if default_value != Undefined:
+            default_value = self.validate(self, default_value)
+
+        if "help" not in kwargs:
+            self.help = "A TelescopeParameter"
+
+        super().__init__(default_value=default_value, **kwargs)
+
+    @property
+    def help(self):
+        sep = "." if not self._help.endswith(".") else ""
+        return f"{self._help}{sep} {self._trait.help}"
+
+    @help.setter
+    def help(self, value):
+        self._help = value
+
+    def from_string(self, s):
+        val = super().from_string(s)
+        # for strings, parsing fails and traitlets returns None
+        if val == [("type", "*", None)] and s != "None":
+            val = [("type", "*", self._trait.from_string(s))]
+        return val
+
+    def _validate_entry(self, obj, value):
+        if value is None and self.allow_none is True:
+            return None
+        return self._trait.validate(obj, value)
+
+    def validate(self, obj, value):
+
+        # Support a single value for all (check and convert into a default value)
+        if not isinstance(value, (list, List, UserList, TelescopePatternList)):
+            value = [("type", "*", self._validate_entry(obj, value))]
+
+        # Check each value of list
+        normalized_value = TelescopePatternList()
+
+        for pattern in value:
+
+            # now check for the standard 3-tuple of (command, argument, value)
+            if len(pattern) != 3:
+                raise TraitError(
+                    "pattern should be a tuple of (command, argument, value)"
+                )
+
+            command, arg, val = pattern
+            val = self._validate_entry(obj, val)
+
+            if not isinstance(command, str):
+                raise TraitError("command must be a string")
+
+            if command not in ["type", "id"]:
+                raise TraitError("command must be one of: 'type', 'id'")
+
+            if command == "type":
+                if not isinstance(arg, str):
+                    raise TraitError("'type' argument should be a string")
+
+            if command == "id":
+                try:
+                    arg = int(arg)
+                except ValueError:
+                    raise TraitError(f"Argument of 'id' should be an int (got '{arg}')")
+
+            val = self._validate_entry(obj, val)
+            normalized_value.append((command, arg, val))
+
+        normalized_value._lookup = TelescopeParameterLookup(normalized_value)
+        if isinstance(value, TelescopePatternList) and value._subarray is not None:
+            normalized_value.attach_subarray(value._subarray)
+
+        return normalized_value
+
+    def set(self, obj, value):
+        # Support a single value for all (check and convert into a default value)
+        if not isinstance(value, (list, List, UserList, TelescopePatternList)):
+            value = [("type", "*", self._validate_entry(obj, value))]
+
+        # Retain existing subarray description
+        # when setting new value for TelescopeParameter
+        try:
+            old_value = obj._trait_values[self.name]
+        except KeyError:
+            old_value = self.default_value
+
+        super().set(obj, value)
+
+        if getattr(old_value, "_subarray", None) is not None:
+            obj._trait_values[self.name].attach_subarray(old_value._subarray)
diff --git a/ctapipe/core/traits.py b/ctapipe/core/traits.py
index f4822962..37a073b7 100644
--- a/ctapipe/core/traits.py
+++ b/ctapipe/core/traits.py
@@ -1,21 +1,17 @@
 """
 Traitlet implementations for ctapipe
 """
-import copy
 import os
 import pathlib
-from collections import UserList
-from fnmatch import fnmatch
-from typing import Optional, Union
 from urllib.parse import urlparse
 
-import numpy as np
 import traitlets
 import traitlets.config
 from astropy.time import Time
 from traitlets import Undefined
 
 from .component import Component, non_abstract_children
+from .telescope_component import TelescopeParameter
 
 __all__ = [
     # Implemented here
@@ -23,7 +19,6 @@ __all__ = [
     "BoolTelescopeParameter",
     "IntTelescopeParameter",
     "FloatTelescopeParameter",
-    "TelescopeParameter",
     "classes_with_traits",
     "create_class_enum_trait",
     "has_traits",
@@ -327,311 +322,8 @@ def has_traits(cls, ignore=("config", "parent")):
     return bool(set(cls.class_trait_names()) - set(ignore))
 
 
-class TelescopePatternList(UserList):
-    """
-    Representation for a list of telescope pattern tuples. This is a helper class
-    used  by the Trait TelescopeParameter as its value type
-    """
-
-    def __init__(self, *args):
-        super().__init__(*args)
-        self._lookup = None
-        self._subarray = None
-
-        for i in range(len(self)):
-            self[i] = self.single_to_pattern(self[i])
-
-    @property
-    def tel(self):
-        """access the value per telescope_id, e.g. `param.tel[2]`"""
-        if self._lookup:
-            return self._lookup
-        else:
-            raise RuntimeError(
-                "No TelescopeParameterLookup was registered. You must "
-                "call attach_subarray() first"
-            )
-
-    @staticmethod
-    def single_to_pattern(value):
-        # make sure we only change things that are not already a
-        # pattern tuple
-        if (
-            not isinstance(value, tuple)
-            or len(value) != 3
-            or value[0] not in {"type", "id"}
-        ):
-            return ["type", "*", value]
-
-        return value
-
-    def append(self, value):
-        """Validate and then append a new value"""
-        super().append(self.single_to_pattern(value))
-
-    def attach_subarray(self, subarray):
-        """
-        Register a SubarrayDescription so that the user-specified values can be
-        looked up by tel_id. This must be done before using the `.tel[x]` property
-        """
-        self._subarray = subarray
-        self._lookup.attach_subarray(subarray)
-
-
-class TelescopeParameterLookup:
-    def __init__(self, telescope_parameter_list):
-        """
-        Handles the lookup of corresponding configuration value from a list of
-        tuples for a tel_id.
-
-        Parameters
-        ----------
-        telescope_parameter_list : list
-            List of tuples in the form `[(command, argument, value), ...]`
-        """
-        self._telescope_parameter_list = copy.deepcopy(telescope_parameter_list)
-        self._value_for_tel_id = None
-        self._value_for_type = None
-        self._subarray = None
-        self._subarray_global_value = Undefined
-        self._type_strs = None
-        for param in telescope_parameter_list:
-            if param[1] == "*":
-                self._subarray_global_value = param[2]
-
-    def attach_subarray(self, subarray):
-        """
-        Prepare the TelescopeParameter by informing it of the
-        subarray description
-
-        Parameters
-        ----------
-        subarray: ctapipe.instrument.SubarrayDescription
-            Description of the subarray
-            (includes mapping of tel_id to tel_type)
-        """
-        self._subarray = subarray
-        self._value_for_tel_id = {}
-        self._value_for_type = {}
-        self._type_strs = {str(tel) for tel in self._subarray.telescope_types}
-        for command, arg, value in self._telescope_parameter_list:
-            if command == "type":
-                matched_tel_types = [
-                    str(t) for t in subarray.telescope_types if fnmatch(str(t), arg)
-                ]
-                logger.debug(f"argument '{arg}' matched: {matched_tel_types}")
-
-                if len(matched_tel_types) == 0:
-                    logger.warning(
-                        "TelescopeParameter type argument '%s' did not match "
-                        "any known telescope types",
-                        arg,
-                    )
-
-                for tel_type in matched_tel_types:
-                    self._value_for_type[tel_type] = value
-                    for tel_id in subarray.get_tel_ids_for_type(tel_type):
-                        self._value_for_tel_id[tel_id] = value
-            elif command == "id":
-                self._value_for_tel_id[int(arg)] = value
-            else:
-                raise ValueError(f"Unrecognized command: {command}")
-
-    def __getitem__(self, tel: Optional[Union[int, str]]):
-        """
-        Returns the resolved parameter for the given telescope id
-        """
-        if tel is None:
-            if self._subarray_global_value is not Undefined:
-                return self._subarray_global_value
-
-            raise KeyError("No subarray global value set for TelescopeParameter")
-
-        if self._value_for_tel_id is None:
-            raise ValueError(
-                "TelescopeParameterLookup: No subarray attached, call "
-                "`attach_subarray` first before trying to access a value by tel_id"
-            )
-
-        if isinstance(tel, (int, np.integer)):
-            try:
-                return self._value_for_tel_id[tel]
-            except KeyError:
-                raise KeyError(
-                    f"TelescopeParameterLookup: no "
-                    f"parameter value was set for telescope with tel_id="
-                    f"{tel}. Please set it explicitly, "
-                    f"or by telescope type or '*'."
-                )
-
-        from ctapipe.instrument import TelescopeDescription
-
-        if isinstance(tel, TelescopeDescription):
-            tel = str(tel)
-
-        if isinstance(tel, str):
-            if tel not in self._type_strs:
-                raise ValueError(
-                    f"Unknown telescope type {tel}, known: {self._type_strs}"
-                )
-            try:
-                return self._value_for_type[tel]
-            except KeyError:
-                raise KeyError(
-                    "TelescopeParameterLookup: no "
-                    "parameter value was set for telescope type"
-                    f" '{tel}'. Please set explicitly or using '*'."
-                )
-
-        raise TypeError(f"Unsupported lookup type: {type(tel)}")
-
-
-class TelescopeParameter(List):
-    """
-    Allow a parameter value to be specified as a simple value (of type *dtype*),
-    or as a list of patterns that match different telescopes.
-
-    The patterns are given as a list of 3-tuples in the
-    form: ``[(command, argument, value), ...]``.
-
-    Command can be one of:
-
-    - ``'type'``: argument is then a telescope type  string (e.g.
-      ``('type', 'SST_ASTRI_CHEC', 4.0)`` to apply to all telescopes of that type,
-      or use a wildcard like "LST*", or "*" to set a pure default value for all
-      telescopes.
-    - ``'id'``:  argument is a specific telescope ID ``['id', 89, 5.0]``)
-
-    These are evaluated in-order, so you can first set a default value, and then set
-    values for specific telescopes or types to override them.
-
-    Examples
-    --------
-
-    .. code-block: python
-
-        tel_param = [
-            ('type', '*', 5.0),                       # default for all
-            ('type', 'LST_*', 5.2),
-            ('type', 'MST_MST_NectarCam', 4.0),
-            ('type', 'MST_MST_FlashCam', 4.5),
-            ('id', 34, 4.0),                   # override telescope 34 specifically
-        ]
-
-    .. code-block: python
-
-        tel_param = 4.0  # sets this value for all telescopes
-
-    """
-
-    klass = TelescopePatternList
-    _valid_defaults = (object,)  # allow everything, we validate the default ourselves
-
-    def __init__(self, trait, default_value=Undefined, **kwargs):
-        """
-        Create a new TelescopeParameter
-        """
-        self._help = ""
-
-        if not isinstance(trait, TraitType):
-            raise TypeError("trait must be a TraitType instance")
-
-        self._trait = trait
-        self.allow_none = kwargs.get("allow_none", False)
-        if default_value != Undefined:
-            default_value = self.validate(self, default_value)
-
-        if "help" not in kwargs:
-            self.help = "A TelescopeParameter"
-
-        super().__init__(default_value=default_value, **kwargs)
-
-    @property
-    def help(self):
-        sep = "." if not self._help.endswith(".") else ""
-        return f"{self._help}{sep} {self._trait.help}"
-
-    @help.setter
-    def help(self, value):
-        self._help = value
-
-    def from_string(self, s):
-        val = super().from_string(s)
-        # for strings, parsing fails and traitlets returns None
-        if val == [("type", "*", None)] and s != "None":
-            val = [("type", "*", self._trait.from_string(s))]
-        return val
-
-    def _validate_entry(self, obj, value):
-        if value is None and self.allow_none is True:
-            return None
-        return self._trait.validate(obj, value)
-
-    def validate(self, obj, value):
-
-        # Support a single value for all (check and convert into a default value)
-        if not isinstance(value, (list, List, UserList, TelescopePatternList)):
-            value = [("type", "*", self._validate_entry(obj, value))]
-
-        # Check each value of list
-        normalized_value = TelescopePatternList()
-
-        for pattern in value:
-
-            # now check for the standard 3-tuple of (command, argument, value)
-            if len(pattern) != 3:
-                raise TraitError(
-                    "pattern should be a tuple of (command, argument, value)"
-                )
-
-            command, arg, val = pattern
-            val = self._validate_entry(obj, val)
-
-            if not isinstance(command, str):
-                raise TraitError("command must be a string")
-
-            if command not in ["type", "id"]:
-                raise TraitError("command must be one of: 'type', 'id'")
-
-            if command == "type":
-                if not isinstance(arg, str):
-                    raise TraitError("'type' argument should be a string")
-
-            if command == "id":
-                try:
-                    arg = int(arg)
-                except ValueError:
-                    raise TraitError(f"Argument of 'id' should be an int (got '{arg}')")
-
-            val = self._validate_entry(obj, val)
-            normalized_value.append((command, arg, val))
-            normalized_value._lookup = TelescopeParameterLookup(normalized_value)
-
-            if isinstance(value, TelescopePatternList) and value._subarray is not None:
-                normalized_value.attach_subarray(value._subarray)
-
-        return normalized_value
-
-    def set(self, obj, value):
-        # Support a single value for all (check and convert into a default value)
-        if not isinstance(value, (list, List, UserList, TelescopePatternList)):
-            value = [("type", "*", self._validate_entry(obj, value))]
-
-        # Retain existing subarray description
-        # when setting new value for TelescopeParameter
-        try:
-            old_value = obj._trait_values[self.name]
-        except KeyError:
-            old_value = self.default_value
-
-        super().set(obj, value)
-
-        if getattr(old_value, "_subarray", None) is not None:
-            obj._trait_values[self.name].attach_subarray(old_value._subarray)
-
-
 class FloatTelescopeParameter(TelescopeParameter):
-    """a `~ctapipe.core.traits.TelescopeParameter` with Float trait type"""
+    """a `~ctapipe.core.telescope_component.TelescopeParameter` with Float trait type"""
 
     def __init__(self, **kwargs):
         """Create a new FloatTelescopeParameter"""
@@ -639,7 +331,7 @@ class FloatTelescopeParameter(TelescopeParameter):
 
 
 class IntTelescopeParameter(TelescopeParameter):
-    """a `~ctapipe.core.traits.TelescopeParameter` with Int trait type"""
+    """a `~ctapipe.core.telescope_component.TelescopeParameter` with Int trait type"""
 
     def __init__(self, **kwargs):
         """Create a new IntTelescopeParameter"""
@@ -647,7 +339,7 @@ class IntTelescopeParameter(TelescopeParameter):
 
 
 class BoolTelescopeParameter(TelescopeParameter):
-    """a `~ctapipe.core.traits.TelescopeParameter` with Bool trait type"""
+    """a `~ctapipe.core.telescope_component.TelescopeParameter` with Bool trait type"""
 
     def __init__(self, **kwargs):
         """Create a new BoolTelescopeParameter"""
diff --git a/ctapipe/image/cleaning.py b/ctapipe/image/cleaning.py
index 052828b6..a62876da 100644
--- a/ctapipe/image/cleaning.py
+++ b/ctapipe/image/cleaning.py
@@ -28,7 +28,7 @@ from abc import abstractmethod
 
 import numpy as np
 
-from ..core.component import TelescopeComponent
+from ..core import TelescopeComponent
 from ..core.traits import (
     BoolTelescopeParameter,
     FloatTelescopeParameter,
diff --git a/ctapipe/image/modifications.py b/ctapipe/image/modifications.py
index 5a6d4120..3da9bdb5 100644
--- a/ctapipe/image/modifications.py
+++ b/ctapipe/image/modifications.py
@@ -1,9 +1,9 @@
 import numpy as np
 from numba import njit
-from ..core.component import TelescopeComponent
-from ..core.traits import FloatTelescopeParameter, BoolTelescopeParameter, Int
-from ..instrument import SubarrayDescription
 
+from ..core import TelescopeComponent
+from ..core.traits import BoolTelescopeParameter, FloatTelescopeParameter, Int
+from ..instrument import SubarrayDescription
 
 __all__ = [
     "ImageModifier",
diff --git a/docs/ctapipe_api/core/index.rst b/docs/ctapipe_api/core/index.rst
index 480fcf8c..89febb70 100644
--- a/docs/ctapipe_api/core/index.rst
+++ b/docs/ctapipe_api/core/index.rst
@@ -48,6 +48,9 @@ Reference/API
 .. automodapi:: ctapipe.core.component
     :no-inheritance-diagram:
 
+.. automodapi:: ctapipe.core.telescope_component
+    :no-inheritance-diagram:
+
 .. automodapi:: ctapipe.core.tool
     :no-inheritance-diagram:
 


Test patch:
diff --git a/ctapipe/core/tests/test_telescope_component.py b/ctapipe/core/tests/test_telescope_component.py
new file mode 100644
index 00000000..7aae1de4
--- /dev/null
+++ b/ctapipe/core/tests/test_telescope_component.py
@@ -0,0 +1,344 @@
+import tempfile
+from unittest import mock
+
+import pytest
+from traitlets import TraitError
+
+from ctapipe.core import Component, TelescopeComponent
+from ctapipe.core.telescope_component import (
+    TelescopeParameter,
+    TelescopeParameterLookup,
+)
+from ctapipe.core.traits import (
+    Bool,
+    BoolTelescopeParameter,
+    Float,
+    FloatTelescopeParameter,
+    Int,
+    IntTelescopeParameter,
+    Path,
+)
+
+
+@pytest.fixture(scope="module")
+def mock_subarray():
+    subarray = mock.MagicMock()
+    subarray.tel_ids = [1, 2, 3, 4]
+    subarray.get_tel_ids_for_type = (
+        lambda x: [3, 4] if x == "LST_LST_LSTCam" else [1, 2]
+    )
+    subarray.telescope_types = [
+        "LST_LST_LSTCam",
+        "MST_MST_NectarCam",
+        "MST_MST_FlashCam",
+    ]
+    return subarray
+
+
+def test_telescope_parameter_lookup(mock_subarray):
+    telparam_list = TelescopeParameterLookup([("type", "*", 10), ("type", "LST*", 100)])
+
+    with pytest.raises(ValueError):
+        telparam_list[1]
+
+    assert telparam_list[None] == 10
+
+    telparam_list.attach_subarray(mock_subarray)
+    assert telparam_list[1] == 10
+    assert telparam_list[3] == 100
+    assert telparam_list[None] == 10
+
+    with pytest.raises(KeyError):
+        telparam_list[200]
+
+    with pytest.raises(ValueError):
+        bad_config = TelescopeParameterLookup([("unknown", "a", 15.0)])
+        bad_config.attach_subarray(mock_subarray)
+
+    telparam_list2 = TelescopeParameterLookup([("type", "LST*", 100)])
+    with pytest.raises(KeyError):
+        telparam_list2[None]
+
+
+def test_telescope_parameter_lookup_by_type(subarray_prod5_paranal):
+    subarray = subarray_prod5_paranal.select_subarray([1, 2, 3, 4, 100, 101])
+
+    lookup = TelescopeParameterLookup([("type", "*", 10), ("type", "LST*", 100)])
+    lookup.attach_subarray(subarray)
+
+    assert lookup["LST_LST_LSTCam"] == 100
+    assert lookup["MST_MST_NectarCam"] == 10
+    assert lookup[subarray.tel[1]] == 100
+    assert lookup[subarray.tel[100]] == 10
+
+    # no global default
+    lookup = TelescopeParameterLookup([("type", "LST*", 100)])
+    lookup.attach_subarray(subarray)
+    assert lookup["LST_LST_LSTCam"] == 100
+
+    with pytest.raises(KeyError, match="no parameter value"):
+        assert lookup["MST_MST_NectarCam"]
+
+    with pytest.raises(ValueError, match="Unknown telescope"):
+        assert lookup["Foo"]
+
+    with pytest.raises(ValueError, match="Unknown telescope"):
+        # sst
+        assert lookup[subarray_prod5_paranal.tel[30]]
+
+
+def test_telescope_parameter_patterns(mock_subarray):
+    """Test validation of TelescopeParameters"""
+
+    with pytest.raises(TypeError):
+        TelescopeParameter(trait=int)
+
+    with pytest.raises(TypeError):
+        TelescopeParameter(trait=Int)
+
+    class SomeComponent(TelescopeComponent):
+        tel_param = TelescopeParameter(Float(default_value=0.0, allow_none=True))
+        tel_param_int = IntTelescopeParameter()
+
+    comp = SomeComponent(mock_subarray)
+
+    # single value allowed (converted to ("default","",val) )
+    comp.tel_param = 4.5
+    assert list(comp.tel_param)[0][2] == 4.5
+
+    comp.tel_param = [("type", "*", 1.0), ("type", "*LSTCam", 16.0), ("id", 16, 10.0)]
+
+    with pytest.raises(TraitError):
+        comp.tel_param = [("badcommand", "", 1.0)]
+
+    with pytest.raises(TraitError):
+        comp.tel_param = [("type", 12, 1.5)]  # bad argument
+
+    with pytest.raises(TraitError):
+        comp.tel_param_int = [("type", "LST_LST_LSTCam", 1.5)]  # not int
+
+    comp.tel_param_int = [("type", "LST_LST_LSTCam", 1)]
+
+    with pytest.raises(TraitError):
+        comp.tel_param_int = [("*", 5)]  # wrong number of args
+
+    with pytest.raises(TraitError):
+        comp.tel_param_int = [(12, "", 5)]  # command not string
+
+
+def test_telescope_parameter_path(mock_subarray, tmp_path):
+    class SomeComponent(TelescopeComponent):
+        path = TelescopeParameter(Path(exists=True, directory_ok=False))
+
+    c = SomeComponent(subarray=mock_subarray)
+
+    # non existing
+    with pytest.raises(TraitError):
+        c.path = "/does/not/exist"
+
+    with tempfile.NamedTemporaryFile() as f:
+        c.path = f.name
+
+        assert str(c.path.tel[1]) == f.name
+
+        with pytest.raises(TraitError):
+            # non existing somewhere in the config
+            c.path = [
+                ("type", "*", f.name),
+                ("type", "LST_LST_LSTCam", "/does/not/exist"),
+            ]
+
+    with tempfile.TemporaryDirectory() as d:
+        with pytest.raises(TraitError):
+            c.path = d
+
+    # test with none default:
+    class SomeComponent(TelescopeComponent):
+        path = TelescopeParameter(
+            Path(exists=True, directory_ok=False, allow_none=True, default_value=None),
+            default_value=None,
+            allow_none=True,
+        )
+
+    s = SomeComponent(subarray=mock_subarray)
+    assert s.path.tel[1] is None
+    path = tmp_path / "foo"
+    path.open("w").close()
+    s.path = [("type", "*", path)]
+    assert s.path.tel[1] == path
+
+
+def test_telescope_parameter_scalar_default(mock_subarray):
+    class SomeComponentInt(Component):
+        tel_param = IntTelescopeParameter(default_value=1)
+
+    comp_int = SomeComponentInt()
+    comp_int.tel_param.attach_subarray(mock_subarray)
+    assert comp_int.tel_param.tel[1] == 1
+
+    class SomeComponentFloat(Component):
+        tel_param = FloatTelescopeParameter(default_value=1.5)
+
+    comp_float = SomeComponentFloat()
+    comp_float.tel_param.attach_subarray(mock_subarray)
+    assert comp_float.tel_param.tel[1] == 1.5
+
+
+def test_telescope_parameter_resolver(mock_subarray):
+    """check that you can resolve the rules specified in a
+    TelescopeParameter trait"""
+
+    class SomeComponent(Component):
+        tel_param1 = IntTelescopeParameter(
+            default_value=[("type", "*", 10), ("type", "LST*", 100)]
+        )
+
+        tel_param2 = FloatTelescopeParameter(
+            default_value=[
+                ("type", "*", 10.0),
+                ("type", "LST_LST_LSTCam", 100.0),
+                ("id", 3, 200.0),
+            ]
+        )
+
+        tel_param3 = FloatTelescopeParameter(
+            default_value=[
+                ("type", "*", 10.0),
+                ("type", "LST_LST_LSTCam", 100.0),
+                ("type", "*", 200.0),  # should overwrite everything with 200.0
+                ("id", 100, 300.0),
+            ]
+        )
+
+    comp = SomeComponent()
+    comp.tel_param1.attach_subarray(mock_subarray)
+    comp.tel_param2.attach_subarray(mock_subarray)
+    comp.tel_param3.attach_subarray(mock_subarray)
+
+    assert comp.tel_param1.tel[1] == 10
+    assert comp.tel_param1.tel[3] == 100
+
+    for tel_id, expected in enumerate([10.0, 10.0, 200.0, 100.0], start=1):
+        assert comp.tel_param2.tel[tel_id] == expected, f"mismatch for tel_id={tel_id}"
+
+    expected = {1: 200.0, 2: 200.0, 3: 200.0, 4: 200.0, 100: 300.0}
+    for tel_id, value in expected.items():
+        assert comp.tel_param3.tel[tel_id] == value, f"mismatch for tel_id={tel_id}"
+
+
+def test_telescope_parameter_component_arg(mock_subarray):
+    class SomeComponent(Component):
+        tel_param1 = IntTelescopeParameter(
+            default_value=[("type", "*", 10), ("type", "LST*", 100)]
+        )
+
+    comp = SomeComponent(tel_param1=[("type", "*", 2), ("type", "LST*", 4)])
+    comp.tel_param1.attach_subarray(mock_subarray)
+    assert comp.tel_param1.tel[1] == 2
+    assert comp.tel_param1.tel[3] == 4
+    assert comp.tel_param1.tel[None] == 2
+
+    comp = SomeComponent(tel_param1=200)
+    comp.tel_param1.attach_subarray(mock_subarray)
+    assert comp.tel_param1.tel[1] == 200
+    assert comp.tel_param1.tel[3] == 200
+    assert comp.tel_param1.tel[None] == 200
+
+    comp = SomeComponent(tel_param1=300)
+    assert comp.tel_param1.tel[None] == 300
+
+
+def test_telescope_parameter_set_retain_subarray(mock_subarray):
+    class SomeComponent(Component):
+        tel_param1 = IntTelescopeParameter(
+            default_value=[("type", "*", 10), ("type", "LST*", 100)]
+        )
+
+    comp = SomeComponent()
+    comp.tel_param1.attach_subarray(mock_subarray)
+    assert comp.tel_param1.tel[1] == 10
+    assert comp.tel_param1.tel[3] == 100
+    assert comp.tel_param1.tel[None] == 10
+
+    comp.tel_param1 = 5
+    assert comp.tel_param1.tel[1] == 5
+    assert comp.tel_param1.tel[3] == 5
+    assert comp.tel_param1.tel[None] == 5
+
+
+def test_telescope_parameter_to_config(mock_subarray):
+    """
+    test that the config can be read back from a component with a TelescopeParameter
+    (see Issue #1216)
+    """
+
+    class SomeComponent(TelescopeComponent):
+        tel_param1 = FloatTelescopeParameter(default_value=6.0).tag(config=True)
+
+    component = SomeComponent(subarray=mock_subarray)
+    component.tel_param1 = 6.0
+    config = component.get_current_config()
+    assert config["SomeComponent"]["tel_param1"] == [("type", "*", 6.0)]
+
+
+def test_telescope_parameter_from_cli(tmp_path, mock_subarray):
+    """
+    Test we can pass single default for telescope components via cli
+    see #1559
+    """
+
+    from ctapipe.core import Tool, run_tool
+
+    class SomeComponent(TelescopeComponent):
+        path = TelescopeParameter(
+            Path(allow_none=True, default_value=None), default_value=None
+        ).tag(config=True)
+        val = TelescopeParameter(Float(), default_value=1.0).tag(config=True)
+        flag = TelescopeParameter(Bool(), default_value=True).tag(config=True)
+
+    # test with and without SomeComponent in classes
+    for tool_classes in [[], [SomeComponent]]:
+
+        class TelescopeTool(Tool):
+            classes = tool_classes
+
+            def setup(self):
+                self.comp = SomeComponent(subarray=mock_subarray, parent=self)
+
+        tool = TelescopeTool()
+        assert run_tool(tool) == 0
+        assert tool.comp.path == [("type", "*", None)]
+        assert tool.comp.val == [("type", "*", 1.0)]
+        assert tool.comp.flag == [("type", "*", True)]
+
+        tool = TelescopeTool()
+        result = run_tool(
+            tool,
+            [
+                "--SomeComponent.path",
+                f"{tmp_path}/test.h5",
+                "--SomeComponent.val",
+                "2.0",
+                "--SomeComponent.flag",
+                "False",
+            ],
+        )
+        assert result == 0
+        assert tool.comp.path == [("type", "*", tmp_path / "test.h5")]
+        assert tool.comp.val == [("type", "*", 2.0)]
+        assert tool.comp.flag == [("type", "*", False)]
+
+
+@pytest.mark.parametrize(
+    "trait_type",
+    [IntTelescopeParameter, FloatTelescopeParameter, BoolTelescopeParameter],
+)
+def test_telescope_parameter_none(trait_type, mock_subarray):
+    class Foo(TelescopeComponent):
+        bar = trait_type(default_value=None, allow_none=True).tag(config=True)
+
+    assert Foo(mock_subarray).bar.tel[1] is None
+    assert Foo(mock_subarray, bar=None).bar.tel[1] is None
+
+    f = Foo(mock_subarray, bar=[("type", "*", 1), ("id", 1, None)])
+    assert f.bar.tel[1] is None
diff --git a/ctapipe/core/tests/test_traits.py b/ctapipe/core/tests/test_traits.py
index 45bf1837..8915b613 100644
--- a/ctapipe/core/tests/test_traits.py
+++ b/ctapipe/core/tests/test_traits.py
@@ -7,18 +7,11 @@ from unittest import mock
 import pytest
 from traitlets import CaselessStrEnum, HasTraits, Int
 
-from ctapipe.core import Component, TelescopeComponent
+from ctapipe.core import Component
 from ctapipe.core.traits import (
     AstroTime,
-    Bool,
-    BoolTelescopeParameter,
-    Float,
-    FloatTelescopeParameter,
-    IntTelescopeParameter,
     List,
     Path,
-    TelescopeParameter,
-    TelescopeParameterLookup,
     TraitError,
     classes_with_traits,
     has_traits,
@@ -27,21 +20,6 @@ from ctapipe.image import ImageExtractor
 from ctapipe.utils.datasets import DEFAULT_URL, get_dataset_path
 
 
-@pytest.fixture(scope="module")
-def mock_subarray():
-    subarray = mock.MagicMock()
-    subarray.tel_ids = [1, 2, 3, 4]
-    subarray.get_tel_ids_for_type = (
-        lambda x: [3, 4] if x == "LST_LST_LSTCam" else [1, 2]
-    )
-    subarray.telescope_types = [
-        "LST_LST_LSTCam",
-        "MST_MST_NectarCam",
-        "MST_MST_FlashCam",
-    ]
-    return subarray
-
-
 def test_path_allow_none_false():
     class C(Component):
         path = Path(default_value=None, allow_none=False)
@@ -267,321 +245,6 @@ def test_has_traits():
     assert has_traits(WithATrait)
 
 
-def test_telescope_parameter_lookup(mock_subarray):
-    telparam_list = TelescopeParameterLookup([("type", "*", 10), ("type", "LST*", 100)])
-
-    with pytest.raises(ValueError):
-        telparam_list[1]
-
-    assert telparam_list[None] == 10
-
-    telparam_list.attach_subarray(mock_subarray)
-    assert telparam_list[1] == 10
-    assert telparam_list[3] == 100
-    assert telparam_list[None] == 10
-
-    with pytest.raises(KeyError):
-        telparam_list[200]
-
-    with pytest.raises(ValueError):
-        bad_config = TelescopeParameterLookup([("unknown", "a", 15.0)])
-        bad_config.attach_subarray(mock_subarray)
-
-    telparam_list2 = TelescopeParameterLookup([("type", "LST*", 100)])
-    with pytest.raises(KeyError):
-        telparam_list2[None]
-
-
-def test_telescope_parameter_lookup_by_type(subarray_prod5_paranal):
-    subarray = subarray_prod5_paranal.select_subarray([1, 2, 3, 4, 100, 101])
-
-    lookup = TelescopeParameterLookup([("type", "*", 10), ("type", "LST*", 100)])
-    lookup.attach_subarray(subarray)
-
-    assert lookup["LST_LST_LSTCam"] == 100
-    assert lookup["MST_MST_NectarCam"] == 10
-    assert lookup[subarray.tel[1]] == 100
-    assert lookup[subarray.tel[100]] == 10
-
-    # no global default
-    lookup = TelescopeParameterLookup([("type", "LST*", 100)])
-    lookup.attach_subarray(subarray)
-    assert lookup["LST_LST_LSTCam"] == 100
-
-    with pytest.raises(KeyError, match="no parameter value"):
-        assert lookup["MST_MST_NectarCam"]
-
-    with pytest.raises(ValueError, match="Unknown telescope"):
-        assert lookup["Foo"]
-
-    with pytest.raises(ValueError, match="Unknown telescope"):
-        # sst
-        assert lookup[subarray_prod5_paranal.tel[30]]
-
-
-def test_telescope_parameter_patterns(mock_subarray):
-    """Test validation of TelescopeParameters"""
-
-    with pytest.raises(TypeError):
-        TelescopeParameter(trait=int)
-
-    with pytest.raises(TypeError):
-        TelescopeParameter(trait=Int)
-
-    class SomeComponent(TelescopeComponent):
-        tel_param = TelescopeParameter(Float(default_value=0.0, allow_none=True))
-        tel_param_int = IntTelescopeParameter()
-
-    comp = SomeComponent(mock_subarray)
-
-    # single value allowed (converted to ("default","",val) )
-    comp.tel_param = 4.5
-    assert list(comp.tel_param)[0][2] == 4.5
-
-    comp.tel_param = [("type", "*", 1.0), ("type", "*LSTCam", 16.0), ("id", 16, 10.0)]
-
-    with pytest.raises(TraitError):
-        comp.tel_param = [("badcommand", "", 1.0)]
-
-    with pytest.raises(TraitError):
-        comp.tel_param = [("type", 12, 1.5)]  # bad argument
-
-    with pytest.raises(TraitError):
-        comp.tel_param_int = [("type", "LST_LST_LSTCam", 1.5)]  # not int
-
-    comp.tel_param_int = [("type", "LST_LST_LSTCam", 1)]
-
-    with pytest.raises(TraitError):
-        comp.tel_param_int = [("*", 5)]  # wrong number of args
-
-    with pytest.raises(TraitError):
-        comp.tel_param_int = [(12, "", 5)]  # command not string
-
-
-def test_telescope_parameter_path(mock_subarray, tmp_path):
-    class SomeComponent(TelescopeComponent):
-        path = TelescopeParameter(Path(exists=True, directory_ok=False))
-
-    c = SomeComponent(subarray=mock_subarray)
-
-    # non existing
-    with pytest.raises(TraitError):
-        c.path = "/does/not/exist"
-
-    with tempfile.NamedTemporaryFile() as f:
-        c.path = f.name
-
-        assert str(c.path.tel[1]) == f.name
-
-        with pytest.raises(TraitError):
-            # non existing somewhere in the config
-            c.path = [
-                ("type", "*", f.name),
-                ("type", "LST_LST_LSTCam", "/does/not/exist"),
-            ]
-
-    with tempfile.TemporaryDirectory() as d:
-        with pytest.raises(TraitError):
-            c.path = d
-
-    # test with none default:
-    class SomeComponent(TelescopeComponent):
-        path = TelescopeParameter(
-            Path(exists=True, directory_ok=False, allow_none=True, default_value=None),
-            default_value=None,
-            allow_none=True,
-        )
-
-    s = SomeComponent(subarray=mock_subarray)
-    assert s.path.tel[1] is None
-    path = tmp_path / "foo"
-    path.open("w").close()
-    s.path = [("type", "*", path)]
-    assert s.path.tel[1] == path
-
-
-def test_telescope_parameter_scalar_default(mock_subarray):
-    class SomeComponentInt(Component):
-        tel_param = IntTelescopeParameter(default_value=1)
-
-    comp_int = SomeComponentInt()
-    comp_int.tel_param.attach_subarray(mock_subarray)
-    assert comp_int.tel_param.tel[1] == 1
-
-    class SomeComponentFloat(Component):
-        tel_param = FloatTelescopeParameter(default_value=1.5)
-
-    comp_float = SomeComponentFloat()
-    comp_float.tel_param.attach_subarray(mock_subarray)
-    assert comp_float.tel_param.tel[1] == 1.5
-
-
-def test_telescope_parameter_resolver():
-    """check that you can resolve the rules specified in a
-    TelescopeParameter trait"""
-
-    class SomeComponent(Component):
-        tel_param1 = IntTelescopeParameter(
-            default_value=[("type", "*", 10), ("type", "LST*", 100)]
-        )
-
-        tel_param2 = FloatTelescopeParameter(
-            default_value=[
-                ("type", "*", 10.0),
-                ("type", "LST_LST_LSTCam", 100.0),
-                ("id", 3, 200.0),
-            ]
-        )
-
-        tel_param3 = FloatTelescopeParameter(
-            default_value=[
-                ("type", "*", 10.0),
-                ("type", "LST_LST_LSTCam", 100.0),
-                ("type", "*", 200.0),  # should overwrite everything with 200.0
-                ("id", 100, 300.0),
-            ]
-        )
-
-    comp = SomeComponent()
-
-    # need to mock a SubarrayDescription
-    subarray = mock.MagicMock()
-    subarray.tel_ids = [1, 2, 3, 4]
-    subarray.get_tel_ids_for_type = (
-        lambda x: [3, 4] if x == "LST_LST_LSTCam" else [1, 2]
-    )
-    subarray.telescope_types = [
-        "LST_LST_LSTCam",
-        "MST_MST_NectarCam",
-        "MST_MST_FlashCam",
-    ]
-
-    comp.tel_param1.attach_subarray(subarray)
-    comp.tel_param2.attach_subarray(subarray)
-    comp.tel_param3.attach_subarray(subarray)
-
-    assert comp.tel_param1.tel[1] == 10
-    assert comp.tel_param1.tel[3] == 100
-
-    assert list(map(comp.tel_param2.tel.__getitem__, [1, 2, 3, 4])) == [
-        10.0,
-        10.0,
-        200.0,
-        100.0,
-    ]
-
-    assert list(map(comp.tel_param3.tel.__getitem__, [1, 2, 3, 4, 100])) == [
-        200.0,
-        200.0,
-        200.0,
-        200.0,
-        300.0,
-    ]
-
-
-def test_telescope_parameter_component_arg(mock_subarray):
-    class SomeComponent(Component):
-        tel_param1 = IntTelescopeParameter(
-            default_value=[("type", "*", 10), ("type", "LST*", 100)]
-        )
-
-    comp = SomeComponent(tel_param1=[("type", "*", 2), ("type", "LST*", 4)])
-    comp.tel_param1.attach_subarray(mock_subarray)
-    assert comp.tel_param1.tel[1] == 2
-    assert comp.tel_param1.tel[3] == 4
-    assert comp.tel_param1.tel[None] == 2
-
-    comp = SomeComponent(tel_param1=200)
-    comp.tel_param1.attach_subarray(mock_subarray)
-    assert comp.tel_param1.tel[1] == 200
-    assert comp.tel_param1.tel[3] == 200
-    assert comp.tel_param1.tel[None] == 200
-
-    comp = SomeComponent(tel_param1=300)
-    assert comp.tel_param1.tel[None] == 300
-
-
-def test_telescope_parameter_set_retain_subarray(mock_subarray):
-    class SomeComponent(Component):
-        tel_param1 = IntTelescopeParameter(
-            default_value=[("type", "*", 10), ("type", "LST*", 100)]
-        )
-
-    comp = SomeComponent()
-    comp.tel_param1.attach_subarray(mock_subarray)
-    assert comp.tel_param1.tel[1] == 10
-    assert comp.tel_param1.tel[3] == 100
-    assert comp.tel_param1.tel[None] == 10
-
-    comp.tel_param1 = 5
-    assert comp.tel_param1.tel[1] == 5
-    assert comp.tel_param1.tel[3] == 5
-    assert comp.tel_param1.tel[None] == 5
-
-
-def test_telescope_parameter_to_config(mock_subarray):
-    """
-    test that the config can be read back from a component with a TelescopeParameter
-    (see Issue #1216)
-    """
-
-    class SomeComponent(TelescopeComponent):
-        tel_param1 = FloatTelescopeParameter(default_value=6.0).tag(config=True)
-
-    component = SomeComponent(subarray=mock_subarray)
-    component.tel_param1 = 6.0
-    config = component.get_current_config()
-    assert config["SomeComponent"]["tel_param1"] == [("type", "*", 6.0)]
-
-
-def test_telescope_parameter_from_cli(tmp_path, mock_subarray):
-    """
-    Test we can pass single default for telescope components via cli
-    see #1559
-    """
-
-    from ctapipe.core import Tool, run_tool
-
-    class SomeComponent(TelescopeComponent):
-        path = TelescopeParameter(
-            Path(allow_none=True, default_value=None), default_value=None
-        ).tag(config=True)
-        val = TelescopeParameter(Float(), default_value=1.0).tag(config=True)
-        flag = TelescopeParameter(Bool(), default_value=True).tag(config=True)
-
-    # test with and without SomeComponent in classes
-    for tool_classes in [[], [SomeComponent]]:
-
-        class TelescopeTool(Tool):
-            classes = tool_classes
-
-            def setup(self):
-                self.comp = SomeComponent(subarray=mock_subarray, parent=self)
-
-        tool = TelescopeTool()
-        assert run_tool(tool) == 0
-        assert tool.comp.path == [("type", "*", None)]
-        assert tool.comp.val == [("type", "*", 1.0)]
-        assert tool.comp.flag == [("type", "*", True)]
-
-        tool = TelescopeTool()
-        result = run_tool(
-            tool,
-            [
-                "--SomeComponent.path",
-                f"{tmp_path}/test.h5",
-                "--SomeComponent.val",
-                "2.0",
-                "--SomeComponent.flag",
-                "False",
-            ],
-        )
-        assert result == 0
-        assert tool.comp.path == [("type", "*", tmp_path / "test.h5")]
-        assert tool.comp.val == [("type", "*", 2.0)]
-        assert tool.comp.flag == [("type", "*", False)]
-
-
 def test_datetimes():
     from astropy import time as t
 
@@ -700,18 +363,3 @@ def test_component_name():
 
     expected = "A list of Base subclass names. Possible values: ['Foo', 'Bar']"
     assert MyListComponent.base_names.help == expected
-
-
-@pytest.mark.parametrize(
-    "trait_type",
-    [IntTelescopeParameter, FloatTelescopeParameter, BoolTelescopeParameter],
-)
-def test_telescope_parameter_none(trait_type, mock_subarray):
-    class Foo(TelescopeComponent):
-        bar = trait_type(default_value=None, allow_none=True).tag(config=True)
-
-    assert Foo(mock_subarray).bar.tel[1] is None
-    assert Foo(mock_subarray, bar=None).bar.tel[1] is None
-
-    f = Foo(mock_subarray, bar=[("type", "*", 1), ("id", 1, None)])
-    assert f.bar.tel[1] is None