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

Variance calibration #2636

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

Variance calibration #2636

wants to merge 18 commits into from

Conversation

ctoennis
Copy link

@ctoennis ctoennis commented Nov 4, 2024

This PR adds a trait to the CameraCalibrator called "image_calibration_type". This trait allows to switch the method of calibration between "charge" and "variance", with the former being the calibration method for the existing image extractors and the latter being the method to calibrate variance images.

I also made some small adjustment to the variance extractor so that it works in the CameraCalibrator.

@ctoennis ctoennis self-assigned this Nov 4, 2024

This comment has been minimized.

1 similar comment

This comment has been minimized.

@TjarkMiener
Copy link
Member

thanks for putting this together @ctoennis! In case I'm not missing something, I think we can (maybe should) apply this sqrt operation before writing the coefficients. Therefore we do not need the different cases here in the code. Would that work?

@ctoennis
Copy link
Author

ctoennis commented Nov 4, 2024

@TjarkMiener Yes, that should work. The calibration events would only need the sqrt coeficcients. Only thing is that this needs to be documented somewhere.

@ctoennis
Copy link
Author

ctoennis commented Nov 4, 2024

Though, the changes to the variance extractor should then still be kept so that it works with the calibrator.

@@ -61,6 +62,11 @@ class CameraCalibrator(TelescopeComponent):
image_extractor_type: str
The name of the ImageExtractor subclass to be used for image extraction
image_calibration_type: str
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need this. Instead, you can check the type of the image extractor.

if isinstance(VarianceExtractor, extractor):
    #apply squared gain calibration

Copy link
Author

Choose a reason for hiding this comment

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

OK, I'll do that

@@ -73,6 +79,15 @@ class CameraCalibrator(TelescopeComponent):
help="Name of the ImageExtractor subclass to be used.",
).tag(config=True)

