Repository: cta-observatory/ctapipe
Pull request #1985 (2022-07-01 11:51:37)

Use pytest fixtures in unit tests instead of `from_name` methods
We are preparing a number of changes to the data model, including also the instrument model and dropping support for older versions.

This is very hard to do when the tests are relying on very old files on the data server accessed via `from_name`.

Here I replace all use in our unit tests of `from_name` with pytest fixtures, which are defined mostly in `ctapipe/conftest` so we can change them in a single location.

Also, most fixtures are updated to use prod5 data.

review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/cta-observatory/ctapipe/pull/1985"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
kosack: what's the reasoning for this?  
maxnoe: @kosack As explained in #1978, I think these methods do more harm than good. 

They make it very easy to make mistakes like loading the wrong instrument compared to the data being analyzed. I stopped counting the occurences I had to change code from doing `from_name` to reading the subarray from the input file / just using the one provided by the event source.
maxnoe: I think the use case for our unit tests is much better solved by defining common fixtures. 

It reduced the code already by quite a lot. I could also imagine just doing the changes in our tests and keeping `from_name` for now, but the main thing is that it was abused far too often resulting in many errors.
codecov[bot]: # [Codecov](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory) Report
> Merging [#1985](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory) (bbb2d38) into [master](https://codecov.io/gh/cta-observatory/ctapipe/commit/4272a4b33b6be71ba7181e44fb228163def5cedb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory) (4272a4b) will **decrease** coverage by `0.06%`.
> The diff coverage is `99.23%`.

```diff
@@            Coverage Diff             @@
##           master    #1985      +/-   ##
==========================================
- Coverage   92.21%   92.14%   -0.07%     
==========================================
  Files         193      192       -1     
  Lines       15379    15272     -107     
==========================================
- Hits        14182    14073     -109     
- Misses       1197     1199       +2     
```


| [Impacted Files](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory) | Coverage Δ | |
|---|---|---|
| [ctapipe/image/hillas.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9pbWFnZS9oaWxsYXMucHk=) | `98.50% <ø> (-1.50%)` | :arrow_down: |
| [ctapipe/instrument/camera/description.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9pbnN0cnVtZW50L2NhbWVyYS9kZXNjcmlwdGlvbi5weQ==) | `93.75% <ø> (-2.81%)` | :arrow_down: |
| [ctapipe/instrument/camera/geometry.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9pbnN0cnVtZW50L2NhbWVyYS9nZW9tZXRyeS5weQ==) | `90.17% <ø> (-0.54%)` | :arrow_down: |
| [ctapipe/instrument/camera/readout.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9pbnN0cnVtZW50L2NhbWVyYS9yZWFkb3V0LnB5) | `89.83% <ø> (-2.82%)` | :arrow_down: |
| [ctapipe/instrument/camera/tests/test\_readout.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9pbnN0cnVtZW50L2NhbWVyYS90ZXN0cy90ZXN0X3JlYWRvdXQucHk=) | `100.00% <ø> (+1.42%)` | :arrow_up: |
| [ctapipe/instrument/telescope.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9pbnN0cnVtZW50L3RlbGVzY29wZS5weQ==) | `80.00% <ø> (-6.49%)` | :arrow_down: |
| [ctapipe/image/tests/test\_hillas.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9pbWFnZS90ZXN0cy90ZXN0X2hpbGxhcy5weQ==) | `98.90% <94.44%> (-1.10%)` | :arrow_down: |
| [ctapipe/calib/camera/tests/test\_flatfield.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9jYWxpYi9jYW1lcmEvdGVzdHMvdGVzdF9mbGF0ZmllbGQucHk=) | `92.85% <100.00%> (+0.17%)` | :arrow_up: |
| [ctapipe/calib/camera/tests/test\_pedestals.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9jYWxpYi9jYW1lcmEvdGVzdHMvdGVzdF9wZWRlc3RhbHMucHk=) | `100.00% <100.00%> (ø)` | |
| [ctapipe/conftest.py](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory#diff-Y3RhcGlwZS9jb25mdGVzdC5weQ==) | `94.89% <100.00%> (+0.64%)` | :arrow_up: |
| ... and [30 more](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory). Last update [4272a4b...bbb2d38](https://codecov.io/gh/cta-observatory/ctapipe/pull/1985?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cta-observatory).

maxnoe: @kosack I removed the "remove from_name" part for now and only included the changes to the fixtures.

It would make it much easier to test the data model changes without constantly having to update files on the data server to include this.
kosack: This is nice in that we don't rely on the data format.  However, what about tests to read old data formats?  We should still have those (perhaps not now but after the 1.0 release,  to ensure backward compatibility)
maxnoe: The point is, that we now do want do break backwards compatibilty.

For me this means that tests checking compatibility to old data should be added after we merge the current PRs changing the data model, make a release and first thing we add tests testing files pepduced with that release?
maxnoe: I would propose the following:

* Merge the remaining, data model changing PRs
* Make the 0.16.0 release
* Process test data files using the released version and add them to the data server in some meaningful structure
* Add (parametrized) tests checking we are still compatible with all data model versions we want to support (i.e. for now the current one for 0.16.0 and the currently implemented on, e.g.:

``
@pytest.fixture("session", params=["v0.16.0", "current"])
def prod5_dl1_files(request, prod5_dl1_file):
    if request.param == "current":
        return prod5_dl1_file
    return get_dataset_path(f"prod5/{request.param}/test.dl1.h5")
```


Patch:
diff --git a/ctapipe/image/hillas.py b/ctapipe/image/hillas.py
index fae58ccb..a1924c6a 100644
--- a/ctapipe/image/hillas.py
+++ b/ctapipe/image/hillas.py
@@ -7,8 +7,8 @@ Hillas-style moment-based shower image parametrization.
 import astropy.units as u
 import numpy as np
 from astropy.coordinates import Angle
-from ..containers import CameraHillasParametersContainer, HillasParametersContainer
 
+from ..containers import CameraHillasParametersContainer, HillasParametersContainer
 
 HILLAS_ATOL = np.finfo(np.float64).eps
 
@@ -66,37 +66,18 @@ def hillas_parameters(geom, image):
     from
     https://github.com/fact-project/fact-tools
 
-    The image passed to this function can be in three forms:
-
-    >>> from ctapipe.image.hillas import hillas_parameters
-    >>> from ctapipe.image.tests.test_hillas import create_sample_image, compare_hillas
-    >>> geom, image, clean_mask = create_sample_image(psi='0d')
-    >>>
-    >>> # Fastest
-    >>> geom_selected = geom[clean_mask]
-    >>> image_selected = image[clean_mask]
-    >>> hillas_selected = hillas_parameters(geom_selected, image_selected)
-    >>>
-    >>> # Mid (1.45 times longer than fastest)
-    >>> image_zeros = image.copy()
-    >>> image_zeros[~clean_mask] = 0
-    >>> hillas_zeros = hillas_parameters(geom, image_zeros)
-    >>>
-    >>> # Slowest (1.51 times longer than fastest)
-    >>> image_masked = np.ma.masked_array(image, mask=~clean_mask)
-    >>> hillas_masked = hillas_parameters(geom, image_masked)
-    >>>
-    >>> compare_hillas(hillas_selected, hillas_zeros)
-    >>> compare_hillas(hillas_selected, hillas_masked)
+    The recommended form is to pass only the sliced geometry and image
+    for the pixels to be considered.
 
     Each method gives the same result, but vary in efficiency
 
     Parameters
     ----------
     geom: ctapipe.instrument.CameraGeometry
-        Camera geometry
+        Camera geometry, the cleaning mask should be applied to improve performance
     image : array_like
-        Charge in each pixel
+        Charge in each pixel, the cleaning mask should already be applied to
+        improve performance.
 
     Returns
     -------
diff --git a/ctapipe/instrument/subarray.py b/ctapipe/instrument/subarray.py
index b91f1f10..fcb08e38 100644
--- a/ctapipe/instrument/subarray.py
+++ b/ctapipe/instrument/subarray.py
@@ -628,10 +628,10 @@ class SubarrayDescription:
                 focal_length = optics.effective_focal_length
                 if np.isnan(focal_length.value):
                     raise RuntimeError(
-                        "`focal_length_choice` was set to 'EFFECTIVE', but the"
+                        "`focal_length_choice` was set to 'effective', but the"
                         " effective focal length was not present in the file. "
-                        " Set `focal_length_choice='EQUIVALENT'` or make sure"
-                        " input files contain the effective focal length"
+                        " Use nominal focal length or adapt configuration"
+                        " to include the effective focal length"
                     )
             elif focal_length_choice is FocalLengthKind.EQUIVALENT:
                 focal_length = optics.equivalent_focal_length
@@ -670,3 +670,19 @@ class SubarrayDescription:
             tel_descriptions=telescope_descriptions,
             reference_location=reference_location,
         )
+
+    @staticmethod
+    def read(path, **kwargs):
+        """Read subarray from path
+
+        This uses the `~ctapipe.io.EventSource` mechanism, so it should be
+        able to read a subarray from any file supported by ctapipe or an
+        installed io plugin.
+
+        kwargs are passed to the event source
+        """
+        # here to prevent circular import
+        from ..io import EventSource
+
+        with EventSource(path, **kwargs) as s:
+            return s.subarray
diff --git a/ctapipe/io/simteleventsource.py b/ctapipe/io/simteleventsource.py
index 196bda33..a3fd225e 100644
--- a/ctapipe/io/simteleventsource.py
+++ b/ctapipe/io/simteleventsource.py
@@ -338,10 +338,11 @@ class SimTelEventSource(EventSource):
             if self.focal_length_choice is FocalLengthKind.EFFECTIVE:
                 if np.isnan(effective_focal_length):
                     raise RuntimeError(
-                        "`SimTelEventSource.focal_length_choice` was set to 'EFFECTIVE'"
-                        ", but the effective focal length was not present in the file."
-                        " Set `focal_length_choice='EQUIVALENT'` or make sure"
-                        " input files contain the effective focal length"
+                        f"`SimTelEventSource.focal_length_choice` was set to"
+                        f" {self.focal_length_choice!r}, but the effective focal length"
+                        f" was not present in the file. "
+                        " Use nominal focal length or adapt your simulation configuration"
+                        " to include the effective focal length"
                     )
                 focal_length = effective_focal_length
             elif self.focal_length_choice is FocalLengthKind.EQUIVALENT:
diff --git a/docs/ctapipe_api/instrument/camera_geometry.rst b/docs/ctapipe_api/instrument/camera_geometry.rst
index a279ead7..94336be2 100644
--- a/docs/ctapipe_api/instrument/camera_geometry.rst
+++ b/docs/ctapipe_api/instrument/camera_geometry.rst
@@ -16,19 +16,14 @@ common way to describe all cameras.
 
 So far there are several ways to construct a `CameraGeometry`:
 
+* `~ctapipe.io.EventSource` instances have a ``subarray`` attribute,
+  e.g. to obtain the geometry for the telescope with id 1, use:
+  ``source.subarray.tel[1].camera.geometry``.
+  `~ctapipe.io.TableLoader` also has the ``.subarray`` attribute.
+
 * use the `CameraGeometry` constructor, where one has to specify all
   necessary information (pixel positions, types, areas, etc)
 
-* use ``CameraGeometry.from_name(telescope, revision)`` (ex: ``geom =
-  CameraGeometry.from_name('HESS',1)``).  This reads the telescope def
-  from the ``$CTAPIPE_EXTRA`` directory, and so far we only have HESS
-  telescopes there (more to come)
-
-* load a Monte-Carlo file, get the list of pixel X and Y positions and
-  the telescope focal length and use
-  ``CameraGeometry.guess(x,y,flen)`` - this will work for all telescopes
-  in CTA so far
-
 * load it from a pre-written file (which can be in any format
   supported by ``astropy.table``, as long as that format allows for
   header-keywords as well as table entries.
diff --git a/docs/ctapipe_api/instrument/camera_readout.rst b/docs/ctapipe_api/instrument/camera_readout.rst
index 1f94bb5e..b455257b 100644
--- a/docs/ctapipe_api/instrument/camera_readout.rst
+++ b/docs/ctapipe_api/instrument/camera_readout.rst
@@ -11,16 +11,15 @@ shape.
 
 There are several ways to obtain a `CameraReadout`:
 
-* Through the `SubarrayDescription` of an `~ctapipe.io.EventSource`
+* Through the `SubarrayDescription` of an `~ctapipe.io.EventSource` or `~ctapipe.io.TableLoader`
+
+* Through reading a `SubarrayDescription` from an input file using `SubarrayDescription.from_hdf`
+  or `SubarrayDescription.read`
 
 * use the `CameraReadout` constructor, where one has to specify all
   necessary information
 
-* use ``CameraReadout.from_name(telescope, revision)`` (ex: ``readout =
-  CameraReadout.from_name('HESS', 1)``).  This tries to read the corresponding data
-  from through the service data utilities.
-
-* load it from a pre-written file (which can be in any format
+* load it from a pre-written file, which can be in any format
   supported by ``astropy.table.Table``, as long as that format allows for
   header-keywords as well as table entries.
 
diff --git a/docs/ctapipe_api/instrument/camerageometry_example.py b/docs/ctapipe_api/instrument/camerageometry_example.py
index 0ebc6de5..188aaa2a 100644
--- a/docs/ctapipe_api/instrument/camerageometry_example.py
+++ b/docs/ctapipe_api/instrument/camerageometry_example.py
@@ -1,7 +1,9 @@
-from ctapipe.instrument import CameraGeometry
 from matplotlib import pyplot as plt
 
-geom = CameraGeometry.from_name("LSTCam")
+from ctapipe.instrument import SubarrayDescription
+
+subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
+geom = subarray.tel[1].camera.geometry
 
 plt.figure(figsize=(8, 3))
 plt.subplot(1, 2, 1)
diff --git a/docs/examples/Tools.ipynb b/docs/examples/Tools.ipynb
index f53c1a5f..eb3ac44c 100644
--- a/docs/examples/Tools.ipynb
+++ b/docs/examples/Tools.ipynb
@@ -15,8 +15,14 @@
    "source": [
     "from ctapipe.core import Tool, Component, TelescopeComponent\n",
     "from ctapipe.core.traits import (\n",
-    "    Integer, Float, List, Dict, Unicode,\n",
-    "    TraitError, observe, FloatTelescopeParameter,\n",
+    "    Integer,\n",
+    "    Float,\n",
+    "    List,\n",
+    "    Dict,\n",
+    "    Unicode,\n",
+    "    TraitError,\n",
+    "    observe,\n",
+    "    FloatTelescopeParameter,\n",
     "    Path,\n",
     ")\n",
     "import logging\n",
@@ -31,7 +37,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "GAMMA_FILE = get_dataset_path('gamma_test.simtel.gz')"
+    "GAMMA_FILE = get_dataset_path(\"gamma_prod5.simtel.zst\")"
    ]
   },
   {
@@ -57,7 +63,7 @@
    "outputs": [],
    "source": [
     "class MyComponent(Component):\n",
-    "    \"\"\" A Component that does stuff \"\"\"\n",
+    "    \"\"\"A Component that does stuff\"\"\"\n",
     "\n",
     "    value = Integer(default_value=-1, help=\"Value to use\").tag(config=True)\n",
     "\n",
@@ -71,22 +77,21 @@
     "\n",
     "\n",
     "class AdvancedComponent(Component):\n",
-    "    \"\"\" An advanced technique \"\"\"\n",
+    "    \"\"\"An advanced technique\"\"\"\n",
     "\n",
     "    value1 = Integer(default_value=-1, help=\"Value to use\").tag(config=True)\n",
     "    infile = Path(\n",
-    "        help=\"input file name\", \n",
+    "        help=\"input file name\",\n",
     "        exists=None,  # set to True to require existing, False for requiring non-existing\n",
     "        directory_ok=False,\n",
     "    ).tag(config=True)\n",
-    "    outfile = Path(\n",
-    "        help=\"output file name\",\n",
-    "        exists=False, directory_ok=False\n",
-    "    ).tag(config=True)\n",
-    "    \n",
+    "    outfile = Path(help=\"output file name\", exists=False, directory_ok=False).tag(\n",
+    "        config=True\n",
+    "    )\n",
+    "\n",
     "    def __init__(self, config=None, parent=None, **kwargs):\n",
     "        super().__init__(config=config, parent=parent, **kwargs)\n",
-    "        # components can have sub components, but these must have \n",
+    "        # components can have sub components, but these must have\n",
     "        # then parent=self as argument and be assigned as member\n",
     "        # so the full config can be received later\n",
     "        self.subcompent = MyComponent(parent=self)\n",
@@ -97,7 +102,7 @@
     "\n",
     "\n",
     "class TelescopeWiseComponent(TelescopeComponent):\n",
-    "    \"\"\" a component that contains parameters that are per-telescope configurable \"\"\"\n",
+    "    \"\"\"a component that contains parameters that are per-telescope configurable\"\"\"\n",
     "\n",
     "    param = FloatTelescopeParameter(\n",
     "        help=\"Something configurable with telescope patterns\", default_value=5.0\n",
@@ -137,14 +142,7 @@
    "source": [
     "from ctapipe.instrument import SubarrayDescription, TelescopeDescription\n",
     "\n",
-    "subarray = SubarrayDescription(\n",
-    "    \"Junk\",\n",
-    "    tel_positions={1: (0.0, 0.0, 0.0) * u.m, 2: (1.0, 1.0, 0.0) * u.m},\n",
-    "    tel_descriptions={\n",
-    "        1: TelescopeDescription.from_name(\"LST\", \"LSTCam\"),\n",
-    "        2: TelescopeDescription.from_name(\"MST\", \"NectarCam\"),\n",
-    "    },\n",
-    ")\n",
+    "subarray = SubarrayDescription.read(GAMMA_FILE)\n",
     "subarray.info()"
    ]
   },
@@ -190,19 +188,26 @@
    "outputs": [],
    "source": [
     "class MyTool(Tool):\n",
-    "    name=\"mytool\"\n",
-    "    description=\"do some things and stuff\"\n",
+    "    name = \"mytool\"\n",
+    "    description = \"do some things and stuff\"\n",
     "    aliases = dict(\n",
-    "        infile='AdvancedComponent.infile',\n",
-    "        outfile='AdvancedComponent.outfile',\n",
-    "        iterations='MyTool.iterations'\n",
+    "        infile=\"AdvancedComponent.infile\",\n",
+    "        outfile=\"AdvancedComponent.outfile\",\n",
+    "        iterations=\"MyTool.iterations\",\n",
     "    )\n",
     "\n",
     "    # Which classes are registered for configuration\n",
-    "    classes = [MyComponent, AdvancedComponent, SecondaryMyComponent, TelescopeWiseComponent]\n",
+    "    classes = [\n",
+    "        MyComponent,\n",
+    "        AdvancedComponent,\n",
+    "        SecondaryMyComponent,\n",
+    "        TelescopeWiseComponent,\n",
+    "    ]\n",
     "\n",
     "    # local configuration parameters\n",
-    "    iterations = Integer(5,help=\"Number of times to run\",allow_none=False).tag(config=True)\n",
+    "    iterations = Integer(5, help=\"Number of times to run\", allow_none=False).tag(\n",
+    "        config=True\n",
+    "    )\n",
     "\n",
     "    def setup(self):\n",
     "        self.comp = MyComponent(parent=self)\n",
@@ -210,7 +215,6 @@
     "        self.comp3 = TelescopeWiseComponent(parent=self, subarray=subarray)\n",
     "        self.advanced = AdvancedComponent(parent=self)\n",
     "\n",
-    "\n",
     "    def start(self):\n",
     "        self.log.info(\"Performing {} iterations...\".format(self.iterations))\n",
     "        for ii in range(self.iterations):\n",
@@ -218,10 +222,9 @@
     "            self.comp.do_thing()\n",
     "            self.comp2.do_thing()\n",
     "            sleep(0.1)\n",
-    "            \n",
+    "\n",
     "    def finish(self):\n",
-    "        self.log.warning(\"Shutting down.\")\n",
-    "    "
+    "        self.log.warning(\"Shutting down.\")"
    ]
   },
   {
@@ -298,9 +301,9 @@
    "outputs": [],
    "source": [
     "try:\n",
-    "    tool.run(argv=['--infile',  str(GAMMA_FILE), '--outfile', 'out.csv'])\n",
+    "    tool.run(argv=[\"--infile\", str(GAMMA_FILE), \"--outfile\", \"out.csv\"])\n",
     "except SystemExit as e:\n",
-    "    assert e.code == 0, f'Tool returned with error status {e}'"
+    "    assert e.code == 0, f\"Tool returned with error status {e}\""
    ]
   },
   {
@@ -309,13 +312,24 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "tool.log_format = \"%(asctime)s : %(levelname)s [%(name)s %(funcName)s] %(message)s\" \n",
+    "tool.log_format = \"%(asctime)s : %(levelname)s [%(name)s %(funcName)s] %(message)s\"\n",
     "\n",
     "\n",
     "try:\n",
-    "    tool.run(argv=['--log-level','INFO','--infile', str(GAMMA_FILE), '--outfile', 'out.csv', '--iterations','3'])\n",
+    "    tool.run(\n",
+    "        argv=[\n",
+    "            \"--log-level\",\n",
+    "            \"INFO\",\n",
+    "            \"--infile\",\n",
+    "            str(GAMMA_FILE),\n",
+    "            \"--outfile\",\n",
+    "            \"out.csv\",\n",
+    "            \"--iterations\",\n",
+    "            \"3\",\n",
+    "        ]\n",
+    "    )\n",
     "except SystemExit as e:\n",
-    "    assert e.code == 0, f'Tool returned with error status {e}'"
+    "    assert e.code == 0, f\"Tool returned with error status {e}\""
    ]
   },
   {
@@ -334,9 +348,18 @@
    "outputs": [],
    "source": [
     "try:\n",
-    "    tool.run(argv=['--log-level','DEBUG','--infile', str(GAMMA_FILE), '--outfile', 'out.csv'])\n",
+    "    tool.run(\n",
+    "        argv=[\n",
+    "            \"--log-level\",\n",
+    "            \"DEBUG\",\n",
+    "            \"--infile\",\n",
+    "            str(GAMMA_FILE),\n",
+    "            \"--outfile\",\n",
+    "            \"out.csv\",\n",
+    "        ]\n",
+    "    )\n",
     "except SystemExit as e:\n",
-    "    assert e.code == 0, f'Tool returned with error status {e}'"
+    "    assert e.code == 0, f\"Tool returned with error status {e}\""
    ]
   },
   {
@@ -358,9 +381,9 @@
     "tool.log_level = 0\n",
     "\n",
     "try:\n",
-    "    tool.run(['--infile', str(GAMMA_FILE), '--outfile', 'out.csv'])\n",
+    "    tool.run([\"--infile\", str(GAMMA_FILE), \"--outfile\", \"out.csv\"])\n",
     "except SystemExit as e:\n",
-    "    assert e.code == 0, f'Tool returned with error status {e}'"
+    "    assert e.code == 0, f\"Tool returned with error status {e}\""
    ]
   },
   {
@@ -379,9 +402,9 @@
     "try:\n",
     "    tool.iterations = \"badval\"\n",
     "except TraitError as E:\n",
-    "    print(\"bad value:\",E)\n",
+    "    print(\"bad value:\", E)\n",
     "except SystemExit as e:\n",
-    "    assert e.code == 0, f'Tool returned with error status {e}'"
+    "    assert e.code == 0, f\"Tool returned with error status {e}\""
    ]
   },
   {
@@ -432,9 +455,9 @@
    "outputs": [],
    "source": [
     "try:\n",
-    "    tool2.run(argv=['--config', 'Tools.json'])\n",
+    "    tool2.run(argv=[\"--config\", \"Tools.json\"])\n",
     "except SystemExit as e:\n",
-    "    assert e.code == 0, f'Tool returned with error status {e}'"
+    "    assert e.code == 0, f\"Tool returned with error status {e}\""
    ]
   },
   {
@@ -571,18 +594,11 @@
    "source": [
     "print(tool.generate_config_file())"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
   },
@@ -596,7 +612,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.10"
+   "version": "3.8.13"
   }
  },
  "nbformat": 4,
diff --git a/docs/examples/camera_display.ipynb b/docs/examples/camera_display.ipynb
index b528ed4c..3397a1f9 100644
--- a/docs/examples/camera_display.ipynb
+++ b/docs/examples/camera_display.ipynb
@@ -23,7 +23,7 @@
    "outputs": [],
    "source": [
     "import matplotlib.pylab as plt\n",
-    "from ctapipe.instrument import CameraGeometry\n",
+    "from ctapipe.instrument import SubarrayDescription\n",
     "from ctapipe.visualization import CameraDisplay\n",
     "from ctapipe.image import toymodel\n",
     "from ctapipe.image import hillas_parameters, tailcuts_clean\n",
@@ -44,8 +44,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "def draw_neighbors(geom, pixel_index, color='r', **kwargs):\n",
-    "    \"\"\" draw lines between a pixel and its neighbors\"\"\"\n",
+    "def draw_neighbors(geom, pixel_index, color=\"r\", **kwargs):\n",
+    "    \"\"\"draw lines between a pixel and its neighbors\"\"\"\n",
     "    neigh = geom.neighbors[pixel_index]  # neighbor indices (not pixel ids)\n",
     "    x, y = geom.pix_x[pixel_index].value, geom.pix_y[pixel_index].value\n",
     "    for nn in neigh:\n",
@@ -66,8 +66,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# get the HESS demo camera geometry\n",
-    "geom = CameraGeometry.from_name(\"NectarCam\")\n",
+    "# load an example camera geometry from a simulation file\n",
+    "subarray = SubarrayDescription.read(\"dataset://gamma_prod5.simtel.zst\")\n",
+    "geom = subarray.tel[100].camera.geometry\n",
     "\n",
     "# create a fake camera image to display:\n",
     "model = toymodel.Gaussian(\n",
@@ -75,7 +76,7 @@
     "    y=0.0 * u.m,\n",
     "    width=0.05 * u.m,\n",
     "    length=0.15 * u.m,\n",
-    "    psi='35d',\n",
+    "    psi=\"35d\",\n",
     ")\n",
     "\n",
     "image, sig, bg = model.generate_image(geom, intensity=1500, nsb_level_pe=5)\n",
@@ -98,15 +99,15 @@
     "disp = CameraDisplay(geom)\n",
     "disp.add_colorbar()\n",
     "disp.image = image\n",
-    "disp.overlay_moments(hillas, color='grey', linewidth=3,zorder=10)\n",
-    "disp.highlight_pixels(mask, alpha=0.1, color='white')\n",
+    "disp.overlay_moments(hillas, color=\"grey\", linewidth=3, zorder=10)\n",
+    "disp.highlight_pixels(mask, alpha=0.1, color=\"white\")\n",
     "\n",
     "# draw the neighbors of pixel 430 in red, and the\n",
     "# neighbor-neighbors in green\n",
     "\n",
     "for ii in geom.neighbors[430]:\n",
-    "    draw_neighbors(geom, ii, color='green')\n",
-    "draw_neighbors(geom, 430, color='red',lw=2)"
+    "    draw_neighbors(geom, ii, color=\"green\")\n",
+    "draw_neighbors(geom, 430, color=\"red\", lw=2)"
    ]
   },
   {
@@ -119,19 +120,12 @@
     "    ...\n",
     "    mpld3.display()"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {
   "anaconda-cloud": {},
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
   },
@@ -145,7 +139,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.8"
+   "version": "3.8.13"
   }
  },
  "nbformat": 4,
diff --git a/docs/examples/convert_images_to_2d.ipynb b/docs/examples/convert_images_to_2d.ipynb
index 6dbf81b9..f9f30563 100644
--- a/docs/examples/convert_images_to_2d.ipynb
+++ b/docs/examples/convert_images_to_2d.ipynb
@@ -15,13 +15,23 @@
    "source": [
     "from ctapipe.utils import get_dataset_path\n",
     "from ctapipe.visualization import CameraDisplay\n",
-    "from ctapipe.instrument import CameraGeometry\n",
+    "from ctapipe.instrument import SubarrayDescription\n",
     "from ctapipe.io import EventSource\n",
     "from ctapipe.image.toymodel import Gaussian\n",
     "import matplotlib.pyplot as plt\n",
     "import astropy.units as u"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# get the subarray from an example file\n",
+    "subarray = SubarrayDescription.read(\"dataset://gamma_prod5.simtel.zst\")"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -37,17 +47,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "geom = CameraGeometry.from_name('CHEC')\n",
+    "geom = subarray.tel[40].camera.geometry\n",
     "model = Gaussian(\n",
-    "        x=0.05*u.m,\n",
-    "        y=0.05*u.m,\n",
-    "        width=0.01*u.m,\n",
-    "        length=0.05*u.m,\n",
-    "        psi='30d',\n",
-    "    )\n",
-    "_, image, _ = model.generate_image(\n",
-    "    geom, intensity=500, nsb_level_pe=3\n",
-    ")"
+    "    x=0.05 * u.m,\n",
+    "    y=0.05 * u.m,\n",
+    "    width=0.01 * u.m,\n",
+    "    length=0.05 * u.m,\n",
+    "    psi=\"30d\",\n",
+    ")\n",
+    "_, image, _ = model.generate_image(geom, intensity=500, nsb_level_pe=3)"
    ]
   },
   {
@@ -56,7 +64,6 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "\n",
     "CameraDisplay(geom, image)"
    ]
   },
@@ -118,17 +125,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "geom = CameraGeometry.from_name('LSTCam')\n",
+    "geom = subarray.tel[1].camera.geometry\n",
     "model = Gaussian(\n",
-    "        x=0.5*u.m,\n",
-    "        y=0.5*u.m,\n",
-    "        width=0.1*u.m,\n",
-    "        length=0.2*u.m,\n",
-    "        psi='30d',\n",
-    "    )\n",
-    "_, image, _ = model.generate_image(\n",
-    "    geom, intensity=5000\n",
-    ")"
+    "    x=0.5 * u.m,\n",
+    "    y=0.5 * u.m,\n",
+    "    width=0.1 * u.m,\n",
+    "    length=0.2 * u.m,\n",
+    "    psi=\"30d\",\n",
+    ")\n",
+    "_, image, _ = model.generate_image(geom, intensity=5000)"
    ]
   },
   {
@@ -185,18 +190,11 @@
    "source": [
     "disp = CameraDisplay(geom, image_1d)"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
   },
@@ -210,7 +208,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.9"
+   "version": "3.9.13"
   }
  },
  "nbformat": 4,
diff --git a/docs/examples/dilate_image.ipynb b/docs/examples/dilate_image.ipynb
index 0d3a0810..e62e7c1a 100644
--- a/docs/examples/dilate_image.ipynb
+++ b/docs/examples/dilate_image.ipynb
@@ -26,12 +26,13 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from ctapipe.instrument import CameraGeometry\n",
+    "from ctapipe.instrument import SubarrayDescription\n",
     "from ctapipe.visualization import CameraDisplay\n",
     "from ctapipe.image import toymodel, tailcuts_clean, dilate\n",
     "\n",
-    "# Load the camera\n",
-    "geom = CameraGeometry.from_name(\"NectarCam\")\n",
+    "# Load a camera from an example file\n",
+    "subarray = SubarrayDescription.read(\"dataset://gamma_prod5.simtel.zst\")\n",
+    "geom = subarray.tel[100].camera.geometry\n",
     "\n",
     "# Create a fake camera image to display:\n",
     "model = toymodel.Gaussian(\n",
@@ -39,12 +40,10 @@
     "    y=0.0 * u.m,\n",
     "    width=0.05 * u.m,\n",
     "    length=0.15 * u.m,\n",
-    "    psi='35d',\n",
+    "    psi=\"35d\",\n",
     ")\n",
     "\n",
-    "image, sig, bg = model.generate_image(\n",
-    "    geom, intensity=1500, nsb_level_pe=5\n",
-    ")"
+    "image, sig, bg = model.generate_image(geom, intensity=1500, nsb_level_pe=5)"
    ]
   },
   {
@@ -60,13 +59,12 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "cleanmask = tailcuts_clean(geom, image, picture_thresh=10,\n",
-    "                           boundary_thresh=5)\n",
+    "cleanmask = tailcuts_clean(geom, image, picture_thresh=10, boundary_thresh=5)\n",
     "clean = image.copy()\n",
     "clean[~cleanmask] = 0.0\n",
     "\n",
     "disp = CameraDisplay(geom, image=image)\n",
-    "disp.highlight_pixels(cleanmask, color='red')"
+    "disp.highlight_pixels(cleanmask, color=\"red\")"
    ]
   },
   {
@@ -95,7 +93,9 @@
     "    m = mask.copy()\n",
     "    for ii in range(times):\n",
     "        m = dilate(geom, m)\n",
-    "    CameraDisplay(geom,image=(m.astype(int)+mask.astype(int)), title='dilate{}'.format(times))"
+    "    CameraDisplay(\n",
+    "        geom, image=(m.astype(int) + mask.astype(int)), title=\"dilate{}\".format(times)\n",
+    "    )"
    ]
   },
   {
@@ -104,17 +104,17 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "plt.figure(figsize=(18,3))\n",
+    "plt.figure(figsize=(18, 3))\n",
     "\n",
-    "for ii in range(0,6):\n",
-    "    plt.subplot(1,7,ii+1)\n",
+    "for ii in range(0, 6):\n",
+    "    plt.subplot(1, 7, ii + 1)\n",
     "    show_dilate(cleanmask.copy(), times=ii)"
    ]
   }
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
   },
@@ -128,7 +128,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.8"
+   "version": "3.8.13"
   }
  },
  "nbformat": 4,
diff --git a/docs/tutorials/coordinates_example.ipynb b/docs/tutorials/coordinates_example.ipynb
index 429efc7e..1d165212 100644
--- a/docs/tutorials/coordinates_example.ipynb
+++ b/docs/tutorials/coordinates_example.ipynb
@@ -65,8 +65,8 @@
    "outputs": [],
    "source": [
     "# make plots and fonts larger\n",
-    "plt.rcParams['figure.figsize'] = (12, 8)\n",
-    "plt.rcParams['font.size'] = 16"
+    "plt.rcParams[\"figure.figsize\"] = (12, 8)\n",
+    "plt.rcParams[\"font.size\"] = 16"
    ]
   },
   {
@@ -96,7 +96,7 @@
     "events = [copy.deepcopy(event) for event in source]\n",
     "event = events[4]\n",
     "\n",
-    "layout = set(source.subarray.tel_ids)  "
+    "layout = set(source.subarray.tel_ids)"
    ]
   },
   {
@@ -123,7 +123,7 @@
    },
    "outputs": [],
    "source": [
-    "print(f'Telescope with data: {event.r1.tel.keys()}')\n",
+    "print(f\"Telescope with data: {event.r1.tel.keys()}\")\n",
     "tel_id = 3"
    ]
   },
@@ -165,14 +165,14 @@
    },
    "outputs": [],
    "source": [
-    "obstime = Time('2013-11-01T03:00')\n",
-    "location = EarthLocation.of_site('Roque de los Muchachos')\n",
+    "obstime = Time(\"2013-11-01T03:00\")\n",
+    "location = EarthLocation.of_site(\"Roque de los Muchachos\")\n",
     "\n",
     "altaz = AltAz(location=location, obstime=obstime)\n",
     "\n",
     "array_pointing = SkyCoord(\n",
     "    alt=event.pointing.array_azimuth,\n",
-    "    az=event.pointing.array_altitude, \n",
+    "    az=event.pointing.array_altitude,\n",
     "    frame=altaz,\n",
     ")\n",
     "\n",
@@ -214,9 +214,9 @@
    "outputs": [],
    "source": [
     "geometry = source.subarray.tel[tel_id].camera.geometry\n",
-    "pix_x = geometry.pix_x \n",
+    "pix_x = geometry.pix_x\n",
     "pix_y = geometry.pix_y\n",
-    "focal_length = source.subarray.tel[tel_id].optics.equivalent_focal_length "
+    "focal_length = source.subarray.tel[tel_id].optics.equivalent_focal_length"
    ]
   },
   {
@@ -230,8 +230,8 @@
    "outputs": [],
    "source": [
     "telescope_pointing = SkyCoord(\n",
-    "    alt=event.pointing.tel[tel_id].altitude, \n",
-    "    az=event.pointing.tel[tel_id].azimuth, \n",
+    "    alt=event.pointing.tel[tel_id].altitude,\n",
+    "    az=event.pointing.tel[tel_id].azimuth,\n",
     "    frame=altaz,\n",
     ")\n",
     "\n",
@@ -257,10 +257,10 @@
    "outputs": [],
    "source": [
     "plt.scatter(cam_coords.x, cam_coords.y)\n",
-    "plt.title(f'Camera type: {geometry.camera_name}')\n",
-    "plt.xlabel(f'x / {cam_coords.x.unit}')\n",
-    "plt.ylabel(f'y / {cam_coords.y.unit}')\n",
-    "plt.axis('square');"
+    "plt.title(f\"Camera type: {geometry.camera_name}\")\n",
+    "plt.xlabel(f\"x / {cam_coords.x.unit}\")\n",
+    "plt.ylabel(f\"y / {cam_coords.y.unit}\")\n",
+    "plt.axis(\"square\");"
    ]
   },
   {
@@ -277,10 +277,10 @@
    "outputs": [],
    "source": [
     "from ctapipe.visualization import CameraDisplay\n",
-    "from ctapipe.instrument import CameraGeometry\n",
+    "from ctapipe.instrument import SubarrayDescription\n",
     "\n",
-    "location = EarthLocation.of_site('Roque de los Muchachos')\n",
-    "obstime = Time('2018-11-01T04:00')\n",
+    "location = EarthLocation.of_site(\"Roque de los Muchachos\")\n",
+    "obstime = Time(\"2018-11-01T04:00\")\n",
     "\n",
     "crab = SkyCoord.from_name(\"crab nebula\")\n",
     "\n",
@@ -295,25 +295,30 @@
     "    location=location,\n",
     ")\n",
     "\n",
-    "cam = CameraGeometry.from_name('LSTCam')\n",
+    "\n",
+    "subarray = SubarrayDescription.read(\"dataset://gamma_prod5.simtel.zst\")\n",
+    "cam = subarray.tel[1].camera.geometry\n",
     "fig, ax = plt.subplots()\n",
     "display = CameraDisplay(cam, ax=ax)\n",
     "\n",
     "ax.set_title(\n",
-    "    f'La Palma, {obstime}, az={pointing.az.deg:.1f}°, zenith={pointing.zen.deg:.1f}°, camera={geometry.camera_name}'\n",
+    "    f\"La Palma, {obstime}, az={pointing.az.deg:.1f}°, zenith={pointing.zen.deg:.1f}°, camera={geometry.camera_name}\"\n",
     ")\n",
     "\n",
-    "for i, name in enumerate(['crab nebula', 'o tau', 'zet tau']):\n",
+    "for i, name in enumerate([\"crab nebula\", \"o tau\", \"zet tau\"]):\n",
     "    star = SkyCoord.from_name(name)\n",
     "    star_cam = star.transform_to(camera_frame)\n",
     "\n",
     "    x = star_cam.x.to_value(u.m)\n",
     "    y = star_cam.y.to_value(u.m)\n",
     "\n",
-    "    ax.plot(x, y, marker='*', color=f'C{i}')\n",
+    "    ax.plot(x, y, marker=\"*\", color=f\"C{i}\")\n",
     "    ax.annotate(\n",
-    "        name, xy=(x, y), xytext=(5, 5),\n",
-    "        textcoords='offset points', color=f'C{i}',\n",
+    "        name,\n",
+    "        xy=(x, y),\n",
+    "        xytext=(5, 5),\n",
+    "        textcoords=\"offset points\",\n",
+    "        color=f\"C{i}\",\n",
     "    )\n",
     "\n",
     "plt.show()"
@@ -369,30 +374,29 @@
    },
    "outputs": [],
    "source": [
-    "wrap_angle = telescope_pointing.az + 180* u.deg\n",
+    "wrap_angle = telescope_pointing.az + 180 * u.deg\n",
     "\n",
-    "plt.axis('equal')\n",
+    "plt.axis(\"equal\")\n",
     "plt.scatter(\n",
-    "    telescope_coords.fov_lon.deg, \n",
-    "    telescope_coords.fov_lat.deg,\n",
-    "    alpha=0.2,\n",
-    "    color='gray'\n",
+    "    telescope_coords.fov_lon.deg, telescope_coords.fov_lat.deg, alpha=0.2, color=\"gray\"\n",
     ")\n",
     "\n",
     "\n",
-    "for i, name in enumerate(['crab nebula', 'o tau', 'zet tau']):\n",
+    "for i, name in enumerate([\"crab nebula\", \"o tau\", \"zet tau\"]):\n",
     "    star = SkyCoord.from_name(name)\n",
     "    star_tel = star.transform_to(telescope_frame)\n",
     "\n",
-    "   \n",
-    "    plt.plot(star_tel.fov_lon.deg, star_tel.fov_lat.deg, '*', ms=10)\n",
+    "    plt.plot(star_tel.fov_lon.deg, star_tel.fov_lat.deg, \"*\", ms=10)\n",
     "    plt.annotate(\n",
-    "        name, xy=(star_tel.fov_lon.deg, star_tel.fov_lat.deg), xytext=(5, 5),\n",
-    "        textcoords='offset points', color=f'C{i}',\n",
+    "        name,\n",
+    "        xy=(star_tel.fov_lon.deg, star_tel.fov_lat.deg),\n",
+    "        xytext=(5, 5),\n",
+    "        textcoords=\"offset points\",\n",
+    "        color=f\"C{i}\",\n",
     "    )\n",
     "\n",
-    "plt.xlabel('fov_lon / {}'.format(telescope_coords.altaz.az.unit))\n",
-    "plt.ylabel('fov_lat / {}'.format(telescope_coords.altaz.alt.unit))"
+    "plt.xlabel(\"fov_lon / {}\".format(telescope_coords.altaz.az.unit))\n",
+    "plt.ylabel(\"fov_lat / {}\".format(telescope_coords.altaz.alt.unit))"
    ]
   },
   {
@@ -433,8 +437,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "location = EarthLocation.of_site('Roque de los Muchachos')\n",
-    "obstime = Time('2018-11-01T02:00')\n",
+    "location = EarthLocation.of_site(\"Roque de los Muchachos\")\n",
+    "obstime = Time(\"2018-11-01T02:00\")\n",
     "altaz = AltAz(location=location, obstime=obstime)\n",
     "\n",
     "crab = SkyCoord.from_name(\"crab nebula\")\n",
@@ -449,8 +453,8 @@
     "\n",
     "\n",
     "tel_pointings = SkyCoord(\n",
-    "    alt=array_pointing.alt + alt_offsets, \n",
-    "    az=array_pointing.az + az_offsets, \n",
+    "    alt=array_pointing.alt + alt_offsets,\n",
+    "    az=array_pointing.az + az_offsets,\n",
     "    frame=altaz,\n",
     ")\n",
     "\n",
@@ -480,34 +484,37 @@
     "for i in range(3):\n",
     "    cam_coord = SkyCoord(x=pix_x, y=pix_y, frame=camera_frames[i])\n",
     "    nom_coord = cam_coord.transform_to(nom_frame)\n",
-    "    \n",
+    "\n",
     "    ax.scatter(\n",
     "        x=nom_coord.fov_lon.deg,\n",
     "        y=nom_coord.fov_lat.deg,\n",
-    "        label=f'Telescope {i + 1}',\n",
+    "        label=f\"Telescope {i + 1}\",\n",
     "        s=30,\n",
     "        alpha=0.15,\n",
-    "    )  \n",
-    "    \n",
+    "    )\n",
+    "\n",
     "\n",
-    "for i, name in enumerate(['Crab', 'o tau', 'zet tau']):\n",
+    "for i, name in enumerate([\"Crab\", \"o tau\", \"zet tau\"]):\n",
     "    s = SkyCoord.from_name(name)\n",
     "    s_nom = s.transform_to(nom_frame)\n",
     "    ax.plot(\n",
     "        s_nom.fov_lon.deg,\n",
     "        s_nom.fov_lat.deg,\n",
-    "        '*',\n",
+    "        \"*\",\n",
     "        ms=10,\n",
     "    )\n",
     "    ax.annotate(\n",
-    "        name, xy=(s_nom.fov_lon.deg, s_nom.fov_lat.deg), xytext=(5, 5),\n",
-    "        textcoords='offset points', color=f'C{i}',\n",
+    "        name,\n",
+    "        xy=(s_nom.fov_lon.deg, s_nom.fov_lat.deg),\n",
+    "        xytext=(5, 5),\n",
+    "        textcoords=\"offset points\",\n",
+    "        color=f\"C{i}\",\n",
     "    )\n",
     "\n",
     "\n",
-    "ax.set_xlabel(f'fov_lon / deg')\n",
-    "ax.set_ylabel(f'fov_lat / deg')\n",
-    "    \n",
+    "ax.set_xlabel(f\"fov_lon / deg\")\n",
+    "ax.set_ylabel(f\"fov_lat / deg\")\n",
+    "\n",
     "ax.legend()\n",
     "plt.show()"
    ]
@@ -634,7 +641,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.13"
+   "version": "3.9.13"
   }
  },
  "nbformat": 4,
diff --git a/docs/tutorials/ctapipe_overview.ipynb b/docs/tutorials/ctapipe_overview.ipynb
index f0a638a6..e13269bb 100644
--- a/docs/tutorials/ctapipe_overview.ipynb
+++ b/docs/tutorials/ctapipe_overview.ipynb
@@ -60,9 +60,9 @@
    },
    "outputs": [],
    "source": [
-    "plt.rcParams['figure.figsize'] = (12, 8)\n",
-    "plt.rcParams['font.size'] = 14\n",
-    "plt.rcParams['figure.figsize']"
+    "plt.rcParams[\"figure.figsize\"] = (12, 8)\n",
+    "plt.rcParams[\"font.size\"] = 14\n",
+    "plt.rcParams[\"figure.figsize\"]"
    ]
   },
   {
@@ -222,7 +222,7 @@
     "from ctapipe.io import EventSource\n",
     "from ctapipe.utils.datasets import get_dataset_path\n",
     "\n",
-    "input_url = get_dataset_path('gamma_prod5.simtel.zst')\n",
+    "input_url = get_dataset_path(\"gamma_prod5.simtel.zst\")\n",
     "\n",
     "# EventSource() automatically detects what kind of file we are giving it,\n",
     "# if already supported by ctapipe\n",
@@ -240,7 +240,11 @@
    "outputs": [],
    "source": [
     "for event in source:\n",
-    "    print('Id: {}, E = {:1.3f}, Telescopes: {}'.format(event.count, event.simulation.shower.energy, len(event.r0.tel)))"
+    "    print(\n",
+    "        \"Id: {}, E = {:1.3f}, Telescopes: {}\".format(\n",
+    "            event.count, event.simulation.shower.energy, len(event.r0.tel)\n",
+    "        )\n",
+    "    )"
    ]
   },
   {
@@ -392,7 +396,7 @@
     "display = CameraDisplay(geometry)\n",
     "\n",
     "# right now, there might be one image per gain channel.\n",
-    "# This will change as soon as \n",
+    "# This will change as soon as\n",
     "display.image = dl1.image\n",
     "display.add_colorbar()"
    ]
@@ -427,10 +431,10 @@
    "source": [
     "# unoptimized cleaning levels\n",
     "cleaning_level = {\n",
-    "    'CHEC': (2, 4, 2),\n",
-    "    'LSTCam': (3.5, 7, 2),\n",
-    "    'FlashCam': (3.5, 7, 2), \n",
-    "    'NectarCam': (4, 8, 2), \n",
+    "    \"CHEC\": (2, 4, 2),\n",
+    "    \"LSTCam\": (3.5, 7, 2),\n",
+    "    \"FlashCam\": (3.5, 7, 2),\n",
+    "    \"NectarCam\": (4, 8, 2),\n",
     "}"
    ]
   },
@@ -445,11 +449,11 @@
     "boundary, picture, min_neighbors = cleaning_level[geometry.camera_name]\n",
     "\n",
     "clean = tailcuts_clean(\n",
-    "    geometry, \n",
+    "    geometry,\n",
     "    dl1.image,\n",
     "    boundary_thresh=boundary,\n",
     "    picture_thresh=picture,\n",
-    "    min_number_picture_neighbors=min_neighbors\n",
+    "    min_number_picture_neighbors=min_neighbors,\n",
     ")"
    ]
   },
@@ -466,17 +470,17 @@
     "d1 = CameraDisplay(geometry, ax=ax1)\n",
     "d2 = CameraDisplay(geometry, ax=ax2)\n",
     "\n",
-    "ax1.set_title('Image')\n",
+    "ax1.set_title(\"Image\")\n",
     "d1.image = dl1.image\n",
     "d1.add_colorbar(ax=ax1)\n",
     "\n",
-    "ax2.set_title('Pulse Time')\n",
+    "ax2.set_title(\"Pulse Time\")\n",
     "d2.image = dl1.peak_time - np.average(dl1.peak_time, weights=dl1.image)\n",
-    "d2.cmap = 'RdBu_r'\n",
+    "d2.cmap = \"RdBu_r\"\n",
     "d2.add_colorbar(ax=ax2)\n",
-    "d2.set_limits_minmax(-20,20)\n",
+    "d2.set_limits_minmax(-20, 20)\n",
     "\n",
-    "d1.highlight_pixels(clean, color='red', linewidth=1)"
+    "d1.highlight_pixels(clean, color=\"red\", linewidth=1)"
    ]
   },
   {
@@ -496,7 +500,11 @@
    },
    "outputs": [],
    "source": [
-    "from ctapipe.image import hillas_parameters, leakage_parameters, concentration_parameters\n",
+    "from ctapipe.image import (\n",
+    "    hillas_parameters,\n",
+    "    leakage_parameters,\n",
+    "    concentration_parameters,\n",
+    ")\n",
     "from ctapipe.image import timing_parameters\n",
     "from ctapipe.image import number_of_islands\n",
     "from ctapipe.image import camera_to_shower_coordinates"
@@ -532,7 +540,7 @@
     "display.image = cleaned\n",
     "display.add_colorbar()\n",
     "\n",
-    "display.overlay_moments(hillas, color='xkcd:red')"
+    "display.overlay_moments(hillas, color=\"xkcd:red\")"
    ]
   },
   {
@@ -543,13 +551,7 @@
    },
    "outputs": [],
    "source": [
-    "timing = timing_parameters(\n",
-    "    geometry,\n",
-    "    dl1.image,\n",
-    "    dl1.peak_time,\n",
-    "    hillas,\n",
-    "    clean\n",
-    ")\n",
+    "timing = timing_parameters(geometry, dl1.image, dl1.peak_time, hillas, clean)\n",
     "\n",
     "print(timing)"
    ]
@@ -563,10 +565,10 @@
    "outputs": [],
    "source": [
     "long, trans = camera_to_shower_coordinates(\n",
-    "    geometry.pix_x, geometry.pix_y,hillas.x, hillas.y, hillas.psi\n",
+    "    geometry.pix_x, geometry.pix_y, hillas.x, hillas.y, hillas.psi\n",
     ")\n",
     "\n",
-    "plt.plot(long[clean], dl1.peak_time[clean], 'o')\n",
+    "plt.plot(long[clean], dl1.peak_time[clean], \"o\")\n",
     "plt.plot(long[clean], timing.slope * long[clean] + timing.intercept)"
    ]
   },
@@ -592,7 +594,7 @@
    "source": [
     "disp = CameraDisplay(geometry)\n",
     "disp.image = dl1.image\n",
-    "disp.highlight_pixels(geometry.get_border_pixel_mask(1), linewidth=2, color='xkcd:red')"
+    "disp.highlight_pixels(geometry.get_border_pixel_mask(1), linewidth=2, color=\"xkcd:red\")"
    ]
   },
   {
@@ -668,62 +670,67 @@
     "from traitlets.config import Config\n",
     "\n",
     "\n",
-    "image_processor_config = Config({\n",
-    "    \"ImageProcessor\": {\n",
-    "        \"image_cleaner_type\": \"TailcutsImageCleaner\",\n",
-    "        \"TailcutsImageCleaner\": {\n",
-    "            \"picture_threshold_pe\": [\n",
-    "                (\"type\", \"LST_LST_LSTCam\", 7.5),\n",
-    "                (\"type\", \"MST_MST_FlashCam\", 8),\n",
-    "                (\"type\", \"MST_MST_NectarCam\", 8),\n",
-    "                (\"type\", \"SST_ASTRI_CHEC\", 7),\n",
-    "            ],\n",
-    "            \"boundary_threshold_pe\": [\n",
-    "                (\"type\", \"LST_LST_LSTCam\", 5),\n",
-    "                (\"type\", \"MST_MST_FlashCam\", 4),\n",
-    "                (\"type\", \"MST_MST_NectarCam\", 4),\n",
-    "                (\"type\", \"SST_ASTRI_CHEC\", 4),\n",
-    "            ]\n",
-    "            \n",
+    "image_processor_config = Config(\n",
+    "    {\n",
+    "        \"ImageProcessor\": {\n",
+    "            \"image_cleaner_type\": \"TailcutsImageCleaner\",\n",
+    "            \"TailcutsImageCleaner\": {\n",
+    "                \"picture_threshold_pe\": [\n",
+    "                    (\"type\", \"LST_LST_LSTCam\", 7.5),\n",
+    "                    (\"type\", \"MST_MST_FlashCam\", 8),\n",
+    "                    (\"type\", \"MST_MST_NectarCam\", 8),\n",
+    "                    (\"type\", \"SST_ASTRI_CHEC\", 7),\n",
+    "                ],\n",
+    "                \"boundary_threshold_pe\": [\n",
+    "                    (\"type\", \"LST_LST_LSTCam\", 5),\n",
+    "                    (\"type\", \"MST_MST_FlashCam\", 4),\n",
+    "                    (\"type\", \"MST_MST_NectarCam\", 4),\n",
+    "                    (\"type\", \"SST_ASTRI_CHEC\", 4),\n",
+    "                ],\n",
+    "            },\n",
     "        }\n",
     "    }\n",
-    "})\n",
+    ")\n",
     "\n",
-    "input_url = get_dataset_path('gamma_prod5.simtel.zst')\n",
+    "input_url = get_dataset_path(\"gamma_prod5.simtel.zst\")\n",
     "source = EventSource(input_url)\n",
     "\n",
     "calibrator = CameraCalibrator(subarray=source.subarray)\n",
-    "image_processor = ImageProcessor(subarray=source.subarray, config=image_processor_config)\n",
+    "image_processor = ImageProcessor(\n",
+    "    subarray=source.subarray, config=image_processor_config\n",
+    ")\n",
     "shower_processor = ShowerProcessor(subarray=source.subarray)\n",
     "horizon_frame = AltAz()\n",
     "\n",
-    "f = tempfile.NamedTemporaryFile(suffix='.hdf5')\n",
+    "f = tempfile.NamedTemporaryFile(suffix=\".hdf5\")\n",
+    "\n",
+    "with DataWriter(\n",
+    "    source, output_path=f.name, overwrite=True, write_showers=True\n",
+    ") as writer:\n",
     "\n",
-    "with DataWriter(source, output_path=f.name, overwrite=True, write_showers=True) as writer:\n",
-    "    \n",
     "    for event in source:\n",
     "        energy = event.simulation.shower.energy\n",
     "        n_telescopes_r1 = len(event.r1.tel)\n",
     "        event_id = event.index.event_id\n",
-    "        print(f'Id: {event_id}, E = {energy:1.3f}, Telescopes (R1): {n_telescopes_r1}')\n",
-    "        \n",
+    "        print(f\"Id: {event_id}, E = {energy:1.3f}, Telescopes (R1): {n_telescopes_r1}\")\n",
+    "\n",
     "        calibrator(event)\n",
     "        image_processor(event)\n",
     "        shower_processor(event)\n",
-    "        \n",
+    "\n",
     "        stereo = event.dl2.stereo.geometry[\"HillasReconstructor\"]\n",
     "        if stereo.is_valid:\n",
-    "            print('  Alt: {:.2f}°'.format(stereo.alt.deg))\n",
-    "            print('  Az: {:.2f}°'.format(stereo.az.deg))\n",
-    "            print('  Hmax: {:.0f}'.format(stereo.h_max))\n",
-    "            print('  CoreX: {:.1f}'.format(stereo.core_x))\n",
-    "            print('  CoreY: {:.1f}'.format(stereo.core_y))\n",
-    "            print('  Multiplicity: {:d}'.format(len(stereo.tel_ids)))\n",
-    "        \n",
+    "            print(\"  Alt: {:.2f}°\".format(stereo.alt.deg))\n",
+    "            print(\"  Az: {:.2f}°\".format(stereo.az.deg))\n",
+    "            print(\"  Hmax: {:.0f}\".format(stereo.h_max))\n",
+    "            print(\"  CoreX: {:.1f}\".format(stereo.core_x))\n",
+    "            print(\"  CoreY: {:.1f}\".format(stereo.core_y))\n",
+    "            print(\"  Multiplicity: {:d}\".format(len(stereo.tel_ids)))\n",
+    "\n",
     "        # save a nice event for plotting later\n",
     "        if event.count == 3:\n",
     "            plotting_event = deepcopy(event)\n",
-    "            \n",
+    "\n",
     "        writer(event)"
    ]
   },
@@ -754,12 +761,14 @@
    "outputs": [],
    "source": [
     "theta = angular_separation(\n",
-    "    events[\"HillasReconstructor_az\"].quantity, events[\"HillasReconstructor_alt\"].quantity,\n",
-    "    events[\"true_az\"].quantity, events[\"true_alt\"].quantity\n",
+    "    events[\"HillasReconstructor_az\"].quantity,\n",
+    "    events[\"HillasReconstructor_alt\"].quantity,\n",
+    "    events[\"true_az\"].quantity,\n",
+    "    events[\"true_alt\"].quantity,\n",
     ")\n",
     "\n",
-    "plt.hist(theta.to_value(u.deg)**2, bins=25, range=[0, 0.3])\n",
-    "plt.xlabel(r'$\\theta² / deg²$')\n",
+    "plt.hist(theta.to_value(u.deg) ** 2, bins=25, range=[0, 0.3])\n",
+    "plt.xlabel(r\"$\\theta² / deg²$\")\n",
     "None"
    ]
   },
@@ -786,13 +795,11 @@
     "angle_offset = plotting_event.pointing.array_azimuth\n",
     "\n",
     "plotting_hillas = {\n",
-    "    tel_id: dl1.parameters.hillas\n",
-    "    for tel_id, dl1 in plotting_event.dl1.tel.items()\n",
+    "    tel_id: dl1.parameters.hillas for tel_id, dl1 in plotting_event.dl1.tel.items()\n",
     "}\n",
     "\n",
     "plotting_core = {\n",
-    "    tel_id: dl1.parameters.core.psi\n",
-    "    for tel_id, dl1 in plotting_event.dl1.tel.items()\n",
+    "    tel_id: dl1.parameters.core.psi for tel_id, dl1 in plotting_event.dl1.tel.items()\n",
     "}\n",
     "\n",
     "\n",
@@ -801,13 +808,20 @@
     "disp.set_line_hillas(plotting_hillas, plotting_core, 500)\n",
     "\n",
     "plt.scatter(\n",
-    "    plotting_event.simulation.shower.core_x, plotting_event.simulation.shower.core_y,\n",
-    "    s=200, c='k', marker='x', label='True Impact',\n",
+    "    plotting_event.simulation.shower.core_x,\n",
+    "    plotting_event.simulation.shower.core_y,\n",
+    "    s=200,\n",
+    "    c=\"k\",\n",
+    "    marker=\"x\",\n",
+    "    label=\"True Impact\",\n",
     ")\n",
     "plt.scatter(\n",
     "    plotting_event.dl2.stereo.geometry[\"HillasReconstructor\"].core_x,\n",
     "    plotting_event.dl2.stereo.geometry[\"HillasReconstructor\"].core_y,\n",
-    "    s=200, c='r', marker='x', label='Estimated Impact',\n",
+    "    s=200,\n",
+    "    c=\"r\",\n",
+    "    marker=\"x\",\n",
+    "    label=\"Estimated Impact\",\n",
     ")\n",
     "\n",
     "plt.legend()\n",
@@ -847,8 +861,8 @@
     "    np.log10(dl1_table[\"true_energy\"].quantity / u.TeV),\n",
     "    np.log10(dl1_table[\"hillas_intensity\"]),\n",
     ")\n",
-    "plt.xlabel('log10(E / TeV)')\n",
-    "plt.ylabel('log10(intensity)')\n",
+    "plt.xlabel(\"log10(E / TeV)\")\n",
+    "plt.ylabel(\"log10(intensity)\")\n",
     "None"
    ]
   },
@@ -892,10 +906,10 @@
    "outputs": [],
    "source": [
     "from ctapipe.image import toymodel\n",
-    "from ctapipe.instrument import CameraGeometry\n",
+    "from ctapipe.instrument import SubarrayDescription\n",
     "\n",
     "\n",
-    "geometry = CameraGeometry.from_name('LSTCam')"
+    "geometry = loader.subarray.tel[1].camera.geometry"
    ]
   },
   {
@@ -921,7 +935,7 @@
     "\n",
     "\n",
     "for i in range(9):\n",
-    "    \n",
+    "\n",
     "    model = toymodel.Gaussian(\n",
     "        x=np.random.uniform(-0.8, 0.8) * u.m,\n",
     "        y=np.random.uniform(-0.8, 0.8) * u.m,\n",
@@ -931,9 +945,7 @@
     "    )\n",
     "\n",
     "    new_image, sig, bg = model.generate_image(\n",
-    "        geometry, \n",
-    "        intensity=np.random.uniform(1000, 3000),\n",
-    "        nsb_level_pe=5\n",
+    "        geometry, intensity=np.random.uniform(1000, 3000), nsb_level_pe=5\n",
     "    )\n",
     "    image += new_image"
    ]
@@ -946,7 +958,13 @@
    },
    "outputs": [],
    "source": [
-    "clean = tailcuts_clean(geometry, image, picture_thresh=10, boundary_thresh=5, min_number_picture_neighbors=2)"
+    "clean = tailcuts_clean(\n",
+    "    geometry,\n",
+    "    image,\n",
+    "    picture_thresh=10,\n",
+    "    boundary_thresh=5,\n",
+    "    min_number_picture_neighbors=2,\n",
+    ")"
    ]
   },
   {
@@ -959,7 +977,7 @@
    "source": [
     "disp = CameraDisplay(geometry)\n",
     "disp.image = image\n",
-    "disp.highlight_pixels(clean, color='xkcd:red', linewidth=1.5)\n",
+    "disp.highlight_pixels(clean, color=\"xkcd:red\", linewidth=1.5)\n",
     "disp.add_colorbar()"
    ]
   },
@@ -972,15 +990,15 @@
    "outputs": [],
    "source": [
     "def num_islands_python(camera, clean):\n",
-    "    ''' A breadth first search to find connected islands of neighboring pixels in the cleaning set'''\n",
-    "    \n",
+    "    \"\"\"A breadth first search to find connected islands of neighboring pixels in the cleaning set\"\"\"\n",
+    "\n",
     "    # the camera geometry has a [n_pixel, n_pixel] boolean array\n",
     "    # that is True where two pixels are neighbors\n",
     "    neighbors = camera.neighbor_matrix\n",
-    "    \n",
+    "\n",
     "    island_ids = np.zeros(camera.n_pixels)\n",
     "    current_island = 0\n",
-    "    \n",
+    "\n",
     "    # a set to remember which pixels we already visited\n",
     "    visited = set()\n",
     "\n",
@@ -989,22 +1007,22 @@
     "        if pix_id not in visited:\n",
     "            # remember that we already checked this pixel\n",
     "            visited.add(pix_id)\n",
-    "            \n",
+    "\n",
     "            # if we land in the outer loop again, we found a new island\n",
     "            current_island += 1\n",
     "            island_ids[pix_id] = current_island\n",
-    "            \n",
+    "\n",
     "            # now check all neighbors of the current pixel recursively\n",
     "            to_check = set(np.where(neighbors[pix_id] & clean)[0])\n",
     "            while to_check:\n",
     "                pix_id = to_check.pop()\n",
-    "                \n",
-    "                if pix_id not in visited:    \n",
+    "\n",
+    "                if pix_id not in visited:\n",
     "                    visited.add(pix_id)\n",
     "                    island_ids[pix_id] = current_island\n",
-    "                    \n",
+    "\n",
     "                    to_check.update(np.where(neighbors[pix_id] & clean)[0])\n",
-    "    \n",
+    "\n",
     "    n_islands = current_island\n",
     "    return n_islands, island_ids"
    ]
@@ -1030,9 +1048,9 @@
    "source": [
     "from matplotlib.colors import ListedColormap\n",
     "\n",
-    "cmap = plt.get_cmap('Paired')\n",
+    "cmap = plt.get_cmap(\"Paired\")\n",
     "cmap = ListedColormap(cmap.colors[:n_islands])\n",
-    "cmap.set_under('k')\n",
+    "cmap.set_under(\"k\")\n",
     "\n",
     "disp = CameraDisplay(geometry)\n",
     "disp.image = island_ids\n",
@@ -1062,15 +1080,16 @@
    "source": [
     "from scipy.sparse.csgraph import connected_components\n",
     "\n",
+    "\n",
     "def num_islands_scipy(geometry, clean):\n",
     "    neighbors = geometry.neighbor_matrix_sparse\n",
-    "    \n",
+    "\n",
     "    clean_neighbors = neighbors[clean][:, clean]\n",
     "    num_islands, labels = connected_components(clean_neighbors, directed=False)\n",
-    "    \n",
+    "\n",
     "    island_ids = np.zeros(geometry.n_pixels)\n",
     "    island_ids[clean] = labels + 1\n",
-    "    \n",
+    "\n",
     "    return num_islands, island_ids"
    ]
   },
@@ -1153,7 +1172,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.13"
+   "version": "3.9.13"
   }
  },
  "nbformat": 4,
diff --git a/examples/camera_animation.py b/examples/camera_animation.py
index 17c300aa..197a1097 100755
--- a/examples/camera_animation.py
+++ b/examples/camera_animation.py
@@ -12,7 +12,7 @@ from astropy import units as u
 from matplotlib.animation import FuncAnimation
 
 from ctapipe.image import toymodel
-from ctapipe.instrument import TelescopeDescription
+from ctapipe.instrument import SubarrayDescription
 from ctapipe.visualization import CameraDisplay
 
 if __name__ == "__main__":
@@ -21,8 +21,8 @@ if __name__ == "__main__":
     fig, ax = plt.subplots()
 
     # load the camera
-    tel = TelescopeDescription.from_name("SST-1M", "DigiCam")
-    geom = tel.camera.geometry
+    subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
+    geom = subarray.tel[1].camera.geometry
 
     fov = 0.3
     maxwid = 0.05
@@ -46,7 +46,11 @@ if __name__ == "__main__":
             length=length * u.m,
             psi=angle * u.deg,
         )
-        image, _, _ = model.generate_image(geom, intensity=intens, nsb_level_pe=5,)
+        image, _, _ = model.generate_image(
+            geom,
+            intensity=intens,
+            nsb_level_pe=5,
+        )
         disp.image = image
 
     anim = FuncAnimation(fig, update, interval=500)
diff --git a/examples/camera_display_multi.py b/examples/camera_display_multi.py
index 540a49e3..fc4e069c 100755
--- a/examples/camera_display_multi.py
+++ b/examples/camera_display_multi.py
@@ -8,20 +8,26 @@ simulations). Also shows how to change the color palette.
 import matplotlib.pylab as plt
 from astropy import units as u
 
-from ctapipe.image import hillas_parameters
-from ctapipe.image import tailcuts_clean
-from ctapipe.image import toymodel
-from ctapipe.instrument import CameraGeometry
+from ctapipe.image import hillas_parameters, tailcuts_clean, toymodel
+from ctapipe.instrument import CameraGeometry, SubarrayDescription
 from ctapipe.visualization import CameraDisplay
 
 
 def draw_several_cams(geom, ncams=4):
 
-    cmaps = ["jet", "afmhot", "terrain", "autumn"]
-    fig, axs = plt.subplots(1, ncams, figsize=(15, 4),)
+    cmaps = ["inferno", "viridis", "terrain", "autumn"]
+    fig, axs = plt.subplots(
+        1,
+        ncams,
+        figsize=(15, 4),
+    )
 
     for ii in range(ncams):
-        disp = CameraDisplay(geom, ax=axs[ii], title="CT{}".format(ii + 1),)
+        disp = CameraDisplay(
+            geom,
+            ax=axs[ii],
+            title="CT{}".format(ii + 1),
+        )
         disp.cmap = cmaps[ii]
 
         model = toymodel.Gaussian(
@@ -32,7 +38,11 @@ def draw_several_cams(geom, ncams=4):
             psi=ii * 20 * u.deg,
         )
 
-        image, _, _ = model.generate_image(geom, intensity=1500, nsb_level_pe=5,)
+        image, _, _ = model.generate_image(
+            geom,
+            intensity=1500,
+            nsb_level_pe=5,
+        )
 
         mask = tailcuts_clean(
             geom,
@@ -53,8 +63,9 @@ def draw_several_cams(geom, ncams=4):
 
 
 if __name__ == "__main__":
+    subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
 
-    hexgeom = CameraGeometry.from_name("LSTCam")
+    hexgeom = subarray.tel[1].camera.geometry
     recgeom = CameraGeometry.make_rectangular()
 
     draw_several_cams(recgeom)
diff --git a/examples/camera_norms.py b/examples/camera_norms.py
index 220eb22a..ecf834a5 100755
--- a/examples/camera_norms.py
+++ b/examples/camera_norms.py
@@ -8,7 +8,7 @@ from matplotlib.colors import PowerNorm
 from matplotlib.style import use
 
 from ctapipe.image import toymodel
-from ctapipe.instrument import CameraGeometry
+from ctapipe.instrument import SubarrayDescription
 from ctapipe.visualization import CameraDisplay
 
 if __name__ == "__main__":
@@ -16,15 +16,24 @@ if __name__ == "__main__":
     use("ggplot")
     # load the camera
     fig, axs = plt.subplots(1, 3, figsize=(15, 5))
-    geom = CameraGeometry.from_name("LSTCam")
+    subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
+    geom = subarray.tel[1].camera.geometry
 
     titles = "Linear Scale", "Log-Scale", "PowerNorm(gamma=2)"
 
     model = toymodel.Gaussian(
-        x=0.2 * u.m, y=0.0 * u.m, width=0.05 * u.m, length=0.15 * u.m, psi="35d",
+        x=0.2 * u.m,
+        y=0.0 * u.m,
+        width=0.05 * u.m,
+        length=0.15 * u.m,
+        psi="35d",
     )
 
-    image, sig, bg = model.generate_image(geom, intensity=1500, nsb_level_pe=5,)
+    image, sig, bg = model.generate_image(
+        geom,
+        intensity=1500,
+        nsb_level_pe=5,
+    )
 
     disps = []
     for ax, title in zip(axs, titles):
diff --git a/examples/camera_rotation.py b/examples/camera_rotation.py
index 2bbc20ee..e7b8f78c 100644
--- a/examples/camera_rotation.py
+++ b/examples/camera_rotation.py
@@ -3,11 +3,13 @@ from astropy import units as u
 from matplotlib import pyplot as plt
 
 from ctapipe.instrument import CameraGeometry
+from ctapipe.utils import get_dataset_path
 from ctapipe.visualization import CameraDisplay
 
 if __name__ == "__main__":
 
-    geom = CameraGeometry.from_name("Whipple109")
+    path = get_dataset_path("Whipple109.camgeom.fits.gz")
+    geom = CameraGeometry.from_table(path)
     image = np.random.uniform(size=geom.pix_id.shape)
 
     plt.figure(figsize=(10, 4))
diff --git a/examples/highlight_pixel.py b/examples/highlight_pixel.py
index 3a8f9adf..de95bb38 100755
--- a/examples/highlight_pixel.py
+++ b/examples/highlight_pixel.py
@@ -1,8 +1,8 @@
-from matplotlib import pyplot as plt
 import astropy.units as u
+from matplotlib import pyplot as plt
 
 from ctapipe.image import toymodel
-from ctapipe.instrument import CameraGeometry
+from ctapipe.instrument import SubarrayDescription
 from ctapipe.visualization import CameraDisplay
 
 if __name__ == "__main__":
@@ -12,7 +12,9 @@ if __name__ == "__main__":
     fig = plt.figure(figsize=(12, 8))
     ax = fig.add_subplot(1, 1, 1)
 
-    geom = CameraGeometry.from_name("NectarCam")
+    subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
+    geom = subarray.tel[1].camera.geometry
+
     disp = CameraDisplay(geom, ax=ax)
     disp.add_colorbar()
 
diff --git a/examples/plot_all_cameras.py b/examples/plot_all_cameras.py
deleted file mode 100644
index 4c1922ea..00000000
--- a/examples/plot_all_cameras.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import numpy as np
-from matplotlib import pyplot as plt
-
-from ctapipe.instrument import CameraDescription, CameraGeometry
-from ctapipe.visualization import CameraDisplay
-
-if __name__ == "__main__":
-
-    plt.style.use("bmh")
-
-    camera_names = CameraDescription.get_known_camera_names()
-    n_tels = len(camera_names)
-    n_rows = np.trunc(np.sqrt(n_tels)).astype(int)
-    n_cols = np.ceil(n_tels / n_rows).astype(int)
-    plt.figure(figsize=(15, 6))
-
-    for ii, name in enumerate(sorted(camera_names)):
-        print("plotting", name)
-        geom = CameraGeometry.from_name(name)
-        ax = plt.subplot(n_rows, n_cols, ii + 1)
-        disp = CameraDisplay(geom)
-        disp.image = np.random.uniform(size=geom.pix_id.shape)
-        disp.cmap = "viridis"
-        plt.xlabel("")
-        plt.ylabel("")
-
-    plt.tight_layout()
-    plt.show()
diff --git a/examples/plot_camera_frames.py b/examples/plot_camera_frames.py
index 4ce20be9..b61922fe 100644
--- a/examples/plot_camera_frames.py
+++ b/examples/plot_camera_frames.py
@@ -2,24 +2,30 @@
 Plot the same event in two camera displays showing the
 different coordinate frames for camera coordinates.
 """
-from ctapipe.instrument import CameraGeometry
-from ctapipe.visualization import CameraDisplay
+import astropy.units as u
 import matplotlib.pyplot as plt
+
 from ctapipe.coordinates import EngineeringCameraFrame
 from ctapipe.image.toymodel import Gaussian
-import astropy.units as u
+from ctapipe.instrument import SubarrayDescription
+from ctapipe.visualization import CameraDisplay
 
 
 def main():
     fig, axs = plt.subplots(1, 2, constrained_layout=True, figsize=(6, 3))
 
     model = Gaussian(0 * u.m, 0.1 * u.m, 0.3 * u.m, 0.05 * u.m, 25 * u.deg)
-    cam = CameraGeometry.from_name("FlashCam")
+
+    subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
+    cam = subarray.tel[5].camera.geometry
+
     image, *_ = model.generate_image(cam, 2500)
 
     CameraDisplay(cam, ax=axs[0], image=image)
     CameraDisplay(
-        cam.transform_to(EngineeringCameraFrame()), ax=axs[1], image=image,
+        cam.transform_to(EngineeringCameraFrame()),
+        ax=axs[1],
+        image=image,
     )
 
     axs[0].set_title("CameraFrame")
diff --git a/examples/plot_hillas_parameters.py b/examples/plot_hillas_parameters.py
index fd55801d..9c0b9edb 100755
--- a/examples/plot_hillas_parameters.py
+++ b/examples/plot_hillas_parameters.py
@@ -3,18 +3,18 @@
 Example of drawing a Camera using a toymodel shower image.
 """
 
-import matplotlib.pylab as plt
 import astropy.units as u
+import matplotlib.pylab as plt
 
-from ctapipe.image import toymodel, hillas_parameters, tailcuts_clean
-from ctapipe.instrument import CameraGeometry
+from ctapipe.image import hillas_parameters, tailcuts_clean, toymodel
+from ctapipe.instrument import SubarrayDescription
 from ctapipe.visualization import CameraDisplay
 
-
 if __name__ == "__main__":
 
     # Load the camera
-    geom = CameraGeometry.from_name("LSTCam")
+    subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
+    geom = subarray.tel[1].camera.geometry
     disp = CameraDisplay(geom)
     disp.add_colorbar()
 


Test patch:
diff --git a/ctapipe/calib/camera/tests/test_flatfield.py b/ctapipe/calib/camera/tests/test_flatfield.py
index 90a93e2b..f9387c25 100644
--- a/ctapipe/calib/camera/tests/test_flatfield.py
+++ b/ctapipe/calib/camera/tests/test_flatfield.py
@@ -1,13 +1,16 @@
+from copy import deepcopy
+
+import astropy.units as u
 import numpy as np
 from astropy.time import Time
+from traitlets.config.loader import Config
+
 from ctapipe.calib.camera.flatfield import FlasherFlatFieldCalculator
 from ctapipe.containers import ArrayEventContainer
-from traitlets.config.loader import Config
-import astropy.units as u
-from ctapipe.instrument import SubarrayDescription, TelescopeDescription
+from ctapipe.instrument import SubarrayDescription
 
 
-def test_flasherflatfieldcalculator():
+def test_flasherflatfieldcalculator(prod5_sst):
     """test of flasherFlatFieldCalculator"""
     tel_id = 0
     n_gain = 2
@@ -18,11 +21,7 @@ def test_flasherflatfieldcalculator():
     subarray = SubarrayDescription(
         "test array",
         tel_positions={0: np.zeros(3) * u.m},
-        tel_descriptions={
-            0: TelescopeDescription.from_name(
-                optics_name="SST-ASTRI", camera_name="CHEC"
-            )
-        },
+        tel_descriptions={0: deepcopy(prod5_sst)},
     )
     subarray.tel[0].camera.readout.reference_pulse_shape = np.ones((1, 2))
     subarray.tel[0].camera.readout.reference_pulse_sample_width = u.Quantity(1, u.ns)
diff --git a/ctapipe/calib/camera/tests/test_pedestals.py b/ctapipe/calib/camera/tests/test_pedestals.py
index b08a1bec..5cec5ec0 100644
--- a/ctapipe/calib/camera/tests/test_pedestals.py
+++ b/ctapipe/calib/camera/tests/test_pedestals.py
@@ -1,20 +1,22 @@
 """
 Tests for pedestal calculator functionality
 """
+from copy import deepcopy
+
 import astropy.units as u
-from astropy.time import Time
 import numpy as np
+from astropy.time import Time
 
 from ctapipe.calib.camera.pedestals import (
     PedestalIntegrator,
     calc_pedestals_from_traces,
 )
-from ctapipe.instrument import SubarrayDescription, TelescopeDescription
 from ctapipe.containers import ArrayEventContainer
+from ctapipe.instrument import SubarrayDescription
 
 
-def test_pedestal_calculator():
-    """ test of PedestalIntegrator """
+def test_pedestal_calculator(prod5_sst):
+    """test of PedestalIntegrator"""
 
     tel_id = 0
     n_events = 10
@@ -25,11 +27,7 @@ def test_pedestal_calculator():
     subarray = SubarrayDescription(
         "test array",
         tel_positions={0: np.zeros(3) * u.m},
-        tel_descriptions={
-            0: TelescopeDescription.from_name(
-                optics_name="SST-ASTRI", camera_name="CHEC"
-            )
-        },
+        tel_descriptions={0: deepcopy(prod5_sst)},
     )
     subarray.tel[0].camera.readout.reference_pulse_shape = np.ones((1, 2))
     subarray.tel[0].camera.readout.reference_pulse_sample_width = u.Quantity(1, u.ns)
@@ -61,7 +59,7 @@ def test_pedestal_calculator():
 
 
 def test_calc_pedestals_from_traces():
-    """ test calc_pedestals_from_traces """
+    """test calc_pedestals_from_traces"""
     # create some test data (all ones, but with a 2 stuck in for good measure):
     npix = 1000
     nsamp = 32
diff --git a/ctapipe/conftest.py b/ctapipe/conftest.py
index a40d0c0c..0442f342 100644
--- a/ctapipe/conftest.py
+++ b/ctapipe/conftest.py
@@ -7,7 +7,7 @@ from copy import deepcopy
 import pytest
 from pytest_astropy_header.display import PYTEST_HEADER_MODULES
 
-from ctapipe.instrument import CameraGeometry
+from ctapipe.instrument import CameraGeometry, SubarrayDescription
 from ctapipe.io import SimTelEventSource
 from ctapipe.utils import get_dataset_path
 from ctapipe.utils.filelock import FileLock
@@ -39,7 +39,8 @@ camera_names = [
 
 @pytest.fixture(scope="function", params=camera_names)
 def camera_geometry(request):
-    return CameraGeometry.from_name(request.param)
+    path = get_dataset_path(f"{request.param}.camgeom.fits.gz")
+    return CameraGeometry.from_table(path)
 
 
 @pytest.fixture(scope="session")
@@ -62,18 +63,24 @@ def _global_example_event():
 
 
 @pytest.fixture(scope="session")
-def example_subarray():
+def subarray_prod5_paranal(prod5_gamma_simtel_path):
+    return SubarrayDescription.read(prod5_gamma_simtel_path)
+
+
+@pytest.fixture(scope="session")
+def subarray_prod3_paranal():
+    return SubarrayDescription.read(
+        "dataset://gamma_test_large.simtel.gz",
+        focal_length_choice="EQUIVALENT",
+    )
+
+
+@pytest.fixture(scope="session")
+def example_subarray(subarray_prod3_paranal):
     """
     Subarray corresponding to the example event
     """
-    filename = get_dataset_path("gamma_test_large.simtel.gz")
-
-    print("******************** LOAD TEST EVENT ***********************")
-
-    with SimTelEventSource(
-        input_url=filename, focal_length_choice="EQUIVALENT"
-    ) as reader:
-        return reader.subarray
+    return subarray_prod3_paranal
 
 
 @pytest.fixture(scope="function")
@@ -146,6 +153,36 @@ def prod5_proton_simtel_path():
     )
 
 
+@pytest.fixture(scope="session")
+def prod5_lst(subarray_prod5_paranal):
+    return subarray_prod5_paranal.tel[1]
+
+
+@pytest.fixture(scope="session")
+def prod3_lst(subarray_prod3_paranal):
+    return subarray_prod3_paranal.tel[1]
+
+
+@pytest.fixture(scope="session")
+def prod5_mst_flashcam(subarray_prod5_paranal):
+    return subarray_prod5_paranal.tel[5]
+
+
+@pytest.fixture(scope="session")
+def prod5_mst_nectarcam(subarray_prod5_paranal):
+    return subarray_prod5_paranal.tel[100]
+
+
+@pytest.fixture(scope="session")
+def prod5_sst(subarray_prod5_paranal):
+    return subarray_prod5_paranal.tel[60]
+
+
+@pytest.fixture(scope="session")
+def prod3_astri(subarray_prod3_paranal):
+    return subarray_prod3_paranal.tel[98]
+
+
 @pytest.fixture(scope="session")
 def dl1_tmp_path(tmp_path_factory):
     """Temporary directory for global dl1 test data"""
diff --git a/ctapipe/core/tests/test_component.py b/ctapipe/core/tests/test_component.py
index 2df00d5a..4daf126a 100644
--- a/ctapipe/core/tests/test_component.py
+++ b/ctapipe/core/tests/test_component.py
@@ -1,10 +1,9 @@
-from abc import abstractmethod, ABC
+import warnings
+from abc import ABC, abstractmethod
 
 import pytest
-from traitlets import Float, TraitError, Int
+from traitlets import Float, Int, TraitError
 from traitlets.config.loader import Config
-import astropy.units as u
-import warnings
 
 from ctapipe.core import Component, Tool
 
@@ -389,15 +388,8 @@ def test_component_html_repr():
     assert len(html) > 10
 
 
-def test_telescope_component():
+def test_telescope_component(subarray_prod5_paranal):
     from ctapipe.core import TelescopeComponent
-    from ctapipe.instrument import SubarrayDescription, TelescopeDescription
-
-    subarray = SubarrayDescription(
-        "test",
-        tel_positions={1: [0, 0, 0] * u.m},
-        tel_descriptions={1: TelescopeDescription.from_name("LST", "LSTCam")},
-    )
 
     class Base(TelescopeComponent):
         pass
@@ -405,7 +397,7 @@ def test_telescope_component():
     class Sub(Base):
         pass
 
-    assert isinstance(Base.from_name("Sub", subarray=subarray), Sub)
+    assert isinstance(Base.from_name("Sub", subarray=subarray_prod5_paranal), Sub)
 
 
 def test_full_config():
diff --git a/ctapipe/image/muon/tests/test_intensity_fit.py b/ctapipe/image/muon/tests/test_intensity_fit.py
index 95b7fb01..a1048aba 100644
--- a/ctapipe/image/muon/tests/test_intensity_fit.py
+++ b/ctapipe/image/muon/tests/test_intensity_fit.py
@@ -19,16 +19,20 @@ def test_chord_length():
     assert np.isclose(length, 0, atol=1e-15)
 
 
-def test_muon_efficiency_fit():
-    from ctapipe.instrument import TelescopeDescription, SubarrayDescription
-    from ctapipe.coordinates import TelescopeFrame, CameraFrame
+def test_muon_efficiency_fit(prod5_lst):
+    from ctapipe.coordinates import CameraFrame, TelescopeFrame
     from ctapipe.image.muon.intensity_fitter import (
-        image_prediction,
         MuonIntensityFitter,
+        image_prediction,
     )
+    from ctapipe.instrument import SubarrayDescription
 
-    telescope = TelescopeDescription.from_name("LST", "LSTCam")
-    subarray = SubarrayDescription("LSTMono", {0: [0, 0, 0] * u.m}, {0: telescope},)
+    telescope = prod5_lst
+    subarray = SubarrayDescription(
+        "LSTMono",
+        {0: [0, 0, 0] * u.m},
+        {0: telescope},
+    )
 
     center_x = 0.8 * u.deg
     center_y = 0.4 * u.deg
@@ -87,12 +91,16 @@ def test_muon_efficiency_fit():
     assert u.isclose(result.optical_efficiency, efficiency, rtol=0.05)
 
 
-def test_scts():
-    from ctapipe.instrument import TelescopeDescription, SubarrayDescription
+def test_scts(prod5_sst):
     from ctapipe.image.muon.intensity_fitter import MuonIntensityFitter
+    from ctapipe.instrument import SubarrayDescription
 
-    telescope = TelescopeDescription.from_name("SST-ASTRI", "CHEC")
-    subarray = SubarrayDescription("ssts", {0: [0, 0, 0] * u.m}, {0: telescope},)
+    telescope = prod5_sst
+    subarray = SubarrayDescription(
+        "ssts",
+        {0: [0, 0, 0] * u.m},
+        {0: telescope},
+    )
 
     fitter = MuonIntensityFitter(subarray=subarray)
     with pytest.raises(NotImplementedError):
diff --git a/ctapipe/image/muon/tests/test_ring_fitter.py b/ctapipe/image/muon/tests/test_ring_fitter.py
index c00448bd..f71af2ed 100644
--- a/ctapipe/image/muon/tests/test_ring_fitter.py
+++ b/ctapipe/image/muon/tests/test_ring_fitter.py
@@ -1,8 +1,8 @@
-import pytest
 import astropy.units as u
-from ctapipe.instrument import CameraGeometry
-from ctapipe.image.muon import MuonRingFitter
+import pytest
+
 from ctapipe.image import tailcuts_clean, toymodel
+from ctapipe.image.muon import MuonRingFitter
 
 
 def test_MuonRingFitter_has_methods():
@@ -12,7 +12,7 @@ def test_MuonRingFitter_has_methods():
 
 
 @pytest.mark.parametrize("method", MuonRingFitter.fit_method.values)
-def test_MuonRingFitter(method):
+def test_MuonRingFitter(method, prod5_mst_flashcam):
     """test MuonRingFitter"""
     # flashCam example
     center_xs = 0.3 * u.m
@@ -21,12 +21,19 @@ def test_MuonRingFitter(method):
     width = 0.05 * u.m
 
     muon_model = toymodel.RingGaussian(
-        x=center_xs, y=center_ys, radius=radius, sigma=width,
+        x=center_xs,
+        y=center_ys,
+        radius=radius,
+        sigma=width,
     )
 
     # testing with flashcam
-    geom = CameraGeometry.from_name("FlashCam")
-    charge, _, _ = muon_model.generate_image(geom, intensity=1000, nsb_level_pe=5,)
+    geom = prod5_mst_flashcam.camera.geometry
+    charge, _, _ = muon_model.generate_image(
+        geom,
+        intensity=1000,
+        nsb_level_pe=5,
+    )
     survivors = tailcuts_clean(geom, charge, 10, 12)
 
     muonfit = MuonRingFitter(fit_method=method)
diff --git a/ctapipe/image/tests/test_cleaning.py b/ctapipe/image/tests/test_cleaning.py
index d004f7bc..9ba107f7 100644
--- a/ctapipe/image/tests/test_cleaning.py
+++ b/ctapipe/image/tests/test_cleaning.py
@@ -1,12 +1,13 @@
+import astropy.units as u
 import numpy as np
 from numpy.testing import assert_allclose
+
 from ctapipe.image import cleaning
 from ctapipe.instrument import CameraGeometry
-import astropy.units as u
 
 
-def test_tailcuts_clean_simple():
-    geom = CameraGeometry.from_name("LSTCam")
+def test_tailcuts_clean_simple(prod5_lst):
+    geom = prod5_lst.camera.geometry
     image = np.zeros_like(geom.pix_id, dtype=np.float64)
 
     num_pix = 40
@@ -22,8 +23,8 @@ def test_tailcuts_clean_simple():
     assert np.count_nonzero(mask) == 4
 
 
-def test_dilate():
-    geom = CameraGeometry.from_name("LSTCam")
+def test_dilate(prod5_lst):
+    geom = prod5_lst.camera.geometry
     mask = np.zeros_like(geom.pix_id, dtype=bool)
 
     mask[100] = True  # a single pixel far from a border is true.
@@ -70,10 +71,10 @@ def test_tailcuts_clean():
         assert (result == mask).all()
 
 
-def test_tailcuts_clean_threshold_array():
+def test_tailcuts_clean_threshold_array(prod5_lst):
     """Tests that tailcuts can also work with individual thresholds per pixel"""
     rng = np.random.default_rng(1337)
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
 
     # artifical event having a "shower" and a "star" at these locations
     star_x = 0.5 * u.m
@@ -255,9 +256,9 @@ def test_tailcuts_clean_with_isolated_pixels():
         assert (result == mask).all()
 
 
-def test_fact_image_cleaning():
+def test_fact_image_cleaning(prod3_lst):
     # use LST pixel geometry
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod3_lst.camera.geometry
     # create some signal pixels
     values = np.zeros(len(geom))
     timing = np.zeros(len(geom))
@@ -287,8 +288,8 @@ def test_fact_image_cleaning():
     assert_allclose(mask, expected_mask)
 
 
-def test_apply_time_delta_cleaning():
-    geom = CameraGeometry.from_name("LSTCam")
+def test_apply_time_delta_cleaning(prod3_lst):
+    geom = prod3_lst.camera.geometry
     peak_time = np.zeros(geom.n_pixels, dtype=np.float64)
 
     pixel = 40
diff --git a/ctapipe/image/tests/test_concentration.py b/ctapipe/image/tests/test_concentration.py
index ee277442..25dd14e0 100644
--- a/ctapipe/image/tests/test_concentration.py
+++ b/ctapipe/image/tests/test_concentration.py
@@ -1,12 +1,15 @@
-from ctapipe.image.tests.test_hillas import create_sample_image
-from ctapipe.image.hillas import hillas_parameters
-from ctapipe.image.concentration import concentration_parameters
 import astropy.units as u
 import pytest
 
+from ctapipe.image.concentration import concentration_parameters
+from ctapipe.image.hillas import hillas_parameters
+from ctapipe.image.tests.test_hillas import create_sample_image
+
+
+def test_concentration(prod5_lst):
 
-def test_concentration():
-    geom, image, clean_mask = create_sample_image("30d")
+    geom = prod5_lst.camera.geometry
+    image, clean_mask = create_sample_image(psi="30d", geometry=geom)
 
     hillas = hillas_parameters(geom[clean_mask], image[clean_mask])
 
@@ -18,8 +21,9 @@ def test_concentration():
 
 
 @pytest.mark.filterwarnings("error")
-def test_width_0():
-    geom, image, clean_mask = create_sample_image("30d")
+def test_width_0(prod5_lst):
+    geom = prod5_lst.camera.geometry
+    image, clean_mask = create_sample_image(psi="30d", geometry=geom)
 
     hillas = hillas_parameters(geom[clean_mask], image[clean_mask])
     hillas.width = 0 * u.m
@@ -28,19 +32,16 @@ def test_width_0():
     assert conc.core == 0
 
 
-def test_no_pixels_near_cog():
-    geom, image, clean_mask = create_sample_image("30d")
+def test_no_pixels_near_cog(prod5_lst):
+    geom = prod5_lst.camera.geometry
+    image, clean_mask = create_sample_image(psi="30d", geometry=geom)
 
     hillas = hillas_parameters(geom[clean_mask], image[clean_mask])
 
     # remove pixels close to cog from the cleaning pixels
     clean_mask &= ((geom.pix_x - hillas.x) ** 2 + (geom.pix_y - hillas.y) ** 2) >= (
-        2 * geom.pixel_width ** 2
+        2 * geom.pixel_width**2
     )
 
     conc = concentration_parameters(geom[clean_mask], image[clean_mask], hillas)
     assert conc.cog == 0
-
-
-if __name__ == "__main__":
-    test_concentration()
diff --git a/ctapipe/image/tests/test_extractor.py b/ctapipe/image/tests/test_extractor.py
index fb683ec9..b93071b2 100644
--- a/ctapipe/image/tests/test_extractor.py
+++ b/ctapipe/image/tests/test_extractor.py
@@ -1,6 +1,13 @@
+from copy import deepcopy
+
 import astropy.units as u
 import numpy as np
 import pytest
+from numpy.testing import assert_allclose, assert_equal
+from scipy.stats import norm
+from traitlets.config.loader import Config
+from traitlets.traitlets import TraitError
+
 from ctapipe.core import non_abstract_children
 from ctapipe.image.extractor import (
     FixedWindowSum,
@@ -16,11 +23,7 @@ from ctapipe.image.extractor import (
     subtract_baseline,
 )
 from ctapipe.image.toymodel import SkewedGaussian, WaveformModel, obtain_time_image
-from ctapipe.instrument import SubarrayDescription, TelescopeDescription
-from numpy.testing import assert_allclose, assert_equal
-from scipy.stats import norm
-from traitlets.config.loader import Config
-from traitlets.traitlets import TraitError
+from ctapipe.instrument import SubarrayDescription
 
 extractors = non_abstract_children(ImageExtractor)
 # FixedWindowSum has no peak finding and need to be set manually
@@ -28,17 +31,13 @@ extractors.remove(FixedWindowSum)
 
 
 @pytest.fixture(scope="module")
-def subarray():
+def subarray(prod5_sst):
     subarray = SubarrayDescription(
         "test array",
         tel_positions={1: np.zeros(3) * u.m, 2: np.zeros(3) * u.m},
         tel_descriptions={
-            1: TelescopeDescription.from_name(
-                optics_name="SST-ASTRI", camera_name="CHEC"
-            ),
-            2: TelescopeDescription.from_name(
-                optics_name="SST-ASTRI", camera_name="CHEC"
-            ),
+            1: deepcopy(prod5_sst),
+            2: deepcopy(prod5_sst),
         },
     )
 
@@ -60,13 +59,11 @@ def subarray():
 
 
 @pytest.fixture(scope="module")
-def subarray_1_LST():
+def subarray_1_LST(prod3_lst):
     subarray = SubarrayDescription(
         "One LST",
         tel_positions={1: np.zeros(3) * u.m},
-        tel_descriptions={
-            1: TelescopeDescription.from_name(optics_name="LST", camera_name="LSTCam")
-        },
+        tel_descriptions={1: prod3_lst},
     )
     return subarray
 
diff --git a/ctapipe/image/tests/test_hillas.py b/ctapipe/image/tests/test_hillas.py
index 16fff8c0..0c8c8e8a 100644
--- a/ctapipe/image/tests/test_hillas.py
+++ b/ctapipe/image/tests/test_hillas.py
@@ -1,18 +1,20 @@
-from astropy.coordinates import Angle, SkyCoord
-from astropy import units as u
-import numpy as np
-from numpy import isclose, zeros_like
-from pytest import approx
 import itertools
+
+import numpy as np
 import pytest
-from ctapipe.coordinates import TelescopeFrame, CameraFrame
-from ctapipe.instrument import CameraGeometry
-from ctapipe.image import tailcuts_clean, toymodel
-from ctapipe.image.hillas import hillas_parameters, HillasParameterizationError
+from astropy import units as u
+from astropy.coordinates import Angle, SkyCoord
+from numpy import isclose
+from pytest import approx
+
 from ctapipe.containers import (
-    HillasParametersContainer,
     CameraHillasParametersContainer,
+    HillasParametersContainer,
 )
+from ctapipe.coordinates import TelescopeFrame
+from ctapipe.image import tailcuts_clean, toymodel
+from ctapipe.image.hillas import HillasParameterizationError, hillas_parameters
+from ctapipe.instrument import CameraGeometry, SubarrayDescription
 
 
 def create_sample_image(
@@ -22,37 +24,26 @@ def create_sample_image(
     width=0.05 * u.m,
     length=0.15 * u.m,
     intensity=1500,
+    geometry=None,
 ):
 
-    geom = CameraGeometry.from_name("LSTCam")
+    if geometry is None:
+        s = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
+        geometry = s.tel[1].camera.geometry
 
     # make a toymodel shower model
     model = toymodel.Gaussian(x=x, y=y, width=width, length=length, psi=psi)
 
     # generate toymodel image in camera for this shower model.
     rng = np.random.default_rng(0)
-    image, _, _ = model.generate_image(geom, intensity=1500, nsb_level_pe=3, rng=rng)
+    image, _, _ = model.generate_image(
+        geometry, intensity=intensity, nsb_level_pe=3, rng=rng
+    )
 
     # calculate pixels likely containing signal
-    clean_mask = tailcuts_clean(geom, image, 10, 5)
-
-    return geom, image, clean_mask
-
-
-def create_sample_image_zeros(psi="-30d"):
-
-    geom, image, clean_mask = create_sample_image(psi)
-
-    # threshold in pe
-    image[~clean_mask] = 0
-
-    return geom, image
+    clean_mask = tailcuts_clean(geometry, image, 10, 5)
 
-
-def create_sample_image_selected_pixel(psi="-30d"):
-    geom, image, clean_mask = create_sample_image(psi)
-
-    return geom[clean_mask], image[clean_mask]
+    return image, clean_mask
 
 
 def compare_result(x, y):
@@ -69,30 +60,37 @@ def compare_hillas(hillas1, hillas2):
         compare_result(hillas1_dict[key], hillas2_dict[key])
 
 
-def test_hillas_selected():
+def test_hillas_selected(prod5_lst):
     """
     test Hillas-parameter routines on a sample image with selected values
-    against a sample image with masked values set tozero
+    against a sample image with masked values set to zero
     """
-    geom, image = create_sample_image_zeros()
-    geom_selected, image_selected = create_sample_image_selected_pixel()
+    geom = prod5_lst.camera.geometry
+    image, clean_mask = create_sample_image(geometry=geom)
+
+    image_zeros = image.copy()
+    image_zeros[~clean_mask] = 0.0
+
+    image_selected = image[clean_mask]
+    geom_selected = geom[clean_mask]
 
-    results = hillas_parameters(geom, image)
+    results = hillas_parameters(geom, image_zeros)
     results_selected = hillas_parameters(geom_selected, image_selected)
 
     compare_hillas(results, results_selected)
 
 
-def test_hillas_failure():
-    geom, image = create_sample_image_zeros(psi="0d")
-    blank_image = zeros_like(image)
+def test_hillas_failure(prod5_lst):
+    geom = prod5_lst.camera.geometry
+    blank_image = np.zeros(geom.n_pixels)
 
     with pytest.raises(HillasParameterizationError):
         hillas_parameters(geom, blank_image)
 
 
-def test_hillas_masked_array():
-    geom, image, clean_mask = create_sample_image(psi="0d")
+def test_hillas_masked_array(prod5_lst):
+    geom = prod5_lst.camera.geometry
+    image, clean_mask = create_sample_image(psi="0d", geometry=geom)
 
     image_zeros = image.copy()
     image_zeros[~clean_mask] = 0
@@ -104,22 +102,22 @@ def test_hillas_masked_array():
     compare_hillas(hillas_zeros, hillas_masked)
 
 
-def test_hillas_container():
-    geom, image = create_sample_image_zeros(psi="0d")
+def test_hillas_container(prod5_lst):
+    geom = prod5_lst.camera.geometry
+    image, clean_mask = create_sample_image(psi="0d", geometry=geom)
 
-    params = hillas_parameters(geom, image)
+    params = hillas_parameters(geom[clean_mask], image[clean_mask])
     assert isinstance(params, CameraHillasParametersContainer)
 
-    geom.frame = CameraFrame(focal_length=28 * u.m)
     geom_telescope_frame = geom.transform_to(TelescopeFrame())
-    params = hillas_parameters(geom_telescope_frame, image)
+    params = hillas_parameters(geom_telescope_frame[clean_mask], image[clean_mask])
     assert isinstance(params, HillasParametersContainer)
 
 
-def test_with_toy():
+def test_with_toy(prod5_lst):
     rng = np.random.default_rng(42)
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
 
     width = 0.03 * u.m
     length = 0.15 * u.m
@@ -157,10 +155,10 @@ def test_with_toy():
             assert signal.sum() == result.intensity
 
 
-def test_skewness():
+def test_skewness(prod5_lst):
     rng = np.random.default_rng(42)
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
 
     width = 0.03 * u.m
     length = 0.15 * u.m
@@ -205,7 +203,7 @@ def test_skewness():
 
 @pytest.mark.filterwarnings("error")
 def test_straight_line_width_0():
-    """ Test that hillas_parameters.width is 0 for a straight line of pixels """
+    """Test that hillas_parameters.width is 0 for a straight line of pixels"""
     # three pixels in a straight line
     long = np.array([0, 1, 2]) * 0.01
     trans = np.zeros(len(long))
@@ -225,7 +223,7 @@ def test_straight_line_width_0():
                     pix_x=x * u.m,
                     pix_y=y * u.m,
                     pix_type="hexagonal",
-                    pix_area=1 * u.m ** 2,
+                    pix_area=1 * u.m**2,
                 )
 
                 img = rng.poisson(5, size=len(long))
@@ -245,7 +243,7 @@ def test_single_pixel():
         pix_x=x.ravel() * u.cm,
         pix_y=y.ravel() * u.cm,
         pix_type="rectangular",
-        pix_area=1 * u.cm ** 2,
+        pix_area=1 * u.cm**2,
     )
 
     image = np.zeros((3, 3))
@@ -259,18 +257,16 @@ def test_single_pixel():
     assert np.isnan(hillas.psi)
 
 
-def test_reconstruction_in_telescope_frame():
+def test_reconstruction_in_telescope_frame(prod5_lst):
     """
     Compare the reconstruction in the telescope
     and camera frame.
     """
     np.random.seed(42)
 
+    geom = prod5_lst.camera.geometry
     telescope_frame = TelescopeFrame()
-    camera_frame = CameraFrame(focal_length=28 * u.m)
-
-    geom = CameraGeometry.from_name("LSTCam")
-    geom.frame = camera_frame
+    camera_frame = geom.frame
     geom_nom = geom.transform_to(telescope_frame)
 
     width = 0.03 * u.m
diff --git a/ctapipe/image/tests/test_image_cleaner_component.py b/ctapipe/image/tests/test_image_cleaner_component.py
index 57f49aa0..7799d568 100644
--- a/ctapipe/image/tests/test_image_cleaner_component.py
+++ b/ctapipe/image/tests/test_image_cleaner_component.py
@@ -3,12 +3,12 @@ import pytest
 from traitlets.config import Config
 
 from ctapipe.image import ImageCleaner
-from ctapipe.instrument import TelescopeDescription, SubarrayDescription
+from ctapipe.instrument import SubarrayDescription
 
 
 @pytest.mark.parametrize("method", ImageCleaner.non_abstract_subclasses().keys())
-def test_image_cleaner(method):
-    """ Test that we can construct and use a component-based ImageCleaner"""
+def test_image_cleaner(method, prod5_mst_nectarcam):
+    """Test that we can construct and use a component-based ImageCleaner"""
 
     config = Config(
         {
@@ -28,14 +28,18 @@ def test_image_cleaner(method):
         }
     )
 
-    tel = TelescopeDescription.from_name("MST", "NectarCam")
     subarray = SubarrayDescription(
-        name="test", tel_positions={1: None}, tel_descriptions={1: tel}
+        name="test",
+        tel_positions={1: None},
+        tel_descriptions={1: prod5_mst_nectarcam},
     )
 
     clean = ImageCleaner.from_name(method, config=config, subarray=subarray)
 
-    image = np.zeros_like(tel.camera.geometry.pix_x.value, dtype=np.float64)
+    image = np.zeros_like(
+        prod5_mst_nectarcam.camera.geometry.pix_x.value,
+        dtype=np.float64,
+    )
     image[10:30] = 20.0
     image[31:40] = 8.0
     times = np.linspace(-5, 10, image.shape[0])
diff --git a/ctapipe/image/tests/test_leakage.py b/ctapipe/image/tests/test_leakage.py
index 2eb2cdca..7545d3cc 100644
--- a/ctapipe/image/tests/test_leakage.py
+++ b/ctapipe/image/tests/test_leakage.py
@@ -1,11 +1,10 @@
-from ctapipe.instrument.camera import CameraGeometry
 import numpy as np
 
 
-def test_leakage():
+def test_leakage(prod5_lst):
     from ctapipe.image.leakage import leakage_parameters
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
 
     img = np.ones(geom.n_pixels)
     mask = np.ones(len(geom), dtype=bool)
diff --git a/ctapipe/image/tests/test_modifications.py b/ctapipe/image/tests/test_modifications.py
index 27304942..2321ab41 100644
--- a/ctapipe/image/tests/test_modifications.py
+++ b/ctapipe/image/tests/test_modifications.py
@@ -1,6 +1,7 @@
 import numpy as np
-from ctapipe.instrument import CameraGeometry
+
 from ctapipe.image import modifications
+from ctapipe.instrument import CameraGeometry
 
 
 def test_add_noise():
@@ -27,7 +28,7 @@ def test_add_noise():
     assert np.sum(diff_no_bias) > np.sum(noisy - image)
 
 
-def test_smear_image():
+def test_smear_image(prod5_lst):
     """
     Test that smearing the image leads to the expected results.
     For random smearing this will not work with all seeds.
@@ -41,35 +42,37 @@ def test_smear_image():
     seed = 20
 
     # Hexagonal geometry -> Thats why we divide by 6 below
-    geom = CameraGeometry.from_name("LSTCam")
+    geom: CameraGeometry = prod5_lst.camera.geometry
     image = np.zeros_like(geom.pix_id, dtype=np.float64)
     # select two pixels, one at the edge with only 5 neighbors
-    signal_pixels = [1, 1853]
-    neighbors = geom.neighbor_matrix[signal_pixels]
-
-    for signal_value in [1, 5]:
-        image[signal_pixels] = signal_value
-        for fraction in [0, 0.2, 1]:
-            # random smearing
-            # The seed is important here (See below)
-            smeared = modifications._smear_psf_randomly(
-                image,
-                fraction=fraction,
-                indices=geom.neighbor_matrix_sparse.indices,
-                indptr=geom.neighbor_matrix_sparse.indptr,
-                smear_probabilities=np.full(6, 1 / 6),
-                seed=seed,
-            )
-            neighbors_1 = smeared[neighbors[0]]
-
-            # this can be False if the "pseudo neighbor" of pixel
-            # 1853 is selected (outside of the camera)
-            assert np.isclose(image.sum(), smeared.sum())
-            assert np.isclose(np.sum(neighbors_1) + smeared[1], image[1])
-            # this can be False if for both pixels a 0 is
-            # drawn from the poissonian (especially with signal value 1)
-            if fraction > 0:
-                assert not ((image > 0) == (smeared > 0)).all()
+
+    border_mask = geom.get_border_pixel_mask()
+    # one border pixel, one central pixel
+    signal_pixels = [np.nonzero(~border_mask)[0][0], np.nonzero(border_mask)[0][0]]
+
+    signal_value = 10
+
+    for signal_pixel in signal_pixels:
+        for signal_value in [1, 10]:
+            image[signal_pixel] = signal_value
+            for fraction in [0, 0.2, 1]:
+                smeared = modifications._smear_psf_randomly(
+                    image,
+                    fraction=fraction,
+                    indices=geom.neighbor_matrix_sparse.indices,
+                    indptr=geom.neighbor_matrix_sparse.indptr,
+                    smear_probabilities=np.full(6, 1 / 6),
+                    seed=seed,
+                )
+
+                # we may loose charge at the border of the camera
+                if border_mask[signal_pixel]:
+                    assert image.sum() >= smeared.sum()
+                else:
+                    assert image.sum() == smeared.sum()
+
+                if fraction > 0:
+                    assert image[signal_pixel] >= smeared[signal_pixel]
 
 
 def test_defaults_no_change(example_subarray):
diff --git a/ctapipe/image/tests/test_morphology.py b/ctapipe/image/tests/test_morphology.py
index 9437b377..cabab0eb 100644
--- a/ctapipe/image/tests/test_morphology.py
+++ b/ctapipe/image/tests/test_morphology.py
@@ -1,13 +1,13 @@
 import numpy as np
 from numpy.testing import assert_allclose
+
 from ctapipe.instrument import CameraGeometry
 
 
-def test_number_of_islands():
+def test_number_of_islands(prod3_lst):
     from ctapipe.image import number_of_islands
 
-    # test with LST geometry (1855 pixels)
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod3_lst.camera.geometry
 
     # create 18 triggered pixels grouped to 5 clusters
     mask = np.zeros(geom.n_pixels).astype("bool")
@@ -60,7 +60,7 @@ def test_number_of_island_sizes():
 
 def test_largest_island():
     """Test selection of largest island in imagea with given cleaning masks."""
-    from ctapipe.image import number_of_islands, largest_island
+    from ctapipe.image import largest_island, number_of_islands
 
     # Create a simple rectangular camera made of 17 pixels
     camera = CameraGeometry.make_rectangular(17, 1)
diff --git a/ctapipe/image/tests/test_reducer.py b/ctapipe/image/tests/test_reducer.py
index b2194909..db0d49ff 100644
--- a/ctapipe/image/tests/test_reducer.py
+++ b/ctapipe/image/tests/test_reducer.py
@@ -1,21 +1,22 @@
-import pytest
+import astropy.units as u
 import numpy as np
+import pytest
 from numpy.testing import assert_array_equal
-import astropy.units as u
 from traitlets.config import Config
-from ctapipe.instrument import SubarrayDescription, TelescopeDescription
+
 from ctapipe.image.reducer import NullDataVolumeReducer, TailCutsDataVolumeReducer
+from ctapipe.instrument import SubarrayDescription
 
 
 @pytest.fixture(scope="module")
-def subarray_lst():
+def subarray_lst(prod3_lst):
     telid = 1
     subarray = SubarrayDescription(
         "test array lst",
         tel_positions={1: np.zeros(3) * u.m, 2: np.ones(3) * u.m},
         tel_descriptions={
-            1: TelescopeDescription.from_name(optics_name="LST", camera_name="LSTCam"),
-            2: TelescopeDescription.from_name(optics_name="LST", camera_name="LSTCam"),
+            1: prod3_lst,
+            2: prod3_lst,
         },
     )
 
diff --git a/ctapipe/image/tests/test_sliding_window_correction.py b/ctapipe/image/tests/test_sliding_window_correction.py
index 3f00ea7c..a93fec28 100644
--- a/ctapipe/image/tests/test_sliding_window_correction.py
+++ b/ctapipe/image/tests/test_sliding_window_correction.py
@@ -3,18 +3,18 @@ Test of sliding window extractor for LST camera pulse shape with
 the correction for the integration window completeness
 """
 
-import numpy as np
 import astropy.units as u
+import numpy as np
 from numpy.testing import assert_allclose
 from traitlets.config.loader import Config
-from ctapipe.containers import DL1CameraContainer
 
-from ctapipe.image.extractor import SlidingWindowMaxSum, ImageExtractor
+from ctapipe.containers import DL1CameraContainer
+from ctapipe.image.extractor import ImageExtractor, SlidingWindowMaxSum
 from ctapipe.image.toymodel import WaveformModel
-from ctapipe.instrument import SubarrayDescription, TelescopeDescription
+from ctapipe.instrument import SubarrayDescription
 
 
-def test_sw_pulse_lst():
+def test_sw_pulse_lst(prod5_lst):
     """
     Test function of sliding window extractor for LST camera pulse shape with
     the correction for the integration window completeness
@@ -24,9 +24,7 @@ def test_sw_pulse_lst():
     subarray = SubarrayDescription(
         "LST1",
         tel_positions={1: np.zeros(3) * u.m},
-        tel_descriptions={
-            1: TelescopeDescription.from_name(optics_name="LST", camera_name="LSTCam")
-        },
+        tel_descriptions={1: prod5_lst},
     )
 
     telid = list(subarray.tel.keys())[0]
diff --git a/ctapipe/image/tests/test_timing_parameters.py b/ctapipe/image/tests/test_timing_parameters.py
index 93efd466..a37c07b1 100644
--- a/ctapipe/image/tests/test_timing_parameters.py
+++ b/ctapipe/image/tests/test_timing_parameters.py
@@ -1,11 +1,11 @@
-import numpy as np
 import astropy.units as u
+import numpy as np
 from numpy.testing import assert_allclose
-from ctapipe.instrument.camera import CameraGeometry
+
 from ctapipe.containers import CameraHillasParametersContainer
 
 
-def test_psi_0():
+def test_psi_0(prod5_lst):
     from ctapipe.image import timing_parameters
 
     """
@@ -16,7 +16,7 @@ def test_psi_0():
     intercept = 1.0
     deviation = 0.1
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
     hillas = CameraHillasParametersContainer(x=0 * u.m, y=0 * u.m, psi=0 * u.deg)
 
     random = np.random.default_rng(0)
@@ -37,7 +37,7 @@ def test_psi_0():
     assert_allclose(timing.deviation, deviation, rtol=1e-2)
 
 
-def test_psi_20():
+def test_psi_20(prod5_lst):
     from ctapipe.image import timing_parameters
 
     # Then try a different rotation angle
@@ -45,7 +45,7 @@ def test_psi_20():
     intercept = 1
     deviation = 0.1
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
     psi = 20 * u.deg
     hillas = CameraHillasParametersContainer(x=0 * u.m, y=0 * u.m, psi=psi)
 
@@ -69,14 +69,14 @@ def test_psi_20():
     assert_allclose(timing.deviation, deviation, rtol=1e-2)
 
 
-def test_ignore_negative():
+def test_ignore_negative(prod5_lst):
     from ctapipe.image import timing_parameters
 
     grad = 2.0
     intercept = 1.0
     deviation = 0.1
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
     hillas = CameraHillasParametersContainer(x=0 * u.m, y=0 * u.m, psi=0 * u.deg)
 
     random = np.random.default_rng(0)
diff --git a/ctapipe/image/tests/test_toy.py b/ctapipe/image/tests/test_toy.py
index 9cd108a4..85f76242 100644
--- a/ctapipe/image/tests/test_toy.py
+++ b/ctapipe/image/tests/test_toy.py
@@ -1,14 +1,16 @@
-import numpy as np
-from ctapipe.instrument import CameraGeometry, CameraReadout
-from ctapipe.image.toymodel import obtain_time_image, WaveformModel
-from pytest import approx
-from scipy.stats import poisson, skewnorm, norm
+from copy import deepcopy
+
 import astropy.units as u
+import numpy as np
 import pytest
+from pytest import approx
+from scipy.stats import norm, poisson, skewnorm
+
+from ctapipe.image.toymodel import WaveformModel, obtain_time_image
 
 
 @pytest.mark.parametrize("seed", [None, 0])
-def test_intensity(seed, monkeypatch):
+def test_intensity(seed, monkeypatch, prod5_lst):
     """
     Test generation of the toymodel roughly follows the given intensity.
 
@@ -17,7 +19,7 @@ def test_intensity(seed, monkeypatch):
     """
     from ctapipe.image import toymodel
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
 
     x, y = u.Quantity([0.2, 0.3], u.m)
     width = 0.05 * u.m
@@ -55,13 +57,13 @@ def test_intensity(seed, monkeypatch):
     assert poisson(intensity).ppf(0.05) <= signal.sum() <= poisson(intensity).ppf(0.95)
 
 
-def test_skewed():
+def test_skewed(prod5_lst):
     from ctapipe.image.toymodel import SkewedGaussian
 
     # test if the parameters we calculated for the skew normal
     # distribution produce the correct moments
     rng = np.random.default_rng(0)
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
 
     x, y = u.Quantity([0.2, 0.3], u.m)
     width = 0.05 * u.m
@@ -83,10 +85,10 @@ def test_skewed():
     assert np.isclose(skew, skewness)
 
 
-def test_compare():
-    from ctapipe.image.toymodel import SkewedGaussian, Gaussian
+def test_compare(prod5_lst):
+    from ctapipe.image.toymodel import Gaussian, SkewedGaussian
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst.camera.geometry
 
     x, y = u.Quantity([0.2, 0.3], u.m)
     width = 0.05 * u.m
@@ -103,8 +105,9 @@ def test_compare():
     assert np.isclose(signal_skewed, signal_normal).all()
 
 
-def test_obtain_time_image():
-    geom = CameraGeometry.from_name("CHEC")
+def test_obtain_time_image(prod5_sst):
+    geom = prod5_sst.camera.geometry
+
     centroid_x = u.Quantity(0.05, u.m)
     centroid_y = u.Quantity(0.05, u.m)
     psi = u.Quantity(70, u.deg)
@@ -146,7 +149,7 @@ def test_obtain_time_image():
         time_gradient,
         time_intercept,
     )
-    np.testing.assert_allclose(time.std(), 1.710435)
+    np.testing.assert_allclose(time.std(), 1.710435, rtol=0.1)
 
     time_gradient = u.Quantity(20, u.ns / u.m)
     time_intercept = u.Quantity(40, u.ns)
@@ -162,11 +165,12 @@ def test_obtain_time_image():
     np.testing.assert_allclose(time, 40)
 
 
-def test_waveform_model():
+def test_waveform_model(prod5_sst):
     from ctapipe.image.toymodel import Gaussian
 
-    geom = CameraGeometry.from_name("CHEC")
-    readout = CameraReadout.from_name("CHEC")
+    prod5_sst = deepcopy(prod5_sst)
+    geom = prod5_sst.camera.geometry
+    readout = prod5_sst.camera.readout
 
     ref_duration = 67
     n_ref_samples = 100
diff --git a/ctapipe/instrument/camera/tests/test_geometry.py b/ctapipe/instrument/camera/tests/test_geometry.py
index eb8e3adb..bb4db9ad 100644
--- a/ctapipe/instrument/camera/tests/test_geometry.py
+++ b/ctapipe/instrument/camera/tests/test_geometry.py
@@ -1,12 +1,15 @@
 """ Tests for CameraGeometry """
+from copy import deepcopy
+
 import numpy as np
+import pytest
 from astropy import units as u
+
 from ctapipe.instrument import CameraGeometry, PixelShape
-import pytest
 
 
 def test_construct():
-    """ Check we can make a CameraGeometry from scratch """
+    """Check we can make a CameraGeometry from scratch"""
     x = np.linspace(-10, 10, 100)
     y = np.linspace(-10, 10, 100)
     geom = CameraGeometry(
@@ -14,7 +17,7 @@ def test_construct():
         pix_id=np.arange(100),
         pix_x=x * u.m,
         pix_y=y * u.m,
-        pix_area=x * u.m ** 2,
+        pix_area=x * u.m**2,
         pix_type=PixelShape.SQUARE,
         pix_rotation="10d",
         cam_rotation="12d",
@@ -31,7 +34,7 @@ def test_construct():
             pix_id=np.arange(100),
             pix_x=x * u.m,
             pix_y=y * u.m,
-            pix_area=x * u.m ** 2,
+            pix_area=x * u.m**2,
             pix_type="foo",
             pix_rotation="10d",
             cam_rotation="12d",
@@ -43,7 +46,7 @@ def test_construct():
         pix_id=np.arange(100),
         pix_x=x * u.m,
         pix_y=y * u.m,
-        pix_area=x * u.m ** 2,
+        pix_area=x * u.m**2,
         pix_type="rectangular",
         pix_rotation="10d",
         cam_rotation="12d",
@@ -51,28 +54,26 @@ def test_construct():
 
 
 def test_make_rectangular_camera_geometry():
-    """ Check that we can construct a dummy camera with square geometry """
+    """Check that we can construct a dummy camera with square geometry"""
     geom = CameraGeometry.make_rectangular()
     assert geom.pix_x.shape == geom.pix_y.shape
 
 
-def test_load_lst_camera():
-    """ test that a specific camera has the expected attributes """
-    geom = CameraGeometry.from_name("LSTCam")
-    assert len(geom.pix_x) == 1855
-    assert geom.pix_type == PixelShape.HEXAGON
+def test_load_lst_camera(prod5_lst):
+    """test that a specific camera has the expected attributes"""
+    assert len(prod5_lst.camera.geometry.pix_x) == 1855
+    assert prod5_lst.camera.geometry.pix_type == PixelShape.HEXAGON
 
 
-def test_position_to_pix_index():
-    """ test that we can lookup a pixel from a coordinate"""
-    geom = CameraGeometry.from_name("LSTCam")
+def test_position_to_pix_index(prod5_lst):
+    """test that we can lookup a pixel from a coordinate"""
     x, y = (0.80 * u.m, 0.79 * u.m)
-    pix_id = geom.position_to_pix_index(x, y)
-    assert pix_id == 1790
+    pix_id = prod5_lst.camera.geometry.position_to_pix_index(x, y)
+    assert pix_id == 1575
 
 
 def test_find_neighbor_pixels():
-    """ test basic neighbor functionality """
+    """test basic neighbor functionality"""
     n_pixels = 5
     x, y = u.Quantity(
         np.meshgrid(np.linspace(-5, 5, n_pixels), np.linspace(-5, 5, n_pixels)), u.cm
@@ -81,7 +82,7 @@ def test_find_neighbor_pixels():
     geom = CameraGeometry(
         "test",
         pix_id=np.arange(n_pixels),
-        pix_area=u.Quantity(4, u.cm ** 2),
+        pix_area=u.Quantity(4, u.cm**2),
         pix_x=x.ravel(),
         pix_y=y.ravel(),
         pix_type="rectangular",
@@ -127,7 +128,7 @@ def test_calc_pixel_neighbors_square():
         pix_type="rectangular",
         pix_x=u.Quantity(x.ravel(), u.cm),
         pix_y=u.Quantity(y.ravel(), u.cm),
-        pix_area=u.Quantity(np.ones(400), u.cm ** 2),
+        pix_area=u.Quantity(np.ones(400), u.cm**2),
     )
 
     assert set(cam.neighbors[0]) == {1, 20}
@@ -147,41 +148,39 @@ def test_calc_pixel_neighbors_square_diagonal():
         pix_type="rectangular",
         pix_x=u.Quantity(x.ravel(), u.cm),
         pix_y=u.Quantity(y.ravel(), u.cm),
-        pix_area=u.Quantity(np.ones(400), u.cm ** 2),
+        pix_area=u.Quantity(np.ones(400), u.cm**2),
     )
 
     cam._neighbors = cam.calc_pixel_neighbors(diagonal=True)
     assert set(cam.neighbors[21]) == {0, 1, 2, 20, 22, 40, 41, 42}
 
 
-def test_to_and_from_table():
-    """ Check converting to and from an astropy Table """
-    geom = CameraGeometry.from_name("LSTCam")
+def test_to_and_from_table(prod5_lst):
+    """Check converting to and from an astropy Table"""
+    prod5_lst_cam = prod5_lst.camera.geometry
+    tab = prod5_lst_cam.to_table()
+    prod5_lst_cam2 = prod5_lst_cam.from_table(tab)
 
-    tab = geom.to_table()
-    geom2 = geom.from_table(tab)
+    assert prod5_lst_cam.camera_name == prod5_lst_cam2.camera_name
+    assert (prod5_lst_cam.pix_x == prod5_lst_cam2.pix_x).all()
+    assert (prod5_lst_cam.pix_y == prod5_lst_cam2.pix_y).all()
+    assert (prod5_lst_cam.pix_area == prod5_lst_cam2.pix_area).all()
+    assert prod5_lst_cam.pix_type == prod5_lst_cam2.pix_type
 
-    assert geom.camera_name == geom2.camera_name
-    assert (geom.pix_x == geom2.pix_x).all()
-    assert (geom.pix_y == geom2.pix_y).all()
-    assert (geom.pix_area == geom2.pix_area).all()
-    assert geom.pix_type == geom2.pix_type
 
-
-def test_write_read(tmpdir):
-    """ Check that serialization to disk doesn't lose info """
+def test_write_read(tmpdir, prod5_lst):
+    """Check that serialization to disk doesn't lose info"""
     filename = str(tmpdir.join("testcamera.fits.gz"))
+    prod5_lst_cam = prod5_lst.camera.geometry
 
-    geom = CameraGeometry.from_name("LSTCam")
-
-    geom.to_table().write(filename, overwrite=True)
-    geom2 = geom.from_table(filename)
+    prod5_lst_cam.to_table().write(filename, overwrite=True)
+    prod5_lst_cam2 = prod5_lst_cam.from_table(filename)
 
-    assert geom.camera_name == geom2.camera_name
-    assert (geom.pix_x == geom2.pix_x).all()
-    assert (geom.pix_y == geom2.pix_y).all()
-    assert (geom.pix_area == geom2.pix_area).all()
-    assert geom.pix_type == geom2.pix_type
+    assert prod5_lst_cam.camera_name == prod5_lst_cam2.camera_name
+    assert (prod5_lst_cam.pix_x == prod5_lst_cam2.pix_x).all()
+    assert (prod5_lst_cam.pix_y == prod5_lst_cam2.pix_y).all()
+    assert (prod5_lst_cam.pix_area == prod5_lst_cam2.pix_area).all()
+    assert prod5_lst_cam.pix_type == prod5_lst_cam2.pix_type
 
 
 def test_precal_neighbors():
@@ -194,7 +193,7 @@ def test_precal_neighbors():
         pix_id=np.arange(3),
         pix_x=np.arange(3) * u.deg,
         pix_y=np.arange(3) * u.deg,
-        pix_area=np.ones(3) * u.deg ** 2,
+        pix_area=np.ones(3) * u.deg**2,
         neighbors=[[1], [0, 2], [1]],
         pix_type="rectangular",
         pix_rotation="0deg",
@@ -209,24 +208,24 @@ def test_precal_neighbors():
     assert np.all(nmat.T == nmat)
 
 
-def test_slicing():
-    """ Check that we can slice a camera into a smaller one """
-    geom = CameraGeometry.from_name("NectarCam")
-    sliced1 = geom[100:200]
+def test_slicing(prod5_mst_nectarcam):
+    """Check that we can slice a camera into a smaller one"""
+    prod5_nectarcam = prod5_mst_nectarcam.camera.geometry
+    sliced1 = prod5_nectarcam[100:200]
 
     assert len(sliced1.pix_x) == 100
     assert len(sliced1.pix_y) == 100
     assert len(sliced1.pix_area) == 100
     assert len(sliced1.pix_id) == 100
 
-    sliced2 = geom[[5, 7, 8, 9, 10]]
+    sliced2 = prod5_nectarcam[[5, 7, 8, 9, 10]]
     assert sliced2.pix_id[0] == 5
     assert sliced2.pix_id[1] == 7
     assert len(sliced2.pix_x) == 5
 
 
 def test_slicing_rotation(camera_geometry):
-    """ Check that we can rotate and slice """
+    """Check that we can rotate and slice"""
     camera_geometry.rotate("25d")
 
     sliced1 = camera_geometry[5:10]
@@ -235,7 +234,7 @@ def test_slicing_rotation(camera_geometry):
 
 
 def test_rectangle_patch_neighbors():
-    """" test that a simple rectangular camera has the expected neighbors """
+    """ " test that a simple rectangular camera has the expected neighbors"""
     pix_x = np.array([-1.1, 0.1, 0.9, -1, 0, 1, -0.9, -0.1, 1.1]) * u.m
     pix_y = np.array([1.1, 1, 0.9, -0.1, 0, 0.1, -0.9, -1, -1.1]) * u.m
     cam = CameraGeometry(
@@ -252,53 +251,51 @@ def test_rectangle_patch_neighbors():
     assert cam.neighbor_matrix.sum(axis=0).min() == 2
 
 
-def test_border_pixels():
-    """ check we can find border pixels"""
-    from ctapipe.instrument.camera import CameraGeometry
-
-    cam = CameraGeometry.from_name("LSTCam")
+def test_border_pixels(prod5_lst, prod3_astri):
+    """check we can find border pixels"""
+    prod5_lst_cam = prod5_lst.camera.geometry
 
-    assert np.sum(cam.get_border_pixel_mask(1)) == 168
-    assert np.sum(cam.get_border_pixel_mask(2)) == 330
+    assert np.sum(prod5_lst_cam.get_border_pixel_mask(1)) == 168
+    assert np.sum(prod5_lst_cam.get_border_pixel_mask(2)) == 330
 
-    cam = CameraGeometry.from_name("ASTRICam")
-    assert np.sum(cam.get_border_pixel_mask(1)) == 212
-    assert np.sum(cam.get_border_pixel_mask(2)) == 408
+    prod3_astri_cam = prod3_astri.camera.geometry
+    assert np.sum(prod3_astri_cam.get_border_pixel_mask(1)) == 212
+    assert np.sum(prod3_astri_cam.get_border_pixel_mask(2)) == 408
 
-    assert cam.get_border_pixel_mask(1)[0]
-    assert cam.get_border_pixel_mask(1)[2351]
-    assert not cam.get_border_pixel_mask(1)[521]
+    assert prod3_astri_cam.get_border_pixel_mask(1)[0]
+    assert prod3_astri_cam.get_border_pixel_mask(1)[2351]
+    assert not prod3_astri_cam.get_border_pixel_mask(1)[521]
 
 
-def test_equals():
-    """ check we can use the == operator """
-    cam1 = CameraGeometry.from_name("LSTCam")
-    cam2 = CameraGeometry.from_name("LSTCam")
-    cam3 = CameraGeometry.from_name("ASTRICam")
+def test_equals(prod5_lst, prod5_mst_nectarcam):
+    """check we can use the == operator"""
+    cam1 = prod5_lst.camera.geometry
+    cam2 = deepcopy(prod5_lst.camera.geometry)
+    cam3 = prod5_mst_nectarcam.camera.geometry
 
     assert cam1 is not cam2
     assert cam1 == cam2
     assert cam1 != cam3
 
 
-def test_hashing():
-    """" check that hashes are correctly computed """
-    cam1 = CameraGeometry.from_name("LSTCam")
-    cam2 = CameraGeometry.from_name("LSTCam")
-    cam3 = CameraGeometry.from_name("ASTRICam")
+def test_hashing(prod5_lst, prod5_mst_nectarcam):
+    """ " check that hashes are correctly computed"""
+    cam1 = prod5_lst.camera.geometry
+    cam2 = deepcopy(prod5_lst.camera.geometry)
+    cam3 = prod5_mst_nectarcam.camera.geometry
 
     assert len(set([cam1, cam2, cam3])) == 2
 
 
 def test_camera_from_name(camera_geometry):
-    """ check we can construct all cameras from name"""
+    """check we can construct all cameras from name"""
     camera = CameraGeometry.from_name(camera_geometry.camera_name)
     assert str(camera) == camera_geometry.camera_name
 
 
 def test_camera_coordinate_transform(camera_geometry):
     """test conversion of the coordinates stored in a camera frame"""
-    from ctapipe.coordinates import EngineeringCameraFrame, CameraFrame, TelescopeFrame
+    from ctapipe.coordinates import CameraFrame, EngineeringCameraFrame, TelescopeFrame
 
     geom = camera_geometry
     trans_geom = geom.transform_to(EngineeringCameraFrame())
@@ -336,7 +333,7 @@ def test_guess_area():
         pix_type="rect",
     )
 
-    assert np.all(geom.pix_area == 1 * u.cm ** 2)
+    assert np.all(geom.pix_area == 1 * u.cm**2)
 
     geom = CameraGeometry(
         "test",
@@ -360,7 +357,7 @@ def test_pixel_width():
     geom = CameraGeometry(
         "test",
         pix_id=[1],
-        pix_area=[2] * u.cm ** 2,
+        pix_area=[2] * u.cm**2,
         pix_x=[0] * u.m,
         pix_y=[0] * u.m,
         pix_type="hex",
@@ -371,7 +368,7 @@ def test_pixel_width():
     geom = CameraGeometry(
         "test",
         pix_id=[1],
-        pix_area=[2] * u.cm ** 2,
+        pix_area=[2] * u.cm**2,
         pix_x=[0] * u.m,
         pix_y=[0] * u.m,
         pix_type="rect",
@@ -380,9 +377,9 @@ def test_pixel_width():
     assert np.isclose(geom.pixel_width.to_value(u.cm), [np.sqrt(2)])
 
 
-def test_guess_radius():
-    lst_cam = CameraGeometry.from_name("LSTCam")
-    assert u.isclose(lst_cam.guess_radius(), 1.1 * u.m, rtol=0.05)
+def test_guess_radius(prod5_lst, prod5_sst):
+    prod5_lst_cam = prod5_lst.camera.geometry
+    assert u.isclose(prod5_lst_cam.guess_radius(), 1.1 * u.m, rtol=0.05)
 
-    lst_cam = CameraGeometry.from_name("CHEC")
-    assert u.isclose(lst_cam.guess_radius(), 0.16 * u.m, rtol=0.05)
+    prod5_chec = prod5_sst.camera.geometry
+    assert u.isclose(prod5_chec.guess_radius(), 0.16 * u.m, rtol=0.05)
diff --git a/ctapipe/instrument/camera/tests/test_readout.py b/ctapipe/instrument/camera/tests/test_readout.py
index 3445e49f..1878ff7e 100644
--- a/ctapipe/instrument/camera/tests/test_readout.py
+++ b/ctapipe/instrument/camera/tests/test_readout.py
@@ -1,12 +1,13 @@
 """ Tests for CameraGeometry """
 import numpy as np
+import pytest
 from astropy import units as u
+
 from ctapipe.instrument import CameraReadout
-import pytest
 
 
 def test_construct():
-    """ Check we can make a CameraReadout from scratch """
+    """Check we can make a CameraReadout from scratch"""
     camera_name = "Unknown"
     sampling_rate = u.Quantity(2, u.GHz)
     reference_pulse_shape = np.ones((2, 20)).astype(np.float64)
@@ -44,7 +45,7 @@ def test_reference_pulse_sample_time(readout):
 
 
 def test_to_and_from_table(readout):
-    """ Check converting to and from an astropy Table """
+    """Check converting to and from an astropy Table"""
     tab = readout.to_table()
     readout2 = readout.from_table(tab)
 
@@ -55,7 +56,7 @@ def test_to_and_from_table(readout):
 
 
 def test_write_read(tmpdir, readout):
-    """ Check that serialization to disk doesn't lose info """
+    """Check that serialization to disk doesn't lose info"""
     filename = str(tmpdir.join("testcamera.fits.gz"))
 
     readout.to_table().write(filename, overwrite=True)
@@ -68,7 +69,7 @@ def test_write_read(tmpdir, readout):
 
 
 def test_equals():
-    """ check we can use the == operator """
+    """check we can use the == operator"""
     camera_name = "Unknown"
     sampling_rate = u.Quantity(2, u.GHz)
     reference_pulse_shape = np.ones((2, 20)).astype(np.float64)
@@ -111,7 +112,7 @@ def test_equals():
 
 
 def test_hashing():
-    """" check that hashes are correctly computed """
+    """ " check that hashes are correctly computed"""
     camera_name = "Unknown"
     sampling_rate = u.Quantity(2, u.GHz)
     reference_pulse_shape = np.ones((2, 20)).astype(np.float64)
@@ -141,7 +142,7 @@ def test_hashing():
 
 
 def test_camera_from_name(camera_geometry):
-    """ check we can construct all cameras from name"""
+    """check we can construct all cameras from name"""
 
     try:
         camera = CameraReadout.from_name(camera_geometry.camera_name)
diff --git a/ctapipe/instrument/tests/test_optics.py b/ctapipe/instrument/tests/test_optics.py
index fb815d5d..938540db 100644
--- a/ctapipe/instrument/tests/test_optics.py
+++ b/ctapipe/instrument/tests/test_optics.py
@@ -1,14 +1,14 @@
 """ Tests for OpticsDescriptions"""
-import pytest
-from astropy import units as u
 import os
 import tempfile
 
-from ctapipe.instrument.optics import OpticsDescription
+import pytest
+from astropy import units as u
 
-from ctapipe.utils import get_table_dataset
 from ctapipe.core.tool import run_tool
+from ctapipe.instrument.optics import OpticsDescription
 from ctapipe.tools.dump_instrument import DumpInstrumentTool
+from ctapipe.utils import get_table_dataset
 from ctapipe.utils.datasets import get_dataset_path
 
 
@@ -18,10 +18,8 @@ def test_guess_optics():
 
     answer = guess_telescope(1855, 28.0 * u.m)
 
-    od = OpticsDescription.from_name(answer.name)
-
-    assert od.equivalent_focal_length.to_value(u.m) == 28
-    assert od.num_mirrors == 1
+    assert answer.name == "LST"
+    assert answer.n_mirrors == 1
 
 
 def test_construct_optics():
diff --git a/ctapipe/instrument/tests/test_subarray.py b/ctapipe/instrument/tests/test_subarray.py
index 03bba2ff..b6907edf 100644
--- a/ctapipe/instrument/tests/test_subarray.py
+++ b/ctapipe/instrument/tests/test_subarray.py
@@ -16,7 +16,7 @@ from ctapipe.instrument import (
 )
 
 
-def example_subarray(n_tels=10):
+def example_subarray(tel_type, n_tels=10):
     """generate a simple subarray for testing purposes"""
     rng = np.random.default_rng(0)
 
@@ -24,9 +24,7 @@ def example_subarray(n_tels=10):
     tel = {}
 
     for tel_id in range(1, n_tels + 1):
-        tel[tel_id] = TelescopeDescription.from_name(
-            optics_name="MST", camera_name="NectarCam"
-        )
+        tel[tel_id] = tel_type
         pos[tel_id] = rng.uniform(-100, 100, size=3) * u.m
 
     return SubarrayDescription(
@@ -41,10 +39,10 @@ def example_subarray(n_tels=10):
     )
 
 
-def test_subarray_description():
+def test_subarray_description(prod5_mst_nectarcam):
     """Test SubarrayDescription functionality"""
     n_tels = 10
-    sub = example_subarray(n_tels)
+    sub = example_subarray(prod5_mst_nectarcam, n_tels)
     sub.peek()
 
     assert len(sub.telescope_types) == 1
@@ -62,7 +60,8 @@ def test_subarray_description():
     assert len(sub.telescope_types) == 1  # only have one type in this array
     assert len(sub.optics_types) == 1  # only have one type in this array
     assert len(sub.camera_types) == 1  # only have one type in this array
-    assert sub.optics_types[0].equivalent_focal_length.to_value(u.m) == 16.0
+    assert u.isclose(sub.optics_types[0].equivalent_focal_length, 16.0 * u.m)
+    assert u.isclose(sub.optics_types[0].effective_focal_length, 16.445 * u.m)
     assert isinstance(sub.tel_coords, SkyCoord)
     assert len(sub.tel_coords) == n_tels
 
@@ -95,12 +94,11 @@ def test_tel_indexing(example_subarray):
     assert np.all(sub.tel_ids_to_indices([1, 2, 3]) == np.array([0, 1, 2]))
 
 
-def test_tel_ids_to_mask(example_subarray):
-    lst = TelescopeDescription.from_name("LST", "LSTCam")
+def test_tel_ids_to_mask(prod5_lst):
     subarray = SubarrayDescription(
         "someone_counted_in_binary",
         tel_positions={1: [0, 0, 0] * u.m, 10: [50, 0, 0] * u.m},
-        tel_descriptions={1: lst, 10: lst},
+        tel_descriptions={1: prod5_lst, 10: prod5_lst},
     )
 
     assert np.all(subarray.tel_ids_to_mask([]) == [False, False])
@@ -159,13 +157,19 @@ def test_hdf(example_subarray, tmp_path):
         assert read == example_subarray
 
 
-def test_hdf_same_camera(tmp_path):
+def test_hdf_same_camera(tmp_path, prod5_lst, prod5_mst_flashcam):
     """Test writing / reading subarray to hdf5 with a subarray that has two
     different telescopes with the same camera
     """
+    frankenstein_lst = TelescopeDescription(
+        name="LST",
+        tel_type="LST",
+        optics=prod5_lst.optics,
+        camera=prod5_mst_flashcam.camera,
+    )
     tel = {
-        1: TelescopeDescription.from_name(optics_name="SST-ASTRI", camera_name="CHEC"),
-        2: TelescopeDescription.from_name(optics_name="SST-GCT", camera_name="CHEC"),
+        1: prod5_lst,
+        2: frankenstein_lst,
     }
     pos = {1: [0, 0, 0] * u.m, 2: [50, 0, 0] * u.m}
 
@@ -177,13 +181,13 @@ def test_hdf_same_camera(tmp_path):
     assert array == read
 
 
-def test_hdf_duplicate_string_repr(tmp_path):
+def test_hdf_duplicate_string_repr(tmp_path, prod5_lst):
     """Test writing and reading of a subarray with two telescopes that
     are different but have the same name.
     """
     # test with a subarray that has two different telescopes with the same
     # camera
-    tel1 = TelescopeDescription.from_name(optics_name="LST", camera_name="LSTCam")
+    tel1 = prod5_lst
 
     # second telescope is almost the same and as the same str repr
     tel2 = deepcopy(tel1)
@@ -210,17 +214,16 @@ def test_hdf_duplicate_string_repr(tmp_path):
     )
 
 
-def test_get_tel_ids(example_subarray):
+def test_get_tel_ids(example_subarray, prod3_astri):
     """Test for SubarrayDescription.get_tel_ids"""
     subarray = example_subarray
-    sst = TelescopeDescription.from_name("SST-ASTRI", "CHEC")
 
-    telescopes = [1, 2, "MST_MST_FlashCam", sst]
+    telescopes = [1, 2, "MST_MST_FlashCam", prod3_astri]
     tel_ids = subarray.get_tel_ids(telescopes)
 
     true_tel_ids = (
         subarray.get_tel_ids_for_type("MST_MST_FlashCam")
-        + subarray.get_tel_ids_for_type(sst)
+        + subarray.get_tel_ids_for_type(prod3_astri)
         + [1, 2]
     )
 
diff --git a/ctapipe/instrument/tests/test_telescope.py b/ctapipe/instrument/tests/test_telescope.py
index 4be192bd..2726d58e 100644
--- a/ctapipe/instrument/tests/test_telescope.py
+++ b/ctapipe/instrument/tests/test_telescope.py
@@ -1,37 +1,17 @@
 """ Tests for TelescopeDescriptions """
 import pytest
 
-from ctapipe.instrument.camera import CameraDescription
-from ctapipe.instrument.optics import OpticsDescription
 from ctapipe.instrument.telescope import TelescopeDescription
 
 
-def test_hash():
-
-    types = ["LST", "MST", "SST"]
-    names = ["LST", "MST", "SST-1M"]
-    cameras = ["LSTCam", "FlashCam", "DigiCam"]
-
-    telescopes = []
-    for name, type, camera in zip(names, types, cameras):
-        for i in range(3):
-
-            telescopes.append(
-                TelescopeDescription(
-                    name=name,
-                    tel_type=type,
-                    optics=OpticsDescription.from_name(name),
-                    camera=CameraDescription.from_name(camera),
-                )
-            )
-
-    assert len(telescopes) == 9
-    assert len(set(telescopes)) == 3
+def test_hash(subarray_prod5_paranal):
+    assert len(subarray_prod5_paranal) == 180
+    assert len(set(subarray_prod5_paranal.tel.values())) == 4
 
 
 @pytest.mark.parametrize("optics_name", ["LST", "MST"])
 def test_telescope_from_name(optics_name, camera_geometry):
-    """ Check we can construct all telescopes from their names """
+    """Check we can construct all telescopes from their names"""
     camera_name = camera_geometry.camera_name
     tel = TelescopeDescription.from_name(optics_name, camera_name)
     assert optics_name in str(tel)
diff --git a/ctapipe/io/tests/test_toysource.py b/ctapipe/io/tests/test_toysource.py
index e477f823..4119c07b 100644
--- a/ctapipe/io/tests/test_toysource.py
+++ b/ctapipe/io/tests/test_toysource.py
@@ -1,14 +1,14 @@
-import pytest
-from ctapipe.instrument import SubarrayDescription, TelescopeDescription
 import astropy.units as u
+import pytest
+
+from ctapipe.instrument import SubarrayDescription
 from ctapipe.utils import get_dataset_path
 
 
 @pytest.fixture(scope="module")
-def subarray():
+def subarray(prod5_lst):
 
-    lst = TelescopeDescription.from_name("LST", "LSTCam")
-    tels = [lst] * 4
+    tels = [prod5_lst] * 4
 
     positions = {
         1: [0, 0, 0] * u.m,
diff --git a/ctapipe/reco/tests/test_HillasReconstructor.py b/ctapipe/reco/tests/test_HillasReconstructor.py
index d50c882c..0d741cb2 100644
--- a/ctapipe/reco/tests/test_HillasReconstructor.py
+++ b/ctapipe/reco/tests/test_HillasReconstructor.py
@@ -9,13 +9,13 @@ from traitlets.config import Config
 from ctapipe.calib import CameraCalibrator
 from ctapipe.containers import HillasParametersContainer
 from ctapipe.image.image_processor import ImageProcessor
-from ctapipe.instrument import SubarrayDescription, TelescopeDescription
+from ctapipe.instrument import SubarrayDescription
 from ctapipe.io import SimTelEventSource
 from ctapipe.reco.hillas_reconstructor import HillasPlane, HillasReconstructor
 from ctapipe.utils import get_dataset_path
 
 
-def test_estimator_results():
+def test_estimator_results(prod5_sst):
     """
     creating some planes pointing in different directions (two
     north-south, two east-west) and that have a slight position errors (+-
@@ -43,11 +43,7 @@ def test_estimator_results():
     subarray = SubarrayDescription(
         "test array",
         tel_positions={1: np.zeros(3) * u.m},
-        tel_descriptions={
-            1: TelescopeDescription.from_name(
-                optics_name="SST-ASTRI", camera_name="CHEC"
-            )
-        },
+        tel_descriptions={1: prod5_sst},
     )
 
     # creating the fit class and setting the the great circle member
diff --git a/ctapipe/visualization/tests/test_mpl.py b/ctapipe/visualization/tests/test_mpl.py
index 8e0de185..30246844 100644
--- a/ctapipe/visualization/tests/test_mpl.py
+++ b/ctapipe/visualization/tests/test_mpl.py
@@ -2,29 +2,31 @@
 Tests for array display
 """
 
+import numpy as np
+
 # skip these tests if matplotlib can't be imported
 import pytest
-from ctapipe.instrument import (
-    CameraGeometry,
-    SubarrayDescription,
-    TelescopeDescription,
-    PixelShape,
-)
+from astropy import units as u
+
 from ctapipe.containers import (
     CameraHillasParametersContainer,
     HillasParametersContainer,
 )
-import numpy as np
-from astropy import units as u
+from ctapipe.instrument import PixelShape, SubarrayDescription
 
 plt = pytest.importorskip("matplotlib.pyplot")
 
 
-def test_camera_display_single():
-    """ test CameraDisplay functionality """
+@pytest.fixture(scope="session")
+def prod5_lst_cam(prod5_lst):
+    return prod5_lst.camera.geometry
+
+
+def test_camera_display_single(prod5_lst_cam):
+    """test CameraDisplay functionality"""
     from ..mpl_camera import CameraDisplay
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst_cam
     disp = CameraDisplay(geom)
     image = np.random.normal(size=len(geom.pix_x))
     disp.image = image
@@ -48,10 +50,10 @@ def test_camera_display_single():
     disp.clear_overlays()
 
 
-def test_hillas_overlay():
+def test_hillas_overlay(prod5_lst_cam):
     from ctapipe.visualization import CameraDisplay
 
-    disp = CameraDisplay(CameraGeometry.from_name("LSTCam"))
+    disp = CameraDisplay(prod5_lst_cam)
     hillas = CameraHillasParametersContainer(
         x=0.1 * u.m, y=-0.1 * u.m, length=0.5 * u.m, width=0.2 * u.m, psi=90 * u.deg
     )
@@ -60,11 +62,11 @@ def test_hillas_overlay():
 
 
 @pytest.mark.parametrize("pix_type", PixelShape.__members__.values())
-def test_pixel_shapes(pix_type):
-    """ test CameraDisplay functionality """
+def test_pixel_shapes(pix_type, prod5_lst_cam):
+    """test CameraDisplay functionality"""
     from ..mpl_camera import CameraDisplay
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst_cam
     geom.pix_type = pix_type
 
     disp = CameraDisplay(geom)
@@ -75,11 +77,11 @@ def test_pixel_shapes(pix_type):
     disp.add_ellipse(centroid=(0, 0), width=0.1, length=0.1, angle=0.1)
 
 
-def test_camera_display_multiple():
-    """ create a figure with 2 subplots, each with a CameraDisplay """
+def test_camera_display_multiple(prod5_lst_cam):
+    """create a figure with 2 subplots, each with a CameraDisplay"""
     from ..mpl_camera import CameraDisplay
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst_cam
     fig, ax = plt.subplots(2, 1)
 
     d1 = CameraDisplay(geom, ax=ax[0])
@@ -90,24 +92,23 @@ def test_camera_display_multiple():
     d2.image = image
 
 
-def test_array_display():
-    """ check that we can do basic array display functionality """
-    from ctapipe.visualization.mpl_array import ArrayDisplay
-    from ctapipe.image import timing_parameters
-
+def test_array_display(prod5_mst_nectarcam):
+    """check that we can do basic array display functionality"""
     from ctapipe.containers import (
         ArrayEventContainer,
-        DL1Container,
+        CoreParametersContainer,
         DL1CameraContainer,
+        DL1Container,
         ImageParametersContainer,
-        CoreParametersContainer,
     )
+    from ctapipe.image import timing_parameters
+    from ctapipe.visualization.mpl_array import ArrayDisplay
 
     # build a test subarray:
     tels = dict()
     tel_pos = dict()
     for ii, pos in enumerate([[0, 0, 0], [100, 0, 0], [-100, 0, 0]] * u.m):
-        tels[ii + 1] = TelescopeDescription.from_name("MST", "NectarCam")
+        tels[ii + 1] = prod5_mst_nectarcam
         tel_pos[ii + 1] = pos
 
     sub = SubarrayDescription(
@@ -142,7 +143,7 @@ def test_array_display():
     # ...with scalar color
     ad.set_vector_uv(np.array([1, 2, 3]) * u.m, np.array([1, 2, 3]) * u.m, c=3)
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_mst_nectarcam.camera.geometry
     rot_angle = 20 * u.deg
     hillas = CameraHillasParametersContainer(x=0 * u.m, y=0 * u.m, psi=rot_angle)
 
@@ -241,11 +242,12 @@ def test_array_display():
     ad.remove_labels()
 
 
-def test_picker():
+def test_picker(prod5_lst_cam):
+    from matplotlib.backend_bases import MouseButton, MouseEvent
+
     from ctapipe.visualization import CameraDisplay
-    from matplotlib.backend_bases import MouseEvent, MouseButton
 
-    geom = CameraGeometry.from_name("LSTCam")
+    geom = prod5_lst_cam
     clicked_pixels = []
 
     class PickingCameraDisplay(CameraDisplay):