"""A collection of functions related to geometrical transformations."""
import logging
import astropy.units as u
import numpy as np
from astropy.units import UnitsError
__all__ = [
"convert_2d_to_radial_distr",
"rotate",
]
_logger = logging.getLogger(__name__)
[docs]
def convert_2d_to_radial_distr(hist_2d, xaxis, yaxis, bins=50, max_dist=1000):
"""
Convert a 2d histogram of positions, e.g. photon positions on the ground, to a 1D distribution.
Parameters
----------
hist_2d: numpy.ndarray
The histogram counts.
xaxis: numpy.array
The values of the x axis (histogram bin edges) on the ground.
yaxis: numpy.array
The values of the y axis (histogram bin edges) on the ground.
bins: float
Number of bins in distance.
max_dist: float
Maximum distance to consider in the 1D histogram, usually in meters.
Returns
-------
np.array
The values of the 1D histogram with size = int(max_dist/bin_size).
np.array
The bin edges of the 1D histogram with size = int(max_dist/bin_size) + 1.
"""
# Check if the histogram will make sense
bins_step = 2 * max_dist / bins # in the 2d array, the positive and negative direction count.
for axis in [xaxis, yaxis]:
if (bins_step < np.diff(axis)).any():
msg = (
f"The histogram with number of bins {bins} and maximum distance of {max_dist} "
f"resulted in a bin size smaller than the original array. Please adjust those "
f"parameters to increase the bin size and avoid nan in the histogram values."
)
_logger.warning(msg)
break
grid_2d_x, grid_2d_y = np.meshgrid(xaxis[:-1], yaxis[:-1]) # [:-1], since xaxis and yaxis are
# the hist bin_edges (n + 1).
# radial_distance_map maps the distance to the center from each element in a square matrix.
radial_distance_map = np.sqrt(grid_2d_x**2 + grid_2d_y**2)
# The sorting and unravel_index give us the two indices for the position of the sorted element
# in the original 2d matrix
sorted_indices = np.unravel_index(
np.argsort(radial_distance_map, axis=None), np.shape(radial_distance_map)
)
x_indices_sorted, y_indices_sorted = sorted_indices[0], sorted_indices[1]
# We construct a 1D array with the histogram counts sorted according to the distance to the
# center.
hist_sorted = np.array(
[hist_2d[i_x, i_y] for i_x, i_y in zip(x_indices_sorted, y_indices_sorted)]
)
distance_sorted = np.sort(radial_distance_map, axis=None)
# For larger distances, we have more elements in a slice 'dr' in radius, hence, we need to
# account for it using weights below.
weights, radial_bin_edges = np.histogram(distance_sorted, bins=bins, range=(0, max_dist))
histogram_1d = np.empty_like(weights, dtype=float)
for i_radial, _ in enumerate(radial_bin_edges[:-1]):
# Here we sum all the events within a radial interval 'dr' and then divide by the number of
# bins that fit this interval.
indices_to_sum = (distance_sorted >= radial_bin_edges[i_radial]) * (
distance_sorted < radial_bin_edges[i_radial + 1]
)
if weights[i_radial] != 0:
histogram_1d[i_radial] = np.sum(hist_sorted[indices_to_sum]) / weights[i_radial]
else:
histogram_1d[i_radial] = 0
return histogram_1d, radial_bin_edges
[docs]
@u.quantity_input(rotation_angle_phi=u.rad, rotation_angle_theta=u.rad)
def rotate(x, y, rotation_around_z_axis, rotation_around_y_axis=0):
"""
Rotate the x and y coordinates of the telescopes.
The two rotations are:
- rotation_angle_around_z_axis gives the rotation on the observation plane (x, y)
- rotation_angle_around_y_axis allows to rotate the observation plane in space.
The function returns the rotated x and y values in the same unit given.
The direction of rotation of the elements in the plane is counterclockwise, i.e.,
the rotation of the coordinate system is clockwise.
Parameters
----------
x: numpy.array or list
x positions of the entries (e.g. telescopes), usually in meters.
y: numpy.array or list
y positions of the entries (e.g. telescopes), usually in meters.
rotation_angle_around_z_axis: astropy.units.rad
Angle to rotate the array in the observation plane (around z axis) in radians.
rotation_angle_around_y_axis: astropy.units.rad
Angle to rotate the observation plane around the y axis in radians.
Returns
-------
2-tuple of list
x and y positions of the rotated entry (e.g. telescopes) positions.
Raises
------
TypeError:
If type of x and y parameters are not valid.
RuntimeError:
If the length of x and y are different.
UnitsError:
If the unit of x and y are different.
"""
allowed_types = (list, np.ndarray, u.Quantity, float, int)
if not all(isinstance(variable, (allowed_types)) for variable in [x, y]):
raise TypeError("x and y types are not valid! Cannot perform transformation.")
if not isinstance(x, list | np.ndarray):
x = [x]
if not isinstance(y, list | np.ndarray):
y = [y]
if (
np.sum(
np.array([isinstance(x, type_now) for type_now in allowed_types[:-2]])
* np.array([isinstance(y, type_now) for type_now in allowed_types[:-2]])
)
== 0
):
raise TypeError("x and y are not from the same type! Cannot perform transformation.")
if len(x) != len(y):
raise RuntimeError(
"Cannot perform coordinate transformation when x and y have different lengths."
)
if all(isinstance(variable, (u.Quantity)) for variable in [x, y]):
if not isinstance(x[0].unit, type(y[0].unit)):
raise UnitsError(
"Cannot perform coordinate transformation when x and y have different units."
)
x_trans = np.cos(rotation_around_y_axis) * (
x * np.cos(rotation_around_z_axis) - y * np.sin(rotation_around_z_axis)
)
y_trans = x * np.sin(rotation_around_z_axis) + y * np.cos(rotation_around_z_axis)
return x_trans, y_trans