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

Effective Area 3D #281

Merged
merged 24 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
14b050e
Add 2 methods to calculate effective area in energy and 2 spacial dim…
Feb 12, 2024
3d80615
Add methods to calculate n_showers in energy and 2 spacial dimensions
Feb 12, 2024
bca9226
Merge branch 'cta-observatory:main' into effective_area_3d
luca-dib Feb 12, 2024
6b6ec8d
Add util for calculating FOV position angle
Feb 13, 2024
52f4e44
Merge branch 'effective_area_3d' of https://github.com/luca-dib/pyirf…
Feb 13, 2024
8056f6e
Fix effective_area_3d and poition angle util
Feb 13, 2024
7e0a1d3
Fix missing comma
Feb 13, 2024
4d5df74
Fix typos causing error when calling calculate_n_showers_3d_*
Feb 15, 2024
34ecab9
Remove redundant argument breaking calculate_n_showers_3d call
Feb 15, 2024
2a6d047
Fix formatting and issues from pull request Effective Area 3D (no. 28…
Feb 15, 2024
8e4da8b
Add functions to transform from sky coordinates to FOV coordinates
Mar 1, 2024
5e170e4
Update calculate_n_showers_3d functions and add tests
Mar 1, 2024
98da953
Fix position angle function, add rectangle solid angle function and t…
Mar 1, 2024
f27eef8
Update effective area 3D functions and add corresponding tests
Mar 1, 2024
777c0b1
Fix code review issues
Apr 15, 2024
44018bc
Update function names in __all__
Apr 18, 2024
c80ef58
Fix all changed occurences of gadf coordinate function calls
Apr 18, 2024
643958a
Merge branch 'cta-observatory:main' into effective_area_3d
luca-dib Apr 30, 2024
d74bd18
Change az/alt to lon/lat due to support of both AltAz and ICRS
Apr 30, 2024
554ef57
Merge branch 'effective_area_3d' of https://github.com/luca-dib/pyirf…
Apr 30, 2024
8ff9e70
Add newsfragment
Apr 30, 2024
be14460
Properly handle viewcone_min > 0, add docstring
May 2, 2024
af8a9d1
Add missing condition to all_outside check
May 14, 2024
af76243
Merge branch 'cta-observatory:main' into effective_area_3d
luca-dib May 17, 2024
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
4 changes: 4 additions & 0 deletions pyirf/irf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
effective_area,
effective_area_per_energy_and_fov,
effective_area_per_energy,
effective_area_3d_polar,
effective_area_3d_lonlat,
)
from .energy_dispersion import energy_dispersion
from .psf import psf_table
Expand All @@ -11,6 +13,8 @@
"effective_area",
"effective_area_per_energy",
"effective_area_per_energy_and_fov",
"effective_area_3d_polar",
"effective_area_3d_lonlat",
"energy_dispersion",
"psf_table",
"background_2d",
Expand Down
98 changes: 98 additions & 0 deletions pyirf/irf/effective_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"effective_area",
"effective_area_per_energy",
"effective_area_per_energy_and_fov",
"effective_area_3d_polar",
"effective_area_3d_lonlat",
]


Expand Down Expand Up @@ -85,3 +87,99 @@ def effective_area_per_energy_and_fov(
)

return effective_area(hist_selected, hist_simulated, area)


def effective_area_3d_polar(
selected_events,
simulation_info,
energy_bins,
fov_offset_bins,
fov_position_angle_bins,
):
"""
Calculate effective area in bins of true energy, field of view offset, and field of view position angle.

Parameters
----------
selected_events: astropy.table.QTable
DL2 events table, required columns for this function:
- `true_energy`
- `true_source_fov_offset`
- `true_source_fov_position_angle`
simulation_info: pyirf.simulations.SimulatedEventsInfo
The overall statistics of the simulated events
true_energy_bins: astropy.units.Quantity[energy]
The true energy bin edges in which to calculate effective area.
fov_offset_bins: astropy.units.Quantity[angle]
The field of view radial bin edges in which to calculate effective area.
fov_position_angle_bins: astropy.units.Quantity[radian]
The field of view azimuthal bin edges in which to calculate effective area.
kosack marked this conversation as resolved.
Show resolved Hide resolved
"""
area = np.pi * simulation_info.max_impact**2

