@@ -81,6 +81,7 @@ def __init__(
8181 # Set some default values for the event-level parameters.
8282 self .event_distances , self .map_index = [], []
8383 self .event_pvalue = {}
84+ self ._result_buffer : npt .NDArray [np .floating ] = np .array ([])
8485
8586 # Obtain the King distribution parameters for all bins. If we're caching parameters
8687 # and a cache file exists, load from the cache instead of fitting. Otherwise,
@@ -215,6 +216,15 @@ def set_events(
215216 self .event_mask = all_dists < cutoff
216217 self .event_distances = all_dists [self .event_mask ]
217218
219+ self ._result_buffer = np .zeros (len (events ))
220+
221+ all_alpha , all_beta = self .get_alpha_beta (events , copy = False )
222+ for i , gamma in enumerate (self .spectral_indices ):
223+ alpha , beta = self .get_alpha_beta_gamma (gamma , alpha = all_alpha , beta = all_beta )
224+ self .event_pvalue [gamma ] = self .king_pdf .pdf (self .event_distances , alpha , beta )
225+ return
226+
227+ def get_alpha_beta (self , events , copy = True ):
218228 # Nearest-bin lookup. Field-first masking (events[key][mask]) avoids
219229 # copying the full structured array before extracting each field.
220230 def index (centers , values ):
@@ -226,13 +236,40 @@ def index(centers, values):
226236 for i , key in enumerate (self .keys )
227237 )
228238
229- all_alpha = self .alpha_values [(slice (None ), * event_indices )]
230- all_beta = self .beta_values [(slice (None ), * event_indices )]
231- all_pvalues = self .king_pdf .pdf (self .event_distances , all_alpha , all_beta )
232- for i , gamma in enumerate (self .spectral_indices ):
233- self .event_pvalue [gamma ] = all_pvalues [i ]
239+ alpha = self .alpha_values [(slice (None ), * event_indices )]
240+ beta = self .beta_values [(slice (None ), * event_indices )]
241+ return alpha , beta
234242
235- return
243+ def get_alpha_beta_gamma (self , gamma , events = None , alpha = None , beta = None ):
244+ if alpha is None :
245+ assert events is not None
246+ alpha , beta = self .get_alpha_beta (events , copy = False )
247+ assert len (alpha ) == len (beta )
248+
249+ # If we have this gamma, just return it. Make sure to use copy()
250+ # so the caller doesn't get a reference into our array.
251+ if gamma in self .spectral_indices :
252+ gamma_idx = np .searchsorted (self .spectral_indices , gamma ) - 1
253+ return alpha [gamma_idx ].copy (), beta [gamma_idx ].copy ()
254+
255+ # Otherwise, we want to interpolate.
256+ gamma_low = max (np .searchsorted (self .spectral_indices , gamma ) - 1 , 0 )
257+ gamma_high = min (gamma_low , len (self .spectral_indices - 1 ))
258+
259+ return (
260+ _interp1d (
261+ self .spectral_indices [gamma_low ],
262+ self .spectral_indices [gamma_high ],
263+ alpha [gamma_low ],
264+ alpha [gamma_high ],
265+ ),
266+ _interp1d (
267+ self .spectral_indices [gamma_low ],
268+ self .spectral_indices [gamma_high ],
269+ beta [gamma_low ],
270+ beta [gamma_high ],
271+ ),
272+ )
236273
237274 def evaluate_pdf (self , events : npt .NDArray [Any ], gamma : float = 2 ) -> npt .NDArray [np .floating ]:
238275 # If we haven't already calculated the per-event alpha and beta parameters, do so now.
@@ -248,12 +285,238 @@ def evaluate_pdf(self, events: npt.NDArray[Any], gamma: float = 2) -> npt.NDArra
248285 )
249286
250287 gamma_low , gamma_high = self .spectral_indices [idx ], self .spectral_indices [idx + 1 ]
251- result = np . zeros ( len ( self . events ))
252- result [self .event_mask ] = _interp1d (
288+ self . _result_buffer [:] = 0.0
289+ self . _result_buffer [self .event_mask ] = _interp1d (
253290 gamma ,
254291 gamma_low ,
255292 gamma_high ,
256293 self .event_pvalue [gamma_low ],
257294 self .event_pvalue [gamma_high ],
258295 )
259- return result
296+ return self ._result_buffer
297+
298+
299+ # class KingTemplateLikelihood:
300+ # # Configuration parameters
301+ # parametrization_bins: Dict[str, npt.NDArray[np.floating]]
302+ # spectral_indices: npt.NDArray[np.floating]
303+ # angular_cutoff: float
304+ # cache_parameters: bool = True
305+ # cache_name: str = "king_parameters_cache.npz"
306+
307+ # # Store an instance of the PDF class to use for evaluations. This will be
308+ # # either a KingPDF for standard point source searches
309+ # king_pdf: KingTemplatePDF
310+
311+ # # Source-level information
312+ # skymap: npt.NDArray[np.floating]
313+ # convolution_dtype: Any = np.float32
314+ # convolution_alphas: npt.NDArray[np.floating]
315+ # convolution_alphas: npt.NDArray[np.floating]
316+ # convolved_skymaps: npt.NDArray[np.floating]
317+
318+ # # Have some place to cache the per-event information so we don't need to
319+ # # recalculate it every time we evaluate the PDF.
320+ # events: Optional[Any] = None
321+ # event_convolution_indices = np.NDArray[np.integer]
322+
323+ # def __init__(
324+ # self,
325+ # skymap: npt.NDArray[np.floating],
326+ # signal_events: npt.NDArray[Any],
327+ # parametrization_bins: Dict[str, Union[int, List, Tuple, npt.NDArray]],
328+ # convolution_nside : int = 128,
329+ # convolution_alphas : Union[npt.NDArray, int] = 10,
330+ # convolution_betas : Union[npt.NDArray, int] = 10,
331+ # convolution_dtype : Any = np.float32,
332+ # dpsi_nbins: int = 101,
333+ # minimum_counts: int = 100,
334+ # spectral_indices: Union[List[float], npt.NDArray[np.floating]] = [
335+ # 1.0,
336+ # 2.0,
337+ # 3.0,
338+ # 4.0,
339+ # ],
340+ # angular_cutoff: float = np.pi,
341+ # cache_parameters: bool = True,
342+ # cache_name: str = "./king_parameters_cache.npz",
343+ # remove_weight_outliers=True,
344+ # weight_outlier_percentiles=(0, 95),
345+ # weight_field: str = "ow",
346+ # true_ra_name: str = "trueRa",
347+ # true_dec_name: str = "trueDec",
348+ # true_energy_name: str = "trueE",
349+ # ):
350+ # # Store some of the configuration parameters for this instance.
351+ # # Note that we don't need to store the signal events, dpsi_nbins,
352+ # # or minimum counts since they're only necessary for fitting during
353+ # # initialization andnot for later evaluation. We'll also be storing
354+ # # the parametrization bins later, since the user may have simply
355+ # # passed in a number of bins instead of actual bin edges.
356+ # self.spectral_indices = np.atleast_1d(spectral_indices)
357+
358+ # # Set some default values for the event-level parameters.
359+ # self.event_pvalue = {}
360+
361+ # # Obtain the King distribution parameters for all bins. If we're caching parameters
362+ # # and a cache file exists, load from the cache instead of fitting. Otherwise,
363+ # # run the fitter and potentially cache the results. Note that if we run the fitter,
364+ # # we explicitly set the angular cutoff to pi: this is to ensure that we allow the
365+ # # full histogram to be fit for each bin without artificially setting the PDF to 0
366+ # # for some bins.
367+ # fitted_parameters: Dict[str, npt.NDArray[np.floating]] = {}
368+ # if cache_parameters and (cache_name is not None) and exists(cache_name):
369+ # fitted_parameters_npz = np.load(cache_name, allow_pickle=True)
370+ # for key in fitted_parameters_npz.files:
371+ # fitted_parameters[key] = fitted_parameters_npz[key]
372+ # else:
373+ # fitter = KingPSFFitter(
374+ # signal_events=signal_events,
375+ # parametrization_bins=parametrization_bins,
376+ # dpsi_nbins=dpsi_nbins,
377+ # minimum_counts=minimum_counts,
378+ # spectral_indices=spectral_indices,
379+ # angular_cutoff=np.pi,
380+ # remove_weight_outliers=remove_weight_outliers,
381+ # weight_outlier_percentiles=weight_outlier_percentiles,
382+ # weight_field=weight_field,
383+ # true_ra_name=true_ra_name,
384+ # true_dec_name=true_dec_name,
385+ # true_energy_name=true_energy_name,
386+ # )
387+ # fitted_parameters = fitter.fit_all_bins(verbose=True)
388+ # if cache_parameters and (cache_name is not None):
389+ # np.savez(cache_name, **fitted_parameters) # type: ignore[arg-type]
390+
391+ # # Store the fitted parameters and bins for later interpolation during PDF evaluation.
392+ # self.parametrization_bins = fitted_parameters["parametrization_bins"] # type: ignore[assignment]
393+ # try:
394+ # self.parametrization_bins.items()
395+ # except AttributeError:
396+ # self.parametrization_bins = self.parametrization_bins.item()
397+
398+ # # Extract the bin centers and keys for each event. The stored bins are
399+ # # edges, but interpn requires coordinates matching the values shape.
400+ # self.keys, self.bin_centers = [], []
401+ # for key, edges in self.parametrization_bins.items():
402+ # self.keys.append(key)
403+ # self.bin_centers.append((edges[:-1] + edges[1:]) / 2)
404+
405+ # # And grab the fitted alpha/beta arrays
406+ # self.alpha_values = fitted_parameters["alpha"]
407+ # self.beta_values = fitted_parameters["beta"]
408+
409+ # # Instantiate the PDF object.
410+ # self.king_pdf = KingTemplatePDF(angular_cutoff=angular_cutoff)
411+ # self.convolution_nside = convolution_nside
412+ # self.convolution_alphas = np.sort(convolution_alphas)
413+ # self.convolution_betas = np.sort(convolution_betas)
414+ # self.convolution_dtype = convolution_dtype
415+
416+ # # We can now take the alpha/beta bins from the fitter and directly convert them to
417+ # # the correct indicies in convolution_alphas and convolution_betas.
418+ # # TODO: Should these be nearest-neighbors instead of flooring?
419+ # self.alpha_values_idx = np.searchsorted(self.convolution_alphas,
420+ # self.alpha_values) - 1
421+ # self.beta_values_idx = np.searchsorted(self.convolution_betas,
422+ # self.beta_values) - 1
423+
424+ # # We can also just do the convolutions now since we know the grid.
425+ # shape = (len(self.alpha_values), len(self.beta_values), hp.nside2npix(self.convolution_nsize))
426+
427+ # # Warn the user if this is more than 1 GB...
428+ # expected_size = np.prod(shape) * self.convolution_dtype().nbytes
429+ # if expected_size / 1024**3 > 1:
430+ # print(f"WARNING: Requested shape (gamma, alpha, beta, skymap) = {shape}"
431+ # f" with dtype {self.convolution_dtype}. This will give a total array"
432+ # f" size of {expected_size} GB.")
433+ # if expected_size / 1024**3 > 4:
434+ # raise MemoryError(f"Requested shape (gamma, alpha, beta, skymap) = {shape}"
435+ # f" with dtype {self.convlution_dtype} will have a total"
436+ # f" size of {expected_size}. This seems unreasonable, so"
437+ # " I'm kicking this back to you to reconsider.")
438+
439+ # self.convolved_skymaps = np.empty(shape, dtype=self.convolution_dtype)
440+
441+ # for index in np.nditer(shape[:-1]):
442+ # i, j = index
443+ # self.convolved_skymaps[i,j] = self.king_pdf.convolve_map(
444+ # self.alpha_values[i], self.beta_values[j])
445+ # return
446+
447+ # def _events_match(self, events: npt.NDArray[Any]) -> bool:
448+ # if self.events is None:
449+ # return False
450+ # if events is None:
451+ # return True
452+ # if len(self.events) != len(events):
453+ # return False
454+ # result = np.array_equal(self.events["ra"][::10], events["ra"][::10])
455+ # result &= np.array_equal(self.events["dec"][::10], events["dec"][::10])
456+ # return result
457+
458+ # def set_events(
459+ # self,
460+ # events: npt.NDArray[Any],
461+ # ) -> None:
462+ # """Calculate per-event pvalues for each spectral index by interpolating
463+ # the King-convolved templates at the nearest parametrization bin for each event.
464+ # """
465+ # if self._events_match(events):
466+ # return
467+
468+ # self.events = events
469+
470+ # # Make sure we have a matching number of source_ras and source_decs if we're given multiple sources.
471+ # if (source_ras is None) and (source_decs is None):
472+ # raise ValueError(
473+ # "No source_ras and source_decs were provided to the set_eventsfunction."
474+ # )
475+ # if (source_ras is None or source_decs is None) or (len(source_ras) != len(source_decs)):
476+ # raise ValueError(
477+ # "The number of source_ras and source_decs must match. Please ensure "
478+ # "that these arrays have the same length when passing into set_events."
479+ # )
480+
481+ # # Nearest-bin lookup. These map the events from their parametrization bins to
482+ # # the correct healpix bins. We'll then do the gamma lookup later.
483+ # def index(centers, values):
484+ # i = np.searchsorted(centers, values).clip(1, len(centers) - 1)
485+ # return np.where(values - centers[i - 1] < centers[i] - values, i - 1, i)
486+
487+ # event_indices = tuple(
488+ # index(self.bin_centers[i], events[key][self.event_mask])
489+ # for i, key in enumerate(self.keys)
490+ # )
491+
492+ # self.event_convolution_indices = np.empty((len(self.spectral_indices),
493+ # len(self.events)), dtype=int)
494+ # for i in range(len(self.spectral_indices)):
495+ # .......................
496+
497+ # return
498+
499+
500+ # def evaluate_pdf(self, events: npt.NDArray[Any], gamma: float = 2) -> npt.NDArray[np.floating]:
501+ # # If we haven't already calculated the per-event alpha and beta parameters, do so now.
502+ # if not self._events_match(events):
503+ # raise RuntimeError(
504+ # "The events provided to evaluate_pdf do not match the events that were used to calculate the per-event parameters."
505+ # " Please ensure that you call set_events with the same events that you later pass into evaluate_pdf."
506+ # )
507+
508+ # # Interpolate over gamma to get the final result for each event
509+ # idx = np.clip(
510+ # np.searchsorted(self.spectral_indices, gamma) - 1, 0, len(self.spectral_indices) - 2
511+ # )
512+
513+ # gamma_low, gamma_high = self.spectral_indices[idx], self.spectral_indices[idx + 1]
514+ # result = np.zeros(len(self.events))
515+ # result[self.event_mask] = _interp1d(
516+ # gamma,
517+ # gamma_low,
518+ # gamma_high,
519+ # self.event_pvalue[gamma_low],
520+ # self.event_pvalue[gamma_high],
521+ # )
522+ # return result
0 commit comments