diff --git a/README.rst b/README.rst index 4c26dddd5c3..931aaece2b7 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ Low-level data processing pipeline software for the `CTAO (Cherenkov Telescope Array Observatory) `__. -This is code is a prototype data processing framework and is under rapid +This code is a prototype data processing framework and is under rapid development. It is not recommended for production use unless you are an expert or developer! @@ -39,7 +39,7 @@ You can find all ctapipe Zenodo records here: `List of ctapipe Records on Zenodo There is also a Zenodo DOI always pointing to the latest version: |doilatest| -At this point, our latest publication is the `2023 ICRC proceeding `_, which you can +At this point, the latest publication is our contribution in the `2023 ICRC proceedings `_, which you can cite using this bibtex entry: .. code:: @@ -74,11 +74,11 @@ or via:: pip install ctapipe -**Note**: to install a specific version of ctapipe take look at the documentation `here `__. +**Note**: to install a specific version of ctapipe take a look at the documentation `here `__. **Note**: ``mamba`` is a C++ reimplementation of conda and can be found `here `__. -Note this is *pre-alpha* software and is not yet stable enough for end-users (expect large API changes until the first stable 1.0 release). +Note that this is *pre-alpha* software and is not yet stable enough for end-users (expect large API changes until the first stable 1.0 release). Developers should follow the development install instructions found in the `documentation `__. diff --git a/docs/changes/2947.feature.rst b/docs/changes/2947.feature.rst new file mode 100644 index 00000000000..ee1924737d6 --- /dev/null +++ b/docs/changes/2947.feature.rst @@ -0,0 +1 @@ +Added ``tel_earth_locations`` lazy property to ``SubarrayDescription`` that caches telescope positions as ``EarthLocation`` objects. diff --git a/docs/changes/2948.maintenance.rst b/docs/changes/2948.maintenance.rst new file mode 100644 index 00000000000..f21be0e1532 --- /dev/null +++ b/docs/changes/2948.maintenance.rst @@ -0,0 +1 @@ +Improve clarity and correctness of the Getting Started developer guide, fixing typos, outdated Git commands, and inconsistent terminology. diff --git a/docs/changes/2949.bugfix.rst b/docs/changes/2949.bugfix.rst new file mode 100644 index 00000000000..d97ba7f037e --- /dev/null +++ b/docs/changes/2949.bugfix.rst @@ -0,0 +1,4 @@ +Fixed incorrect trigger compatibility check in ``HDF5EventSource.is_compatible``. + +``has_trigger`` was mistakenly checking ``SIMULATION_TEL_TABLE`` due to a +copy-paste error. It now correctly checks for the presence of DL1 trigger tables. diff --git a/docs/changes/2950.feature.rst b/docs/changes/2950.feature.rst new file mode 100644 index 00000000000..311bc24520e --- /dev/null +++ b/docs/changes/2950.feature.rst @@ -0,0 +1 @@ +Implement calculation of ``true_disp`` parameters (norm and sign) in ``ctapipe-process`` for simulated events. diff --git a/docs/changes/2956.maintenance.rst b/docs/changes/2956.maintenance.rst new file mode 100644 index 00000000000..de0021cd075 --- /dev/null +++ b/docs/changes/2956.maintenance.rst @@ -0,0 +1 @@ +Fix typos in ``README.rst`` and ``ctapipe.image.hillas.py``. diff --git a/docs/changes/2957.maintenance.rst b/docs/changes/2957.maintenance.rst new file mode 100644 index 00000000000..32d374952c2 --- /dev/null +++ b/docs/changes/2957.maintenance.rst @@ -0,0 +1 @@ +Fix typos in ``ctapipe.reco.hillas_reconstructor``. diff --git a/docs/developer-guide/getting-started.rst b/docs/developer-guide/getting-started.rst index 56974aa90f5..6f1b06541d0 100644 --- a/docs/developer-guide/getting-started.rst +++ b/docs/developer-guide/getting-started.rst @@ -12,7 +12,7 @@ We strongly recommend using the `mambaforge conda distribution `_ it. - After that, clone your fork of the repository and add the main reposiory as a second + After that, clone your fork of the repository and add the main repository as a second remote called ``upstream``, so that you can keep your fork synchronized with the main repository. .. code-block:: console @@ -93,7 +93,7 @@ terminal to activate the conda environment. Installing ctapipe in Development Mode ====================================== -Now setup this cloned version for development. +Now set up this cloned version for development. The following command will use the editable installation feature of python packages. From then on, all the ctapipe executables and the library itself will be usable from anywhere, given you have activated the ``cta-dev`` conda environment. @@ -116,7 +116,7 @@ test plugin via $ pip install -e ./test_plugin -We are using the ``pre-commit``, ``code-spell`` and ``ruff`` tools +We are using the ``pre-commit``, ``codespell`` and ``ruff`` tools for automatic adherence to the code style (see our :doc:`/developer-guide/style-guide`). To enforce running these tools whenever you make a commit, setup the @@ -128,7 +128,7 @@ To enforce running these tools whenever you make a commit, setup the The pre-commit hook will then execute the tools with the same settings as when a pull request is checked on GitHub, and if any problems are reported the commit will be rejected. -You then have to fix the reported issues before tying to commit again. +You then have to fix the reported issues before trying to commit again. Note that a common problem is code not complying with the style guide, and that whenever this was the only problem found, simply adding the changes resulting from the pre-commit hook to the commit will result in your changes being accepted. @@ -200,7 +200,7 @@ to keep forks in sync. Remember that ``git switch `` [#switch]_ switches between branches, ``git switch -c `` creates a new branch and switches to it, -and ``git branch`` on it's own will tell you which branches are available +and ``git branch`` on its own will tell you which branches are available and which one you are currently on. @@ -267,8 +267,8 @@ sub-module), check the style, and make sure the docs render correctly (e.g it should not mix changes that are logically different). Therefore it's best to group related changes with ``git add ``. You may even commit only *parts* of a changed file - using and ``git add -p``. If you want to keep your git commit - history clean, learn to use commands like ``git commit --ammend`` + using ``git add -p``. If you want to keep your git commit + history clean, learn to use commands like ``git commit --amend`` (append to previous commit without creating a new one, e.g. when you find a typo or something small). @@ -358,7 +358,7 @@ For differences between rebasing and merging and when to use which, see `this tu Create a *Pull Request* ----------------------- -When you're happy, you create PR on on your github fork page by clicking +When you're happy, you create a PR on your GitHub fork page by clicking "pull request". You can also do this via *GitHub Desktop* if you have that installed, by pushing the pull-request button in the upper-right-hand corner. @@ -402,7 +402,7 @@ since it is no longer needed (assuming it was accepted and merged in): .. code-block:: console - $ git switch main # switch back to your master branch + $ git switch main # switch back to your main branch pull in the upstream changes, which should include your new features, and remove the branch from the local and remote (github). @@ -428,7 +428,9 @@ And then delete your branch: .. code-block:: console - $ git branch --delete --remotes implement_feature_1 + $ git push origin --delete implement_feature_1 + $ git pull + $ git fetch --prune Debugging Your Code diff --git a/scripts/deploy.bat b/scripts/deploy.bat new file mode 100644 index 00000000000..2dae4a7fd8b --- /dev/null +++ b/scripts/deploy.bat @@ -0,0 +1,47 @@ +@echo off +set SERVER_IP=159.65.61.54 +set ZIP_FILE=ctapipe_deploy.zip + +REM Automatically change to the project root (parent directory of this script) +cd /d "%~dp0.." + +echo Running from: %CD% +echo Packaging code for deployment... + +rem Create a temporary deployment directory if it doesn't exist +if not exist "deploy" mkdir deploy + +rem Copy source code +if exist "src" ( + echo Copying src... + xcopy /E /I /Y src deploy\src +) + +rem Copy documentation +if exist "docs" ( + echo Copying docs... + xcopy /E /I /Y docs deploy\docs +) + +rem Copy examples +if exist "examples" ( + echo Copying examples... + xcopy /E /I /Y examples deploy\examples +) + +rem Copy setup files +if exist "pyproject.toml" copy pyproject.toml deploy\ +if exist "setup.cfg" copy setup.cfg deploy\ +if exist "setup.py" copy setup.py deploy\ +if exist "README.md" copy README.md deploy\ +if exist "LICENSE" copy LICENSE deploy\ + +echo. +echo Deployment package created in the 'deploy' directory. +echo To move to Linux server, use scp or similar: +echo scp -r deploy/* user@linux-server:/path/to/ctapipe/ +echo. +echo NOTE: Ensure you have installed dependencies on the Linux server: +echo pip install -e .[all] +echo. +pause diff --git a/src/ctapipe/containers.py b/src/ctapipe/containers.py index efc0c04af82..e42cb0ba1ae 100644 --- a/src/ctapipe/containers.py +++ b/src/ctapipe/containers.py @@ -788,6 +788,12 @@ class SimulatedCameraContainer(Container): description="true impact parameter", ) + true_disp = Field( + nan * u.deg, + description="True disp parameter", + unit=u.deg, + ) + class SimulatedEventContainer(Container): shower = Field( diff --git a/src/ctapipe/core/traits.py b/src/ctapipe/core/traits.py index 84c3d3384eb..6c4b55e47f8 100644 --- a/src/ctapipe/core/traits.py +++ b/src/ctapipe/core/traits.py @@ -270,6 +270,8 @@ def _validate_str(self, obj, value): value = get_dataset_path(value.partition("dataset://")[2]) elif url.scheme in ("", "file"): value = pathlib.Path(url.netloc, url.path) + elif os.name == "nt" and len(url.scheme) == 1: + value = pathlib.Path(value) else: self.error(obj, value) diff --git a/src/ctapipe/image/hillas.py b/src/ctapipe/image/hillas.py index 7042889ace7..584e35bc74d 100644 --- a/src/ctapipe/image/hillas.py +++ b/src/ctapipe/image/hillas.py @@ -69,7 +69,7 @@ def hillas_parameters(geom, image): 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 + The method also supports giving a full geometry with image as a masked array, however this performs worse than passing geometry and image only for the selected pixels. Parameters ---------- diff --git a/src/ctapipe/image/image_processor.py b/src/ctapipe/image/image_processor.py index 0483bac7049..7709838e583 100644 --- a/src/ctapipe/image/image_processor.py +++ b/src/ctapipe/image/image_processor.py @@ -251,8 +251,10 @@ def _process_telescope_event(self, event): peak_time=None, # true image from simulation has no peak time default=DEFAULT_TRUE_IMAGE_PARAMETERS, ) + from ctapipe.core import Container + for container in sim_camera.true_parameters.values(): - if not container.prefix.startswith("true_"): + if isinstance(container, Container) and not container.prefix.startswith("true_"): container.prefix = f"true_{container.prefix}" self.log.debug( @@ -261,3 +263,34 @@ def _process_telescope_event(self, event): recursive=True ), ) + + if ( + dl1_camera.parameters.hillas is not None + and np.isfinite(dl1_camera.parameters.hillas.fov_lat) + and np.isfinite(dl1_camera.parameters.hillas.fov_lon) + ): + pointing = event.monitoring.tel[tel_id].pointing + shower = event.simulation.shower + + from ctapipe.reco.preprocessing import ( + horizontal_to_telescope, + calculate_true_disp, + ) + + fov_lon, fov_lat = horizontal_to_telescope( + alt=shower.alt, + az=shower.az, + pointing_alt=pointing.altitude, + pointing_az=pointing.azimuth, + ) + + hillas = dl1_camera.parameters.hillas + + sim_camera.true_disp = calculate_true_disp( + fov_lon=fov_lon, + fov_lat=fov_lat, + hillas_psi=hillas.psi, + hillas_lon=hillas.fov_lon, + hillas_lat=hillas.fov_lat, + ) + diff --git a/src/ctapipe/instrument/subarray.py b/src/ctapipe/instrument/subarray.py index 9e22fe97964..c501f98034e 100644 --- a/src/ctapipe/instrument/subarray.py +++ b/src/ctapipe/instrument/subarray.py @@ -168,6 +168,22 @@ def tel_coords(self): return SkyCoord(x=pos_x, y=pos_y, z=pos_z, unit=u.m, frame=frame) + @lazyproperty + def tel_earth_locations(self): + """ + Telescope positions as `~astropy.coordinates.EarthLocation` objects. + + Returns + ------- + dict[int, EarthLocation] + Dictionary mapping telescope IDs to their EarthLocation. + This is cached to avoid expensive repeated conversions. + """ + return { + tel_id: coord.to_earth_location() + for tel_id, coord in zip(self.tel_ids, self.tel_coords) + } + @lazyproperty def tel_ids(self): """Array of telescope ids in order of telescope indices""" diff --git a/src/ctapipe/instrument/tests/test_subarray.py b/src/ctapipe/instrument/tests/test_subarray.py index 95df7f1fdee..63987d4f38d 100644 --- a/src/ctapipe/instrument/tests/test_subarray.py +++ b/src/ctapipe/instrument/tests/test_subarray.py @@ -313,3 +313,30 @@ def test_check_matchings_subarray(example_subarray, subarray_prod5_paranal): assert not SubarrayDescription.check_matching_subarrays( [example_subarray, subarray_prod5_paranal] ) + + +def test_tel_earth_locations(example_subarray): + """Test cached tel_earth_locations property""" + # Get the cached property + earth_locs = example_subarray.tel_earth_locations + + assert isinstance(earth_locs, dict) + assert len(earth_locs) == example_subarray.n_tels + + # Check all telescope IDs are present + for tel_id in example_subarray.tel_ids: + assert tel_id in earth_locs + assert isinstance(earth_locs[tel_id], EarthLocation) + + # Verify conversion is correct by comparing with manual conversion + tel_id = example_subarray.tel_ids[0] + tel_index = example_subarray.tel_index_array[tel_id] + manual_location = example_subarray.tel_coords[tel_index].to_earth_location() + + assert u.isclose(earth_locs[tel_id].x, manual_location.x) + assert u.isclose(earth_locs[tel_id].y, manual_location.y) + assert u.isclose(earth_locs[tel_id].z, manual_location.z) + + # Verify it's cached (same object returned) + earth_locs_2 = example_subarray.tel_earth_locations + assert earth_locs is earth_locs_2 diff --git a/src/ctapipe/instrument/trigger.py b/src/ctapipe/instrument/trigger.py index 7555add179b..cd9b77940ca 100644 --- a/src/ctapipe/instrument/trigger.py +++ b/src/ctapipe/instrument/trigger.py @@ -31,20 +31,22 @@ class SoftwareTrigger(TelescopeComponent): With the default settings, this class is a no-op. To get the correct behavior for CTA simulations, use the following configuration: - .. - SoftwareTrigger: - min_telescopes: 2 - min_telescopes_of_type: - - ["type", "*", 0] - - ["type", "LST*", 2] + .. code-block:: yaml + + SoftwareTrigger: + min_telescopes: 2 + min_telescopes_of_type: + - ["type", "*", 0] + - ["type", "LST*", 2] With this class it is also possible to filter for specific telescope event types, e.g. to analyze the RANDOM_MONO or MUON tagged telescope events in isolation: - .. - SoftwareTrigger: - allowed_telescope_event_types: - - "RANDOM_MONO" + .. code-block:: yaml + + SoftwareTrigger: + allowed_telescope_event_types: + - "RANDOM_MONO" """ min_telescopes = Integer( diff --git a/src/ctapipe/io/datawriter.py b/src/ctapipe/io/datawriter.py index 387d40812be..2d5e2460f55 100644 --- a/src/ctapipe/io/datawriter.py +++ b/src/ctapipe/io/datawriter.py @@ -660,6 +660,7 @@ def _write_dl1_telescope_events(self, event: ArrayEventContainer): true_parameters.concentration, true_parameters.morphology, true_parameters.intensity_statistics, + event.simulation.tel[tel_id].true_disp, ], ) diff --git a/src/ctapipe/io/hdf5eventsource.py b/src/ctapipe/io/hdf5eventsource.py index 59596c388e8..40da63198e8 100644 --- a/src/ctapipe/io/hdf5eventsource.py +++ b/src/ctapipe/io/hdf5eventsource.py @@ -348,7 +348,9 @@ def is_compatible(file_path): # we can now read both R1 and DL1 has_muons = DL1_TEL_MUON_GROUP in f.root has_sim = SIMULATION_TEL_TABLE in f.root - has_trigger = SIMULATION_TEL_TABLE in f.root + has_trigger = (DL1_SUBARRAY_TRIGGER_TABLE in f) or ( + DL1_TEL_TRIGGER_TABLE in f + ) datalevels = set(metadata["CTA PRODUCT DATA LEVELS"].split(",")) datalevels = ( diff --git a/src/ctapipe/io/hdf5tableio.py b/src/ctapipe/io/hdf5tableio.py index abd5285f6b4..276e357cf2b 100644 --- a/src/ctapipe/io/hdf5tableio.py +++ b/src/ctapipe/io/hdf5tableio.py @@ -377,6 +377,9 @@ class Schema(tables.IsDescription): # create pytables schema description for the given container for container in containers: + if not isinstance(container, Container): + continue + container.validate() # ensure the data are complete it = zip( @@ -419,7 +422,8 @@ def _setup_new_table(self, table_name, containers, time_format): table_group, table_basename = split_h5path(table_path) for container in containers: - meta.update(container.meta) # copy metadata from container + if isinstance(container, Container): + meta.update(container.meta) # copy metadata from container if table_path not in self.h5file: table = self.h5file.create_table( @@ -449,6 +453,9 @@ def _append_row(self, table_name, containers): row = table.row for container in containers: + if not isinstance(container, Container): + continue + selected_fields = filter( lambda kv: kv[0] in table.colnames, container.items(add_prefix=self.add_prefix), diff --git a/src/ctapipe/io/tableio.py b/src/ctapipe/io/tableio.py index ced85038304..d0cce2191ff 100644 --- a/src/ctapipe/io/tableio.py +++ b/src/ctapipe/io/tableio.py @@ -12,7 +12,7 @@ from ctapipe.compat import COPY_IF_NEEDED -from ..core import Component +from ..core import Component, Container from ..instrument import SubarrayDescription from ..time import ctao_high_res_to_time, time_to_ctao_high_res @@ -138,6 +138,9 @@ def _realize_regexp_transforms(self, table_name, containers): for column_regexp, transform in column_regexp_dict.items(): for container in containers: + if not isinstance(container, Container): + continue + for col_name, _ in container.items(add_prefix=self.add_prefix): if re.fullmatch(column_regexp, col_name): self.log.debug( diff --git a/src/ctapipe/io/tests/test_hdf5eventsource.py b/src/ctapipe/io/tests/test_hdf5eventsource.py index 0713fa23cae..73b430bdc75 100644 --- a/src/ctapipe/io/tests/test_hdf5eventsource.py +++ b/src/ctapipe/io/tests/test_hdf5eventsource.py @@ -346,3 +346,25 @@ def test_read_dl2_tel_ml(gamma_diffuse_full_reco_file): assert energy.prefix == algorithm + "_tel" assert energy.energy is not None assert np.isfinite(energy.energy) + + +def test_is_compatible_with_only_trigger(tmp_path): + """ + Regression test for has_trigger copy-paste bug. + """ + + import tables + + filename = tmp_path / "only_trigger.h5" + + with tables.open_file(filename, mode="w") as h5: + h5.root._v_attrs["CTA PRODUCT DATA MODEL VERSION"] = "v7.3.0" + + h5.root._v_attrs["CTA PRODUCT DATA LEVELS"] = "R0" + + h5.create_group("/", "dl1") + h5.create_group("/dl1", "event") + h5.create_group("/dl1/event", "subarray") + h5.create_group("/dl1/event/subarray", "trigger") + + assert HDF5EventSource.is_compatible(str(filename)) diff --git a/src/ctapipe/reco/hillas_reconstructor.py b/src/ctapipe/reco/hillas_reconstructor.py index 37d168ce108..49817961781 100644 --- a/src/ctapipe/reco/hillas_reconstructor.py +++ b/src/ctapipe/reco/hillas_reconstructor.py @@ -364,7 +364,7 @@ def estimate_direction(norm, weight): @staticmethod def estimate_core_position(array_pointing, psi, positions): """ - Estimate the core position by intersection the major ellipse lines of each telescope. + Estimate the core position by intersecting the major ellipse lines of each telescope. Parameters ---------- @@ -394,7 +394,7 @@ def estimate_core_position(array_pointing, psi, positions): # the shower core the ground. # Estimate the position of the shower's core - # from the TiltedFram to the GroundFrame + # from the TiltedFrame to the GroundFrame z = np.zeros(len(psi)) uvw_vectors = np.column_stack([np.cos(psi), np.sin(psi), z]) diff --git a/src/ctapipe/reco/preprocessing.py b/src/ctapipe/reco/preprocessing.py index d92ac7bc672..f006ce650d9 100644 --- a/src/ctapipe/reco/preprocessing.py +++ b/src/ctapipe/reco/preprocessing.py @@ -29,6 +29,7 @@ "table_to_X", "horizontal_to_telescope", "telescope_to_horizontal", + "calculate_true_disp", ] @@ -154,3 +155,35 @@ def telescope_to_horizontal(lon, lat, pointing_alt, pointing_az): horizontal_coord = tel_coord.transform_to(AltAz()) return horizontal_coord.alt.to(u.deg), horizontal_coord.az.to(u.deg) + + +@u.quantity_input( + fov_lon=u.deg, fov_lat=u.deg, hillas_psi=u.rad, hillas_lon=u.deg, hillas_lat=u.deg +) +def calculate_true_disp(fov_lon, fov_lat, hillas_psi, hillas_lon, hillas_lat): + """ + Calculate the true disp parameter (distance between hillas cog and source position) + + Parameters + ---------- + fov_lon : u.Quantity + Source longitude in telescope frame + fov_lat : u.Quantity + Source latitude in telescope frame + hillas_psi : u.Quantity + Hillas psi parameter (angle between major axis and x-axis) + hillas_lon : u.Quantity + Hillas center longitude + hillas_lat : u.Quantity + Hillas center latitude + + Returns + ------- + true_disp : u.Quantity + The true disp parameter (signed distance) + """ + delta_lon = fov_lon - hillas_lon + delta_lat = fov_lat - hillas_lat + + true_disp = np.cos(hillas_psi) * delta_lon + np.sin(hillas_psi) * delta_lat + return true_disp diff --git a/src/ctapipe/tools/process.py b/src/ctapipe/tools/process.py index 124e48ed875..b10658f4ae5 100644 --- a/src/ctapipe/tools/process.py +++ b/src/ctapipe/tools/process.py @@ -5,6 +5,8 @@ # pylint: disable=W0201 import sys +import astropy.units as u +import numpy as np from tqdm.auto import tqdm from ..calib import CameraCalibrator, GainSelector @@ -30,6 +32,7 @@ DL2_EVENT_STATISTICS_GROUP, ) from ..reco import Reconstructor, ShowerProcessor +from ..reco.preprocessing import horizontal_to_telescope from ..utils import EventTypeFilter COMPATIBLE_DATALEVELS = [ @@ -362,6 +365,8 @@ def start(self): if self.should_compute_dl1: self.process_images(event) + + if self.should_compute_muon_parameters: self.process_muons(event) diff --git a/src/ctapipe/tools/tests/test_process_true_disp.py b/src/ctapipe/tools/tests/test_process_true_disp.py new file mode 100644 index 00000000000..0572ce2bd91 --- /dev/null +++ b/src/ctapipe/tools/tests/test_process_true_disp.py @@ -0,0 +1,49 @@ + +import numpy as np +import pandas as pd +import pytest +import tables + +from ctapipe.core import run_tool +from ctapipe.tools.process import ProcessorTool +from ctapipe.utils import get_dataset_path, resource_file +from ctapipe.io.hdf5dataformat import SIMULATION_PARAMETERS_GROUP + +def test_true_disp_calculation(tmp_path, dl1_image_file): + """check true_disp calculation in ctapipe-process""" + print("DEBUG: Starting test_true_disp_calculation", flush=True) + config = resource_file("stage1_config.json") + + output_file = tmp_path / "true_disp_test.dl1.h5" + + run_tool( + ProcessorTool(), + argv=[ + f"--config={config}", + f"--input={dl1_image_file}", + f"--output={output_file}", + "--write-parameters", + "--overwrite", + ], + cwd=tmp_path, + raises=True, + ) + + # check if fields exist and are not all NaN + # We need to find a telescope that has events + + with tables.open_file(output_file, mode="r") as testfile: + # Check if true parameters group exists for at least one telescope + sim_params_group = testfile.root.simulation.event.telescope.parameters + assert len(sim_params_group._v_children) > 0 + + first_tel_group = list(sim_params_group._v_children.keys())[0] + + true_params = pd.read_hdf( + output_file, f"simulation/event/telescope/images/{first_tel_group}" + ) + + assert "true_disp" in true_params.columns + + # Check that we have some valid values + assert np.count_nonzero(np.isfinite(true_params["true_disp"])) > 0