hist_simulated = simulation_info.calculate_n_showers_3d_polar(
energy_bins, fov_offset_bins, fov_position_angle_bins
)

hist_selected, _ = np.histogramdd(
np.column_stack(
[
selected_events["true_energy"].to_value(u.TeV),
selected_events["true_source_fov_offset"].to_value(u.deg),
selected_events["true_source_fov_position_angle"].to_value(u.rad),
]
),
bins=(
energy_bins.to_value(u.TeV),
fov_offset_bins.to_value(u.deg),
fov_position_angle_bins.to_value(u.rad),
),
)

return effective_area(hist_selected, hist_simulated, area)


def effective_area_3d_lonlat(
selected_events, simulation_info, energy_bins, fov_longitude_bins, fov_latitude_bins
):
"""
Calculate effective area in bins of true energy, field of view longitude, and field of view latitude.

Parameters
----------
selected_events: astropy.table.QTable
DL2 events table, required columns for this function:
- `true_energy`
- `true_source_fov_lon`
- `true_source_fov_lat`
simulation_info: pyirf.simulations.SimulatedEventsInfo
The overall statistics of the simulated events
true_energy_bins: astropy.units.Quantity[energy]
maxnoe marked this conversation as resolved.
Show resolved Hide resolved
The true energy bin edges in which to calculate effective area.
fov_longitude_bins: astropy.units.Quantity[angle]
The field of view longitude bin edges in which to calculate effective area.
fov_latitude_bins: astropy.units.Quantity[angle]
The field of view latitude bin edges in which to calculate effective area.
"""
area = np.pi * simulation_info.max_impact**2

hist_simulated = simulation_info.calculate_n_showers_3d_lonlat(
energy_bins, fov_longitude_bins, fov_latitude_bins
)

hist_selected, _ = np.histogramdd(
np.column_stack(
[
selected_events["true_energy"].to_value(u.TeV),
selected_events["true_source_fov_lon"].to_value(u.deg),
selected_events["true_source_fov_lat"].to_value(u.deg),
]
),
bins=(
energy_bins.to_value(u.TeV),
fov_longitude_bins.to_value(u.deg),
fov_latitude_bins.to_value(u.deg),
),
)

return effective_area(hist_selected, hist_simulated, area)
110 changes: 110 additions & 0 deletions pyirf/simulations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import astropy.units as u
import numpy as np
from .utils import cone_solid_angle

