Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add another cleaning method in cleaning.py #1672

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
de9b878
test : change is_stream function in eventsource.py
satoshifukami0115 Jul 13, 2020
203d3e3
Merge remote-tracking branch 'upstream/master'
satoshifukami0115 Sep 21, 2020
43d5a5c
Merge branch 'master' of https://github.com/cta-observatory/ctapipe
satoshifukami0115 Mar 22, 2021
f7443d6
add time_constrained_clean()
satoshifukami0115 Mar 30, 2021
a0ca6ef
add docstrings to new cleaning functions
satoshifukami0115 Mar 31, 2021
4455e40
Revert "test : change is_stream function in eventsource.py"
satoshifukami0115 Mar 31, 2021
0705a8d
remove for loop in time_constrained_clean()
satoshifukami0115 Mar 31, 2021
125be4f
use np.count_nonzero() instead of sum() and remove for loop in apply_…
satoshifukami0115 Mar 31, 2021
1374212
explain doubled time limit in the docstring of apply_time_average_cle…
satoshifukami0115 Mar 31, 2021
a98c4e4
Merge branch 'master' of https://github.com/cta-observatory/ctapipe
satoshifukami0115 Apr 16, 2021
779ee2b
Merge branch 'master' into additional_timing_cleaning
satoshifukami0115 Apr 16, 2021
8d12eea
use brightest_island() to identify the brightest island
satoshifukami0115 Apr 16, 2021
766e8d3
Merge branch 'master' of https://github.com/cta-observatory/ctapipe
satoshifukami0115 Apr 12, 2022
2f7b668
Merge branch 'master' into additional_timing_cleaning
satoshifukami0115 Apr 12, 2022
e6102ec
create test function for time_constrained_clean()
satoshifukami0115 Apr 12, 2022
1fd189a
create TimeConstrainedImageCleaner class in cleaning.py
satoshifukami0115 Apr 12, 2022
311debd
refactor the test function for time_constrained_clean slightly
satoshifukami0115 Apr 12, 2022
5b79a0c
remove unnecessary lines and replace the function to time_constrained…
satoshifukami0115 Apr 13, 2022
de8d356
remove unnecessary tkinter module in test_cleaning.py
satoshifukami0115 Apr 13, 2022
ad5a2ae
fix docstring errors
satoshifukami0115 Apr 13, 2022
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
168 changes: 167 additions & 1 deletion ctapipe/image/cleaning.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"mars_cleaning_1st_pass",
"fact_image_cleaning",
"apply_time_delta_cleaning",
"apply_time_average_cleaning",
"time_constrained_clean",
"ImageCleaner",
"TailcutsImageCleaner",
]
Expand All @@ -32,7 +34,7 @@
IntTelescopeParameter,
BoolTelescopeParameter,
)

from .morphology import number_of_islands, brightest_island

