|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +"""Components for computing event weights.""" |
| 4 | + |
| 5 | +from abc import abstractmethod |
| 6 | +from collections.abc import Callable |
| 7 | +from typing import override |
| 8 | + |
| 9 | +import numpy as np |
| 10 | +from astropy import units as u |
| 11 | +from astropy.table import QTable |
| 12 | +from pyirf.spectral import ( |
| 13 | + calculate_event_weights, |
| 14 | +) |
| 15 | + |
| 16 | +from ..core import Component, traits |
| 17 | +from ..core.feature_generator import _shallow_copy_table |
| 18 | +from .binning import DefaultFoVOffsetBins |
| 19 | +from .spectra import SPECTRA, Spectra |
| 20 | + |
| 21 | +__all__ = [ |
| 22 | + "EventWeighter", |
| 23 | + "SimpleEventWeighter", |
| 24 | + "RadialEventWeighter", |
| 25 | + "PolarEventWeighter", |
| 26 | +] |
| 27 | + |
| 28 | + |
| 29 | +class EventWeighter(Component): |
| 30 | + """Compute weights to go from source to target spectra.""" |
| 31 | + |
| 32 | + target_spectrum_name = traits.UseEnum( |
| 33 | + Spectra, |
| 34 | + default_value=Spectra.CRAB_HEGRA, |
| 35 | + help="Pre-defined source spectrum to reweight to.", |
| 36 | + ).tag(config=True) |
| 37 | + |
| 38 | + is_diffuse = traits.Bool( |
| 39 | + default_value=True, help="If True, assume the source is diffuse." |
| 40 | + ).tag(config=True) |
| 41 | + |
| 42 | + energy_column = traits.Unicode( |
| 43 | + help="name of energy column", default_value="true_energy" |
| 44 | + ).tag(config=True) |
| 45 | + weight_column = traits.Unicode( |
| 46 | + help="name of output weight column", default_value="weight" |
| 47 | + ).tag(config=True) |
| 48 | + |
| 49 | + def __init__( |
| 50 | + self, |
| 51 | + source_spectrum: Callable[[u.Quantity], u.Quantity], |
| 52 | + config=None, |
| 53 | + parent=None, |
| 54 | + **kwargs, |
| 55 | + ): |
| 56 | + """ |
| 57 | + Parameters |
| 58 | + ---------- |
| 59 | + source_spectrum: Callable |
| 60 | + initial spectrum of the events to be processed. |
| 61 | + target_spectrum: Callable | None |
| 62 | + target spectrum to weight to. If None, a pre-defined spectrum |
| 63 | + function from the `target_spectrum_name` attribute will be used` |
| 64 | + """ |
| 65 | + super().__init__(config=config, parent=parent, **kwargs) |
| 66 | + self.source_spectrum = source_spectrum |
| 67 | + self.target_spectrum = SPECTRA[self.target_spectrum_name] |
| 68 | + |
| 69 | + @abstractmethod |
| 70 | + def _compute_weights(self, events_table: QTable): |
| 71 | + raise NotImplementedError( |
| 72 | + f"{self.__class__.__name__} weighting is not implemented" |
| 73 | + ) |
| 74 | + |
| 75 | + def __call__(self, events_table: QTable) -> QTable: |
| 76 | + """Returns shallow copy of input table with a `weight` column added""" |
| 77 | + |
| 78 | + table = _shallow_copy_table(events_table) |
| 79 | + self._compute_weights(table) |
| 80 | + return table |
| 81 | + |
| 82 | + |
| 83 | +class SimpleEventWeighter(EventWeighter): |
| 84 | + """Weights all events spectrally with no spatial binning. |
| 85 | +
|
| 86 | + Calling this class adds a column to the output table with the event-wise |
| 87 | + spectral weights, with column name ``weight_column``. |
| 88 | + """ |
| 89 | + |
| 90 | + fov_offset_max = traits.AstroQuantity( |
| 91 | + help="upper bound of spatial integral applied to source function", |
| 92 | + default_value=u.Quantity(10, u.deg), |
| 93 | + physical_type=u.physical.angle, |
| 94 | + ).tag(config=True) |
| 95 | + |
| 96 | + @override |
| 97 | + def _compute_weights(self, events_table: QTable): |
| 98 | + energy = events_table[self.energy_column] |
| 99 | + source_spectrum = self.source_spectrum |
| 100 | + if self.is_diffuse: |
| 101 | + source_spectrum = source_spectrum.integrate_cone( |
| 102 | + 0 * u.deg, self.fov_offset_max |
| 103 | + ) |
| 104 | + weights = calculate_event_weights( |
| 105 | + energy, |
| 106 | + target_spectrum=self.target_spectrum, |
| 107 | + simulated_spectrum=source_spectrum, |
| 108 | + ) |
| 109 | + events_table[self.weight_column] = weights |
| 110 | + |
| 111 | + |
| 112 | +class RadialEventWeighter(EventWeighter, DefaultFoVOffsetBins): |
| 113 | + """ |
| 114 | + Weights in radial (FOV) offset bins in the `~ctapipe.coordinates.NominalFrame`. |
| 115 | +
|
| 116 | + Calling this class adds a column to the output table with the event-wise |
| 117 | + spectral-spatial weights, with column name `weight_column``. This implementation |
| 118 | + additionally adds the column ``output_table["fov_offset_bin"]``, and the |
| 119 | + list of offset bin edges in ``output_table.meta["OFFSBINS"]`` |
| 120 | + """ |
| 121 | + |
| 122 | + fov_offset_column = traits.Unicode( |
| 123 | + help="name of FOV radial offset column", default_value="true_fov_offset" |
| 124 | + ).tag(config=True) |
| 125 | + |
| 126 | + @override |
| 127 | + def _compute_weights(self, events_table: QTable): |
| 128 | + offset_bins = self.fov_offset_bins |
| 129 | + offset = events_table[self.fov_offset_column].to_value(offset_bins.unit) |
| 130 | + energy = events_table[self.energy_column] |
| 131 | + weights = np.zeros_like(energy.value) |
| 132 | + |
| 133 | + # note that the bin i from digitize starts at 1 and means: |
| 134 | + # offset_bins[i-1] <= offset < offset_bins[ii]) |
| 135 | + r_bin = np.digitize(offset, offset_bins.value) |
| 136 | + |
| 137 | + for ii in range(0, len(offset_bins)): |
| 138 | + self.log.debug( |
| 139 | + f"bin {ii} offset=[{offset_bins[ii - 1]}, {offset_bins[ii]})" |
| 140 | + ) |
| 141 | + mask = r_bin == ii |
| 142 | + weights[mask] = calculate_event_weights( |
| 143 | + true_energy=energy[mask], |
| 144 | + target_spectrum=self.target_spectrum, |
| 145 | + simulated_spectrum=self.source_spectrum.integrate_cone( |
| 146 | + offset_bins[ii - 1], offset_bins[ii] |
| 147 | + ), |
| 148 | + ) |
| 149 | + |
| 150 | + events_table[self.weight_column] = weights |
| 151 | + events_table["fov_offset_bin"] = r_bin |
| 152 | + |
| 153 | + events_table.columns["fov_offset_bin"].description = ( |
| 154 | + "Bin i defined as offset[i-1] <= fov_offset < offset[i]. " |
| 155 | + "Where offset is `OFFSBINS` array found this table's metadata." |
| 156 | + ) |
| 157 | + |
| 158 | + events_table.meta["OFFSBINS"] = list(offset_bins.to_value("deg")) |
| 159 | + |
| 160 | + |
| 161 | +class PolarEventWeighter(EventWeighter): |
| 162 | + """ |
| 163 | + Weights in field-of-view polar $(r, \\phi)$ bins in the `~ctapipe.coordinates.NominalFrame`. |
| 164 | +
|
| 165 | + Calling this class adds a column to the output table with the event-wise |
| 166 | + spectral-spatial weights, with column name `weight_column``. This |
| 167 | + implementation additionally adds the columns |
| 168 | + ``output_table["fov_offset_bin"]``, ``output_table["fov_phi_bin"]``, and the |
| 169 | + list of offset and phi bin edges in ``output_table.meta["OFFSBINS"]`` and |
| 170 | + ``output_table.meta["PHIBINS"]`` respectively. |
| 171 | + """ |
| 172 | + |
| 173 | + fov_offset_column = traits.Unicode( |
| 174 | + help="name of FOV radial offset column", default_value="true_fov_offset" |
| 175 | + ).tag(config=True) |
| 176 | + |
| 177 | + fov_phi_column = traits.Unicode( |
| 178 | + help="name of the FOV azimuthal coordinate", default_value="true_fov_phi" |
| 179 | + ).tag(config=True) |
| 180 | + |
| 181 | + @override |
| 182 | + def _compute_weights(self, events_table: QTable): |
| 183 | + raise NotImplementedError( |
| 184 | + f"{self.__class__.__name__} weighting is not implemented" |
| 185 | + ) |
0 commit comments