Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5845e78
feat: Implement true_disp calculation in ctapipe-process + test
koopatroopa787 Feb 17, 2026
d775870
docs: Add changelog fragment for #2950
koopatroopa787 Feb 17, 2026
bea9764
refactor: Use single true_disp field and shared calculation utility
koopatroopa787 Feb 17, 2026
7ed8dec
Refactor true_disp: Move to SimulatedCameraContainer, use shared util…
koopatroopa787 Feb 17, 2026
db1e5ac
feat: Calculate true_disp and fix writer bugs
koopatroopa787 Feb 25, 2026
348f76e
fix: Robust HDF5TableWriter handling for metadata and rows
koopatroopa787 Feb 25, 2026
2f6f3ce
fix: Import Container in tableio.py and finalize writer robustness
koopatroopa787 Feb 25, 2026
2bb287f
add lazy property with telescope coordinates as EarthLocation
mexanick Feb 13, 2026
928abe1
add changelog
mexanick Feb 13, 2026
1b013b5
Fix changelog
mexanick Feb 13, 2026
df21fae
Improve developer setup instructions in Getting Started guide
yaochengchen Feb 16, 2026
dc000e4
add changelog
yaochengchen Feb 16, 2026
035297a
update as reviewer's suggestion
yaochengchen Feb 17, 2026
fba4463
Fix trigger check in HDF5EventSource.is_compatible
yaochengchen Feb 16, 2026
9d9e010
add changelog
yaochengchen Feb 16, 2026
aa32c83
Fix typos in hillas_reconstructor.py
yaochengchen Feb 24, 2026
b157186
Fix a typo in README.md
yaochengchen Feb 24, 2026
6bb1777
Add changelog
yaochengchen Feb 24, 2026
66a2994
Fix a typo in README.rst
yaochengchen Feb 24, 2026
b44a018
Fix a typo in README.rst
yaochengchen Feb 24, 2026
bd7e32c
Fix a typo in README.rst
yaochengchen Feb 24, 2026
d1dc7aa
Fix a typo in hillas.py
yaochengchen Feb 24, 2026
8eeedd3
Modify changelog
yaochengchen Feb 24, 2026
37603e8
Update src/ctapipe/image/hillas.py
yaochengchen Feb 24, 2026
ccc3c6d
Update README.rst
yaochengchen Feb 24, 2026
43188e7
Fix missing example blocks in trigger doc
maxnoe Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changes/2950.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement calculation of ``true_disp`` parameters (norm and sign) in ``ctapipe-process`` for simulated events.
6 changes: 6 additions & 0 deletions src/ctapipe/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
35 changes: 34 additions & 1 deletion src/ctapipe/image/image_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
)

1 change: 1 addition & 0 deletions src/ctapipe/io/datawriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
)

Expand Down
33 changes: 33 additions & 0 deletions src/ctapipe/reco/preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"table_to_X",
"horizontal_to_telescope",
"telescope_to_horizontal",
"calculate_true_disp",
]


Expand Down Expand Up @@ -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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good to add this utility, but it should then also be used in the training.

Note that we have two different conventions for computing true disp and this should be configurable here (see the code in the training tool / the project_disp option)

"""
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
5 changes: 5 additions & 0 deletions src/ctapipe/tools/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = [
Expand Down Expand Up @@ -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)

Expand Down
49 changes: 49 additions & 0 deletions src/ctapipe/tools/tests/test_process_true_disp.py
Original file line number Diff line number Diff line change
@@ -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_PARAMETERS_GROUP}/{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