def tailcuts_clean(
geom,
Expand Down Expand Up @@ -238,6 +240,57 @@ def apply_time_delta_cleaning(
return pixels_to_keep


def apply_time_average_cleaning(
geom, mask, image, arrival_times, picture_thresh, time_limit
):
"""
Extract all pixels that arrived within a given timeframe
with respect to the time average of the pixels on the main island.
maxnoe marked this conversation as resolved.
Show resolved Hide resolved

In order to avoid removing signal pixels of large impact-parameter events,
the time limit for bright pixels is doubled.

Parameters
----------
geom: `ctapipe.instrument.CameraGeometry`
Camera geometry information
mask: array, boolean
boolean mask of *clean* pixels before time_delta_cleaning
image: array
pixel values
arrival_times: array
pixel timing information
picture_thresh: float
threshold above which time limit is extended twice its value
time_limit: int or float
arrival time limit w.r.t. the average time of the core pixels

Returns
-------

A boolean mask of *clean* pixels. To get a zero-suppressed image and pixel
list, use `image[mask], geom.pix_id[mask]`, or to keep the same
image size and just set unclean pixels to 0 or similar, use
`image[~mask] = 0`

"""
pixels_to_remove = []
Copy link
Member

Choose a reason for hiding this comment

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

this is an unused variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is. I will remove this line...

mask = mask.copy()
if np.count_nonzero(mask) > 0:

# use main island (maximum charge) for time average calculation
num_islands, island_labels = number_of_islands(geom, mask)
mask_main = brightest_island(num_islands, island_labels, image)
time_ave = np.average(arrival_times[mask_main], weights=image[mask_main]**2)

time_diffs = np.abs(arrival_times[mask] - time_ave)
time_limit_pixwise = np.where(image < (2 * picture_thresh), time_limit, time_limit * 2)[mask]

mask[mask] &= time_diffs < time_limit_pixwise

return mask


def fact_image_cleaning(
geom,
image,
Expand Down Expand Up @@ -323,6 +376,88 @@ def fact_image_cleaning(
return pixels_to_keep


def time_constrained_clean(
geom,
image,
arrival_times,
picture_thresh=7,
boundary_thresh=5,
time_limit_core=4.5,
time_limit_boundary=1.5,
min_number_picture_neighbors=1,
):
"""
time constrained cleaning by MAGIC

Cleaning contains the following steps:
- Find core pixels (containing more photons than a picture threshold)
- Remove pixels with less than N neighbors
- Remove pixels whose arrival times are within a time limit of the average time
- Find boundary pixels (containing more photons than a boundary threshold)
- Remove pixels with less than N neighbors arriving within a given timeframe

Parameters
----------
geom: `ctapipe.instrument.CameraGeometry`
Camera geometry information
image: array
pixel values
arrival_times: array
pixel timing information
picture_threshold: float or array
threshold above which all pixels are retained
boundary_threshold: float or array
threshold above which pixels are retained if they have a neighbor
already above the picture_thresh
time_limit_core: int or float
arrival time limit of core pixels w.r.t the average time
time_limit_boundary: int or float
arrival time limit of boundary pixels w.r.t neighboring core pixels
min_number_neighbors: int
Threshold to determine if a pixel survives cleaning steps.
These steps include checks of neighbor arrival time and value

Returns
-------
A boolean mask of *clean* pixels. To get a zero-suppressed image and pixel
list, use `image[mask], geom.pix_id[mask]`, or to keep the same
image size and just set unclean pixels to 0 or similar, use
`image[~mask] = 0`

"""

# find core pixels that pass a picture threshold
pixels_above_picture = image >= picture_thresh

# require at least min_number_picture_neighbors
number_of_neighbors_above_picture = geom.neighbor_matrix_sparse.dot(
pixels_above_picture.view(np.byte)
)
pixels_in_picture = pixels_above_picture & (
number_of_neighbors_above_picture >= min_number_picture_neighbors
)

# keep core pixels whose arrival times are within a certain time limit of the average
mask_core = apply_time_average_cleaning(geom, pixels_in_picture, image, arrival_times, picture_thresh, time_limit_core)

# find boundary pixels that pass a boundary threshold
pixels_above_boundary = image >= boundary_thresh
pixels_with_picture_neighbors = geom.neighbor_matrix_sparse.dot(mask_core)
mask_boundary = ( pixels_above_boundary & pixels_with_picture_neighbors ) & np.invert(mask_core)

# keep boundary pixels whose arrival times are within a certain time limit of the neighboring core pixels
pixels_to_remove = []
Copy link
Member

Choose a reason for hiding this comment

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

Same here: this is an unused variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, same here.

mask_boundary = mask_boundary.copy()

time_diffs = np.abs(arrival_times[mask_boundary, None] - arrival_times)
valid_neighbors = (time_diffs < time_limit_boundary) & geom.neighbor_matrix[mask_boundary] & mask_core
enough_neighbors = np.count_nonzero(valid_neighbors, axis=1) >= min_number_picture_neighbors
mask_boundary[mask_boundary] &= enough_neighbors

return mask_core | mask_boundary



class ImageCleaner(TelescopeComponent):
"""
Abstract class for all configurable Image Cleaning algorithms. Use
Expand Down Expand Up @@ -440,3 +575,34 @@ def __call__(
min_number_neighbors=self.min_picture_neighbors.tel[tel_id],
time_limit=self.time_limit_ns.tel[tel_id],
)


class TimeConstrainedImageCleaner(TailcutsImageCleaner):
"""
MAGIC-like Image cleaner with timing information (See `ctapipe.image.time_constrained_clean`)
"""

time_limit_core_ns = FloatTelescopeParameter(
default_value=4.5, help="arrival time limit for neighboring " "core pixels, in ns"
).tag(config=True)
time_limit_boundary_ns = FloatTelescopeParameter(
default_value=1.5, help="arrival time limit for neighboring " "boundary pixels, in ns"
).tag(config=True)

def __call__(
self, tel_id: int, image: np.ndarray, arrival_times=None
) -> np.ndarray:
"""
Apply MAGIC-like image cleaning with timing information. See `ImageCleaner.__call__()`
"""

return mars_cleaning_1st_pass(
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you really want to run this function here, and not your time_constrained_clean() function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this I should have changed to time_constrained_clean(). I will do it now...

self.subarray.tel[tel_id].camera.geometry,
image,
arrival_times=arrival_times,
picture_thresh=self.picture_threshold_pe.tel[tel_id],
boundary_thresh=self.boundary_threshold_pe.tel[tel_id],
min_number_picture_neighbors=self.min_picture_neighbors.tel[tel_id],
time_limit_core=self.time_limit_core_ns.tel[tel_id],
time_limit_boundary=self.time_limit_boundary_ns.tel[tel_id]
)
76 changes: 76 additions & 0 deletions ctapipe/image/tests/test_cleaning.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from tkinter import Image
import numpy as np
from numpy.testing import assert_allclose
from ctapipe.image import cleaning
Expand Down Expand Up @@ -331,3 +332,78 @@ def test_apply_time_delta_cleaning():
test_mask = mask.copy()
test_mask[[41, 157]] = 0
assert (test_mask == td_mask).all()


def test_time_constrained_clean():
geom = CameraGeometry.from_name("LSTCam")
charge = np.zeros(geom.n_pixels, dtype=np.float64)
peak_time = np.zeros(geom.n_pixels, dtype=np.float64)

# define signal pixels and their charges/timings (1 core pixel + 6 neighboring core pixels + 12 neighboring boundary pixels)
core_pixel = 100
core_neighbors = geom.neighbors[core_pixel]
boundary_pixels = np.setdiff1d(np.array([geom.neighbors[core_neighbor] for core_neighbor in core_neighbors]), np.append(core_neighbors, core_pixel))
charge[core_pixel], charge[core_neighbors], charge[boundary_pixels] = 15, 10, 6
peak_time[core_pixel], peak_time[core_neighbors], peak_time[boundary_pixels] = 18, 20, 21

# define initial cleaning parameters
picture_thresh, boundary_thresh = 8, 4
time_limit_core, time_limit_boundary = 4.5, 1.5
min_number_picture_neighbors = 1

mask_signal = charge > 0

# 1. basic test
mask_reco = cleaning.time_constrained_clean(
geom, charge, peak_time, picture_thresh=picture_thresh, boundary_thresh=boundary_thresh, time_limit_core=time_limit_core, time_limit_boundary=time_limit_boundary, min_number_picture_neighbors=min_number_picture_neighbors,
)
test_mask = mask_signal.copy()
assert (test_mask == mask_reco).all()

# 2. increased min_number_picture_neighbors test (here 3)
min_number_picture_neighbors = 3
mask_reco = cleaning.time_constrained_clean(
geom, charge, peak_time, picture_thresh=picture_thresh, boundary_thresh=boundary_thresh, time_limit_core=time_limit_core, time_limit_boundary=time_limit_boundary, min_number_picture_neighbors=min_number_picture_neighbors,
)
## removed pixels : boundary pixels
test_mask = mask_signal.copy()
test_mask[boundary_pixels] = 0
assert (test_mask == mask_reco).all()

# 3. strict time_limit_boundary test (here 0.5)
min_number_picture_neighbors = 1
time_limit_boundary = 0.5
mask_reco = cleaning.time_constrained_clean(
geom, charge, peak_time, picture_thresh=picture_thresh, boundary_thresh=boundary_thresh, time_limit_core=time_limit_core, time_limit_boundary=time_limit_boundary, min_number_picture_neighbors=min_number_picture_neighbors,
)
## removed pixels : boundary pixels
test_mask = mask_signal.copy()
test_mask[boundary_pixels] = 0
assert (test_mask == mask_reco).all()

# 4. time_limit_core test (one of core_neighbors have peak time >5 slice away from the average)
time_limit_boundary = 1.5
noise_core_neighbor = core_neighbors[0]
peak_time[noise_core_neighbor] = 25
mask_reco = cleaning.time_constrained_clean(
geom, charge, peak_time, picture_thresh=picture_thresh, boundary_thresh=boundary_thresh, time_limit_core=time_limit_core, time_limit_boundary=time_limit_boundary, min_number_picture_neighbors=min_number_picture_neighbors,
)
## removed pixels : the noise core neighbor pixel + one neighboring boundary
test_mask = mask_signal.copy()
test_mask[noise_core_neighbor] = 0
noise_boundary = np.setdiff1d(geom.neighbors[noise_core_neighbor], np.array([geom.neighbors[core_neighbor] for core_neighbor in core_neighbors[1:]]))
test_mask[noise_boundary] = 0
assert (test_mask == mask_reco).all()

# 5. time_limit_core test for brighter pixels (one of core_neighbors have peak time >5 slice away from the average)
charge[core_pixel], charge[core_neighbors] = 30, 20
mask_reco = cleaning.time_constrained_clean(
geom, charge, peak_time, picture_thresh=picture_thresh, boundary_thresh=boundary_thresh, time_limit_core=time_limit_core, time_limit_boundary=time_limit_boundary, min_number_picture_neighbors=min_number_picture_neighbors,
)
## removed pixels : one neighboring boundary to the noise core pixel
test_mask = mask_signal.copy()
noise_boundary = np.setdiff1d(geom.neighbors[noise_core_neighbor], np.array([geom.neighbors[core_neighbor] for core_neighbor in core_neighbors[1:]]))
test_mask[noise_boundary] = 0
assert (test_mask == mask_reco).all()