__all__ = [
'SimulatedEventsInfo',
Expand Down Expand Up @@ -168,6 +169,115 @@ def calculate_n_showers_per_energy_and_fov(self, energy_bins, fov_bins):
fov_integral = self.calculate_n_showers_per_fov(fov_bins)
return e_integral[:, np.newaxis] * fov_integral / self.n_showers

@u.quantity_input(
energy_bins=u.TeV, fov_offset_bins=u.deg, fov_position_angle_bins=u.rad
)
def calculate_n_showers_3d_polar(
self, energy_bins, fov_offset_bins, fov_position_angle_bins
):
"""
Calculate number of showers that were simulated in the given
energy and 2D fov bins in polar coordinates.

This assumes the events were generated uniformly distributed per solid angle,
and from a powerlaw in energy like CORSIKA simulates events.

Parameters
----------
energy_bins: astropy.units.Quantity[energy]
The energy bin edges for which to calculate the number of simulated showers
fov_offset_bins: astropy.units.Quantity[angle]
The FOV radial bin edges for which to calculate the number of simulated showers
fov_position_angle_bins: astropy.units.Quantity[radian]
The FOV azimuthal bin edges for which to calculate the number of simulated showers

Returns
-------
n_showers: numpy.ndarray(ndim=3)
The expected number of events inside each of the
``energy_bins``, ``fov_offset_bins`` and ``fov_position_angle_bins``.
Dimension (n_energy_bins, n_fov_offset_bins, n_fov_position_angle_bins)
This is a floating point number.
The actual numbers will follow a poissionian distribution around this
expected value.
"""
e_fov_offset_integral = self.calculate_n_showers_per_energy_and_fov(
energy_bins, fov_offset_bins
)
position_angle_integral = np.full(
len(fov_position_angle_bins) - 1,
self.calculate_n_showers_per_fov(
np.linspace(self.viewcone_min, self.viewcone_max, 2)
)
/ (len(fov_position_angle_bins) - 1),
)

return (
e_fov_offset_integral[:, :, np.newaxis]
* position_angle_integral
/ self.n_showers
)

@u.quantity_input(
energy_bins=u.TeV, fov_longitude_bins=u.deg, fov_latitude_bins=u.rad
)
def calculate_n_showers_3d_lonlat(
self, energy_bins, fov_longitude_bins, fov_latitude_bins
):
"""
Calculate number of showers that were simulated in the given
energy and 2D fov bins in nominal coordinates.

This assumes the events were generated uniformly distributed per solid angle,
and from a powerlaw in energy like CORSIKA simulates events.

Parameters
----------
energy_bins: astropy.units.Quantity[energy]
The energy bin edges for which to calculate the number of simulated showers
fov_longitude_bins: astropy.units.Quantity[angle]
The FOV longitude bin edges for which to calculate the number of simulated showers
fov_latitude_bins: astropy.units.Quantity[angle]
The FOV latitude bin edges for which to calculate the number of simulated showers

Returns
-------
n_showers: numpy.ndarray(ndim=3)
The expected number of events inside each of the
``energy_bins``, ``fov_longitude_bins`` and ``fov_latitude_bins``.
Dimension (n_energy_bins, n_fov_longitude_bins, n_fov_latitude_bins)
This is a floating point number.
The actual numbers will follow a poissionian distribution around this
expected value.
"""
fov_bin_midpoints_lon, fov_bin_midpoints_lat = np.meshgrid(
(fov_longitude_bins[:-1] + fov_longitude_bins[1:]) / 2,
maxnoe marked this conversation as resolved.
Show resolved Hide resolved
(fov_latitude_bins[:-1] + fov_latitude_bins[1:]) / 2,
)
mask_outside_fov = np.logical_or(
np.sqrt(fov_bin_midpoints_lon**2 + fov_bin_midpoints_lat**2)
maxnoe marked this conversation as resolved.
Show resolved Hide resolved
> self.viewcone_max,
np.sqrt(fov_bin_midpoints_lon**2 + fov_bin_midpoints_lat**2)
maxnoe marked this conversation as resolved.
Show resolved Hide resolved
< self.viewcone_min,
)
A_bins = (
maxnoe marked this conversation as resolved.
Show resolved Hide resolved
np.outer(
np.diff(np.sin(fov_latitude_bins.to_value(u.rad))),
np.diff(fov_longitude_bins.to_value(u.rad)),
)
* u.sr
)
shower_density = self.n_showers / (
cone_solid_angle(self.viewcone_max) - cone_solid_angle(self.viewcone_min)
)

fov_integral = shower_density * A_bins
e_integral = self.calculate_n_showers_per_energy(energy_bins)

fov_integral[mask_outside_fov] = 0

return (e_integral[:, np.newaxis, np.newaxis] * fov_integral) / self.n_showers

def __repr__(self):
return (
f"{self.__class__.__name__}("
Expand Down
30 changes: 30 additions & 0 deletions pyirf/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import astropy.units as u
from astropy.coordinates import angular_separation
from astropy.coordinates import position_angle

from .exceptions import MissingColumns, WrongColumnUnit

Expand All @@ -9,6 +10,7 @@
"is_scalar",
"calculate_theta",
"calculate_source_fov_offset",
"calculate_source_fov_position_angle",
"check_histograms",
"cone_solid_angle",
]
Expand Down Expand Up @@ -88,6 +90,34 @@ def calculate_source_fov_offset(events, prefix="true"):
return theta.to(u.deg)


def calculate_source_fov_position_angle(events, prefix="true"):
"""Calculate position_angle of true positions relative to pointing positions.

Parameters
----------
events : astropy.QTable
Astropy Table object containing the reconstructed events information.

prefix: str
Column prefix for az / alt, can be used to calculate reco or true
source fov position_angle.

Returns
-------
phi: astropy.units.Quantity
Position angle of the true positions relative to the pointing positions
in the sky.
"""
phi = position_angle(
events["pointing_az"],
maxnoe marked this conversation as resolved.
Show resolved Hide resolved
events["pointing_alt"],
events[f"{prefix}_az"],
events[f"{prefix}_alt"],
)

return phi.to(u.deg)


def check_histograms(hist1, hist2, key="reco_energy"):
"""
Check if two histogram tables have the same binning
Expand Down