image_calibration_type = CaselessStrEnum(
Copy link
Contributor

Choose a reason for hiding this comment

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

see l.66

if (
self.apply_peak_time_shift.tel[tel_id]
and remaining_shift is not None
and self.image_calibration_type == "charge"
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need to alter this condition?

Copy link
Author

Choose a reason for hiding this comment

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

The variance extractor has no peak time, so applying the peak time shift otherwise throws an error

Copy link
Member

Choose a reason for hiding this comment

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

Just check if dl1.peak_time is not None instead of a specific extractor.

Copy link
Author

Choose a reason for hiding this comment

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

@maxnoe that makes sense

@@ -292,13 +311,21 @@ def _calibrate_dl1(self, event, tel_id):
and dl1_calib.absolute_factor is not None
):
if selected_gain_channel is None:
dl1.image *= dl1_calib.relative_factor / dl1_calib.absolute_factor
if self.image_calibration_type == "charge":
Copy link
Contributor

Choose a reason for hiding this comment

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

See l.66

calibrator(example_event)
image = example_event.dl1.tel[tel_id].image
assert image is not None
assert image.shape == (1764,)
Copy link
Contributor

Choose a reason for hiding this comment

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

from where this shape comes? What telescope is this?

Copy link
Author

Choose a reason for hiding this comment

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

That is from the example event. Not sure what telescope this is

Copy link
Member

Choose a reason for hiding this comment

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

FlashCam from the number of pixels

@@ -1308,7 +1308,7 @@ def __call__(
self, waveforms, tel_id, selected_gain_channel, broken_pixels
) -> DL1CameraContainer:
container = DL1CameraContainer(
image=np.nanvar(waveforms, dtype="float32", axis=2),
image=np.nanvar(waveforms, dtype="float32", axis=2)[0],
Copy link
Contributor

Choose a reason for hiding this comment

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

We shall be able to work with multiple gains, why do you introduce this change?

Copy link
Author

Choose a reason for hiding this comment

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

np.nanvar was giving me an extra dimension on top of the gain. I think I can fix this with the keepdims parameter

Comment on lines 314 to 319
if self.image_calibration_type == "charge":
dl1.image *= dl1_calib.relative_factor / dl1_calib.absolute_factor
elif self.image_calibration_type == "variance":
dl1.image *= np.sqrt(
dl1_calib.relative_factor / dl1_calib.absolute_factor
)
Copy link
Member

Choose a reason for hiding this comment

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

You could check here if the extractor.__class__.__name__ is VarianceExtractor rather than passing the specific config parameter.

This comment has been minimized.

@@ -1308,7 +1308,7 @@ def __call__(
self, waveforms, tel_id, selected_gain_channel, broken_pixels
) -> DL1CameraContainer:
container = DL1CameraContainer(
image=np.nanvar(waveforms, dtype="float32", axis=2),
image=np.nanvar(waveforms, dtype="float32", keepdims=False, axis=2),
Copy link
Contributor

Choose a reason for hiding this comment

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

A bit weird that you need to set keepdims=False explicitly, but ok.

Copy link
Author

Choose a reason for hiding this comment

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

i will check again if i make it work without that

Copy link
Contributor

Choose a reason for hiding this comment

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

Should work without it:

x  = np.random.uniform(size=(2,1800, 1000))

In [1]: np.nanvar(x, axis=2).shape
Out[2]: (2, 1800)

In [2]: np.nanvar(x, axis=2, keepdims=True).shape
Out[2]: (2, 1800, 1)

In [3]: np.nanvar(x, axis=2, keepdims=False).shape
Out[3]: (2, 1800)

calibrator(example_event)
image = example_event.dl1.tel[tel_id].image
assert image is not None
assert image.shape == (
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a test with the LST (two gains)

Copy link
Author

Choose a reason for hiding this comment

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

ok

Copy link
Author

Choose a reason for hiding this comment

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

would it make sense to add a test for other extractor with the LST?

Copy link
Contributor

Choose a reason for hiding this comment

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

You could use a parametrized test here to test all telescope types. (@pytest.mark.parametrized). See for example the code in ctapipe/instrument/tests/test_telescope.py

@@ -292,13 +296,21 @@ def _calibrate_dl1(self, event, tel_id):
and dl1_calib.absolute_factor is not None
):
if selected_gain_channel is None:
dl1.image *= dl1_calib.relative_factor / dl1_calib.absolute_factor
if extractor.__class__.__name__ == "VarianceExtractor":
dl1.image *= np.sqrt(
Copy link
Contributor

Choose a reason for hiding this comment

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

should be square, not square root

else:
corr = (
dl1_calib.relative_factor[selected_gain_channel, pixel_index]
/ dl1_calib.absolute_factor[selected_gain_channel, pixel_index]
)
dl1.image *= corr
if extractor.__class__.__name__ == "VarianceExtractor":
dl1.image *= np.sqrt(corr)
Copy link
Contributor

Choose a reason for hiding this comment

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

same as l.300

This comment has been minimized.

mexanick
mexanick previously approved these changes Nov 6, 2024
@@ -0,0 +1 @@
Makes changes to the CameraCalibrator in ctapipe.calib.camera.calibrator that allows it to correctly variance images generated with the VarianceExtractor
Copy link
Member

Choose a reason for hiding this comment

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

Please describe the actual change, not just say "there are changes". Also there is a verb missing here (calibrate?)

Copy link
Author

Choose a reason for hiding this comment

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

i added some explanation

@@ -0,0 +1,3 @@
Makes changes to the CameraCalibrator in ctapipe.calib.camera.calibrator that allows it to correctly calibrate variance images generated with the VarianceExtractor
if the VarianceExtractor is used for the CameraCalibrator the element-wise square of the relative and absolute gain calibration factors are applied to the image
Copy link
Contributor

Choose a reason for hiding this comment

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

Please format according to ReST syntax. This will render as a single line.

Copy link
Author

Choose a reason for hiding this comment

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

I changed it

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

1 similar comment
Copy link

Passed

Analysis Details

2 Issues

  • Bug 0 Bugs
  • Vulnerability 0 Vulnerabilities
  • Code Smell 2 Code Smells

Coverage and Duplications

  • Coverage 97.30% Coverage (93.70% Estimated after merge)
  • Duplications 0.00% Duplicated Code (0.70% Estimated after merge)

Project ID: cta-observatory_ctapipe_AY52EYhuvuGcMFidNyUs

View in SonarQube

@maxnoe
Copy link
Member

maxnoe commented Nov 6, 2024

I think we can (maybe should) apply this sqrt operation before writing the coefficients. Therefore we do not need the different cases here in the code. Would that work?

I think this is the way to go, compute the appropriate calibration coefficients directly instead of adding special casing in the application.

Also: shouldn't it be square and not square root for the variance?

@ctoennis
Copy link
Author

ctoennis commented Nov 6, 2024

@maxnoe square is correct. If we apply the square operation when writing the calibration corrections then we will not need this PR

@maxnoe
Copy link
Member

maxnoe commented Nov 6, 2024

You still need the peak_time check, right?

@ctoennis
Copy link
Author

ctoennis commented Nov 6, 2024

Oh yes, that will be still needed

@TjarkMiener
Copy link
Member

I discussed with @mexanick offline yesterday. We might not want to run the tool for computing camera calibration coefficients twice and store two separated files (one for comics; one for pointing calibration), just to square one column here. What do you think?

@mexanick
Copy link
Contributor

mexanick commented Nov 6, 2024

at least for now, I would avoid recomputation of the calibrations just for variance. With the current code, we can always have this possibility to test, if needed.

else:
corr = (
dl1_calib.relative_factor[selected_gain_channel, pixel_index]
/ dl1_calib.absolute_factor[selected_gain_channel, pixel_index]
)
dl1.image *= corr
if extractor.__class__.__name__ == "VarianceExtractor":
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use this to check type, not a string comparison (which is slower and makes refactoring more difficult later)

Suggested change
if extractor.__class__.__name__ == "VarianceExtractor":
if isinstance(extractor, VarianceExtractor):

Copy link
Author

Choose a reason for hiding this comment

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

Makes sense, im changing it now

@@ -292,13 +296,21 @@ def _calibrate_dl1(self, event, tel_id):
and dl1_calib.absolute_factor is not None
):
if selected_gain_channel is None:
dl1.image *= dl1_calib.relative_factor / dl1_calib.absolute_factor
if extractor.__class__.__name__ == "VarianceExtractor":
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if extractor.__class__.__name__ == "VarianceExtractor":
if isinstance(extractor, VarianceExtractor):

calibrator(example_event)
image = example_event.dl1.tel[tel_id].image
assert image is not None
assert image.shape == (
Copy link
Contributor

Choose a reason for hiding this comment

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

You could use a parametrized test here to test all telescope types. (@pytest.mark.parametrized). See for example the code in ctapipe/instrument/tests/test_telescope.py

@ctoennis
Copy link
Author

ctoennis commented Nov 7, 2024

i will change the test to check all cameras

@ctoennis
Copy link
Author

ctoennis commented Nov 7, 2024

@kosack I used the camera_geometry fixture instead to check for all cameras. In the end I just need the geometry, so that should work well.

@ctoennis
Copy link
Author

ctoennis commented Nov 7, 2024

I think there is an issue with the recent change to the changelog check (#2637).

@ctoennis
Copy link
Author

ctoennis commented Nov 8, 2024

i switched it to a parametrized test for the different cameras now and caught the other test for name and switched it to isinstance

@ctoennis ctoennis requested a review from kosack November 8, 2024 14:51
@@ -292,13 +296,21 @@ def _calibrate_dl1(self, event, tel_id):
and dl1_calib.absolute_factor is not None
):
if selected_gain_channel is None:
Copy link
Member

@maxnoe maxnoe Nov 8, 2024

Choose a reason for hiding this comment

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

I think this section could use some refactoring to make it easier to follow.

I think the logic is the same if we simplify to:

if selected_gain_channel is None:
    calibration = dl1_calib.relative_factor / dl1_calib.absolute_factor
else:
    calibration = (
        dl1_calib.relative_factor[selected_gain_channel, pixel_index]
         / dl1_calib.absolute_factor[selected_gain_channel, pixel_index]
    )

if isinstance(extractor, VarianceExtractor):
    calibration = calibration**2

dl1.image *= calibration

Copy link
Author

Choose a reason for hiding this comment

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

Sounds good, I'll get to it when I arrive home

Copy link
Author

Choose a reason for hiding this comment

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

i put it in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants